From d315f4d3dad7d6be1f51a0c0ce54b72958b1de09 Mon Sep 17 00:00:00 2001 From: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:55:16 -0500 Subject: [PATCH] fix(core): ensure silent local subagent delegation while allowing remote confirmation (#16395) Co-authored-by: N. Taylor Mullen --- docs/core/policy-engine.md | 5 +- .../src/agents/delegate-to-agent-tool.test.ts | 53 +++++++++++-------- .../core/src/agents/delegate-to-agent-tool.ts | 3 +- packages/core/src/policy/policies/agent.toml | 2 +- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/docs/core/policy-engine.md b/docs/core/policy-engine.md index 6f5dc600c1..6811b8b6fb 100644 --- a/docs/core/policy-engine.md +++ b/docs/core/policy-engine.md @@ -258,8 +258,9 @@ The Gemini CLI ships with a set of default policies to provide a safe out-of-the-box experience. - **Read-only tools** (like `read_file`, `glob`) are generally **allowed**. -- **Agent delegation** (like `delegate_to_agent`) is **allowed** (sub-agent - actions are checked individually). +- **Agent delegation** (like `delegate_to_agent`) defaults to **`ask_user`** to + ensure remote agents can prompt for confirmation, but local sub-agent actions + are executed silently and checked individually. - **Write tools** (like `write_file`, `run_shell_command`) default to **`ask_user`**. - In **`yolo`** mode, a high-priority rule allows all tools. diff --git a/packages/core/src/agents/delegate-to-agent-tool.test.ts b/packages/core/src/agents/delegate-to-agent-tool.test.ts index 9b2bd16aaf..65dcd2b2e0 100644 --- a/packages/core/src/agents/delegate-to-agent-tool.test.ts +++ b/packages/core/src/agents/delegate-to-agent-tool.test.ts @@ -187,24 +187,28 @@ describe('DelegateToAgentTool', () => { ); }); - it('should use correct tool name "delegate_to_agent" when requesting confirmation', async () => { + it('should execute local agents silently without requesting confirmation', async () => { const invocation = tool.build({ agent_name: 'test_agent', arg1: 'valid', }); // Trigger confirmation check - const p = invocation.shouldConfirmExecute(new AbortController().signal); - void p; - - expect(messageBus.publish).toHaveBeenCalledWith( - expect.objectContaining({ - type: MessageBusType.TOOL_CONFIRMATION_REQUEST, - toolCall: expect.objectContaining({ - name: DELEGATE_TO_AGENT_TOOL_NAME, - }), - }), + const result = await invocation.shouldConfirmExecute( + new AbortController().signal, ); + + expect(result).toBe(false); + + // Verify it did NOT call messageBus.publish with 'delegate_to_agent' + const delegateToAgentPublish = vi + .mocked(messageBus.publish) + .mock.calls.find( + (call) => + call[0].type === MessageBusType.TOOL_CONFIRMATION_REQUEST && + call[0].toolCall.name === DELEGATE_TO_AGENT_TOOL_NAME, + ); + expect(delegateToAgentPublish).toBeUndefined(); }); it('should delegate to remote agent correctly', async () => { @@ -227,24 +231,27 @@ describe('DelegateToAgentTool', () => { }); describe('Confirmation', () => { - it('should use default behavior for local agents (super call)', async () => { + it('should return false for local agents (silent execution)', async () => { const invocation = tool.build({ agent_name: 'test_agent', arg1: 'valid', }); - // We expect it to call messageBus.publish with 'delegate_to_agent' - // because super.shouldConfirmExecute checks the policy for the tool itself. - await invocation.shouldConfirmExecute(new AbortController().signal); - - expect(messageBus.publish).toHaveBeenCalledWith( - expect.objectContaining({ - type: MessageBusType.TOOL_CONFIRMATION_REQUEST, - toolCall: expect.objectContaining({ - name: DELEGATE_TO_AGENT_TOOL_NAME, - }), - }), + // Local agents should now return false directly, bypassing policy check + const result = await invocation.shouldConfirmExecute( + new AbortController().signal, ); + + expect(result).toBe(false); + + const delegateToAgentPublish = vi + .mocked(messageBus.publish) + .mock.calls.find( + (call) => + call[0].type === MessageBusType.TOOL_CONFIRMATION_REQUEST && + call[0].toolCall.name === DELEGATE_TO_AGENT_TOOL_NAME, + ); + expect(delegateToAgentPublish).toBeUndefined(); }); it('should forward to remote agent confirmation logic', async () => { diff --git a/packages/core/src/agents/delegate-to-agent-tool.ts b/packages/core/src/agents/delegate-to-agent-tool.ts index 7f3c4466b5..84d398aa4d 100644 --- a/packages/core/src/agents/delegate-to-agent-tool.ts +++ b/packages/core/src/agents/delegate-to-agent-tool.ts @@ -172,7 +172,8 @@ class DelegateInvocation extends BaseToolInvocation< ): Promise { const definition = this.registry.getDefinition(this.params.agent_name); if (!definition || definition.kind !== 'remote') { - return super.shouldConfirmExecute(abortSignal); + // Local agents should execute without confirmation. Inner tool calls will bubble up their own confirmations to the user. + return false; } const { agent_name: _agent_name, ...agentArgs } = this.params; diff --git a/packages/core/src/policy/policies/agent.toml b/packages/core/src/policy/policies/agent.toml index 58876246bf..218f2dc986 100644 --- a/packages/core/src/policy/policies/agent.toml +++ b/packages/core/src/policy/policies/agent.toml @@ -27,5 +27,5 @@ [[rule]] toolName = "delegate_to_agent" -decision = "allow" +decision = "ask_user" priority = 50