AWS Builder Center
Run Vercel AI SDK harness sandboxes in your own AWS account

Run Vercel AI SDK harness sandboxes in your own AWS account

The Vercel AI SDK v7 harness (@ai-sdk/harness) runs coding agents in a sandbox behind an open interface, and both shipped backends run on Vercel's hosted product or in-process. createLambdaSandbox() implements that interface on AWS Lambda Firecracker microVMs, so the sandbox runs in your account under your own network policy.

Run Vercel AI SDK harness sandboxes in your own AWS account on AWS Lambda MicroVMs

On AWS Lambda MicroVMs. Tags: #vercel-ai-sdk #ai-sdk-harness #sandbox #aws-lambda #firecracker #microvm #serverless #amazon-bedrock
The Vercel AI SDK v7 harness  runs a coding agent inside a sandbox, and the sandbox is a pluggable backend behind an open interface. Vercel ships two: @ai-sdk/sandbox-vercel , which runs on their hosted product, and @ai-sdk/sandbox-just-bash , which runs in-process and can't back bridged agents at all. Both put the sandbox on infrastructure you don't control. I wanted the sandbox in my own AWS account, under my own network policy, inside my own isolation boundary, so I built a third backend on AWS Lambda MicroVMs . It implements the same HarnessV1SandboxProvider  interface, so any agent the harness can drive runs on it unchanged. To prove that, I've run both Claude Code and Codex on it against Amazon Bedrock, each completing a full agent turn with real tool calls. Here's how the provider works, and how you drop it into a HarnessAgent.

TL;DR

createLambdaSandbox() is a HarnessV1SandboxProvider. Hand it to a HarnessAgent and every coding-agent turn runs inside a Firecracker MicroVM in your account:
  • A drop-in harness backend. It implements the same open sandbox interface as @ai-sdk/sandbox-vercel and @ai-sdk/sandbox-just-bash, so agents run on it with no agent-side changes. Claude Code and Codex are the two I've verified live.
  • Isolation you own. Each session is a Firecracker MicroVM in your AWS account. The agent runs untrusted code as root inside the VM; the VM and its network connectors are the isolation boundary.
  • Network policy per instance. Launch a session allow-all, deny-all, or custom with a host allow-list realized by a VPC egress connector and AWS Network Firewall. Egress is a create-time decision that can't drift once the session starts.
  • Suspend and resume. session.stop() maps to SuspendMicrovm, which preserves memory and disk and resumes in under a second. Park a session between turns cheaply; sessions run up to 8 hours.
  • Bedrock with no shipped secret. The image bakes the model wiring, so the agent reads its model and credentials from the execution role via IMDS, no provider key in the VM. My live tests exercise this on Bedrock: Claude Code on Opus 4.8, Codex on GPT-5.5.
The package is @theagenticguy/sandbox-lambda. It depends on @aws-sdk/client-lambda-microvms and takes the harness as peers, pinned to @ai-sdk/harness@^1.0.0 alongside @ai-sdk/provider-utils and ws.

Prerequisites

To follow along you'll want a MicroVM-capable AWS account in one of the five GA regions (us-east-1, us-east-2, us-west-2, ap-northeast-1, eu-west-1), an execution role with bedrock:InvokeModel and the inference-profile read actions, and a MicroVM image built from the repo's image/Dockerfile (there's an infra/build-image.mjs script that registers it for you). For the Codex path, also attach the AWS-managed AmazonBedrockMantleInferenceAccess policy to that role. You'll want Node 22+ locally.
The package is @theagenticguy/sandbox-lambda. It's not on npm yet, so build it from source and reference the local build, or install it straight from the Git URL (the package's prepare script builds it on install):
1
2
3
4
5
6
7
8
# Option A: build from source
git clone https://github.com/theagenticguy/lambda-microvm-sandbox.git
cd lambda-microvm-sandbox && pnpm install && pnpm build
# then reference it from your project's package.json:
# "@theagenticguy/sandbox-lambda": "file:../lambda-microvm-sandbox"

# Option B: install from the Git URL
pnpm add github:theagenticguy/lambda-microvm-sandbox
The harness peers come from npm (the harness owns their versions):
1
pnpm add @ai-sdk/harness @ai-sdk/provider-utils ws

