Sandboxed Code Execution (AIG-634)

May 28, 2026 · View on GitHub

aistack ships a pluggable sandbox layer so the coder agent — and any other component that needs to run model-generated code — can do so in isolation instead of on the host machine.

Three backends are bundled:

ProviderWhere it runsCostWhen to use
dockerLocal Docker engineFreeDefault for self-hosted dev / CI
e2bE2B managed cloudMeteredNo local Docker, want managed isolation
daytonaSelf-hosted DaytonaSelf-hostAlready running Daytona for dev environments
noneDisabled (refuses)Default — opt-in by switching this

All adapters implement the same interface:

sandbox.run(code, { language, timeout, env, files, network }) → { stdout, stderr, exitCode, timedOut, durationMs, provider }

Setup

1. Pick a provider in aistack.config.json

{
  "sandbox": {
    "provider": "docker",     // 'none' | 'docker' | 'e2b' | 'daytona'
    "timeout": 30000,          // wall-clock ms (default 30s)
    "memoryMb": 512,           // Docker only
    "cpus": 1,                 // Docker only
    "pidsLimit": 100,          // Docker only
    "network": false           // false = --network=none (default)
  }
}

2. (E2B) Install the SDK and set the API key

npm install --save-optional @e2b/code-interpreter
export E2B_API_KEY=...
{ "sandbox": { "provider": "e2b" } }

3. (Daytona) Point at your server

{
  "sandbox": {
    "provider": "daytona",
    "daytonaApiUrl": "https://daytona.internal.example.com/api"
  }
}
export DAYTONA_API_KEY=...

Using it from code

import { createSandbox, getConfig } from '@blackms/aistack';

const sandbox = createSandbox(getConfig().sandbox);

const result = await sandbox.run('print("hello")', {
  language: 'python',
  timeout: 5_000,
});

console.log(result.exitCode, result.stdout);

The coder agent's system prompt (src/agents/definitions/coder.ts) tells the model the sandbox is available when configured. The agent decides per-task whether to use it — execution is opt-in so existing workflows keep working.

Security Model

Code generated by an LLM is untrusted input. This section enumerates the threats we defend against and the mitigations applied by each adapter. It is the source of truth — any contributor changing src/sandbox/*.ts MUST keep this section in sync.

Threat model (who attacks what)

  • Attacker: a malicious or compromised LLM emits code intended to exfil data, persist on the host, escape the container, or exhaust resources.
  • Asset: the host machine running aistack — its filesystem, network reachability (private networks, cloud metadata), env vars (API keys), and uptime.
  • Trust boundary: the call into sandbox.run(code, ...). Anything before is trusted, anything after is untrusted.

Mitigations (Docker adapter)

#Attack vectorMitigation in DockerSandbox
1Command injection via codeCode is passed via stdin to the interpreter (python3 -, node -, sh -s). It is never concatenated into a shell command on the host.
2Container escape (privileges)--cap-drop=ALL, --security-opt no-new-privileges, --user 65534:65534 (nobody), no --privileged, no --cap-add.
3Resource exhaustion (CPU / RAM / fork)--memory=<m>m, --memory-swap=<m>m (no swap escape), --cpus=<c>, --pids-limit=<n>, plus a hard wall-clock timeout that SIGKILLs the CLI.
4Secret leak (host env vars)Env is allowlist-only. Caller passes opts.env; host env is never forwarded. Env var names are regex-validated to prevent metacharacter injection into -e. --env-file is forbidden.
5Filesystem escape via volume mountNo -v / --volume flag is ever emitted. Only --tmpfs /tmp and --tmpfs /sandbox, each size=64m and (/tmp) noexec,nosuid. Rootfs is --read-only.
6Network exfiltrationDefault --network=none. Caller may opt-in with opts.network=true--network=bridge. --network=host is never emitted.
7Container persistence--rm always set; container is reaped at exit.

The argv builder (buildDockerArgs) is pure and is asserted by tests/unit/sandbox/security.test.ts against all the "NEVER" rules above — that test must pass on every PR touching the Docker adapter.

Mitigations (managed providers)

E2B and Daytona delegate isolation to the provider's runtime (Firecracker micro-VMs for E2B, container/devcontainer runtime for Daytona). The aistack adapters are responsible for:

  • Reading API keys only from explicit config or env vars (E2B_API_KEY, DAYTONA_API_KEY). Never hard-coded, never logged.
  • Forwarding only the caller's opts.env allowlist — host env is never passed through implicitly.
  • Applying opts.timeout as a hard wall-clock (AbortController for Daytona, timeoutMs for E2B) and reaping the remote sandbox in a finally block.
  • Surfacing missing SDK / API key as SandboxUnavailableError rather than silently doing nothing.
  • Never including the API key in error messages (asserted by tests/unit/sandbox/daytona.test.ts).

Things we explicitly do NOT do

  • --privileged Docker containers
  • --cap-add of any capability
  • --device mounts
  • --pid=host, --network=host, --ipc=host
  • Host-path --volume mounts (only tmpfs)
  • --env-file or implicit host env forwarding

If you need any of the above, write a separate adapter — do not loosen the defaults of the bundled ones.

When to pick which provider

  • Docker — default for self-hosted dev and CI. Free, well-understood, the security flags above are enforced by the argv builder.
  • E2B — pick when you don't run Docker locally (e.g. lightweight laptop setups, restricted CI), or when you specifically want Firecracker-grade isolation. Bring your own E2B_API_KEY.
  • Daytona — pick when your organisation already runs Daytona for dev environments and you want sandbox runs to live alongside them.
  • none — the default. Sandboxing is opt-in; if a caller invokes sandbox.run(...) while disabled, it throws SandboxPolicyError so you notice instead of silently running unsandboxed.

Troubleshooting

  • SandboxUnavailableError: docker CLI not found — install Docker Desktop or the Docker engine, or switch provider to e2b / daytona.
  • SandboxUnavailableError: E2B_API_KEY not set — export the env var or set sandbox.e2bApiKey (env is preferred, do not commit keys).
  • Tests skippingdocker.test.ts skips when no daemon is reachable, e2b.test.ts skips when E2B_API_KEY is unset. This is intentional.
  • pip install fails in a Python sandbox — by default network: false. Enable network on the specific run: sandbox.run(code, { language: 'python', network: true }).