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:
| Provider | Where it runs | Cost | When to use |
|---|---|---|---|
docker | Local Docker engine | Free | Default for self-hosted dev / CI |
e2b | E2B managed cloud | Metered | No local Docker, want managed isolation |
daytona | Self-hosted Daytona | Self-host | Already running Daytona for dev environments |
none | Disabled (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 vector | Mitigation in DockerSandbox |
|---|---|---|
| 1 | Command injection via code | Code is passed via stdin to the interpreter (python3 -, node -, sh -s). It is never concatenated into a shell command on the host. |
| 2 | Container escape (privileges) | --cap-drop=ALL, --security-opt no-new-privileges, --user 65534:65534 (nobody), no --privileged, no --cap-add. |
| 3 | Resource 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. |
| 4 | Secret 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. |
| 5 | Filesystem escape via volume mount | No -v / --volume flag is ever emitted. Only --tmpfs /tmp and --tmpfs /sandbox, each size=64m and (/tmp) noexec,nosuid. Rootfs is --read-only. |
| 6 | Network exfiltration | Default --network=none. Caller may opt-in with opts.network=true → --network=bridge. --network=host is never emitted. |
| 7 | Container 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.envallowlist — host env is never passed through implicitly. - Applying
opts.timeoutas a hard wall-clock (AbortController for Daytona,timeoutMsfor E2B) and reaping the remote sandbox in afinallyblock. - Surfacing missing SDK / API key as
SandboxUnavailableErrorrather 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
--privilegedDocker containers--cap-addof any capability--devicemounts--pid=host,--network=host,--ipc=host- Host-path
--volumemounts (onlytmpfs) --env-fileor 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 throwsSandboxPolicyErrorso you notice instead of silently running unsandboxed.
Troubleshooting
SandboxUnavailableError: docker CLI not found— install Docker Desktop or the Docker engine, or switch provider toe2b/daytona.SandboxUnavailableError: E2B_API_KEY not set— export the env var or setsandbox.e2bApiKey(env is preferred, do not commit keys).- Tests skipping —
docker.test.tsskips when no daemon is reachable,e2b.test.tsskips whenE2B_API_KEYis unset. This is intentional. pip installfails in a Python sandbox — by defaultnetwork: false. Enable network on the specific run:sandbox.run(code, { language: 'python', network: true }).