Skip to content

About

createLambdaSandbox(): a HarnessV1SandboxProvider for the Vercel AI SDK v7 harness, backed by AWS Lambda MicroVMs. Run Claude Code / Codex in a Firecracker-isolated microVM.

Topics

Resources

Stars

7 stars

Watchers

0 watching

Forks

Repository files navigation

lambda-microvm-sandbox

createLambdaSandbox(): a HarnessV1SandboxProvider for the Vercel AI SDK v7 harness, backed by AWS Lambda MicroVMs.

Run coding-agent harnesses (Claude Code, Codex, OpenCode) inside a Firecracker-isolated, network-policy-controlled microVM in your own AWS account. Published as @theagenticguy/sandbox-lambda.

The AI SDK v7 harness ships HarnessAgent with two published sandbox backends: @ai-sdk/sandbox-vercel (Vercel's hosted product) and @ai-sdk/sandbox-just-bash (in-process, cannot run bridge-backed agents). The sandbox interface is open. This is a third backend that runs in your account, under your network policy, inside your isolation boundary, verified against Claude Code on Amazon Bedrock.

Why this exists

The harness sandbox interface accepts any backend that can run a process and expose a port. This package implements it on AWS Lambda MicroVMs, the Firecracker sandbox primitive AWS released in June 2026 with a documented pattern for sandboxing Claude Managed Agents. You get VM-level isolation, suspend/resume with preserved memory and disk, up to 8-hour sessions, and per-instance network policy, all driven from a standard HarnessAgent.

How it maps to the harness contract

The harness asks a provider for a session that runs commands, streams output, reads and writes files, exposes a port for its in-VM bridge, and applies a network policy. Lambda MicroVMs supplies each piece:

Harness needs Lambda MicroVMs
createSession() RunMicrovm (from a prebuilt image) + CreateMicrovmAuthToken
resumeSession({ sessionId }) GetMicrovm + ResumeMicrovm (state preserved by snapshot)
run / spawn (stream stdout/stderr + exit) HTTPS to the in-VM agent daemon's /exec (NDJSON stream)
readFile / writeFile the daemon's /fs/read and /fs/write
getPortUrl({ protocol: 'ws' }) a localhost relay that injects MicroVM auth onto the adapter's bridge WebSocket
stop() SuspendMicrovm (parks the session, resumable)
destroy() TerminateMicrovm
setNetworkPolicy(...) ingress/egress network connectors, applied at launch

Lambda MicroVMs exposes a per-instance HTTPS endpoint and no exec/file API. This package supplies the exec/file surface itself: a small zero-dependency agent daemon (image/agentd.mjs) baked into the image. The control plane talks to that daemon over the endpoint.

Architecture

flowchart TB
  subgraph host["Your process"]
    HA["HarnessAgent"]
    PROV["LambdaSandboxProvider<br/>(createLambdaSandbox)"]
    AC["AgentClient"]
    BR["BridgeRelay<br/>(localhost)"]
    HA --> PROV
    PROV --> AC
    PROV --> BR
  end

  subgraph vm["Lambda MicroVM (Firecracker)"]
    EP["public HTTPS endpoint<br/>mvm-….lambda-microvm.&lt;region&gt;.on.aws"]
    AGENTD[":9000 agentd.mjs<br/>/exec /fs /pwd /healthz + lifecycle hooks"]
    BRIDGE[":8090 harness bridge<br/>(spawned by the adapter)"]
    WS["workspace /workspace<br/>bash · git · node · agent CLI"]
    EP --> AGENTD
    EP --> BRIDGE
    AGENTD --- WS
    BRIDGE --- WS
  end

  AC -- "HTTPS · X-aws-proxy-auth (JWE) · X-aws-proxy-port: 9000" --> EP
  BR -- "wss · lambda-microvms.* subprotocols → port 8090" --> EP
Loading

The control plane reaches the in-VM daemon on port 9000 for exec and file operations over the JWE-authed HTTPS endpoint, and dials the harness bridge on port 8090 through a localhost relay. Port 8080 is avoided because it is the MicroVM default inbound port.

The bridge, and how bridge-backed agents connect

Claude Code and Codex run the agent runtime inside the sandbox. Their adapter spawns a bridge that binds a WebSocket server on a port, writes {type:'bridge-ready',port} to stdout, and the host dials in to that port via getPortUrl(...). just-bash cannot expose a reachable port, so it throws HarnessCapabilityUnsupportedError and cannot run those agents.

A Lambda MicroVM carries inbound WebSocket on its public HTTPS endpoint, routed to an in-VM port by the X-aws-proxy-port header or a lambda-microvms.port.<n> subprotocol. The endpoint requires the MicroVM JWE auth token, and the adapter opens its socket with a bare new WebSocket(url) that sets no headers. getPortUrl({ protocol: 'ws' }) therefore returns a localhost relay that this package runs in your process; the relay opens the upstream socket with the MicroVM subprotocols and forwards frames in both directions. The adapter stays unmodified. Two independent auth layers run end to end: the MicroVM JWE (outer, terminated by Lambda) and the bridge's own token (inner, terminated by the bridge).

sequenceDiagram
  participant A as Claude Code adapter
  participant R as BridgeRelay (localhost)
  participant E as MicroVM endpoint
  participant B as in-VM bridge (:8090)
  A->>A: spawn bridge, read {bridge-ready, port}
  A->>R: new WebSocket(getPortUrl(ws) + ?agent_bridge_token)
  R->>E: wss + lambda-microvms.authentication.<jwe> + lambda-microvms.port.8090
  E->>B: forward upgrade (strips lambda-microvms.* subprotocols)
  B-->>A: bridge-hello (via relay)
  A->>B: start (prompt, tools)
  B-->>A: stream-start / text-delta / tool-call / finish
Loading

Install

Not yet published to npm. Build from source:

git clone https://github.com/theagenticguy/lambda-microvm-sandbox.git
cd lambda-microvm-sandbox
pnpm install
pnpm build

Then depend on the local build from your project, for example with a file/workspace reference in package.json:

{
  "dependencies": {
    "@theagenticguy/sandbox-lambda": "file:../lambda-microvm-sandbox"
  }
}

Or install straight from the Git URL:

pnpm add github:theagenticguy/lambda-microvm-sandbox

Your project also needs the harness peers, which the harness owns the versions of: @ai-sdk/harness, @ai-sdk/provider-utils, and ws. @aws-sdk/client-lambda-microvms ships as a direct dependency of this package.

pnpm add @ai-sdk/harness @ai-sdk/provider-utils ws

When it is published, the line above becomes pnpm add @theagenticguy/sandbox-lambda.

Usage

import { HarnessAgent } from '@ai-sdk/harness/agent';
import { createClaudeCode } from '@ai-sdk/harness-claude-code';
import { createLambdaSandbox } from '@theagenticguy/sandbox-lambda';

const sandbox = createLambdaSandbox({
  imageIdentifier: process.env.SANDBOX_IMAGE_ARN!, // build with infra/build-image.mjs
  region: 'us-east-1',
  executionRoleArn: process.env.SANDBOX_ROLE_ARN!,
  agentToken: process.env.SANDBOX_AGENT_TOKEN!,     // a stable value enables resume
  defaultNetworkPolicy: {
    mode: 'custom',
    allowedHosts: ['api.anthropic.com', 'registry.npmjs.org', 'github.com'],
    deniedCIDRs: ['169.254.169.254/32'],            // block the instance metadata IP
  },
  vpcEgressConnectorArn: process.env.SANDBOX_VPC_EGRESS_CONNECTOR_ARN,
});

const agent = new HarnessAgent({
  harness: createClaudeCode({ auth: { anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! } } }),
  sandbox,
});

