fix: per-agent sandbox overrides

This commit is contained in:
Peter Steinberger
2026-01-07 12:24:12 +01:00
parent 1143b3eff0
commit 6352f33799
13 changed files with 138 additions and 223 deletions

View File

@@ -22,6 +22,7 @@
- Pairing: lock + atomically write pairing stores with 0600 perms and stop logging pairing codes in provider logs. - Pairing: lock + atomically write pairing stores with 0600 perms and stop logging pairing codes in provider logs.
- Discord: include all inbound attachments in `MediaPaths`/`MediaUrls` (back-compat `MediaPath`/`MediaUrl` still first). - Discord: include all inbound attachments in `MediaPaths`/`MediaUrls` (back-compat `MediaPath`/`MediaUrl` still first).
- Sandbox: add `agent.sandbox.workspaceAccess` (`none`/`ro`/`rw`) to control agent workspace visibility inside the container; `ro` hard-disables `write`/`edit`. - Sandbox: add `agent.sandbox.workspaceAccess` (`none`/`ro`/`rw`) to control agent workspace visibility inside the container; `ro` hard-disables `write`/`edit`.
- Routing: allow per-agent sandbox overrides (including `workspaceAccess` and `sandbox.tools`) plus per-agent tool policies in multi-agent configs. Thanks @pasogott for PR #380.
- Tools: add Telegram/WhatsApp reaction tools (with per-provider gating). Thanks @zats for PR #353. - Tools: add Telegram/WhatsApp reaction tools (with per-provider gating). Thanks @zats for PR #353.
- Tools: unify reaction removal semantics across Discord/Slack/Telegram/WhatsApp and allow WhatsApp reaction routing across accounts. - Tools: unify reaction removal semantics across Discord/Slack/Telegram/WhatsApp and allow WhatsApp reaction routing across accounts.
- Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect. - Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect.

View File

