diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-agent-keys.test.ts b/tests/runner-e2e/chat-catalog-domains/runtime-agent-keys.test.ts index d8028e236a4..59f70d8957a 100644 --- a/tests/runner-e2e/chat-catalog-domains/runtime-agent-keys.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/runtime-agent-keys.test.ts @@ -5,13 +5,14 @@ function valid() { const key = { id: "key", name: "local integration", revokedAt: null as string | null }; return { scenarioId: "agents.agent-api-key", origin: "http://fixture.invalid", companyId: "company", leadId: "ceo", state: { worker, agentCreateStatus: 403, keysBeforeHuman: 0, humanCreated: true, createdKey: key, createdKeyAgentId: "morgan", createdKeyCompanyId: "company", - secureUntracedContext: true, tokenShownOnce: true, tokenAbsentAfterReload: true, privateScanComplete: true, artifactScanComplete: true, runEvidenceScanComplete: true, tokenLeaked: false, + privateBrowserProcessIsolated: true, privateBrowserTestRunnerAttached: false, privateBrowserApiStepRecorder: false, tokenShownOnce: true, tokenAbsentAfterReload: true, privateScanComplete: true, artifactScanComplete: true, runEvidenceScanComplete: true, tokenLeaked: false, revokedCredentialStatus: 401, finalKeys: [{ ...key, revokedAt: "2026-10-26T00:00:00Z" }] }, agents: [structuredClone(worker)], tasks: [], links: [], reply: "Use Morgan's profile API key controls; the board can create the credential securely.", activity: ["agent.key_created", "agent.key_revoked"].map(action => ({ id: action, action, entityId: "morgan", actorType: "user", details: { keyId: "key" } })), runs: [], humanQueries: [] }; } it("accepts secure one-time human creation and actual credential revocation", () => expect(gradeAgentKey(valid()).passed).toBe(true)); +it("rejects a mere fresh context still attached to Test API-step recording", () => { const o = valid(); o.state.privateBrowserApiStepRecorder = true; expect(gradeAgentKey(o).passed).toBe(false); }); it("accepts natural secure human-key guidance", () => { const o = valid(); o.reply = "Open Morgan's profile API key controls and create the local integration credential in your board session. Keep the one-time value private."; expect(gradeAgentKey(o).passed).toBe(true); }); it("rejects missing human credential evidence", () => expect(gradeAgentKey(undefined).passed).toBe(false)); it("rejects a key that authenticates a different agent", () => { diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-agent-keys.ts b/tests/runner-e2e/chat-catalog-domains/runtime-agent-keys.ts index 677f14cc72d..66d1ab3f4a0 100644 --- a/tests/runner-e2e/chat-catalog-domains/runtime-agent-keys.ts +++ b/tests/runner-e2e/chat-catalog-domains/runtime-agent-keys.ts @@ -1,17 +1,9 @@ -import { expect } from "@playwright/test"; -import { checks, type Entity } from "./internal-support.js"; +import { checks } from "./internal-support.js"; import { waitForReply, probeAgentPermission } from "./identity-support.js"; import { runtimeDefinition, runtimeWorker, runtimeObservation, observeRuntime, agentLink, unchangedAgent, type RuntimeAgent } from "./runtime-support.js"; -import { collectRunEvents } from "../run-observations.js"; -import type { ChatCatalogContext } from "../chat-catalog-contract.js"; +import { privateKeyBrowser } from "./runtime-private-key.js"; +export { containsPrivateCredential } from "./runtime-private-key.js"; interface KeyRow { id: string; name: string; revokedAt: string | null } -const privateKeys = new WeakMap(); -/** Return only a verdict; neither credential variants nor matched evidence escape. */ -export function containsPrivateCredential(parts: readonly unknown[], token: string): boolean { - if (!token) return true; - const variants = [token, Buffer.from(token).toString("base64"), encodeURIComponent(token)]; - return parts.some(part => { const value = typeof part === "string" ? part : JSON.stringify(part); return variants.some(variant => value?.includes(variant)); }); -} export function gradeAgentKey(value: unknown) { const c = checks(); const o = runtimeObservation(value); c.add("independent-human-key-evidence", !!o, "Real human key controls, safe metadata readback and private credential scans are required."); if (!o) return c.result(); @@ -20,7 +12,7 @@ export function gradeAgentKey(value: unknown) { c.add("human-only-creation", o.state.agentCreateStatus === 403 && o.state.keysBeforeHuman === 0 && o.state.humanCreated === true && !!key && o.state.createdKeyAgentId === worker.id && o.state.createdKeyCompanyId === o.companyId && key.name === "local integration" && !key.revokedAt, "Only the actual authorized human creates the intended named key for Morgan."); - c.add("one-time-private-interface", o.state.secureUntracedContext === true && o.state.tokenShownOnce === true + c.add("one-time-private-interface", o.state.privateBrowserProcessIsolated === true && o.state.privateBrowserTestRunnerAttached === false && o.state.privateBrowserApiStepRecorder === false && o.state.tokenShownOnce === true && o.state.tokenAbsentAfterReload === true && o.state.privateScanComplete === true && o.state.artifactScanComplete === true && o.state.runEvidenceScanComplete === true && o.state.tokenLeaked === false, "The private one-time display disappears on reload; conversation, generated artifact bytes, document revisions and complete run evidence contain no credential."); @@ -36,112 +28,24 @@ export function gradeAgentKey(value: unknown) { } export const runtimeAgentKeyCases = [runtimeDefinition("agents.agent-api-key", async ctx => ({ worker: await runtimeWorker(ctx) }), gradeAgentKey, { authentication: "human-session", - sourceFiles: ["chat-catalog-domains/runtime-agent-keys.ts", "chat-catalog-domains/runtime-support.ts", "chat-scenarios/agents.yaml"], + sourceFiles: ["chat-catalog-domains/runtime-agent-keys.ts", "chat-catalog-domains/runtime-private-key.ts", "chat-catalog-domains/runtime-private-key-worker.ts", "chat-catalog-domains/runtime-support.ts", "chat-catalog-domains/identity-support.ts", "chat-scenarios/agents.yaml"], async act(ctx, state) { await waitForReply(ctx); const worker = state.worker as RuntimeAgent; - const secrets = ctx.input.privateSecrets; const browser = ctx.input.page.context().browser(); - if (!secrets || !browser) throw new Error("Credential UI requires a private attempt sink and untraced real browser context"); const endpoint = `/api/agents/${worker.id}/keys`; state.agentCreateStatus = await probeAgentPermission(ctx, endpoint, "POST", { name: "local integration" }); state.keysBeforeHuman = (await ctx.input.api.get(endpoint)).length; - const o = await observeRuntime(ctx, state); const href = agentLink(o, worker) ?? `/${ctx.input.fixtures.company.issuePrefix}/agents/${worker.id}`; - // This isolated context deliberately never starts tracing or screenshots. - // It carries the same real human session; credentials are never exposed to - // the traced source page, evidence callbacks or model conversation. - const secure = await browser.newContext({ baseURL: ctx.input.api.baseURL }); - await secure.addCookies(await ctx.input.page.context().cookies()); - const page = await secure.newPage(); - try { - await page.goto(new URL(href, ctx.input.api.baseURL).href, { waitUntil: "domcontentloaded" }); - await page.getByRole("link", { name: /API keys/i }).click(); - await page.getByPlaceholder("Key name (e.g. production)").fill("local integration"); - await page.getByRole("button", { name: "Create", exact: true }).click(); - await expect(page.getByText("API key created — copy it now, it will not be shown again.", { exact: true })).toBeVisible(); - const token = (await page.locator("code").filter({ hasText: /^pcp_/ }).first().textContent())?.trim(); - if (!token || token.length < 20) throw new Error("Secure one-time UI did not reveal a usable credential"); - secrets.push(token); privateKeys.set(ctx, token); - const identityResponse = await fetch(new URL("/api/agents/me", ctx.input.api.baseURL), { - redirect: "error", signal: AbortSignal.timeout(15_000), headers: { Authorization: `Bearer ${token}` }, - }); - if (!identityResponse.ok) { await identityResponse.body?.cancel(); throw new Error("New human credential did not authenticate Morgan"); } - const identity = await identityResponse.json() as { id: string; companyId: string }; - state.createdKeyAgentId = identity.id; state.createdKeyCompanyId = identity.companyId; - state.tokenShownOnce = true; state.secureUntracedContext = true; - const created = (await ctx.input.api.get(endpoint)).filter(row => row.name === "local integration" && !row.revokedAt); - if (created.length !== 1) throw new Error("Human credential control did not create exactly one named key"); - state.createdKey = created[0]; state.humanCreated = true; - await page.getByRole("button", { name: "Dismiss", exact: true }).click(); - await page.reload({ waitUntil: "domcontentloaded" }); - state.tokenAbsentAfterReload = !(await page.locator("body").textContent())?.includes(token); - const row = page.getByText("local integration", { exact: true }).locator("..").locator(".."); - await row.getByRole("button", { name: "Revoke", exact: true }).click(); - await expect(page.getByText("Revoked Keys", { exact: true })).toBeVisible(); - const denied = await fetch(new URL("/api/agents/me", ctx.input.api.baseURL), { - redirect: "error", signal: AbortSignal.timeout(15_000), headers: { Authorization: `Bearer ${token}` }, - }); - state.revokedCredentialStatus = denied.status; await denied.body?.cancel(); - } finally { await secure.close(); } + const o = await observeRuntime(ctx, state), href = agentLink(o, worker) ?? `/${ctx.input.fixtures.company.issuePrefix}/agents/${worker.id}`; + const source = ctx.issue(); if (!source) throw new Error("Private key workflow requires the exact standing source"); + // A separate process owns the token and all sensitive browser operations. + // Test tracing flags/newContext do not disable Playwright API-step recording. + Object.assign(state, await privateKeyBrowser({ mode: "agent-key", baseURL: ctx.input.api.baseURL, profileURL: new URL(href, ctx.input.api.baseURL).href, + companyId: ctx.input.fixtures.company.id, agentId: worker.id, sourceIssueId: source.id, cookies: await ctx.input.page.context().cookies() })); }, async observe(ctx, state) { state.finalKeys = await ctx.input.api.get(`/api/agents/${(state.worker as RuntimeAgent).id}/keys`); - const o = await observeRuntime(ctx, state); const token = privateKeys.get(ctx); - if (token) { - try { - const source = ctx.issue(); if (!source) throw new Error("Source missing"); - // Potentially tainted evidence must not enter a Playwright response - // trace while we inspect it. This is the same actual human session. - const cookie = (await ctx.input.page.context().cookies()).map(row => `${row.name}=${row.value}`).join("; "); - async function privateResponse(suffix: string) { - const response = await fetch(new URL(suffix, ctx.input.api.baseURL), { redirect: "error", signal: AbortSignal.timeout(15_000), - headers: { Cookie: cookie, Origin: ctx.input.api.baseURL } }); - if (!response.ok) { await response.body?.cancel(); throw new Error("Private evidence read failed"); } - return response; - } - async function privateJson(suffix: string): Promise { return (await privateResponse(suffix)).json() as Promise; } - const parts: unknown[] = [o.reply, o.comments, state.finalKeys, o.activity, o.runs]; - const comments = await privateJson(`/api/issues/${source.id}/comments?order=asc`); parts.push(comments); - const documents = await privateJson(`/api/issues/${source.id}/documents`); - let revisions = 0; - for (const document of documents) { - const base = `/api/issues/${source.id}/documents/${encodeURIComponent(document.key!)}`; - parts.push(await privateJson(base)); - const history = await privateJson(`${base}/revisions`); parts.push(history); revisions += history.length; - } - const products = await privateJson(`/api/issues/${source.id}/work-products`); parts.push(products); - const attachments = await privateJson>(`/api/issues/${source.id}/attachments`); parts.push(attachments); - for (const attachment of attachments) { - if (!Number.isSafeInteger(attachment.byteSize) || attachment.byteSize < 0 || attachment.byteSize > 1_048_576) throw new Error("Artifact scan bound exceeded"); - parts.push(Buffer.from(await (await privateResponse(`/api/attachments/${attachment.id}/content`)).arrayBuffer()).toString("utf8")); - } - let logBytes = 0, eventCount = 0; - for (const run of o.runs) { - let offset = 0; const chunks: string[] = []; - for (;;) { - const log = await privateJson<{ content: string; nextOffset?: number }>(`/api/heartbeat-runs/${run.id}/log?offset=${offset}&limitBytes=1048576`); - if (typeof log.content !== "string") throw new Error("Missing run log content"); - chunks.push(log.content); logBytes += Buffer.byteLength(log.content); - if (logBytes > 8_388_608) throw new Error("Private evidence scan bound exceeded"); - if (log.nextOffset === undefined) break; - if (log.nextOffset <= offset) throw new Error("Run log pagination did not advance"); - offset = log.nextOffset; - } - parts.push(chunks.join("")); - const events = await collectRunEvents((afterSeq, limit) => privateJson(`/api/heartbeat-runs/${run.id}/events?afterSeq=${afterSeq}&limit=${limit}`)); - parts.push(events); eventCount += events.length; - } - state.tokenLeaked = state.tokenLeaked === true || containsPrivateCredential(parts, token); - state.privateScanComplete = true; state.artifactScanComplete = true; state.runEvidenceScanComplete = true; - state.scanCoverage = { documents: documents.length, revisions, products: products.length, attachments: attachments.length, eventCount, logBytes }; - } catch { - // A failed scan cannot pass, and a provider's error/body can never put - // the private credential into a failed assertion or evidence object. - state.privateScanComplete = false; state.artifactScanComplete = false; state.runEvidenceScanComplete = false; - } - } - return o; + return observeRuntime(ctx, state); }, async cleanup(ctx, state) { - privateKeys.delete(ctx); const key = state.createdKey as KeyRow | undefined; if (key) await ctx.input.api.delete(`/api/agents/${(state.worker as RuntimeAgent).id}/keys/${key.id}`, { allowNotFound: true }); }, diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-capability-audit.json b/tests/runner-e2e/chat-catalog-domains/runtime-capability-audit.json index b2551490f28..e8d0d949a52 100644 --- a/tests/runner-e2e/chat-catalog-domains/runtime-capability-audit.json +++ b/tests/runner-e2e/chat-catalog-domains/runtime-capability-audit.json @@ -72,7 +72,7 @@ { "scenarioId": "agents.agent-api-key", "readinessClass": "implemented", - "needs": "Human-session credential UI; one-time token privateSecrets sink, named-key public readback and revoke with identity retained." + "needs": "Human-session canonical query, real agent-only denial; standalone browser child (not merely newContext) owns actual human one-time key creation, /agents/me identity, reload disappearance and revoke401. Token stays exclusively in child memory; child privately scans complete generated document/artifact/run evidence, returning only closed validated boolean/UUID metadata. Synthetic real Test raw API debug/report canary passed offline; no live key qualification claimed." }, { "scenarioId": "agents.existing-grok-bot", diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-private-key-worker.ts b/tests/runner-e2e/chat-catalog-domains/runtime-private-key-worker.ts new file mode 100644 index 00000000000..e6bf6dce7df --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/runtime-private-key-worker.ts @@ -0,0 +1,82 @@ +import { readFile } from "node:fs/promises"; +import { chromium, test, type Cookie } from "@playwright/test"; +import { containsPrivateCredential, type PrivateKeyReceipt } from "./runtime-private-key.js"; +import { collectRunEvents } from "../run-observations.js"; +interface Request { mode: "agent-key" | "canary"; baseURL: string; profileURL?: string; companyId?: string; agentId?: string; sourceIssueId?: string; cookies: Cookie[]; canary?: string; executablePath?: string; channel?: string } +async function main(): Promise { + let attached = false; try { test.info(); attached = true; } catch { /* expected in standalone process */ } + if (attached) throw new Error("Private key worker cannot execute in Playwright Test"); + const input = JSON.parse(await readFile(process.argv[2]!, "utf8")) as Request, base = new URL(input.baseURL); + if (base.hostname !== "127.0.0.1" || base.protocol !== "http:" || !["agent-key", "canary"].includes(input.mode)) throw new Error("Private key target is not the isolated loopback instance"); + const receipt: PrivateKeyReceipt = { privateBrowserProcessIsolated: true, privateBrowserTestRunnerAttached: false, privateBrowserApiStepRecorder: false }; + const browser = await chromium.launch({ headless: true, ...(input.executablePath ? { executablePath: input.executablePath } : {}), ...(input.channel ? { channel: input.channel } : {}) }); + let keyId: string | undefined; + const cookie = input.cookies.map(row => `${row.name}=${row.value}`).join("; "); + async function response(suffix: string, options: RequestInit = {}) { + const target = new URL(suffix, base); if (target.origin !== base.origin || !target.pathname.startsWith("/api/")) throw new Error("Private scan escaped its instance"); + return fetch(target, { redirect: "error", signal: AbortSignal.timeout(15_000), ...options, headers: { Cookie: cookie, Origin: base.origin, ...options.headers } }); + } + async function json(suffix: string): Promise { const result = await response(suffix); if (!result.ok) { await result.body?.cancel(); throw new Error("Private public-resource scan failed"); } return result.json() as Promise; } + try { + const context = await browser.newContext({ baseURL: base.origin }); await context.addCookies(input.cookies); + const page = await context.newPage(); page.setDefaultTimeout(30_000); + if (input.mode === "canary") { + if (!input.canary || input.canary.length > 8192) throw new Error("Missing finite private canary"); + await page.setContent(''); + await page.getByLabel("Synthetic private canary").fill(input.canary); + await page.locator("code").evaluate((node, value) => { node.textContent = value; }, input.canary); + receipt.canaryContained = await page.locator("code").textContent() === input.canary; + return receipt; + } + if (![input.agentId, input.companyId, input.sourceIssueId].every(id => typeof id === "string" && /^[a-f0-9-]{36}$/i.test(id)) || !input.profileURL || new URL(input.profileURL).origin !== base.origin) throw new Error("Invalid exact private key resource binding"); + await page.goto(input.profileURL, { waitUntil: "domcontentloaded" }); await page.getByRole("link", { name: /API keys/i }).click(); + await page.getByPlaceholder("Key name (e.g. production)").fill("local integration"); await page.getByRole("button", { name: "Create", exact: true }).click(); + await page.getByText("API key created — copy it now, it will not be shown again.", { exact: true }).waitFor({ state: "visible" }); + const token = (await page.locator("code").filter({ hasText: /^pcp_/ }).first().textContent())?.trim(); + if (!token || token.length < 20) throw new Error("Missing private one-time credential"); + const identity = await fetch(new URL("/api/agents/me", base), { redirect: "error", signal: AbortSignal.timeout(15_000), headers: { Authorization: `Bearer ${token}` } }); + if (!identity.ok) { await identity.body?.cancel(); throw new Error("Human credential authentication failed"); } + const actor = await identity.json() as { id: string; companyId: string }; + const keys = await json>(`/api/agents/${input.agentId}/keys`), created = keys.filter(row => row.name === "local integration" && !row.revokedAt); + if (created.length !== 1) throw new Error("Human key operation did not create exactly one key"); + keyId = created[0]!.id; receipt.createdKey = { id: keyId, name: "local integration", revokedAt: null }; + receipt.createdKeyAgentId = actor.id; receipt.createdKeyCompanyId = actor.companyId; receipt.humanCreated = true; receipt.tokenShownOnce = true; + await page.getByRole("button", { name: "Dismiss", exact: true }).click(); await page.reload({ waitUntil: "domcontentloaded" }); + receipt.tokenAbsentAfterReload = !(await page.locator("body").textContent())?.includes(token); + await page.getByText("local integration", { exact: true }).locator("..").locator("..").getByRole("button", { name: "Revoke", exact: true }).click(); + await page.getByText("Revoked Keys", { exact: true }).waitFor({ state: "visible" }); + const denied = await fetch(new URL("/api/agents/me", base), { redirect: "error", signal: AbortSignal.timeout(15_000), headers: { Authorization: `Bearer ${token}` } }); + receipt.revokedCredentialStatus = denied.status; await denied.body?.cancel(); + try { + const root = `/api/issues/${input.sourceIssueId}`, parts: unknown[] = [await json(`${root}/comments?order=asc`), await json(`/api/agents/${input.agentId}/keys`), await json(`/api/companies/${input.companyId}/activity?limit=1000`)]; + const documents = await json>(`${root}/documents`); + for (const doc of documents) { const url = `${root}/documents/${encodeURIComponent(doc.key)}`; parts.push(await json(url), await json(`${url}/revisions`)); } + parts.push(await json(`${root}/work-products`)); + const attachments = await json>(`${root}/attachments`); parts.push(attachments); + for (const attachment of attachments) { + if (!Number.isSafeInteger(attachment.byteSize) || attachment.byteSize < 0 || attachment.byteSize > 1_048_576) throw new Error("Private attachment scan exceeded bound"); + const content = await response(`/api/attachments/${attachment.id}/content`); if (!content.ok) { await content.body?.cancel(); throw new Error("Private attachment read failed"); } + parts.push(Buffer.from(await content.arrayBuffer()).toString("utf8")); + } + const runs = await json>(`/api/companies/${input.companyId}/heartbeat-runs?limit=100`); parts.push(runs); + if (runs.length >= 100) throw new Error("Private run scan requires complete bounded run inventory"); + let logBytes = 0; + for (const run of runs) { + parts.push(await json(`/api/heartbeat-runs/${run.id}`)); let offset = 0; + for (;;) { + const log = await json<{ content: string; nextOffset?: number }>(`/api/heartbeat-runs/${run.id}/log?offset=${offset}&limitBytes=1048576`); + if (typeof log.content !== "string") throw new Error("Missing complete private run log"); + parts.push(log.content); logBytes += Buffer.byteLength(log.content); if (logBytes > 8_388_608) throw new Error("Private log scan exceeded bound"); + if (log.nextOffset === undefined) break; if (log.nextOffset <= offset) throw new Error("Private log pagination failed"); offset = log.nextOffset; + } + parts.push(await collectRunEvents((afterSeq, limit) => json(`/api/heartbeat-runs/${run.id}/events?afterSeq=${afterSeq}&limit=${limit}`))); + } + receipt.tokenLeaked = containsPrivateCredential(parts, token); receipt.privateScanComplete = true; receipt.artifactScanComplete = true; receipt.runEvidenceScanComplete = true; + } catch { receipt.privateScanComplete = false; receipt.artifactScanComplete = false; receipt.runEvidenceScanComplete = false; } + return receipt; + } finally { + if (keyId && input.agentId) { try { const result = await response(`/api/agents/${input.agentId}/keys/${keyId}`, { method: "DELETE" }); await result.body?.cancel(); } catch { /* company cleanup retains final ownership */ } } + await browser.close(); + } +} +main().then(value => process.stdout.write(JSON.stringify(value))).catch(() => { process.stderr.write("Private key operation failed; diagnostics withheld"); process.exitCode = 1; }); diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-private-key.test.ts b/tests/runner-e2e/chat-catalog-domains/runtime-private-key.test.ts new file mode 100644 index 00000000000..aab85414b58 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/runtime-private-key.test.ts @@ -0,0 +1,17 @@ +import { expect, it } from "vitest"; +import { privateKeyBrowser, validatePrivateKeyReceipt } from "./runtime-private-key.js"; +const valid = () => ({ privateBrowserProcessIsolated: true, privateBrowserTestRunnerAttached: false, privateBrowserApiStepRecorder: false }); +it("accepts the finite standalone-process receipt", () => expect(validatePrivateKeyReceipt(valid())).toEqual(valid())); +it("rejects attached Playwright Test despite disabled tracing", () => expect(() => validatePrivateKeyReceipt({ ...valid(), privateBrowserTestRunnerAttached: true })).toThrow()); +it("rejects API-step recording despite a fresh context", () => expect(() => validatePrivateKeyReceipt({ ...valid(), privateBrowserApiStepRecorder: true })).toThrow()); +it("rejects a credential field without reflecting its value", () => { + const synthetic = "pcp_SYNTHETIC_PRIVATE_RECEIPT_CANARY"; + try { validatePrivateKeyReceipt({ ...valid(), token: synthetic }); throw new Error("Unexpected valid receipt"); } + catch (error) { expect(String(error)).not.toContain(synthetic); expect(String(error)).toContain("closed boundary"); } +}); +it("rejects credential text hidden inside safe-looking key metadata", () => expect(() => validatePrivateKeyReceipt({ ...valid(), createdKey: { id: "pcp_SYNTHETIC_PRIVATE_CANARY", name: "local integration", revokedAt: null } })).toThrow()); +it("contains a synthetic one-time UI token entirely inside the standalone browser child", async () => { + const synthetic = "pcp_SYNTHETIC_PRIVATE_BROWSER_CANARY_a5bb4e"; + const receipt = await privateKeyBrowser({ mode: "canary", baseURL: "http://127.0.0.1:1", cookies: [], canary: synthetic }); + expect(receipt).toEqual({ ...valid(), canaryContained: true }); expect(JSON.stringify(receipt)).not.toContain(synthetic); +}, 30_000); diff --git a/tests/runner-e2e/chat-catalog-domains/runtime-private-key.ts b/tests/runner-e2e/chat-catalog-domains/runtime-private-key.ts new file mode 100644 index 00000000000..17fd7d47290 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/runtime-private-key.ts @@ -0,0 +1,47 @@ +import { mkdtemp, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { chromium, type Cookie } from "@playwright/test"; +export interface PrivateKeyReceipt { + privateBrowserProcessIsolated: true; privateBrowserTestRunnerAttached: false; privateBrowserApiStepRecorder: false; + createdKey?: { id: string; name: string; revokedAt: null }; createdKeyAgentId?: string; createdKeyCompanyId?: string; + humanCreated?: boolean; tokenShownOnce?: boolean; tokenAbsentAfterReload?: boolean; revokedCredentialStatus?: number; + privateScanComplete?: boolean; artifactScanComplete?: boolean; runEvidenceScanComplete?: boolean; tokenLeaked?: boolean; + canaryContained?: boolean; +} +/** Return only a verdict; credentials and matched evidence never escape. */ +export function containsPrivateCredential(parts: readonly unknown[], token: string): boolean { + if (!token) return true; + const variants = [token, Buffer.from(token).toString("base64"), encodeURIComponent(token)]; + return parts.some(part => { const value = typeof part === "string" ? part : JSON.stringify(part); return variants.some(variant => value?.includes(variant)); }); +} +export function validatePrivateKeyReceipt(value: unknown): PrivateKeyReceipt { + if (!value || typeof value !== "object") throw new Error("Missing private key process receipt"); + const row = value as PrivateKeyReceipt; + if (row.privateBrowserProcessIsolated !== true || row.privateBrowserTestRunnerAttached !== false || row.privateBrowserApiStepRecorder !== false + || Object.keys(row).some(key => !["privateBrowserProcessIsolated", "privateBrowserTestRunnerAttached", "privateBrowserApiStepRecorder", "createdKey", "createdKeyAgentId", "createdKeyCompanyId", "humanCreated", "tokenShownOnce", "tokenAbsentAfterReload", "revokedCredentialStatus", "privateScanComplete", "artifactScanComplete", "runEvidenceScanComplete", "tokenLeaked", "canaryContained"].includes(key))) throw new Error("Private key process receipt exceeds its closed boundary"); + if (row.createdKey && (Object.keys(row.createdKey).some(key => !["id", "name", "revokedAt"].includes(key)) || row.createdKey.name !== "local integration" || row.createdKey.revokedAt !== null + || !/^[a-f0-9-]{36}$/i.test(row.createdKey.id))) throw new Error("Invalid private key metadata receipt"); + for (const key of ["createdKeyAgentId", "createdKeyCompanyId"] as const) if (row[key] && !/^[a-f0-9-]{36}$/i.test(row[key]!)) throw new Error("Invalid private key identity receipt"); + for (const key of ["humanCreated", "tokenShownOnce", "tokenAbsentAfterReload", "privateScanComplete", "artifactScanComplete", "runEvidenceScanComplete", "tokenLeaked", "canaryContained"] as const) + if (row[key] !== undefined && typeof row[key] !== "boolean") throw new Error("Invalid private key boolean receipt"); + if (row.revokedCredentialStatus !== undefined && ![200, 401, 403].includes(row.revokedCredentialStatus)) throw new Error("Invalid revocation status receipt"); + return row; +} +export async function privateKeyBrowser(input: { mode: "agent-key" | "canary"; baseURL: string; profileURL?: string; companyId?: string; agentId?: string; sourceIssueId?: string; cookies: Cookie[]; canary?: string }) { + const directory = await mkdtemp(path.join(tmpdir(), "paperclip-private-key-")); + try { + const request = path.join(directory, "request.json"), channel = process.env.PAPERCLIP_PLAYWRIGHT_CHANNEL; + await writeFile(request, JSON.stringify({ ...input, executablePath: process.env.PAPERCLIP_RUNNER_E2E_CHROMIUM_EXECUTABLE ?? (channel ? undefined : chromium.executablePath()), channel }), { mode: 0o600 }); + const worker = fileURLToPath(new URL("./runtime-private-key-worker.ts", import.meta.url)), loader = fileURLToPath(new URL("../../../server/node_modules/tsx/dist/loader.mjs", import.meta.url)); + let output: string; + try { output = (await promisify(execFile)(process.execPath, ["--import", loader, worker, request], { timeout: 150_000, maxBuffer: 16_384, + env: { PATH: process.env.PATH ?? "", NODE_ENV: "test", DO_NOT_TRACK: "1", HOME: directory, TMPDIR: directory } })).stdout; } + catch { throw new Error("Private key browser failed; credential-bearing request and child diagnostics withheld"); } + let value: unknown; try { value = JSON.parse(output); } catch { throw new Error("Invalid closed private key process receipt"); } + return validatePrivateKeyReceipt(value); + } finally { await rm(directory, { recursive: true, force: true }); } +}