---
title: "Adapters"
description: "The framework's two adapter seams: sandbox adapters swap the backend that runs the agent's run-code tool, and CLI adapters give the agent structured access to command-line tools."
search: "adapters sandbox adapter cli adapter run-code SandboxAdapter CliAdapter ShellCliAdapter durable runner remote sandbox edge serverless child_process"
---

# Adapters

<Callout id="doc-block-adap1who" tone="info">

**Who is this for:** host authors extending the runtime. App developers rarely
need this — the defaults work out of the box.

</Callout>

Agent-Native has two adapter seams that factor a concern out behind a narrow,
swappable interface:

- **Sandbox adapters** swap the backend that runs the agent's `run-code` tool —
  a local child process by default, or a Docker / remote / durable runner.
- **CLI adapters** give the agent structured access to command-line tools
  (`gh`, `ffmpeg`, `stripe`) with discovery, availability checks, and a
  consistent result shape.

Both share one runtime constraint: they rely on Node.js system bindings and do
not run on edge/worker runtimes — see [Edge and serverless](#edge-serverless).

## Which coding doc do I want? {#which-doc}

| You want to…                                                               | Use                                          |
| -------------------------------------------------------------------------- | -------------------------------------------- |
| Swap the backend that runs the agent's **`run-code` tool**                 | **Sandbox adapters** (this page)             |
| Wrap a CLI tool (`gh`, `ffmpeg`) for the agent to call                     | [CLI Adapters](/docs/cli-adapters)           |
| Render a Claude-Code/Codex-style **coding workspace UI**                   | [Agent-Native Code UI](/docs/code-agents-ui) |
| Run Claude Code / Codex / Pi **as the agent**, with their own loop + tools | [Harness Agents](/docs/harness-agents)       |

## Sandbox Adapters {#sandbox-adapters}

The `run-code` tool runs agent-supplied JavaScript in an isolated environment. **Sandbox adapters** factor the _execution_ concern out of that tool so the backend can be swapped — a local child process by default, or a Docker / remote / durable runner — without touching the agent loop, `run-code.ts`, the localhost bridge, the env scrub, or the output formatting.

### Why a seam {#why}

The default backend spawns a locked-down local Node child process. That's bounded by the hosting process: on the hosted platform it shares the agent loop's soft execution ceiling (~40s before timeout/continuation thrash). A remote or durable adapter is the lever to exceed that ceiling — it runs large data jobs to completion independently of the request lifecycle.

Keeping the contract narrow means a remote adapter inherits the same security posture. The parent process keeps ownership of everything secret-bearing: it builds the sandbox module, runs the localhost bridge (which holds the request context and applies host allowlists + SSRF guards), scrubs the env, and formats output. An adapter only receives an already-prepared, **non-secret** module source plus resource limits — it is responsible solely for _running_ it and capturing stdout/stderr/exit status.

<Diagram id="doc-block-1k1zjzo" title={"The parent keeps the secrets; the adapter only runs code"} summary={"run-code builds the module and runs the loopback bridge; the adapter receives a non-secret module + limits and returns stdout/stderr/exit."}>

```html
<div class="diagram-sandbox">
  <div class="diagram-box" data-rough>
    <strong>Parent process</strong
    ><small class="diagram-muted"
      >builds module · loopback bridge · env scrub · output format</small
    >
  </div>
  <div class="diagram-col">
    <div class="diagram-pill accent">non-secret module + limits &rarr;</div>
    <div class="diagram-pill ok">&larr; stdout / stderr / exitCode</div>
    <div class="diagram-pill">&harr; bridge calls (127.0.0.1)</div>
  </div>
  <div class="diagram-panel center" data-rough>
    <strong>SandboxAdapter.run</strong
    ><small class="diagram-muted"
      >local child · Docker · remote · durable</small
    >
  </div>
</div>
```

```css
.diagram-sandbox {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.diagram-sandbox .diagram-col {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.diagram-sandbox .center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
```

</Diagram>

### The interface {#interface}

The seam lives in core at `packages/core/src/coding-tools/sandbox/` — `adapter.ts` (the contract), `index.ts` (selection: `getSandboxAdapter()` / `registerSandboxAdapter()`), and `local-child-process-adapter.ts` (the default). It is wired in-package by `run-code.ts`; a host plugs in a different backend through the `index.ts` registration helper (or, for a Docker backend, via the [blueprint](/docs/blueprint-installer) that edits these files directly).

<FileTree
  id="doc-block-b1k8n3"
  title="The sandbox seam in core"
  entries={[
    {
      path: "adapter.ts",
      note: "the SandboxAdapter contract (SandboxRunRequest / SandboxRunResult)",
    },
    {
      path: "index.ts",
      note: "selection: getSandboxAdapter() / registerSandboxAdapter()",
    },
    {
      path: "background.ts",
      note: "durable queue marker and background execution lifecycle",
    },
    {
      path: "local-child-process-adapter.ts",
      note: "the default backend — locked-down Node child process",
    },
    {
      path: "../run-code.ts",
      note: "wires the seam; never changes when you swap backends",
    },
  ]}
/>

Every backend implements `SandboxAdapter`:

```ts
interface SandboxAdapter {
  /** Stable id, surfaced for diagnostics and adapter selection. */
  readonly id: string;
  /** Execute one prepared sandbox module and capture its output. */
  run(request: SandboxRunRequest): Promise<SandboxRunResult>;
}
```

The request and result are intentionally small and opaque:

```ts
interface SandboxRunRequest {
  /**
   * The complete ESM module source to execute. Already wraps the user's code
   * and embeds the loopback bridge URL/token; the adapter does NOT parse or
   * rewrite it.
   */
  moduleSource: string;
  /**
   * Scrubbed environment — only safe POSIX vars (PATH/HOME/TMPDIR/…), never app
   * secrets. Adapters must not augment this with the parent's own environment.
   */
  env: Record<string, string>;
  /** Hard wall-clock timeout in milliseconds. The adapter must enforce it. */
  timeoutMs: number;
  /**
   * Loopback port of the parent's bridge server (reachable over 127.0.0.1). A
   * remote adapter that can't reach the parent's loopback must tunnel or proxy
   * this to support bridge-backed globals (`appAction`, `providerFetch`, …).
   */
  bridgePort: number;
}

interface SandboxRunResult {
  stdout: string;
  stderr: string;
  /** `0` on clean exit, non-zero on failure, `null` when killed by a signal. */
  exitCode: number | null;
  /** True when the run was killed for exceeding `timeoutMs`. */
  timedOut: boolean;
}
```

### The default: `LocalChildProcessAdapter` {#default}

Out of the box, `getSandboxAdapter()` returns `LocalChildProcessAdapter` (`id: "local-child-process"`). It preserves the historical `run-code` behavior byte-for-byte:

- The prepared module source is written to a fresh temp dir.
- The child runs with the scrubbed env (no secrets), with `TMPDIR`/`TEMP`/`TMP` pointed inside the sandbox dir.
- When the Node permission model is available (`--permission`, or `--experimental-permission` on Node 20), the child is denied filesystem access outside its temp dir, plus child processes, workers, and native addons. Outbound network is _not_ blocked by the permission model — but the env scrub means such requests carry no credentials, and all authenticated calls go through the parent's loopback bridge.
- A timeout sends `SIGTERM`, then `SIGKILL` after a 2s grace period.
- Temp files are cleaned up best-effort after the run.

<Callout id="doc-block-adap1def" tone="warning">

The default adapter uses `node:child_process`, which does not exist on edge/worker runtimes. Run `run-code` in a standard Node.js environment, or register a remote adapter — see [Edge and serverless](#edge-serverless).

</Callout>

### Selecting an adapter {#selection}

Resolution order — an explicitly registered adapter wins; otherwise the env var selects a built-in; otherwise the local default is used:

```text
registerSandboxAdapter(adapter)  →  AGENT_NATIVE_SANDBOX  →  local default
```

#### `AGENT_NATIVE_SANDBOX` env var {#env}

Selects a built-in adapter by id. Both `local` (the default) and `background` are wired; unknown values fall back to local rather than failing the run.

```bash
AGENT_NATIVE_SANDBOX=local        # the default — explicit
AGENT_NATIVE_SANDBOX=background   # enqueue every run-code call durably
```

`background` stores the raw code in `sandbox_executions` and returns an `executionId` with `status: "queued"` immediately. A signed self-dispatch starts a fresh serverless invocation; long-lived Node starts detached in-process work; status polling and the periodic sweep redrive lost dispatches or expired leases. Poll the execution later for its persisted result.

`BackgroundQueueAdapter` is a durable queue marker, not the executor. Queued work uses a registered non-queued execution adapter when one is available; otherwise it falls back to the local child process. Selecting `background` alone does not provide remote or container isolation, and it does not make an edge runtime compatible with `node:child_process`.

#### `registerSandboxAdapter()` {#register}

A host process overrides the backend for all subsequent `run-code` invocations through the seam's `index.ts` — for example, to run every call in a remote container:

```ts
import {
  registerSandboxAdapter,
  type SandboxAdapter,
} from "./coding-tools/sandbox/index.js";

class RemoteSandboxAdapter implements SandboxAdapter {
  readonly id = "remote";
  async run(request) {
    // Ship request.moduleSource to the durable runner, enforce request.timeoutMs,
    // proxy bridge calls back to request.bridgePort, and return stdout/stderr/exitCode.
  }
}

registerSandboxAdapter(new RemoteSandboxAdapter());
// Pass `null` to clear the override and fall back to env-var / default resolution.
```

### The seam for a durable runner {#durable}

The built-in `background` adapter already provides durable queuing and a fresh execution budget, while actual execution still uses a non-queued adapter or the local child-process fallback. A separately registered remote or isolated adapter (Docker or a Vercel-Sandbox-style runner) can provide a different execution boundary and can also execute per-call background work.

Such an adapter would:

1. Implement `SandboxAdapter.run` against an out-of-process runtime.
2. Tunnel the loopback bridge (or proxy bridge calls back to the parent).
3. Supply remote or container isolation independently of the shipped durable queue lifecycle.

Register it programmatically via `registerSandboxAdapter()`. The agent loop and `run-code.ts` do not need to change.

<Callout id="doc-block-adap1tip" tone="info">

The `agent-native add sandbox docker` blueprint emits a full, self-contained recipe for implementing a Docker adapter against this seam. See [Blueprint Installer](/docs/blueprint-installer).

</Callout>

## CLI Adapters {#cli-adapters}

The other adapter seam wraps a single command-line tool (`gh`, `ffmpeg`, `stripe`, `aws`) behind a `CliAdapter` contract, so the agent can discover it, check whether it's installed, and run it with a consistent stdout/stderr/exit-code result.

See [CLI Adapters](/docs/cli-adapters) for the `CliAdapter` contract, `ShellCliAdapter`, `CliRegistry`, and the action-wrapping pattern — this page only covers the seam it shares with sandbox adapters.

## Edge and serverless {#edge-serverless}

<Callout id="doc-block-adap1edge" tone="warning">

Both adapter seams rely on Node.js system bindings. The sandbox `LocalChildProcessAdapter` and CLI adapters (`ShellCliAdapter` and custom adapters) use `node:child_process` (`execFile` / `spawn`), which **does not exist** on edge/worker runtimes such as Cloudflare Workers or Netlify Edge Functions. If you deploy server routes to these edge presets, executing these adapters throws a runtime exception. Run adapter endpoints and tasks in a standard Node.js environment (traditional server containers or serverless Node functions) — or, for the sandbox seam, register a remote adapter that ships work out of process.

</Callout>

## What's next

- [**CLI Adapters**](/docs/cli-adapters) — the quick reference for the CLI seam
- [**Blueprint Installer**](/docs/blueprint-installer) — `agent-native add sandbox docker` prints a Docker-adapter recipe
- [**Agent Teams**](/docs/agent-teams) — delegating heavy work to sub-agents
- [**Security**](/docs/security) — the env scrub and bridge allowlist posture