@@ -1,203 +0,0 @@
# PR: Agent-specific Sandbox and Tool Configuration
## Summary
Adds support for per-agent sandbox and tool configurations in multi-agent setups. This allows running multiple agents with different security profiles (e.g., personal assistant with full access, family bot with read-only restrictions).
## Changes
### Core Implementation (5 files, +49 LoC)
1. **`src/config/types.ts`** (+4 lines)
- Added `sandbox` and `tools` fields to `routing.agents[agentId]` type
2. **`src/config/zod-schema.ts`** (+6 lines)
- Added Zod validation for `routing.agents[].sandbox` and `routing.agents[].tools`
3. **`src/agents/agent-scope.ts`** (+12 lines)
- Extended `resolveAgentConfig()` to return `sandbox` and `tools` fields
4. **`src/agents/sandbox.ts`** (+12 lines)
- Modified `defaultSandboxConfig()` to accept `agentId` parameter
- Added logic to prefer agent-specific sandbox config over global config
- Updated `resolveSandboxContext()` and `ensureSandboxWorkspaceForSession()` to extract and pass `agentId`
5. **`src/agents/pi-tools.ts`** (+15 lines)
- Added agent-specific tool filtering before sandbox tool filtering
- Imports `resolveAgentConfig` and `resolveAgentIdFromSessionKey`
### Tests (3 new test files, 18 tests)
1. **`src/agents/agent-scope.test.ts`** (7 tests)
- Tests for `resolveAgentConfig()` with sandbox and tools fields
2. **`src/agents/sandbox-agent-config.test.ts`** (6 tests)
- Tests for agent-specific sandbox mode, scope, and workspaceRoot overrides
- Tests for multiple agents with different sandbox configs
3. **`src/agents/pi-tools-agent-config.test.ts`** (5 tests)
- Tests for agent-specific tool filtering
- Tests for combined global + agent + sandbox tool policies
### Documentation (3 files)
1. **`docs/multi-agent-sandbox-tools.md`** (new)
- Comprehensive guide for per-agent sandbox and tool configuration
- Examples for common use cases
- Migration guide from single-agent configs
2. **`docs/concepts/multi-agent.md`** (updated)
- Added section on per-agent sandbox and tool configuration
- Link to detailed guide
3. **`docs/gateway/configuration.md`** (updated)
- Added documentation for `routing.agents[].sandbox` and `routing.agents[].tools` fields
## Features
### Agent-specific Sandbox Config
```json
{
"routing": {
"agents": {
"main": {
"workspace": "~/clawd",
"sandbox": { "mode": "off" }
},
"family": {
"workspace": "~/clawd-family",
"sandbox": {
"mode": "all",
"scope": "agent"
}
}
}
}
}
```
**Result:**
- `main` agent runs on host (no Docker)
- `family` agent runs in Docker with one container per agent
### Agent-specific Tool Restrictions
```json
{
"routing": {
"agents": {
"family": {
"workspace": "~/clawd-family",
"tools": {
"allow": ["read"],
"deny": ["bash", "write", "edit", "process"]
}
}
}
}
}
```
**Result:**
- `family` agent can only use the `read` tool
- All other tools are denied
## Configuration Precedence
### Sandbox Config
Agent-specific settings override global:
- `routing.agents[id].sandbox.mode` > `agent.sandbox.mode`
- `routing.agents[id].sandbox.scope` > `agent.sandbox.scope`
- `routing.agents[id].sandbox.workspaceRoot` > `agent.sandbox.workspaceRoot`
Note: `docker`, `browser`, `tools`, and `prune` settings from `agent.sandbox` remain global.
### Tool Filtering
Filtering order (each level can only further restrict):
1. Global tool policy (`agent.tools`)
2. **Agent-specific tool policy** (`routing.agents[id].tools`) ← NEW
3. Sandbox tool policy (`agent.sandbox.tools`)
4. Subagent tool policy (if applicable)
## Backward Compatibility
**100% backward compatible**
- All existing configs work unchanged
- New fields (`routing.agents[].sandbox`, `routing.agents[].tools`) are optional
- Default behavior: if no agent-specific config exists, use global config
- All 1325 existing tests pass
## Testing
### New Tests: 18 tests, all passing
```
✓ src/agents/agent-scope.test.ts (7 tests)
✓ src/agents/sandbox-agent-config.test.ts (6 tests)
✓ src/agents/pi-tools-agent-config.test.ts (5 tests)
```
### Existing Tests: All passing
```
Test Files 227 passed | 2 skipped (229)
Tests 1325 passed | 2 skipped (1327)
```
Specifically verified:
- Discord provider tests: ✓ 23 tests
- Telegram provider tests: ✓ 42 tests
- Routing tests: ✓ 7 tests
- Gateway tests: ✓ All passed
## Use Cases
### Use Case 1: Personal Assistant + Restricted Family Bot
- Personal agent: Host, all tools
- Family agent: Docker, read-only
### Use Case 2: Work Agent with Limited Access
- Personal agent: Full access
- Work agent: Docker, no browser/gateway tools
### Use Case 3: Public-facing Bot
- Main agent: Trusted, full access
- Public agent: Always sandboxed, minimal tools
## Migration Path
**Before (global config):**
```json
{
"agent": {
"sandbox": { "mode": "non-main" }
}
}
```
**After (per-agent config):**
```json
{
"routing": {
"agents": {
"main": { "sandbox": { "mode": "off" } },
"family": { "sandbox": { "mode": "all", "scope": "agent" } }
}
}
}
```
## Related Issues
- Addresses need for per-agent security policies in multi-agent setups
- Complements existing multi-agent routing feature (introduced in 7360abad)
- Prepares for upcoming `clawdbot agents` CLI (announced 2026-01-07)
## Checklist
- [x] Code changes implemented
- [x] Tests written and passing
- [x] Documentation updated
- [x] Backward compatibility verified
- [x] No breaking changes
- [x] TypeScript types updated
- [x] Zod schema validation added

View File

