Add docker bind mounds for sandboxing

This commit is contained in:
Aaron Konyer
2026-01-12 10:13:32 -07:00
committed by Peter Steinberger
parent 5d83be76c9
commit 0b2b8c7c52
7 changed files with 11225 additions and 1 deletions

View File

@@ -1606,7 +1606,8 @@ Legacy: `perSession` is still supported (`true` → `scope: "session"`,
seccompProfile: "/path/to/seccomp.json",
apparmorProfile: "clawdbot-sandbox",
dns: ["1.1.1.1", "8.8.8.8"],
extraHosts: ["internal.service:10.0.0.5"]
extraHosts: ["internal.service:10.0.0.5"],
binds: ["/var/run/docker.sock:/var/run/docker.sock", "/home/user/source:/source:rw"]
},
browser: {
enabled: false,
@@ -1652,6 +1653,8 @@ to `"bridge"` (or your custom network) if the agent needs outbound access.
Note: inbound attachments are staged into the active workspace at `media/inbound/*`. With `workspaceAccess: "rw"`, that means files are written into the agent workspace.
Note: `docker.binds` mounts additional host directories; global and per-agent binds are merged.
Build the optional browser image with:
```bash
scripts/sandbox-browser-setup.sh

View File

@@ -56,6 +56,12 @@ Clawdbot mirrors eligible skills into the sandbox workspace (`.../skills`) so
they can be read. With `"rw"`, workspace skills are readable from
`/workspace/skills`.
## Custom bind mounts
`agents.defaults.sandbox.docker.binds` mounts additional host directories into the container.
Format: `host:container:mode` (e.g., `"/home/user/source:/source:rw"`).
Global and per-agent binds are **merged** (not replaced). Under `scope: "shared"`, per-agent binds are ignored.
## Images + setup
Default image: `clawdbot-sandbox:bookworm-slim`

11154
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,55 @@ describe("sandbox config merges", () => {
});
});
it("merges sandbox docker binds (global + agent combined)", async () => {
const { resolveSandboxDockerConfig } = await import("./sandbox.js");
const resolved = resolveSandboxDockerConfig({
scope: "agent",
globalDocker: {
binds: ["/var/run/docker.sock:/var/run/docker.sock"],
},
agentDocker: {
binds: ["/home/user/source:/source:rw"],
},
});
expect(resolved.binds).toEqual([
"/var/run/docker.sock:/var/run/docker.sock",
"/home/user/source:/source:rw",
]);
});
it("returns undefined binds when neither global nor agent has binds", async () => {
const { resolveSandboxDockerConfig } = await import("./sandbox.js");
const resolved = resolveSandboxDockerConfig({
scope: "agent",
globalDocker: {},
agentDocker: {},
});
expect(resolved.binds).toBeUndefined();
});
it("ignores agent binds under shared scope", async () => {
const { resolveSandboxDockerConfig } = await import("./sandbox.js");
const resolved = resolveSandboxDockerConfig({
scope: "shared",
globalDocker: {
binds: ["/var/run/docker.sock:/var/run/docker.sock"],
},
agentDocker: {
binds: ["/home/user/source:/source:rw"],
},
});
expect(resolved.binds).toEqual([
"/var/run/docker.sock:/var/run/docker.sock",
]);
});
it("ignores agent docker overrides under shared scope", async () => {
const { resolveSandboxDockerConfig } = await import("./sandbox.js");

View File

@@ -107,6 +107,7 @@ export type SandboxDockerConfig = {
apparmorProfile?: string;
dns?: string[];
extraHosts?: string[];
binds?: string[];
};
export type SandboxPruneConfig = {
@@ -325,6 +326,8 @@ export function resolveSandboxDockerConfig(params: {
? { ...globalDocker?.ulimits, ...agentDocker.ulimits }
: globalDocker?.ulimits;
const binds = [...(globalDocker?.binds ?? []), ...(agentDocker?.binds ?? [])];
return {
image: agentDocker?.image ?? globalDocker?.image ?? DEFAULT_SANDBOX_IMAGE,
containerPrefix:
@@ -352,6 +355,7 @@ export function resolveSandboxDockerConfig(params: {
agentDocker?.apparmorProfile ?? globalDocker?.apparmorProfile,
dns: agentDocker?.dns ?? globalDocker?.dns,
extraHosts: agentDocker?.extraHosts ?? globalDocker?.extraHosts,
binds: binds.length ? binds : undefined,
};
}
@@ -1051,6 +1055,11 @@ async function createSandboxContainer(params: {
`${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`,
);
}
if (cfg.binds?.length) {
for (const bind of cfg.binds) {
args.push("-v", bind);
}
}
args.push(cfg.image, "sleep", "infinity");
await execDocker(args);

View File

@@ -922,6 +922,8 @@ export type SandboxDockerSettings = {
dns?: string[];
/** Extra host mappings (e.g. ["api.local:10.0.0.2"]). */
extraHosts?: string[];
/** Additional bind mounts (host:container:mode format, e.g. ["/host/path:/container/path:rw"]). */
binds?: string[];
};
export type SandboxBrowserSettings = {

View File

@@ -801,6 +801,7 @@ const SandboxDockerSchema = z
apparmorProfile: z.string().optional(),
dns: z.array(z.string()).optional(),
extraHosts: z.array(z.string()).optional(),
binds: z.array(z.string()).optional(),
})
.optional();