import { describe, expect, it } from "vitest"; import { buildSandboxCreateArgs } from "./sandbox/docker.js"; import type { SandboxDockerConfig } from "./sandbox/types.js"; describe("buildSandboxCreateArgs", () => { function createSandboxConfig( overrides: Partial = {}, binds?: string[], ): SandboxDockerConfig { return { image: "openclaw-sandbox:bookworm-slim", containerPrefix: "openclaw-sbx-", workdir: "/workspace", readOnlyRoot: false, tmpfs: [], network: "none", capDrop: [], ...(binds ? { binds } : {}), ...overrides, }; } function expectBuildToThrow( name: string, cfg: SandboxDockerConfig, expectedMessage: RegExp, ): void { expect( () => buildSandboxCreateArgs({ name, cfg, scopeKey: "main", createdAtMs: 1700000000000, }), name, ).toThrow(expectedMessage); } it("includes hardening and resource flags", () => { const cfg: SandboxDockerConfig = { image: "openclaw-sandbox:bookworm-slim", containerPrefix: "openclaw-sbx-", workdir: "/workspace", readOnlyRoot: true, tmpfs: ["/tmp"], network: "none", user: "1000:1000", capDrop: ["ALL"], env: { LANG: "C.UTF-8" }, pidsLimit: 256, memory: "512m", memorySwap: 1024, cpus: 1.5, ulimits: { nofile: { soft: 1024, hard: 2048 }, nproc: 128, core: "0", }, seccompProfile: "/tmp/seccomp.json", apparmorProfile: "openclaw-sandbox", dns: ["1.1.1.1"], extraHosts: ["internal.service:10.0.0.5"], }; const args = buildSandboxCreateArgs({ name: "openclaw-sbx-test", cfg, scopeKey: "main", createdAtMs: 1700000000000, labels: { "openclaw.sandboxBrowser": "1" }, }); expect(args).toEqual( expect.arrayContaining([ "create", "--name", "openclaw-sbx-test", "--label", "openclaw.sandbox=1", "--label", "openclaw.sessionKey=main", "--label", "openclaw.createdAtMs=1700000000000", "--label", "openclaw.sandboxBrowser=1", "--read-only", "--tmpfs", "/tmp", "--network", "none", "--user", "1000:1000", "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--security-opt", "seccomp=/tmp/seccomp.json", "--security-opt", "apparmor=openclaw-sandbox", "--dns", "1.1.1.1", "--add-host", "internal.service:10.0.0.5", "--pids-limit", "256", "--memory", "512m", "--memory-swap", "1024", "--cpus", "1.5", ]), ); expect(args).toEqual(expect.arrayContaining(["--env", "LANG=C.UTF-8"])); const ulimitValues: string[] = []; for (let i = 0; i < args.length; i += 1) { if (args[i] === "--ulimit") { const value = args[i + 1]; if (value) { ulimitValues.push(value); } } } expect(ulimitValues).toEqual( expect.arrayContaining(["nofile=1024:2048", "nproc=128", "core=0"]), ); }); it("emits -v flags for safe custom binds", () => { const cfg: SandboxDockerConfig = { image: "openclaw-sandbox:bookworm-slim", containerPrefix: "openclaw-sbx-", workdir: "/workspace", readOnlyRoot: false, tmpfs: [], network: "none", capDrop: [], binds: ["/home/user/source:/source:rw", "/var/data/myapp:/data:ro"], }; const args = buildSandboxCreateArgs({ name: "openclaw-sbx-binds", cfg, scopeKey: "main", createdAtMs: 1700000000000, }); expect(args).toContain("-v"); const vFlags: string[] = []; for (let i = 0; i < args.length; i++) { if (args[i] === "-v") { const value = args[i + 1]; if (value) { vFlags.push(value); } } } expect(vFlags).toContain("/home/user/source:/source:rw"); expect(vFlags).toContain("/var/data/myapp:/data:ro"); }); it.each([ { name: "dangerous Docker socket bind mounts", containerName: "openclaw-sbx-dangerous", cfg: createSandboxConfig({}, ["/var/run/docker.sock:/var/run/docker.sock"]), expected: /blocked path/, }, { name: "dangerous parent bind mounts", containerName: "openclaw-sbx-dangerous-parent", cfg: createSandboxConfig({}, ["/run:/run"]), expected: /blocked path/, }, { name: "network host mode", containerName: "openclaw-sbx-host", cfg: createSandboxConfig({ network: "host" }), expected: /network mode "host" is blocked/, }, { name: "network container namespace join", containerName: "openclaw-sbx-container-network", cfg: createSandboxConfig({ network: "container:peer" }), expected: /network mode "container:peer" is blocked by default/, }, { name: "seccomp unconfined", containerName: "openclaw-sbx-seccomp", cfg: createSandboxConfig({ seccompProfile: "unconfined" }), expected: /seccomp profile "unconfined" is blocked/, }, { name: "apparmor unconfined", containerName: "openclaw-sbx-apparmor", cfg: createSandboxConfig({ apparmorProfile: "unconfined" }), expected: /apparmor profile "unconfined" is blocked/, }, ])("throws on $name", ({ containerName, cfg, expected }) => { expectBuildToThrow(containerName, cfg, expected); }); it("omits -v flags when binds is empty or undefined", () => { const cfg: SandboxDockerConfig = { image: "openclaw-sandbox:bookworm-slim", containerPrefix: "openclaw-sbx-", workdir: "/workspace", readOnlyRoot: false, tmpfs: [], network: "none", capDrop: [], binds: [], }; const args = buildSandboxCreateArgs({ name: "openclaw-sbx-no-binds", cfg, scopeKey: "main", createdAtMs: 1700000000000, }); // Count -v flags that are NOT workspace mounts (workspace mounts are internal) const customVFlags: string[] = []; for (let i = 0; i < args.length; i++) { if (args[i] === "-v") { const value = args[i + 1]; if (value && !value.includes("/workspace")) { customVFlags.push(value); } } } expect(customVFlags).toHaveLength(0); }); it("blocks bind sources outside runtime allowlist roots", () => { const cfg = createSandboxConfig({}, ["/opt/external:/data:rw"]); expect(() => buildSandboxCreateArgs({ name: "openclaw-sbx-outside-roots", cfg, scopeKey: "main", createdAtMs: 1700000000000, bindSourceRoots: ["/tmp/workspace", "/tmp/agent"], }), ).toThrow(/outside allowed roots/); }); it("allows bind sources outside runtime allowlist with explicit override", () => { const cfg = createSandboxConfig({}, ["/opt/external:/data:rw"]); const args = buildSandboxCreateArgs({ name: "openclaw-sbx-outside-roots-override", cfg, scopeKey: "main", createdAtMs: 1700000000000, bindSourceRoots: ["/tmp/workspace", "/tmp/agent"], allowSourcesOutsideAllowedRoots: true, }); expect(args).toEqual(expect.arrayContaining(["-v", "/opt/external:/data:rw"])); }); it("blocks reserved /workspace target bind mounts by default", () => { const cfg = createSandboxConfig({}, ["/tmp/override:/workspace:rw"]); expectBuildToThrow("openclaw-sbx-reserved-target", cfg, /reserved container path/); }); it("allows reserved /workspace target bind mounts with explicit dangerous override", () => { const cfg = createSandboxConfig({}, ["/tmp/override:/workspace:rw"]); const args = buildSandboxCreateArgs({ name: "openclaw-sbx-reserved-target-override", cfg, scopeKey: "main", createdAtMs: 1700000000000, allowReservedContainerTargets: true, }); expect(args).toEqual(expect.arrayContaining(["-v", "/tmp/override:/workspace:rw"])); }); it("allows container namespace join with explicit dangerous override", () => { const cfg = createSandboxConfig({ network: "container:peer", dangerouslyAllowContainerNamespaceJoin: true, }); const args = buildSandboxCreateArgs({ name: "openclaw-sbx-container-network-override", cfg, scopeKey: "main", createdAtMs: 1700000000000, }); expect(args).toEqual(expect.arrayContaining(["--network", "container:peer"])); }); });