What it is

The harness asks a sandbox provider for a session that runs commands and streams their output, reads and writes files, exposes a port for the agent's in-VM bridge, applies a network policy, and stops or resumes. Lambda MicroVMs  supplies each piece, though two of them take work. AWS documents a reference pattern for exactly this shape, sandboxing Claude Managed Agents .
Harness needsLambda MicroVMs
createSession()RunMicrovm from a prebuilt image, plus CreateMicrovmAuthToken
resumeSession({ sessionId })GetMicrovm + ResumeMicrovm, state preserved by snapshot
run / spawn with streamed stdout/stderrHTTPS to an in-VM daemon's /exec (NDJSON stream)
readFile / writeFilethe daemon's /fs/read and /fs/write
getPortUrl({ protocol: 'ws' })a localhost relay that injects MicroVM auth onto the bridge socket
stop()SuspendMicrovm, which parks the session for resume
destroy()TerminateMicrovm
setNetworkPolicy(...)ingress and egress connectors, chosen at launch
The two rows that took real work are the ones Lambda MicroVMs doesn't hand you directly, so let me walk through both.
A MicroVM gives you a transport. The package supplies the exec API. Lambda MicroVMs exposes one thing per instance: an HTTPS endpoint that forwards HTTP and WebSocket to a port inside the VM. There's no "run this command" or "read this file" API. So the package ships its own: a small daemon, image/agentd.mjs, baked into the image, with zero dependencies beyond the Node standard library. The control plane talks to the daemon over the endpoint, and the daemon runs the commands and touches the files. Keeping the wire contract in one module that both halves import means the two sides can't drift.
Bridge-backed agents need a reachable, authenticated port. Claude Code and Codex run their agent runtime inside the sandbox. Their harness adapter spawns a bridge that binds a WebSocket server on a port, then the host dials in. That's why just-bash can't run them: an in-process sandbox has no reachable port. A Lambda MicroVM does carry inbound WebSocket on its endpoint, but the endpoint requires the MicroVM JWE auth token, and the adapter opens its socket with a bare new WebSocket(url) that sets no headers. Closing that gap is where the localhost relay comes in, and it's my favorite part of the code, so it gets its own section below.
Two independent auth layers run the whole way through. The outer layer is the MicroVM JWE, minted by CreateMicrovmAuthToken, scoped to the instance and its ports, expiring in 60 minutes or less; Lambda's proxy terminates it. The inner layer is the bridge's own token, which the in-VM bridge terminates. The relay adds the outer auth and forwards the inner token untouched.

Why you need it

Reach for this when the sandbox has to sit inside your account, under your controls. A few concrete reasons.
The isolation boundary is a VM you operate. A coding agent runs arbitrary code. On a shared host or an in-process sandbox, a container escape or a dependency with a malicious build script is loose in an environment you share. Here the boundary is a Firecracker MicroVM plus its network connectors, in your account. The daemon adds defense in depth: /fs reads and writes resolve through realpath and stay confined to an allow-list of roots (the workspace, HOME, /tmp), so a leaked endpoint token can't read /etc/shadow or the run-hook payload.
Egress is a launch-time decision you make. Network connectors are fixed at RunMicrovm and immutable for the life of the instance, which is a feature for a policy control: a session's outbound reach can't drift after it starts. A custom policy attaches your VPC egress connector, and the actual host allow-list lives in that connector's security groups and AWS Network Firewall. deny-all keeps ingress for the bridge and attaches no egress, so the agent has no outbound path at all.
Bedrock credentials never leave the execution role. The image bakes the Bedrock wiring, so the agent authenticates with the MicroVM execution role's credentials through IMDS. No API key is written into the VM, and no key sits on the control plane. Grant the role bedrock:InvokeModel and the inference-profile read actions, and the turn runs. Both agents take this path: Claude Code reads CLAUDE_CODE_USE_BEDROCK and its model id from the image env, and Codex reads the baked config.toml.
The endpoint clears the limits that make API Gateway unsuitable for a long-lived agent. A coding-agent bridge holds a WebSocket open for the length of a turn, which can run for many minutes. API Gateway WebSocket caps a request at 29 seconds, a connection at 2 hours, and closes on 10 minutes idle, none of them raisable. The MicroVM endpoint is a transparent reverse proxy. It documents no per-request timeout and no idle hard-close; an idle policy suspends the VM, and the next request resumes it. A request lives as long as the app holds it, bounded by the 8-hour session cap. That absence of a hard timeout is what the long-lived bridge depends on.
Parking a session is cheap. Between turns, the harness calls stop(). That suspends the VM, preserving memory and disk, and the next turn resumes it in under a second. You're not paying to hold an idle host or rebuilding state on every turn.
Two constraints to size against before you commit: Lambda MicroVMs is ARM64 (Graviton) only, and it runs in five regions today. The 8-hour session cap is hard; for longer work, checkpoint and start a fresh session. The quotas and limits  page has the full sizing table.

