From 3a2525be5fe08becaf88c33ca36aa3c0522efc3f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 20 Mar 2026 13:25:07 -0700 Subject: [PATCH] Agents: apply tool policy to bundle MCP tools --- src/agents/pi-embedded-runner/run/attempt.ts | 28 +++- src/agents/pi-tools.policy.test.ts | 21 +++ src/agents/pi-tools.ts | 145 +++++++++++++++---- 3 files changed, 163 insertions(+), 31 deletions(-) diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 73b7d0fbff6..91ecd5ef30f 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -74,7 +74,11 @@ import { subscribeEmbeddedPiSession } from "../../pi-embedded-subscribe.js"; import { createPreparedEmbeddedPiSettingsManager } from "../../pi-project-settings.js"; import { applyPiAutoCompactionGuard } from "../../pi-settings.js"; import { toClientToolDefinitions } from "../../pi-tool-definition-adapter.js"; -import { createOpenClawCodingTools, resolveToolLoopDetectionConfig } from "../../pi-tools.js"; +import { + applyOpenClawToolPolicies, + createOpenClawCodingTools, + resolveToolLoopDetectionConfig, +} from "../../pi-tools.js"; import { resolveSandboxContext } from "../../sandbox.js"; import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js"; import { isXaiProvider } from "../../schema/clean-for-xai.js"; @@ -1559,10 +1563,30 @@ export async function runEmbeddedAttempt( ], }) : undefined; - const effectiveTools = + const effectiveToolsUnfiltered = bundleMcpRuntime && bundleMcpRuntime.tools.length > 0 ? [...tools, ...bundleMcpRuntime.tools] : tools; + const effectiveTools = applyOpenClawToolPolicies({ + tools: effectiveToolsUnfiltered, + config: params.config, + sessionKey: sandboxSessionKey, + agentId: sessionAgentId, + modelProvider: params.model.provider, + modelId: params.modelId, + messageProvider: params.messageChannel ?? params.messageProvider, + groupId: params.groupId, + groupChannel: params.groupChannel, + groupSpace: params.groupSpace, + agentAccountId: params.agentAccountId, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + senderIsOwner: params.senderIsOwner, + spawnedBy: params.spawnedBy, + sandbox, + }); const allowedToolNames = collectAllowedToolNames({ tools: effectiveTools, clientTools, diff --git a/src/agents/pi-tools.policy.test.ts b/src/agents/pi-tools.policy.test.ts index 846044c41c0..db765863851 100644 --- a/src/agents/pi-tools.policy.test.ts +++ b/src/agents/pi-tools.policy.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { applyOpenClawToolPolicies } from "./pi-tools.js"; import { filterToolsByPolicy, isToolAllowedByPolicyName, @@ -35,6 +36,26 @@ describe("pi-tools.policy", () => { }); }); +describe("applyOpenClawToolPolicies", () => { + it("removes owner-only tools for non-owner senders", () => { + const tools = [createStubTool("read"), { ...createStubTool("bundle_probe"), ownerOnly: true }]; + const filtered = applyOpenClawToolPolicies({ tools, senderIsOwner: false }); + expect(filtered.map((tool) => tool.name)).toEqual(["read"]); + }); + + it("applies allow/deny policy to non-plugin tools", () => { + const cfg = { + tools: { + allow: ["read"], + deny: ["bundle_probe"], + }, + } as unknown as OpenClawConfig; + const tools = [createStubTool("read"), createStubTool("bundle_probe")]; + const filtered = applyOpenClawToolPolicies({ tools, config: cfg }); + expect(filtered.map((tool) => tool.name)).toEqual(["read"]); + }); +}); + describe("resolveSubagentToolPolicy depth awareness", () => { const baseCfg = { agents: { defaults: { subagents: { maxSpawnDepth: 2 } } }, diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index b8be63f65e5..1e01144e5f8 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -195,6 +195,105 @@ export const __testing = { applyModelProviderToolPolicy, } as const; +export function applyOpenClawToolPolicies(params: { + tools: AnyAgentTool[]; + config?: OpenClawConfig; + sessionKey?: string; + agentId?: string; + modelProvider?: string; + modelId?: string; + messageProvider?: string; + groupId?: string | null; + groupChannel?: string | null; + groupSpace?: string | null; + agentAccountId?: string; + senderId?: string | null; + senderName?: string | null; + senderUsername?: string | null; + senderE164?: string | null; + senderIsOwner?: boolean; + spawnedBy?: string | null; + sandbox?: SandboxContext | null; +}): AnyAgentTool[] { + const sandbox = params.sandbox?.enabled ? params.sandbox : undefined; + const { + agentId, + globalPolicy, + globalProviderPolicy, + agentPolicy, + agentProviderPolicy, + profile, + providerProfile, + profileAlsoAllow, + providerProfileAlsoAllow, + } = resolveEffectiveToolPolicy({ + config: params.config, + sessionKey: params.sessionKey, + agentId: params.agentId, + modelProvider: params.modelProvider, + modelId: params.modelId, + }); + const groupPolicy = resolveGroupToolPolicy({ + config: params.config, + sessionKey: params.sessionKey, + spawnedBy: params.spawnedBy, + messageProvider: params.messageProvider, + groupId: params.groupId, + groupChannel: params.groupChannel, + groupSpace: params.groupSpace, + accountId: params.agentAccountId, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + }); + const profilePolicyWithAlsoAllow = mergeAlsoAllowPolicy( + resolveToolProfilePolicy(profile), + profileAlsoAllow, + ); + const providerProfilePolicyWithAlsoAllow = mergeAlsoAllowPolicy( + resolveToolProfilePolicy(providerProfile), + providerProfileAlsoAllow, + ); + const subagentPolicy = + isSubagentSessionKey(params.sessionKey) && params.sessionKey + ? resolveSubagentToolPolicyForSession(params.config, params.sessionKey) + : undefined; + const toolsForMessageProvider = applyMessageProviderToolPolicy( + params.tools, + params.messageProvider, + ); + const toolsForModelProvider = applyModelProviderToolPolicy(toolsForMessageProvider, { + modelProvider: params.modelProvider, + modelId: params.modelId, + }); + const toolsByAuthorization = applyOwnerOnlyToolPolicy( + toolsForModelProvider, + params.senderIsOwner === true, + ); + return applyToolPolicyPipeline({ + tools: toolsByAuthorization, + toolMeta: (tool) => getPluginToolMeta(tool), + warn: logWarn, + steps: [ + ...buildDefaultToolPolicyPipelineSteps({ + profilePolicy: profilePolicyWithAlsoAllow, + profile, + providerProfilePolicy: providerProfilePolicyWithAlsoAllow, + providerProfile, + globalPolicy, + globalProviderPolicy, + agentPolicy, + agentProviderPolicy, + groupPolicy, + agentId, + }), + { policy: sandbox?.tools, label: "sandbox tools.allow" }, + { policy: subagentPolicy, label: "subagent tools.allow" }, + ], + }); +} + export function createOpenClawCodingTools(options?: { agentId?: string; exec?: ExecToolDefaults & ProcessToolDefaults; @@ -562,37 +661,25 @@ export function createOpenClawCodingTools(options?: { return [tool]; }) : tools; - const toolsForMessageProvider = applyMessageProviderToolPolicy( - toolsForMemoryFlush, - options?.messageProvider, - ); - const toolsForModelProvider = applyModelProviderToolPolicy(toolsForMessageProvider, { + const subagentFiltered = applyOpenClawToolPolicies({ + tools: toolsForMemoryFlush, + config: options?.config, + sessionKey: options?.sessionKey, + agentId: options?.agentId, modelProvider: options?.modelProvider, modelId: options?.modelId, - }); - // Security: treat unknown/undefined as unauthorized (opt-in, not opt-out) - const senderIsOwner = options?.senderIsOwner === true; - const toolsByAuthorization = applyOwnerOnlyToolPolicy(toolsForModelProvider, senderIsOwner); - const subagentFiltered = applyToolPolicyPipeline({ - tools: toolsByAuthorization, - toolMeta: (tool) => getPluginToolMeta(tool), - warn: logWarn, - steps: [ - ...buildDefaultToolPolicyPipelineSteps({ - profilePolicy: profilePolicyWithAlsoAllow, - profile, - providerProfilePolicy: providerProfilePolicyWithAlsoAllow, - providerProfile, - globalPolicy, - globalProviderPolicy, - agentPolicy, - agentProviderPolicy, - groupPolicy, - agentId, - }), - { policy: sandbox?.tools, label: "sandbox tools.allow" }, - { policy: subagentPolicy, label: "subagent tools.allow" }, - ], + messageProvider: options?.messageProvider, + groupId: options?.groupId, + groupChannel: options?.groupChannel, + groupSpace: options?.groupSpace, + agentAccountId: options?.agentAccountId, + senderId: options?.senderId, + senderName: options?.senderName, + senderUsername: options?.senderUsername, + senderE164: options?.senderE164, + senderIsOwner: options?.senderIsOwner, + spawnedBy: options?.spawnedBy, + sandbox, }); // Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai. // Without this, some providers (notably OpenAI) will reject root-level union schemas.