// The harness agent is stateless; spawn a session, pass it on every turn.
const session = await agent.createSession({ sessionId: 'my-session' });
const result = await agent.stream({ prompt: 'Refactor utils.ts and run the tests.', session });
for await (const part of result.fullStream) {
  if (part.type === 'text-delta') process.stdout.write(part.text);
}
await session.destroy(); // or session.stop() to suspend (resumable)

The factory is synchronous and does no I/O; the MicroVM launches lazily inside the provider's createSession(). For direct use of one MicroVM without the harness:

const session = await sandbox.createSession();
await session.restricted().writeTextFile({ path: 'hello.txt', content: 'hi' });
const { stdout } = await session.restricted().run({ command: 'cat hello.txt' });
await session.stop(); // suspend (resumable); session.destroy() terminates

session.restricted() returns the tool-safe Experimental_SandboxSession (file I/O and exec, no infra controls), suitable to hand to AI SDK tools. The network session itself carries getPortUrl, setNetworkPolicy, and stop, which only the harness should reach.

Running Claude Code on Amazon Bedrock

The MicroVM image bakes the Bedrock environment so Claude Code uses the execution role's credentials via IMDS, with no API key on the control plane. The image sets, verbatim from the Claude Code Bedrock docs:

ENV CLAUDE_CODE_USE_BEDROCK=1
ENV ANTHROPIC_DEFAULT_OPUS_MODEL=us.anthropic.claude-opus-4-8
ENV ANTHROPIC_MODEL=us.anthropic.claude-opus-4-8

