diff --git a/src/agents/pi-tools.policy.test.ts b/src/agents/pi-tools.policy.test.ts index 846044c41c0..805ab556d5a 100644 --- a/src/agents/pi-tools.policy.test.ts +++ b/src/agents/pi-tools.policy.test.ts @@ -271,4 +271,76 @@ describe("resolveEffectiveToolPolicy", () => { const result = resolveEffectiveToolPolicy({ config: cfg, agentId: "coder" }); expect(result.profileAlsoAllow).toEqual(["read", "write", "edit"]); }); + + it("does not implicitly expose global exec when agent has explicit alsoAllow without it", () => { + // Regression test for #47487: tool profile restrictions not enforced + const cfg = { + tools: { + exec: { security: "full", ask: "off" }, + }, + agents: { + list: [ + { + id: "messenger", + tools: { + profile: "messaging", + alsoAllow: ["web_search"], + }, + }, + ], + }, + } as OpenClawConfig; + const result = resolveEffectiveToolPolicy({ config: cfg, agentId: "messenger" }); + expect(result.profileAlsoAllow).toEqual(["web_search"]); + expect(result.profileAlsoAllow).not.toContain("exec"); + expect(result.profileAlsoAllow).not.toContain("process"); + }); + + it("does not implicitly expose global fs when agent has explicit empty alsoAllow", () => { + const cfg = { + tools: { + fs: { workspaceOnly: false }, + }, + agents: { + list: [ + { + id: "restricted", + tools: { + profile: "messaging", + alsoAllow: [], + }, + }, + ], + }, + } as OpenClawConfig; + const result = resolveEffectiveToolPolicy({ config: cfg, agentId: "restricted" }); + // Empty array is returned (not undefined) when explicit alsoAllow is set + expect(result.profileAlsoAllow).toEqual([]); + expect(result.profileAlsoAllow).not.toContain("read"); + expect(result.profileAlsoAllow).not.toContain("write"); + expect(result.profileAlsoAllow).not.toContain("edit"); + }); + + it("still uses agent-level exec section for implicit exposure even with explicit alsoAllow", () => { + const cfg = { + tools: { + profile: "messaging", + }, + agents: { + list: [ + { + id: "coder", + tools: { + alsoAllow: ["web_search"], + exec: { host: "sandbox" }, + }, + }, + ], + }, + } as OpenClawConfig; + const result = resolveEffectiveToolPolicy({ config: cfg, agentId: "coder" }); + expect(result.profileAlsoAllow).toContain("web_search"); + expect(result.profileAlsoAllow).toContain("exec"); + expect(result.profileAlsoAllow).toContain("process"); + }); }); diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index 4e7cea7c94e..be91fb9c2db 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -210,18 +210,25 @@ function hasExplicitToolSection(section: unknown): boolean { function resolveImplicitProfileAlsoAllow(params: { globalTools?: OpenClawConfig["tools"]; agentTools?: AgentToolsConfig; + /** When agent has explicit alsoAllow, skip global tool sections for implicit exposure. */ + agentHasExplicitAlsoAllow?: boolean; }): string[] | undefined { const implicit = new Set(); + // When agent has explicit alsoAllow set, only agent-level tool sections should + // trigger implicit exposure. Global tool sections should not override agent's + // explicit restriction. This prevents tools.exec at global level from bypassing + // an agent's profile + alsoAllow restriction. + const useGlobalSections = !params.agentHasExplicitAlsoAllow; if ( hasExplicitToolSection(params.agentTools?.exec) || - hasExplicitToolSection(params.globalTools?.exec) + (useGlobalSections && hasExplicitToolSection(params.globalTools?.exec)) ) { implicit.add("exec"); implicit.add("process"); } if ( hasExplicitToolSection(params.agentTools?.fs) || - hasExplicitToolSection(params.globalTools?.fs) + (useGlobalSections && hasExplicitToolSection(params.globalTools?.fs)) ) { implicit.add("read"); implicit.add("write"); @@ -260,9 +267,14 @@ export function resolveEffectiveToolPolicy(params: { modelProvider: params.modelProvider, modelId: params.modelId, }); + const agentExplicitAlsoAllow = resolveExplicitProfileAlsoAllow(agentTools); const explicitProfileAlsoAllow = - resolveExplicitProfileAlsoAllow(agentTools) ?? resolveExplicitProfileAlsoAllow(globalTools); - const implicitProfileAlsoAllow = resolveImplicitProfileAlsoAllow({ globalTools, agentTools }); + agentExplicitAlsoAllow ?? resolveExplicitProfileAlsoAllow(globalTools); + const implicitProfileAlsoAllow = resolveImplicitProfileAlsoAllow({ + globalTools, + agentTools, + agentHasExplicitAlsoAllow: agentExplicitAlsoAllow !== undefined, + }); const profileAlsoAllow = explicitProfileAlsoAllow || implicitProfileAlsoAllow ? Array.from(