pleach
Coding agent

Sandbox providers

The SandboxProvider facade over @pleach/sandbox's three endpoint-shape adapters, plus the SandboxComposite high-level tool surface coding agents reach for.

@pleach/coding-agent/sandbox ships a small facade — SandboxProvider — that sits ABOVE the three endpoint-shape adapters in @pleach/sandbox (httpStream / httpPoll / childProcess). The adapters know how to talk to a vendor; the facade decides when to acquire, when to reuse a pooled session, and what high-level value the coding-agent loop holds across a turn.

The shape this page documents is the layer ABOVE the SandboxAdapter contract in @pleach/core (see Sandbox for the lower layer) — not a replacement.

What you get

Two factories. Both vendor-neutral; vendor coupling lives in the underlying @pleach/sandbox adapter.

import {
  createPooledSandboxProvider,
  createSandboxComposite,
} from "@pleach/coding-agent/sandbox";
FactoryWhat it returnsWhen you reach for it
createPooledSandboxProvider({ adapter, poolSize? })SandboxProvider with acquire() / release()Once per runtime. The provider holds a bounded pool of warm sessions.
createSandboxComposite({ session })SandboxComposite with install / upload / download / diff / env / gitClone / benchmarkOnce per acquired session, inside the tool loop.

The provider is the long-lived value. The composite is the per-session value. The coding-agent runtime holds the provider for its lifetime and constructs a composite each time the agent's loop enters a turn.

The three endpoint shapes (below)

The provider's adapter field is one of the three endpoint-shape adapters from @pleach/sandbox:

AdapterWire shapeWhere vendors compose in
createHttpStreamSandboxProviderREST + text/event-stream for /execute/streamModal, Vercel Sandbox, E2B (streaming variant)
createHttpPollSandboxProviderREST; executeStream buffers and emits one terminal chunkDaytona, Azure Container Instances
createChildProcessSandboxProviderchild_process.spawn + node:fs/promisesLocal dev, in-CI sandboxing

Plus a first-class AWS Fargate provider (createFargateSandboxProvider) that owns the ECS task lifecycle and delegates the in-container surface to the canonical HTTP + SSE shape.

Vendor wiring is one-page cookbook recipes — see packages/sandbox/ADAPTERS.md for full configurations covering Modal, E2B, Daytona, Vercel Sandbox, AWS Fargate, Azure Container Instances, and local Docker. Each entry shows the auth callback, path overrides, and capability hints for the vendor.

Pooled provider

import { createPooledSandboxProvider } from "@pleach/coding-agent/sandbox";
import { createHttpStreamSandboxProvider } from "@pleach/sandbox";

const adapter = createHttpStreamSandboxProvider({
  baseURL: process.env.MODAL_SANDBOX_BASE_URL!,
  auth: async () => ({
    headers: { Authorization: `Bearer ${process.env.MODAL_TOKEN!}` },
  }),
});

export const provider = createPooledSandboxProvider({
  adapter,
  poolSize: 1, // default — per-runtime singleton
});

poolSize defaults to 1, which matches the chat-app baseline (one session warm at a time). Raise it for parallel-verify swarms — when each sub-agent in a swarm needs its own isolated workspace, set poolSize: 4 (or however many sub-agents you fan out to).

Acquire and release

const session = await provider.acquire({
  workspaceDir: "/workspace",
  runtime: { language: "node" },
});

try {
  await session.client.exec("npm install");
  // ...do work...
} finally {
  await provider.release(session);
}

The session is a { id, client, runtime, handle } value. The coding-agent tool factories (file tools) consume session.client — the 3-method SandboxClient contract. session.handle is the underlying @pleach/sandbox handle, opaque for the typical case; reach for it only when you need the wider 8-method @pleach/sandbox SandboxProvider contract (listFiles / isAlive / capabilities / executeStream).

Idempotency on sessionId

const a = await provider.acquire({ sessionId: "chat-42" });
const b = await provider.acquire({ sessionId: "chat-42" });
// a === b — pool short-circuits on the caller-supplied id