@@ -168,4 +168,4 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio
- **Resource control**: Sandbox specific agents while keeping others on host - **Resource control**: Sandbox specific agents while keeping others on host
- **Flexible policies**: Different permissions per agent - **Flexible policies**: Different permissions per agent
See [Multi-Agent Sandbox & Tools](/docs/multi-agent-sandbox-tools) for detailed examples. See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for detailed examples.

View File

@@ -336,8 +336,10 @@ Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside o
- `model`: per-agent default model (provider/model), overrides `agent.model` for that agent. - `model`: per-agent default model (provider/model), overrides `agent.model` for that agent.
- `sandbox`: per-agent sandbox config (overrides `agent.sandbox`). - `sandbox`: per-agent sandbox config (overrides `agent.sandbox`).
- `mode`: `"off"` | `"non-main"` | `"all"` - `mode`: `"off"` | `"non-main"` | `"all"`
- `workspaceAccess`: `"none"` | `"ro"` | `"rw"`
- `scope`: `"session"` | `"agent"` | `"shared"` - `scope`: `"session"` | `"agent"` | `"shared"`
- `workspaceRoot`: custom sandbox workspace root - `workspaceRoot`: custom sandbox workspace root
- `tools`: per-agent sandbox tool policy (deny wins; overrides `agent.sandbox.tools`)
- `tools`: per-agent tool restrictions (applied before sandbox tool policy). - `tools`: per-agent tool restrictions (applied before sandbox tool policy).
- `allow`: array of allowed tool names - `allow`: array of allowed tool names
- `deny`: array of denied tool names (deny wins) - `deny`: array of denied tool names (deny wins)

View File

@@ -3,7 +3,7 @@
## Overview ## Overview
Each agent in a multi-agent setup can now have its own: Each agent in a multi-agent setup can now have its own:
- **Sandbox configuration** (`mode`, `scope`, `workspaceRoot`) - **Sandbox configuration** (`mode`, `scope`, `workspaceRoot`, `workspaceAccess`, `tools`)
- **Tool restrictions** (`allow`, `deny`) - **Tool restrictions** (`allow`, `deny`)
This allows you to run multiple agents with different security profiles: This allows you to run multiple agents with different security profiles:
@@ -141,9 +141,10 @@ Agent-specific settings override global:
routing.agents[id].sandbox.mode > agent.sandbox.mode routing.agents[id].sandbox.mode > agent.sandbox.mode
routing.agents[id].sandbox.scope > agent.sandbox.scope routing.agents[id].sandbox.scope > agent.sandbox.scope
routing.agents[id].sandbox.workspaceRoot > agent.sandbox.workspaceRoot routing.agents[id].sandbox.workspaceRoot > agent.sandbox.workspaceRoot
routing.agents[id].sandbox.workspaceAccess > agent.sandbox.workspaceAccess
``` ```
**Note:** `docker`, `browser`, `tools`, and `prune` settings from `agent.sandbox` are still **global** and apply to all sandboxed agents. **Note:** `docker`, `browser`, and `prune` settings from `agent.sandbox` are still **global** and apply to all sandboxed agents.
### Tool Restrictions ### Tool Restrictions
The filtering order is: The filtering order is:
@@ -153,6 +154,7 @@ The filtering order is:
4. **Subagent tool policy** (if applicable) 4. **Subagent tool policy** (if applicable)
Each level can further restrict tools, but cannot grant back denied tools from earlier levels. Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
If `routing.agents[id].sandbox.tools` is set, it replaces `agent.sandbox.tools` for that agent.
--- ---

View File

@@ -55,7 +55,12 @@ describe("resolveAgentConfig", () => {
mode: "all", mode: "all",
scope: "agent", scope: "agent",
perSession: false, perSession: false,
workspaceAccess: "ro",
workspaceRoot: "~/sandboxes", workspaceRoot: "~/sandboxes",
tools: {
allow: ["read"],
deny: ["bash"],
},
}, },
}, },
}, },
@@ -66,7 +71,12 @@ describe("resolveAgentConfig", () => {
mode: "all", mode: "all",
scope: "agent", scope: "agent",
perSession: false, perSession: false,
workspaceAccess: "ro",
workspaceRoot: "~/sandboxes", workspaceRoot: "~/sandboxes",
tools: {
allow: ["read"],
deny: ["bash"],
},
}); });
}); });