Let's wire it up

Start with the provider, then pick your agent. The provider is the same for both; the agent is one create* call you swap. The factory is synchronous and does no I/O, so the MicroVM launches lazily the first time the harness calls createSession(). Here's the Claude Code wiring:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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!, // built 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: [
'bedrock-runtime.us-east-1.amazonaws.com', // the Bedrock endpoint the agent calls
'registry.npmjs.org',
'github.com',
],
},
vpcEgressConnectorArn: process.env.SANDBOX_VPC_EGRESS_CONNECTOR_ARN,
});

const agent = new HarnessAgent({
// No auth block: the image env (CLAUDE_CODE_USE_BEDROCK=1 + the model id) drives
// Bedrock, and the agent reads AWS credentials from the execution role via IMDS.
harness: createClaudeCode(),
sandbox,
});

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)
To run Codex instead, keep everything and swap the harness. Codex reads its model and provider from a config.toml baked into the image and authenticates with the execution role, so createCodex() needs no auth block either (examples/codex-on-lambda.ts):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createCodex } from '@ai-sdk/harness-codex';

// GPT-5.5 on Bedrock is us-east-2 only, so launch the MicroVM there.
const sandbox = createLambdaSandbox({
imageIdentifier: process.env.SANDBOX_IMAGE_ARN!,
region: 'us-east-2',
executionRoleArn: process.env.SANDBOX_ROLE_ARN!, // AmazonBedrockMantleInferenceAccess attached
agentToken: process.env.SANDBOX_AGENT_TOKEN!,
});

const agent = new HarnessAgent({
// Set model explicitly: the adapter defaults to gpt-5.3-codex, which Bedrock
// doesn't serve. No auth block; credentials come from the execution role.
harness: createCodex({ model: 'openai.gpt-5.5' }),
sandbox,
permissionMode: 'allow-all', // Codex has no built-in approvals
});
That's the whole integration for either agent. Everything below is what's happening underneath, if you want to know how the pieces fit.

Turning an HTTPS response body into a live process

The MicroVM gives you no exec API, so AgentClient.exec() is an async generator over the daemon's NDJSON stream. Each line is one JSON frame; stdout and stderr arrive base64-encoded, and the exit frame ends the stream. That's the whole of "run a command and stream its output," built on a plain fetch (src/agent-client.ts):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for await (const line of ndjson(res.body)) {
const frame = line as ExecFrame;
switch (frame.type) {
case 'pid':
yield { type: 'pid', pid: frame.pid };
break;
case 'stdout':
yield { type: 'stdout', bytes: fromBase64(frame.data) };
break;
case 'stderr':
yield { type: 'stderr', bytes: fromBase64(frame.data) };
break;
case 'exit':
yield { type: 'exit', exitCode: frame.exitCode };
return;
case 'error':
throw new Error(`remote exec error: ${frame.message}`);
}
}
On the VM side, the daemon is about thirty lines of Node standard library: spawn('bash', ['-lc', command]), a pid frame, base64 stdout and stderr frames, and a terminal exit frame written as newline-delimited JSON.

The bridge relay: smuggling auth through a header-less WebSocket