When the caller supplies a sessionId that's already pooled, the provider returns the existing session whether idle or in-use. This is the load-bearing primitive for re-attaching to a session across a restart — pair it with a persistent chatIdsandboxSessionId mapping at the host layer.

The composite tool surface

SandboxComposite is the layer the coding-agent loop holds for the session's lifetime. Seven composite ops, each a thin shell over session.client.exec + readFile + writeFile.

import { createSandboxComposite } from "@pleach/coding-agent/sandbox";

const composite = createSandboxComposite({ session });
OpReturnsNotes
install(packages, opts?)SandboxExecResultProbes for lockfile (pnpm / yarn / bun / npm / poetry / uv / pip) when manager: "auto"; falls back to npm.
upload(local, remote, opts?)voidReads local via node:fs/promises (host) and writes via session.client.writeFile.
download(remote, local, opts?)voidSymmetric — reads from sandbox, writes to host.
diff(pathA, pathB, opts?){ unifiedDiff, differs }Shells out to diff -u; differs is true when exit code is 1.
env.get(name) / env.set(name, value) / env.list()string | undefined / string / Record<string, string>set writes to .pleach-env; consumer sources it before subsequent execs.
gitClone(url, opts?)SandboxExecResultDefaults: --depth 1, destination = repo basename.
benchmark(command, opts?)BenchmarkResultN iterations (default 3); returns durations, mean, min, max, per-iteration exit codes.

install — package-manager probe

The auto mode runs one probe exec, looking for lockfiles in order: pnpm-lock.yaml, yarn.lock, bun.lockb, package-lock.json, poetry.lock, uv.lock, requirements.txt. First hit wins; no match falls through to npm. Pin a manager to skip the probe:

await composite.install(["zod", "valibot"], {
  manager: "pnpm",
  dev: true,
});

env.set and the .pleach-env file

The composite does NOT mutate session-wide env in-band — most sandbox transports don't expose a "persist env" wire op. Instead, env.set appends to a .pleach-env file at the workspace root; subsequent execs source it explicitly:

await composite.env.set("FOO", "bar");

await session.client.exec("set -a; . .pleach-env; set +a && my-tool");

env.list runs printenv against the live session — its result reflects what the sandbox sees right now, not what .pleach-env contains. Treat the two as orthogonal.

benchmark for cost-aware loops

const result = await composite.benchmark("pnpm vitest run", {
  iterations: 5,
  timeoutMs: 60_000,
});
console.log(result.meanMs, result.minMs, result.maxMs);
console.log(result.exitCodes); // any non-zero indicates a failed iteration

The composite does NOT warm up the workspace between iterations — callers handle that out of band. Pair with eval and replay to capture per-iteration durations into the audit ledger.

Wiring into a coding-agent runtime

import { createCodingAgentRuntime } from "@pleach/coding-agent/runtime";
import { createPooledSandboxProvider } from "@pleach/coding-agent/sandbox";
import { createHttpStreamSandboxProvider } from "@pleach/sandbox";

const adapter = createHttpStreamSandboxProvider({
  baseURL: process.env.MODAL_SANDBOX_BASE_URL!,
  auth: async () => ({
    headers: { Authorization: `Bearer ${process.env.MODAL_TOKEN!}` },
  }),
});

const provider = createPooledSandboxProvider({ adapter, poolSize: 1 });

const runtime = createCodingAgentRuntime({
  sandboxProvider: provider,
  // ...rest of SessionRuntimeConfig
});

await runtime.start({ sessionLabel: "chat-42" });

The runtime owns the provider's lifetime — runtime.stop() invokes the provider's dispose / destroy / shutdown if present (duck-type dispatch).

What's vendor-neutral, what isn't

The provider, the composite, and the tool factories are vendor-neutral by construction. The only vendor-coupling point is the adapter you pass to createPooledSandboxProvider. Swap the adapter — keep everything else.

The composite does NOT branch on session.handle.provider. Vendor optimizations (a vendor's bulk-upload endpoint, a vendor's faster exec shape) belong on the underlying adapter, not in the composite. This keeps the surface stable as vendors come and go.

Where to go next

On this page