fix(core): ensure silent local subagent delegation while allowing remote confirmation (#16395)

Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
Adam Weidman
2026-01-12 03:55:16 -05:00
committed by GitHub
parent 9703fe73cf
commit d315f4d3da
4 changed files with 36 additions and 27 deletions

View File

@@ -258,8 +258,9 @@ The Gemini CLI ships with a set of default policies to provide a safe
out-of-the-box experience. out-of-the-box experience.
- **Read-only tools** (like `read_file`, `glob`) are generally **allowed**. - **Read-only tools** (like `read_file`, `glob`) are generally **allowed**.
- **Agent delegation** (like `delegate_to_agent`) is **allowed** (sub-agent - **Agent delegation** (like `delegate_to_agent`) defaults to **`ask_user`** to
actions are checked individually). 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 - **Write tools** (like `write_file`, `run_shell_command`) default to
**`ask_user`**. **`ask_user`**.
- In **`yolo`** mode, a high-priority rule allows all tools. - In **`yolo`** mode, a high-priority rule allows all tools.

View File

@@ -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({ const invocation = tool.build({
agent_name: 'test_agent', agent_name: 'test_agent',
arg1: 'valid', arg1: 'valid',
}); });
// Trigger confirmation check // Trigger confirmation check
const p = invocation.shouldConfirmExecute(new AbortController().signal); const result = await invocation.shouldConfirmExecute(
void p; new AbortController().signal,
expect(messageBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall: expect.objectContaining({
name: DELEGATE_TO_AGENT_TOOL_NAME,
}),
}),
); );
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 () => { it('should delegate to remote agent correctly', async () => {
@@ -227,24 +231,27 @@ describe('DelegateToAgentTool', () => {
}); });
describe('Confirmation', () => { 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({ const invocation = tool.build({
agent_name: 'test_agent', agent_name: 'test_agent',
arg1: 'valid', arg1: 'valid',
}); });
// We expect it to call messageBus.publish with 'delegate_to_agent' // Local agents should now return false directly, bypassing policy check
// because super.shouldConfirmExecute checks the policy for the tool itself. const result = await invocation.shouldConfirmExecute(
await invocation.shouldConfirmExecute(new AbortController().signal); new AbortController().signal,
expect(messageBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall: expect.objectContaining({
name: DELEGATE_TO_AGENT_TOOL_NAME,
}),
}),
); );
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 () => { it('should forward to remote agent confirmation logic', async () => {

View File

@@ -172,7 +172,8 @@ class DelegateInvocation extends BaseToolInvocation<
): Promise<ToolCallConfirmationDetails | false> { ): Promise<ToolCallConfirmationDetails | false> {
const definition = this.registry.getDefinition(this.params.agent_name); const definition = this.registry.getDefinition(this.params.agent_name);
if (!definition || definition.kind !== 'remote') { 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; const { agent_name: _agent_name, ...agentArgs } = this.params;

View File

@@ -27,5 +27,5 @@
[[rule]] [[rule]]
toolName = "delegate_to_agent" toolName = "delegate_to_agent"
decision = "allow" decision = "ask_user"
priority = 50 priority = 50