Grant the execution role bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream, bedrock:ListInferenceProfiles, and bedrock:GetInferenceProfile (see infra/roles.cfn.yaml). No auth block is needed on createClaudeCode() for the Bedrock path; the CLI reads the model and credentials from the VM environment.

Running Codex on Amazon Bedrock

The same image serves the codex harness adapter. It bakes the Codex CLI and a /root/.codex/config.toml that points Codex at Bedrock GPT-5.5:

model = "openai.gpt-5.5"
model_provider = "amazon-bedrock"
approval_policy = "never"
sandbox_mode = "danger-full-access"

[model_providers.amazon-bedrock.aws]
region = "us-east-2"

Codex authenticates with the execution role's credentials (or AWS_BEARER_TOKEN_BEDROCK if set), so no OpenAI key is needed. Two requirements differ from the Claude Code path:

  • Region. GPT-5.5 on Bedrock is us-east-2 only, so a Codex session must run the MicroVM in us-east-2 (createLambdaSandbox({ region: 'us-east-2' })). Build the image with CODEX_REGION/CODEX_MODEL Docker build args to change this.
  • IAM. The execution role needs the AWS-managed AmazonBedrockMantleInferenceAccess policy; a hand-rolled bedrock:CallWithBearerToken statement returns 401. Deploy infra/roles.cfn.yaml with EnableCodexBedrock=true.

See examples/codex-on-lambda.ts. createCodex() needs no auth block on the Bedrock path, and Codex requires permissionMode: 'allow-all'.

Network policy

MicroVM network connectors are fixed at RunMicrovm and cannot change on a running instance, so policy is a create-time decision:

  • defaultNetworkPolicy is applied at launch and is the policy the session runs with.
  • 'allow-all' uses the managed ALL_INGRESS + INTERNET_EGRESS connectors.
  • 'deny-all' keeps ingress for the bridge and attaches no egress connector, leaving no outbound path.
  • 'custom' attaches ingress plus your vpcEgressConnectorArn, whose security groups and AWS Network Firewall realize the host allow-list. Without a connector, 'custom' falls back to deny-all egress and logs a warning.
  • setNetworkPolicy(...) at runtime succeeds as a no-op when it re-asserts the launch policy, and throws HarnessCapabilityUnsupportedError when it would tighten or change the policy. Create a fresh session for a new policy.

Configuration

See src/settings.ts for the full LambdaSandboxSettings. The settings that matter most:

