Files
viettranx b7592bcacf feat(hooks/script): integrate script handler — FireResult + source-tier gate + migration
Wave 1 Phase 03. Wires the Phase 02 Goja handler into the dispatcher, widens
the DB schema for script + builtin sources, and refactors Dispatcher.Fire to
return a FireResult so builtin-source hooks can mutate event input.

Dispatcher:
- FireResult{Decision, UpdatedToolInput, UpdatedRawInput} replaces the plain
  Decision return. stdDispatcher.runSync keeps a local evMut copy so a
  builtin-source hook's updatedInput flows to downstream hooks in the chain
  and to the caller; non-builtin (source=ui) script mutations are dropped +
  WARN-logged (defense-in-depth; source tier enforced at the dispatcher, not
  only at the handler).
- applyBuiltinMutation + placeholder builtinAllowlistFor (rawInput, toolInput)
  — Phase 04 overrides the allowlist from builtins.yaml.
- noopDispatcher returns FireResult{DecisionAllow}.
- Script mutation tests: TestDispatcher_ScriptMutation_BuiltinSourceApplies,
  TestDispatcher_ScriptMutation_UISourceDenied.

Pipeline + callers (14 Fire sites refactored):
- FireHook wrapper returns FireResult; context_stage.go:52 applies
  UpdatedRawInput → state.Input.Message; tool_stage.go:52 applies
  UpdatedToolInput → tc.Arguments before ExecuteToolCall.
- delegate_bridge + delegate_tool keep the Decision branch; Updated* ignored.
- dispatcher_test / delegate_bridge_test / delegate_tool_hooks_test /
  integration test helpers updated for the new shape.

Validation:
- edition_gate allows HandlerScript on every edition (sandboxed, no shell
  escape surface).
- config.validateHandler rejects empty source, source > 32 KiB, goja compile
  error; validateTimeout rejects on_timeout=ask|defer (reserved).
- Six new config tests cover the script path.

Migrations:
- PG migration 000053 relaxes handler_type + source CHECKs and drops
  uq_hooks_{global,tenant,agent} — scripts routinely want many small hooks
  per event. RequiredSchemaVersion → 53.
- SQLite SchemaVersion → 21; patch 20 is a SELECT 1 placeholder because
  SQLite can't ALTER a CHECK — rebuildAgentHooksV21 runs outside the
  migration tx (parallel to backfillV16) to rename/recreate the table with
  widened CHECKs. schema.sql fresh-DB path matches.
- H9 fix: PGHookStore.Create + SqliteHookStore.Create honor caller-provided
  cfg.ID (uuid.Nil falls back to UUIDv7), unblocking Phase 04 idempotent
  UUIDv5 seed. TestCreateHonorsFixedID on both stores.

Config:
- config.HooksConfig{ScriptConcurrency, ScriptPerTenantConcurrency,
  ScriptCacheSize, BuiltinDisable}; buildHookHandlers wires the script
  handler with the caps from appCfg.Hooks.

Gates green: go build ./... + sqliteonly, go test -race -count=1 across
hooks/pipeline/sqlitestore.
2026-04-16 14:17:47 +07:00

56 lines
1.5 KiB
Go

package hooks
import (
"context"
"log/slog"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/eventbus"
)
// SubscribeDelegateEvents wires delegate.completed and delegate.failed eventbus
// events into SubagentStop hook invocations. Call once during startup after
// both the event bus and dispatcher are initialised.
// The SubagentStop hook is non-blocking — fire-and-forget via dispatcher.
func SubscribeDelegateEvents(bus eventbus.DomainEventBus, d Dispatcher) {
if bus == nil || d == nil {
return
}
handler := func(ctx context.Context, event eventbus.DomainEvent) error {
var delegationID string
switch p := event.Payload.(type) {
case eventbus.DelegateCompletedPayload:
delegationID = p.DelegationID
case eventbus.DelegateFailedPayload:
delegationID = p.DelegationID
default:
return nil
}
tenantID, _ := uuid.Parse(event.TenantID)
agentID, _ := uuid.Parse(event.AgentID)
ev := Event{
EventID: delegationID,
SessionID: event.SourceID,
TenantID: tenantID,
AgentID: agentID,
HookEvent: EventSubagentStop,
}
if _, err := d.Fire(ctx, ev); err != nil {
slog.Warn("hooks.delegate_bridge.fire_error",
"delegation_id", delegationID,
"err", err,
)
}
// SubagentStop is non-blocking; Updated* from FireResult is ignored
// (delegate bridge has no mutation path in Wave 1).
return nil
}
bus.Subscribe(eventbus.EventDelegateCompleted, handler)
bus.Subscribe(eventbus.EventDelegateFailed, handler)
}