Skip to content

Golden path: safe code execution

The supported route to executing code you do not trust - model-written scripts in code-mode, user-supplied snippets, third-party skill hooks - behind a real isolation boundary.

Read this page as a contract: what the boundary enforces, what it explicitly does not, and how to verify both on your own machine.

Threat model in one paragraph

Treat generated code as attacker-controlled. The worker-threads tier (the built-in default) is best-effort hygiene, not a security boundary: it blocks the obvious escape hatches (fs, network imports, child_process, vm) and scrubs the environment, but it shares your process. The docker tier moves execution into a disposable container with kernel-level isolation - that is the tier this page wires. The full tier table and threat-model prose live in Security.

Prerequisites

  • A Docker daemon the server user can reach.
  • The dockerode peer (v5+): npm install dockerode@^5 - the base install ships zero native sandbox code; the peer loads lazily.
  • A sandbox image. The framework deliberately ships none; a minimal one is:
bash
cat > Dockerfile.sandbox <<'EOF'
FROM node:22-slim
USER 10001:10001
EOF
docker build -f Dockerfile.sandbox -t graphorin-sandbox:latest .

The worked example

ts
import { createDockerSandbox } from '@graphorin/security/sandbox';

const sandbox = createDockerSandbox({
  image: 'graphorin-sandbox:latest',
  memoryLimitMb: 256,
  cpus: 0.5,
  pidsLimit: 64,
  defaultTimeoutMs: 10_000,
});

// The code is a plain source string - imagine the model wrote it. It
// runs inside an async wrapper; the input payload is visible as the
// __INPUT__ global (JSON-embedded, the only data door into the box).
const result = await sandbox.run<{ readonly n: number }, number>(
  {
    kind: 'source',
    source: 'const { n } = __INPUT__; return n * n;',
  },
  { input: { n: 12 } },
);

if (result.ok) {
  console.log('output:', result.output, `(${result.durationMs}ms)`);
} else {
  console.log('blocked:', result.error.kind, result.error.message);
}

Expected output (duration varies):

output: 144 (412ms)

Results are errors-as-data: every failure comes back as { ok: false, error: { kind, message }, durationMs } with kind one of timeout | memory-exceeded | sandbox-violation | aborted | execution-failed - never a throw you have to catch around every call. The runtime maps this shape onto the tool-outcome union when the sandbox backs a tool.

What the boundary enforces

Container defaults (every one is a createDockerSandbox option; the values below are the shipped defaults):

ControlDefaultWhat it stops
NetworkdisabledExfiltration, C2 callbacks, dependency fetching
Root filesystemread-only; /work tmpfs (64 MB, mode 0700) is the only writable mountPersistence, host tampering
User10001:10001 (non-root; user: '' opts back to the image default)Container-root privilege escalation surface
Capabilitiesall dropped + no-new-privilegessetuid tricks, raw sockets
Memory512 MB (memoryLimitMb)Memory bombs
PIDs128, clamped 16-4096 (pidsLimit)Fork bombs
CPU1 CPU, clamped 0.1-8 (cpus)CPU starvation of the host
Wall clock30 s (defaultTimeoutMs), force-removed afterInfinite loops, zombie containers

Verify the negatives yourself - each should come back ok: false (or an empty result), not hang and not succeed:

ts
import { createDockerSandbox } from '@graphorin/security/sandbox';

const sandbox = createDockerSandbox({ image: 'graphorin-sandbox:latest' });

// Network is unreachable (fetch rejects; surfaces as execution-failed):
const net = await sandbox.run(
  { kind: 'source', source: "await fetch('https://example.com'); return 'reached';" },
  { input: undefined },
);
console.log('network attempt ok?', net.ok); // false

// Writes outside /work are denied by the read-only rootfs (EROFS):
const write = await sandbox.run(
  {
    kind: 'source',
    source: "require('node:fs').writeFileSync('/etc/owned', 'x'); return 'wrote';",
  },
  { input: undefined },
);
console.log('rootfs write ok?', write.ok); // false

The same assertions run as live negative tests against a real daemon in the repo's security suite (uid, rootfs, network, pid ceiling), so the defaults cannot silently regress.

Wiring it into an agent

Code-mode is where this matters in practice: the agent writes a script against projected tool APIs and code_execute runs it in a sandbox. Point the executor's sandboxResolver at the docker tier for untrusted trust classes and keep worker-threads for your own first-party hooks - Tools: sandbox tiers shows the resolver wiring and the per-tier policy table.

What stays out of scope

Be explicit with yourself about what this page did NOT give you:

  • Worker-threads is not a boundary. If the code is hostile and the docker tier is unavailable, the honest options are "do not run it" or an infrastructure sandbox (gVisor, Firecracker, a throwaway VM).
  • The container shares the host kernel. A kernel 0-day is outside this boundary; regulated multi-tenant isolation wants a VM boundary on top.
  • Secrets hygiene is yours. Nothing you mount or bake into the sandbox image is protected from the code running there - keep the image empty of credentials; the sandbox passes input explicitly and that is the only data door.
  • Resource limits are per-run. A queue of hostile runs is a denial-of-service surface at the scheduler level - cap concurrency upstream.

Troubleshooting

SymptomCause and fix
sandbox-violation ... dockerodeThe peer is not installed (or not v5). npm install dockerode@^5.
connect ENOENT /var/run/docker.sockThe daemon socket is elsewhere or unreadable - pass socketPath, and never add the server user to docker group on a shared host without understanding that is root-equivalent.
No such image: graphorin-sandbox:latestBuild or pull the sandbox image first; the framework ships none by design.
Every run times out at 30 sCold image pulls or a busy daemon; raise defaultTimeoutMs per call via timeoutMs, and pre-pull the image in deployment.
EACCES on /workYou overrode user: to a uid the tmpfs is not owned by - the tmpfs is owned by the numeric user option; keep them consistent.