mirror of
https://github.com/clawdbot/clawdbot.git
synced 2026-01-31 19:37:45 +01:00
feat(tools): add tool profiles and group shorthands
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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”
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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();
|
||||
|
||||
|
||||
@@ -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<string, unknown>): unknown {
|
||||
return cleanSchemaForGemini(schema);
|
||||
}
|
||||
|
||||
const TOOL_NAME_ALIASES: Record<string, string> = {
|
||||
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;
|
||||
|
||||
@@ -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<string, string[]> = {
|
||||
// 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;
|
||||
|
||||
116
src/agents/tool-policy.ts
Normal file
116
src/agents/tool-policy.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
|
||||
|
||||
type ToolProfilePolicy = {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
|
||||
const TOOL_NAME_ALIASES: Record<string, string> = {
|
||||
bash: "exec",
|
||||
"apply-patch": "apply_patch",
|
||||
};
|
||||
|
||||
export const TOOL_GROUPS: Record<string, string[]> = {
|
||||
// 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<ToolProfileId, ToolProfilePolicy> = {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -107,6 +107,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"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",
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user