feat(sandbox): per-agent docker setupCommand
This commit is contained in:
@@ -30,6 +30,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"`).
|
||||||
- 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.
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio
|
|||||||
sandbox: {
|
sandbox: {
|
||||||
mode: "all", // Always sandboxed
|
mode: "all", // Always sandboxed
|
||||||
scope: "agent", // One container per agent
|
scope: "agent", // One container per agent
|
||||||
|
docker: {
|
||||||
|
// Optional one-time setup after container creation
|
||||||
|
setupCommand: "apt-get update && apt-get install -y git curl",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"], // Only read tool
|
allow: ["read"], // Only read tool
|
||||||
|
|||||||
@@ -339,6 +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"`)
|
||||||
- `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
|
||||||
@@ -1115,6 +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
|
||||||
pidsLimit: 256,
|
pidsLimit: 256,
|
||||||
memory: "1g",
|
memory: "1g",
|
||||||
memorySwap: "2g",
|
memorySwap: "2g",
|
||||||
|
|||||||
@@ -160,6 +160,9 @@ 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`
|
||||||
|
(ignored when `agent.sandbox.scope` / `routing.agents.<agentId>.sandbox.scope` is `"shared"`).
|
||||||
|
|
||||||
### Build the default sandbox image
|
### Build the default sandbox image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -54,6 +54,85 @@ describe("Agent-specific sandbox config", () => {
|
|||||||
expect(context?.enabled).toBe(true);
|
expect(context?.enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow agent-specific docker setupCommand overrides", async () => {
|
||||||
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agent: {
|
||||||
|
sandbox: {
|
||||||
|
mode: "all",
|
||||||
|
scope: "agent",
|
||||||
|
docker: {
|
||||||
|
setupCommand: "echo global",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
agents: {
|
||||||
|
work: {
|
||||||
|
workspace: "~/clawd-work",
|
||||||
|
sandbox: {
|
||||||
|
mode: "all",
|
||||||
|
scope: "agent",
|
||||||
|
docker: {
|
||||||
|
setupCommand: "echo work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = await resolveSandboxContext({
|
||||||
|
config: cfg,
|
||||||
|
sessionKey: "agent:work:main",
|
||||||
|
workspaceDir: "/tmp/test-work",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context).toBeDefined();
|
||||||
|
expect(context?.docker.setupCommand).toBe("echo work");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore agent-specific docker overrides when scope is shared", async () => {
|
||||||
|
const { resolveSandboxContext } = await import("./sandbox.js");
|
||||||
|
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agent: {
|
||||||
|
sandbox: {
|
||||||
|
mode: "all",
|
||||||
|
scope: "shared",
|
||||||
|
docker: {
|
||||||
|
setupCommand: "echo global",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
agents: {
|
||||||
|
work: {
|
||||||
|
workspace: "~/clawd-work",
|
||||||
|
sandbox: {
|
||||||
|
mode: "all",
|
||||||
|
scope: "shared",
|
||||||
|
docker: {
|
||||||
|
setupCommand: "echo work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = await resolveSandboxContext({
|
||||||
|
config: cfg,
|
||||||
|
sessionKey: "agent:work:main",
|
||||||
|
workspaceDir: "/tmp/test-work",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context).toBeDefined();
|
||||||
|
expect(context?.docker.setupCommand).toBe("echo global");
|
||||||
|
expect(context?.containerName).toContain("shared");
|
||||||
|
});
|
||||||
|
|
||||||
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");
|
||||||
|
|
||||||
|
|||||||
@@ -241,12 +241,14 @@ function defaultSandboxConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scope = resolveSandboxScope({
|
||||||
|
scope: agentSandbox?.scope ?? agent?.scope,
|
||||||
|
perSession: agentSandbox?.perSession ?? agent?.perSession,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
|
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
|
||||||
scope: resolveSandboxScope({
|
scope,
|
||||||
scope: agentSandbox?.scope ?? agent?.scope,
|
|
||||||
perSession: agentSandbox?.perSession ?? agent?.perSession,
|
|
||||||
}),
|
|
||||||
workspaceAccess:
|
workspaceAccess:
|
||||||
agentSandbox?.workspaceAccess ?? agent?.workspaceAccess ?? "none",
|
agentSandbox?.workspaceAccess ?? agent?.workspaceAccess ?? "none",
|
||||||
workspaceRoot:
|
workspaceRoot:
|
||||||
@@ -264,7 +266,10 @@ function defaultSandboxConfig(
|
|||||||
user: agent?.docker?.user,
|
user: agent?.docker?.user,
|
||||||
capDrop: agent?.docker?.capDrop ?? ["ALL"],
|
capDrop: agent?.docker?.capDrop ?? ["ALL"],
|
||||||
env: agent?.docker?.env ?? { LANG: "C.UTF-8" },
|
env: agent?.docker?.env ?? { LANG: "C.UTF-8" },
|
||||||
setupCommand: agent?.docker?.setupCommand,
|
setupCommand:
|
||||||
|
scope === "shared"
|
||||||
|
? agent?.docker?.setupCommand
|
||||||
|
: (agentSandbox?.docker?.setupCommand ?? agent?.docker?.setupCommand),
|
||||||
pidsLimit: agent?.docker?.pidsLimit,
|
pidsLimit: agent?.docker?.pidsLimit,
|
||||||
memory: agent?.docker?.memory,
|
memory: agent?.docker?.memory,
|
||||||
memorySwap: agent?.docker?.memorySwap,
|
memorySwap: agent?.docker?.memorySwap,
|
||||||
|
|||||||
@@ -617,6 +617,11 @@ export type RoutingConfig = {
|
|||||||
/** 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;
|
||||||
|
/** Docker-specific sandbox overrides for this agent. */
|
||||||
|
docker?: {
|
||||||
|
/** 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[];
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ const RoutingSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
perSession: z.boolean().optional(),
|
perSession: z.boolean().optional(),
|
||||||
workspaceRoot: z.string().optional(),
|
workspaceRoot: z.string().optional(),
|
||||||
|
docker: z
|
||||||
|
.object({
|
||||||
|
setupCommand: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
tools: z
|
tools: z
|
||||||
.object({
|
.object({
|
||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user