Execution Sandbox

The execution sandbox gives a provider-keyed Agent Protocol thread an isolated filesystem, shell, and network boundary instead of letting workspace tools operate in the app's local workspace/ directory. That boundary depends on thread IDs remaining distinct after provider resource naming; the built-in providers do not add an application or tenant namespace. Add a sandbox key to dawn.config.ts, and Dawn routes readFile, writeFile, listDir, and runBash through a SandboxProvider. Dawn includes a Docker reference implementation; the same portable contract also supports other providers.

This is one of three independent controls: tool scoping decides which tools the model can call, permissions decide whether a call may run, and the sandbox constrains what an allowed call can touch. Use all three where your threat model calls for them.

Quickstart

Install Docker, start its daemon, and configure dockerSandbox:

bash
pnpm add @dawn-ai/sandbox
dawn.config.ts
import { config } from "@dawn-ai/cli"
import { dockerSandbox } from "@dawn-ai/sandbox"
 
export default config({
  sandbox: {
    provider: dockerSandbox({ image: "node:24-slim" }),
    network: { mode: "allow", denylist: ["169.254.169.254"] },
    env: { NODE_ENV: "production" },
    resources: { memoryMb: 512, cpus: 1, timeoutMs: 120_000 },
    idleTimeoutMs: 600_000,
  },
})

No sandbox key means no behavior change: workspace tools still use the app's local workspace/ directory. dawn check validates the configuration and calls the provider's optional preflight(). For Docker, that check confirms the daemon is reachable so a stopped daemon fails before the first agent turn.

What's isolated

  • Filesystem — workspace file tools use a sandbox volume, not the host filesystem.
  • ShellrunBash executes in the sandbox and remains subject to permissions.
  • Networksandbox.network expresses portable policy intent; enforcement depends on the provider.
  • Environment — the host environment is not inherited. Only sandbox.env entries are injected.
  • ResourcesmemoryMb and cpus cap compute; timeoutMs caps one command.

Lifecycle

Dawn passes the conversation thread ID to the provider and reuses the resulting sandbox across turns.

  • acquire() creates or reattaches the thread's live compute and workspace.
  • Idle reap and release() discard warm compute but retain the workspace volume.
  • A later turn reattaches that retained volume.
  • Thread deletion calls destroy(), removing both compute and workspace data.

Storage is named from a provider-specific transformation of the thread ID so a provider can reattach it after compute is released. Provider retention can shorten this lifecycle: an operator-owned cleanup policy may delete released storage before the thread is deleted. See Kubernetes Sandbox for that provider's reaper boundary. Subagents share their parent's thread and therefore share its sandbox.

Security hardening

The Docker provider applies these defaults:

ControlDocker mechanism
Linux capabilities--cap-drop ALL
Privilege escalation--security-opt no-new-privileges
Process count--pids-limit 512
Root filesystem--read-only, with writable /tmp and /run tmpfs mounts
User--user 1000:1000, with HOME=/workspace

The portable security policy exposes dropAllCapabilities, noNewPrivileges, readOnlyRootFilesystem, runAsNonRoot, and pidsLimit. Each can be overridden for a compatible image, but every relaxation changes the isolation boundary.

Per-command timeout

resources.timeoutMs bounds one runBash call. Docker wraps the command with GNU timeout; an overrun exits with code 124, rounded up to a whole second. The limit applies only when configured, and the sandbox image must contain the timeout binary. node:24-slim, Debian, and Ubuntu include or can install it; minimal and distroless images may not.

ts
sandbox: {
  provider: dockerSandbox({ image: "node:24-slim" }),
  resources: { timeoutMs: 120_000 },
},

Network policy

The portable policy has two modes:

  • { mode: "deny" } asks the provider for default-closed egress. Docker enforces this with --network none.
  • { mode: "allow", denylist?: [...] } leaves egress open and expresses hosts that should be blocked.

When omitted, the policy defaults to allow mode with 169.254.169.254 in the denylist. Docker's allow-mode denylist is best-effort, not a rigorous firewall; use an egress proxy or another provider when host-level filtering is a security boundary. Docker deny mode is the stronger reference behavior because --network none removes network access.

Provider mappings are intentionally not identical. Read the provider-specific guide before assuming Docker behavior applies elsewhere.

Kubernetes provider

Configuration, prerequisites, and the cluster security boundary live in Kubernetes Sandbox.

Security hardening on Kubernetes

See Kubernetes Sandbox for Pod security contexts, Pod Security Standards, resource controls, ServiceAccount-token isolation, and PID-limit scope.

