Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5b207ec
feat(vercel): add Stagehand code-mode MCP example
shrey150 Aug 6, 2026
85e9ac3
test(integrations): smoke Vercel code mode in CI
shrey150 Aug 6, 2026
95902d6
Merge updated code-mode guidance parent
shrey150 Aug 6, 2026
eca28f1
Merge updated code-mode guidance parent
shrey150 Aug 7, 2026
fd2003a
Merge updated code-mode guidance parent
shrey150 Aug 7, 2026
175128f
Merge refreshed code-mode guidance parent
shrey150 Aug 7, 2026
f95a3bd
fix(vercel): use current agent instructions field
shrey150 Aug 7, 2026
d41e0f9
feat: add sandboxed code-mode image
shrey150 Aug 8, 2026
a434b4c
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-sandbox'…
shrey150 Aug 8, 2026
bd2c800
feat(vercel): run code mode in an E2B sandbox
shrey150 Aug 8, 2026
e4bbe74
fix: cover tsconfig changes in image CI
shrey150 Aug 8, 2026
c43ed42
Merge sandbox foundation updates
shrey150 Aug 8, 2026
df3959d
feat: add a source-installed E2B MCP boundary
shrey150 Aug 8, 2026
a804d84
fix: make E2B egress default deny
shrey150 Aug 8, 2026
8155c03
fix: keep MCP session setup offline
shrey150 Aug 8, 2026
8cf5b40
fix: poll E2B build readiness safely
shrey150 Aug 8, 2026
3a54a90
fix: terminate the offline setup branch
shrey150 Aug 8, 2026
bc92188
fix: validate the offline source mirror
shrey150 Aug 8, 2026
f8c6e80
docs: bound E2B custom servers to one tool call
shrey150 Aug 8, 2026
a6c7954
feat: run code-mode MCP in Vercel Sandbox
shrey150 Aug 8, 2026
ec7a7ee
fix: use the locked sandbox gateway
shrey150 Aug 8, 2026
e9c6682
fix(vercel): harden sandbox lifecycle boundary
shrey150 Aug 8, 2026
938efe1
test(vercel): make sandbox contracts deterministic
shrey150 Aug 8, 2026
48b87ba
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-publish'…
shrey150 Aug 8, 2026
4678772
refactor(vercel): install exact code-mode package artifacts
shrey150 Aug 8, 2026
245c2bb
test(vercel): cover invalid proxy port configuration
shrey150 Aug 8, 2026
75aa26c
fix(vercel): harden package-installed sandbox setup
shrey150 Aug 8, 2026
6a01338
fix(ci): register sandbox build task
shrey150 Aug 8, 2026
63ddf58
fix(vercel): close remaining setup review gaps
shrey150 Aug 8, 2026
35c1578
test(vercel): lock credential and pack failure branches
shrey150 Aug 8, 2026
e3e70f2
test(vercel): make stderr sentinel deterministic
shrey150 Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
*
!package.json
!pnpm-lock.yaml
!pnpm-workspace.yaml
!tsconfig.json
!turbo.json
!packages
!packages/extension
!packages/extension/**
!packages/integrations
!packages/integrations/**
!packages/protocol
!packages/protocol/**
!packages/sdk-ts
!packages/sdk-ts/**

**/.turbo
**/dist
**/node_modules
**/tests
**/*.test.ts
64 changes: 64 additions & 0 deletions .github/workflows/codemode-framework-examples.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Code-mode framework examples

on:
pull_request:
types: [opened, synchronize, reopened, labeled]
paths:
- ".github/workflows/codemode-framework-examples.yml"
- "packages/integrations/**"
- "packages/extension/**"
- "packages/protocol/**"
- "packages/sdk-ts/**"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
push:
branches: [main, v4-spike]
paths:
- ".github/workflows/codemode-framework-examples.yml"
- "packages/integrations/**"
- "packages/extension/**"
- "packages/protocol/**"
- "packages/sdk-ts/**"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
framework:
name: ${{ matrix.name }}
if: >-
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository ||
contains(github.event.pull_request.labels.*.name, 'safe-to-test')
runs-on: ubuntu-latest
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- name: Vercel AI SDK
package: "@browserbasehq/stagehand-integrations-example-vercel"
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- uses: ./.github/actions/setup-node-pnpm
with:
use-prebuilt-artifacts: "false"

- uses: ./.github/actions/setup-chrome-verified
id: setup-chrome

- run: pnpm exec turbo run build --filter @browserbasehq/stagehand-integrations
- run: pnpm --filter ${{ matrix.package }} typecheck
- run: pnpm --filter ${{ matrix.package }} smoke
env:
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
STAGEHAND_BROWSER: local
120 changes: 120 additions & 0 deletions .github/workflows/codemode-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Code-mode MCP image

on:
pull_request:
types: [opened, synchronize, reopened, labeled]
paths:
- ".dockerignore"
- "Dockerfile.codemode"
- ".github/workflows/codemode-image.yml"
- "packages/integrations/**"
- "packages/extension/**"
- "packages/protocol/**"
- "packages/sdk-ts/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
push:
tags:
- "stagehand-codemode-v*.*.*-*"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
IMAGE_NAME: ghcr.io/browserbase/stagehand-codemode

jobs:
build:
name: Build unprivileged image
if: >-
github.event_name == 'pull_request' &&
(github.event.pull_request.head.repo.full_name == github.repository ||
contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Build image without publishing
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: Dockerfile.codemode
load: true
platforms: linux/amd64
push: false
tags: stagehand-codemode:ci

- name: Discover MCP tools without network access
run: |
request='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"container-smoke","version":"1.0.0"}}}'
initialized='{"jsonrpc":"2.0","method":"notifications/initialized"}'
list_tools='{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
output="$(printf '%s\n%s\n%s\n' "$request" "$initialized" "$list_tools" | docker run --rm -i --network none stagehand-codemode:ci)"
grep -qF '"name":"code_execute"' <<<"$output"
printf 'code_execute discovery PASS\n'

publish:
name: Publish immutable image
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Generate immutable tags and OCI labels
id: metadata
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.IMAGE_NAME }}
flavor: latest=false
tags: |
type=sha,format=long,prefix=sha-
type=match,pattern=stagehand-codemode-v(.*),group=1
labels: |
org.opencontainers.image.source=https://github.com/browserbase/stagehand
org.opencontainers.image.description=Stagehand code-mode MCP stdio server
org.opencontainers.image.licenses=MIT

- name: Build and publish image
id: publish
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: Dockerfile.codemode
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

- name: Attest image provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.publish.outputs.digest }}
push-to-registry: true
41 changes: 41 additions & 0 deletions Dockerfile.codemode
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# syntax=docker/dockerfile:1.7

FROM node:24.19.0-bookworm-slim@sha256:3638d9a6fe4030bd716be989438248074489337ba3275657f93595428be4fc03 AS build

ENV PNPM_HOME=/pnpm
ENV PATH=$PNPM_HOME:$PATH
ENV TURBO_TELEMETRY_DISABLED=1

RUN npm install --global pnpm@11.10.0

WORKDIR /workspace

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json turbo.json ./
COPY packages/protocol ./packages/protocol
COPY packages/extension ./packages/extension
COPY packages/sdk-ts ./packages/sdk-ts
COPY packages/integrations ./packages/integrations

RUN pnpm install --frozen-lockfile
RUN pnpm exec turbo run build --filter @browserbasehq/stagehand-integrations...
RUN pnpm --filter @browserbasehq/stagehand-integrations deploy \
--prod \
--legacy \
/opt/stagehand-codemode

FROM node:24.19.0-bookworm-slim@sha256:3638d9a6fe4030bd716be989438248074489337ba3275657f93595428be4fc03 AS runtime

LABEL org.opencontainers.image.source="https://github.com/browserbase/stagehand" \
org.opencontainers.image.description="Stagehand code-mode MCP stdio server" \
org.opencontainers.image.licenses="MIT"

ENV NODE_ENV=production
ENV NODE_OPTIONS=--enable-source-maps

WORKDIR /opt/stagehand-codemode

COPY --from=build --chown=node:node /opt/stagehand-codemode ./

USER node

CMD ["node", "dist/codemode/stdio-server.mjs"]
8 changes: 8 additions & 0 deletions packages/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ STAGEHAND_BROWSER=browserbase

The process stays alive across calls and closes when its input stream ends. `SIGINT` and `SIGTERM` perform bounded graceful cleanup and preserve signal-style exit codes. If generated JavaScript blocks the JavaScript event loop, the server cannot run its cleanup handlers. The owner must terminate the entire process tree, escalate to `SIGKILL` after its own deadline, and start a new process before accepting more work. Killing only the Node process can leave its local browser child alive.

### Framework examples

- [Vercel AI SDK](./examples/vercel) launches the stdio server through the AI SDK MCP client and keeps one process alive for the complete agent run.

### Configuration

`stagehandCodeConfigFromEnv()` recognizes:
Expand All @@ -69,3 +73,7 @@ Native callers run generated JavaScript in their own process. An `AbortSignal` c
### Security boundary

The code-mode executor does not provide a sandbox. Generated JavaScript runs in the host process and inherits that process's filesystem, network, and environment access. A framework may place the tool inside its own sandbox, container, or other isolation boundary.

For untrusted generated code, use the OCI image and microVM architecture in
[`codemode/SANDBOX.md`](./codemode/SANDBOX.md). The image packages the stdio server; the sandbox
provider supplies the security boundary.
115 changes: 115 additions & 0 deletions packages/integrations/codemode/SANDBOX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Run Stagehand code mode inside a sandbox

Stagehand code mode evaluates model-generated JavaScript. Run the MCP server inside an ephemeral
microVM or equivalent sandbox when that JavaScript is not fully trusted.

The `ghcr.io/browserbase/stagehand-codemode` image is a reproducible package for the stdio server.
It is **not** the security boundary. A container shares its host kernel; the sandbox provider must
isolate the container or process from the agent application's filesystem, processes, credentials,
and network.

## Architecture

```text
Agent application
└─ authenticated Streamable HTTP MCP client
└─ sandbox provider gateway
└─ Firecracker microVM (security boundary)
└─ Stagehand code-mode MCP (stdio)
└─ generated JavaScript + Stagehand browser
```

Keep stdio inside the sandbox. Expose only the provider's authenticated MCP endpoint to a hosted
agent framework. Give the sandbox only the browser credentials it needs, restrict network egress
where the provider supports it, and destroy the complete sandbox when the agent run finishes or
times out.

## Pull an immutable image

The image is built from this repository on Node.js 24 and runs as the non-root `node` user. It starts
`dist/codemode/stdio-server.mjs` by default.

```bash
docker pull ghcr.io/browserbase/stagehand-codemode@sha256:<digest>
```

GHCR publishes a `sha-<40-character-git-commit>` tag for every permitted publish event. A digest is
the strongest production pin. The workflow never publishes `latest`.

## E2B template

[E2B custom images](https://e2b.dev/docs/template/base-image) currently require a Debian derivative,
which this image uses. Consume the final published image instead of passing `Dockerfile.codemode` to
`fromDockerfile()` because E2B's Dockerfile parser does not support multi-stage Dockerfiles.

```ts
import { Template, defaultBuildLogger, waitForTimeout } from "e2b";

const image = "ghcr.io/browserbase/stagehand-codemode@sha256:<digest>";

const template = Template()
.fromImage(image)
// Override the image entrypoint while E2B snapshots the template. Start the
// stdio server per agent run so it receives that run's short-lived secrets.
.setStartCmd("sleep infinity", waitForTimeout(1_000));

await Template.build(template, "stagehand-codemode", {
cpuCount: 2,
memoryMB: 2_048,
onBuildLogs: defaultBuildLogger(),
});
```

For hosted frameworks, use an MCP gateway inside the same E2B microVM. The current
[custom-server gateway](https://e2b.dev/docs/mcp/custom-servers) launches a GitHub checkout over
stdio, then gives the outside client an authenticated Streamable HTTP URL. Until that gateway can
pre-pull arbitrary GHCR servers, use its source-checkout configuration for the bridge and use this
image for providers that accept an OCI root image directly.

## Other sandbox providers

- [Modal `Image.from_registry()`](https://modal.com/docs/reference/modal.Image#from_registry) can
consume the GHCR image. Publish and select `linux/amd64` because Modal requires that architecture.
- [Vercel Sandbox custom images](https://vercel.com/docs/sandbox) boot in a Firecracker microVM, but
currently pull custom root images from Vercel Container Registry. Mirror the pinned GHCR digest to
VCR, or run this image with Docker inside the microVM; do not run generated code in the agent host.

## Codex and Claude Code devboxes

Codex and Claude Code commonly run inside the devbox. In that layout the CLI agent and Stagehand
stdio server are sibling processes inside one sandbox, so no HTTP bridge is necessary:

```text
Firecracker microVM / devbox (security boundary)
├─ Codex or Claude Code
└─ Stagehand code-mode MCP (stdio child process)
```

After installing the CLI in the sandbox image or an E2B template layer, register the extracted
entrypoint from inside the sandbox:

```bash
codex mcp add stagehand -- \
node /opt/stagehand-codemode/dist/codemode/stdio-server.mjs

claude mcp add --transport stdio stagehand -- \
node /opt/stagehand-codemode/dist/codemode/stdio-server.mjs
```

See the official [Codex MCP configuration](https://developers.openai.com/codex/mcp/) and
[Claude Code MCP configuration](https://code.claude.com/docs/en/mcp) references.
Inject only the required browser credentials into the short-lived devbox environment; never bake
them into the image or a checked-in MCP configuration. When the CLI exits it closes the child's
stdin, which triggers graceful Stagehand cleanup. The sandbox owner must still enforce a deadline,
kill the whole process tree if cleanup stalls, and destroy the microVM.

## Security checklist

- Pin the image by digest and verify its provenance attestation.
- Put the stdio process and generated JavaScript inside the sandbox boundary.
- Pass only `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, and an explicit Stagehand model key when
required; do not forward the agent host's complete environment.
- Authenticate the external MCP endpoint and pin the MCP protocol version required by the gateway.
- Apply provider network policy. The sandbox boundary protects the host but does not prevent a
malicious snippet from reading secrets inside the sandbox or using allowed network egress.
- Close the MCP client, terminate the server process tree, and destroy the sandbox in cleanup paths.
Loading
Loading