diff --git a/.changeset/tool-approvals-subagents.md b/.changeset/tool-approvals-subagents.md new file mode 100644 index 0000000000..ec4559008a --- /dev/null +++ b/.changeset/tool-approvals-subagents.md @@ -0,0 +1,5 @@ +--- +'@roomote/cloud-agents': patch +--- + +Apply tool approval choices to integration calls that a session's subagents make, so those calls ask for approval like any other. diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-native-tool-bridge.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-native-tool-bridge.test.ts index 18abcb2418..9ad91aa712 100644 --- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-native-tool-bridge.test.ts +++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-native-tool-bridge.test.ts @@ -477,7 +477,7 @@ describe('Fast native OpenCode tool bridge', () => { expect(config.agent.build.tools['foo_bar__roomote_2_*']).toBe(true); }); - it('writes tool approval permission entries for the build agent and helper subagents', async () => { + it('writes tool approval permission entries for every agent, subagents included', async () => { const runtime = await getFastAgentNativeToolRuntime( 'code-mode-tool-approval-permission', [ @@ -509,6 +509,9 @@ describe('Fast native OpenCode tool bridge', () => { expect(config.agent.judge.permission).toEqual( config.agent.build.permission, ); + // Any other subagent the model starts (OpenCode's built-in ones too) + // inherits the top-level rules instead of the default allow. + expect(config.permission).toEqual(config.agent.build.permission); expect(runtime.env.OPENCODE_EXPERIMENTAL_CODE_MODE).toBe('1'); }); @@ -530,6 +533,7 @@ describe('Fast native OpenCode tool bridge', () => { await readFile(join(runtime.directory, 'opencode.json'), 'utf8'), ); expect(config.agent.build.permission).toBeUndefined(); + expect(config.permission).toBeUndefined(); expect(config.agent.advisor).toBeUndefined(); expect(config.agent.judge).toBeUndefined(); expect(runtime.env.OPENCODE_EXPERIMENTAL_CODE_MODE).toBe('1'); diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts index b68471bcec..d813f42266 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts @@ -1647,8 +1647,8 @@ export async function getFastAgentNativeToolRuntime( automationLaunchCriteriaEnabled?: boolean; brainEnabled?: boolean; /** - * Per-tool approval rules in OpenCode config-permission shape, applied to the parent build agent - * and the helper subagents in the generated per-conversation config. + * Per-tool approval rules in OpenCode config-permission shape, applied to every agent in the + * generated per-conversation config, subagents included. * Rules live in config rather than the session ruleset so a policy * change never strands stale state in a persisted session: this file is * rewritten every turn, and a policy change disposes the directory's @@ -1701,10 +1701,15 @@ export async function getFastAgentNativeToolRuntime( ); runtime.env.OPENCODE_EXPERIMENTAL_CODE_MODE = '1'; runtime.codeModeIntegrationsActive = true; - // Approval rules apply to the parent build agent and to the helper - // subagents. OpenCode merges this per-directory config over the shared - // server config, so a permission-only entry extends the existing advisor - // and judge definitions instead of replacing them. + // Approval rules apply to every agent: the top-level permission covers any + // subagent the model starts (including OpenCode's built-in ones), which + // would otherwise call integration tools under the default allow. Session + // calls are not gated at the proxy, so these native asks are the only + // gate. The build, advisor, and judge entries repeat the rules because an + // agent's own permission takes precedence over the top-level one. OpenCode + // merges this per-directory config over the shared server config, so a + // permission-only entry extends the existing advisor and judge definitions + // instead of replacing them. const toolApprovalAgentEntries = options.toolApprovalPermission ? { permission: options.toolApprovalPermission, @@ -1713,6 +1718,7 @@ export async function getFastAgentNativeToolRuntime( writeFileSync( join(runtime.directory, 'opencode.json'), JSON.stringify({ + ...toolApprovalAgentEntries, // Keep the parent's fail-closed filter on its agent rather than on the // session. OpenCode copies session deny rules into task-created child // sessions, which would otherwise give advisor and judge the parent's