219 lines · 8.7 KB · TypeScript
RawDownload
// Cloud-only: MCP browser approval URLs must preserve the session's org scope.//// The MCP session is created in org A, then the browser cookie is moved to org B// before the human opens the approval link. The returned approval URL itself// must carry org A's slug so the rendered approval page scopes both GET and// resume POST requests to org A, not the cookie-pinned org B.import { randomBytes } from "node:crypto"; import { expect } from "@effect/vitest";import { Effect } from "effect";import type { Page, Request } from "playwright";import { composePluginApi } from "@executor-js/api/server"; import { scenario } from "../src/scenario";import { Api, Browser, Mcp, Target } from "../src/services";import { type McpBrowserApproval, parseBrowserApproval } from "../src/surfaces/mcp"; const coreApi = composePluginApi([] as const); const GATE_TOOL = "executor.coreTools.policies.list"; const GATED_CODE = `const result = await tools.executor.coreTools.policies.list({});return JSON.stringify(result);`; const cookiePair = (response: Response, name: string): string | undefined => {  for (const header of response.headers.getSetCookie?.() ?? []) {    if (header.startsWith(`${name}=`)) return header.split(";")[0];  }  return undefined;}; const cookieValue = (pair: string): string => {  const [, value] = pair.split(/=(.*)/s);  if (!value) throw new Error("cookie pair has no value");  return value;}; const cookieOf = (identity: { readonly headers?: Record<string, string> }): string =>  identity.headers?.cookie ?? ""; const originHeaders = (baseUrl: string) => ({ origin: new URL(baseUrl).origin }); const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const setWorkosSessionCookie = async (page: Page, baseUrl: string, cookie: string) => {  await page.context().addCookies([    {      name: "wos-session",      value: cookieValue(cookie),      url: baseUrl,    },  ]);}; const expectOrgShell = async (  page: Page,  org: { readonly name: string; readonly slug: string },) => {  await page.waitForURL(    (url) => url.pathname === `/${org.slug}` || url.pathname === `/${org.slug}/`,    {      timeout: 30_000,    },  );  await page.getByRole("button", { name: new RegExp(escapeRegExp(org.name)) }).waitFor();  await page.getByRole("heading", { name: "Integrations" }).waitFor();}; const activeOrg = (baseUrl: string, cookie: string) =>  Effect.promise(async () => {    const response = await fetch(new URL("/api/auth/me", baseUrl), {      headers: { cookie },    });    if (!response.ok) throw new Error(`/api/auth/me failed (${response.status})`);    const body = (await response.json()) as {      organization: { id: string; name: string; slug: string } | null;    };    if (!body.organization) throw new Error("identity has no active organization");    return body.organization;  }); const createOrganization = (baseUrl: string, cookie: string, name: string) =>  Effect.promise(async () => {    const response = await fetch(new URL("/api/auth/create-organization", baseUrl), {      method: "POST",      headers: {        "content-type": "application/json",        cookie,        ...originHeaders(baseUrl),      },      body: JSON.stringify({ name }),    });    if (!response.ok) {      throw new Error(`/api/auth/create-organization failed (${response.status})`);    }    const session = cookiePair(response, "wos-session");    if (!session) throw new Error("create organization did not refresh the session");    const org = (await response.json()) as { id: string; name: string; slug: string };    return { org, session };  }); const approvalApiRequest =  (approval: McpBrowserApproval, method: "GET" | "POST") =>  (request: Request): boolean => {    const url = request.url();    const executionPath = `/executions/${encodeURIComponent(approval.executionId)}`;    return (      request.method() === method &&      url.includes("/api/mcp-sessions/") &&      url.includes(executionPath) &&      (method === "POST" ? url.endsWith(`${executionPath}/resume`) : !url.endsWith("/resume"))    );  }; scenario(  "MCP approval · URL-scoped org survives approval while the session cookie points elsewhere",  {    timeout: 180_000,    // `mcpSession.listTools()` drives mcporter's OWN generic MCP-session OAuth    // login (its consentStrategy hook against the WorkOS emulator's    // /oauth2/authorize), unrelated to the org-scoped-approval-URL behavior    // this scenario actually tests. That handshake hangs and mcporter's own    // code-wait times out after 60s ("OAuth authorization required ...    // Waiting for browser approval..." -> McpError -32001), before any of    // this scenario's assertions run. Same root cause as    // scenarios/browser-approval.test.ts's cloud-only skip. Real    // harness/product defect (suspect: cloud's mcporter<->WorkOS-emulator    // OAuth session flow), needs a live-debugged fix, tracked separately.    skip: "cloud's mcporter MCP-session OAuth login (listTools' consentStrategy handshake against the WorkOS emulator) hangs and times out after 60s, before this scenario's org-scope assertions ever run — suspect: cloud mcporter<->WorkOS-emulator OAuth session flow",  },  Effect.gen(function* () {    const target = yield* Target;    const api = yield* Api;    const browser = yield* Browser;    const mcp = yield* Mcp;     const identity = yield* target.newIdentity();    const sessionA = cookieOf(identity);    const orgA = yield* activeOrg(target.baseUrl, sessionA);    const client = yield* api.client(coreApi, identity);     const policy = yield* client.policies.create({      payload: { owner: "org", pattern: GATE_TOOL, action: "require_approval" },    });     yield* Effect.gen(function* () {      const mcpSession = mcp.session(identity, { elicitationMode: "browser" });      yield* mcpSession.listTools();       const paused = yield* mcpSession.call("execute", { code: GATED_CODE });      const approval = parseBrowserApproval(paused);      const approvalUrl = new URL(approval.approvalUrl);      expect(approvalUrl.pathname, "the real MCP approval URL is pinned to the session org").toBe(        `/${orgA.slug}/resume/${approval.executionId}`,      );       const { org: orgB, session: sessionB } = yield* createOrganization(        target.baseUrl,        sessionA,        `MCP Approval Org B ${randomBytes(3).toString("hex")}`,      );      expect(orgB.slug, "the browser can be pinned to a different org").not.toBe(orgA.slug);       const [resumed] = yield* Effect.all(        [          mcpSession.awaitResume(approval.executionId),          browser.session(identity, async ({ page, step }) => {            await step("Land in the original organization", async () => {              await page.goto(`/${orgA.slug}`, { waitUntil: "networkidle" });              await expectOrgShell(page, orgA);            });             await step("The browser session is switched to another organization", async () => {              await setWorkosSessionCookie(page, target.baseUrl, sessionB);              await page.goto(`/${orgB.slug}`, { waitUntil: "networkidle" });              await expectOrgShell(page, orgB);            });             await step("Open the original organization's approval URL and approve", async () => {              const loadRequest = page.waitForRequest(approvalApiRequest(approval, "GET"), {                timeout: 30_000,              });              await page.goto(approval.approvalUrl, { waitUntil: "networkidle" });              expect(                (await loadRequest).headers()["x-executor-organization"],                "loading the approval page scopes the paused execution lookup to org A",              ).toBe(orgA.slug);               await page.getByRole("button", { name: "Approve" }).waitFor();              const resumeRequest = page.waitForRequest(approvalApiRequest(approval, "POST"), {                timeout: 30_000,              });              await page.getByRole("button", { name: "Approve" }).click();              expect(                (await resumeRequest).headers()["x-executor-organization"],                "approving the paused execution scopes the resume POST to org A",              ).toBe(orgA.slug);              await page.getByText("Approve sent").waitFor();            });          }),        ],        { concurrency: "unbounded" },      );       expect(resumed.ok, "the approved execution completed without error").toBe(true);      expect(resumed.text, "the gated tool ran in org A and returned its policy").toContain(        policy.id,      );    }).pipe(      Effect.ensuring(        client.policies          .remove({ params: { policyId: policy.id }, payload: { owner: "org" } })          .pipe(Effect.ignore),      ),    );  }),);