Setting Default Notes
imageIdentifier (required) The CreateMicrovmImage ARN.
region SDK default One of the 5 GA regions.
executionRoleArn none Runtime IAM (logs, Bedrock invoke, Secrets Manager for the model key).
agentToken random per session Set a stable value to enable resumeSession.
maxDurationSeconds 28800 (8h) Platform hard cap.
idlePolicy suspend at 15m idle, terminate at +30m, auto-resume Cheap to park between turns.
agentPort 9000 Control/daemon port. 8080 is the MicroVM default inbound port.
bridgePort 8090 The one exposed port; the adapter binds it.
workspaceDir /workspace Must match the image.
defaultNetworkPolicy allow-all Applied at launch, immutable after.
vpcEgressConnectorArn none Required to realize a custom allow-list.

Provisioning

The image and roles are provisioned out of band; see infra/README.md. Deploy infra/roles.cfn.yaml, then run infra/build-image.mjs (Node) or infra/build-image.sh to register the MicroVM image from image/Dockerfile.

Lambda MicroVM limits (from AWS docs)

Every figure below is from current AWS documentation (June 2026), cited per row.

Endpoint and connection

The MicroVM endpoint is a transparent reverse proxy, so it carries none of API Gateway WebSocket's blocking limits:

Concern MicroVM endpoint API Gateway WS, for contrast
Per-request / integration timeout None documented. A request lives as long as the app holds it, bounded by the session's maximumDurationInSeconds (≤8h). 29s, unraisable
Connection max duration None documented beyond the 8h session cap. 2h, unraisable
Idle behavior The idle policy (maxIdleDurationSeconds, default 300s) suspends the VM, and with autoResumeEnabled the next request resumes it. The socket is not hard-closed on idle. (idle policy) 10min idle close, unraisable
Message / frame size cap None documented for the proxy. 128KB msg / 32KB frame
Binary frames Supported (HTTP/2, gRPC, raw WebSocket pass through). Not supported
Protocols HTTP/1.1, HTTP/2, WebSocket, gRPC, SSE. (networking) WebSocket only
Ports Any port except 0–1024, with 80 and 443 allowed. Routed by X-aws-proxy-port header or lambda-microvms.port.N WS subprotocol; default 8080. Must be within the auth token's allowedPorts or the request gets 403. (networking console) n/a
Auth JWE token in X-aws-proxy-auth (or the lambda-microvms.authentication.<jwe> subprotocol). Token expiry ≤60 min, port-scoped. No unauthenticated access. (security) n/a
Endpoint error codes 400 bad port/proto, 403 bad/expired token or disallowed port, 429 rate limit, 502 app down or resume failed, 500 internal. (networking) n/a

The docs do not state a proxy-level message-size cap, request timeout, or connection-max-duration. No published limit does not mean no limit; the practical ceiling is bandwidth (below) and the 8-hour session. The two facts the bridge depends on are the absence of a 29s integration cap and the absence of an idle hard-close, which is why API Gateway was unsuitable.

Bandwidth (scales with VM size, applies to all endpoint traffic)

Baseline size Max endpoint bandwidth
0.5 GB / 0.25 vCPU 1 MB/s
1 GB / 0.5 vCPU 2 MB/s
2 GB / 1 vCPU (default) 4 MB/s
4 GB / 2 vCPU 8 MB/s
8 GB / 4 vCPU 16 MB/s

Source: networking. This bandwidth caps streaming agent output; size the VM accordingly.

Per-MicroVM throughput (hard, not adjustable)

Concern Limit
Concurrent connections per VM 8 (1 vCPU), 16 (2), 32 (4), 64 (8), 128 (16 vCPU)
Requests/sec per VM 40 (4 vCPU/8 GB) to 160 (16 vCPU/32 GB)

Source: Lambda quotas. One coding-agent session fits comfortably; this matters when multiplexing many bridge connections through one VM.

Compute, storage, session

Concern Limit
Memory across all MicroVMs (per account/Region) 400 GB (1,024 GB in IAD/PDX/CMH/NRT), burstable 4×, adjustable
Per-VM size (baseline → peak, disk) 0.5GB/0.25vCPU→2GB/1vCPU/8GB, up to 8GB/4vCPU→32GB/16vCPU/32GB disk
Max session duration 8h (28,800s), hard
Architecture ARM64 / Graviton only, hard
Disk 8 GB at the three smaller sizes; 16 GB / 32 GB at the two largest

