Rhys Sullivanharden local auth (#1012)19c26574 months agoHistory
255 lines · 10.0 KB · TypeScript
RawDownload
// Desktop-only: the local bearer-auth model through the REAL Electron app +// sidecar, covering the two flows a code review found the e2e suite missed:////   A. MCP BROWSER APPROVAL (guards the resume-page bearer): an MCP client hits//      a gated tool against the desktop's sidecar; the user approves in the//      renderer; the agent's resume completes. This drives the whole desktop//      bearer path — the renderer carries NO client-side token, so every//      /api/mcp-sessions call (the session-scoped paused GET + the resume POST)//      relies on the main process injecting the bearer at the session layer.////   B. BEARER SCOPING (guards against the ambient-credential regression): a//      NON-app BrowserWindow (what an OAuth popup is) issuing a request to the//      sidecar's gated /api must NOT get the bearer auto-attached — it 401s —//      while the app's own window does. Pre-fix the injection covered every//      webContents in the session, so a popup could ride the bearer (CSRF).//// Both launch the real app via `_electron.launch` against a throwaway HOME and// read the sidecar's origin + bearer from the on-disk manifest.import { execFile } from "node:child_process";import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";import { createRequire } from "node:module";import { tmpdir } from "node:os";import { join } from "node:path";import { fileURLToPath } from "node:url";import { promisify } from "node:util"; import { expect } from "@effect/vitest";import { Effect } from "effect";import { _electron, type ElectronApplication } from "playwright";import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { scenario } from "../src/scenario";import { RunDir } from "../src/services"; const appDir = fileURLToPath(new URL("../../apps/desktop/", import.meta.url));const electronBinary = createRequire(join(appDir, "package.json"))("electron") as string; const APPROVAL_TARGET_TOOL = "executor.coreTools.policies.list";const EXECUTE_CODE = `const result = await tools.executor.coreTools.policies.list({});return JSON.stringify(result);`; interface SidecarConn {  readonly origin: string;  readonly token: string;} const launchDesktop = async (home: string, runDir: string): Promise<ElectronApplication> =>  _electron.launch({    executablePath: electronBinary,    args: [appDir],    cwd: appDir,    env: { ...process.env, HOME: home },    recordVideo: { dir: join(runDir, ".video-tmp"), size: { width: 1280, height: 800 } },    timeout: 120_000,  }); // The sidecar writes server.json (origin + bearer) once it emits EXECUTOR_READY,// which is exactly when firstWindow resolves — so this is always safe to read// after the window is up.const readSidecar = (home: string): SidecarConn => {  const manifest = JSON.parse(    readFileSync(join(home, ".executor/server-control/server.json"), "utf8"),  ) as { connection: { origin: string; auth?: { token?: string } } };  const origin = manifest.connection.origin;  const token = manifest.connection.auth?.token;  expect(typeof origin, "sidecar origin in the manifest").toBe("string");  expect(typeof token, "sidecar bearer token in the manifest").toBe("string");  return { origin, token: token! };}; const closeWithVideo = async (app: ElectronApplication, runDir: string, home: string) => {  const page = app.windows()[0];  const video = page?.video();  await app.close().catch(() => {});  const recordedPath = await video?.path().catch(() => undefined);  if (recordedPath && existsSync(recordedPath)) {    await promisify(execFile)("ffmpeg", [      "-y",      "-i",      recordedPath,      "-c:v",      "libx264",      "-preset",      "veryfast",      "-crf",      "26",      "-pix_fmt",      "yuv420p",      "-movflags",      "+faststart",      join(runDir, "session.mp4"),    ]).catch(() => {});  }  rmSync(join(runDir, ".video-tmp"), { recursive: true, force: true });  rmSync(home, { recursive: true, force: true });}; // ---------------------------------------------------------------------------// A. MCP browser approval through the desktop app// --------------------------------------------------------------------------- scenario(  "Desktop · MCP browser approval: gated tool, approve in the app, resume completes",  { timeout: 300_000 },  Effect.gen(function* () {    const runDir = yield* RunDir;    yield* Effect.promise(() => runApproval(runDir));  }),); const runApproval = async (runDir: string) => {  const home = mkdtempSync(join(tmpdir(), "executor-desktop-mcp-"));  const app = await launchDesktop(home, runDir);  let stepIndex = 0;  try {    const page = await app.firstWindow({ timeout: 120_000 });    const step = async (label: string, body: () => Promise<void>) => {      await body();      stepIndex += 1;      await page.screenshot({        path: join(          runDir,          `${String(stepIndex).padStart(2, "0")}-${label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.png`,        ),      });    };     await step("app boots into the web console", async () => {      await page.getByText("Settings").first().waitFor({ timeout: 120_000 });    });     const { origin, token } = readSidecar(home);     // Plant a require_approval policy so the gated tool elicits (bearer-authed —    // the test process is not the app, so it carries the token explicitly).    const policyRes = await fetch(`${origin}/api/policies`, {      method: "POST",      headers: { "content-type": "application/json", authorization: `Bearer ${token}` },      body: JSON.stringify({        owner: "org",        pattern: APPROVAL_TARGET_TOOL,        action: "require_approval",      }),    });    expect(policyRes.ok, `policy create ok (${policyRes.status})`).toBe(true);     const mcp = new Client({ name: "e2e-desktop-approve", version: "1.0.0" }, { capabilities: {} });    const transport = new StreamableHTTPClientTransport(      new URL(`${origin}/mcp?elicitation_mode=browser`),      { requestInit: { headers: { authorization: `Bearer ${token}` } } },    );    await mcp.connect(transport);     // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: the test owns the MCP transport lifecycle    try {      const executed = await mcp.callTool({ name: "execute", arguments: { code: EXECUTE_CODE } });      const paused = executed.structuredContent as {        status: string;        executionId: string;        approvalUrl: string;      };      expect(paused.status, "execute paused for browser approval").toBe("user_approval_required");       await step("approve the gated tool in the desktop renderer", async () => {        // The renderer carries no token; the main process injects the bearer for        // THIS window. No ?_token (bootstrap no-ops on desktop).        await page.goto(paused.approvalUrl, { waitUntil: "domcontentloaded" });        await page.getByRole("button", { name: "Approve" }).waitFor({ timeout: 30_000 });        expect(          await page.getByText("This paused execution is no longer available").count(),          "approval page loaded the paused execution (session-injected bearer reached the gated GET)",        ).toBe(0);        await page.getByRole("button", { name: "Approve" }).click();        await page.getByText("Approve sent").waitFor({ timeout: 15_000 });      });       const resumed = await mcp.callTool({        name: "resume",        arguments: { executionId: paused.executionId },      });      expect(        (resumed.structuredContent as { status: string }).status,        "resume completed after the in-app approval",      ).toBe("completed");    } finally {      await mcp.close();    }  } finally {    await closeWithVideo(app, runDir, home);  }}; // ---------------------------------------------------------------------------// B. Bearer scoping: a non-app webContents does NOT get the bearer// --------------------------------------------------------------------------- scenario(  "Desktop · the bearer is scoped to the app window — a popup webContents gets 401",  { timeout: 300_000 },  Effect.gen(function* () {    const runDir = yield* RunDir;    yield* Effect.promise(() => runBearerScoping(runDir));  }),); const runBearerScoping = async (runDir: string) => {  const home = mkdtempSync(join(tmpdir(), "executor-desktop-scope-"));  const app = await launchDesktop(home, runDir);  try {    const page = await app.firstWindow({ timeout: 120_000 });    await page.getByText("Settings").first().waitFor({ timeout: 120_000 });     const { origin } = readSidecar(home);    const gatedUrl = `${origin}/api/scope`;     // The app's OWN window: the main process injects the bearer → not 401.    const mainStatus = await page.evaluate(      (url) =>        fetch(url)          .then((r) => r.status)          .catch(() => -1),      gatedUrl,    );    expect(mainStatus, "app window: injected bearer reaches the gated endpoint").not.toBe(401);     // A non-app BrowserWindow (what an OAuth popup is) in the SAME session: its    // requests to the sidecar must NOT get the bearer auto-attached → 401.    const popupStatus = await app.evaluate(      async ({ BrowserWindow }, { origin: o, gatedUrl: g }) => {        const popup = new BrowserWindow({          show: false,          webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },        });        // Load the SPA (served unauthenticated) so the fetch has the right origin.        await popup.loadURL(`${o}/`);        const status = await popup.webContents.executeJavaScript(          `fetch(${JSON.stringify(g)}).then((r) => r.status).catch(() => -1)`,        );        popup.destroy();        return status as number;      },      { origin, gatedUrl },    );    expect(popupStatus, "popup webContents: no bearer injected → gated endpoint rejects it").toBe(      401,    );     await page.screenshot({ path: join(runDir, "01-bearer-scoping.png") });  } finally {    await closeWithVideo(app, runDir, home);  }};