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.
- **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.

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({
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 () => {

View File

@@ -172,7 +172,8 @@ class DelegateInvocation extends BaseToolInvocation<
): Promise<ToolCallConfirmationDetails | false> {
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;

View File

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