This is the part that makes bridge-backed agents work. The adapter (Claude Code's or Codex's) dials the bridge with a bare new WebSocket(url) and can't set headers, but the MicroVM endpoint admits a WebSocket only when the JWE and the target port ride on the connection. So getPortUrl({ protocol: 'ws' }) returns a localhost URL for a relay running in your process. The relay opens the upstream socket with the MicroVM auth carried as WebSocket subprotocols, and forwards frames in both directions. The adapter stays unmodified (src/bridge-relay.ts):
1
2
3
4
5
6
const upstreamUrl = `wss://${this.options.endpoint}${reqUrl}`;
const upstream = new WebSocket(upstreamUrl, [
MVM_BASE_SUBPROTOCOL,
`${MVM_BASE_SUBPROTOCOL}.authentication.${jweToken}`,
`${MVM_BASE_SUBPROTOCOL}.port.${this.options.bridgePort}`,
]);
The relay mints a fresh JWE per upstream connection, so a session that runs for hours always opens new sockets with a token that hasn't expired. It never inspects the bridge's own agent_bridge_token; that rides through in the query string to the in-VM bridge.

Keeping auth alive across an 8-hour session

A JWE expires in 60 minutes or less, and a session lasts up to 8 hours, so a token minted once at launch would start returning 403 at the one-hour mark. The TokenManager re-mints 5 minutes early and coalesces concurrent callers onto a single in-flight mint, so a burst of requests near the refresh boundary doesn't fire a burst of CreateMicrovmAuthToken calls (src/token-manager.ts):
1
2
3
4
5
6
7
8
9
10
async getToken(): Promise<string> {
if (this.token && Date.now() < this.expiresAt - REFRESH_SKEW_MS) {
return this.token;
}
if (this.inFlight) return this.inFlight;
this.inFlight = this.mint().finally(() => {
this.inFlight = undefined;
});
return this.inFlight;
}

How both agents reach Bedrock with no shipped key

Neither createClaudeCode() nor createCodex() gets an auth block. The image bakes each agent's Bedrock wiring, and both authenticate with the execution role over IMDS, so no key lands in the VM or on the control plane. The difference is only where each reads its model from. Claude Code reads env vars, straight from the Claude Code on Amazon Bedrock docs  (image/Dockerfile):
1
2
3
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
Codex reads a baked config.toml (image/Dockerfile):
1
2
3
4
5
6
7
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"
The Codex path has three gotchas that bit me before it worked, and they're worth stating plainly because none are obvious. GPT-5.5 on Bedrock lives in us-east-2 only, so the MicroVM has to launch there. The execution role needs the AWS-managed AmazonBedrockMantleInferenceAccess policy; a hand-rolled bedrock:CallWithBearerToken statement just returns 401 (deploy infra/roles.cfn.yaml with EnableCodexBedrock=true). And you must pass createCodex({ model: 'openai.gpt-5.5' }) explicitly: the adapter defaults to gpt-5.3-codex, which Bedrock doesn't serve, so it 404s. One more that only shows up at runtime: the daemon runs commands with HOME=/workspace, and the Codex adapter looks for its config at $HOME/.codex/config.toml, so the image bakes the config at /workspace/.codex/, where that lookup resolves. Miss that and Codex silently falls back to api.openai.com and 401s.
I've run both paths live against AWS. The test prompt tells the agent to write hello.py, run it with python3, and report its model and provider. The write and bash tool-calls in the stream prove the turn ran inside the VM: Claude Code on Opus 4.8 in us-east-1, Codex on GPT-5.5 through the Bedrock Mantle endpoint in us-east-2, each with credentials from the execution role and no provider key in the VM.

What's proven, and what to pin

Two live tests drive real agent turns against live AWS, each with a write and a bash tool-call, credentials from the execution role via IMDS, no provider key: test/live-claude-code.mjs runs Claude Code on Bedrock Opus 4.8 in us-east-1, and test/live-codex.mjs runs Codex on Bedrock GPT-5.5 in us-east-2. A control-plane test walks the full RunMicrovm → token → endpoint GET → SuspendMicrovm → ResumeMicrovm → TerminateMicrovm lifecycle. A no-AWS suite runs the real compiled provider and the real agentd.mjs behind an emulator that enforces the documented endpoint contract, then connects the published harness SandboxChannel through the relay to verify the bridge handshake and full-duplex streaming.
This is experimental software. The AI SDK harness and AWS Lambda MicroVMs are both new in 2026, so pin your versions. The code is Apache-2.0 at github.com/theagenticguy/lambda-microvm-sandbox . If you build something on it, I'd love to hear how it went.

References


Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article