Skip to content
Merged
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
3 changes: 3 additions & 0 deletions packages/evals/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
.browserbase/
.trajectories/
.rubric-cache/
packages/
13 changes: 1 addition & 12 deletions packages/evals/framework/codexCodeBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
import http from "node:http";
import type { AddressInfo } from "node:net";
import { sanitizeErrorMessage } from "@browserbasehq/stagehand-integrations/harness";
import type { AgentMount } from "../core/contracts/tool.js";
import type { ExternalHarnessTaskPlan } from "./externalHarnessPlan.js";
import type { EvalLogger } from "../logger.js";
Expand Down Expand Up @@ -55,18 +56,6 @@ function stringifyResult(value: unknown): string {
}
}

/**
* Snippet errors can embed connection URLs whose query strings carry
* credentials (Browserbase connect URLs include signing keys). Redact
* credential-bearing fragments before the message crosses the HTTP boundary
* or reaches the logs; the rest stays intact so the agent can self-correct.
*/
export function sanitizeErrorMessage(message: string): string {
return message
.replace(/([?&](?:signingKey|apiKey|api_key|token|key)=)[^&\s"']+/gi, "$1[redacted]")
.replace(/\b(sk-[A-Za-z0-9_-]{6})[A-Za-z0-9_-]+/g, "$1[redacted]");
}

export interface CodeBridge {
port: number;
close: () => Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions packages/evals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@braintrust/otel": "^0.2.1",
"@browserbasehq/sdk": "catalog:",
"@browserbasehq/stagehand": "workspace:*",
"@browserbasehq/stagehand-integrations": "workspace:*",
"@openai/codex-sdk": "catalog:",
"@opentelemetry/api": "catalog:",
"@opentelemetry/exporter-trace-otlp-proto": "^0.220.0",
Expand Down
3 changes: 1 addition & 2 deletions packages/integrations/claude-code/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import {
FACADE_AGENT_INSTRUCTIONS,
FACADE_TOOLS,
} from "@browserbasehq/stagehand-integrations/facade";
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";
import { fileURLToPath } from "node:url";

import { buildAllowlistedEnv } from "./env.ts";

export const STAGEHAND_TOOL_NAMES = FACADE_TOOLS.map((tool) => `mcp__stagehand__${tool.name}`);

async function main(): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion packages/integrations/claude-code/tests/agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { FACADE_TOOLS } from "@browserbasehq/stagehand-integrations/facade";
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";
import { describe, expect, it } from "vitest";

import { STAGEHAND_TOOL_NAMES } from "../src/agent.ts";
import { buildAllowlistedEnv } from "../src/env.ts";

describe("claude-code stagehand example", () => {
it("allows exactly the namespaced facade tools", () => {
Expand Down
3 changes: 1 addition & 2 deletions packages/integrations/codex/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Codex, type CodexOptions } from "@openai/codex-sdk";
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";
import { fileURLToPath } from "node:url";

import { buildAllowlistedEnv } from "./env.ts";

const serverPath = fileURLToPath(
import.meta.resolve("@browserbasehq/stagehand-integrations/facade/stdio-server"),
);
Expand Down
10 changes: 0 additions & 10 deletions packages/integrations/codex/src/env.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/integrations/codex/tests/agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";
import { describe, expect, it } from "vitest";

import { buildCodexConfig } from "../src/agent.ts";
import { buildAllowlistedEnv } from "../src/env.ts";

describe("codex stagehand example", () => {
it("mounts the facade server in the config override", () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/integrations/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
},
"./facade/stdio-server": {
"import": "./dist/facade/stdio-server.mjs"
},
"./harness": {
"types": "./dist/harness/index.d.mts",
"import": "./dist/harness/index.mjs"
}
},
"scripts": {
Expand Down
10 changes: 1 addition & 9 deletions packages/integrations/core/src/facade/stdio-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { closeCodeModeStdio } from "../codemode/stdio-lifecycle.js";
import { sanitizeErrorMessage } from "../harness/redact.js";
import { stagehandFacadeConfigFromEnv } from "./config.js";
import {
CodeModeRunInputSchema,
Expand Down Expand Up @@ -149,15 +150,6 @@ function stringifyResult(value: unknown): string {
}
}

export function sanitizeErrorMessage(message: string): string {
return message
.replace(/([?&](?:signingKey|apiKey|api_key|token|key)=)[^&\s"']+/gi, "$1[redacted]")
.replace(/\b(sk-[A-Za-z0-9_-]{6})[A-Za-z0-9_-]+/g, "$1[redacted]")
.replace(/\b(bb_(?:live|test)_[A-Za-z0-9]{4})[A-Za-z0-9_-]+/g, "$1[redacted]")
.replace(/\bAIza[0-9A-Za-z_-]{30,}/g, "AIza[redacted]")
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, "$1[redacted]");
}

async function shutdown(code: number): Promise<void> {
if (closing) return;
closing = true;
Expand Down
29 changes: 29 additions & 0 deletions packages/integrations/core/src/harness/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
type LogLine = {
id?: string;
category?: string;
message: string;
level?: 0 | 1 | 2;
timestamp?: string;
auxiliary?: Record<
string,
{
value: string;
type: "object" | "string" | "html" | "integer" | "float" | "boolean";
Comment thread
miguelg719 marked this conversation as resolved.
}
>;
};

/** Logger surface required by agent harness adapters. */
export type HarnessLogger = {
log(line: LogLine): void;
warn(line: LogLine): void;
error(line: LogLine): void;
};

/** Error raised when a harness adapter cannot complete its work. */
export class HarnessAdapterError extends Error {
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
this.name = "HarnessAdapterError";
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** Only STAGEHAND_* and BROWSERBASE_* host env vars cross into the server. */
/** Only STAGEHAND_* and BROWSERBASE_* host env vars cross into harness processes. */
export function buildAllowlistedEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
Expand Down
3 changes: 3 additions & 0 deletions packages/integrations/core/src/harness/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./contract.js";
export * from "./env.js";
export * from "./redact.js";
Comment thread
miguelg719 marked this conversation as resolved.
19 changes: 19 additions & 0 deletions packages/integrations/core/src/harness/redact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function sanitizeErrorMessage(message: string): string {
let sanitized = message;
// stdio-server and codexCodeBridge: credential-bearing URL query parameters.
sanitized = sanitized.replace(
/([?&](?:signingKey|apiKey|api_key|token|key)=)[^&\s"']+/gi,
"$1[redacted]",
);
// stdio-server and codexCodeBridge: OpenAI-style secret keys.
sanitized = sanitized.replace(/\b(sk-[A-Za-z0-9_-]{6})[A-Za-z0-9_-]+/g, "$1[redacted]");
// stdio-server: Browserbase live and test keys.
sanitized = sanitized.replace(
/\b(bb_(?:live|test)_[A-Za-z0-9]{4})[A-Za-z0-9_-]+/g,
"$1[redacted]",
);
// stdio-server: Google API keys.
sanitized = sanitized.replace(/\bAIza[0-9A-Za-z_-]{30,}/g, "AIza[redacted]");
// stdio-server: bearer authorization values.
return sanitized.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, "$1[redacted]");
}
46 changes: 46 additions & 0 deletions packages/integrations/core/tests/harness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildAllowlistedEnv, sanitizeErrorMessage } from "../src/harness/index.js";
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

describe("harness contract", () => {
afterEach(() => {
vi.unstubAllEnvs();
});

it("redacts credential-bearing URL query parameters", () => {
expect(sanitizeErrorMessage("wss://example.test?signingKey=top-secret&foo=bar")).toBe(
"wss://example.test?signingKey=[redacted]&foo=bar",
);
});

it("redacts OpenAI-style secret keys", () => {
expect(sanitizeErrorMessage("key sk-abcdef1234567890")).toBe("key sk-abcdef[redacted]");
});

it("redacts Browserbase keys", () => {
expect(sanitizeErrorMessage("key bb_live_abcd1234567890")).toBe("key bb_live_abcd[redacted]");
});

it("redacts Google API keys", () => {
expect(sanitizeErrorMessage(`key AIza${"a".repeat(32)}`)).toBe("key AIza[redacted]");
});

it("redacts bearer authorization values", () => {
expect(sanitizeErrorMessage("Authorization: Bearer abcdefgh123456")).toBe(
"Authorization: Bearer [redacted]",
);
});

it("allows Stagehand and Browserbase env vars while excluding other and empty values", () => {
vi.stubEnv("STAGEHAND_MODEL", "model");
vi.stubEnv("BROWSERBASE_API_KEY", "browserbase-key");
vi.stubEnv("STAGEHAND_EMPTY", "");
vi.stubEnv("NOT_ALLOWLISTED_SECRET", "secret");

const env = buildAllowlistedEnv();

expect(env.STAGEHAND_MODEL).toBe("model");
expect(env.BROWSERBASE_API_KEY).toBe("browserbase-key");
expect(env).not.toHaveProperty("STAGEHAND_EMPTY");
expect(env).not.toHaveProperty("NOT_ALLOWLISTED_SECRET");
});
});
1 change: 1 addition & 0 deletions packages/integrations/core/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default defineConfig({
"codemode/stdio-server": "src/codemode/stdio-server.ts",
"facade/index": "src/facade/index.ts",
"facade/stdio-server": "src/facade/stdio-server.ts",
"harness/index": "src/harness/index.ts",
},
format: ["esm"],
platform: "node",
Expand Down
12 changes: 1 addition & 11 deletions packages/integrations/mastra/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MCPClient } from "@mastra/mcp";
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";
import { fileURLToPath } from "node:url";

export type FacadeMCPClientOptions = {
Expand All @@ -9,17 +10,6 @@ export type FacadeMCPClientOptions = {
* Starts the facade MCP server and returns its Mastra client.
* Call `client.disconnect()` when finished to stop the child process and browser.
*/
/** Only STAGEHAND_* and BROWSERBASE_* host env vars cross into the server. */
export function buildAllowlistedEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (/^(STAGEHAND_|BROWSERBASE_)/.test(key) && value) {
env[key] = value;
}
}
return env;
}

export function createFacadeMCPClient(options: FacadeMCPClientOptions = {}) {
// Build @browserbasehq/stagehand-integrations first so this dist entrypoint exists.
const serverPath = fileURLToPath(
Expand Down
3 changes: 2 additions & 1 deletion packages/integrations/mastra/tests/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FACADE_TOOLS } from "@browserbasehq/stagehand-integrations/facade";
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";
import { afterEach, describe, expect, it } from "vitest";

import { buildAllowlistedEnv, createFacadeMCPClient } from "../src/client.js";
import { createFacadeMCPClient } from "../src/client.js";

type FacadeClient = ReturnType<typeof createFacadeMCPClient>;

Expand Down Expand Up @@ -31,7 +32,7 @@
expect(tools).toBeDefined();
if (!tools) throw new Error("stagehand toolset is missing");

const expectedNames = [...FACADE_TOOLS.map((tool) => tool.name)].sort();

Check warning on line 35 in packages/integrations/mastra/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Check

unicorn(no-useless-spread)

Using a spread operator here creates a new array unnecessarily.
expect(Object.keys(tools).sort()).toEqual(expectedNames);
for (const tool of FACADE_TOOLS) {
expect(tools[tool.name]?.description).toBe(tool.description);
Expand Down
12 changes: 1 addition & 11 deletions packages/integrations/vercel-ai/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import { createMCPClient } from "@ai-sdk/mcp";
import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";
import { fileURLToPath } from "node:url";

export type FacadeMCPClientOptions = {
env?: Record<string, string>;
};

/** Only STAGEHAND_* and BROWSERBASE_* host env vars cross into the server. */
export function buildAllowlistedEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (/^(STAGEHAND_|BROWSERBASE_)/.test(key) && value) {
env[key] = value;
}
}
return env;
}

/**
* Starts the stdio facade server as a child process and returns the AI SDK
* MCP client connected to it. Call `client.close()` when finished to stop the
Expand Down
2 changes: 1 addition & 1 deletion packages/integrations/vercel-ai/tests/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { buildAllowlistedEnv } from "@browserbasehq/stagehand-integrations/harness";

import { createFacadeMCPClient } from "../src/client.js";

Expand Down Expand Up @@ -53,7 +54,6 @@ describe("Stagehand facade MCP client", () => {
it("allowlist excludes non-STAGEHAND/BROWSERBASE host vars", async () => {
process.env.STAGEHAND_BROWSER = "local";
process.env.NOT_ALLOWLISTED_SECRET = "must-not-cross";
const { buildAllowlistedEnv } = await import("../src/client.js");
const env = buildAllowlistedEnv();
expect(env.STAGEHAND_BROWSER).toBe("local");
expect(env).not.toHaveProperty("NOT_ALLOWLISTED_SECRET");
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading