Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

Architecture: Sandbox

Strategy pattern, binary bridges internals, and the integration pipeline

This page explains the internal architecture of the sandbox system. Read this if you want to understand how the pieces fit together, extend the system with custom strategies, or debug issues.

Strategy Pattern

The Docker sandbox uses the Strategy pattern with a Template Method base class. Three concrete strategies handle different container creation approaches:

DockerSandboxStrategy (abstract)
├── RuntimeStrategy      — install packages/binaries at runtime
├── DockerfileStrategy   — build image from Dockerfile (cached)
└── ComposeStrategy      — multi-container via docker compose

Template Method Flow

The base class defines the creation algorithm. Subclasses override getImage() and configure():

create()

  ├─ 1. getImage()           ← strategy-specific
  │     ├─ Runtime:    return image name (e.g., 'alpine:latest')
  │     ├─ Dockerfile: build image, return cached tag
  │     └─ Compose:    return '' (compose manages images)

  ├─ 2. prepareVolumes()     ← common: validate bind paths, inspect/create Docker volumes

  ├─ 3. startContainer()     ← common: docker run -d --rm
  │     └─ Compose override:  docker compose up -d

  ├─ 4. configure()          ← strategy-specific
  │     ├─ Runtime:    install packages (apk/apt), install binaries (curl)
  │     ├─ Dockerfile: no-op (image already configured)
  │     └─ Compose:    no-op (compose file defines everything)

  └─ 5. createSandboxMethods()  ← common: return { executeCommand, spawn, readFile, writeFiles, dispose }

If configure() throws, the base class auto-stops the container before re-throwing.

Container Startup

All single-container strategies start containers with:

docker run -d --rm \
  --name sandbox-<uuid> \
  --memory=1g --cpus=2 \
  -w /workspace \
  --mount type=bind,src=/host/path,dst=/container/path,readonly \
  <image> [command ...]

Command resolution for Runtime and Dockerfile strategies:

  • command omitted (default): append tail -f /dev/null as a keep-alive.
  • command: null or command: []: append nothing; image/Dockerfile CMD/ENTRYPOINT run as declared.
  • Non-empty command: append verbatim, overriding image/Dockerfile CMD.

Commands then execute via docker exec <id> sh -c "<command>".

Dockerfile Image Caching

The DockerfileStrategy generates deterministic image tags from Dockerfile content:

Dockerfile content → SHA-256 → first 12 chars → "sandbox-a1b2c3d4e5f6"

Same Dockerfile produces the same tag. Docker's layer cache handles the rest — if the image exists locally, the build is skipped entirely.

Compose Overrides

ComposeStrategy overrides three base class methods:

MethodStandardCompose Override
startContainer()docker rundocker compose up -d
exec()docker exec <id>docker compose exec -T <service>
stopContainer()docker stopdocker compose down

Binary Bridges

Binary bridges connect just-bash virtual environments to real host binaries. They solve three path resolution problems:

Virtual CWD to Real CWD

just-bash uses virtual paths like /home/user. Binary bridges resolve them to real host paths:

ReadWriteFs:  root + cwd → path.join(fs.root, ctx.cwd)
OverlayFs:    fs.toRealPath(ctx.cwd)
InMemoryFs:   fallback to process.cwd()

Virtual PATH to Real PATH

just-bash sets PATH=/bin:/usr/bin which doesn't include host binary locations (nvm, homebrew, etc.). Binary bridges always use process.env.PATH for binary resolution.

File Argument Detection

Arguments that look like file paths are resolved relative to the real CWD:

Detected as path:     Has file extension (.md, .py), contains /, starts with .
Passed through:       Starts with - (flags), no path indicators

Security via allowedArgs

createBinaryBridges({
  name: 'git',
  allowedArgs: /^(status|log|diff|show)/,
});

// Allowed:  git status, git log, git diff
// Blocked:  git log --oneline (flags are also tested), git reset --hard

Each argument is tested individually against the regex. Any non-matching argument returns exit code 1 with a security policy error.

Integration Pipeline

To give an AI agent a bash tool that runs inside Docker, compose the two independent systems explicitly at the call site:

1. createDockerSandbox(sandboxOptions)
   └─ Returns: DockerSandbox { executeCommand, spawn, readFile, writeFiles, dispose }

2. createBashTool({ sandbox, skills, ...bashOptions })
   └─ Returns: { bash, tools, sandbox, skills }

The DockerSandbox interface still matches what createBashTool expects from its sandbox parameter (executeCommand(command, options?) → { stdout, stderr, exitCode }), while also exposing Docker-only spawn(command, options?) for streaming stdout/stderr. This is why Docker sandboxes can be plugged directly into the shared createBashTool from @deepagents/context, which in turn delegates to the upstream bash-tool package.

Decorator Chain Behavior

createBashTool layers decorators over the backend sandbox. Both core decorators now preserve spawn when the backend exposes it:

  • withAbortSignal injects the ambient signal from runWithAbortSignal(...) into both executeCommand and spawn unless the call already provides options.signal.
  • observeSandboxFileEvents snapshots before and after spawn to record write/modify/delete events under destination, and delays exit resolution until the post-spawn snapshot is captured.

File Operations: Base64 Encoding

readFile and writeFiles use base64 encoding internally because buffered executeCommand uses nano-spawn, which strips trailing newlines from stdout:

readFile:   docker exec <id> sh -c 'base64 "/path/to/file"'  → decode on host
writeFiles: echo "<base64>" | base64 -d > "/path/to/file"    → decode in container

spawn bypasses nano-spawn and uses raw child-process streams so callers get byte-accurate live output and structured exit metadata.

Extending with Custom Strategies

Subclass DockerSandboxStrategy to add new container creation approaches:

import { DockerSandboxStrategy, type DockerSandbox } from '@deepagents/context';

class KubernetesStrategy extends DockerSandboxStrategy {
  protected async getImage(): Promise<string> {
    return 'my-registry/my-image:latest';
  }

  protected async configure(): Promise<void> {
    // Post-creation configuration
  }

  // Override exec/startContainer/stopContainer for k8s
}

Next Steps