feat(sandbox): per-agent docker overrides
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
- 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.
|
- 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.
|
||||||
- Sandbox: allow per-agent `routing.agents.<agentId>.sandbox.docker.setupCommand` overrides for multi-agent gateways (ignored when `scope: "shared"`).
|
- Sandbox: allow per-agent `routing.agents.<agentId>.sandbox.docker.*` overrides for multi-agent gateways (ignored when `scope: "shared"`).
|
||||||
- Tools: make per-agent tool policies override global defaults and run bash synchronously when `process` is disallowed.
|
- Tools: make per-agent tool policies override global defaults and run bash synchronously when `process` is disallowed.
|
||||||
- Tools: scope `process` sessions per agent to prevent cross-agent visibility.
|
- Tools: scope `process` sessions per agent to prevent cross-agent visibility.
|
||||||
- Cron: clamp timer delay to avoid TimeoutOverflowWarning. Thanks @emanuelst for PR #412.
|
- Cron: clamp timer delay to avoid TimeoutOverflowWarning. Thanks @emanuelst for PR #412.
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside o
|
|||||||
- `workspaceAccess`: `"none"` | `"ro"` | `"rw"`
|
- `workspaceAccess`: `"none"` | `"ro"` | `"rw"`
|
||||||
- `scope`: `"session"` | `"agent"` | `"shared"`
|
- `scope`: `"session"` | `"agent"` | `"shared"`
|
||||||
- `workspaceRoot`: custom sandbox workspace root
|
- `workspaceRoot`: custom sandbox workspace root
|
||||||
- `docker.setupCommand`: optional one-time setup command (runs once after container creation; ignored when `scope: "shared"`)
|
- `docker`: per-agent docker overrides (e.g. `image`, `network`, `env`, `setupCommand`, limits; ignored when `scope: "shared"`)
|
||||||
- `tools`: per-agent sandbox tool policy (deny wins; overrides `agent.sandbox.tools`)
|
- `tools`: per-agent sandbox tool policy (deny wins; overrides `agent.sandbox.tools`)
|
||||||
- `tools`: per-agent tool restrictions (overrides `agent.tools`; applied before sandbox tool policy).
|
- `tools`: per-agent tool restrictions (overrides `agent.tools`; applied before sandbox tool policy).
|
||||||
- `allow`: array of allowed tool names
|
- `allow`: array of allowed tool names
|
||||||
@@ -1116,7 +1116,7 @@ Legacy: `perSession` is still supported (`true` → `scope: "session"`,
|
|||||||
capDrop: ["ALL"],
|
capDrop: ["ALL"],
|
||||||
env: { LANG: "C.UTF-8" },
|
env: { LANG: "C.UTF-8" },
|
||||||
setupCommand: "apt-get update && apt-get install -y git curl jq",
|
setupCommand: "apt-get update && apt-get install -y git curl jq",
|
||||||
// Per-agent override (multi-agent): routing.agents.<agentId>.sandbox.docker.setupCommand
|
// Per-agent override (multi-agent): routing.agents.<agentId>.sandbox.docker.*
|
||||||
pidsLimit: 256,
|
pidsLimit: 256,
|
||||||
memory: "1g",
|
memory: "1g",
|
||||||
memorySwap: "2g",
|
memorySwap: "2g",
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ Hardening knobs live under `agent.sandbox.docker`:
|
|||||||
`network`, `user`, `pidsLimit`, `memory`, `memorySwap`, `cpus`, `ulimits`,
|
`network`, `user`, `pidsLimit`, `memory`, `memorySwap`, `cpus`, `ulimits`,
|
||||||
`seccompProfile`, `apparmorProfile`, `dns`, `extraHosts`.
|
`seccompProfile`, `apparmorProfile`, `dns`, `extraHosts`.
|
||||||
|
|
||||||
Multi-agent: override `setupCommand` per agent via `routing.agents.<agentId>.sandbox.docker.setupCommand`
|
Multi-agent: override `agent.sandbox.docker.*` per agent via `routing.agents.<agentId>.sandbox.docker.*`
|
||||||
(ignored when `agent.sandbox.scope` / `routing.agents.<agentId>.sandbox.scope` is `"shared"`).
|
(ignored when `agent.sandbox.scope` / `routing.agents.<agentId>.sandbox.scope` is `"shared"`).
|
||||||
|
|
||||||
### Build the default sandbox image
|
### Build the default sandbox image
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
---
|
||||||
|
summary: "Per-agent sandbox + tool restrictions, precedence, and examples"
|
||||||
|
title: Multi-Agent Sandbox & Tools
|
||||||
|
read_when: "You want per-agent sandboxing or per-agent tool allow/deny policies in a multi-agent gateway."
|
||||||
|
status: active
|
||||||
|
---
|
||||||
|
|
||||||
# Multi-Agent Sandbox & Tools Configuration
|
# Multi-Agent Sandbox & Tools Configuration
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -142,9 +149,12 @@ 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
|
routing.agents[id].sandbox.workspaceAccess > agent.sandbox.workspaceAccess
|
||||||
|
routing.agents[id].sandbox.docker.* > agent.sandbox.docker.*
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** `docker`, `browser`, and `prune` settings from `agent.sandbox` are still **global** and apply to all sandboxed agents.
|
**Notes:**
|
||||||
|
- `routing.agents[id].sandbox.docker.*` overrides `agent.sandbox.docker.*` for that agent (ignored when sandbox scope resolves to `"shared"`).
|
||||||
|
- `browser` and `prune` settings under `agent.sandbox` are still **global** and apply to all sandboxed agents.
|
||||||
|
|
||||||
### Tool Restrictions
|
### Tool Restrictions
|
||||||
The filtering order is:
|
The filtering order is:
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { beforeEach, 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.
|
||||||
|
|
||||||
|
type SpawnCall = {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const spawnCalls: SpawnCall[] = [];
|
||||||
|
|
||||||
vi.mock("node:child_process", async (importOriginal) => {
|
vi.mock("node:child_process", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
spawn: () => {
|
spawn: (command: string, args: string[]) => {
|
||||||
|
spawnCalls.push({ command, args });
|
||||||
const child = new EventEmitter() as {
|
const child = new EventEmitter() as {
|
||||||
stdout?: Readable;
|
stdout?: Readable;
|
||||||
stderr?: Readable;
|
stderr?: Readable;
|
||||||
@@ -18,13 +26,31 @@ vi.mock("node:child_process", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
child.stdout = new Readable({ read() {} });
|
child.stdout = new Readable({ read() {} });
|
||||||
child.stderr = new Readable({ read() {} });
|
child.stderr = new Readable({ read() {} });
|
||||||
|
|
||||||
|
const dockerArgs = command === "docker" ? args : [];
|
||||||
|
const shouldFailContainerInspect =
|
||||||
|
dockerArgs[0] === "inspect" &&
|
||||||
|
dockerArgs[1] === "-f" &&
|
||||||
|
dockerArgs[2] === "{{.State.Running}}";
|
||||||
|
const shouldSucceedImageInspect =
|
||||||
|
dockerArgs[0] === "image" && dockerArgs[1] === "inspect";
|
||||||
|
|
||||||
|
const code = shouldFailContainerInspect ? 1 : 0;
|
||||||
|
if (shouldSucceedImageInspect) {
|
||||||
queueMicrotask(() => child.emit("close", 0));
|
queueMicrotask(() => child.emit("close", 0));
|
||||||
|
} else {
|
||||||
|
queueMicrotask(() => child.emit("close", code));
|
||||||
|
}
|
||||||
return child;
|
return child;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Agent-specific sandbox config", () => {
|
describe("Agent-specific sandbox config", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spawnCalls.length = 0;
|
||||||
|
});
|
||||||
|
|
||||||
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");
|
||||||
|
|
||||||
@@ -91,6 +117,15 @@ describe("Agent-specific sandbox config", () => {
|
|||||||
|
|
||||||
expect(context).toBeDefined();
|
expect(context).toBeDefined();
|
||||||
expect(context?.docker.setupCommand).toBe("echo work");
|
expect(context?.docker.setupCommand).toBe("echo work");
|
||||||
|
expect(
|
||||||
|
spawnCalls.some(
|
||||||
|
(call) =>
|
||||||
|
call.command === "docker" &&
|
||||||
|
call.args[0] === "exec" &&
|
||||||
|
call.args.includes("-lc") &&
|
||||||
|
call.args.includes("echo work"),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should ignore agent-specific docker overrides when scope is shared", async () => {
|
it("should ignore agent-specific docker overrides when scope is shared", async () => {
|
||||||
@@ -131,6 +166,57 @@ describe("Agent-specific sandbox config", () => {
|
|||||||
expect(context).toBeDefined();
|
expect(context).toBeDefined();
|
||||||
expect(context?.docker.setupCommand).toBe("echo global");
|
expect(context?.docker.setupCommand).toBe("echo global");
|
||||||
expect(context?.containerName).toContain("shared");
|
expect(context?.containerName).toContain("shared");
|
||||||
|
expect(
|
||||||
|
spawnCalls.some(
|
||||||
|
(call) =>
|
||||||
|
call.command === "docker" &&
|
||||||
|
call.args[0] === "exec" &&
|
||||||
|
call.args.includes("-lc") &&
|
||||||
|
call.args.includes("echo global"),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow agent-specific docker settings beyond setupCommand", async () => {
|
||||||
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agent: {
|
||||||
|
sandbox: {
|
||||||
|
mode: "all",
|
||||||
|
scope: "agent",
|
||||||
|
docker: {
|
||||||
|
image: "global-image",
|
||||||
|
network: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
agents: {
|
||||||
|
work: {
|
||||||
|
workspace: "~/clawd-work",
|
||||||
|
sandbox: {
|
||||||
|
mode: "all",
|
||||||
|
scope: "agent",
|
||||||
|
docker: {
|
||||||
|
image: "work-image",
|
||||||
|
network: "bridge",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = await resolveSandboxContext({
|
||||||
|
config: cfg,
|
||||||
|
sessionKey: "agent:work:main",
|
||||||
|
workspaceDir: "/tmp/test-work",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context).toBeDefined();
|
||||||
|
expect(context?.docker.image).toBe("work-image");
|
||||||
|
expect(context?.docker.network).toBe("bridge");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should override with agent-specific sandbox mode 'off'", async () => {
|
it("should override with agent-specific sandbox mode 'off'", async () => {
|
||||||
|
|||||||
@@ -246,6 +246,9 @@ function defaultSandboxConfig(
|
|||||||
perSession: agentSandbox?.perSession ?? agent?.perSession,
|
perSession: agentSandbox?.perSession ?? agent?.perSession,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const globalDocker = agent?.docker;
|
||||||
|
const agentDocker = scope === "shared" ? undefined : agentSandbox?.docker;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
|
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
|
||||||
scope,
|
scope,
|
||||||
@@ -256,29 +259,39 @@ function defaultSandboxConfig(
|
|||||||
agent?.workspaceRoot ??
|
agent?.workspaceRoot ??
|
||||||
DEFAULT_SANDBOX_WORKSPACE_ROOT,
|
DEFAULT_SANDBOX_WORKSPACE_ROOT,
|
||||||
docker: {
|
docker: {
|
||||||
image: agent?.docker?.image ?? DEFAULT_SANDBOX_IMAGE,
|
image: agentDocker?.image ?? globalDocker?.image ?? DEFAULT_SANDBOX_IMAGE,
|
||||||
containerPrefix:
|
containerPrefix:
|
||||||
agent?.docker?.containerPrefix ?? DEFAULT_SANDBOX_CONTAINER_PREFIX,
|
agentDocker?.containerPrefix ??
|
||||||
workdir: agent?.docker?.workdir ?? DEFAULT_SANDBOX_WORKDIR,
|
globalDocker?.containerPrefix ??
|
||||||
readOnlyRoot: agent?.docker?.readOnlyRoot ?? true,
|
DEFAULT_SANDBOX_CONTAINER_PREFIX,
|
||||||
tmpfs: agent?.docker?.tmpfs ?? ["/tmp", "/var/tmp", "/run"],
|
workdir:
|
||||||
network: agent?.docker?.network ?? "none",
|
agentDocker?.workdir ??
|
||||||
user: agent?.docker?.user,
|
globalDocker?.workdir ??
|
||||||
capDrop: agent?.docker?.capDrop ?? ["ALL"],
|
DEFAULT_SANDBOX_WORKDIR,
|
||||||
env: agent?.docker?.env ?? { LANG: "C.UTF-8" },
|
readOnlyRoot:
|
||||||
setupCommand:
|
agentDocker?.readOnlyRoot ?? globalDocker?.readOnlyRoot ?? true,
|
||||||
scope === "shared"
|
tmpfs: agentDocker?.tmpfs ??
|
||||||
? agent?.docker?.setupCommand
|
globalDocker?.tmpfs ?? ["/tmp", "/var/tmp", "/run"],
|
||||||
: (agentSandbox?.docker?.setupCommand ?? agent?.docker?.setupCommand),
|
network: agentDocker?.network ?? globalDocker?.network ?? "none",
|
||||||
pidsLimit: agent?.docker?.pidsLimit,
|
user: agentDocker?.user ?? globalDocker?.user,
|
||||||
memory: agent?.docker?.memory,
|
capDrop: agentDocker?.capDrop ?? globalDocker?.capDrop ?? ["ALL"],
|
||||||
memorySwap: agent?.docker?.memorySwap,
|
env: agentDocker?.env
|
||||||
cpus: agent?.docker?.cpus,
|
? { ...(globalDocker?.env ?? { LANG: "C.UTF-8" }), ...agentDocker.env }
|
||||||
ulimits: agent?.docker?.ulimits,
|
: (globalDocker?.env ?? { LANG: "C.UTF-8" }),
|
||||||
seccompProfile: agent?.docker?.seccompProfile,
|
setupCommand: agentDocker?.setupCommand ?? globalDocker?.setupCommand,
|
||||||
apparmorProfile: agent?.docker?.apparmorProfile,
|
pidsLimit: agentDocker?.pidsLimit ?? globalDocker?.pidsLimit,
|
||||||
dns: agent?.docker?.dns,
|
memory: agentDocker?.memory ?? globalDocker?.memory,
|
||||||
extraHosts: agent?.docker?.extraHosts,
|
memorySwap: agentDocker?.memorySwap ?? globalDocker?.memorySwap,
|
||||||
|
cpus: agentDocker?.cpus ?? globalDocker?.cpus,
|
||||||
|
ulimits: agentDocker?.ulimits
|
||||||
|
? { ...globalDocker?.ulimits, ...agentDocker.ulimits }
|
||||||
|
: globalDocker?.ulimits,
|
||||||
|
seccompProfile:
|
||||||
|
agentDocker?.seccompProfile ?? globalDocker?.seccompProfile,
|
||||||
|
apparmorProfile:
|
||||||
|
agentDocker?.apparmorProfile ?? globalDocker?.apparmorProfile,
|
||||||
|
dns: agentDocker?.dns ?? globalDocker?.dns,
|
||||||
|
extraHosts: agentDocker?.extraHosts ?? globalDocker?.extraHosts,
|
||||||
},
|
},
|
||||||
browser: {
|
browser: {
|
||||||
enabled: agent?.browser?.enabled ?? false,
|
enabled: agent?.browser?.enabled ?? false,
|
||||||
|
|||||||
@@ -63,6 +63,50 @@ function resolveMode(cfg: ClawdbotConfig): "local" | "remote" {
|
|||||||
return cfg.gateway?.mode === "remote" ? "remote" : "local";
|
return cfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SandboxScope = "session" | "agent" | "shared";
|
||||||
|
|
||||||
|
function resolveSandboxScope(params: {
|
||||||
|
scope?: SandboxScope;
|
||||||
|
perSession?: boolean;
|
||||||
|
}): SandboxScope {
|
||||||
|
if (params.scope) return params.scope;
|
||||||
|
if (typeof params.perSession === "boolean") {
|
||||||
|
return params.perSession ? "session" : "shared";
|
||||||
|
}
|
||||||
|
return "agent";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDockerOverrides(docker?: unknown) {
|
||||||
|
if (!docker || typeof docker !== "object") return false;
|
||||||
|
return Object.values(docker).some((value) => value !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSandboxSharedDockerOverrideWarnings(cfg: ClawdbotConfig) {
|
||||||
|
const globalSandbox = cfg.agent?.sandbox;
|
||||||
|
const agents = cfg.routing?.agents;
|
||||||
|
if (!agents) return [];
|
||||||
|
|
||||||
|
const warnings: string[] = [];
|
||||||
|
for (const [agentId, agentCfg] of Object.entries(agents)) {
|
||||||
|
if (!agentCfg || typeof agentCfg !== "object") continue;
|
||||||
|
const agentSandbox = agentCfg.sandbox;
|
||||||
|
if (!agentSandbox || typeof agentSandbox !== "object") continue;
|
||||||
|
if (!hasDockerOverrides(agentSandbox.docker)) continue;
|
||||||
|
|
||||||
|
const scope = resolveSandboxScope({
|
||||||
|
scope: (agentSandbox.scope ?? globalSandbox?.scope) as SandboxScope,
|
||||||
|
perSession: agentSandbox.perSession ?? globalSandbox?.perSession,
|
||||||
|
});
|
||||||
|
if (scope !== "shared") continue;
|
||||||
|
|
||||||
|
warnings.push(
|
||||||
|
`- routing.agents.${agentId}.sandbox.docker.* is ignored when sandbox scope resolves to "shared" (single shared container).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveLegacyConfigPath(env: NodeJS.ProcessEnv): string {
|
function resolveLegacyConfigPath(env: NodeJS.ProcessEnv): string {
|
||||||
const override = env.CLAWDIS_CONFIG_PATH?.trim();
|
const override = env.CLAWDIS_CONFIG_PATH?.trim();
|
||||||
if (override) return override;
|
if (override) return override;
|
||||||
@@ -976,6 +1020,19 @@ export async function doctorCommand(
|
|||||||
|
|
||||||
await noteSecurityWarnings(cfg);
|
await noteSecurityWarnings(cfg);
|
||||||
|
|
||||||
|
const sharedDockerOverrideWarnings =
|
||||||
|
collectSandboxSharedDockerOverrideWarnings(cfg);
|
||||||
|
if (sharedDockerOverrideWarnings.length > 0) {
|
||||||
|
note(
|
||||||
|
[
|
||||||
|
...sharedDockerOverrideWarnings,
|
||||||
|
"",
|
||||||
|
'Fix: set scope to "agent"/"session", or move the docker config to agent.sandbox.docker (global).',
|
||||||
|
].join("\n"),
|
||||||
|
"Sandbox",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
options.nonInteractive !== true &&
|
options.nonInteractive !== true &&
|
||||||
process.platform === "linux" &&
|
process.platform === "linux" &&
|
||||||
|
|||||||
@@ -581,6 +581,50 @@ export type QueueModeByProvider = {
|
|||||||
webchat?: QueueMode;
|
webchat?: QueueMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SandboxDockerSettings = {
|
||||||
|
/** Docker image to use for sandbox containers. */
|
||||||
|
image?: string;
|
||||||
|
/** Prefix for sandbox container names. */
|
||||||
|
containerPrefix?: string;
|
||||||
|
/** Container workdir mount path (default: /workspace). */
|
||||||
|
workdir?: string;
|
||||||
|
/** Run container rootfs read-only. */
|
||||||
|
readOnlyRoot?: boolean;
|
||||||
|
/** Extra tmpfs mounts for read-only containers. */
|
||||||
|
tmpfs?: string[];
|
||||||
|
/** Container network mode (bridge|none|custom). */
|
||||||
|
network?: string;
|
||||||
|
/** Container user (uid:gid). */
|
||||||
|
user?: string;
|
||||||
|
/** Drop Linux capabilities. */
|
||||||
|
capDrop?: string[];
|
||||||
|
/** Extra environment variables for sandbox exec. */
|
||||||
|
env?: Record<string, string>;
|
||||||
|
/** Optional setup command run once after container creation. */
|
||||||
|
setupCommand?: string;
|
||||||
|
/** Limit container PIDs (0 = Docker default). */
|
||||||
|
pidsLimit?: number;
|
||||||
|
/** Limit container memory (e.g. 512m, 2g, or bytes as number). */
|
||||||
|
memory?: string | number;
|
||||||
|
/** Limit container memory swap (same format as memory). */
|
||||||
|
memorySwap?: string | number;
|
||||||
|
/** Limit container CPU shares (e.g. 0.5, 1, 2). */
|
||||||
|
cpus?: number;
|
||||||
|
/**
|
||||||
|
* Set ulimit values by name (e.g. nofile, nproc).
|
||||||
|
* Use "soft:hard" string, a number, or { soft, hard }.
|
||||||
|
*/
|
||||||
|
ulimits?: Record<string, string | number | { soft?: number; hard?: number }>;
|
||||||
|
/** Seccomp profile (path or profile name). */
|
||||||
|
seccompProfile?: string;
|
||||||
|
/** AppArmor profile name. */
|
||||||
|
apparmorProfile?: string;
|
||||||
|
/** DNS servers (e.g. ["1.1.1.1", "8.8.8.8"]). */
|
||||||
|
dns?: string[];
|
||||||
|
/** Extra host mappings (e.g. ["api.local:10.0.0.2"]). */
|
||||||
|
extraHosts?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type GroupChatConfig = {
|
export type GroupChatConfig = {
|
||||||
mentionPatterns?: string[];
|
mentionPatterns?: string[];
|
||||||
historyLimit?: number;
|
historyLimit?: number;
|
||||||
@@ -618,10 +662,7 @@ export type RoutingConfig = {
|
|||||||
perSession?: boolean;
|
perSession?: boolean;
|
||||||
workspaceRoot?: string;
|
workspaceRoot?: string;
|
||||||
/** Docker-specific sandbox overrides for this agent. */
|
/** Docker-specific sandbox overrides for this agent. */
|
||||||
docker?: {
|
docker?: SandboxDockerSettings;
|
||||||
/** Optional setup command run once after container creation. */
|
|
||||||
setupCommand?: string;
|
|
||||||
};
|
|
||||||
/** Tool allow/deny policy for sandboxed sessions (deny wins). */
|
/** Tool allow/deny policy for sandboxed sessions (deny wins). */
|
||||||
tools?: {
|
tools?: {
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
@@ -1050,52 +1091,7 @@ export type ClawdbotConfig = {
|
|||||||
/** Root directory for sandbox workspaces. */
|
/** Root directory for sandbox workspaces. */
|
||||||
workspaceRoot?: string;
|
workspaceRoot?: string;
|
||||||
/** Docker-specific sandbox settings. */
|
/** Docker-specific sandbox settings. */
|
||||||
docker?: {
|
docker?: SandboxDockerSettings;
|
||||||
/** Docker image to use for sandbox containers. */
|
|
||||||
image?: string;
|
|
||||||
/** Prefix for sandbox container names. */
|
|
||||||
containerPrefix?: string;
|
|
||||||
/** Container workdir mount path (default: /workspace). */
|
|
||||||
workdir?: string;
|
|
||||||
/** Run container rootfs read-only. */
|
|
||||||
readOnlyRoot?: boolean;
|
|
||||||
/** Extra tmpfs mounts for read-only containers. */
|
|
||||||
tmpfs?: string[];
|
|
||||||
/** Container network mode (bridge|none|custom). */
|
|
||||||
network?: string;
|
|
||||||
/** Container user (uid:gid). */
|
|
||||||
user?: string;
|
|
||||||
/** Drop Linux capabilities. */
|
|
||||||
capDrop?: string[];
|
|
||||||
/** Extra environment variables for sandbox exec. */
|
|
||||||
env?: Record<string, string>;
|
|
||||||
/** Optional setup command run once after container creation. */
|
|
||||||
setupCommand?: string;
|
|
||||||
/** Limit container PIDs (0 = Docker default). */
|
|
||||||
pidsLimit?: number;
|
|
||||||
/** Limit container memory (e.g. 512m, 2g, or bytes as number). */
|
|
||||||
memory?: string | number;
|
|
||||||
/** Limit container memory swap (same format as memory). */
|
|
||||||
memorySwap?: string | number;
|
|
||||||
/** Limit container CPU shares (e.g. 0.5, 1, 2). */
|
|
||||||
cpus?: number;
|
|
||||||
/**
|
|
||||||
* Set ulimit values by name (e.g. nofile, nproc).
|
|
||||||
* Use "soft:hard" string, a number, or { soft, hard }.
|
|
||||||
*/
|
|
||||||
ulimits?: Record<
|
|
||||||
string,
|
|
||||||
string | number | { soft?: number; hard?: number }
|
|
||||||
>;
|
|
||||||
/** Seccomp profile (path or profile name). */
|
|
||||||
seccompProfile?: string;
|
|
||||||
/** AppArmor profile name. */
|
|
||||||
apparmorProfile?: string;
|
|
||||||
/** DNS servers (e.g. ["1.1.1.1", "8.8.8.8"]). */
|
|
||||||
dns?: string[];
|
|
||||||
/** Extra host mappings (e.g. ["api.local:10.0.0.2"]). */
|
|
||||||
extraHosts?: string[];
|
|
||||||
};
|
|
||||||
/** Optional sandboxed browser settings. */
|
/** Optional sandboxed browser settings. */
|
||||||
browser?: {
|
browser?: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
|||||||
@@ -224,6 +224,42 @@ const HeartbeatSchema = z
|
|||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
const SandboxDockerSchema = z
|
||||||
|
.object({
|
||||||
|
image: z.string().optional(),
|
||||||
|
containerPrefix: z.string().optional(),
|
||||||
|
workdir: z.string().optional(),
|
||||||
|
readOnlyRoot: z.boolean().optional(),
|
||||||
|
tmpfs: z.array(z.string()).optional(),
|
||||||
|
network: z.string().optional(),
|
||||||
|
user: z.string().optional(),
|
||||||
|
capDrop: z.array(z.string()).optional(),
|
||||||
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
|
setupCommand: z.string().optional(),
|
||||||
|
pidsLimit: z.number().int().positive().optional(),
|
||||||
|
memory: z.union([z.string(), z.number()]).optional(),
|
||||||
|
memorySwap: z.union([z.string(), z.number()]).optional(),
|
||||||
|
cpus: z.number().positive().optional(),
|
||||||
|
ulimits: z
|
||||||
|
.record(
|
||||||
|
z.string(),
|
||||||
|
z.union([
|
||||||
|
z.string(),
|
||||||
|
z.number(),
|
||||||
|
z.object({
|
||||||
|
soft: z.number().int().nonnegative().optional(),
|
||||||
|
hard: z.number().int().nonnegative().optional(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
seccompProfile: z.string().optional(),
|
||||||
|
apparmorProfile: z.string().optional(),
|
||||||
|
dns: z.array(z.string()).optional(),
|
||||||
|
extraHosts: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
const RoutingSchema = z
|
const RoutingSchema = z
|
||||||
.object({
|
.object({
|
||||||
groupChat: GroupChatSchema,
|
groupChat: GroupChatSchema,
|
||||||
@@ -265,11 +301,7 @@ const RoutingSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
perSession: z.boolean().optional(),
|
perSession: z.boolean().optional(),
|
||||||
workspaceRoot: z.string().optional(),
|
workspaceRoot: z.string().optional(),
|
||||||
docker: z
|
docker: SandboxDockerSchema,
|
||||||
.object({
|
|
||||||
setupCommand: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
tools: z
|
tools: z
|
||||||
.object({
|
.object({
|
||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
@@ -673,41 +705,7 @@ export const ClawdbotSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
perSession: z.boolean().optional(),
|
perSession: z.boolean().optional(),
|
||||||
workspaceRoot: z.string().optional(),
|
workspaceRoot: z.string().optional(),
|
||||||
docker: z
|
docker: SandboxDockerSchema,
|
||||||
.object({
|
|
||||||
image: z.string().optional(),
|
|
||||||
containerPrefix: z.string().optional(),
|
|
||||||
workdir: z.string().optional(),
|
|
||||||
readOnlyRoot: z.boolean().optional(),
|
|
||||||
tmpfs: z.array(z.string()).optional(),
|
|
||||||
network: z.string().optional(),
|
|
||||||
user: z.string().optional(),
|
|
||||||
capDrop: z.array(z.string()).optional(),
|
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
|
||||||
setupCommand: z.string().optional(),
|
|
||||||
pidsLimit: z.number().int().positive().optional(),
|
|
||||||
memory: z.union([z.string(), z.number()]).optional(),
|
|
||||||
memorySwap: z.union([z.string(), z.number()]).optional(),
|
|
||||||
cpus: z.number().positive().optional(),
|
|
||||||
ulimits: z
|
|
||||||
.record(
|
|
||||||
z.string(),
|
|
||||||
z.union([
|
|
||||||
z.string(),
|
|
||||||
z.number(),
|
|
||||||
z.object({
|
|
||||||
soft: z.number().int().nonnegative().optional(),
|
|
||||||
hard: z.number().int().nonnegative().optional(),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
seccompProfile: z.string().optional(),
|
|
||||||
apparmorProfile: z.string().optional(),
|
|
||||||
dns: z.array(z.string()).optional(),
|
|
||||||
extraHosts: z.array(z.string()).optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
browser: z
|
browser: z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user