From 4cb46f223c4bf7e5e91721d74418fe76ea128c30 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 21:39:48 -0700 Subject: [PATCH] Security: trim audit policy import surfaces --- src/agents/pi-tools.policy.ts | 48 +++---------------------------- src/agents/tool-policy-match.ts | 44 ++++++++++++++++++++++++++++ src/gateway/hooks-policy.ts | 24 ++++++++++++++++ src/gateway/hooks.ts | 26 ++--------------- src/security/audit-extra.async.ts | 8 ++---- src/security/audit-extra.sync.ts | 10 +++---- 6 files changed, 81 insertions(+), 79 deletions(-) create mode 100644 src/agents/tool-policy-match.ts create mode 100644 src/gateway/hooks-policy.ts diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index 0353c454865..a6f8651f72d 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -7,7 +7,6 @@ import { normalizeAgentId } from "../routing/session-key.js"; import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js"; -import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js"; import type { AnyAgentTool } from "./pi-tools.types.js"; import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js"; import type { SandboxToolPolicy } from "./sandbox.js"; @@ -15,34 +14,8 @@ import { resolveStoredSubagentCapabilities, type SubagentSessionRole, } from "./subagent-capabilities.js"; -import { expandToolGroups, normalizeToolName } from "./tool-policy.js"; - -function makeToolPolicyMatcher(policy: SandboxToolPolicy) { - const deny = compileGlobPatterns({ - raw: expandToolGroups(policy.deny ?? []), - normalize: normalizeToolName, - }); - const allow = compileGlobPatterns({ - raw: expandToolGroups(policy.allow ?? []), - normalize: normalizeToolName, - }); - return (name: string) => { - const normalized = normalizeToolName(name); - if (matchesAnyGlobPattern(normalized, deny)) { - return false; - } - if (allow.length === 0) { - return true; - } - if (matchesAnyGlobPattern(normalized, allow)) { - return true; - } - if (normalized === "apply_patch" && matchesAnyGlobPattern("exec", allow)) { - return true; - } - return false; - }; -} +import { isToolAllowedByPolicies, isToolAllowedByPolicyName } from "./tool-policy-match.js"; +import { normalizeToolName } from "./tool-policy.js"; /** * Tools always denied for sub-agents regardless of depth. @@ -140,19 +113,11 @@ export function resolveSubagentToolPolicyForSession( return { allow: mergedAllow, deny }; } -export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean { - if (!policy) { - return true; - } - return makeToolPolicyMatcher(policy)(name); -} - export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) { if (!policy) { return tools; } - const matcher = makeToolPolicyMatcher(policy); - return tools.filter((tool) => matcher(tool.name)); + return tools.filter((tool) => isToolAllowedByPolicyName(tool.name, policy)); } type ToolPolicyConfig = { @@ -381,9 +346,4 @@ export function resolveGroupToolPolicy(params: { return pickSandboxToolPolicy(toolsConfig); } -export function isToolAllowedByPolicies( - name: string, - policies: Array, -) { - return policies.every((policy) => isToolAllowedByPolicyName(name, policy)); -} +export { isToolAllowedByPolicies, isToolAllowedByPolicyName } from "./tool-policy-match.js"; diff --git a/src/agents/tool-policy-match.ts b/src/agents/tool-policy-match.ts new file mode 100644 index 00000000000..112bd94be10 --- /dev/null +++ b/src/agents/tool-policy-match.ts @@ -0,0 +1,44 @@ +import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js"; +import type { SandboxToolPolicy } from "./sandbox/types.js"; +import { expandToolGroups, normalizeToolName } from "./tool-policy.js"; + +function makeToolPolicyMatcher(policy: SandboxToolPolicy) { + const deny = compileGlobPatterns({ + raw: expandToolGroups(policy.deny ?? []), + normalize: normalizeToolName, + }); + const allow = compileGlobPatterns({ + raw: expandToolGroups(policy.allow ?? []), + normalize: normalizeToolName, + }); + return (name: string) => { + const normalized = normalizeToolName(name); + if (matchesAnyGlobPattern(normalized, deny)) { + return false; + } + if (allow.length === 0) { + return true; + } + if (matchesAnyGlobPattern(normalized, allow)) { + return true; + } + if (normalized === "apply_patch" && matchesAnyGlobPattern("exec", allow)) { + return true; + } + return false; + }; +} + +export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean { + if (!policy) { + return true; + } + return makeToolPolicyMatcher(policy)(name); +} + +export function isToolAllowedByPolicies( + name: string, + policies: Array, +) { + return policies.every((policy) => isToolAllowedByPolicyName(name, policy)); +} diff --git a/src/gateway/hooks-policy.ts b/src/gateway/hooks-policy.ts new file mode 100644 index 00000000000..27ce19b40cf --- /dev/null +++ b/src/gateway/hooks-policy.ts @@ -0,0 +1,24 @@ +import { normalizeAgentId } from "../routing/session-key.js"; + +export function resolveAllowedAgentIds(raw: string[] | undefined): Set | undefined { + if (!Array.isArray(raw)) { + return undefined; + } + const allowed = new Set(); + let hasWildcard = false; + for (const entry of raw) { + const trimmed = entry.trim(); + if (!trimmed) { + continue; + } + if (trimmed === "*") { + hasWildcard = true; + break; + } + allowed.add(normalizeAgentId(trimmed)); + } + if (hasWildcard) { + return undefined; + } + return allowed; +} diff --git a/src/gateway/hooks.ts b/src/gateway/hooks.ts index f371e3565a9..d9e23060f04 100644 --- a/src/gateway/hooks.ts +++ b/src/gateway/hooks.ts @@ -5,9 +5,10 @@ import { listChannelPlugins } from "../channels/plugins/index.js"; import type { ChannelId } from "../channels/plugins/types.js"; import type { OpenClawConfig } from "../config/config.js"; import { readJsonBodyWithLimit, requestBodyErrorToText } from "../infra/http-body.js"; -import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js"; +import { parseAgentSessionKey } from "../routing/session-key.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { type HookMappingResolved, resolveHookMappings } from "./hooks-mapping.js"; +import { resolveAllowedAgentIds } from "./hooks-policy.js"; const DEFAULT_HOOKS_PATH = "/hooks"; const DEFAULT_HOOKS_MAX_BODY_BYTES = 256 * 1024; @@ -100,29 +101,6 @@ function resolveKnownAgentIds(cfg: OpenClawConfig, defaultAgentId: string): Set< return known; } -export function resolveAllowedAgentIds(raw: string[] | undefined): Set | undefined { - if (!Array.isArray(raw)) { - return undefined; - } - const allowed = new Set(); - let hasWildcard = false; - for (const entry of raw) { - const trimmed = entry.trim(); - if (!trimmed) { - continue; - } - if (trimmed === "*") { - hasWildcard = true; - break; - } - allowed.add(normalizeAgentId(trimmed)); - } - if (hasWildcard) { - return undefined; - } - return allowed; -} - function resolveSessionKey(raw: string | undefined): string | undefined { const value = raw?.trim(); return value ? value : undefined; diff --git a/src/security/audit-extra.async.ts b/src/security/audit-extra.async.ts index 7ad36855852..88df46bafa1 100644 --- a/src/security/audit-extra.async.ts +++ b/src/security/audit-extra.async.ts @@ -6,15 +6,13 @@ import fs from "node:fs/promises"; import path from "node:path"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js"; -import { - resolveSandboxConfigForAgent, - resolveSandboxToolPolicyForAgent, -} from "../agents/sandbox.js"; +import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js"; import { SANDBOX_BROWSER_SECURITY_HASH_EPOCH } from "../agents/sandbox/constants.js"; import { execDockerRaw, type ExecDockerRawResult } from "../agents/sandbox/docker.js"; +import { resolveSandboxToolPolicyForAgent } from "../agents/sandbox/tool-policy.js"; import type { SandboxToolPolicy } from "../agents/sandbox/types.js"; import { loadWorkspaceSkillEntries } from "../agents/skills.js"; +import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js"; import { resolveToolProfilePolicy } from "../agents/tool-policy.js"; import { listAgentWorkspaceDirs } from "../agents/workspace-dirs.js"; import { formatCliCommand } from "../cli/command-format.js"; diff --git a/src/security/audit-extra.sync.ts b/src/security/audit-extra.sync.ts index 79a701c5489..bebcc44c0d0 100644 --- a/src/security/audit-extra.sync.ts +++ b/src/security/audit-extra.sync.ts @@ -1,16 +1,14 @@ -import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js"; -import { - resolveSandboxConfigForAgent, - resolveSandboxToolPolicyForAgent, -} from "../agents/sandbox.js"; +import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js"; import { isDangerousNetworkMode, normalizeNetworkMode } from "../agents/sandbox/network-mode.js"; /** * Synchronous security audit collector functions. * * These functions analyze config-based security properties without I/O. */ +import { resolveSandboxToolPolicyForAgent } from "../agents/sandbox/tool-policy.js"; import type { SandboxToolPolicy } from "../agents/sandbox/types.js"; import { getBlockedBindReason } from "../agents/sandbox/validate-sandbox-security.js"; +import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js"; import { resolveToolProfilePolicy } from "../agents/tool-policy.js"; import { resolveBrowserConfig } from "../browser/config.js"; import { formatCliCommand } from "../cli/command-format.js"; @@ -21,7 +19,7 @@ import { } from "../config/model-input.js"; import type { AgentToolsConfig } from "../config/types.tools.js"; import { resolveGatewayAuth } from "../gateway/auth.js"; -import { resolveAllowedAgentIds } from "../gateway/hooks.js"; +import { resolveAllowedAgentIds } from "../gateway/hooks-policy.js"; import { DEFAULT_DANGEROUS_NODE_COMMANDS, resolveNodeCommandAllowlist,