diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 9101c3fc1..2ec9444ce 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -583,6 +583,7 @@ Inbound messages are routed to an agent via bindings. - `subagents`: per-agent sub-agent defaults. - `allowAgents`: allowlist of agent ids for `sessions_spawn` from this agent (`["*"]` = allow any; default: only same agent) - `tools`: per-agent tool restrictions (applied before sandbox tool policy). + - `profile`: base tool profile (applied before allow/deny) - `allow`: array of allowed tool names - `deny`: array of denied tool names (deny wins) - `agents.defaults`: shared agent defaults (model, workspace, sandbox, etc.). @@ -1503,6 +1504,34 @@ Legacy: `tools.bash` is still accepted as an alias. - `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable) - Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins) +`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`: +- `minimal`: `session_status` only +- `coding`: `group:fs`, `group:runtime`, `group:sessions`, `group:memory`, `image` +- `messaging`: `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status` +- `full`: no restriction (same as unset) + +Per-agent override: `agents.list[].tools.profile`. + +Example (messaging-only by default, allow Slack + Discord tools too): +```json5 +{ + tools: { + profile: "messaging", + allow: ["slack", "discord"] + } +} +``` + +Example (coding profile, but deny exec/process everywhere): +```json5 +{ + tools: { + profile: "coding", + deny: ["group:runtime"] + } +} +``` + `tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins). This is applied even when the Docker sandbox is **off**. @@ -1513,6 +1542,17 @@ Example (disable browser/canvas everywhere): } ``` +Tool groups (shorthands) work in **global** and **per-agent** tool policies: +- `group:runtime`: `exec`, `bash`, `process` +- `group:fs`: `read`, `write`, `edit`, `apply_patch` +- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` +- `group:memory`: `memory_search`, `memory_get` +- `group:ui`: `browser`, `canvas` +- `group:automation`: `cron`, `gateway` +- `group:messaging`: `message` +- `group:nodes`: `nodes` +- `group:clawdbot`: all built-in Clawdbot tools (excludes provider plugins) + `tools.elevated` controls elevated (host) exec access: - `enabled`: allow elevated mode (default true) - `allowFrom`: per-provider allowlists (empty = disabled) diff --git a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md index 40b30d43d..20c44ec49 100644 --- a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md +++ b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @@ -50,6 +50,7 @@ See [Sandboxing](/gateway/sandboxing) for the full matrix (scope, workspace moun ## Tool policy: which tools exist/are callable Two layers matter: +- **Tool profile**: `tools.profile` and `agents.list[].tools.profile` (base allowlist) - **Global/per-agent tool policy**: `tools.allow`/`tools.deny` and `agents.list[].tools.allow`/`agents.list[].tools.deny` - **Sandbox tool policy** (only applies when sandboxed): `tools.sandbox.tools.allow`/`tools.sandbox.tools.deny` and `agents.list[].tools.sandbox.tools.*` @@ -59,7 +60,7 @@ Rules of thumb: ### Tool groups (shorthands) -For sandbox tool policy, you can use `group:*` entries that expand to multiple tools: +Tool policies (global, agent, sandbox) support `group:*` entries that expand to multiple tools: ```json5 { @@ -78,6 +79,11 @@ Available groups: - `group:fs`: `read`, `write`, `edit`, `apply_patch` - `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` - `group:memory`: `memory_search`, `memory_get` +- `group:ui`: `browser`, `canvas` +- `group:automation`: `cron`, `gateway` +- `group:messaging`: `message` +- `group:nodes`: `nodes` +- `group:clawdbot`: all built-in Clawdbot tools (excludes provider plugins) ## Elevated: exec-only “run on host” diff --git a/docs/multi-agent-sandbox-tools.md b/docs/multi-agent-sandbox-tools.md index 7f1195a0f..f469535bf 100644 --- a/docs/multi-agent-sandbox-tools.md +++ b/docs/multi-agent-sandbox-tools.md @@ -106,6 +106,28 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate --- +### Example 2b: Global coding profile + messaging-only agent + +```json +{ + "tools": { "profile": "coding" }, + "agents": { + "list": [ + { + "id": "support", + "tools": { "profile": "messaging", "allow": ["slack"] } + } + ] + } +} +``` + +**Result:** +- default agents get coding tools +- `support` agent is messaging-only (+ Slack tool) + +--- + ### Example 3: Different Sandbox Modes per Agent ```json @@ -165,22 +187,29 @@ agents.list[].sandbox.prune.* > agents.defaults.sandbox.prune.* ### Tool Restrictions The filtering order is: -1. **Global tool policy** (`tools.allow` / `tools.deny`) -2. **Agent-specific tool policy** (`agents.list[].tools`) -3. **Sandbox tool policy** (`tools.sandbox.tools` or `agents.list[].tools.sandbox.tools`) -4. **Subagent tool policy** (`tools.subagents.tools`, if applicable) +1. **Tool profile** (`tools.profile` or `agents.list[].tools.profile`) +2. **Global tool policy** (`tools.allow` / `tools.deny`) +3. **Agent-specific tool policy** (`agents.list[].tools`) +4. **Sandbox tool policy** (`tools.sandbox.tools` or `agents.list[].tools.sandbox.tools`) +5. **Subagent tool policy** (`tools.subagents.tools`, if applicable) Each level can further restrict tools, but cannot grant back denied tools from earlier levels. If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` for that agent. +If `agents.list[].tools.profile` is set, it overrides `tools.profile` for that agent. ### Tool groups (shorthands) -Sandbox tool policy supports `group:*` entries that expand to multiple concrete tools: +Tool policies (global, agent, sandbox) support `group:*` entries that expand to multiple concrete tools: - `group:runtime`: `exec`, `bash`, `process` - `group:fs`: `read`, `write`, `edit`, `apply_patch` - `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` - `group:memory`: `memory_search`, `memory_get` +- `group:ui`: `browser`, `canvas` +- `group:automation`: `cron`, `gateway` +- `group:messaging`: `message` +- `group:nodes`: `nodes` +- `group:clawdbot`: all built-in Clawdbot tools (excludes provider plugins) ### Elevated Mode `tools.elevated` is the global baseline (sender-based allowlist). `agents.list[].tools.elevated` can further restrict elevated for specific agents (both must allow). diff --git a/docs/tools/index.md b/docs/tools/index.md index 9d2a4b707..e0a867745 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -22,6 +22,77 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot. } ``` +## Tool profiles (base allowlist) + +`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`. +Per-agent override: `agents.list[].tools.profile`. + +Profiles: +- `minimal`: `session_status` only +- `coding`: `group:fs`, `group:runtime`, `group:sessions`, `group:memory`, `image` +- `messaging`: `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status` +- `full`: no restriction (same as unset) + +Example (messaging-only by default, allow Slack + Discord tools too): +```json5 +{ + tools: { + profile: "messaging", + allow: ["slack", "discord"] + } +} +``` + +Example (coding profile, but deny exec/process everywhere): +```json5 +{ + tools: { + profile: "coding", + deny: ["group:runtime"] + } +} +``` + +Example (global coding profile, messaging-only support agent): +```json5 +{ + tools: { profile: "coding" }, + agents: { + list: [ + { + id: "support", + tools: { profile: "messaging", allow: ["slack"] } + } + ] + } +} +``` + +## Tool groups (shorthands) + +Tool policies (global, agent, sandbox) support `group:*` entries that expand to multiple tools. +Use these in `tools.allow` / `tools.deny`. + +Available groups: +- `group:runtime`: `exec`, `bash`, `process` +- `group:fs`: `read`, `write`, `edit`, `apply_patch` +- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` +- `group:memory`: `memory_search`, `memory_get` +- `group:ui`: `browser`, `canvas` +- `group:automation`: `cron`, `gateway` +- `group:messaging`: `message` +- `group:nodes`: `nodes` +- `group:clawdbot`: all built-in Clawdbot tools (excludes provider plugins) + +Example (allow only file tools + browser): +```json5 +{ + tools: { + allow: ["group:fs", "browser"] + } +} +``` + ## Plugins + tools Plugins can register **additional tools** (and CLI commands) beyond the core set. diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index 21e5fa2da..4f5b83fce 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -5,6 +5,7 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import sharp from "sharp"; import { describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../config/config.js"; +import { createClawdbotTools } from "./clawdbot-tools.js"; import { __testing, createClawdbotCodingTools } from "./pi-tools.js"; import { createBrowserTool } from "./tools/browser-tool.js"; @@ -16,7 +17,7 @@ describe("createClawdbotCodingTools", () => { expect(schema.anyOf).toBeUndefined(); }); - it("merges properties for union tool schemas", () => { + it("keeps browser tool schema properties after normalization", () => { const tools = createClawdbotCodingTools(); const browser = tools.find((tool) => tool.name === "browser"); expect(browser).toBeDefined(); @@ -266,6 +267,95 @@ describe("createClawdbotCodingTools", () => { expect(offenders).toEqual([]); }); + it("avoids anyOf/oneOf/allOf in tool schemas", () => { + const tools = createClawdbotCodingTools(); + const offenders: Array<{ + name: string; + keyword: string; + path: string; + }> = []; + const keywords = new Set(["anyOf", "oneOf", "allOf"]); + + const walk = (value: unknown, path: string, name: string): void => { + if (!value) return; + if (Array.isArray(value)) { + for (const [index, entry] of value.entries()) { + walk(entry, `${path}[${index}]`, name); + } + return; + } + if (typeof value !== "object") return; + + const record = value as Record; + for (const [key, entry] of Object.entries(record)) { + const nextPath = path ? `${path}.${key}` : key; + if (keywords.has(key)) { + offenders.push({ name, keyword: key, path: nextPath }); + } + walk(entry, nextPath, name); + } + }; + + for (const tool of tools) { + walk(tool.parameters, "", tool.name); + } + + expect(offenders).toEqual([]); + }); + + it("keeps raw core tool schemas union-free", () => { + const tools = createClawdbotTools(); + const coreTools = new Set([ + "browser", + "canvas", + "nodes", + "cron", + "message", + "gateway", + "agents_list", + "sessions_list", + "sessions_history", + "sessions_send", + "sessions_spawn", + "session_status", + "memory_search", + "memory_get", + "image", + ]); + const offenders: Array<{ + name: string; + keyword: string; + path: string; + }> = []; + const keywords = new Set(["anyOf", "oneOf", "allOf"]); + + const walk = (value: unknown, path: string, name: string): void => { + if (!value) return; + if (Array.isArray(value)) { + for (const [index, entry] of value.entries()) { + walk(entry, `${path}[${index}]`, name); + } + return; + } + if (typeof value !== "object") return; + const record = value as Record; + for (const [key, entry] of Object.entries(record)) { + const nextPath = path ? `${path}.${key}` : key; + if (keywords.has(key)) { + offenders.push({ name, keyword: key, path: nextPath }); + } + walk(entry, nextPath, name); + } + }; + + for (const tool of tools) { + if (!coreTools.has(tool.name)) continue; + walk(tool.parameters, "", tool.name); + } + + expect(offenders).toEqual([]); + }); + it("does not expose provider-specific message tools", () => { const tools = createClawdbotCodingTools({ messageProvider: "discord" }); const names = new Set(tools.map((tool) => tool.name)); @@ -517,6 +607,46 @@ describe("createClawdbotCodingTools", () => { expect(tools.some((tool) => tool.name === "browser")).toBe(false); }); + it("applies tool profiles before allow/deny policies", () => { + const tools = createClawdbotCodingTools({ + config: { tools: { profile: "messaging" } }, + }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("message")).toBe(true); + expect(names.has("sessions_send")).toBe(true); + expect(names.has("sessions_spawn")).toBe(false); + expect(names.has("exec")).toBe(false); + expect(names.has("browser")).toBe(false); + }); + + it("expands group shorthands in global tool policy", () => { + const tools = createClawdbotCodingTools({ + config: { tools: { allow: ["group:fs"] } }, + }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("read")).toBe(true); + expect(names.has("write")).toBe(true); + expect(names.has("edit")).toBe(true); + expect(names.has("exec")).toBe(false); + expect(names.has("browser")).toBe(false); + }); + + it("lets agent profiles override global profiles", () => { + const tools = createClawdbotCodingTools({ + sessionKey: "agent:work:main", + config: { + tools: { profile: "coding" }, + agents: { + list: [{ id: "work", tools: { profile: "messaging" } }], + }, + }, + }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("message")).toBe(true); + expect(names.has("exec")).toBe(false); + expect(names.has("read")).toBe(false); + }); + it("removes unsupported JSON Schema keywords for Cloud Code Assist API compatibility", () => { const tools = createClawdbotCodingTools(); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 0da7f858e..980a2a558 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -28,6 +28,11 @@ import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js"; import { assertSandboxPath } from "./sandbox-paths.js"; import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js"; import { sanitizeToolResultImages } from "./tool-images.js"; +import { + expandToolGroups, + normalizeToolName, + resolveToolProfilePolicy, +} from "./tool-policy.js"; // NOTE(steipete): Upstream read now does file-magic MIME detection; we keep the wrapper // to normalize payloads and sanitize oversized images before they hit providers. @@ -291,21 +296,6 @@ function cleanToolSchemaForGemini(schema: Record): unknown { return cleanSchemaForGemini(schema); } -const TOOL_NAME_ALIASES: Record = { - bash: "exec", - "apply-patch": "apply_patch", -}; - -function normalizeToolName(name: string) { - const normalized = name.trim().toLowerCase(); - return TOOL_NAME_ALIASES[normalized] ?? normalized; -} - -function normalizeToolNames(list?: string[]) { - if (!list) return []; - return list.map(normalizeToolName).filter(Boolean); -} - function isOpenAIProvider(provider?: string) { const normalized = provider?.trim().toLowerCase(); return normalized === "openai" || normalized === "openai-codex"; @@ -357,8 +347,8 @@ function isToolAllowedByPolicyName( policy?: SandboxToolPolicy, ): boolean { if (!policy) return true; - const deny = new Set(normalizeToolNames(policy.deny)); - const allowRaw = normalizeToolNames(policy.allow); + const deny = new Set(expandToolGroups(policy.deny)); + const allowRaw = expandToolGroups(policy.allow); const allow = allowRaw.length > 0 ? new Set(allowRaw) : null; const normalized = normalizeToolName(name); if (deny.has(normalized)) return false; @@ -391,11 +381,15 @@ function resolveEffectiveToolPolicy(params: { : undefined; const agentTools = agentConfig?.tools; const hasAgentToolPolicy = - Array.isArray(agentTools?.allow) || Array.isArray(agentTools?.deny); + Array.isArray(agentTools?.allow) || + Array.isArray(agentTools?.deny) || + typeof agentTools?.profile === "string"; const globalTools = params.config?.tools; + const profile = agentTools?.profile ?? globalTools?.profile; return { agentId, policy: hasAgentToolPolicy ? agentTools : globalTools, + profile, }; } @@ -703,10 +697,15 @@ export function createClawdbotCodingTools(options?: { }): AnyAgentTool[] { const execToolName = "exec"; const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined; - const { agentId, policy: effectiveToolsPolicy } = resolveEffectiveToolPolicy({ + const { + agentId, + policy: effectiveToolsPolicy, + profile, + } = resolveEffectiveToolPolicy({ config: options?.config, sessionKey: options?.sessionKey, }); + const profilePolicy = resolveToolProfilePolicy(profile); const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); const subagentPolicy = @@ -714,6 +713,7 @@ export function createClawdbotCodingTools(options?: { ? resolveSubagentToolPolicy(options.config) : undefined; const allowBackground = isToolAllowedByPolicies("process", [ + profilePolicy, effectiveToolsPolicy, sandbox?.tools, subagentPolicy, @@ -829,12 +829,15 @@ export function createClawdbotCodingTools(options?: { hasRepliedRef: options?.hasRepliedRef, }), ]; - const toolsFiltered = effectiveToolsPolicy - ? filterToolsByPolicy(tools, effectiveToolsPolicy) + const toolsFiltered = profilePolicy + ? filterToolsByPolicy(tools, profilePolicy) : tools; - const sandboxed = sandbox - ? filterToolsByPolicy(toolsFiltered, sandbox.tools) + const policyFiltered = effectiveToolsPolicy + ? filterToolsByPolicy(toolsFiltered, effectiveToolsPolicy) : toolsFiltered; + const sandboxed = sandbox + ? filterToolsByPolicy(policyFiltered, sandbox.tools) + : policyFiltered; const subagentFiltered = subagentPolicy ? filterToolsByPolicy(sandboxed, subagentPolicy) : sandboxed; diff --git a/src/agents/sandbox.ts b/src/agents/sandbox.ts index 3bf96c6e3..dc7c4e077 100644 --- a/src/agents/sandbox.ts +++ b/src/agents/sandbox.ts @@ -33,6 +33,7 @@ import { resolveSessionAgentId, } from "./agent-scope.js"; import { syncSkillsToWorkspace } from "./skills.js"; +import { expandToolGroups } from "./tool-policy.js"; import { DEFAULT_AGENT_WORKSPACE_DIR, DEFAULT_AGENTS_FILENAME, @@ -239,58 +240,10 @@ const BROWSER_BRIDGES = new Map< { bridge: BrowserBridge; containerName: string } >(); -function normalizeToolList(values?: string[]) { - if (!values) return []; - return values - .map((value) => value.trim()) - .filter(Boolean) - .map((value) => value.toLowerCase()); -} - -const TOOL_GROUPS: Record = { - // NOTE: Keep canonical (lowercase) tool names here. - "group:memory": ["memory_search", "memory_get"], - // Basic workspace/file tools - "group:fs": ["read", "write", "edit", "apply_patch"], - // Session management tools - "group:sessions": [ - "sessions_list", - "sessions_history", - "sessions_send", - "sessions_spawn", - "session_status", - ], - // Host/runtime execution tools - "group:runtime": ["exec", "bash", "process"], -}; - -function expandToolGroupEntry(entry: string): string[] { - const raw = entry.trim(); - if (!raw) return []; - const lower = raw.toLowerCase(); - - const group = TOOL_GROUPS[lower]; - if (group) return group; - return [raw]; -} - -function expandToolGroups(values?: string[]): string[] { - if (!values) return []; - const out: string[] = []; - for (const value of values) { - for (const expanded of expandToolGroupEntry(value)) { - const trimmed = expanded.trim(); - if (!trimmed) continue; - out.push(trimmed); - } - } - return out; -} - function isToolAllowed(policy: SandboxToolPolicy, name: string) { - const deny = new Set(normalizeToolList(expandToolGroups(policy.deny))); + const deny = new Set(expandToolGroups(policy.deny)); if (deny.has(name.toLowerCase())) return false; - const allow = normalizeToolList(expandToolGroups(policy.allow)); + const allow = expandToolGroups(policy.allow); if (allow.length === 0) return true; return allow.includes(name.toLowerCase()); } @@ -687,8 +640,8 @@ export function formatSandboxToolPolicyBlockedMessage(params: { }); if (!runtime.sandboxed) return undefined; - const deny = new Set(normalizeToolList(runtime.toolPolicy.deny)); - const allow = normalizeToolList(runtime.toolPolicy.allow); + const deny = new Set(expandToolGroups(runtime.toolPolicy.deny)); + const allow = expandToolGroups(runtime.toolPolicy.allow); const allowSet = allow.length > 0 ? new Set(allow) : null; const blockedByDeny = deny.has(tool); const blockedByAllow = allowSet ? !allowSet.has(tool) : false; diff --git a/src/agents/tool-policy.ts b/src/agents/tool-policy.ts new file mode 100644 index 000000000..55dfd7612 --- /dev/null +++ b/src/agents/tool-policy.ts @@ -0,0 +1,116 @@ +export type ToolProfileId = "minimal" | "coding" | "messaging" | "full"; + +type ToolProfilePolicy = { + allow?: string[]; + deny?: string[]; +}; + +const TOOL_NAME_ALIASES: Record = { + bash: "exec", + "apply-patch": "apply_patch", +}; + +export const TOOL_GROUPS: Record = { + // NOTE: Keep canonical (lowercase) tool names here. + "group:memory": ["memory_search", "memory_get"], + // Basic workspace/file tools + "group:fs": ["read", "write", "edit", "apply_patch"], + // Host/runtime execution tools + "group:runtime": ["exec", "bash", "process"], + // Session management tools + "group:sessions": [ + "sessions_list", + "sessions_history", + "sessions_send", + "sessions_spawn", + "session_status", + ], + // UI helpers + "group:ui": ["browser", "canvas"], + // Automation + infra + "group:automation": ["cron", "gateway"], + // Messaging surface + "group:messaging": ["message"], + // Nodes + device tools + "group:nodes": ["nodes"], + // All Clawdbot native tools (excludes provider plugins). + "group:clawdbot": [ + "browser", + "canvas", + "nodes", + "cron", + "message", + "gateway", + "agents_list", + "sessions_list", + "sessions_history", + "sessions_send", + "sessions_spawn", + "session_status", + "memory_search", + "memory_get", + "image", + ], +}; + +const TOOL_PROFILES: Record = { + minimal: { + allow: ["session_status"], + }, + coding: { + allow: [ + "group:fs", + "group:runtime", + "group:sessions", + "group:memory", + "image", + ], + }, + messaging: { + allow: [ + "group:messaging", + "sessions_list", + "sessions_history", + "sessions_send", + "session_status", + ], + }, + full: {}, +}; + +export function normalizeToolName(name: string) { + const normalized = name.trim().toLowerCase(); + return TOOL_NAME_ALIASES[normalized] ?? normalized; +} + +export function normalizeToolList(list?: string[]) { + if (!list) return []; + return list.map(normalizeToolName).filter(Boolean); +} + +export function expandToolGroups(list?: string[]) { + const normalized = normalizeToolList(list); + const expanded: string[] = []; + for (const value of normalized) { + const group = TOOL_GROUPS[value]; + if (group) { + expanded.push(...group); + continue; + } + expanded.push(value); + } + return Array.from(new Set(expanded)); +} + +export function resolveToolProfilePolicy( + profile?: string, +): ToolProfilePolicy | undefined { + if (!profile) return undefined; + const resolved = TOOL_PROFILES[profile as ToolProfileId]; + if (!resolved) return undefined; + if (!resolved.allow && !resolved.deny) return undefined; + return { + allow: resolved.allow ? [...resolved.allow] : undefined, + deny: resolved.deny ? [...resolved.deny] : undefined, + }; +} diff --git a/src/config/schema.ts b/src/config/schema.ts index b213b0ea2..4d5f0045e 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -107,6 +107,8 @@ const FIELD_LABELS: Record = { "tools.audio.transcription.args": "Audio Transcription Args", "tools.audio.transcription.timeoutSeconds": "Audio Transcription Timeout (sec)", + "tools.profile": "Tool Profile", + "agents.list[].tools.profile": "Agent Tool Profile", "tools.exec.applyPatch.enabled": "Enable apply_patch", "tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist", "gateway.controlUi.basePath": "Control UI Base Path", diff --git a/src/config/types.ts b/src/config/types.ts index 133f4aad1..9838c8ffd 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -988,7 +988,11 @@ export type QueueConfig = { drop?: QueueDropPolicy; }; +export type ToolProfileId = "minimal" | "coding" | "messaging" | "full"; + export type AgentToolsConfig = { + /** Base tool profile applied before allow/deny lists. */ + profile?: ToolProfileId; allow?: string[]; deny?: string[]; /** Per-agent elevated exec gate (can only further restrict global tools.elevated). */ @@ -1053,6 +1057,8 @@ export type MemorySearchConfig = { }; export type ToolsConfig = { + /** Base tool profile applied before allow/deny lists. */ + profile?: ToolProfileId; allow?: string[]; deny?: string[]; audio?: { diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index cacd7fcfb..75921029f 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -837,6 +837,15 @@ const ToolPolicySchema = z }) .optional(); +const ToolProfileSchema = z + .union([ + z.literal("minimal"), + z.literal("coding"), + z.literal("messaging"), + z.literal("full"), + ]) + .optional(); + // Provider docking: allowlists keyed by provider id (no schema updates when adding providers). const ElevatedAllowFromSchema = z .record(z.string(), z.array(z.union([z.string(), z.number()]))) @@ -866,6 +875,7 @@ const AgentSandboxSchema = z const AgentToolsSchema = z .object({ + profile: ToolProfileSchema, allow: z.array(z.string()).optional(), deny: z.array(z.string()).optional(), elevated: z @@ -962,6 +972,7 @@ const AgentEntrySchema = z.object({ const ToolsSchema = z .object({ + profile: ToolProfileSchema, allow: z.array(z.string()).optional(), deny: z.array(z.string()).optional(), audio: z