Network policy on Kubernetes

See Kubernetes Sandbox for DNS exceptions, CNI requirements, and how chart and per-thread NetworkPolicies compose.

Deploying the sandbox infrastructure (Helm)

Install and operate dawn-sandbox-infra with the canonical Kubernetes Sandbox guide.

Key caveats

Namespace alignment, storage, DNS, CNI enforcement, quotas, and PVC cleanup are covered in Kubernetes Sandbox.

Deploying a Dawn app (Helm)

Deploy the app with Kubernetes; operate its provider with Kubernetes Sandbox.

ServiceAccount and namespace wiring

Use Kubernetes Sandbox for RBAC and Kubernetes deployment for the app chart.

Env, secrets, and replicas

See Kubernetes deployment for app settings and Kubernetes Sandbox for namespace controls.

Subagents

A subagent runs under its parent's conversation thread. The coordinator and all of its subagents therefore resolve to the same sandbox and workspace rather than receiving one sandbox each.

Custom providers

Implement SandboxProvider to integrate a microVM, cloud sandbox, or another isolation backend:

ts
import type { SandboxHandle, SandboxPolicy } from "@dawn-ai/sandbox"
 
export interface SandboxProvider {
  readonly name: string
  acquire(input: {
    readonly threadId: string
    readonly policy: SandboxPolicy
    readonly signal: AbortSignal
  }): Promise<SandboxHandle>
  release(threadId: string): Promise<void>
  destroy(threadId: string): Promise<void>
  preflight?(): Promise<{
    readonly ok: boolean
    readonly detail?: string
    readonly warnings?: readonly string[]
  }>
}

acquire() must be idempotent for a thread. release() drops warm compute while retaining workspace data; destroy() removes both. preflight() can fail a configuration before runtime or return warnings when a provider cannot prove a requested control.

Validate the implementation with the shared conformance suite:

ts
import { runProviderConformance } from "@dawn-ai/sandbox/testing"
import { describe } from "vitest"
import { myCloudSandbox } from "./my-cloud-sandbox.js"
 
runProviderConformance({
  name: "my-cloud-sandbox",
  makeProvider: () => myCloudSandbox({ apiKey: process.env.MY_SANDBOX_KEY! }),
  describe,
})

The suite checks acquire/reattach idempotency, per-thread isolation, release-versus-destroy storage semantics, and numeric command exit codes.

Testing your agent

Use fakeSandbox() for deterministic tests that need the provider contract without Docker:

dawn.config.ts (test)
import { config } from "@dawn-ai/cli"
import { fakeSandbox } from "@dawn-ai/sandbox/testing"
 
export default config({
  sandbox: { provider: fakeSandbox() },
})

This exercises Dawn's per-thread acquisition, reuse, and subagent wiring. Provider conformance and gated integration tests cover the actual isolation backend.

Verifying the full arc (end-to-end)

Two gated CI lanes exercise a built Dawn app, a real provider, command output inside the isolated workload, and cleanup after thread deletion:

  • sandbox-docker-e2e runs the app as a container and drives dockerSandbox to create a sibling container.
  • sandbox-k8s-e2e deploys the app and drives kubernetesSandbox to create a sandbox Pod; operational setup is in Kubernetes Sandbox.

Both use a mocked model, assert that the in-sandbox process runs as UID 1000, and confirm compute and storage are removed on thread deletion. They run only when DAWN_TEST_SMOKE_E2E=1 is enabled.

Only a Dawn runtime entry (dawn dev, dawn start, or the Node build target) constructs the configured sandbox. LangSmith platform artifacts do not run that runtime. Edge targets reject incompatible filesystem and sandbox capabilities rather than silently running workspace tools without isolation; see Deployment Options.

What it is — and isn't

It is: a filesystem and process boundary keyed by the provider's derived thread resource name; explicit environment injection; CPU and memory controls; provider-specific network policy; a workspace lifecycle that outlives warm compute; and hardened Docker defaults including a non-root user, dropped capabilities, no privilege escalation, a read-only root filesystem, and a process-count limit.

It is not: authorization, tool selection, or a guarantee against container-escape vulnerabilities. Tool scoping and permissions remain separate controls. Docker's allow-mode denylist is best-effort, and container isolation is not a microVM boundary. For hostile multi-tenant workloads, implement a stronger provider behind the same SandboxProvider seam and validate its controls against your threat model.

Kubernetes maps the same policy intent to different mechanisms and limitations. Treat Kubernetes Sandbox as canonical for that provider rather than inferring Kubernetes behavior from Docker.