createLambdaSandbox(): aHarnessV1SandboxProviderfor 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.
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.
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.
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.<region>.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
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.
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
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 buildThen depend on the local build from your project, for example with a file/workspace reference in package.json:
Or install straight from the Git URL:
pnpm add github:theagenticguy/lambda-microvm-sandboxYour 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 wsWhen it is published, the line above becomes pnpm add @theagenticguy/sandbox-lambda.
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() terminatessession.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.
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-8Grant 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.
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 withCODEX_REGION/CODEX_MODELDocker build args to change this. - IAM. The execution role needs the AWS-managed
AmazonBedrockMantleInferenceAccesspolicy; a hand-rolledbedrock:CallWithBearerTokenstatement returns 401. Deployinfra/roles.cfn.yamlwithEnableCodexBedrock=true.
See examples/codex-on-lambda.ts. createCodex() needs no auth block on the Bedrock path, and Codex requires permissionMode: 'allow-all'.
MicroVM network connectors are fixed at RunMicrovm and cannot change on a running instance, so policy is a create-time decision:
defaultNetworkPolicyis applied at launch and is the policy the session runs with.'allow-all'uses the managedALL_INGRESS+INTERNET_EGRESSconnectors.'deny-all'keeps ingress for the bridge and attaches no egress connector, leaving no outbound path.'custom'attaches ingress plus yourvpcEgressConnectorArn, 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 throwsHarnessCapabilityUnsupportedErrorwhen it would tighten or change the policy. Create a fresh session for a new policy.
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. |
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.
Every figure below is from current AWS documentation (June 2026), cited per row.
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.
| 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.
| 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.
| 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 |
| 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.
| 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.
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 byCreateMicrovmAuthToken, scoped to the instance and its ports, expiring in ≤60 minutes. The provider re-mints before expiry (theTokenManager) 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/readand/fs/writeresolve throughrealpathand are confined to an allow-list of roots (the workspace,HOME,/tmp). A leaked endpoint token cannot read/etc/shadowor the run-hook payload through/fs. - Egress. Outbound network access is governed by the connectors chosen at launch (see Network policy). A
deny-allorcustompolicy 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
/execwall-clock timeout.
- 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.
RunMicrovm5 TPS andSuspendMicrovm2 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
/runlifecycle 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.
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
/readyand/validatebuild probes never arrive and the image build fails. - Enable both
readyandvalidatehooks inCreateMicrovmImage(the build runs a post-snapshot validation MicroVM). AWS_REGIONis a reserved key inCreateMicrovmImage.environmentVariables; set it via the DockerfileENV, not the API.- Node 24 from NodeSource (20.4+ suffices), not AL2023's
dnf nodejs(v18). The harness bridge's@anthropic-ai/claude-agent-sdkusesSymbol.asyncDispose/using; on Node 18 the turn fails withTypeError: Object not disposable. IS_SANDBOX=1so Claude Code allows--dangerously-skip-permissionsas root inside the VM.COREPACK_ENABLE_DOWNLOAD_PROMPT=0so the bootstrap's corepack-pinned pnpm downloads without prompting.PNPM_CONFIG_DANGEROUSLY_ALLOW_ALL_BUILDS=truesopnpm install --frozen-lockfileruns claude-code'sinstall.cjs(which fetches the nativeclaudebinary) instead of exiting 1 onERR_PNPM_IGNORED_BUILDS.python3if agent tasks use it.
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
AgentClientround-trip over a local stand-in, andTokenManagerrefresh/invalidate/coalesce behavior. - Local end-to-end: an agentd security suite (fail-closed, token check,
/fsconfinement, body cap,/exectimeout) plus the data-plane suite, which runs the compiled provider and the actualagentd.mjsbehind an emulated MicroVM edge that enforces the documented endpoint contract. The bridge path connects the published@ai-sdk/harnessSandboxChannelthrough theBridgeRelayand verifies thebridge-hellohandshake and full-duplex streaming. - Live AWS: launches a live MicroVM and runs
RunMicrovm→CreateMicrovmAuthToken→ endpoint GET →SuspendMicrovm→ResumeMicrovm→TerminateMicrovm.
Vercel AI SDK v7 and the harness
- AI SDK 6/7 announcement and agent abstractions: ai-sdk.dev/docs/announcing-ai-sdk-6-beta and the AI SDK blog.
- Harness package source:
packages/harness, specificallyharness-v1-sandbox-provider.tsandharness-v1-network-sandbox-session.ts. - The base
Experimental_SandboxSessiontype:packages/provider-utils/src/types/sandbox.ts. - Reference providers this mirrors:
@ai-sdk/sandbox-verceland@ai-sdk/sandbox-just-bash. - Harness adapters:
@ai-sdk/harness-claude-code,@ai-sdk/harness-codex.
AWS Lambda MicroVMs
- Launch announcement: Run isolated sandboxes with full lifecycle control.
- Developer guide: overview, running & using, networking, images, security.
- Quotas and limits.
- Using Lambda MicroVMs as a sandbox for Claude Managed Agents.
- SDK:
@aws-sdk/client-lambda-microvms(API model2025-09-09).
Claude Code on Amazon Bedrock
- Claude Code on Amazon Bedrock.
- Claude Code settings and environment variables.
- Bedrock inference profiles, including the
us.anthropic.claude-opus-4-8cross-region profile.
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-managedAmazonBedrockMantleInferenceAccesspolicy, andcreateCodex({ model: 'openai.gpt-5.5' })must be set explicitly because the adapter defaults togpt-5.3-codex, which Bedrock does not serve. The image bakes the Codexconfig.tomlat$WORKSPACE_DIR/.codex/(the daemon runs commands withHOME=$WORKSPACE_DIR, so a config at/root/.codexalone is not found and Codex falls back toapi.openai.com).
This is experimental software. The AI SDK harness and AWS Lambda MicroVMs are both new in 2026; pin versions.
{ "dependencies": { "@theagenticguy/sandbox-lambda": "file:../lambda-microvm-sandbox" } }