Sources: quotas, sizing.

API control-plane rate limits (per account/Region, all adjustable)

Operation TPS / Burst
RunMicrovm 5 / 5
ResumeMicrovm 5 / 5
SuspendMicrovm 2 / 2
TerminateMicrovm 10 / 10
GetMicrovm 100 / 100
CreateMicrovmAuthToken 50 / 50
CreateMicrovmShellAuthToken 5 / 5

Source: quotas. Raise RunMicrovm (5 TPS) before a fleet rollout, since it caps cold-launch rate. SuspendMicrovm (2 TPS) is the next limit to raise when parking sessions aggressively.

Image, runtime, lifecycle

Concern Limit
runHookPayload (per-VM config to /run) 16 KB string, hard
Images per account/Region 100, adjustable
Versions per image 50, adjustable
Concurrent image builds 5 (10 in IAD/PDX/CMH/NRT), adjustable
/ready + /validate build-hook timeouts 1–3,600s each (readyTimeoutInSeconds / validateTimeoutInSeconds)
Idle policy: suspend after maxIdleDurationSeconds, default 300s, max 28,800s
Idle policy: terminate after suspended suspendedDurationSeconds, default 300s
OS capabilities default restricted; additionalOsCapabilities: ["ALL"] for FUSE/eBPF/netns
Base image Amazon Linux 2023 (public.ecr.aws/lambda/microvms:al2023-minimal); private ECR allowed
Snapshot retention 1-week minimum (pricing)

Sources: launching, images, pricing.

Security model

The isolation boundary is the Firecracker MicroVM plus its network connectors. The agent runs untrusted code as root inside the VM; the VM and the egress connectors are what contain it.

  • Endpoint auth. Every request to the MicroVM endpoint carries a JWE (X-aws-proxy-auth), minted by CreateMicrovmAuthToken, scoped to the instance and its ports, expiring in ≤60 minutes. The provider re-mints before expiry (the TokenManager) so an 8-hour session never loses auth, and retries once on a 401/403.
  • Daemon shared secret. The in-VM control daemon also requires a per-session secret (x-sandbox-agent-token). This gates the control routes against a leaked endpoint JWE. It is not a boundary against the agent inside the same VM, which can exec directly; do not rely on it for that. The daemon fails closed: with no secret configured it refuses every protected route.
  • Filesystem confinement. The daemon's /fs/read and /fs/write resolve through realpath and are confined to an allow-list of roots (the workspace, HOME, /tmp). A leaked endpoint token cannot read /etc/shadow or the run-hook payload through /fs.
  • Egress. Outbound network access is governed by the connectors chosen at launch (see Network policy). A deny-all or custom policy is the enforced control over what the agent can reach; the egress allow-list belongs in the VPC connector's security groups and AWS Network Firewall.
  • Hardening already applied. Constant-time secret comparison, a request-body size cap, generic error responses (detail logged in-VM, not echoed to the caller), and an optional /exec wall-clock timeout.

Constraints

  • ARM64 only (Graviton). Build the image --platform=linux/arm64.
  • 5 GA regions: us-east-1, us-east-2, us-west-2, ap-northeast-1, eu-west-1.
  • 8-hour hard session cap. For longer work, checkpoint to a fresh session.
  • RunMicrovm 5 TPS and SuspendMicrovm 2 TPS defaults; raise both before fleet use.
  • Per-VM connection and RPS ceilings scale with vCPU; size the VM for the bridge's connection count, not CPU need alone.
  • Network connectors are immutable per instance (see Network policy).
  • Snapshot correctness: values that must be unique per session (host keys, random secrets) should be generated in the /run lifecycle hook, not baked into the image, since every instance resumes from the same snapshot.
  • Undocumented endpoint ceilings: no published proxy-level message-size, request-timeout, or connection-max-duration. The long-lived bridge depends on their absence; if a future limit appears, the relay is where it would be handled.

