From 3ea73189824d28c4d643b4acea21941bea041ad4 Mon Sep 17 00:00:00 2001 From: kiriha1203 Date: Sun, 14 Jun 2026 11:32:54 +0900 Subject: [PATCH] feat(hooks): populate agent_name on tool-use hook events Tool events (pre/post_tool_use) build their hooks.Input via toolexec.NewHooksInput, which has no agent reference, so they shipped an empty agent_name while every other event sets it explicitly. Auto-fill AgentName in the shared dispatchHook path (which already receives the agent), mirroring how Executor.Dispatch auto-fills Cwd. Caller-set values still win, and the field is omitempty, so the change is backward compatible. This lets pre_tool_use hooks and external policy engines see which agent issued a tool call (per-agent governance / auditing). Co-authored-by: Claude Opus 4.8 (1M context) Signed-off-by: kiriha1203 --- pkg/runtime/hooks.go | 7 +++++ pkg/runtime/pre_tool_use_approval_test.go | 38 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/pkg/runtime/hooks.go b/pkg/runtime/hooks.go index 3381fe543..4b9b02455 100644 --- a/pkg/runtime/hooks.go +++ b/pkg/runtime/hooks.go @@ -104,6 +104,13 @@ func (r *LocalRuntime) dispatchHook( return nil } + // Auto-fill AgentName for events whose Input builder omits it (tool + // events via toolexec.NewHooksInput), mirroring Cwd auto-fill in + // Executor.Dispatch. Caller-set values win. + if input != nil && input.AgentName == "" { + input.AgentName = a.Name() + } + started := time.Now() if events != nil { events.Emit(HookStarted(event, input.SessionID, a.Name())) diff --git a/pkg/runtime/pre_tool_use_approval_test.go b/pkg/runtime/pre_tool_use_approval_test.go index 2beaff6f6..30f8766d5 100644 --- a/pkg/runtime/pre_tool_use_approval_test.go +++ b/pkg/runtime/pre_tool_use_approval_test.go @@ -260,3 +260,41 @@ func TestPreToolUseHook_PermissionsAllowShortCircuitsHook(t *testing.T) { assert.True(t, *executed, "deterministic Allow must short-circuit the hook (cost / latency win)") } + +// TestPreToolUseHook_ReceivesAgentName verifies dispatchHook auto-fills +// Input.AgentName for tool events, whose toolexec.NewHooksInput builder +// can't set it — matching every other event type. +func TestPreToolUseHook_ReceivesAgentName(t *testing.T) { + t.Parallel() + + executed := new(bool) + agentTools := recordingTool("the_tool", executed) + hookName := "test_agentname_" + t.Name() + + prov := &mockProvider{id: "test/mock-model", stream: &mockStream{}} + root := agent.New("root", "instr", + agent.WithModel(prov), + agent.WithToolSets(newStubToolSet(nil, agentTools, nil)), + agent.WithHooks(preToolUseHooksConfig(hookName)), + ) + rt, err := NewLocalRuntime(team.New(team.WithAgents(root)), + WithSessionCompaction(false), WithModelStore(mockModelStore{})) + require.NoError(t, err) + + var gotAgentName string + require.NoError(t, rt.hooksRegistry.RegisterBuiltin(hookName, func(_ context.Context, in *hooks.Input, _ []string) (*hooks.Output, error) { + if in != nil && in.HookEventName == hooks.EventPreToolUse { + gotAgentName = in.AgentName + } + return &hooks.Output{HookSpecificOutput: &hooks.HookSpecificOutput{ + HookEventName: hooks.EventPreToolUse, + PermissionDecision: hooks.DecisionAllow, + }}, nil + })) + + sess := session.New(session.WithUserMessage("test")) + _ = runJudgedToolCall(t, rt, sess, agentTools) + + assert.Equal(t, "root", gotAgentName, + "pre_tool_use hook Input must carry the dispatching agent's name") +}