View File

@@ -29,9 +29,14 @@ export function resolveAgentConfig(
model?: string; model?: string;
sandbox?: { sandbox?: {
mode?: "off" | "non-main" | "all"; mode?: "off" | "non-main" | "all";
workspaceAccess?: "none" | "ro" | "rw";
scope?: "session" | "agent" | "shared"; scope?: "session" | "agent" | "shared";
perSession?: boolean; perSession?: boolean;
workspaceRoot?: string; workspaceRoot?: string;
tools?: {
allow?: string[];
deny?: string[];
};
}; };
tools?: { tools?: {
allow?: string[]; allow?: string[];

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { createClawdbotCodingTools } from "./pi-tools.js"; import { createClawdbotCodingTools } from "./pi-tools.js";
import type { SandboxDockerConfig } from "./sandbox.js";
describe("Agent-specific tool filtering", () => { describe("Agent-specific tool filtering", () => {
it("should apply global tool policy when no agent-specific policy exists", () => { it("should apply global tool policy when no agent-specific policy exists", () => {
@@ -188,7 +189,15 @@ describe("Agent-specific tool filtering", () => {
workspaceAccess: "none", workspaceAccess: "none",
containerName: "test-container", containerName: "test-container",
containerWorkdir: "/workspace", containerWorkdir: "/workspace",
docker: {} as any, docker: {
image: "test-image",
containerPrefix: "test-",
workdir: "/workspace",
readOnlyRoot: true,
tmpfs: [],
network: "none",
capDrop: [],
} satisfies SandboxDockerConfig,
tools: { tools: {
allow: ["read", "write", "bash"], allow: ["read", "write", "bash"],
deny: [], deny: [],

View File

@@ -596,7 +596,7 @@ export function createClawdbotCodingTools(options?: {
options.config.agent.tools.deny?.length) options.config.agent.tools.deny?.length)
? filterToolsByPolicy(filtered, options.config.agent.tools) ? filterToolsByPolicy(filtered, options.config.agent.tools)
: filtered; : filtered;
// Agent-specific tool policy // Agent-specific tool policy
let agentFiltered = globallyFiltered; let agentFiltered = globallyFiltered;
if (options?.sessionKey && options?.config) { if (options?.sessionKey && options?.config) {
@@ -606,7 +606,7 @@ export function createClawdbotCodingTools(options?: {
agentFiltered = filterToolsByPolicy(globallyFiltered, agentConfig.tools); agentFiltered = filterToolsByPolicy(globallyFiltered, agentConfig.tools);
} }
} }
const sandboxed = sandbox const sandboxed = sandbox
? filterToolsByPolicy(agentFiltered, sandbox.tools) ? filterToolsByPolicy(agentFiltered, sandbox.tools)
: agentFiltered; : agentFiltered;

View File

@@ -1,13 +1,33 @@
import { describe, expect, it } from "vitest"; import { EventEmitter } from "node:events";
import { Readable } from "node:stream";
import { describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
// We need to test the internal defaultSandboxConfig function, but it's not exported. // We need to test the internal defaultSandboxConfig function, but it's not exported.
// Instead, we test the behavior through resolveSandboxContext which uses it. // Instead, we test the behavior through resolveSandboxContext which uses it.
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: () => {
const child = new EventEmitter() as {
stdout?: Readable;
stderr?: Readable;
on: (event: string, cb: (...args: unknown[]) => void) => void;
};
child.stdout = new Readable({ read() {} });
child.stderr = new Readable({ read() {} });
queueMicrotask(() => child.emit("close", 0));
return child;
},
};
});
describe("Agent-specific sandbox config", () => { describe("Agent-specific sandbox config", () => {
it("should use global sandbox config when no agent-specific config exists", async () => { it("should use global sandbox config when no agent-specific config exists", async () => {
const { resolveSandboxContext } = await import("./sandbox.js"); const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
agent: { agent: {
sandbox: { sandbox: {
@@ -36,7 +56,7 @@ describe("Agent-specific sandbox config", () => {
it("should override with agent-specific sandbox mode 'off'", async () => { it("should override with agent-specific sandbox mode 'off'", async () => {
const { resolveSandboxContext } = await import("./sandbox.js"); const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
agent: { agent: {
sandbox: { sandbox: {
@@ -68,7 +88,7 @@ describe("Agent-specific sandbox config", () => {
it("should use agent-specific sandbox mode 'all'", async () => { it("should use agent-specific sandbox mode 'all'", async () => {
const { resolveSandboxContext } = await import("./sandbox.js"); const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
agent: { agent: {
sandbox: { sandbox: {
@@ -100,7 +120,7 @@ describe("Agent-specific sandbox config", () => {
it("should use agent-specific scope", async () => { it("should use agent-specific scope", async () => {
const { resolveSandboxContext } = await import("./sandbox.js"); const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
agent: { agent: {
sandbox: { sandbox: {
@@ -134,7 +154,7 @@ describe("Agent-specific sandbox config", () => {
it("should use agent-specific workspaceRoot", async () => { it("should use agent-specific workspaceRoot", async () => {
const { resolveSandboxContext } = await import("./sandbox.js"); const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
agent: { agent: {
sandbox: { sandbox: {
@@ -169,7 +189,7 @@ describe("Agent-specific sandbox config", () => {
it("should prefer agent config over global for multiple agents", async () => { it("should prefer agent config over global for multiple agents", async () => {
const { resolveSandboxContext } = await import("./sandbox.js"); const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
agent: { agent: {
sandbox: { sandbox: {
@@ -213,4 +233,48 @@ describe("Agent-specific sandbox config", () => {
expect(familyContext).toBeDefined(); expect(familyContext).toBeDefined();
expect(familyContext?.enabled).toBe(true); expect(familyContext?.enabled).toBe(true);
}); });
it("should prefer agent-specific sandbox tool policy", async () => {
const { resolveSandboxContext } = await import("./sandbox.js");
const cfg: ClawdbotConfig = {
agent: {
sandbox: {
mode: "all",
scope: "agent",
tools: {
allow: ["read"],
deny: ["bash"],
},
},
},
routing: {
agents: {
restricted: {
workspace: "~/clawd-restricted",
sandbox: {
mode: "all",
scope: "agent",
tools: {
allow: ["read", "write"],
deny: ["edit"],
},
},
},
},
},
};
const context = await resolveSandboxContext({
config: cfg,
sessionKey: "agent:restricted:main",
workspaceDir: "/tmp/test-restricted",
});
expect(context).toBeDefined();
expect(context?.tools).toEqual({
allow: ["read", "write"],
deny: ["edit"],
});
});
}); });

View File

@@ -226,9 +226,12 @@ function resolveSandboxScopeKey(scope: SandboxScope, sessionKey: string) {
return `agent:${agentId}`; return `agent:${agentId}`;
} }
function defaultSandboxConfig(cfg?: ClawdbotConfig, agentId?: string): SandboxConfig { function defaultSandboxConfig(
cfg?: ClawdbotConfig,
agentId?: string,
): SandboxConfig {
const agent = cfg?.agent?.sandbox; const agent = cfg?.agent?.sandbox;
// Agent-specific sandbox config overrides global // Agent-specific sandbox config overrides global
let agentSandbox: typeof agent | undefined; let agentSandbox: typeof agent | undefined;
if (agentId && cfg?.routing?.agents) { if (agentId && cfg?.routing?.agents) {
@@ -237,15 +240,19 @@ function defaultSandboxConfig(cfg?: ClawdbotConfig, agentId?: string): SandboxCo
agentSandbox = agentConfig.sandbox; agentSandbox = agentConfig.sandbox;
} }
} }
return { return {
mode: agentSandbox?.mode ?? agent?.mode ?? "off", mode: agentSandbox?.mode ?? agent?.mode ?? "off",
scope: resolveSandboxScope({ scope: resolveSandboxScope({
scope: agentSandbox?.scope ?? agent?.scope, scope: agentSandbox?.scope ?? agent?.scope,
perSession: agentSandbox?.perSession ?? agent?.perSession, perSession: agentSandbox?.perSession ?? agent?.perSession,
}), }),
workspaceAccess: agentSandbox?.workspaceAccess ?? agent?.workspaceAccess ?? "none", workspaceAccess:
workspaceRoot: agentSandbox?.workspaceRoot ?? agent?.workspaceRoot ?? DEFAULT_SANDBOX_WORKSPACE_ROOT, agentSandbox?.workspaceAccess ?? agent?.workspaceAccess ?? "none",
workspaceRoot:
agentSandbox?.workspaceRoot ??
agent?.workspaceRoot ??
DEFAULT_SANDBOX_WORKSPACE_ROOT,
docker: { docker: {
image: agent?.docker?.image ?? DEFAULT_SANDBOX_IMAGE, image: agent?.docker?.image ?? DEFAULT_SANDBOX_IMAGE,
containerPrefix: containerPrefix:
@@ -281,8 +288,10 @@ function defaultSandboxConfig(cfg?: ClawdbotConfig, agentId?: string): SandboxCo
enableNoVnc: agent?.browser?.enableNoVnc ?? true, enableNoVnc: agent?.browser?.enableNoVnc ?? true,
}, },
tools: { tools: {
allow: agent?.tools?.allow ?? DEFAULT_TOOL_ALLOW, allow:
deny: agent?.tools?.deny ?? DEFAULT_TOOL_DENY, agentSandbox?.tools?.allow ?? agent?.tools?.allow ?? DEFAULT_TOOL_ALLOW,
deny:
agentSandbox?.tools?.deny ?? agent?.tools?.deny ?? DEFAULT_TOOL_DENY,
}, },
prune: { prune: {
idleHours: agent?.prune?.idleHours ?? DEFAULT_SANDBOX_IDLE_HOURS, idleHours: agent?.prune?.idleHours ?? DEFAULT_SANDBOX_IDLE_HOURS,

View File

@@ -586,11 +586,18 @@ export type RoutingConfig = {
model?: string; model?: string;
sandbox?: { sandbox?: {
mode?: "off" | "non-main" | "all"; mode?: "off" | "non-main" | "all";
/** Agent workspace access inside the sandbox. */
workspaceAccess?: "none" | "ro" | "rw";
/** Container/workspace scope for sandbox isolation. */ /** Container/workspace scope for sandbox isolation. */
scope?: "session" | "agent" | "shared"; scope?: "session" | "agent" | "shared";
/** Legacy alias for scope ("session" when true, "shared" when false). */ /** Legacy alias for scope ("session" when true, "shared" when false). */
perSession?: boolean; perSession?: boolean;
workspaceRoot?: string; workspaceRoot?: string;
/** Tool allow/deny policy for sandboxed sessions (deny wins). */
tools?: {
allow?: string[];
deny?: string[];
};
}; };
tools?: { tools?: {
allow?: string[]; allow?: string[];

View File

@@ -236,6 +236,9 @@ const RoutingSchema = z
z.literal("all"), z.literal("all"),
]) ])
.optional(), .optional(),
workspaceAccess: z
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
.optional(),
scope: z scope: z
.union([ .union([
z.literal("session"), z.literal("session"),
@@ -245,6 +248,12 @@ const RoutingSchema = z
.optional(), .optional(),
perSession: z.boolean().optional(), perSession: z.boolean().optional(),
workspaceRoot: z.string().optional(), workspaceRoot: z.string().optional(),
tools: z
.object({
allow: z.array(z.string()).optional(),
deny: z.array(z.string()).optional(),
})
.optional(),
}) })
.optional(), .optional(),
tools: z tools: z