Dispatch runs the hooks registered for event and aggregates their verdicts into a single [Result]. Sets input.HookEventName so handlers don't have to remember. Defaults [Input.Cwd] to the executor's working directory when the caller didn't supply one. EventPreToolUsePreYolo is an internal sentinel
(ctx context.Context, event EventType, input *Input)
| 168 | // handlers) and aggregation reuses the EventPreToolUse branches. |
| 169 | // Tracing keeps the lane visible via the span name. |
| 170 | func (e *Executor) Dispatch(ctx context.Context, event EventType, input *Input) (*Result, error) { |
| 171 | hooks := e.hooksFor(event, input.ToolName) |
| 172 | if len(hooks) == 0 { |
| 173 | return &Result{Allowed: true}, nil |
| 174 | } |
| 175 | |
| 176 | // Hooks on the preempt-yolo lane see the public pre_tool_use event |
| 177 | // name so a single handler implementation works on either lane. |
| 178 | publicEvent := event |
| 179 | if event == EventPreToolUsePreYolo { |
| 180 | publicEvent = EventPreToolUse |
| 181 | } |
| 182 | |
| 183 | // Single span per Dispatch call covers every hook the event matched. |
| 184 | // Custom name `hook.{event}` because there is no GenAI semconv for |
| 185 | // arbitrary user-defined lifecycle hooks; we surface the event type, |
| 186 | // matched hook count, and session/agent identifiers so dashboards can |
| 187 | // split by event class without parsing span events. |
| 188 | ctx, span := otel.Tracer("github.com/docker/docker-agent/pkg/hooks").Start( |
| 189 | ctx, |
| 190 | "hook."+string(event), |
| 191 | trace.WithSpanKind(trace.SpanKindInternal), |
| 192 | trace.WithAttributes( |
| 193 | attribute.String("cagent.hook.event", string(event)), |
| 194 | attribute.Int("cagent.hook.count", len(hooks)), |
| 195 | attribute.String("cagent.agent.name", input.AgentName), |
| 196 | attribute.String("gen_ai.conversation.id", input.SessionID), |
| 197 | ), |
| 198 | ) |
| 199 | if input.ToolName != "" { |
| 200 | span.SetAttributes(attribute.String("gen_ai.tool.name", input.ToolName)) |
| 201 | } |
| 202 | defer span.End() |
| 203 | |
| 204 | input.HookEventName = publicEvent |
| 205 | if input.Cwd == "" { |
| 206 | input.Cwd = e.workingDir |
| 207 | } |
| 208 | |
| 209 | slog.DebugContext(ctx, "Executing hooks", "event", event, "session_id", input.SessionID, "count", len(hooks)) |
| 210 | |
| 211 | inputJSON, err := input.ToJSON() |
| 212 | if err != nil { |
| 213 | span.RecordError(err) |
| 214 | span.SetStatus(codes.Error, err.Error()) |
| 215 | return nil, fmt.Errorf("failed to serialize hook input: %w", err) |
| 216 | } |
| 217 | |
| 218 | results := make([]hookResult, len(hooks)) |
| 219 | var wg sync.WaitGroup |
| 220 | for i, hook := range hooks { |
| 221 | wg.Go(func() { results[i] = e.runHook(ctx, hook, inputJSON) }) |
| 222 | } |
| 223 | wg.Wait() |
| 224 | |
| 225 | final := aggregate(results, event) |
| 226 | annotateHookSpan(span, event, final) |
| 227 | return final, nil |