Compare commits

...

1 Commits

Author SHA1 Message Date
Vincent Koc
3a2525be5f Agents: apply tool policy to bundle MCP tools 2026-03-20 13:25:07 -07:00
3 changed files with 163 additions and 31 deletions

View File

@ -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,

View File

@ -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 } } },

View File

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