diff --git a/surfaces/gui/e2e/artifacts.spec.ts b/surfaces/gui/e2e/artifacts.spec.ts new file mode 100644 index 00000000..0bcddcfe --- /dev/null +++ b/surfaces/gui/e2e/artifacts.spec.ts @@ -0,0 +1,38 @@ +// OPE-91: agent-authored HTML renders in the artifact viewer inside an AIRTIGHT sandbox. +// The app webview is privileged (Tauri IPC), so the report page must be null-origin +// (no parent access) and offline (no subresource exfiltration) — while inline scripts, +// the thing report interactivity needs, keep working. The fixture page actively probes +// all three properties and reports into #probe. +import { expect } from "@playwright/test"; +import { test } from "./fixtures"; + +async function openReport(page: import("@playwright/test").Page) { + await page.goto("/"); + await page.getByPlaceholder(/Ask the coworker/).fill("hello"); + await page.getByRole("button", { name: "Send" }).click(); + await page.locator(".artifact-row", { hasText: "security-review.html" }).click(); +} + +test("HTML artifact renders sandboxed: scripts run, parent and network stay sealed", async ({ + page, +}) => { + await openReport(page); + const frame = page.getByTestId("artifact-frame"); + await expect(frame).toBeVisible(); + // No allow-same-origin, ever: with srcDoc it would run the page same-origin with the + // privileged app webview. This assertion is the regression lock for that exact flag. + await expect(frame).toHaveAttribute("sandbox", "allow-scripts"); + + const probe = page.frameLocator('[data-testid="artifact-frame"]').locator("#probe"); + await expect(probe).toContainText("script ran in sandbox"); // interactivity works + await expect(probe).toContainText("parent blocked"); // null origin held + await expect(probe).toContainText("network blocked"); // CSP stopped the exfil img + await expect(page).not.toHaveTitle("ESCAPED"); +}); + +test("HTML artifact offers Open in browser as the unsandboxed escape hatch", async ({ + page, +}) => { + await openReport(page); + await expect(page.getByTestId("artifact-open-browser")).toBeVisible(); +}); diff --git a/surfaces/gui/e2e/fixtures.ts b/surfaces/gui/e2e/fixtures.ts index 0fd085d7..0675eaa7 100644 --- a/surfaces/gui/e2e/fixtures.ts +++ b/surfaces/gui/e2e/fixtures.ts @@ -863,6 +863,42 @@ export async function mockApi(page: import("@playwright/test").Page) { } return json({ roots }); } + // Artifacts (OPE-91): one HTML report whose content actively probes the sandbox — + // an inline script that renders proof-of-execution, a parent-window escape attempt, + // and an external subresource that must be CSP-blocked. + if (/\/v1\/sessions\/[^/]+\/artifacts\/read$/.test(p)) { + return json({ + ok: true, + path: "reports/security-review.html", + kind: "html", + content: [ + "
',
+ ].join("\n"),
+ });
+ }
+ if (/\/v1\/sessions\/[^/]+\/artifacts\/reveal$/.test(p)) return json({ ok: true });
+ if (/\/v1\/sessions\/[^/]+\/artifacts$/.test(p)) {
+ return json({
+ artifacts: [
+ {
+ path: "reports/security-review.html",
+ abs_path: "/Users/test/OpenWorker/launch-note/reports/security-review.html",
+ name: "security-review.html",
+ kind: "html",
+ size: 2048,
+ modified_at: Math.floor(Date.now() / 1000) - 60,
+ },
+ ],
+ });
+ }
if (/\/v1\/sessions\/[^/]+\/messages$/.test(p)) return json({ messages: [] });
if (/\/v1\/sessions\/[^/]+\/unattended$/.test(p)) {
const id = decodeURIComponent(p.split("/").slice(-2)[0]);
diff --git a/surfaces/gui/src/components/RightRail.tsx b/surfaces/gui/src/components/RightRail.tsx
index ac3ea669..ab6302bf 100644
--- a/surfaces/gui/src/components/RightRail.tsx
+++ b/surfaces/gui/src/components/RightRail.tsx
@@ -294,6 +294,22 @@ function RailSection({
);
}
+// OPE-91: agent-authored HTML is untrusted active content rendered inside the PRIVILEGED
+// app webview (Tauri IPC). The sandbox must therefore be airtight on two axes:
+// - no `allow-same-origin`: with srcDoc, that flag would run the page same-origin with
+// the app — scripts could reach the parent document and the IPC bridge.
+// - no network: a poisoned report exfiltrates at DISPLAY time via subresources
+// (