From 0d14ea27fdfc1145ce0da9a81d2fe83b43adbed3 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 6 Aug 2026 11:48:31 -0700 Subject: [PATCH 1/6] feat(mastra): add Stagehand code-mode MCP example --- .../workflows/codemode-framework-examples.yml | 21 + packages/integrations/README.md | 1 + .../integrations/examples/mastra/README.md | 56 ++ .../integrations/examples/mastra/package.json | 23 + .../integrations/examples/mastra/src/agent.ts | 112 ++++ .../integrations/examples/mastra/src/smoke.ts | 79 +++ .../examples/mastra/tsconfig.json | 13 + pnpm-lock.yaml | 484 +++++++++++++++++- pnpm-workspace.yaml | 2 + 9 files changed, 790 insertions(+), 1 deletion(-) create mode 100644 packages/integrations/examples/mastra/README.md create mode 100644 packages/integrations/examples/mastra/package.json create mode 100644 packages/integrations/examples/mastra/src/agent.ts create mode 100644 packages/integrations/examples/mastra/src/smoke.ts create mode 100644 packages/integrations/examples/mastra/tsconfig.json diff --git a/.github/workflows/codemode-framework-examples.yml b/.github/workflows/codemode-framework-examples.yml index 996ce8ee6c..6af411392f 100644 --- a/.github/workflows/codemode-framework-examples.yml +++ b/.github/workflows/codemode-framework-examples.yml @@ -51,3 +51,24 @@ jobs: env: CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }} STAGEHAND_BROWSER: local + + mastra: + name: Mastra + 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" + + - uses: ./.github/actions/setup-chrome-verified + id: setup-chrome + + - run: pnpm exec turbo run build --filter @browserbasehq/stagehand-integrations + - run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra typecheck + - run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra smoke + env: + CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }} + STAGEHAND_BROWSER: local diff --git a/packages/integrations/README.md b/packages/integrations/README.md index f66aa85aa7..8447be54e5 100644 --- a/packages/integrations/README.md +++ b/packages/integrations/README.md @@ -47,6 +47,7 @@ The process stays alive across calls and closes when its input stream ends. `SIG ### Framework examples - [Vercel AI SDK](./examples/vercel) launches the stdio server through the AI SDK MCP client and keeps one process alive for the complete agent run. +- [Mastra](./examples/mastra) discovers the canonical MCP toolset once and reuses one client and browser for the complete agent run. ### Configuration diff --git a/packages/integrations/examples/mastra/README.md b/packages/integrations/examples/mastra/README.md new file mode 100644 index 0000000000..fa41834f32 --- /dev/null +++ b/packages/integrations/examples/mastra/README.md @@ -0,0 +1,56 @@ +# Mastra with Stagehand code mode + +This example gives a Mastra agent one browser tool: `code_execute`. It launches the canonical +Stagehand code-mode MCP server from this repository over stdio, keeps that process and its browser +session alive for the lifetime of the agent handle, and disconnects it during cleanup. + +The agent instructions come directly from the MCP tool description. The example intentionally does +not copy the Stagehand executor, schema, or code-mode skill, so the agent and the tool cannot drift +onto different browser APIs. + +## Run the smoke test + +From the repository root, install dependencies, build the integration package, and run the smoke: + +```bash +pnpm install +pnpm --filter @browserbasehq/stagehand-extension build +pnpm --filter @browserbasehq/stagehand build +pnpm --filter @browserbasehq/stagehand-integrations build +STAGEHAND_BROWSER=local pnpm --dir packages/integrations/examples/mastra smoke +``` + +The smoke opens a real local browser and invokes the Mastra MCP tool twice. The first call writes a +marker into the page; the second reads it back. Seeing the same marker proves that both calls used +the same MCP process and browser session. The final output also confirms that the MCP client +disconnected cleanly. + +To use a Browserbase browser instead, inherit the normal Stagehand environment variables: + +```bash +STAGEHAND_BROWSER=browserbase \ +BROWSERBASE_API_KEY= \ +BROWSERBASE_PROJECT_ID= \ +pnpm --dir packages/integrations/examples/mastra smoke +``` + +## Use the agent + +```ts +import { createStagehandAgent } from "./src/agent.js"; + +const stagehand = await createStagehandAgent(); + +try { + const response = await stagehand.agent.generate("Open example.com and return the page heading.", { + maxSteps: 8, + }); + console.log(response.text); +} finally { + await stagehand.close(); +} +``` + +Set `MASTRA_MODEL` to choose a different Mastra model. `createStagehandMcpClient()` forwards the +current process environment to the stdio server, including `STAGEHAND_BROWSER`, +`BROWSERBASE_API_KEY`, and `BROWSERBASE_PROJECT_ID`. diff --git a/packages/integrations/examples/mastra/package.json b/packages/integrations/examples/mastra/package.json new file mode 100644 index 0000000000..7c7003114b --- /dev/null +++ b/packages/integrations/examples/mastra/package.json @@ -0,0 +1,23 @@ +{ + "name": "@browserbasehq/stagehand-integrations-example-mastra", + "version": "4.0.0", + "private": true, + "type": "module", + "scripts": { + "smoke": "tsx src/smoke.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@browserbasehq/stagehand-integrations": "workspace:*", + "@mastra/core": "catalog:", + "@mastra/mcp": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "tsx": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": ">=22.18.0" + } +} diff --git a/packages/integrations/examples/mastra/src/agent.ts b/packages/integrations/examples/mastra/src/agent.ts new file mode 100644 index 0000000000..117f9997cf --- /dev/null +++ b/packages/integrations/examples/mastra/src/agent.ts @@ -0,0 +1,112 @@ +import { fileURLToPath } from "node:url"; + +import { Agent } from "@mastra/core/agent"; +import { MCPClient } from "@mastra/mcp"; + +const DEFAULT_STDIO_SERVER_PATH = fileURLToPath( + new URL("../../../dist/codemode/stdio-server.mjs", import.meta.url), +); + +export function createStagehandMcpClient(stdioServerPath = DEFAULT_STDIO_SERVER_PATH): MCPClient { + return new MCPClient({ + id: "stagehand-codemode", + servers: { + stagehand: { + command: process.execPath, + args: [stdioServerPath], + env: definedEnvironment(process.env), + }, + }, + }); +} + +export async function loadStagehandCodeTools(mcp: MCPClient) { + const { toolsets, errors } = await mcp.listToolsetsWithErrors(); + if (Object.keys(errors).length > 0) { + throw new Error(`Could not load the Stagehand MCP tools: ${formatErrors(errors)}`); + } + + const stagehandTools = toolsets.stagehand ?? {}; + const toolNames = Object.keys(stagehandTools); + if (toolNames.length !== 1 || toolNames[0] !== "code_execute") { + throw new Error( + `Expected exactly the code_execute tool, received: ${toolNames.join(", ") || "none"}`, + ); + } + + const codeExecute = stagehandTools.code_execute; + if (!codeExecute) { + throw new Error("The Stagehand MCP server did not provide code_execute"); + } + + const guidance = codeExecute.description?.trim(); + if (!guidance?.includes("# Stagehand V4 code-mode syntax")) { + throw new Error("code_execute did not include the canonical Stagehand code-mode guidance"); + } + + return { code_execute: codeExecute }; +} + +export type StagehandCodeTools = Awaited>; + +export type StagehandAgentHandle = { + agent: Agent; + close: () => Promise; +}; + +export async function createStagehandAgent( + model = process.env.MASTRA_MODEL ?? "openai/gpt-5-mini", +): Promise { + const mcp = createStagehandMcpClient(); + + try { + const tools = await loadStagehandCodeTools(mcp); + const instructions = tools.code_execute.description; + if (!instructions) { + throw new Error("code_execute did not provide agent guidance"); + } + + const agent = new Agent({ + id: "stagehand-browser-agent", + name: "Stagehand browser agent", + instructions, + model, + tools, + }); + + return { + agent, + close: () => mcp.disconnect(), + }; + } catch (error) { + await mcp.disconnect().catch(() => undefined); + throw error; + } +} + +export async function runStagehandAgent(prompt: string, model?: string): Promise { + const handle = await createStagehandAgent(model); + + try { + const result = await handle.agent.generate(prompt, { maxSteps: 8 }); + return result.text; + } finally { + await handle.close(); + } +} + +function definedEnvironment(environment: NodeJS.ProcessEnv): Record { + return Object.fromEntries( + Object.entries(environment).filter( + (entry): entry is [string, string] => entry[1] !== undefined, + ), + ); +} + +function formatErrors(errors: Record): string { + return Object.entries(errors) + .map( + ([server, error]) => `${server}: ${error instanceof Error ? error.message : String(error)}`, + ) + .join("; "); +} diff --git a/packages/integrations/examples/mastra/src/smoke.ts b/packages/integrations/examples/mastra/src/smoke.ts new file mode 100644 index 0000000000..4883648172 --- /dev/null +++ b/packages/integrations/examples/mastra/src/smoke.ts @@ -0,0 +1,79 @@ +import assert from "node:assert/strict"; + +import { noopObserve } from "@mastra/core/tools"; + +import { createStagehandMcpClient, loadStagehandCodeTools } from "./agent.js"; + +const marker = "persisted-across-mastra-mcp-calls"; +const title = "Mastra code-mode smoke"; + +const firstCall = ` +await page.evaluate( + ({ marker, title }) => { + document.title = title; + document.body.innerHTML = '
' + marker + '
'; + }, + { marker: ${JSON.stringify(marker)}, title: ${JSON.stringify(title)} }, +); +return { + title: await page.title(), + marker: await page.locator("#mastra-session-marker").innerText(), + pageCount: (await context.pages()).length, +}; +`; + +const secondCall = ` +return { + title: await page.title(), + marker: await page.locator("#mastra-session-marker").innerText(), + pageCount: (await context.pages()).length, +}; +`; + +const browserMode = process.env.STAGEHAND_BROWSER ?? "local"; +process.env.STAGEHAND_BROWSER = browserMode; + +const mcp = createStagehandMcpClient(); + +try { + const tools = await loadStagehandCodeTools(mcp); + const execute = tools.code_execute.execute; + assert.ok(execute, "code_execute must be executable"); + + const first = expectSuccessfulResult( + await execute({ code: firstCall }, { observe: noopObserve }), + ); + assert.deepEqual(first.value, { title, marker, pageCount: 1 }); + + const second = expectSuccessfulResult( + await execute({ code: secondCall }, { observe: noopObserve }), + ); + assert.deepEqual(second.value, { title, marker, pageCount: 1 }); + + console.log( + JSON.stringify({ + status: "PASS", + browser: browserMode, + tool: "code_execute", + calls: 2, + persistentState: second.value, + }), + ); +} finally { + await mcp.disconnect(); + console.log("Mastra MCP disconnect PASS"); +} + +function expectSuccessfulResult(result: unknown): { + ok: true; + value: unknown; +} { + assert.ok(isRecord(result), "code_execute must return an object"); + assert.equal(result.ok, true, `code_execute failed: ${JSON.stringify(result)}`); + assert.ok("value" in result, "code_execute must return a value"); + return result as { ok: true; value: unknown }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/integrations/examples/mastra/tsconfig.json b/packages/integrations/examples/mastra/tsconfig.json new file mode 100644 index 0000000000..bfe3e0e397 --- /dev/null +++ b/packages/integrations/examples/mastra/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "types": ["node"], + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a8104c19d..167987a763 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,12 @@ catalogs: '@changesets/cli': specifier: 2.31.1 version: 2.31.1 + '@mastra/core': + specifier: ^1.55.0 + version: 1.56.0 + '@mastra/mcp': + specifier: ^1.15.0 + version: 1.15.1 '@mdx-js/mdx': specifier: 3.1.1 version: 3.1.1 @@ -586,6 +592,28 @@ importers: specifier: 'catalog:' version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.9.0)) + packages/integrations/examples/mastra: + dependencies: + '@browserbasehq/stagehand-integrations': + specifier: workspace:* + version: link:../.. + '@mastra/core': + specifier: 'catalog:' + version: 1.56.0(ai@7.0.16(zod@4.4.3))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(zod@4.4.3) + '@mastra/mcp': + specifier: 'catalog:' + version: 1.15.1(@mastra/core@1.56.0(ai@7.0.16(zod@4.4.3))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(zod@4.4.3))(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(zod@4.4.3) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.13.2 + tsx: + specifier: 'catalog:' + version: 4.23.1 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/integrations/examples/vercel: dependencies: '@ai-sdk/groq': @@ -679,6 +707,21 @@ importers: packages: + '@a2a-js/sdk@0.3.14': + resolution: {integrity: sha512-F6Ew1AtPzCLhTn8h9yiqTe7DiDf6XVrSnq9V1YqSl9eWqPm6anMveTiKdCSb/76cW0YiJc24rNaUrVezFFHbqQ==} + engines: {node: '>=18'} + peerDependencies: + '@bufbuild/protobuf': ^2.10.2 + '@grpc/grpc-js': ^1.11.0 + express: ^4.21.2 || ^5.1.0 + peerDependenciesMeta: + '@bufbuild/protobuf': + optional: true + '@grpc/grpc-js': + optional: true + express: + optional: true + '@ai-sdk/amazon-bedrock@3.0.111': resolution: {integrity: sha512-vkwKdIn8qLsAXEsr8IPp+6U0INmL0Z6aXz93SPCKU9pfWHVnZXEpBE6N4tVIpa6Y9EBqdRo9XGiLmaoYx5XhrQ==} engines: {node: '>=18'} @@ -811,6 +854,18 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.40': + resolution: {integrity: sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@5.0.11': + resolution: {integrity: sha512-7/96wE+ZsKB35iS9ASyllrE4Ym/EolXEB7AkuJ5FI++fmS85BVTAs77890C+1Z2jwHfBKjBQSBmsliOsAh0iFQ==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@5.0.5': resolution: {integrity: sha512-oI0t3dvCoqWNV1I8o1Rybi2DXDvHES5r/TrwtJW90tuFLVepgJlftPxrcjh8vaSvjqC2diTuA2vXyjKAyHJm4A==} engines: {node: '>=22'} @@ -825,10 +880,18 @@ packages: resolution: {integrity: sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww==} engines: {node: '>=18'} + '@ai-sdk/provider@3.0.14': + resolution: {integrity: sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA==} + engines: {node: '>=18'} + '@ai-sdk/provider@4.0.2': resolution: {integrity: sha512-pfPoy9J1B1xV7cqJ8MYHOsDYrMv5tR3+EMNfI249OhkD2uRakvav3Fo7XpD2luuN/YNCBY7KfEQc7vEV7KEtyw==} engines: {node: '>=22'} + '@ai-sdk/provider@4.0.3': + resolution: {integrity: sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==} + engines: {node: '>=22'} + '@ai-sdk/togetherai@1.0.49': resolution: {integrity: sha512-g4BpEatN7flh3GZ0CN9KvAUX6uLPmIqGSrKKFvAmC3HZdnF940zl+ChXs3atdbtpr6+cwirxM5RACbUzr0uYhA==} engines: {node: '>=18'} @@ -1905,6 +1968,10 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@isaacs/ttlcache@2.1.5': + resolution: {integrity: sha512-VwGZqqjAWPICTmxUZnbpEfO60LhPWzquik+bmyXGY7pYRn6diEvCI5i6Ca+J6o2y4vS73HrpuMTo2dOvUevH8w==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1945,12 +2012,38 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@lukeed/uuid@2.0.1': + resolution: {integrity: sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==} + engines: {node: '>=8'} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@mastra/core@1.56.0': + resolution: {integrity: sha512-0wpDpg3T6pDRs+MtvPiyiGVxAuLKalR1xabtn3sz6rhervunjBspiRD7Ho0QUW1eRamfFc5mjl7tazkuEK1YJA==} + engines: {node: '>=22.13.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + '@mastra/mcp@1.15.1': + resolution: {integrity: sha512-ripT9hKGV8lRaFAITO1myYTvMprMg75epjH06tEtms+mRyfNcJi0B1bdpX8B1Xd5am5ko2nq5LDkBJCXEN+0Ig==} + engines: {node: '>=22.13.0'} + peerDependencies: + '@mastra/core': '>=1.0.0-0 <2.0.0-0' + + '@mastra/schema-compat@1.3.4': + resolution: {integrity: sha512-2ObUsd21KIVelQy+eKPxJvnMxtmKnWacsIkZovhEYjVcQX9OYTDQ+u4E4RboIJZvurJnFx++/ujQLFznUaEYMg==} + engines: {node: '>=22.13.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -2002,6 +2095,20 @@ packages: '@mintlify/validation@0.1.801': resolution: {integrity: sha512-cikcMYuUAqXnMBFlv6AWPOb9cqGZSaqD9gDa+5I/lxkrZa/+v9ADtTRQxKdEYxHMRpwgd95XVEyh5dUxiNkZag==} + '@modelcontextprotocol/ext-apps@1.7.5': + resolution: {integrity: sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==} + engines: {node: '>=20'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.29.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -2452,9 +2559,15 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@posthog/core@1.46.8': + resolution: {integrity: sha512-WQTSwlFhsWk09xngTimHiTyhFU8QZHFZEGSm+6brY3acwYdvTLcTxpIhgl/qOCgn/XoqlCFTZqU1ldWLxNntfA==} + '@posthog/core@1.7.1': resolution: {integrity: sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA==} + '@posthog/types@1.402.1': + resolution: {integrity: sha512-4PZ9wMYI8m8AqJuZ9YR1IAHGVtSnYbBVBgTevrKpzZbcSe/OUwhEV2Ks//rJh/L8eMTU8R5DrD4D/hlrOwaAiQ==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -2804,6 +2917,9 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shikijs/core@3.23.0': resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} @@ -2843,10 +2959,18 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@sindresorhus/slugify@2.2.0': resolution: {integrity: sha512-9Vybc/qX8Kj6pxJaapjkFbiUJPk7MAkCh/GFCxIBnnsuYCFPIXKvnLidG8xlepht3i24L5XemUmGtrJ3UWrl6w==} engines: {node: '>=12'} + '@sindresorhus/slugify@2.2.1': + resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==} + engines: {node: '>=12'} + '@sindresorhus/transliterate@1.6.0': resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==} engines: {node: '>=12'} @@ -3142,6 +3266,9 @@ packages: '@workflow/serde@4.1.0': resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} + '@workflow/serde@4.1.0-beta.2': + resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -3605,6 +3732,21 @@ packages: chardet@2.2.0: resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + chat@4.36.0: + resolution: {integrity: sha512-3A5HjnjilStMazAgY2PESloyT8g+GsoEf5uvqzPtvpGJrNh9oGWQ7IRZzw4+pFFTbUnCEtsclEAc2KMloRRZRQ==} + engines: {node: '>=20'} + peerDependencies: + ai: ^6.0.182 || ^7.0.0 + workflow: ^5.0.0-beta.35 + zod: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + ai: + optional: true + workflow: + optional: true + zod: + optional: true + chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -3778,6 +3920,10 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} + croner@10.0.1: + resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} + engines: {node: '>=18.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -4205,6 +4351,14 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + exit-hook@5.1.0: + resolution: {integrity: sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog==} + engines: {node: '>=20'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -4311,6 +4465,10 @@ packages: fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + filelist@1.0.6: resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} @@ -4482,6 +4640,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -4691,6 +4853,10 @@ packages: resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -4902,6 +5068,10 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-network-error@1.3.2: + resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==} + engines: {node: '>=16'} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -4937,6 +5107,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -4953,6 +5127,10 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -5001,6 +5179,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5034,6 +5215,10 @@ packages: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} + json-schema-to-zod@2.8.1: + resolution: {integrity: sha512-fRr1mHgZ7hboLKBUdR428gd9dIHUFGivUqOeiDcSmyXkNZCtB1uGaZLvsjZ4GaN5pwBIs+TGIOf6s+Rp5/R/zA==} + hasBin: true + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -5210,6 +5395,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -5747,6 +5936,10 @@ packages: resolution: {integrity: sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==} engines: {node: '>=14.16'} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -5910,10 +6103,18 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} + p-map@7.0.6: + resolution: {integrity: sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==} + engines: {node: '>=18'} + p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} + p-retry@7.1.1: + resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} + engines: {node: '>=20'} + p-some@6.0.0: resolution: {integrity: sha512-CJbQCKdfSX3fIh8/QKgS+9rjm7OBNUTmwWswAFQAhc8j1NR1dsEDETUEuVUtQHZpV+J03LqWBEwvu0g1Yn+TYg==} engines: {node: '>=12.20'} @@ -5957,6 +6158,10 @@ packages: parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -5984,6 +6189,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -6122,6 +6331,15 @@ packages: resolution: {integrity: sha512-lz3YJOr0Nmiz0yHASaINEDHqoV+0bC3eD8aZAG+Ky292dAnVYul+ga/dMX8KCBXg8hHfKdxw0SztYD5j6dgUqQ==} engines: {node: '>=20'} + posthog-node@5.48.0: + resolution: {integrity: sha512-YIH82XV24aa1nnVph1ndiW/vBN2QDIKlrHNEJcmAYutMGeoGC4WeEefg7O5aD3dDcO5dzociy8sVwXq4tNDYsQ==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -6133,6 +6351,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -6403,6 +6625,9 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + remend@1.3.0: + resolution: {integrity: sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -6802,6 +7027,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -6924,6 +7153,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tokenx@1.6.0: + resolution: {integrity: sha512-CKTjk345ajvBAUp5xUI9a5KKN0zU0lBueVHQbCskH1Hp6WkUKsPW2qGCYNs0pxNyfzxfo+IIjdt2W4sMbw/qBw==} + tough-cookie@6.0.2: resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} @@ -7070,6 +7302,10 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7381,6 +7617,9 @@ packages: engines: {node: '>= 0.10.0'} hasBin: true + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -7413,6 +7652,10 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} @@ -7420,6 +7663,12 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} + zod-from-json-schema@0.0.5: + resolution: {integrity: sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ==} + + zod-from-json-schema@0.5.6: + resolution: {integrity: sha512-U33AJ7ZWS6y9XNSzMWcdy8hRAvZmWhTtpYJu0SXPT5AArbc9nq2ur7Magzmn5RF9KBV4b3FP0nCpmqqlfXlR9w==} + zod-to-json-schema@3.20.4: resolution: {integrity: sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg==} peerDependencies: @@ -7465,6 +7714,12 @@ packages: snapshots: + '@a2a-js/sdk@0.3.14(express@5.2.1)': + dependencies: + uuid: 11.1.1 + optionalDependencies: + express: 5.2.1 + '@ai-sdk/amazon-bedrock@3.0.111(zod@4.4.3)': dependencies: '@ai-sdk/anthropic': 2.0.91(zod@4.4.3) @@ -7627,6 +7882,21 @@ snapshots: eventsource-parser: 3.1.0 zod: 4.4.3 + '@ai-sdk/provider-utils@4.0.40(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.14 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.1.0 + zod: 4.4.3 + + '@ai-sdk/provider-utils@5.0.11(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.3 + '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 + eventsource-parser: 3.1.0 + zod: 4.4.3 + '@ai-sdk/provider-utils@5.0.5(zod@4.4.3)': dependencies: '@ai-sdk/provider': 4.0.2 @@ -7643,10 +7913,18 @@ snapshots: dependencies: json-schema: 0.4.0 + '@ai-sdk/provider@3.0.14': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/provider@4.0.2': dependencies: json-schema: 0.4.0 + '@ai-sdk/provider@4.0.3': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/togetherai@1.0.49(zod@4.4.3)': dependencies: '@ai-sdk/openai-compatible': 1.0.46(zod@4.4.3) @@ -8620,6 +8898,8 @@ snapshots: dependencies: minipass: 7.1.3 + '@isaacs/ttlcache@2.1.5': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8656,6 +8936,12 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} + '@lukeed/csprng@1.1.0': {} + + '@lukeed/uuid@2.0.1': + dependencies: + '@lukeed/csprng': 1.1.0 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.29.7 @@ -8672,6 +8958,74 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@mastra/core@1.56.0(ai@7.0.16(zod@4.4.3))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(zod@4.4.3)': + dependencies: + '@a2a-js/sdk': 0.3.14(express@5.2.1) + '@ai-sdk/provider-utils-v5': '@ai-sdk/provider-utils@3.0.30(zod@4.4.3)' + '@ai-sdk/provider-utils-v6': '@ai-sdk/provider-utils@4.0.40(zod@4.4.3)' + '@ai-sdk/provider-utils-v7': '@ai-sdk/provider-utils@5.0.11(zod@4.4.3)' + '@ai-sdk/provider-v5': '@ai-sdk/provider@2.0.3' + '@ai-sdk/provider-v6': '@ai-sdk/provider@3.0.14' + '@ai-sdk/provider-v7': '@ai-sdk/provider@4.0.3' + '@isaacs/ttlcache': 2.1.5 + '@lukeed/uuid': 2.0.1 + '@mastra/schema-compat': 1.3.4(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + '@sindresorhus/slugify': 2.2.1 + '@standard-schema/spec': 1.1.0 + ajv: 8.20.0 + chat: 4.36.0(ai@7.0.16(zod@4.4.3))(zod@4.4.3) + croner: 10.0.1 + dotenv: 17.4.2 + execa: 9.6.1 + fastq: 1.20.1 + gray-matter: 4.0.3 + ignore: 7.0.5 + jpeg-js: 0.4.4 + json-schema: 0.4.0 + lru-cache: 11.5.2 + p-map: 7.0.6 + p-retry: 7.1.1 + picomatch: 4.0.5 + posthog-node: 5.48.0(rxjs@7.8.2) + tokenx: 1.6.0 + ws: 8.21.0(bufferutil@4.1.0) + xxhash-wasm: 1.1.0 + zod: 4.4.3 + transitivePeerDependencies: + - '@bufbuild/protobuf' + - '@cfworker/json-schema' + - '@grpc/grpc-js' + - ai + - bufferutil + - express + - rxjs + - supports-color + - utf-8-validate + - workflow + + '@mastra/mcp@1.15.1(@mastra/core@1.56.0(ai@7.0.16(zod@4.4.3))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(zod@4.4.3))(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(zod@4.4.3)': + dependencies: + '@mastra/core': 1.56.0(ai@7.0.16(zod@4.4.3))(bufferutil@4.1.0)(express@5.2.1)(rxjs@7.8.2)(zod@4.4.3) + '@modelcontextprotocol/ext-apps': 1.7.5(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + exit-hook: 5.1.0 + fast-deep-equal: 3.1.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - react + - react-dom + - supports-color + - zod + + '@mastra/schema-compat@1.3.4(zod@4.4.3)': + dependencies: + json-schema-to-zod: 2.8.1 + zod: 4.4.3 + zod-from-json-schema: 0.5.6 + zod-from-json-schema-v3: zod-from-json-schema@0.0.5 + zod-to-json-schema: 3.25.2(zod@4.4.3) + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.9 @@ -8868,7 +9222,7 @@ snapshots: react: 19.2.3 react-dom: 18.3.1(react@19.2.3) rehype-katex: 7.0.1 - remark-gfm: 4.0.0 + remark-gfm: 4.0.1 remark-math: 6.0.0 remark-smartypants: 3.0.3 shiki: 3.23.0 @@ -9029,6 +9383,15 @@ snapshots: - supports-color - typescript + '@modelcontextprotocol/ext-apps@1.7.5(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(zod@4.4.3)': + dependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + '@standard-schema/spec': 1.1.0 + zod: 4.4.3 + optionalDependencies: + react: 19.2.3 + react-dom: 18.3.1(react@19.2.3) + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.15(hono@4.12.32) @@ -9352,10 +9715,16 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@posthog/core@1.46.8': + dependencies: + '@posthog/types': 1.402.1 + '@posthog/core@1.7.1': dependencies: cross-spawn: 7.0.6 + '@posthog/types@1.402.1': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -9626,6 +9995,8 @@ snapshots: '@scarf/scarf@1.4.0': {} + '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/core@3.23.0': dependencies: '@shikijs/types': 3.23.0 @@ -9681,11 +10052,18 @@ snapshots: '@sindresorhus/is@5.6.0': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@sindresorhus/slugify@2.2.0': dependencies: '@sindresorhus/transliterate': 1.6.0 escape-string-regexp: 5.0.0 + '@sindresorhus/slugify@2.2.1': + dependencies: + '@sindresorhus/transliterate': 1.6.0 + escape-string-regexp: 5.0.0 + '@sindresorhus/transliterate@1.6.0': dependencies: escape-string-regexp: 5.0.0 @@ -10065,6 +10443,8 @@ snapshots: '@workflow/serde@4.1.0': {} + '@workflow/serde@4.1.0-beta.2': {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -10564,6 +10944,21 @@ snapshots: chardet@2.2.0: {} + chat@4.36.0(ai@7.0.16(zod@4.4.3))(zod@4.4.3): + dependencies: + '@workflow/serde': 4.1.0-beta.2 + mdast-util-to-string: 4.0.0 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + remend: 1.3.0 + unified: 11.0.5 + optionalDependencies: + ai: 7.0.16(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - supports-color + chokidar@3.5.3: dependencies: anymatch: 3.1.3 @@ -10732,6 +11127,8 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 + croner@10.0.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -11237,6 +11634,23 @@ snapshots: dependencies: eventsource-parser: 3.1.0 + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.2.0 + + exit-hook@5.1.0: {} + expand-template@2.0.3: optional: true @@ -11407,6 +11821,10 @@ snapshots: fflate@0.8.3: {} + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + filelist@1.0.6: dependencies: minimatch: 5.1.9 @@ -11598,6 +12016,11 @@ snapshots: get-stream@6.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -11966,6 +12389,8 @@ snapshots: human-id@4.2.0: {} + human-signals@8.0.1: {} + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -12175,6 +12600,8 @@ snapshots: is-negative-zero@2.0.3: {} + is-network-error@1.3.2: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -12208,6 +12635,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@4.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -12227,6 +12656,8 @@ snapshots: dependencies: which-typed-array: 1.1.22 + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -12268,6 +12699,8 @@ snapshots: joycon@3.1.1: {} + jpeg-js@0.4.4: {} + js-tokens@4.0.0: {} js-yaml@3.15.0: @@ -12296,6 +12729,8 @@ snapshots: '@babel/runtime': 7.29.7 ts-algebra: 2.0.0 + json-schema-to-zod@2.8.1: {} + json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} @@ -12443,6 +12878,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.2: {} + lru-cache@7.18.3: {} magic-string@0.30.21: @@ -13165,6 +13602,11 @@ snapshots: normalize-url@8.1.1: {} + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -13340,11 +13782,17 @@ snapshots: p-map@2.1.0: {} + p-map@7.0.6: {} + p-retry@4.6.2: dependencies: '@types/retry': 0.12.0 retry: 0.13.1 + p-retry@7.1.1: + dependencies: + is-network-error: 1.3.2 + p-some@6.0.0: dependencies: aggregate-error: 4.0.1 @@ -13410,6 +13858,8 @@ snapshots: unist-util-visit-children: 3.0.0 vfile: 6.0.3 + parse-ms@4.0.0: {} + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -13426,6 +13876,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -13558,6 +14010,12 @@ snapshots: dependencies: '@posthog/core': 1.7.1 + posthog-node@5.48.0(rxjs@7.8.2): + dependencies: + '@posthog/core': 1.46.8 + optionalDependencies: + rxjs: 7.8.2 + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -13576,6 +14034,10 @@ snapshots: prettier@2.8.8: {} + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + process-nextick-args@2.0.1: {} process-warning@5.0.0: {} @@ -14010,6 +14472,8 @@ snapshots: transitivePeerDependencies: - supports-color + remend@1.3.0: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -14579,6 +15043,8 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@2.0.1: optional: true @@ -14757,6 +15223,8 @@ snapshots: toidentifier@1.0.1: {} + tokenx@1.6.0: {} + tough-cookie@6.0.2: dependencies: tldts: 7.4.9 @@ -14910,6 +15378,8 @@ snapshots: undici-types@7.24.6: {} + unicorn-magic@0.3.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -15258,6 +15728,8 @@ snapshots: commander: 2.20.3 cssfilter: 0.0.10 + xxhash-wasm@1.1.0: {} + y18n@5.0.8: {} yallist@5.0.0: {} @@ -15293,6 +15765,8 @@ snapshots: yoctocolors-cjs@2.1.3: {} + yoctocolors@2.2.0: {} + yoga-layout@3.2.1: {} zip-stream@6.0.1: @@ -15301,6 +15775,14 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 + zod-from-json-schema@0.0.5: + dependencies: + zod: 3.25.76 + + zod-from-json-schema@0.5.6: + dependencies: + zod: 4.4.3 + zod-to-json-schema@3.20.4(zod@3.24.0): dependencies: zod: 3.24.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c32214057b..8f76f69b5b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,6 +6,8 @@ catalogMode: prefer catalog: "@ai-sdk/mcp": 2.0.8 + "@mastra/core": ^1.55.0 + "@mastra/mcp": ^1.15.0 "@modelcontextprotocol/sdk": 1.29.0 "@ast-grep/lang-go": 0.0.6 "@ast-grep/lang-python": 0.0.6 From e4367d1d5cdeb82dd0bcfbd85ab6d8550a2e8a80 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Fri, 7 Aug 2026 15:11:16 -0700 Subject: [PATCH 2/6] fix(mastra): satisfy repository lint --- packages/integrations/examples/mastra/src/smoke.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integrations/examples/mastra/src/smoke.ts b/packages/integrations/examples/mastra/src/smoke.ts index 4883648172..58661d2c90 100644 --- a/packages/integrations/examples/mastra/src/smoke.ts +++ b/packages/integrations/examples/mastra/src/smoke.ts @@ -50,18 +50,18 @@ try { ); assert.deepEqual(second.value, { title, marker, pageCount: 1 }); - console.log( - JSON.stringify({ + process.stdout.write( + `${JSON.stringify({ status: "PASS", browser: browserMode, tool: "code_execute", calls: 2, persistentState: second.value, - }), + })}\n`, ); } finally { await mcp.disconnect(); - console.log("Mastra MCP disconnect PASS"); + process.stdout.write("Mastra MCP disconnect PASS\n"); } function expectSuccessfulResult(result: unknown): { From e987646a06da4ce9921fa899a0825495423b68c2 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Sat, 8 Aug 2026 03:26:26 +0000 Subject: [PATCH 3/6] refactor(mastra): use sandboxed code-mode MCP --- .../workflows/codemode-framework-examples.yml | 20 +- .../integrations/examples/mastra/README.md | 66 ++++--- .../integrations/examples/mastra/package.json | 2 +- .../integrations/examples/mastra/src/agent.ts | 39 ++-- .../integrations/examples/mastra/src/e2e.ts | 179 ++++++++++++++++++ .../integrations/examples/mastra/src/smoke.ts | 79 -------- 6 files changed, 252 insertions(+), 133 deletions(-) create mode 100644 packages/integrations/examples/mastra/src/e2e.ts delete mode 100644 packages/integrations/examples/mastra/src/smoke.ts diff --git a/.github/workflows/codemode-framework-examples.yml b/.github/workflows/codemode-framework-examples.yml index ce8b046a6f..8513cb5fc7 100644 --- a/.github/workflows/codemode-framework-examples.yml +++ b/.github/workflows/codemode-framework-examples.yml @@ -80,6 +80,10 @@ jobs: mastra: name: Mastra + 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: @@ -89,12 +93,16 @@ jobs: 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 @browserbasehq/stagehand-integrations-example-mastra typecheck - - run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra smoke + - run: pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts + - run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra e2e env: - CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }} - STAGEHAND_BROWSER: local + 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 }} diff --git a/packages/integrations/examples/mastra/README.md b/packages/integrations/examples/mastra/README.md index fa41834f32..e5b4cbf1e4 100644 --- a/packages/integrations/examples/mastra/README.md +++ b/packages/integrations/examples/mastra/README.md @@ -1,45 +1,57 @@ # Mastra with Stagehand code mode -This example gives a Mastra agent one browser tool: `code_execute`. It launches the canonical -Stagehand code-mode MCP server from this repository over stdio, keeps that process and its browser -session alive for the lifetime of the agent handle, and disconnects it during cleanup. +This example gives a Mastra agent one browser tool: `code_execute`. The Stagehand code-mode MCP +server runs inside the package-installed [Vercel Sandbox example](../vercel-sandbox); Mastra connects +to its authenticated Streamable HTTP endpoint and keeps one MCP client and browser session alive for +the agent handle's lifetime. -The agent instructions come directly from the MCP tool description. The example intentionally does -not copy the Stagehand executor, schema, or code-mode skill, so the agent and the tool cannot drift -onto different browser APIs. +The agent instructions come directly from the MCP tool description. The adapter does not copy the +Stagehand executor, schema, or code-mode skill, so the agent and the tool cannot drift onto different +browser APIs. -## Run the smoke test +## Run the end-to-end proof -From the repository root, install dependencies, build the integration package, and run the smoke: +From the repository root, install dependencies, build and pack the exact Stagehand artifacts, then +run the Mastra proof: ```bash pnpm install -pnpm --filter @browserbasehq/stagehand-extension build -pnpm --filter @browserbasehq/stagehand build -pnpm --filter @browserbasehq/stagehand-integrations build -STAGEHAND_BROWSER=local pnpm --dir packages/integrations/examples/mastra smoke +pnpm exec turbo run build --filter @browserbasehq/stagehand-codemode +pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts +STAGEHAND_SANDBOX_ARTIFACTS="$PWD/packages/integrations/examples/vercel-sandbox/.artifacts" \ +BROWSERBASE_API_KEY= \ +BROWSERBASE_PROJECT_ID= \ +VERCEL_OIDC_TOKEN= \ +OPENAI_API_KEY= \ +pnpm --dir packages/integrations/examples/mastra e2e ``` -The smoke opens a real local browser and invokes the Mastra MCP tool twice. The first call writes a -marker into the page; the second reads it back. Seeing the same marker proves that both calls used -the same MCP process and browser session. The final output also confirms that the MCP client -disconnected cleanly. +For local Vercel credentials, replace `VERCEL_OIDC_TOKEN` with `VERCEL_TEAM_ID`, +`VERCEL_PROJECT_ID`, and `VERCEL_TOKEN`. -To use a Browserbase browser instead, inherit the normal Stagehand environment variables: +The proof exercises one live package-installed sandbox and one persistent Mastra MCP client. It: -```bash -STAGEHAND_BROWSER=browserbase \ -BROWSERBASE_API_KEY= \ -BROWSERBASE_PROJECT_ID= \ -pnpm --dir packages/integrations/examples/mastra smoke -``` +1. invokes `code_execute` twice directly and verifies the same page ID and DOM marker; +2. makes a real Mastra model select `code_execute` and modify that existing page; +3. invokes the tool again to independently verify the model's browser-side change; +4. proves `OPENAI_API_KEY` and a host-only marker never enter model-generated code; and +5. disconnects Mastra before stopping and deleting the sandbox, emitting `PASS` only after cleanup. + +Set `MASTRA_MODEL` to use a model other than `openai/gpt-5-mini`. ## Use the agent ```ts +import { createStagehandSandbox } from "@browserbasehq/stagehand-integrations-example-vercel-sandbox"; + import { createStagehandAgent } from "./src/agent.js"; -const stagehand = await createStagehandAgent(); +const connection = await createStagehandSandbox({ + packageArtifactsPath: process.env.STAGEHAND_SANDBOX_ARTIFACTS!, + browserbaseApiKey: process.env.BROWSERBASE_API_KEY!, + browserbaseProjectId: process.env.BROWSERBASE_PROJECT_ID!, +}); +const stagehand = await createStagehandAgent(connection); try { const response = await stagehand.agent.generate("Open example.com and return the page heading.", { @@ -48,9 +60,9 @@ try { console.log(response.text); } finally { await stagehand.close(); + await connection.close(); } ``` -Set `MASTRA_MODEL` to choose a different Mastra model. `createStagehandMcpClient()` forwards the -current process environment to the stdio server, including `STAGEHAND_BROWSER`, -`BROWSERBASE_API_KEY`, and `BROWSERBASE_PROJECT_ID`. +The outer application owns the sandbox connection. Disconnect the Mastra MCP client before closing +that connection so the HTTP transport can finish cleanly. diff --git a/packages/integrations/examples/mastra/package.json b/packages/integrations/examples/mastra/package.json index 5366ee9a2a..1a81b14e6c 100644 --- a/packages/integrations/examples/mastra/package.json +++ b/packages/integrations/examples/mastra/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "smoke": "tsx src/smoke.ts", + "e2e": "tsx src/e2e.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/integrations/examples/mastra/src/agent.ts b/packages/integrations/examples/mastra/src/agent.ts index 117f9997cf..a61afe6fb7 100644 --- a/packages/integrations/examples/mastra/src/agent.ts +++ b/packages/integrations/examples/mastra/src/agent.ts @@ -1,20 +1,20 @@ -import { fileURLToPath } from "node:url"; - import { Agent } from "@mastra/core/agent"; import { MCPClient } from "@mastra/mcp"; +import type { StagehandSandboxConnection } from "@browserbasehq/stagehand-integrations-example-vercel-sandbox"; -const DEFAULT_STDIO_SERVER_PATH = fileURLToPath( - new URL("../../../dist/codemode/stdio-server.mjs", import.meta.url), -); +type RemoteStagehandConnection = Pick; -export function createStagehandMcpClient(stdioServerPath = DEFAULT_STDIO_SERVER_PATH): MCPClient { +export function createStagehandMcpClient(connection: RemoteStagehandConnection): MCPClient { return new MCPClient({ id: "stagehand-codemode", servers: { stagehand: { - command: process.execPath, - args: [stdioServerPath], - env: definedEnvironment(process.env), + url: connection.url, + fetch: async (input, init) => { + const headers = new Headers(init?.headers); + headers.set("Authorization", `Bearer ${connection.token}`); + return fetch(input, { ...init, headers }); + }, }, }, }); @@ -51,13 +51,15 @@ export type StagehandCodeTools = Awaited Promise; }; export async function createStagehandAgent( + connection: RemoteStagehandConnection, model = process.env.MASTRA_MODEL ?? "openai/gpt-5-mini", ): Promise { - const mcp = createStagehandMcpClient(); + const mcp = createStagehandMcpClient(connection); try { const tools = await loadStagehandCodeTools(mcp); @@ -76,6 +78,7 @@ export async function createStagehandAgent( return { agent, + tools, close: () => mcp.disconnect(), }; } catch (error) { @@ -84,8 +87,12 @@ export async function createStagehandAgent( } } -export async function runStagehandAgent(prompt: string, model?: string): Promise { - const handle = await createStagehandAgent(model); +export async function runStagehandAgent( + connection: RemoteStagehandConnection, + prompt: string, + model?: string, +): Promise { + const handle = await createStagehandAgent(connection, model); try { const result = await handle.agent.generate(prompt, { maxSteps: 8 }); @@ -95,14 +102,6 @@ export async function runStagehandAgent(prompt: string, model?: string): Promise } } -function definedEnvironment(environment: NodeJS.ProcessEnv): Record { - return Object.fromEntries( - Object.entries(environment).filter( - (entry): entry is [string, string] => entry[1] !== undefined, - ), - ); -} - function formatErrors(errors: Record): string { return Object.entries(errors) .map( diff --git a/packages/integrations/examples/mastra/src/e2e.ts b/packages/integrations/examples/mastra/src/e2e.ts new file mode 100644 index 0000000000..a971ac6005 --- /dev/null +++ b/packages/integrations/examples/mastra/src/e2e.ts @@ -0,0 +1,179 @@ +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; + +import { noopObserve } from "@mastra/core/tools"; +import { createStagehandSandbox } from "@browserbasehq/stagehand-integrations-example-vercel-sandbox"; + +import { createStagehandAgent, type StagehandAgentHandle } from "./agent.js"; + +const directMarker = `mastra-direct-${randomUUID()}`; +const modelMarker = `mastra-model-${randomUUID()}`; +const NO_ERROR = Symbol("no error"); +process.env.MASTRA_HOST_ONLY_MARKER = `host-${randomUUID()}`; + +const connection = await createStagehandSandbox({ + packageArtifactsPath: requiredEnvironment("STAGEHAND_SANDBOX_ARTIFACTS"), + browserbaseApiKey: requiredEnvironment("BROWSERBASE_API_KEY"), + browserbaseProjectId: requiredEnvironment("BROWSERBASE_PROJECT_ID"), + vercelCredentials: vercelCredentialsFromEnvironment(), +}); + +let handle: StagehandAgentHandle | undefined; +let primaryError: unknown = NO_ERROR; +let modelToolCalls = 0; +let finalState: Record | undefined; + +try { + handle = await createStagehandAgent(connection); + const execute = handle.tools.code_execute.execute; + assert.ok(execute, "code_execute must be executable"); + + const first = expectSuccessfulResult( + await execute( + { + code: ` + await page.goto("https://example.com", { waitUntil: "domcontentloaded" }); + await page.evaluate((marker) => { + document.documentElement.dataset.mastraDirectMarker = marker; + }, ${JSON.stringify(directMarker)}); + return { + title: await page.title(), + pageId: page.pageId, + directMarker: await page.evaluate( + () => document.documentElement.dataset.mastraDirectMarker, + ), + modelKeyVisible: process.env.OPENAI_API_KEY ?? null, + hostMarkerVisible: process.env.MASTRA_HOST_ONLY_MARKER ?? null, + }; + `, + }, + { observe: noopObserve }, + ), + ); + assert.equal(first.value.title, "Example Domain"); + assert.equal(first.value.directMarker, directMarker); + assert.equal(first.value.modelKeyVisible, null); + assert.equal(first.value.hostMarkerVisible, null); + + const second = expectSuccessfulResult( + await execute( + { + code: ` + return { + title: await page.title(), + pageId: page.pageId, + directMarker: await page.evaluate( + () => document.documentElement.dataset.mastraDirectMarker, + ), + }; + `, + }, + { observe: noopObserve }, + ), + ); + assert.equal(second.value.title, "Example Domain"); + assert.equal(second.value.pageId, first.value.pageId); + assert.equal(second.value.directMarker, directMarker); + + const result = await handle.agent.generate( + [ + "Use code_execute to modify the already-open page.", + `Set document.documentElement.dataset.mastraModelMarker to ${JSON.stringify(modelMarker)}.`, + "Then read that dataset value and the current pageId and report them.", + "You must call code_execute; do not merely describe JavaScript.", + ].join(" "), + { maxSteps: 8 }, + ); + assert.equal(result.error, undefined, "the Mastra agent run must not report an error"); + modelToolCalls = result.steps + .flatMap((step) => step.toolCalls) + .filter((call) => call.payload.toolName === "code_execute").length; + assert.ok(modelToolCalls > 0, "the real Mastra model must select code_execute"); + + const verified = expectSuccessfulResult( + await execute( + { + code: ` + return { + title: await page.title(), + pageId: page.pageId, + directMarker: await page.evaluate( + () => document.documentElement.dataset.mastraDirectMarker, + ), + modelMarker: await page.evaluate( + () => document.documentElement.dataset.mastraModelMarker, + ), + }; + `, + }, + { observe: noopObserve }, + ), + ); + assert.equal(verified.value.title, "Example Domain"); + assert.equal(verified.value.pageId, first.value.pageId); + assert.equal(verified.value.directMarker, directMarker); + assert.equal(verified.value.modelMarker, modelMarker); + finalState = verified.value; +} catch (error) { + primaryError = error; +} + +const cleanupErrors: unknown[] = []; +await handle?.close().catch((error: unknown) => cleanupErrors.push(error)); +await connection.close().catch((error: unknown) => cleanupErrors.push(error)); +throwPrimaryOrCleanup(primaryError, cleanupErrors); + +process.stdout.write( + `${JSON.stringify({ + status: "PASS", + framework: "mastra", + directToolCalls: 3, + modelToolCalls, + sessionPersisted: true, + modelCredentialIsolated: true, + finalState, + cleanup: ["mastra-mcp", "vercel-sandbox"], + })}\n`, +); + +function expectSuccessfulResult(result: unknown): { + ok: true; + value: Record; +} { + assert.ok(isRecord(result), "code_execute must return an object"); + assert.equal(result.ok, true, `code_execute failed: ${JSON.stringify(result)}`); + assert.ok(isRecord(result.value), "code_execute must return an object value"); + return result as { ok: true; value: Record }; +} + +function throwPrimaryOrCleanup(primary: unknown, cleanup: unknown[]): void { + if (primary !== NO_ERROR && cleanup.length > 0) { + throw new AggregateError([primary, ...cleanup], "Mastra E2E and cleanup both failed"); + } + if (primary !== NO_ERROR) throw primary; + if (cleanup.length > 0) { + throw new AggregateError(cleanup, "Could not close Mastra MCP and the Vercel Sandbox"); + } +} + +function requiredEnvironment(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`Missing ${name}`); + return value; +} + +function vercelCredentialsFromEnvironment(): + | { teamId: string; projectId: string; token: string } + | undefined { + const token = process.env.VERCEL_TOKEN; + if (!token) return undefined; + return { + teamId: requiredEnvironment("VERCEL_TEAM_ID"), + projectId: requiredEnvironment("VERCEL_PROJECT_ID"), + token, + }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/integrations/examples/mastra/src/smoke.ts b/packages/integrations/examples/mastra/src/smoke.ts deleted file mode 100644 index 58661d2c90..0000000000 --- a/packages/integrations/examples/mastra/src/smoke.ts +++ /dev/null @@ -1,79 +0,0 @@ -import assert from "node:assert/strict"; - -import { noopObserve } from "@mastra/core/tools"; - -import { createStagehandMcpClient, loadStagehandCodeTools } from "./agent.js"; - -const marker = "persisted-across-mastra-mcp-calls"; -const title = "Mastra code-mode smoke"; - -const firstCall = ` -await page.evaluate( - ({ marker, title }) => { - document.title = title; - document.body.innerHTML = '
' + marker + '
'; - }, - { marker: ${JSON.stringify(marker)}, title: ${JSON.stringify(title)} }, -); -return { - title: await page.title(), - marker: await page.locator("#mastra-session-marker").innerText(), - pageCount: (await context.pages()).length, -}; -`; - -const secondCall = ` -return { - title: await page.title(), - marker: await page.locator("#mastra-session-marker").innerText(), - pageCount: (await context.pages()).length, -}; -`; - -const browserMode = process.env.STAGEHAND_BROWSER ?? "local"; -process.env.STAGEHAND_BROWSER = browserMode; - -const mcp = createStagehandMcpClient(); - -try { - const tools = await loadStagehandCodeTools(mcp); - const execute = tools.code_execute.execute; - assert.ok(execute, "code_execute must be executable"); - - const first = expectSuccessfulResult( - await execute({ code: firstCall }, { observe: noopObserve }), - ); - assert.deepEqual(first.value, { title, marker, pageCount: 1 }); - - const second = expectSuccessfulResult( - await execute({ code: secondCall }, { observe: noopObserve }), - ); - assert.deepEqual(second.value, { title, marker, pageCount: 1 }); - - process.stdout.write( - `${JSON.stringify({ - status: "PASS", - browser: browserMode, - tool: "code_execute", - calls: 2, - persistentState: second.value, - })}\n`, - ); -} finally { - await mcp.disconnect(); - process.stdout.write("Mastra MCP disconnect PASS\n"); -} - -function expectSuccessfulResult(result: unknown): { - ok: true; - value: unknown; -} { - assert.ok(isRecord(result), "code_execute must return an object"); - assert.equal(result.ok, true, `code_execute failed: ${JSON.stringify(result)}`); - assert.ok("value" in result, "code_execute must return a value"); - return result as { ok: true; value: unknown }; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} From 4da6eed48e7ac0382131834457832c2af9d8e6e7 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Sat, 8 Aug 2026 04:03:37 +0000 Subject: [PATCH 4/6] fix(mastra): gate live proof on credentials --- .../workflows/codemode-framework-examples.yml | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codemode-framework-examples.yml b/.github/workflows/codemode-framework-examples.yml index 7b5fa8ad51..d94c9e7657 100644 --- a/.github/workflows/codemode-framework-examples.yml +++ b/.github/workflows/codemode-framework-examples.yml @@ -114,7 +114,26 @@ jobs: - run: pnpm exec turbo run build --filter @browserbasehq/stagehand-codemode - run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra typecheck - run: pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts - - run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra e2e + - name: Detect Mastra live test credentials + id: mastra-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 }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + if [[ -n "$BROWSERBASE_API_KEY" && -n "$BROWSERBASE_PROJECT_ID" && -n "$OPENAI_API_KEY" ]] && \ + [[ -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 Mastra live sandbox proof + if: steps.mastra-live-credentials.outputs.available == 'true' + run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra e2e env: STAGEHAND_SANDBOX_ARTIFACTS: ${{ github.workspace }}/packages/integrations/examples/vercel-sandbox/.artifacts BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }} From 7d72238001456ea196e62168567afcb88b1f9b07 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Sat, 8 Aug 2026 04:08:39 +0000 Subject: [PATCH 5/6] fix(mastra): sanitize sandbox adapter failures --- .../workflows/codemode-framework-examples.yml | 1 + .../integrations/examples/mastra/package.json | 1 + .../examples/mastra/src/agent.test.ts | 57 ++++++++++++++++ .../integrations/examples/mastra/src/agent.ts | 67 ++++++++++--------- .../integrations/examples/mastra/src/e2e.ts | 25 +++++-- 5 files changed, 115 insertions(+), 36 deletions(-) create mode 100644 packages/integrations/examples/mastra/src/agent.test.ts diff --git a/.github/workflows/codemode-framework-examples.yml b/.github/workflows/codemode-framework-examples.yml index d94c9e7657..7d361420d4 100644 --- a/.github/workflows/codemode-framework-examples.yml +++ b/.github/workflows/codemode-framework-examples.yml @@ -113,6 +113,7 @@ jobs: - run: pnpm exec turbo run build --filter @browserbasehq/stagehand-codemode - run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra typecheck + - run: pnpm --filter @browserbasehq/stagehand-integrations-example-mastra test:contract - run: pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts - name: Detect Mastra live test credentials id: mastra-live-credentials diff --git a/packages/integrations/examples/mastra/package.json b/packages/integrations/examples/mastra/package.json index 1a81b14e6c..ed684f6f55 100644 --- a/packages/integrations/examples/mastra/package.json +++ b/packages/integrations/examples/mastra/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "e2e": "tsx src/e2e.ts", + "test:contract": "tsx --test src/*.test.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/integrations/examples/mastra/src/agent.test.ts b/packages/integrations/examples/mastra/src/agent.test.ts new file mode 100644 index 0000000000..c4b71c612e --- /dev/null +++ b/packages/integrations/examples/mastra/src/agent.test.ts @@ -0,0 +1,57 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { MCPClient } from "@mastra/mcp"; + +import { + loadStagehandCodeTools, + StagehandMastraSetupError, + StagehandMastraToolContractError, +} from "./agent.js"; + +const canonicalDescription = "# Stagehand V4 code-mode syntax\nUse the persistent page."; +const codeExecute = { description: canonicalDescription, execute: async () => undefined }; + +void test("accepts exactly one canonical code_execute tool", async () => { + const tools = await loadStagehandCodeTools( + fakeMcp({ toolsets: { stagehand: { code_execute: codeExecute } }, errors: {} }), + ); + assert.equal(tools.code_execute, codeExecute); +}); + +void test("sanitizes transport discovery errors", async () => { + const secret = "https://sandbox.example.test/mcp?token=do-not-reflect"; + const mcp = { + listToolsetsWithErrors: async () => { + throw new Error(secret); + }, + } as unknown as MCPClient; + + await assert.rejects(loadStagehandCodeTools(mcp), (error: unknown) => { + assert.ok(error instanceof StagehandMastraSetupError); + assert.equal(error.message, "Could not configure the Mastra Stagehand agent."); + assert.equal(error.message.includes(secret), false); + return true; + }); +}); + +void test("rejects non-canonical toolsets with a fixed typed error", async () => { + await assert.rejects( + loadStagehandCodeTools( + fakeMcp({ + toolsets: { stagehand: { unexpected_secret_tool: codeExecute } }, + errors: {}, + }), + ), + { + name: StagehandMastraToolContractError.name, + message: "The Stagehand MCP server returned an invalid tool contract.", + }, + ); +}); + +function fakeMcp(result: unknown): MCPClient { + return { + listToolsetsWithErrors: async () => result, + } as unknown as MCPClient; +} diff --git a/packages/integrations/examples/mastra/src/agent.ts b/packages/integrations/examples/mastra/src/agent.ts index a61afe6fb7..7c73c937a0 100644 --- a/packages/integrations/examples/mastra/src/agent.ts +++ b/packages/integrations/examples/mastra/src/agent.ts @@ -4,6 +4,22 @@ import type { StagehandSandboxConnection } from "@browserbasehq/stagehand-integr type RemoteStagehandConnection = Pick; +export class StagehandMastraSetupError extends Error { + override readonly name = "StagehandMastraSetupError"; + + constructor() { + super("Could not configure the Mastra Stagehand agent."); + } +} + +export class StagehandMastraToolContractError extends Error { + override readonly name = "StagehandMastraToolContractError"; + + constructor() { + super("The Stagehand MCP server returned an invalid tool contract."); + } +} + export function createStagehandMcpClient(connection: RemoteStagehandConnection): MCPClient { return new MCPClient({ id: "stagehand-codemode", @@ -21,27 +37,31 @@ export function createStagehandMcpClient(connection: RemoteStagehandConnection): } export async function loadStagehandCodeTools(mcp: MCPClient) { - const { toolsets, errors } = await mcp.listToolsetsWithErrors(); + let discovery: Awaited>; + try { + discovery = await mcp.listToolsetsWithErrors(); + } catch { + throw new StagehandMastraSetupError(); + } + const { toolsets, errors } = discovery; if (Object.keys(errors).length > 0) { - throw new Error(`Could not load the Stagehand MCP tools: ${formatErrors(errors)}`); + throw new StagehandMastraSetupError(); } const stagehandTools = toolsets.stagehand ?? {}; const toolNames = Object.keys(stagehandTools); if (toolNames.length !== 1 || toolNames[0] !== "code_execute") { - throw new Error( - `Expected exactly the code_execute tool, received: ${toolNames.join(", ") || "none"}`, - ); + throw new StagehandMastraToolContractError(); } const codeExecute = stagehandTools.code_execute; if (!codeExecute) { - throw new Error("The Stagehand MCP server did not provide code_execute"); + throw new StagehandMastraToolContractError(); } const guidance = codeExecute.description?.trim(); if (!guidance?.includes("# Stagehand V4 code-mode syntax")) { - throw new Error("code_execute did not include the canonical Stagehand code-mode guidance"); + throw new StagehandMastraToolContractError(); } return { code_execute: codeExecute }; @@ -65,7 +85,7 @@ export async function createStagehandAgent( const tools = await loadStagehandCodeTools(mcp); const instructions = tools.code_execute.description; if (!instructions) { - throw new Error("code_execute did not provide agent guidance"); + throw new StagehandMastraToolContractError(); } const agent = new Agent({ @@ -83,29 +103,12 @@ export async function createStagehandAgent( }; } catch (error) { await mcp.disconnect().catch(() => undefined); - throw error; - } -} - -export async function runStagehandAgent( - connection: RemoteStagehandConnection, - prompt: string, - model?: string, -): Promise { - const handle = await createStagehandAgent(connection, model); - - try { - const result = await handle.agent.generate(prompt, { maxSteps: 8 }); - return result.text; - } finally { - await handle.close(); + if ( + error instanceof StagehandMastraSetupError || + error instanceof StagehandMastraToolContractError + ) { + throw error; + } + throw new StagehandMastraSetupError(); } } - -function formatErrors(errors: Record): string { - return Object.entries(errors) - .map( - ([server, error]) => `${server}: ${error instanceof Error ? error.message : String(error)}`, - ) - .join("; "); -} diff --git a/packages/integrations/examples/mastra/src/e2e.ts b/packages/integrations/examples/mastra/src/e2e.ts index a971ac6005..4e071216af 100644 --- a/packages/integrations/examples/mastra/src/e2e.ts +++ b/packages/integrations/examples/mastra/src/e2e.ts @@ -11,6 +11,22 @@ const modelMarker = `mastra-model-${randomUUID()}`; const NO_ERROR = Symbol("no error"); process.env.MASTRA_HOST_ONLY_MARKER = `host-${randomUUID()}`; +class MastraIsolationError extends Error { + override readonly name = "MastraIsolationError"; + + constructor() { + super("A host-only value crossed the Mastra sandbox boundary."); + } +} + +class MastraAgentRunError extends Error { + override readonly name = "MastraAgentRunError"; + + constructor() { + super("The Mastra agent run failed."); + } +} + const connection = await createStagehandSandbox({ packageArtifactsPath: requiredEnvironment("STAGEHAND_SANDBOX_ARTIFACTS"), browserbaseApiKey: requiredEnvironment("BROWSERBASE_API_KEY"), @@ -52,8 +68,9 @@ try { ); assert.equal(first.value.title, "Example Domain"); assert.equal(first.value.directMarker, directMarker); - assert.equal(first.value.modelKeyVisible, null); - assert.equal(first.value.hostMarkerVisible, null); + if (first.value.modelKeyVisible !== null || first.value.hostMarkerVisible !== null) { + throw new MastraIsolationError(); + } const second = expectSuccessfulResult( await execute( @@ -84,7 +101,7 @@ try { ].join(" "), { maxSteps: 8 }, ); - assert.equal(result.error, undefined, "the Mastra agent run must not report an error"); + if (result.error !== undefined) throw new MastraAgentRunError(); modelToolCalls = result.steps .flatMap((step) => step.toolCalls) .filter((call) => call.payload.toolName === "code_execute").length; @@ -141,7 +158,7 @@ function expectSuccessfulResult(result: unknown): { value: Record; } { assert.ok(isRecord(result), "code_execute must return an object"); - assert.equal(result.ok, true, `code_execute failed: ${JSON.stringify(result)}`); + assert.equal(result.ok, true, "code_execute reported a failure"); assert.ok(isRecord(result.value), "code_execute must return an object value"); return result as { ok: true; value: Record }; } From 6db5cdfe5a3fdd4182a0a831569f715c048731e6 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Sat, 8 Aug 2026 04:30:18 +0000 Subject: [PATCH 6/6] test(mastra): cover resolved discovery errors --- .../examples/mastra/src/agent.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/integrations/examples/mastra/src/agent.test.ts b/packages/integrations/examples/mastra/src/agent.test.ts index c4b71c612e..a325d6cad8 100644 --- a/packages/integrations/examples/mastra/src/agent.test.ts +++ b/packages/integrations/examples/mastra/src/agent.test.ts @@ -35,6 +35,25 @@ void test("sanitizes transport discovery errors", async () => { }); }); +void test("sanitizes resolved MCP discovery errors", async () => { + const secret = "https://sandbox.example.test/mcp?token=do-not-reflect"; + + await assert.rejects( + loadStagehandCodeTools( + fakeMcp({ + toolsets: {}, + errors: { stagehand: new Error(secret) }, + }), + ), + (error: unknown) => { + assert.ok(error instanceof StagehandMastraSetupError); + assert.equal(error.message, "Could not configure the Mastra Stagehand agent."); + assert.equal(error.message.includes(secret), false); + return true; + }, + ); +}); + void test("rejects non-canonical toolsets with a fixed typed error", async () => { await assert.rejects( loadStagehandCodeTools(