-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: run code-mode MCP in Vercel Sandbox #2626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
shrey150
wants to merge
31
commits into
shrey/stg-2765-codemode-publish
Choose a base branch
from
shrey/stg-2765-codemode-vercel
base: shrey/stg-2765-codemode-publish
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 85e9ac3
test(integrations): smoke Vercel code mode in CI
shrey150 95902d6
Merge updated code-mode guidance parent
shrey150 eca28f1
Merge updated code-mode guidance parent
shrey150 fd2003a
Merge updated code-mode guidance parent
shrey150 175128f
Merge refreshed code-mode guidance parent
shrey150 f95a3bd
fix(vercel): use current agent instructions field
shrey150 d41e0f9
feat: add sandboxed code-mode image
shrey150 a434b4c
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-sandbox'…
shrey150 bd2c800
feat(vercel): run code mode in an E2B sandbox
shrey150 e4bbe74
fix: cover tsconfig changes in image CI
shrey150 c43ed42
Merge sandbox foundation updates
shrey150 df3959d
feat: add a source-installed E2B MCP boundary
shrey150 a804d84
fix: make E2B egress default deny
shrey150 8155c03
fix: keep MCP session setup offline
shrey150 8cf5b40
fix: poll E2B build readiness safely
shrey150 3a54a90
fix: terminate the offline setup branch
shrey150 bc92188
fix: validate the offline source mirror
shrey150 f8c6e80
docs: bound E2B custom servers to one tool call
shrey150 a6c7954
feat: run code-mode MCP in Vercel Sandbox
shrey150 ec7a7ee
fix: use the locked sandbox gateway
shrey150 e9c6682
fix(vercel): harden sandbox lifecycle boundary
shrey150 938efe1
test(vercel): make sandbox contracts deterministic
shrey150 48b87ba
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-publish'…
shrey150 4678772
refactor(vercel): install exact code-mode package artifacts
shrey150 245c2bb
test(vercel): cover invalid proxy port configuration
shrey150 75aa26c
fix(vercel): harden package-installed sandbox setup
shrey150 6a01338
fix(ci): register sandbox build task
shrey150 63ddf58
fix(vercel): close remaining setup review gaps
shrey150 35c1578
test(vercel): lock credential and pack failure branches
shrey150 e3e70f2
test(vercel): make stderr sentinel deterministic
shrey150 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.