diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index 4e7cea7c94e..af3b9fe992b 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -89,7 +89,7 @@ export function resolveSubagentToolPolicy(cfg?: OpenClawConfig, depth?: number): ...(Array.isArray(configured?.deny) ? configured.deny : []), ]; const mergedAllow = allow && alsoAllow ? Array.from(new Set([...allow, ...alsoAllow])) : allow; - return { allow: mergedAllow, deny }; + return { allow: mergedAllow, alsoAllow, deny }; } export function resolveSubagentToolPolicyForSession( @@ -109,8 +109,13 @@ export function resolveSubagentToolPolicyForSession( ), ...(Array.isArray(configured?.deny) ? configured.deny : []), ]; + // Merge allow + alsoAllow when both are present (keeps allow list accurate for + // access gating). When only alsoAllow is set, preserve it on the returned + // object so collectExplicitAllowlist() can build the plugin tool allowlist — + // callers that only gate on `allow` remain unaffected because allow stays + // undefined, but the alsoAllow entries are no longer silently dropped. const mergedAllow = allow && alsoAllow ? Array.from(new Set([...allow, ...alsoAllow])) : allow; - return { allow: mergedAllow, deny }; + return { allow: mergedAllow, alsoAllow, deny }; } export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) { diff --git a/src/agents/sandbox/types.ts b/src/agents/sandbox/types.ts index 482ce6a922e..72430dcd43d 100644 --- a/src/agents/sandbox/types.ts +++ b/src/agents/sandbox/types.ts @@ -6,6 +6,7 @@ export type { SandboxDockerConfig } from "./types.docker.js"; export type SandboxToolPolicy = { allow?: string[]; + alsoAllow?: string[]; deny?: string[]; }; diff --git a/src/agents/tool-policy.test.ts b/src/agents/tool-policy.test.ts index 963c703a409..056cc7f26db 100644 --- a/src/agents/tool-policy.test.ts +++ b/src/agents/tool-policy.test.ts @@ -5,6 +5,7 @@ import type { SandboxToolPolicy } from "./sandbox/types.js"; import { TOOL_POLICY_CONFORMANCE } from "./tool-policy.conformance.js"; import { applyOwnerOnlyToolPolicy, + collectExplicitAllowlist, expandToolGroups, isOwnerOnlyToolName, normalizeToolName, @@ -225,3 +226,38 @@ describe("resolveSandboxToolPolicyForAgent", () => { expect(resolved.deny).toEqual(["image"]); }); }); + +describe("collectExplicitAllowlist", () => { + it("collects entries from allow", () => { + expect(collectExplicitAllowlist([{ allow: ["read", "write"] }])).toEqual(["read", "write"]); + }); + + it("collects entries from alsoAllow when allow is absent", () => { + expect(collectExplicitAllowlist([{ alsoAllow: ["openrag_search"] }])).toEqual([ + "openrag_search", + ]); + }); + + it("collects entries from both allow and alsoAllow", () => { + expect( + collectExplicitAllowlist([{ allow: ["read"], alsoAllow: ["openrag_search"] }]), + ).toEqual(["read", "openrag_search"]); + }); + + it("returns empty array when policy has neither allow nor alsoAllow", () => { + expect(collectExplicitAllowlist([{ deny: ["exec"] }])).toEqual([]); + }); + + it("returns empty array for undefined policies", () => { + expect(collectExplicitAllowlist([undefined, undefined])).toEqual([]); + }); + + it("collects alsoAllow across multiple policies", () => { + expect( + collectExplicitAllowlist([ + { alsoAllow: ["openrag_search"] }, + { alsoAllow: ["custom_tool"] }, + ]), + ).toEqual(["openrag_search", "custom_tool"]); + }); +}); diff --git a/src/agents/tool-policy.ts b/src/agents/tool-policy.ts index 5538fb765ce..49aeba5f002 100644 --- a/src/agents/tool-policy.ts +++ b/src/agents/tool-policy.ts @@ -58,6 +58,7 @@ export function applyOwnerOnlyToolPolicy(tools: AnyAgentTool[], senderIsOwner: b export type ToolPolicyLike = { allow?: string[]; + alsoAllow?: string[]; deny?: string[]; }; @@ -75,10 +76,14 @@ export type AllowlistResolution = { export function collectExplicitAllowlist(policies: Array): string[] { const entries: string[] = []; for (const policy of policies) { - if (!policy?.allow) { - continue; - } - for (const value of policy.allow) { + // Collect both `allow` and `alsoAllow` entries. `alsoAllow` is the additive + // form used when a profile is set and the user wants to extend it with + // additional tools (e.g. plugin tools for subagents) without replacing the + // full allow list. Without including `alsoAllow` here, plugin tools listed + // in `tools.subagents.tools.alsoAllow` are silently ignored and never reach + // subagent sessions. + const sources = [...(policy?.allow ?? []), ...(policy?.alsoAllow ?? [])]; + for (const value of sources) { if (typeof value !== "string") { continue; } @@ -149,6 +154,7 @@ export function expandPolicyWithPluginGroups( } return { allow: expandPluginGroups(policy.allow, groups), + alsoAllow: expandPluginGroups(policy.alsoAllow, groups), deny: expandPluginGroups(policy.deny, groups), }; }