-
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 all 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,97 @@ | ||
| name: Code-mode sandbox 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/**" | ||
| - "package.json" | ||
| - "pnpm-lock.yaml" | ||
| - "pnpm-workspace.yaml" | ||
| - "tsconfig.json" | ||
| - "turbo.json" | ||
| push: | ||
| branches: [main, v4-spike] | ||
| paths: | ||
| - ".github/workflows/codemode-framework-examples.yml" | ||
| - "packages/integrations/**" | ||
| - "packages/extension/**" | ||
| - "packages/protocol/**" | ||
| - "packages/sdk-ts/**" | ||
| - "package.json" | ||
| - "pnpm-lock.yaml" | ||
| - "pnpm-workspace.yaml" | ||
| - "tsconfig.json" | ||
| - "turbo.json" | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| example: | ||
| 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 Sandbox package-installed MCP | ||
| package: "@browserbasehq/stagehand-integrations-example-vercel-sandbox" | ||
| 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-codemode | ||
| - run: pnpm --filter ${{ matrix.package }} typecheck | ||
| - run: pnpm --filter ${{ matrix.package }} test:contract | ||
| - run: pnpm --filter ${{ matrix.package }} pack:artifacts | ||
| - run: pnpm --filter ${{ matrix.package }} smoke | ||
| env: | ||
| CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }} | ||
| STAGEHAND_BROWSER: local | ||
| - name: Detect live test credentials | ||
| id: live-credentials | ||
| env: | ||
| BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }} | ||
| BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }} | ||
| VERCEL_OIDC_TOKEN: ${{ secrets.VERCEL_OIDC_TOKEN }} | ||
| VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} | ||
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | ||
| VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | ||
| run: | | ||
| if [[ -n "$BROWSERBASE_API_KEY" && -n "$BROWSERBASE_PROJECT_ID" ]] && \ | ||
| [[ -n "$VERCEL_OIDC_TOKEN" || ( -n "$VERCEL_TEAM_ID" && -n "$VERCEL_PROJECT_ID" && -n "$VERCEL_TOKEN" ) ]]; then | ||
| echo "available=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "available=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Run live package-installed sandbox proof | ||
| if: steps.live-credentials.outputs.available == 'true' | ||
| run: pnpm --filter ${{ matrix.package }} e2e | ||
| env: | ||
| STAGEHAND_SANDBOX_ARTIFACTS: ${{ github.workspace }}/packages/integrations/examples/vercel-sandbox/.artifacts | ||
| BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }} | ||
| BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }} | ||
| VERCEL_OIDC_TOKEN: ${{ secrets.VERCEL_OIDC_TOKEN }} | ||
| VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} | ||
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | ||
| VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | ||
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
161 changes: 161 additions & 0 deletions
161
packages/integrations/examples/vercel-sandbox/README.md
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,161 @@ | ||
| # Run Stagehand code mode in Vercel Sandbox | ||
|
|
||
| Use this example when an agent framework runs on your host but Stagehand code mode must execute | ||
| untrusted JavaScript behind a microVM boundary. | ||
|
|
||
| ```text | ||
| Your MCP client | ||
| └─ bearer-authenticated Streamable HTTP | ||
| └─ Vercel Sandbox exposed port | ||
| └─ SHA-256 auth proxy (stagehand-proxy user) | ||
| └─ stateful HTTP-to-stdio bridge (stagehand-mcp user) | ||
| └─ Stagehand MCP over stdio | ||
| └─ generated JavaScript + Browserbase browser | ||
| ``` | ||
|
|
||
| The private workspace package exports one framework-neutral contract: | ||
|
|
||
| ```ts | ||
| type StagehandSandboxConnection = { | ||
| url: URL; | ||
| token: string; | ||
| close: () => Promise<void>; | ||
| }; | ||
| ``` | ||
|
|
||
| `createStagehandSandbox()` creates a fresh Vercel Firecracker microVM with open setup egress, uploads | ||
| the exact packed Stagehand and code-mode artifacts supplied by the trusted host, verifies their | ||
| SHA-256 digests in the guest, and installs them with the pinned HTTP-to-stdio bridge. Before the MCP | ||
| server starts, it replaces setup egress with an allowlist containing only Browserbase's API and the | ||
| regional CDP hostname discovered for the configured project. | ||
|
|
||
| ## Install and run | ||
|
|
||
| Authenticate the host for [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox), then set: | ||
|
|
||
| ```bash | ||
| pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts | ||
|
|
||
| STAGEHAND_SANDBOX_ARTIFACTS="$PWD/packages/integrations/examples/vercel-sandbox/.artifacts" | ||
| BROWSERBASE_API_KEY=<browserbase-api-key> | ||
| BROWSERBASE_PROJECT_ID=<browserbase-project-id> | ||
|
|
||
| pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox e2e | ||
| ``` | ||
|
|
||
| The artifact directory path must be absolute. The pack step builds the exact checkout under review, | ||
| produces stable local tarball names, and records an integrity-pinned npm installation lock; no | ||
| moving branch, tag, registry package version, or guest-side repository checkout participates in the | ||
| proof. Vercel's credential-brokering header transforms are currently available on Pro and Enterprise | ||
| plans. Check the | ||
| [credential-brokering announcement](https://vercel.com/changelog/safely-inject-credentials-in-http-headers-with-vercel-sandbox) | ||
| before relying on this example with another plan. | ||
|
|
||
| Vercel credentials authenticate the host to the Sandbox control plane so it can create, update, | ||
| stop, and delete the microVM. They are separate from the random application bearer returned by this | ||
| helper, which protects only the MCP port exposed by this sandbox. The SDK uses | ||
| `VERCEL_OIDC_TOKEN` when available. Outside Vercel, pass `{ teamId, projectId, token }` as | ||
| `vercelCredentials`; the E2E and lease derive it from `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, and | ||
| `VERCEL_TOKEN` when the token is present. | ||
|
|
||
| ## Connect an MCP client | ||
|
|
||
| This raw [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) | ||
| example is the adapter boundary that agent frameworks build on: | ||
|
|
||
| ```ts | ||
| import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | ||
| import { | ||
| createStagehandSandbox, | ||
| stagehandTransport, | ||
| } from "@browserbasehq/stagehand-integrations-example-vercel-sandbox"; | ||
|
|
||
| const stagehand = await createStagehandSandbox({ | ||
| packageArtifactsPath: process.env.STAGEHAND_SANDBOX_ARTIFACTS!, | ||
| browserbaseApiKey: process.env.BROWSERBASE_API_KEY!, | ||
| browserbaseProjectId: process.env.BROWSERBASE_PROJECT_ID!, | ||
| ...(process.env.VERCEL_TOKEN | ||
| ? { | ||
| vercelCredentials: { | ||
| teamId: process.env.VERCEL_TEAM_ID!, | ||
| projectId: process.env.VERCEL_PROJECT_ID!, | ||
| token: process.env.VERCEL_TOKEN, | ||
| }, | ||
| } | ||
| : {}), | ||
| }); | ||
| const client = new Client({ name: "my-agent", version: "1.0.0" }); | ||
|
|
||
| try { | ||
| await client.connect(stagehandTransport(stagehand)); | ||
| const tools = await client.listTools(); | ||
| console.log(tools); | ||
| } finally { | ||
| await client.close(); | ||
| await stagehand.close(); | ||
| } | ||
| ``` | ||
|
|
||
| Create one external MCP client and one MCP session per sandbox. The bridge holds one Stagehand stdio | ||
| process for that session, so browser pages, DOM changes, cookies, and guest files survive across tool | ||
| calls. Destroy the sandbox after the agent run; generated JavaScript can mutate its guest filesystem, | ||
| so reconnecting or reusing that VM would cross a trust boundary. | ||
|
|
||
| The authenticated `/mcp` endpoint accepts POST and DELETE. It returns `405 Method Not Allowed` for | ||
| the optional standalone GET event stream because Vercel's public edge buffers an idle SSE response | ||
| and Stagehand does not send server-initiated notifications. MCP calls still stream their responses | ||
| over POST. | ||
|
|
||
| ## Use the cross-language lease | ||
|
|
||
| Python and other non-Node adapters can launch the same provider implementation without copying its | ||
| setup or network-policy logic: | ||
|
|
||
| ```bash | ||
| pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox lease | ||
| ``` | ||
|
|
||
| The launcher writes exactly one JSON line to stdout: | ||
|
|
||
| ```json | ||
| { "url": "https://<sandbox-domain>/mcp", "token": "<bearer-token>" } | ||
| ``` | ||
|
|
||
| It then holds stdin open as the sandbox lease. Keep the process and stdin pipe alive for the entire | ||
| MCP session. Close stdin for normal cleanup; `SIGINT` and `SIGTERM` trigger bounded cleanup and retain | ||
| signal-style exit semantics. Spawn it with an explicit environment allowlist containing only the | ||
| runtime variables it needs: `PATH`, the relevant Vercel authentication variables, | ||
| `STAGEHAND_SANDBOX_ARTIFACTS`, `BROWSERBASE_API_KEY`, and `BROWSERBASE_PROJECT_ID`. The token is | ||
| emitted once over the trusted parent pipe and is never placed in command arguments or environment | ||
| variables. | ||
|
|
||
| ## Security boundary | ||
|
|
||
| [Vercel Sandbox](https://vercel.com/sandbox) supplies the microVM boundary. Process users are an | ||
| additional defense inside that VM, not a substitute for it: | ||
|
|
||
| - `stagehand-mcp` runs supergateway, the Stagehand stdio server, and generated JavaScript without | ||
| sudo. | ||
| - `stagehand-proxy` runs only the exposed-port auth proxy without sudo. Its bootstrap environment | ||
| receives the SHA-256 digest of a random 32-byte bearer, not the raw bearer. | ||
| - The host retains the raw bearer and Browserbase key. The guest MCP process receives a fixed | ||
| placeholder key. Vercel's [credential-brokering transform](https://vercel.com/changelog/safely-inject-credentials-in-http-headers-with-vercel-sandbox) | ||
| overwrites the Browserbase API header at the network boundary. | ||
| - The host discovers and validates the exact regional Browserbase CDP hostname before lockdown. The | ||
| running VM allows only that hostname and `api.browserbase.com`; all other egress is denied. | ||
| - Installed package artifacts, dependencies, and bridge code become root-owned and read-only before | ||
| untrusted code runs. | ||
| - The only published guest port is the authenticated proxy. The stateful bridge listens on guest | ||
| loopback. | ||
|
|
||
| Credential brokering prevents key disclosure, but it still grants the sandbox the Browserbase API | ||
| capabilities of that key. Use a separately scoped project/key and host-side timeouts. AI-backed | ||
| Stagehand methods require a separately scoped model credential and an exact provider-host policy; | ||
| this example intentionally does not forward outer-agent model keys or broaden egress. | ||
|
|
||
| The Vercel policy constrains network requests made by guest processes. The browser itself runs | ||
| remotely on Browserbase, so this policy does not restrict which URLs that browser can navigate to. | ||
| Apply separate browser-navigation controls when the agent must stay within an approved site set. | ||
|
|
||
| `close()` is idempotent and attempts both stop and permanent delete even when one cleanup operation | ||
| fails. The lease adds a bounded fallback. Always close the MCP client first, then the connection. |
33 changes: 33 additions & 0 deletions
33
packages/integrations/examples/vercel-sandbox/package.json
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,33 @@ | ||
| { | ||
| "name": "@browserbasehq/stagehand-integrations-example-vercel-sandbox", | ||
| "version": "4.0.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "exports": { | ||
| ".": "./src/sandbox.ts", | ||
| "./lease": "./src/lease.ts" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc --noEmit", | ||
| "e2e": "tsx src/e2e.ts", | ||
| "lease": "tsx src/lease.ts", | ||
| "pack:artifacts": "node scripts/pack-artifacts.mjs", | ||
| "smoke": "tsx src/smoke.ts", | ||
| "test:contract": "tsx --test src/*.test.ts src/*.test.mjs src/guest/*.test.mjs", | ||
| "typecheck": "tsc --noEmit" | ||
| }, | ||
| "dependencies": { | ||
| "@browserbasehq/stagehand-codemode": "workspace:*", | ||
| "@modelcontextprotocol/sdk": "catalog:", | ||
| "@vercel/sandbox": "catalog:", | ||
| "supergateway": "catalog:" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "catalog:", | ||
| "tsx": "catalog:", | ||
| "typescript": "catalog:" | ||
| }, | ||
| "engines": { | ||
| "node": ">=22.18.0" | ||
| } | ||
| } |
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.