Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 44 additions & 0 deletions .github/workflows/codemode-framework-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,47 @@ jobs:
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

eve:
name: Eve
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
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

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

- run: pnpm --filter @browserbasehq/stagehand-integrations-example-eve typecheck
- run: pnpm --filter @browserbasehq/stagehand-integrations-example-eve contract
- run: pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts
- name: Detect Eve live test credentials
id: eve-live-credentials
uses: ./.github/actions/detect-codemode-live-credentials
with:
require-openai: "true"
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 }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Run Eve live sandbox proof
if: steps.eve-live-credentials.outputs.available == 'true'
run: pnpm --filter @browserbasehq/stagehand-integrations-example-eve 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 }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
2 changes: 2 additions & 0 deletions packages/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,5 @@ implementation modules and an in-process arbitrary-code executor are not public
one crew execution.
- [LangChain Deep Agents](./examples/langchain) uses one explicit MCP session so every tool call
reaches the same browser.
- [Eve](./examples/eve) discovers the same authenticated remote MCP connection and exposes only
`code_execute` to the agent.
2 changes: 2 additions & 0 deletions packages/integrations/examples/eve/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.eve/
.output/
68 changes: 68 additions & 0 deletions packages/integrations/examples/eve/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Eve with Stagehand code mode

This example gives an Eve agent one browser tool, `stagehand__code_execute`, without running
generated JavaScript in the Eve process. Eve stays on the host while the package-installed
[Vercel Sandbox example](../vercel-sandbox) owns Stagehand, the browser session, and generated code:

```text
Eve connection_search -> authenticated Streamable HTTP -> Vercel Sandbox -> Stagehand MCP
`-> generated JavaScript
```

[`agent/connections/stagehand.ts`](./agent/connections/stagehand.ts) is the complete framework
adapter. It receives the shared sandbox `{ url, token }`, uses Eve's native
`defineMcpClientConnection`, and allows only `code_execute`. Eve qualifies the discovered tool as
`stagehand__code_execute`; the canonical Stagehand guidance stays in the MCP tool description.

## Dependency version

The example pins Eve 0.29.4, the newest release old enough for this repository's dependency-age
policy when the integration was validated. Eve is pre-1.0 and its public API is still evolving, so
upgrade this pin only with the contract and live proofs below.

## Secret-free framework contract

From the repository root:

```bash
pnpm install
pnpm --filter @browserbasehq/stagehand-integrations-example-eve typecheck
pnpm --filter @browserbasehq/stagehand-integrations-example-eve contract
```

The contract starts an authenticated MCP server built on the official MCP SDK, deliberately returns
405 for the optional GET stream, and runs the real Eve build, server, and eval runtime with a
deterministic model. The eval must:

1. use `connection_search` exactly once;
2. discover only `stagehand__code_execute`;
3. call it exactly twice through one connection;
4. receive persistent-page-shaped results and the exact expected markers; and
5. reject an unauthenticated request while tolerating authenticated GET 405.

This proves Eve's connection behavior without claiming browser or sandbox isolation.

## Live package-backed proof

Build and pack the exact Stagehand packages under review, then run the live composition:

```bash
pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts

STAGEHAND_SANDBOX_ARTIFACTS="$PWD/packages/integrations/examples/vercel-sandbox/.artifacts" \
BROWSERBASE_API_KEY=<api-key> \
BROWSERBASE_PROJECT_ID=<project-id> \
VERCEL_OIDC_TOKEN=<oidc-token> \
OPENAI_API_KEY=<openai-key> \
pnpm --filter @browserbasehq/stagehand-integrations-example-eve e2e
```

For external CI, replace `VERCEL_OIDC_TOKEN` with `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, and
`VERCEL_TOKEN`. `EVE_STAGEHAND_MODEL` selects the direct OpenAI model and defaults to
`gpt-5-mini`.

The live proof checks unauthenticated 401 and authenticated optional-GET 405, then runs both the
deterministic and real-model Eve evals against one package-installed Vercel Sandbox connection.
Each eval must discover the connection, make two `code_execute` calls, retain a DOM marker between
calls, and observe neither `OPENAI_API_KEY` nor a host-only marker inside generated code. `PASS` is
emitted only after the Eve runtimes stop and the sandbox is destroyed.
92 changes: 92 additions & 0 deletions packages/integrations/examples/eve/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { openai } from "@ai-sdk/openai";
import { defineAgent } from "eve";
import { mockModel } from "eve/evals";

const stagehandTool = "stagehand__code_execute";

export default defineAgent({
modelContextWindowTokens: 131_072,
model:
process.env.EVE_STAGEHAND_DETERMINISTIC === "1"
? mockModel(({ toolResults, tools }) => {
const stagehandResults = toolResults.filter(({ name }) => name === stagehandTool);

if (!tools.some(({ name }) => name === stagehandTool)) {
return {
toolCalls: [
{
name: "connection_search",
input: { connection: "stagehand", keywords: "execute browser JavaScript" },
},
],
};
}

if (stagehandResults.length === 0) {
return {
toolCalls: [
{
name: stagehandTool,
input: {
code: `
await page.goto("https://example.com", {
waitUntil: "domcontentloaded",
});
await page.evaluate(() => {
document.documentElement.dataset.eveStagehandDirectMarker =
"eve-direct-persistent";
});
return {
pageId: page.pageId,
title: await page.title(),
directMarker: await page.evaluate(
() => document.documentElement.dataset.eveStagehandDirectMarker,
),
modelKeyVisible: process.env.OPENAI_API_KEY ?? null,
hostMarkerVisible: process.env.EVE_HOST_ONLY_MARKER ?? null,
};
`,
},
},
],
};
}

if (stagehandResults.length === 1) {
return {
toolCalls: [
{
name: stagehandTool,
input: {
code: `
const directMarker = await page.evaluate(
() => document.documentElement.dataset.eveStagehandDirectMarker,
);
if (directMarker !== "eve-direct-persistent") {
throw new Error("Eve lost Stagehand browser state between tool calls");
}
await page.evaluate(() => {
document.documentElement.dataset.eveStagehandModelMarker =
"eve-model-persistent";
});
return {
pageId: page.pageId,
title: await page.title(),
directMarker,
modelMarker: await page.evaluate(
() => document.documentElement.dataset.eveStagehandModelMarker,
),
modelKeyVisible: process.env.OPENAI_API_KEY ?? null,
hostMarkerVisible: process.env.EVE_HOST_ONLY_MARKER ?? null,
};
`,
},
},
],
};
}

return `STAGEHAND_RESULTS ${JSON.stringify(stagehandResults.map(({ output }) => output))}`;
})
: openai(process.env.EVE_STAGEHAND_MODEL ?? "gpt-5-mini"),
});
17 changes: 17 additions & 0 deletions packages/integrations/examples/eve/agent/connections/stagehand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineMcpClientConnection } from "eve/connections";

const endpoint = process.env.STAGEHAND_MCP_URL ?? "http://127.0.0.1:3000/mcp";

export default defineMcpClientConnection({
url: endpoint,
description:
"Stagehand browser automation isolated behind an authenticated code-mode MCP gateway.",
auth: {
getToken: async () => {
const token = process.env.STAGEHAND_MCP_TOKEN;
if (!token) throw new Error("STAGEHAND_MCP_TOKEN is required");
return { token };
},
},
tools: { allow: ["code_execute"] },
});
3 changes: 3 additions & 0 deletions packages/integrations/examples/eve/agent/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You are a browser agent. Use `connection_search` to discover the Stagehand connection, then use its
`code_execute` tool for browser work. Follow the requested number and order of calls exactly. Keep
browser state in the existing tool session and return only the evidence the user requested.
3 changes: 3 additions & 0 deletions packages/integrations/examples/eve/evals/evals.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineEvalConfig } from "eve/evals";

export default defineEvalConfig({});
41 changes: 41 additions & 0 deletions packages/integrations/examples/eve/evals/stagehand.eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { defineEval } from "eve/evals";
import { includes } from "eve/evals/expect";

export default defineEval({
description: "Eve discovers and calls Stagehand through an authenticated remote MCP gateway.",
async test(t) {
await t.send(
[
"Use connection_search to discover the Stagehand connection.",
"Then call stagehand__code_execute exactly twice, in order.",
'First open https://example.com and store "eve-direct-persistent" in',
"document.documentElement.dataset.eveStagehandDirectMarker.",
"Return the pageId, title, marker, process.env.OPENAI_API_KEY, and",
"process.env.EVE_HOST_ONLY_MARKER from that first call.",
"In the second call, read and verify that same direct marker without navigating.",
'Then store "eve-model-persistent" in',
"document.documentElement.dataset.eveStagehandModelMarker and return both markers,",
"the current pageId and title, and the same two environment lookups.",
"Report the title and both exact markers after the two calls.",
].join(" "),
);

t.succeeded();
t.calledTool("connection_search", { count: 1 });
t.calledTool("stagehand__code_execute", {
count: 2,
output: (value) => {
const output = JSON.stringify(value);
return (
output.includes("Example Domain") &&
output.includes("eve-direct-persistent") &&
output.includes('"modelKeyVisible":null') &&
output.includes('"hostMarkerVisible":null')
);
},
});
t.check(t.reply, includes("Example Domain"));
t.check(t.reply, includes("eve-direct-persistent"));
t.check(t.reply, includes("eve-model-persistent"));
},
});
28 changes: 28 additions & 0 deletions packages/integrations/examples/eve/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@browserbasehq/stagehand-integrations-example-eve",
"version": "4.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "eve build",
"contract": "tsx src/contract.ts",
"dev": "eve dev",
"e2e": "tsx src/e2e.ts",
"typecheck": "eve build && tsc --noEmit"
},
"dependencies": {
"@ai-sdk/openai": "catalog:",
"@browserbasehq/stagehand-integrations-example-vercel-sandbox": "workspace:*",
"@modelcontextprotocol/sdk": "catalog:",
"eve": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:"
},
"engines": {
"node": ">=24"
}
}
Loading
Loading