Image requirements

Each requirement below was a failure mode resolved against live AWS. image/Dockerfile and infra/build-image.mjs encode all of them.

  • Control/hook server on port 9000, not 8080. Port 8080 is the MicroVM default inbound port; sharing it means the /ready and /validate build probes never arrive and the image build fails.
  • Enable both ready and validate hooks in CreateMicrovmImage (the build runs a post-snapshot validation MicroVM).
  • AWS_REGION is a reserved key in CreateMicrovmImage.environmentVariables; set it via the Dockerfile ENV, not the API.
  • Node 24 from NodeSource (20.4+ suffices), not AL2023's dnf nodejs (v18). The harness bridge's @anthropic-ai/claude-agent-sdk uses Symbol.asyncDispose/using; on Node 18 the turn fails with TypeError: Object not disposable.
  • IS_SANDBOX=1 so Claude Code allows --dangerously-skip-permissions as root inside the VM.
  • COREPACK_ENABLE_DOWNLOAD_PROMPT=0 so the bootstrap's corepack-pinned pnpm downloads without prompting.
  • PNPM_CONFIG_DANGEROUSLY_ALLOW_ALL_BUILDS=true so pnpm install --frozen-lockfile runs claude-code's install.cjs (which fetches the native claude binary) instead of exiting 1 on ERR_PNPM_IGNORED_BUILDS.
  • python3 if agent tasks use it.

Tests

Three layers, run with pnpm test (unit), pnpm test:e2e (local, no AWS), and pnpm test:live (live AWS). See test/README.md:

  • Unit (vitest, 18 tests): contract conformance, network-policy enforcement, the bridge relay's URL and subprotocol shape, an AgentClient round-trip over a local stand-in, and TokenManager refresh/invalidate/coalesce behavior.
  • Local end-to-end: an agentd security suite (fail-closed, token check, /fs confinement, body cap, /exec timeout) plus the data-plane suite, which runs the compiled provider and the actual agentd.mjs behind an emulated MicroVM edge that enforces the documented endpoint contract. The bridge path connects the published @ai-sdk/harness SandboxChannel through the BridgeRelay and verifies the bridge-hello handshake and full-duplex streaming.
  • Live AWS: launches a live MicroVM and runs RunMicrovm → CreateMicrovmAuthToken → endpoint GET → SuspendMicrovm → ResumeMicrovm → TerminateMicrovm.

References

Vercel AI SDK v7 and the harness

AWS Lambda MicroVMs

Claude Code on Amazon Bedrock

Status

Verified against live AWS for both harness adapters, each driving a real write + bash tool-call inside a Firecracker MicroVM with credentials from the execution role via IMDS and no provider API key:

  • test/live-claude-code.mjs: a Claude Code turn on Bedrock Opus 4.8 (us.anthropic.claude-opus-4-8) in us-east-1.
  • test/live-codex.mjs: a Codex turn on Bedrock GPT-5.5 (openai.gpt-5.5) in us-east-2, over the Bedrock Mantle endpoint. Two things the Codex path requires, both learned here against live AWS: the execution role needs the AWS-managed AmazonBedrockMantleInferenceAccess policy, and createCodex({ model: 'openai.gpt-5.5' }) must be set explicitly because the adapter defaults to gpt-5.3-codex, which Bedrock does not serve. The image bakes the Codex config.toml at $WORKSPACE_DIR/.codex/ (the daemon runs commands with HOME=$WORKSPACE_DIR, so a config at /root/.codex alone is not found and Codex falls back to api.openai.com).

This is experimental software. The AI SDK harness and AWS Lambda MicroVMs are both new in 2026; pin versions.

License

Apache-2.0.

About

createLambdaSandbox(): a HarnessV1SandboxProvider for the Vercel AI SDK v7 harness, backed by AWS Lambda MicroVMs. Run Claude Code / Codex in a Firecracker-isolated microVM.

Topics

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages