Skip to content
Open
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
66 changes: 64 additions & 2 deletions packages/integrations/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,67 @@
# Stagehand integrations

Private workspace package for Stagehand integration adapters.
This package contains shared integration surfaces for Stagehand V4. It is private while the public API and packaging contract are validated.

The code-mode stdio entrypoint currently provides the MCP host and process lifecycle used by later code-mode capabilities. It intentionally advertises no tools yet.
## Code mode

The `./codemode` export gives an agent one `code_execute` tool backed by a persistent Stagehand browser. Frameworks can either launch the thin local MCP server or wrap `StagehandCodeExecutor` as a native tool.

```ts
import {
StagehandCodeExecutor,
stagehandCodeConfigFromEnv,
} from "@browserbasehq/stagehand-integrations/codemode";

const executor = new StagehandCodeExecutor(stagehandCodeConfigFromEnv());

try {
const result = await executor.execute({
code: `
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
return { title: await page.title(), url: await page.url() };
`,
});
console.log(result);
} finally {
await executor.close();
}
```

The executor initializes the browser on the first valid call, serializes calls, and preserves pages, cookies, and navigation state until its owner closes it.

### Low-level eval integration

Eval harnesses that already own Stagehand and browser initialization should call `executeStagehandSnippet` directly. This reuses the exact generated-code semantics without replacing the eval harness's startup, cleanup, task bindings, or metrics collection.

### Local MCP integration

The `./codemode/stdio-server` export is an internal process entrypoint. It is not a command-line interface and accepts no arguments. The owning framework launches one process per agent run and selects local or Browserbase startup through its environment:

```text
STAGEHAND_BROWSER=local
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.

### Configuration

`stagehandCodeConfigFromEnv()` recognizes:

| Variable | Purpose |
| ------------------------------------------------------------------ | ---------------------------------------------------------------- |
| `STAGEHAND_BROWSER` | Optional `local` or `browserbase` override |
| `BROWSERBASE_API_KEY` | Selects and authenticates Browserbase when present |
| `BROWSERBASE_PROJECT_ID` | Optional Browserbase project forwarded when creating a session |
| `STAGEHAND_MODEL_NAME` | Optional Stagehand model name |
| `STAGEHAND_MODEL_API_KEY` | Optional explicit model-provider key |
| Provider API keys | Supplies the key for a matching explicit model provider |
| `GEMINI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_API_KEY` | Selects `google/gemini-2.5-flash-lite` when no model is explicit |

Without a browser override, the helper selects Browserbase when `BROWSERBASE_API_KEY` exists and a headless local browser otherwise.

Native callers run generated JavaScript in their own process. An `AbortSignal` can cancel queued work before the snippet begins, but it cannot safely preempt arbitrary JavaScript already running in the same process. Native integrations that require hard time limits should put the executor behind a child-process boundary, as the stdio MCP integration does.

### 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.
8 changes: 7 additions & 1 deletion packages/integrations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
],
"type": "module",
"exports": {
"./codemode": {
"types": "./dist/codemode/index.d.mts",
"import": "./dist/codemode/index.mjs"
},
"./codemode/stdio-server": {
"import": "./dist/codemode/stdio-server.mjs"
}
Expand All @@ -19,7 +23,9 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@modelcontextprotocol/sdk": "catalog:"
"@browserbasehq/stagehand": "workspace:*",
"@modelcontextprotocol/sdk": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:",
Expand Down
109 changes: 109 additions & 0 deletions packages/integrations/src/codemode/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { StagehandClientCreateConfigSchema } from "@browserbasehq/stagehand";
import type { StagehandCodeConfig } from "./types.js";

const ANTHROPIC_DIRECT_BROWSER_ACCESS_HEADER = "anthropic-dangerous-direct-browser-access";

class StagehandCodeConfigError extends Error {
override readonly name = "StagehandCodeConfigError";
}

export function stagehandCodeConfigFromEnv(
env: NodeJS.ProcessEnv = process.env,
): StagehandCodeConfig {
const requestedBrowser = nonEmpty(env.STAGEHAND_BROWSER)?.toLowerCase();
if (
requestedBrowser !== undefined &&
requestedBrowser !== "local" &&
requestedBrowser !== "browserbase"
) {
throw new StagehandCodeConfigError(
'STAGEHAND_BROWSER must be either "local" or "browserbase".',
);
}

const browserbaseApiKey = nonEmpty(env.BROWSERBASE_API_KEY);
const browserbaseProjectId = nonEmpty(env.BROWSERBASE_PROJECT_ID);
const browserType = requestedBrowser ?? (browserbaseApiKey ? "browserbase" : "local");
if (browserType === "browserbase" && !browserbaseApiKey) {
throw new StagehandCodeConfigError(
'BROWSERBASE_API_KEY is required when STAGEHAND_BROWSER="browserbase".',
);
}

const explicitModelName = nonEmpty(env.STAGEHAND_MODEL_NAME);
const explicitModelApiKey = nonEmpty(env.STAGEHAND_MODEL_API_KEY);
if (!explicitModelName && explicitModelApiKey) {
throw new StagehandCodeConfigError(
"STAGEHAND_MODEL_NAME is required when STAGEHAND_MODEL_API_KEY is set.",
);
}

const inferredGoogleKey = providerApiKey("google", env);
const modelName =
explicitModelName ?? (inferredGoogleKey ? "google/gemini-2.5-flash-lite" : undefined);
const modelProvider = modelName ? providerName(modelName) : undefined;
const modelApiKey = explicitModelApiKey ?? providerApiKey(modelProvider, env);

const stagehand = StagehandClientCreateConfigSchema.parse({
logging: { level: "off" },
...(modelName
? {
model: {
modelName,
...(modelApiKey ? { apiKey: modelApiKey } : {}),
...(modelProvider === "anthropic"
? { headers: { [ANTHROPIC_DIRECT_BROWSER_ACCESS_HEADER]: "true" } }
: {}),
},
}
: {}),
});

return {
browser:
browserType === "browserbase"
? {
type: "browserbase",
launchOptions: {
apiKey: browserbaseApiKey,
...(browserbaseProjectId ? { projectId: browserbaseProjectId } : {}),
},
}
: {
type: "local",
launchOptions: { headless: true },
},
stagehand,
};
}

function providerName(modelName: string): string | undefined {
const separator = modelName.indexOf("/");
return separator === -1 ? undefined : modelName.slice(0, separator).toLowerCase();
}

function providerApiKey(provider: string | undefined, env: NodeJS.ProcessEnv): string | undefined {
switch (provider) {
case "openai":
return nonEmpty(env.OPENAI_API_KEY);
case "anthropic":
return nonEmpty(env.ANTHROPIC_API_KEY);
case "google":
return (
nonEmpty(env.GOOGLE_GENERATIVE_AI_API_KEY) ??
nonEmpty(env.GEMINI_API_KEY) ??
nonEmpty(env.GOOGLE_API_KEY)
);
case "groq":
return nonEmpty(env.GROQ_API_KEY);
case "cerebras":
return nonEmpty(env.CEREBRAS_API_KEY);
default:
return undefined;
}
}

function nonEmpty(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
Loading
Loading