From 9950a5f6f6b5fed66d5619f88eb6fe6d28fc4a34 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 01:06:26 +0530 Subject: [PATCH 001/143] docs(progress): reconcile release candidate state --- docs/PROGRESS.md | 67 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index a07aa816..a6a3986b 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -13,17 +13,23 @@ pass is never presented as a browser or production pass. - **Branch:** `feat/learner-ux-v6.1` - **Current product checkpoint:** - `d6c4aec6c3f32b264aef8a90e1f9c8fa148b77f4`. At - `2026-07-20T19:17:12Z`, 28 hours, 42 minutes, and 48 seconds remained. The - newest independent commits are `b9dd722` for retry-safe notebook intake and - `d6c4aec` for source-bound recovery from terminal learner responses. The - exact staged restart snapshot passed App **55/55**, client API **32/32**, - Worker API **90/90**, and session core **29/29** tests; repository, web, and - Worker strict TypeScript; scoped Prettier; Git whitespace; and an eight-file - secret scan. Two independent review axes found no source blocker. These are - local source results only. No current CloakBrowser journey, exact image, - qualification receipt, deployment, production smoke, learner observation, - Devpost publication, submission receipt, or merge is inferred. + `134c22ff21afefb82c4e1bf0a51357254ca9a306`. The owner partitioned the + preserved integration delta into nine commits and fast-forwarded both + `feat/learner-ux-v6.1` and `main` to this candidate before qualification. The + lead switched back to the feature branch for small independent fix-forward + commits; the existing `main` ref is an unqualified candidate, not release + evidence. At `2026-07-20T19:33:40Z`, 28 hours, 26 minutes, and 20 seconds + remained. Current local source verification passes web Vitest **605/605**, + repository/web/Worker strict TypeScript, and the expanded Python matrix + **323/324**; the sole Python failure is denied localhost socket creation in + the managed sandbox. Root Vitest passes **619/646**; 24 failures are denied + process/socket/sandbox capabilities and three are stale scientific-evidence + expectations. The source-bound SBOM and scientific manifests still name + source `6f551367`, not this checkpoint, so qualification and deployment are + prohibited until the final source freezes and all evidence is regenerated. + No current CloakBrowser journey, exact image, qualification receipt, + deployment, production smoke, learner observation, Devpost publication, or + submission receipt is inferred. - **Finish-command starting checkpoint:** committed `HEAD` `24262ea6f40e988d7be054bdba6f531531b1679f` plus the explicitly preserved dirty slices listed by `git status`. At `2026-07-19T15:18:04Z`, the @@ -2806,3 +2812,42 @@ browser proof, submission, and the final merge remain serialized. 8. Merge the reviewed feature branch into `main` only after the release and submission state is truthfully recorded. No deployment, submission, or merge is claimed at this checkpoint. + +## Finish Commander reconciliation — 2026-07-20T19:33:40Z + +### Completed and frozen + +- `0509833 fix(api): bound stalled client requests` +- `51d989f fix(admission): fence retry lease releases` +- `b9dd722 fix(upload): reconcile retryable notebook intake` +- `d6c4aec fix(session): reconcile terminal investigation restart` +- The owner partitioned the remaining preserved integration delta into nine + commits ending at `134c22f`; web Vitest passes **77/77 files, 605/605 tests**, + repository/web/Worker TypeScript passes, and the expanded kernel, runner, + secret, smoke, and OCI-normalization Python matrix passes **323/324**. The + one Python failure is `test_kernel_service_exposes_health_and_only_the_fixed_public_run` + because this managed sandbox rejects an IPv4 socket with `EPERM`. + +### Open release-critical graph + +```text +truthful live-readiness gate + accessibility/publication hygiene + -> focused tests and small independent commits + -> complete deterministic suites, build, format, whitespace, secrets + -> clean final source freeze + -> one exact runner image and real aggregate containment sentinel + -> regenerate source/image-bound SBOM, VEX, registry, and negative controls + -> issue exact qualification and release-check receipts + -> verify contained Wrangler identity and deploy only that tuple + -> public identity/smoke plus full CloakBrowser desktop/mobile matrix + -> truthful learner evidence or NO_DATA, video, Devpost, and receipt + -> fast-forward the verified feature tip into main +``` + +Current blockers are explicit: `PARTIAL` generation-filesystem isolation is +still treated as live-ready in two web surfaces; generated scientific evidence +is stale; one untracked FUSE placeholder prevents a clean tree; no aggregate +cgroup-v2 enforcement receipt exists; and public browser, impact, video, and +submission evidence have not occurred. Audit-agent attribution and a residual +`Ctrl K` accessible-name defect are publication/accessibility repairs, not +release proof. No blocker is closed by source code alone. From 6d8d9c6f58ab44e1317118b484c70b710465b7f3 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 01:17:25 +0530 Subject: [PATCH 002/143] fix(learner): require enforced live isolation --- apps/web/e2e/judged-flow.spec.ts | 38 +++++++++++++++++-- apps/web/src/App.test.tsx | 32 +++++++++++++--- apps/web/src/App.tsx | 6 ++- apps/web/src/api.test.ts | 25 ++++++++++++ apps/web/src/api.ts | 2 +- .../src/features/judge/JudgeModeView.test.tsx | 33 ++++++++++++++-- apps/web/src/features/judge/JudgeModeView.tsx | 10 +++-- 7 files changed, 128 insertions(+), 18 deletions(-) diff --git a/apps/web/e2e/judged-flow.spec.ts b/apps/web/e2e/judged-flow.spec.ts index 114c8d2f..39fdc06b 100644 --- a/apps/web/e2e/judged-flow.spec.ts +++ b/apps/web/e2e/judged-flow.spec.ts @@ -1511,7 +1511,7 @@ test("Judge Mode distinguishes every authority path", async ({ page }) => { health.data.liveCodex === "configured" && health.data.liveKernel === "configured" && health.data.sandbox === "credential-and-privilege-boundary" && - health.data.generationFilesystemReadIsolation === "PARTIAL" && + health.data.generationFilesystemReadIsolation === "OS_ENFORCED" && health.data.release?.status === "bound"; await page.goto("/judge"); await expect(page).toHaveURL(/\/judge$/); @@ -2191,6 +2191,38 @@ test("configured reasoning cannot start without a qualified hosted runner", asyn ); }); +test("partial generation isolation cannot expose live notebook upload", async ({ + page, +}) => { + await page.route("**/api/health", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + ok: true, + data: { + platform: "cloudflare-workers", + sample: "available", + replay: "available", + liveGpt: "configured", + liveCodex: "configured", + liveKernel: "configured", + readiness: "ready", + sandbox: "credential-and-privilege-boundary", + generationFilesystemReadIsolation: "PARTIAL", + requestId: "e2e-health-partial-isolation", + }, + }), + }); + }); + + await openLiveSetup(page); + await expect( + page.getByText(/generation filesystem read isolation is partial/i), + ).toBeVisible(); + await expect(page.locator('input[type="file"]')).toHaveCount(0); +}); + test("unsupported notebooks are parsed without execution and cannot advance", async ({ page, }) => { @@ -2382,7 +2414,7 @@ test("a configured hosted runner completes an untouched leakage notebook", async liveCodex: "configured", liveKernel: "configured", sandbox: "credential-and-privilege-boundary", - generationFilesystemReadIsolation: "PARTIAL", + generationFilesystemReadIsolation: "OS_ENFORCED", release: { status: "bound" }, }); @@ -2621,7 +2653,7 @@ test("a configured hosted runner completes an untouched class-imbalance notebook liveCodex: "configured", liveKernel: "configured", sandbox: "credential-and-privilege-boundary", - generationFilesystemReadIsolation: "PARTIAL", + generationFilesystemReadIsolation: "OS_ENFORCED", release: { status: "bound" }, }); diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index b54ed4a1..467fde5c 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -418,6 +418,7 @@ function installApi( options: { liveGpt?: "configured" | "server-key-required"; runner?: "configured" | "local-runner-required"; + generationIsolation?: "PARTIAL" | "OS_ENFORCED"; rejectLiveBelief?: boolean; cancelOffline?: boolean; beliefTest?: typeof liveBeliefTest | typeof imbalanceBeliefTest; @@ -491,7 +492,9 @@ function installApi( options.runner === "configured" ? "credential-and-privilege-boundary" : "local-runner-required", - generationFilesystemReadIsolation: "PARTIAL", + generationFilesystemReadIsolation: + options.generationIsolation ?? + (options.runner === "configured" ? "OS_ENFORCED" : "PARTIAL"), ...(options.runner === "configured" ? { release: { @@ -1275,6 +1278,28 @@ describe("CounterLab judged flow", () => { expect(document.body).not.toHaveTextContent(/local runner required/i); }); + it("keeps live notebook upload disabled when generation read isolation is partial", async () => { + const user = userEvent.setup(); + installApi({ + liveGpt: "configured", + runner: "configured", + generationIsolation: "PARTIAL", + }); + render(); + + await openLiveSetup(user); + + expect( + await screen.findByText(/qualified hosted runner is needed/i), + ).toBeInTheDocument(); + expect(document.body).toHaveTextContent( + /generation filesystem read isolation is partial.*not accepted as live authority/i, + ); + expect( + screen.queryByLabelText(/attach a supported notebook/i), + ).not.toBeInTheDocument(); + }); + it("does not continue live setup until a supported notebook has been uploaded", async () => { const user = userEvent.setup(); const fetcher = installApi({ liveGpt: "configured", runner: "configured" }); @@ -2176,10 +2201,7 @@ describe("CounterLab judged flow", () => { }; window.localStorage.setItem("counterlab.sessionId", "session_ui"); window.localStorage.setItem("counterlab.claim", learnerClaim); - window.localStorage.setItem( - "counterlab.claimSessionId", - "session_ui", - ); + window.localStorage.setItem("counterlab.claimSessionId", "session_ui"); window.localStorage.setItem("counterlab.mode", "live"); window.history.replaceState({}, "", "/session/session_ui"); const fetcher = installApi({ diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 4b736c9d..822aa502 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -4107,7 +4107,7 @@ function LiveSetup({ health?.liveCodex === "configured" && health.liveKernel === "configured" && health.sandbox === "credential-and-privilege-boundary" && - health.generationFilesystemReadIsolation === "PARTIAL" && + health.generationFilesystemReadIsolation === "OS_ENFORCED" && health.release?.status === "bound"; return (
@@ -4168,7 +4168,9 @@ function LiveSetup({

{runnerConfigured ? "Plans, verification, and fixed-kernel results can complete in this hosted session." - : "You can review the notebook and make a prediction here. The checked experiment runs only on a separately protected device."} + : health?.generationFilesystemReadIsolation === "PARTIAL" + ? "Generation filesystem read isolation is partial and is not accepted as live authority. Use the verified sample or replay." + : "You can review the notebook and make a prediction here. The checked experiment runs only on a separately protected device."}

diff --git a/apps/web/src/api.test.ts b/apps/web/src/api.test.ts index ea2b3119..bbb2b29d 100644 --- a/apps/web/src/api.test.ts +++ b/apps/web/src/api.test.ts @@ -662,6 +662,31 @@ describe("CounterLabApiClient", () => { ); }); + it("accepts an explicitly OS-enforced generation isolation claim", async () => { + const fetcher = vi.fn(async () => + jsonResponse({ + ok: true, + data: { + platform: "cloudflare-workers", + sample: "available", + replay: "available", + liveGpt: "configured", + liveCodex: "configured", + liveKernel: "configured", + readiness: "ready", + sandbox: "credential-and-privilege-boundary", + generationFilesystemReadIsolation: "OS_ENFORCED", + requestId: "request_isolated", + }, + }), + ); + const client = new CounterLabApiClient({ fetch: fetcher }); + + await expect(client.getHealth()).resolves.toMatchObject({ + generationFilesystemReadIsolation: "OS_ENFORCED", + }); + }); + it("rejects health responses that claim configured means available", async () => { const fetcher = vi.fn(async () => jsonResponse({ diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index 076b411e..738fa3d3 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -137,7 +137,7 @@ export const CapabilityHealthSchema = z "credential-and-privilege-boundary", "local-runner-required", ]), - generationFilesystemReadIsolation: z.literal("PARTIAL"), + generationFilesystemReadIsolation: z.enum(["PARTIAL", "OS_ENFORCED"]), requestId: NonEmptyString, }) .strict(); diff --git a/apps/web/src/features/judge/JudgeModeView.test.tsx b/apps/web/src/features/judge/JudgeModeView.test.tsx index b9304ab4..0dda267b 100644 --- a/apps/web/src/features/judge/JudgeModeView.test.tsx +++ b/apps/web/src/features/judge/JudgeModeView.test.tsx @@ -39,11 +39,16 @@ const configuredHealth: CapabilityHealth = { requestId: "request_judge_1", }; +const isolatedHealth: CapabilityHealth = { + ...configuredHealth, + generationFilesystemReadIsolation: "OS_ENFORCED", +}; + describe("JudgeModeView", () => { it("distinguishes sample, live, and legacy replay authority", async () => { render( { ); }); + it("does not offer live authority for partial generation read isolation", () => { + render( + , + ); + + expect( + screen.queryByRole("link", { name: /run an unprimed live test/i }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("link", { name: /run live/i }), + ).not.toBeInTheDocument(); + expect(document.body).toHaveTextContent( + /generation filesystem read isolation is partial.*live authority remains unavailable/i, + ); + }); + it("labels the primed sample as a disclosed walkthrough", async () => { const user = userEvent.setup(); const onStartSample = vi.fn(); @@ -229,7 +256,7 @@ describe("JudgeModeView", () => { it("does not equate configured services with observed readiness", () => { render( { it("does not deep-probe or expose live entry before readiness is checked", () => { render( {liveReady ? ( From 5259a495948a68024702ba5239bf8057d1785f08 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 01:19:57 +0530 Subject: [PATCH 003/143] fix(a11y): simplify command trigger name --- .../components/studio/ProjectSidebar.test.tsx | 40 +++++++++++++++++++ .../src/components/studio/ProjectSidebar.tsx | 4 +- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/studio/ProjectSidebar.test.tsx diff --git a/apps/web/src/components/studio/ProjectSidebar.test.tsx b/apps/web/src/components/studio/ProjectSidebar.test.tsx new file mode 100644 index 00000000..9f82c1de --- /dev/null +++ b/apps/web/src/components/studio/ProjectSidebar.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { ProjectSidebar } from "./ProjectSidebar"; +import type { StudioContext } from "./types"; + +const context: StudioContext = { + mode: "instant", + stage: "question-path", + artifact: null, + session: null, + events: [], +}; + +describe("ProjectSidebar", () => { + it("keeps the keyboard shortcut visible without adding it to the command name", async () => { + const user = userEvent.setup(); + const onOpenCommands = vi.fn(); + + render( + , + ); + + const trigger = screen.getByRole("button", { name: "Open commands" }); + expect(trigger).toHaveTextContent("Ctrl K"); + expect(trigger).not.toHaveAccessibleName(/ctrl k/i); + + await user.click(trigger); + expect(onOpenCommands).toHaveBeenCalledOnce(); + }); +}); diff --git a/apps/web/src/components/studio/ProjectSidebar.tsx b/apps/web/src/components/studio/ProjectSidebar.tsx index 2af714cd..753ad5c7 100644 --- a/apps/web/src/components/studio/ProjectSidebar.tsx +++ b/apps/web/src/components/studio/ProjectSidebar.tsx @@ -111,9 +111,11 @@ export function ProjectSidebar({ ); From 22d48a527dfb4bc256416c5a6d861dc7dcad0c44 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 01:30:20 +0530 Subject: [PATCH 004/143] docs(progress): record browser design review --- docs/PROGRESS.md | 77 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index a6a3986b..9e8facbb 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -13,23 +13,23 @@ pass is never presented as a browser or production pass. - **Branch:** `feat/learner-ux-v6.1` - **Current product checkpoint:** - `134c22ff21afefb82c4e1bf0a51357254ca9a306`. The owner partitioned the + `5259a495948a68024702ba5239bf8057d1785f08`. The owner partitioned the preserved integration delta into nine commits and fast-forwarded both - `feat/learner-ux-v6.1` and `main` to this candidate before qualification. The - lead switched back to the feature branch for small independent fix-forward - commits; the existing `main` ref is an unqualified candidate, not release - evidence. At `2026-07-20T19:33:40Z`, 28 hours, 26 minutes, and 20 seconds - remained. Current local source verification passes web Vitest **605/605**, - repository/web/Worker strict TypeScript, and the expanded Python matrix - **323/324**; the sole Python failure is denied localhost socket creation in - the managed sandbox. Root Vitest passes **619/646**; 24 failures are denied - process/socket/sandbox capabilities and three are stale scientific-evidence - expectations. The source-bound SBOM and scientific manifests still name - source `6f551367`, not this checkpoint, so qualification and deployment are - prohibited until the final source freezes and all evidence is regenerated. - No current CloakBrowser journey, exact image, qualification receipt, - deployment, production smoke, learner observation, Devpost publication, or - submission receipt is inferred. + `feat/learner-ux-v6.1` and `main` to `134c22f` before qualification. The lead + switched back to the feature branch and added seven independent fix-forward + commits through `5259a49`; the existing `main` ref remains an unqualified + earlier candidate, not release evidence. At `2026-07-20T19:33:40Z`, 28 + hours, 26 minutes, and 20 seconds remained. Current local source verification + passes web Vitest **608/608**, repository/web/Worker/E2E strict TypeScript, + and the expanded Python product matrix **324/324** when the one localhost + socket test runs with the required socket permission. Elevated root Vitest + passes **643/646**; its remaining three failures are stale + scientific-evidence expectations. The source-bound SBOM and scientific + manifests still name source `6f551367`, not this checkpoint, so + qualification and deployment are prohibited until the final source freezes + and all evidence is regenerated. No current CloakBrowser journey, exact + image, qualification receipt, deployment, production smoke, learner + observation, Devpost publication, or submission receipt is inferred. - **Finish-command starting checkpoint:** committed `HEAD` `24262ea6f40e988d7be054bdba6f531531b1679f` plus the explicitly preserved dirty slices listed by `git status`. At `2026-07-19T15:18:04Z`, the @@ -2851,3 +2851,48 @@ cgroup-v2 enforcement receipt exists; and public browser, impact, video, and submission evidence have not occurred. Audit-agent attribution and a residual `Ctrl K` accessible-name defect are publication/accessibility repairs, not release proof. No blocker is closed by source code alone. + +## Finish Commander browser and authority checkpoint — 2026-07-20 + +### Newly frozen independent commits + +- `6d8d9c6 fix(learner): require enforced live isolation` makes Live visible + and selectable only when health reports `OS_ENFORCED` generation-filesystem + isolation. The current Worker truthfully reports `PARTIAL`, so its Live path + is intentionally unavailable instead of being advertised as production + ready. Focused App/API/Judge coverage passed **117/117** and the complete web + suite passed **608/608**. +- `5259a49 fix(a11y): simplify command trigger name` keeps the visible + `Ctrl K` shortcut while exposing the stable accessible name `Open commands`. + Its focused component test passed **1/1**. + +### Labelled design-review evidence + +The checked-in Playwright authority guard was used with the explicit +`stock-chromium-design-review` mode because this session has no +`CLOAK_CDP_ENDPOINT`. Desktop `1440x900` and mobile `390x844` Landing and Judge +first-fold checks passed **4/4** in **16.0 seconds**, including first-viewport +visibility, pre-Prediction value hiding, horizontal-overflow checks, and zero +console, page, request, or HTTP-response failures. Captures are retained under +`apps/web/test-results/runtime/design-review-20260720/evidence/belief-break/`. +They are design-review artifacts only and are not CloakBrowser or release +qualification evidence. + +### Current remaining gates + +1. Finish the deduplicated P0/P1 source review and any small independent + repairs, while keeping Live disabled unless a genuine OS-enforced boundary + is proved. +2. Resolve the untracked in-repository FUSE placeholder through the required + immediate deletion approval so the source can become clean. +3. Freeze one exact source, rebuild once, regenerate all source/image-bound + scientific and supply-chain evidence, and pass the real containment and + negative-control gates. +4. Deploy only a qualified tuple, then run the public sample/replay and any + genuinely ready Live journey through CloakBrowser. The labelled Chrome + captures above cannot close this gate. +5. Keep learner impact `NO_DATA` until consented observations exist; align the + exact app, repository, video, Devpost entry, feedback identifier, and + submission receipt before final publication. +6. Push the finished feature branch and fast-forward its verified tip into + `main` only after the preceding gates are truthfully recorded. From 2b94c9d0d0aa2b34c5a7bbf1263b30cb1001a27d Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 02:19:21 +0530 Subject: [PATCH 005/143] feat(runner): enforce Bubblewrap generation boundary --- Dockerfile.runner | 6 +- apps/web/worker/runner-container-env.test.ts | 2 + apps/web/worker/runner-container-env.ts | 2 + packages/codex-client/src/app-server.ts | 52 ++- packages/codex-client/src/index.test.ts | 21 +- .../hosted-runner/src/launch-boundary.test.ts | 285 ++++++++++- services/hosted-runner/src/launch-boundary.ts | 442 +++++++++++++++--- services/hosted-runner/src/server.test.ts | 2 + services/hosted-runner/src/server.ts | 11 + .../hosted-runner/src/startup-probe.test.ts | 39 +- services/hosted-runner/src/startup-probe.ts | 34 +- 11 files changed, 779 insertions(+), 117 deletions(-) diff --git a/Dockerfile.runner b/Dockerfile.runner index 9d97bbc1..d4faff0e 100644 --- a/Dockerfile.runner +++ b/Dockerfile.runner @@ -69,8 +69,12 @@ ENV DEBIAN_FRONTEND=noninteractive \ COPY --from=node-builder /usr/local/bin/node /usr/local/bin/node -RUN chmod 0555 /usr/local/bin/node \ +RUN apt-get update \ + && apt-get install --yes --no-install-recommends bubblewrap=0.11.0-2+deb13u1 \ + && chmod 0555 /usr/local/bin/node \ && test -x /usr/local/bin/node \ + && test "$(/usr/bin/bwrap --version)" = "bubblewrap 0.11.0" \ + && test -x /usr/bin/bwrap \ && test -x /usr/sbin/groupadd \ && test -x /usr/sbin/useradd \ && test -x /usr/bin/setpriv \ diff --git a/apps/web/worker/runner-container-env.test.ts b/apps/web/worker/runner-container-env.test.ts index eab5d9ff..278ea4b1 100644 --- a/apps/web/worker/runner-container-env.test.ts +++ b/apps/web/worker/runner-container-env.test.ts @@ -27,7 +27,9 @@ describe("runner Container environment", () => { PORT: "8080", COUNTERLAB_RUNNER_WORK_ROOT: "/tmp/counterlab-jobs", COUNTERLAB_CODEX_HOME_ROOT: "/tmp/counterlab-codex", + COUNTERLAB_CODEX_ROOT: "/opt/codex", COUNTERLAB_CODEX_EXECUTABLE: "/usr/local/bin/codex", + COUNTERLAB_BWRAP_EXECUTABLE: "/usr/bin/bwrap", COUNTERLAB_SETPRIV_EXECUTABLE: "/usr/bin/setpriv", COUNTERLAB_PYTHON_EXECUTABLE: "/opt/counterlab-venv/bin/python", COUNTERLAB_CODEX_UID: "10001", diff --git a/apps/web/worker/runner-container-env.ts b/apps/web/worker/runner-container-env.ts index a1d7bea8..5d4731fe 100644 --- a/apps/web/worker/runner-container-env.ts +++ b/apps/web/worker/runner-container-env.ts @@ -25,7 +25,9 @@ export function createRunnerContainerEnvVars( PORT: "8080", COUNTERLAB_RUNNER_WORK_ROOT: "/tmp/counterlab-jobs", COUNTERLAB_CODEX_HOME_ROOT: "/tmp/counterlab-codex", + COUNTERLAB_CODEX_ROOT: "/opt/codex", COUNTERLAB_CODEX_EXECUTABLE: "/usr/local/bin/codex", + COUNTERLAB_BWRAP_EXECUTABLE: "/usr/bin/bwrap", COUNTERLAB_SETPRIV_EXECUTABLE: "/usr/bin/setpriv", COUNTERLAB_PYTHON_EXECUTABLE: "/opt/counterlab-venv/bin/python", COUNTERLAB_CODEX_UID: "10001", diff --git a/packages/codex-client/src/app-server.ts b/packages/codex-client/src/app-server.ts index e523e7b0..d718684e 100644 --- a/packages/codex-client/src/app-server.ts +++ b/packages/codex-client/src/app-server.ts @@ -709,6 +709,7 @@ function parseInput(schema: z.ZodType, value: unknown): T { class AppServerConnection { private readonly process: ChildProcessWithoutNullStreams; + private readonly terminated: Promise; private readonly pending = new Map(); private readonly events = new AsyncEventQueue(); private stdoutBuffer = ""; @@ -737,6 +738,9 @@ class AppServerConnection { } catch (error) { throw asSetupError(error); } + this.terminated = new Promise((resolve) => { + this.process.once("close", () => resolve()); + }); this.process.stdout.setEncoding("utf8"); this.process.stderr.setEncoding("utf8"); @@ -794,20 +798,22 @@ class AppServerConnection { : undefined; } - close(): void { - if (this.closed) return; - this.closed = true; - for (const request of this.pending.values()) { - clearTimeout(request.timer); - request.reject( - new CompilerSetupError( - "CODEX_PROCESS_EXITED", - "Codex App Server connection closed.", - ), - ); + async close(): Promise { + if (!this.closed) { + this.closed = true; + for (const request of this.pending.values()) { + clearTimeout(request.timer); + request.reject( + new CompilerSetupError( + "CODEX_PROCESS_EXITED", + "Codex App Server connection closed.", + ), + ); + } + this.pending.clear(); + this.terminateProcess(); } - this.pending.clear(); - this.terminateProcess(); + await this.awaitTermination(); } cancel(): void { @@ -964,6 +970,24 @@ class AppServerConnection { timer.unref(); } + private async awaitTermination(): Promise { + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject( + new CompilerSetupError( + "CODEX_PROCESS_EXITED", + "Codex App Server did not terminate after forced shutdown.", + ), + ); + }, 5_000); + timer.unref(); + void this.terminated.then(() => { + clearTimeout(timer); + resolve(); + }); + }); + } + private killProcessTree(signal: NodeJS.Signals): void { const pid = this.process.pid; if (process.platform !== "win32" && pid !== undefined) { @@ -1470,7 +1494,7 @@ export class AppServerCodexCompiler implements CodexCompiler { throw asSetupError(error); } finally { signal?.removeEventListener("abort", cancel); - connection.close(); + await connection.close(); await launch.dispose?.(); } } diff --git a/packages/codex-client/src/index.test.ts b/packages/codex-client/src/index.test.ts index 56fec952..2d1f5e75 100644 --- a/packages/codex-client/src/index.test.ts +++ b/packages/codex-client/src/index.test.ts @@ -1102,6 +1102,7 @@ describe("AppServerCodexCompiler stdio transport", () => { const work = await mkdtemp(join(tmpdir(), "counterlab-codex-cancel-")); const pidFile = join(work, "app-server.pid"); let processId: number | undefined; + let disposedAfterExit = false; try { const compiler = new AppServerCodexCompiler({ command: process.execPath, @@ -1111,7 +1112,23 @@ describe("AppServerCodexCompiler stdio transport", () => { `--ignore-sigterm=${pidFile}`, ], timeoutMs: 5_000, - ...unisolatedTestProcess, + launchBoundary: { + async health() { + return { available: true as const }; + }, + async prepare(request) { + return { + command: request.command, + args: request.args, + environment: request.environment, + protocolCwd: request.hostCwd, + async dispose() { + expect(() => process.kill(processId!, 0)).toThrow(); + disposedAfterExit = true; + }, + }; + }, + }, }); const controller = new AbortController(); const compilation = collect( @@ -1125,8 +1142,8 @@ describe("AppServerCodexCompiler stdio transport", () => { name: "CompilerSetupError", code: "CODEX_CANCELLED", }); - await new Promise((resolve) => setTimeout(resolve, 350)); expect(() => process.kill(processId!, 0)).toThrow(); + expect(disposedAfterExit).toBe(true); } finally { if (processId !== undefined) { try { diff --git a/services/hosted-runner/src/launch-boundary.test.ts b/services/hosted-runner/src/launch-boundary.test.ts index 4de1c9f6..ee2dc215 100644 --- a/services/hosted-runner/src/launch-boundary.test.ts +++ b/services/hosted-runner/src/launch-boundary.test.ts @@ -1,12 +1,28 @@ -import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import { execFile } from "node:child_process"; +import { + mkdir, + mkdtemp, + readFile, + readdir, + rm, + stat, + symlink, + writeFile, +} from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; -import { ContainerCodexLaunchBoundary } from "./launch-boundary.js"; +import { + buildContainerBubblewrapLaunch, + buildContainerBubblewrapProbe, + ContainerCodexLaunchBoundary, +} from "./launch-boundary.js"; const roots: string[] = []; +const execFileAsync = promisify(execFile); afterEach(async () => { await Promise.all( @@ -25,24 +41,28 @@ describe("ContainerCodexLaunchBoundary", () => { const workspaceRoot = await root(); const workspace = join(workspaceRoot, "job_1"); const codexHomeRoot = await root(); + const codexRoot = await root(); + const codexExecutable = join(codexRoot, "bin", "codex"); + await mkdir(join(codexRoot, "bin"), { recursive: true }); + await writeFile(codexExecutable, "#!/bin/sh\n", { mode: 0o700 }); const boundary = new ContainerCodexLaunchBoundary({ authJson: JSON.stringify({ tokens: { access_token: "access-token-long-enough-for-test" }, }), workspaceRoot, codexHomeRoot, - codexExecutable: process.execPath, + codexRoot, + codexExecutable, + bwrapExecutable: "/usr/bin/bwrap", setprivExecutable: "/usr/bin/setpriv", uid: process.getuid?.() ?? 1000, gid: process.getgid?.() ?? 1000, }); - await import("node:fs/promises").then(({ mkdir }) => - mkdir(workspace, { mode: 0o700 }), - ); + await mkdir(workspace, { mode: 0o700 }); await expect(boundary.health()).resolves.toEqual({ available: true }); const prepared = await boundary.prepare({ - command: process.execPath, + command: codexExecutable, args: ["app-server", "--stdio"], environment: { PATH: process.env.PATH, @@ -60,45 +80,86 @@ describe("ContainerCodexLaunchBoundary", () => { } expect((await stat(workspace)).mode & 0o777).toBe(0o700); - expect(prepared.command).toBe("/usr/bin/setpriv"); + expect(prepared.command).toBe("/usr/bin/bwrap"); + expect(prepared.args).toContain("--unshare-user"); + expect(prepared.args).toContain("--unshare-pid"); + expect(prepared.args).toContain("--unshare-ipc"); + expect(prepared.args).toContain("--unshare-uts"); + expect(prepared.args).toContain("--clearenv"); + expect(prepared.args).toContain("--cap-drop"); expect(prepared.args).toContain("--no-new-privs"); - expect(prepared.args).not.toContain( - `--reuid=${process.getuid?.() ?? 1000}`, - ); - expect(prepared.args).not.toContain( - `--regid=${process.getgid?.() ?? 1000}`, - ); - expect(prepared.args).not.toContain("--clear-groups"); - expect(prepared.args).toContain(process.execPath); + expect(prepared.args).toContain("/opt/codex/bin/codex"); expect(prepared.environment).not.toHaveProperty("CODEX_AUTH_JSON"); expect(prepared.environment).not.toHaveProperty("OPENAI_API_KEY"); expect(prepared.environment).not.toHaveProperty("CODEX_ACCESS_TOKEN"); - expect(prepared.environment.CODEX_HOME).toMatch(/counterlab-codex-/u); - expect(prepared.environment.TMPDIR).toMatch(/counterlab-codex-.*\/tmp$/u); - expect(prepared.environment.TMPDIR).not.toBe(workspace); - expect( - (await stat(prepared.environment.TMPDIR ?? "missing")).mode & 0o777, - ).toBe(0o700); - const authPath = join(prepared.environment.CODEX_HOME ?? "", "auth.json"); + expect(prepared.environment.CODEX_HOME).toBe("/home/counterlab"); + expect(prepared.environment.TMPDIR).toBe("/tmp"); + const guestAuthIndex = prepared.args.indexOf("/home/counterlab/auth.json"); + const authPath = prepared.args[guestAuthIndex - 1] ?? "missing"; + expect(authPath).toMatch(/counterlab-codex-.*\.auth\.json$/u); expect(await readFile(authPath, "utf8")).toContain("access_token"); + const workspaceIndex = prepared.args.indexOf(workspace); + expect(prepared.args.slice(workspaceIndex - 1, workspaceIndex + 2)).toEqual( + ["--bind", workspace, "/workspace"], + ); expect(prepared.args).toContain("--strict-config"); expect(prepared.args).toContain("shell_environment_policy.inherit=none"); expect(prepared.args).toContain( 'shell_environment_policy.exclude=["CODEX_ACCESS_TOKEN","OPENAI_API_KEY"]', ); await prepared.revokeCredentials?.(); + await expect(readFile(authPath, "utf8")).resolves.toBe(""); + await prepared.revokeCredentials?.(); + await prepared.dispose?.(); await expect(readFile(authPath, "utf8")).rejects.toThrow(); + expect(await readdir(codexHomeRoot)).toEqual([]); await prepared.dispose?.(); }); + it("rejects a requested executable that differs from the configured Codex binary", async () => { + const workspaceRoot = await root(); + const workspace = join(workspaceRoot, "job_1"); + const codexRoot = await root(); + const codexExecutable = join(codexRoot, "codex"); + await mkdir(workspace); + await writeFile(codexExecutable, "#!/bin/sh\n", { mode: 0o700 }); + const boundary = new ContainerCodexLaunchBoundary({ + authJson: JSON.stringify({ + tokens: { access_token: "access-token-long-enough-for-test" }, + }), + workspaceRoot, + codexHomeRoot: await root(), + codexRoot, + codexExecutable, + bwrapExecutable: "/usr/bin/bwrap", + setprivExecutable: "/usr/bin/setpriv", + uid: process.getuid?.() ?? 1000, + gid: process.getgid?.() ?? 1000, + }); + + await expect( + boundary.prepare({ + command: process.execPath, + args: ["app-server", "--stdio"], + environment: { PATH: process.env.PATH }, + hostCwd: workspace, + }), + ).rejects.toMatchObject({ code: "CODEX_ISOLATION_UNAVAILABLE" }); + }); + it("rejects a generation directory outside the configured workspace root", async () => { + const codexRoot = await root(); + const codexExecutable = join(codexRoot, "codex"); + await writeFile(codexExecutable, "#!/bin/sh\n", { mode: 0o700 }); const boundary = new ContainerCodexLaunchBoundary({ authJson: JSON.stringify({ tokens: { access_token: "access-token-long-enough-for-test" }, }), workspaceRoot: await root(), codexHomeRoot: await root(), - codexExecutable: process.execPath, + codexRoot, + codexExecutable, + bwrapExecutable: "/usr/bin/bwrap", setprivExecutable: "/usr/bin/setpriv", uid: process.getuid?.() ?? 1000, gid: process.getgid?.() ?? 1000, @@ -113,4 +174,180 @@ describe("ContainerCodexLaunchBoundary", () => { }), ).rejects.toMatchObject({ code: "CODEX_ISOLATION_UNAVAILABLE" }); }); + + it("rejects a symlinked generation directory", async () => { + const workspaceRoot = await root(); + const workspace = join(workspaceRoot, "job_real"); + const linkedWorkspace = join(workspaceRoot, "job_link"); + const codexRoot = await root(); + const codexExecutable = join(codexRoot, "codex"); + await mkdir(workspace); + await symlink(workspace, linkedWorkspace); + await writeFile(codexExecutable, "#!/bin/sh\n", { mode: 0o700 }); + const boundary = new ContainerCodexLaunchBoundary({ + authJson: JSON.stringify({ + tokens: { access_token: "access-token-long-enough-for-test" }, + }), + workspaceRoot, + codexHomeRoot: await root(), + codexRoot, + codexExecutable, + bwrapExecutable: "/usr/bin/bwrap", + setprivExecutable: "/usr/bin/setpriv", + uid: process.getuid?.() ?? 1000, + gid: process.getgid?.() ?? 1000, + }); + + await expect( + boundary.prepare({ + command: codexExecutable, + args: ["app-server", "--stdio"], + environment: { PATH: process.env.PATH }, + hostCwd: linkedWorkspace, + }), + ).rejects.toMatchObject({ code: "CODEX_ISOLATION_UNAVAILABLE" }); + }); + + it("rejects overlapping writable roots and executables outside the mounted system root", async () => { + const workspaceRoot = await root(); + const codexHomeRoot = join(workspaceRoot, "codex-home"); + const codexRoot = await root(); + const codexExecutable = join(codexRoot, "codex"); + await mkdir(codexHomeRoot); + await writeFile(codexExecutable, "#!/bin/sh\n", { mode: 0o700 }); + const common = { + authJson: JSON.stringify({ + tokens: { access_token: "access-token-long-enough-for-test" }, + }), + workspaceRoot, + codexHomeRoot, + codexRoot, + codexExecutable, + bwrapExecutable: "/usr/bin/bwrap", + uid: process.getuid?.() ?? 1000, + gid: process.getgid?.() ?? 1000, + }; + + await expect( + new ContainerCodexLaunchBoundary({ + ...common, + setprivExecutable: "/usr/bin/setpriv", + }).health(), + ).resolves.toMatchObject({ available: false }); + await expect( + new ContainerCodexLaunchBoundary({ + ...common, + codexHomeRoot: await root(), + setprivExecutable: codexExecutable, + }).health(), + ).resolves.toMatchObject({ available: false }); + }); + + it("builds a fixed mount allowlist with no inherited secret environment", () => { + const plan = buildContainerBubblewrapLaunch({ + appServerArgs: ["app-server", "--stdio"], + bwrapExecutable: "/usr/bin/bwrap", + codexExecutable: "/opt/counterlab-codex/bin/codex", + codexRoot: "/opt/counterlab-codex", + setprivExecutable: "/usr/bin/setpriv", + stagedAuthFile: "/run/counterlab-codex/job_1.auth.json", + workspace: "/work/jobs/job_1", + }); + + expect(plan.mountedHostPaths).toEqual([ + "/usr", + "/etc/ca-certificates", + "/etc/hosts", + "/etc/nsswitch.conf", + "/etc/resolv.conf", + "/etc/ssl", + "/opt/counterlab-codex", + "/run/counterlab-codex/job_1.auth.json", + "/work/jobs/job_1", + ]); + expect(plan.mountedHostPaths).not.toContain("/app"); + expect(plan.mountedHostPaths).not.toContain("/repo"); + expect(plan.mountedHostPaths).not.toContain("/opt/counterlab-venv"); + expect(plan.environment).toEqual({ + CODEX_HOME: "/home/counterlab", + HOME: "/home/counterlab", + LANG: "C.UTF-8", + PATH: "/usr/bin", + TMPDIR: "/tmp", + }); + expect(plan.environment).not.toHaveProperty("CODEX_AUTH_JSON"); + expect(plan.environment).not.toHaveProperty("CODEX_ACCESS_TOKEN"); + expect(plan.environment).not.toHaveProperty("OPENAI_API_KEY"); + }); + + it.runIf(process.env.COUNTERLAB_REAL_BWRAP_PROBE === "1")( + "proves the real Bubblewrap boundary hides host roots and permits bounded workspace writes", + async () => { + const workspace = await root(); + const codexRoot = await root(); + await mkdir(join(codexRoot, "bin"), { recursive: true }); + await writeFile(join(codexRoot, "bin", "codex"), "#!/bin/sh\n", { + mode: 0o700, + }); + await writeFile(join(workspace, "approved.txt"), "approved\n", { + mode: 0o600, + }); + const plan = buildContainerBubblewrapProbe({ + bwrapExecutable: "/usr/bin/bwrap", + codexRoot, + workspace, + }); + + const { stdout } = await execFileAsync(plan.command, plan.args, { + env: plan.environment, + timeout: 30_000, + }); + expect(JSON.parse(stdout)).toEqual({ + forbiddenHostPathsHidden: true, + parentEnvironmentHidden: true, + workspaceVisible: true, + workspaceWritable: true, + }); + }, + ); + + it.runIf(process.env.COUNTERLAB_REAL_BWRAP_PROBE === "1")( + "proves the real Bubblewrap launch invokes setpriv and the mounted Codex binary", + async () => { + const workspace = await root(); + const codexRoot = await root(); + const authRoot = await root(); + const stagedAuthFile = join(authRoot, "staged-auth.json"); + await mkdir(join(codexRoot, "bin"), { recursive: true }); + await writeFile( + join(codexRoot, "bin", "codex"), + "#!/bin/sh\nset -eu\ntest -s /home/counterlab/auth.json\nprintf 'generated\\n' > /workspace/generated.txt\nprintf '{\"authVisibleToAppServer\":true,\"workspaceWritable\":true}\\n'\n", + { mode: 0o700 }, + ); + await writeFile(stagedAuthFile, '{"tokens":{"access_token":"test"}}', { + mode: 0o600, + }); + const plan = buildContainerBubblewrapLaunch({ + appServerArgs: ["app-server", "--stdio"], + bwrapExecutable: "/usr/bin/bwrap", + codexExecutable: join(codexRoot, "bin", "codex"), + codexRoot, + setprivExecutable: "/usr/bin/setpriv", + stagedAuthFile, + workspace, + }); + + const { stdout } = await execFileAsync(plan.command, plan.args, { + env: plan.environment, + timeout: 30_000, + }); + expect(JSON.parse(stdout)).toEqual({ + authVisibleToAppServer: true, + workspaceWritable: true, + }); + expect(await readFile(join(workspace, "generated.txt"), "utf8")).toBe( + "generated\n", + ); + }, + ); }); diff --git a/services/hosted-runner/src/launch-boundary.ts b/services/hosted-runner/src/launch-boundary.ts index 6cf0cf49..cadc7952 100644 --- a/services/hosted-runner/src/launch-boundary.ts +++ b/services/hosted-runner/src/launch-boundary.ts @@ -1,13 +1,14 @@ import { constants } from "node:fs"; +import { randomUUID } from "node:crypto"; import { access, chmod, - chown, - mkdir, - mkdtemp, + lstat, + open, + readFile, realpath, - rm, stat, + unlink, writeFile, } from "node:fs/promises"; import { isAbsolute, join, relative, resolve } from "node:path"; @@ -31,17 +32,57 @@ const CodexAuthSchema = z const CREDENTIAL_EXCLUSIONS = 'shell_environment_policy.exclude=["CODEX_ACCESS_TOKEN","OPENAI_API_KEY"]'; +const GUEST_CODEX_ROOT = "/opt/codex"; +const GUEST_CODEX_HOME = "/home/counterlab"; +const GUEST_WORKSPACE = "/workspace"; +const NETWORK_RUNTIME_MOUNTS = [ + "/etc/ca-certificates", + "/etc/hosts", + "/etc/nsswitch.conf", + "/etc/resolv.conf", + "/etc/ssl", +] as const; export type ContainerCodexLaunchBoundaryOptions = { authJson: string; workspaceRoot: string; codexHomeRoot: string; + codexRoot: string; codexExecutable: string; + bwrapExecutable: string; setprivExecutable: string; + ptraceScopePath?: string; uid: number; gid: number; }; +export type ContainerBubblewrapLaunchPlan = PreparedAppServerLaunch & { + mountedHostPaths: string[]; +}; + +type BuildContainerBubblewrapLaunchOptions = { + appServerArgs: string[]; + bwrapExecutable: string; + codexExecutable: string; + codexRoot: string; + setprivExecutable: string; + stagedAuthFile: string; + workspace: string; +}; + +export type BuildContainerBubblewrapProbeOptions = { + bwrapExecutable: string; + codexRoot: string; + workspace: string; +}; + +export type ContainerBubblewrapProbePlan = { + command: string; + args: string[]; + environment: NodeJS.ProcessEnv; + mountedHostPaths: string[]; +}; + function isolationError(message: string, cause?: unknown): CompilerSetupError { return new CompilerSetupError("CODEX_ISOLATION_UNAVAILABLE", message, { ...(cause === undefined ? {} : { cause }), @@ -57,17 +98,217 @@ function isContained(root: string, candidate: string): boolean { ); } -export class ContainerCodexLaunchBoundary implements AppServerLaunchBoundary { - private readonly parsedAuth: string; +function pathsOverlap(left: string, right: string): boolean { + return left === right || isContained(left, right) || isContained(right, left); +} - private get requiresPrivilegeDrop(): boolean { - return ( - process.getuid?.() !== this.options.uid || - process.getgid?.() !== this.options.gid +function requireAbsolutePath(label: string, value: string): void { + if (!isAbsolute(value) || value.includes("\0")) { + throw isolationError(`${label} must be an absolute path.`); + } +} + +function guestCodexExecutable(codexRoot: string, codexExecutable: string) { + const fromRoot = relative(codexRoot, codexExecutable); + if ( + fromRoot === "" || + fromRoot.startsWith("..") || + isAbsolute(fromRoot) || + fromRoot.includes("\0") + ) { + throw isolationError( + "The Codex executable must resolve beneath the mounted Codex root.", ); } + return `${GUEST_CODEX_ROOT}/${fromRoot}`; +} + +function buildContainerBubblewrapBase(options: { + bwrapExecutable: string; + codexRoot: string; + stagedAuthFile?: string; + workspace: string; +}): ContainerBubblewrapProbePlan { + for (const [label, value] of [ + ["Bubblewrap executable", options.bwrapExecutable], + ["Codex root", options.codexRoot], + ["Generation workspace", options.workspace], + ] as const) { + requireAbsolutePath(label, value); + } + if (options.stagedAuthFile !== undefined) { + requireAbsolutePath("Staged Codex auth file", options.stagedAuthFile); + } + const mountedHostPaths = [ + "/usr", + ...NETWORK_RUNTIME_MOUNTS, + options.codexRoot, + ...(options.stagedAuthFile === undefined ? [] : [options.stagedAuthFile]), + options.workspace, + ]; + const environment: NodeJS.ProcessEnv = { + HOME: GUEST_CODEX_HOME, + CODEX_HOME: GUEST_CODEX_HOME, + LANG: "C.UTF-8", + PATH: "/usr/bin", + TMPDIR: "/tmp", + }; + return { + command: options.bwrapExecutable, + args: [ + "--die-with-parent", + "--new-session", + "--unshare-user", + "--unshare-pid", + "--unshare-ipc", + "--unshare-uts", + "--unshare-cgroup-try", + "--cap-drop", + "ALL", + "--clearenv", + "--ro-bind", + "/usr", + "/usr", + "--symlink", + "usr/bin", + "/bin", + "--symlink", + "usr/lib", + "/lib", + "--symlink", + "usr/lib", + "/lib64", + "--dir", + "/etc", + ...NETWORK_RUNTIME_MOUNTS.flatMap((path) => ["--ro-bind", path, path]), + "--dev", + "/dev", + "--proc", + "/proc", + "--ro-bind", + "/dev/null", + "/proc/1/environ", + "--dir", + "/opt", + "--ro-bind", + options.codexRoot, + GUEST_CODEX_ROOT, + "--tmpfs", + "/tmp", + "--dir", + "/home", + "--tmpfs", + GUEST_CODEX_HOME, + ...(options.stagedAuthFile === undefined + ? [] + : [ + "--ro-bind", + options.stagedAuthFile, + `${GUEST_CODEX_HOME}/auth.json`, + ]), + "--dir", + GUEST_WORKSPACE, + "--bind", + options.workspace, + GUEST_WORKSPACE, + "--setenv", + "HOME", + GUEST_CODEX_HOME, + "--setenv", + "CODEX_HOME", + GUEST_CODEX_HOME, + "--setenv", + "LANG", + "C.UTF-8", + "--setenv", + "PATH", + "/usr/bin", + "--setenv", + "TMPDIR", + "/tmp", + "--chdir", + GUEST_WORKSPACE, + ], + environment, + mountedHostPaths, + }; +} + +export function buildContainerBubblewrapLaunch( + options: BuildContainerBubblewrapLaunchOptions, +): ContainerBubblewrapLaunchPlan { + for (const [label, value] of [ + ["Codex executable", options.codexExecutable], + ["setpriv executable", options.setprivExecutable], + ] as const) { + requireAbsolutePath(label, value); + } + if (options.appServerArgs.some((argument) => argument.includes("\0"))) { + throw isolationError("Codex App Server arguments contain an invalid byte."); + } + const codexCommand = guestCodexExecutable( + options.codexRoot, + options.codexExecutable, + ); + const base = buildContainerBubblewrapBase(options); + + return { + ...base, + args: [ + ...base.args, + options.setprivExecutable, + "--no-new-privs", + codexCommand, + ...options.appServerArgs, + "--strict-config", + "-c", + "shell_environment_policy.inherit=none", + "-c", + CREDENTIAL_EXCLUSIONS, + ], + protocolCwd: GUEST_WORKSPACE, + }; +} + +export function buildContainerBubblewrapProbe( + options: BuildContainerBubblewrapProbeOptions, +): ContainerBubblewrapProbePlan { + const base = buildContainerBubblewrapBase(options); + const probeScript = String.raw` +set -euo pipefail +test -x /opt/codex/bin/codex +test ! -s /proc/1/environ +test ! -e /app +test ! -e /repo +test ! -e /opt/counterlab-venv +test ! -e /work/jobs +test ! -e /run/counterlab-codex +test -f /workspace/approved.txt +/usr/bin/setpriv --no-new-privs /opt/codex/bin/codex --version >/dev/null +printf 'bounded output\n' > /workspace/probe-output.txt +test "$(cat /workspace/probe-output.txt)" = 'bounded output' +printf '{"forbiddenHostPathsHidden":true,"parentEnvironmentHidden":true,"workspaceVisible":true,"workspaceWritable":true}\n' +`; + return { + ...base, + args: [ + ...base.args, + "/usr/bin/bash", + "--noprofile", + "--norc", + "-c", + probeScript, + ], + }; +} + +export class ContainerCodexLaunchBoundary implements AppServerLaunchBoundary { + private readonly parsedAuth: string; + private readonly ptraceScopePath: string; constructor(private readonly options: ContainerCodexLaunchBoundaryOptions) { + this.ptraceScopePath = + options.ptraceScopePath ?? "/proc/sys/kernel/yama/ptrace_scope"; try { this.parsedAuth = JSON.stringify( CodexAuthSchema.parse(JSON.parse(options.authJson) as unknown), @@ -83,7 +324,9 @@ export class ContainerCodexLaunchBoundary implements AppServerLaunchBoundary { this.options.gid <= 0 || !isAbsolute(this.options.workspaceRoot) || !isAbsolute(this.options.codexHomeRoot) || + !isAbsolute(this.options.codexRoot) || !isAbsolute(this.options.codexExecutable) || + !isAbsolute(this.options.bwrapExecutable) || !isAbsolute(this.options.setprivExecutable) ) { return { @@ -92,18 +335,67 @@ export class ContainerCodexLaunchBoundary implements AppServerLaunchBoundary { }; } try { + if ( + process.getuid?.() !== this.options.uid || + process.getgid?.() !== this.options.gid + ) { + return { + available: false, + reason: + "Container Codex must start as the declared non-root process identity.", + }; + } + const [workspaceRoot, codexHomeRoot, codexRoot, codexExecutable] = + await Promise.all([ + realpath(this.options.workspaceRoot), + realpath(this.options.codexHomeRoot), + realpath(this.options.codexRoot), + realpath(this.options.codexExecutable), + ]); + const [bwrapExecutable, setprivExecutable, systemRoot] = + await Promise.all([ + realpath(this.options.bwrapExecutable), + realpath(this.options.setprivExecutable), + realpath("/usr"), + ]); + if ( + !isContained(codexRoot, codexExecutable) || + !isContained(systemRoot, bwrapExecutable) || + !isContained(systemRoot, setprivExecutable) || + pathsOverlap(workspaceRoot, codexHomeRoot) || + pathsOverlap(workspaceRoot, codexRoot) || + pathsOverlap(codexHomeRoot, codexRoot) + ) { + return { + available: false, + reason: "Container Codex mount roots are not independently bounded.", + }; + } + const ptraceScope = Number.parseInt( + (await readFile(this.ptraceScopePath, "utf8")).trim(), + 10, + ); + if (!Number.isInteger(ptraceScope) || ptraceScope < 1) { + return { + available: false, + reason: "Container Codex requires child-to-parent ptrace protection.", + }; + } await Promise.all([ access(this.options.codexExecutable, constants.X_OK), + access(this.options.bwrapExecutable, constants.X_OK), access(this.options.setprivExecutable, constants.X_OK), + access(this.options.codexRoot, constants.R_OK | constants.X_OK), access(this.options.workspaceRoot, constants.R_OK | constants.W_OK), access(this.options.codexHomeRoot, constants.R_OK | constants.W_OK), + ...NETWORK_RUNTIME_MOUNTS.map((path) => access(path, constants.R_OK)), ]); return { available: true }; } catch { return { available: false, reason: - "Container Codex executables or private directories are unavailable.", + "Container Codex isolation executables or private roots are unavailable.", }; } } @@ -115,8 +407,8 @@ export class ContainerCodexLaunchBoundary implements AppServerLaunchBoundary { if (!health.available) throw isolationError(health.reason); try { await Promise.all([ - chmod(this.options.workspaceRoot, 0o711), - chmod(this.options.codexHomeRoot, 0o711), + chmod(this.options.workspaceRoot, 0o700), + chmod(this.options.codexHomeRoot, 0o700), ]); const rootModes = await Promise.all([ stat(this.options.workspaceRoot), @@ -125,101 +417,103 @@ export class ContainerCodexLaunchBoundary implements AppServerLaunchBoundary { for (const metadata of rootModes) { const mode = metadata.mode & 0o777; const ownerIsPrivate = (mode & 0o700) === 0o700 && (mode & 0o066) === 0; - const alternateUidCanTraverse = - !this.requiresPrivilegeDrop || (mode & 0o011) === 0o011; - if (!ownerIsPrivate || !alternateUidCanTraverse) { + if (!ownerIsPrivate) { throw isolationError( - "Container Codex private roots did not retain the required traverse-only permissions.", + "Container Codex private roots did not retain private permissions.", ); } } } catch (error) { if (error instanceof CompilerSetupError) throw error; throw isolationError( - "Container Codex private roots cannot grant traverse-only access.", + "Container Codex private roots cannot enforce private access.", error, ); } const workspaceRoot = await realpath(resolve(this.options.workspaceRoot)); + const requestedWorkspaceMetadata = await lstat(resolve(request.hostCwd)); + if (requestedWorkspaceMetadata.isSymbolicLink()) { + throw isolationError("Generation directory must not be a symbolic link."); + } const workspace = await realpath(resolve(request.hostCwd)); if (!isContained(workspaceRoot, workspace)) { throw isolationError( "Generation directory is outside the hosted runner workspace.", ); } - const codexHome = await mkdtemp( - join(resolve(this.options.codexHomeRoot), "counterlab-codex-"), + if (!isAbsolute(request.command)) { + throw isolationError("The requested Codex executable must be absolute."); + } + const [requestedCommand, codexRoot, codexExecutable] = await Promise.all([ + realpath(request.command), + realpath(this.options.codexRoot), + realpath(this.options.codexExecutable), + ]); + if (requestedCommand !== codexExecutable) { + throw isolationError( + "The requested Codex executable does not match the configured executable.", + ); + } + const authPath = join( + resolve(this.options.codexHomeRoot), + `counterlab-codex-${randomUUID()}.auth.json`, ); - const authPath = join(codexHome, "auth.json"); - const runtimeTemp = join(codexHome, "tmp"); let disposed = false; let revoked = false; + let authHandle: Awaited> | undefined; try { - await chmod(codexHome, 0o700); - await mkdir(runtimeTemp, { mode: 0o700 }); - await chmod(runtimeTemp, 0o700); - await writeFile(authPath, this.parsedAuth, { - encoding: "utf8", - mode: 0o600, - flag: "wx", - }); - await chmod(authPath, 0o600); - if (this.requiresPrivilegeDrop) { - await Promise.all([ - chown(codexHome, this.options.uid, this.options.gid), - chown(runtimeTemp, this.options.uid, this.options.gid), - chown(authPath, this.options.uid, this.options.gid), - chown(workspace, this.options.uid, this.options.gid), - ]); - } + authHandle = await open(authPath, "wx", 0o600); + await authHandle.writeFile(this.parsedAuth, "utf8"); + await authHandle.close(); + authHandle = undefined; await chmod(workspace, 0o700); } catch (error) { - await rm(codexHome, { force: true, recursive: true }); + await authHandle?.close().catch(() => undefined); + await writeFile(authPath, "", { flag: "w" }).catch(() => undefined); + await unlink(authPath).catch(() => undefined); throw isolationError("Codex credential staging failed.", error); } - const environment: NodeJS.ProcessEnv = { - ...request.environment, - HOME: codexHome, - CODEX_HOME: codexHome, - TMPDIR: runtimeTemp, - }; - delete environment.CODEX_AUTH_JSON; - delete environment.OPENAI_API_KEY; - delete environment.CODEX_ACCESS_TOKEN; + const plan = buildContainerBubblewrapLaunch({ + appServerArgs: request.args, + bwrapExecutable: this.options.bwrapExecutable, + codexExecutable, + codexRoot, + setprivExecutable: this.options.setprivExecutable, + stagedAuthFile: authPath, + workspace, + }); return { - command: this.options.setprivExecutable, - args: [ - ...(this.requiresPrivilegeDrop - ? [ - `--reuid=${this.options.uid}`, - `--regid=${this.options.gid}`, - "--clear-groups", - ] - : []), - "--no-new-privs", - request.command, - ...request.args, - "--strict-config", - "-c", - "shell_environment_policy.inherit=none", - "-c", - CREDENTIAL_EXCLUSIONS, - ], - environment, - protocolCwd: workspace, - spawnCwd: workspace, + ...plan, async revokeCredentials() { if (revoked) return; - revoked = true; - await rm(authPath, { force: true }); + try { + await writeFile(authPath, "", { encoding: "utf8", flag: "w" }); + if ((await stat(authPath)).size !== 0) { + throw isolationError( + "Codex authentication remained readable after revocation.", + ); + } + revoked = true; + } catch (error) { + if (error instanceof CompilerSetupError) throw error; + throw isolationError("Codex credential revocation failed.", error); + } }, async dispose() { if (disposed) return; + if (!revoked) { + await writeFile(authPath, "", { encoding: "utf8", flag: "w" }); + if ((await stat(authPath)).size !== 0) { + throw isolationError( + "Codex authentication remained readable during disposal.", + ); + } + revoked = true; + } + await unlink(authPath); disposed = true; - await chmod(codexHome, 0o700).catch(() => undefined); - await rm(codexHome, { force: true, recursive: true }); }, }; } diff --git a/services/hosted-runner/src/server.test.ts b/services/hosted-runner/src/server.test.ts index 92101e45..1d6046cc 100644 --- a/services/hosted-runner/src/server.test.ts +++ b/services/hosted-runner/src/server.test.ts @@ -36,6 +36,7 @@ async function start( releaseIdentity?: HostedRunnerServerOptions["releaseIdentity"], ) { const server = createHostedRunnerServer({ + generationFilesystemReadIsolation: "OS_ENFORCED", authorizeToken, processJob, ...(onJobSettled === undefined ? {} : { onJobSettled }), @@ -117,6 +118,7 @@ describe("hosted runner HTTP service", () => { await expect(ready.json()).resolves.toEqual({ status: "ready", service: "counterlab-hosted-runner", + generationFilesystemReadIsolation: "OS_ENFORCED", runnerSourceCommit, runnerImageDigest, }); diff --git a/services/hosted-runner/src/server.ts b/services/hosted-runner/src/server.ts index e84e3ca1..aaa27cba 100644 --- a/services/hosted-runner/src/server.ts +++ b/services/hosted-runner/src/server.ts @@ -30,6 +30,7 @@ const DispatchSchema = z .strict(); export type HostedRunnerServerOptions = { + generationFilesystemReadIsolation: "OS_ENFORCED"; releaseIdentity?: { runnerSourceCommit: string; runnerImageDigest: string; @@ -104,6 +105,8 @@ export function createHostedRunnerServer(options: HostedRunnerServerOptions) { respond(response, 200, { status: "ready", service: "counterlab-hosted-runner", + generationFilesystemReadIsolation: + options.generationFilesystemReadIsolation, ...(options.releaseIdentity ?? {}), }); return; @@ -219,6 +222,7 @@ async function startProductionServer(): Promise { console.log(JSON.stringify(await runHostedRunnerStartupProbe())); return; } + const startupProbe = await runHostedRunnerStartupProbe(); const authJson = process.env.CODEX_AUTH_JSON; const runnerVerifyingPublicKey = process.env.COUNTERLAB_RUNNER_VERIFYING_PUBLIC_KEY; @@ -255,6 +259,9 @@ async function startProductionServer(): Promise { process.env.COUNTERLAB_CODEX_HOME_ROOT ?? "/run/counterlab-codex"; const codexExecutable = process.env.COUNTERLAB_CODEX_EXECUTABLE ?? "/usr/local/bin/codex"; + const codexRoot = process.env.COUNTERLAB_CODEX_ROOT ?? "/opt/codex"; + const bwrapExecutable = + process.env.COUNTERLAB_BWRAP_EXECUTABLE ?? "/usr/bin/bwrap"; const setprivExecutable = process.env.COUNTERLAB_SETPRIV_EXECUTABLE ?? "/usr/bin/setpriv"; const uid = Number(process.env.COUNTERLAB_CODEX_UID ?? "10001"); @@ -267,7 +274,9 @@ async function startProductionServer(): Promise { authJson, workspaceRoot: resolve(workspaceRoot), codexHomeRoot: resolve(codexHomeRoot), + codexRoot: resolve(codexRoot), codexExecutable: resolve(codexExecutable), + bwrapExecutable: resolve(bwrapExecutable), setprivExecutable: resolve(setprivExecutable), uid, gid, @@ -280,6 +289,8 @@ async function startProductionServer(): Promise { current?: ReturnType; } = {}; const server = createHostedRunnerServer({ + generationFilesystemReadIsolation: + startupProbe.generationFilesystemReadIsolation, ...(releaseIdentityMissing ? {} : { releaseIdentity: { runnerSourceCommit, runnerImageDigest } }), diff --git a/services/hosted-runner/src/startup-probe.test.ts b/services/hosted-runner/src/startup-probe.test.ts index 50ffc76a..b035039b 100644 --- a/services/hosted-runner/src/startup-probe.test.ts +++ b/services/hosted-runner/src/startup-probe.test.ts @@ -16,6 +16,7 @@ describe("hosted runner startup probe", () => { _options: { env: NodeJS.ProcessEnv; timeout: number }, ) => undefined, ); + const writeFile = vi.fn(async () => undefined); const stat = vi.fn(async (path: string) => ({ uid: 0, gid: 0, @@ -32,6 +33,8 @@ describe("hosted runner startup probe", () => { COUNTERLAB_RUNNER_WORK_ROOT: "/runtime/jobs", COUNTERLAB_CODEX_HOME_ROOT: "/runtime/codex", COUNTERLAB_CODEX_EXECUTABLE: "/runtime/codex-bin", + COUNTERLAB_CODEX_ROOT: "/runtime/codex-package", + COUNTERLAB_BWRAP_EXECUTABLE: "/runtime/bwrap", COUNTERLAB_SETPRIV_EXECUTABLE: "/runtime/setpriv", COUNTERLAB_PYTHON_EXECUTABLE: "/runtime/python", }, @@ -41,6 +44,8 @@ describe("hosted runner startup probe", () => { access: access as unknown as typeof import("node:fs/promises").access, mkdir: mkdir as unknown as typeof import("node:fs/promises").mkdir, stat: stat as unknown as typeof import("node:fs/promises").stat, + writeFile: + writeFile as unknown as typeof import("node:fs/promises").writeFile, getUid: () => 10001, getGid: () => 10001, execute, @@ -56,9 +61,12 @@ describe("hosted runner startup probe", () => { "immutable-paths", "codex", "python", + "bubblewrap", + "bubblewrap-read-isolation", "setpriv", "writable-roots", ], + generationFilesystemReadIsolation: "OS_ENFORCED", }); expect(stat.mock.calls.map(([path]) => path)).toEqual([ "/runtime/app", @@ -68,6 +76,8 @@ describe("hosted runner startup probe", () => { "/runtime/node", "/runtime/runner.mjs", "/runtime/codex-bin", + "/runtime/codex-package", + "/runtime/bwrap", "/runtime/setpriv", "/runtime/python", ]); @@ -79,11 +89,37 @@ describe("hosted runner startup probe", () => { recursive: true, mode: 0o700, }); - expect(execute).toHaveBeenCalledTimes(3); + expect(mkdir).toHaveBeenCalledWith("/runtime/jobs/.isolation-probe", { + recursive: true, + mode: 0o700, + }); + expect(writeFile).toHaveBeenCalledWith( + "/runtime/jobs/.isolation-probe/approved.txt", + "approved\n", + { encoding: "utf8", flag: "w", mode: 0o600 }, + ); + expect(execute).toHaveBeenCalledTimes(4); for (const [, , options] of execute.mock.calls) { expect(options.env).not.toHaveProperty("CODEX_AUTH_JSON"); expect(options.env).not.toHaveProperty("OPENAI_API_KEY"); } + const [probeExecutable, probeArgs, probeOptions] = + execute.mock.calls[3] ?? []; + expect(probeExecutable).toBe("/runtime/bwrap"); + expect(probeArgs).toContain("--unshare-user"); + expect(probeArgs).toContain("--unshare-pid"); + expect(probeArgs).toContain("--clearenv"); + expect(probeArgs?.join(" ")).toContain("/usr/bin/setpriv"); + expect(probeArgs?.join(" ")).toContain("/opt/codex/bin/codex"); + expect(probeArgs).toContain("/runtime/jobs/.isolation-probe"); + expect(probeArgs).not.toContain("/runtime/jobs"); + expect(probeOptions?.env).toEqual({ + CODEX_HOME: "/home/counterlab", + HOME: "/home/counterlab", + LANG: "C.UTF-8", + PATH: "/usr/bin", + TMPDIR: "/tmp", + }); }); it("fails closed when a runtime executable cannot be launched", async () => { @@ -92,6 +128,7 @@ describe("hosted runner startup probe", () => { bundlePath: "/app/runner.mjs", access: async () => undefined, mkdir: async () => undefined as never, + writeFile: async () => undefined, stat: (async ( path: Parameters[0], ) => ({ diff --git a/services/hosted-runner/src/startup-probe.ts b/services/hosted-runner/src/startup-probe.ts index f1b9e7bd..a9a3f96b 100644 --- a/services/hosted-runner/src/startup-probe.ts +++ b/services/hosted-runner/src/startup-probe.ts @@ -1,8 +1,11 @@ import { execFile } from "node:child_process"; import { constants } from "node:fs"; -import { access, mkdir, stat } from "node:fs/promises"; +import { access, mkdir, stat, writeFile } from "node:fs/promises"; +import { join } from "node:path"; import { promisify } from "node:util"; +import { buildContainerBubblewrapProbe } from "./launch-boundary.js"; + const execFileAsync = promisify(execFile); type Execute = ( @@ -19,6 +22,7 @@ export type HostedRunnerStartupProbeOptions = { access?: typeof access; mkdir?: typeof mkdir; stat?: typeof stat; + writeFile?: typeof writeFile; getUid?: () => number | undefined; getGid?: () => number | undefined; execute?: Execute; @@ -34,9 +38,12 @@ export type HostedRunnerStartupProbeResult = { "immutable-paths", "codex", "python", + "bubblewrap", + "bubblewrap-read-isolation", "setpriv", "writable-roots", ]; + generationFilesystemReadIsolation: "OS_ENFORCED"; }; export async function runHostedRunnerStartupProbe( @@ -46,6 +53,7 @@ export async function runHostedRunnerStartupProbe( const accessFile = options.access ?? access; const makeDirectory = options.mkdir ?? mkdir; const readMetadata = options.stat ?? stat; + const writeProbeFile = options.writeFile ?? writeFile; const uid = (options.getUid ?? process.getuid)?.(); const gid = (options.getGid ?? process.getgid)?.(); const execute = @@ -61,11 +69,15 @@ export async function runHostedRunnerStartupProbe( environment.COUNTERLAB_CODEX_HOME_ROOT ?? "/run/counterlab-codex"; const codexExecutable = environment.COUNTERLAB_CODEX_EXECUTABLE ?? "/usr/local/bin/codex"; + const codexRoot = environment.COUNTERLAB_CODEX_ROOT ?? "/opt/codex"; + const bwrapExecutable = + environment.COUNTERLAB_BWRAP_EXECUTABLE ?? "/usr/bin/bwrap"; const setprivExecutable = environment.COUNTERLAB_SETPRIV_EXECUTABLE ?? "/usr/bin/setpriv"; const pythonExecutable = environment.COUNTERLAB_PYTHON_EXECUTABLE ?? "/opt/counterlab-venv/bin/python"; + const probeWorkspace = join(workspaceRoot, ".isolation-probe"); if (uid !== 10001 || gid !== 10001) { throw new Error( @@ -95,11 +107,19 @@ export async function runHostedRunnerStartupProbe( accessFile(nodeExecutable, constants.X_OK), accessFile(bundlePath, constants.R_OK), accessFile(codexExecutable, constants.X_OK), + accessFile(codexRoot, constants.R_OK | constants.X_OK), + accessFile(bwrapExecutable, constants.X_OK), accessFile(setprivExecutable, constants.X_OK), accessFile(pythonExecutable, constants.X_OK), makeDirectory(workspaceRoot, { recursive: true, mode: 0o700 }), makeDirectory(codexHomeRoot, { recursive: true, mode: 0o700 }), + makeDirectory(probeWorkspace, { recursive: true, mode: 0o700 }), ]); + await writeProbeFile(join(probeWorkspace, "approved.txt"), "approved\n", { + encoding: "utf8", + flag: "w", + mode: 0o600, + }); const childEnvironment: NodeJS.ProcessEnv = { HOME: codexHomeRoot, @@ -133,6 +153,15 @@ export async function runHostedRunnerStartupProbe( env: childEnvironment, timeout: 30_000, }); + const isolationProbe = buildContainerBubblewrapProbe({ + bwrapExecutable, + codexRoot, + workspace: probeWorkspace, + }); + await execute(isolationProbe.command, isolationProbe.args, { + env: isolationProbe.environment, + timeout: 30_000, + }); return { status: "ready", @@ -144,8 +173,11 @@ export async function runHostedRunnerStartupProbe( "immutable-paths", "codex", "python", + "bubblewrap", + "bubblewrap-read-isolation", "setpriv", "writable-roots", ], + generationFilesystemReadIsolation: "OS_ENFORCED", }; } From f3651328593018ee90395d3cdfddc2ee56397b05 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 02:35:31 +0530 Subject: [PATCH 006/143] fix(release): version enforced isolation receipts --- apps/web/worker/api.test.ts | 6 +- apps/web/worker/api.ts | 5 +- apps/web/worker/deployment-config.test.ts | 25 +- apps/web/worker/runner-control-plane.test.ts | 6 + apps/web/worker/runner-control-plane.ts | 10 +- .../scientific-engine-registry/src/schema.ts | 87 ++++++- .../scientific-engine-registry/src/types.ts | 8 + .../test/release-check-receipt.test.ts | 83 ++++++- .../test/release-schema.test.ts | 166 +++++++++++-- .../schemas/deployment-receipt-v5.schema.json | 229 +++++++++++++++++ .../qualified-runner-release-v5.schema.json | 235 ++++++++++++++++++ .../release-check-receipt-v3.schema.json | 144 +++++++++++ scripts/create-deployment-receipt.test.ts | 3 + scripts/create-deployment-receipt.ts | 13 +- scripts/generate-contract-schemas.ts | 45 +++- scripts/prepare-qualified-deploy.test.ts | 1 + scripts/prepare-qualified-deploy.ts | 9 +- scripts/production-smoke.sh | 8 +- scripts/qualify-runner-release.ts | 6 +- scripts/release-check-receipt.ts | 6 +- scripts/test_production_smoke.py | 10 +- scripts/verify-scientific-engines.sh | 3 +- 22 files changed, 1043 insertions(+), 65 deletions(-) create mode 100644 scientific-engines/schemas/deployment-receipt-v5.schema.json create mode 100644 scientific-engines/schemas/qualified-runner-release-v5.schema.json create mode 100644 scientific-engines/schemas/release-check-receipt-v3.schema.json diff --git a/apps/web/worker/api.test.ts b/apps/web/worker/api.test.ts index 31d21843..44c36db3 100644 --- a/apps/web/worker/api.test.ts +++ b/apps/web/worker/api.test.ts @@ -10412,7 +10412,10 @@ describe("Cloudflare Worker API", () => { ); await expect(probedHealth.json()).resolves.toMatchObject({ ok: true, - data: { readiness: "ready" }, + data: { + readiness: "ready", + generationFilesystemReadIsolation: "OS_ENFORCED", + }, }); }); @@ -10443,6 +10446,7 @@ describe("Cloudflare Worker API", () => { liveCodex: "configured", liveKernel: "configured", readiness: "not-ready", + generationFilesystemReadIsolation: "PARTIAL", }, }); }); diff --git a/apps/web/worker/api.ts b/apps/web/worker/api.ts index 249cda2b..87726907 100644 --- a/apps/web/worker/api.ts +++ b/apps/web/worker/api.ts @@ -4254,7 +4254,10 @@ export function createApi(options: ApiOptions = {}) { runner === "configured" ? ("credential-and-privilege-boundary" as const) : ("local-runner-required" as const), - generationFilesystemReadIsolation: "PARTIAL" as const, + generationFilesystemReadIsolation: + readiness?.checks.runner === true + ? ("OS_ENFORCED" as const) + : ("PARTIAL" as const), requestId: context.get("requestId"), }), ); diff --git a/apps/web/worker/deployment-config.test.ts b/apps/web/worker/deployment-config.test.ts index 04216c76..ccbc49d6 100644 --- a/apps/web/worker/deployment-config.test.ts +++ b/apps/web/worker/deployment-config.test.ts @@ -515,6 +515,13 @@ describe("Cloudflare static asset routing", () => { expect(dockerfile).toContain("/repo/requirements.runner.lock.txt"); expect(dockerfile).toContain("chmod -R a=rX /repo"); expect(dockerfile).toContain("chmod 0555 /usr/local/bin/node"); + expect(dockerfile).toContain("bubblewrap=0.11.0-2+deb13u1"); + expect(dockerfile).toContain( + 'test "$(/usr/bin/bwrap --version)" = "bubblewrap 0.11.0"', + ); + expect(dockerfile.indexOf("bubblewrap=0.11.0-2+deb13u1")).toBeLessThan( + dockerfile.indexOf("USER 10001:10001"), + ); expect(dockerfile.indexOf("chmod 0555 /usr/local/bin/node")).toBeLessThan( dockerfile.indexOf("USER 10001:10001"), ); @@ -547,8 +554,9 @@ describe("Cloudflare static asset routing", () => { const evidenceCommit = "2".repeat(40); const image = `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`; const receipt = { - schemaVersion: "4", + schemaVersion: "5", status: "VERIFIED", + generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, sourceArchiveSha256: "b".repeat(64), sourceTreeSha256: "c".repeat(64), @@ -580,9 +588,10 @@ describe("Cloudflare static asset routing", () => { registryDigest: `sha256:${"3".repeat(64)}`, registryResolvedAt: "2026-07-16T16:20:00.000Z", qualifiedAt: "2026-07-16T16:30:00.000Z", - verifierVersion: "counterlab-release-v4", + verifierVersion: "counterlab-release-v5", }; const observation = { + generationFilesystemReadIsolation: "OS_ENFORCED" as const, sourceCommit, sourceArchiveSha256: receipt.sourceArchiveSha256, sourceTreeSha256: receipt.sourceTreeSha256, @@ -789,8 +798,9 @@ describe("Cloudflare static asset routing", () => { const evidenceCommit = "2".repeat(40); const image = `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`; const receipt = { - schemaVersion: "4", + schemaVersion: "5", status: "VERIFIED", + generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, sourceArchiveSha256: "b".repeat(64), sourceTreeSha256: "c".repeat(64), @@ -822,7 +832,7 @@ describe("Cloudflare static asset routing", () => { registryDigest: `sha256:${"3".repeat(64)}`, registryResolvedAt: "2026-07-16T16:20:00.000Z", qualifiedAt: "2026-07-16T16:30:00.000Z", - verifierVersion: "counterlab-release-v4", + verifierVersion: "counterlab-release-v5", }; const config = productionDeployConfig(); config.durable_objects.bindings = [ @@ -835,6 +845,7 @@ describe("Cloudflare static asset routing", () => { receipt, image, observation: { + generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, sourceArchiveSha256: receipt.sourceArchiveSha256, sourceTreeSha256: receipt.sourceTreeSha256, @@ -877,8 +888,9 @@ describe("Cloudflare static asset routing", () => { const sourceCommit = "a".repeat(40); const image = `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`; const receipt = { - schemaVersion: "4", + schemaVersion: "5", status: "VERIFIED", + generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, sourceArchiveSha256: "b".repeat(64), sourceTreeSha256: "c".repeat(64), @@ -910,7 +922,7 @@ describe("Cloudflare static asset routing", () => { registryDigest: `sha256:${"3".repeat(64)}`, registryResolvedAt: "2026-07-16T15:55:00.000Z", qualifiedAt: "2026-07-16T16:30:00.000Z", - verifierVersion: "counterlab-release-v4", + verifierVersion: "counterlab-release-v5", }; expect(() => @@ -922,6 +934,7 @@ describe("Cloudflare static asset routing", () => { receipt, image, observation: { + generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, sourceArchiveSha256: "9".repeat(64), sourceTreeSha256: receipt.sourceTreeSha256, diff --git a/apps/web/worker/runner-control-plane.test.ts b/apps/web/worker/runner-control-plane.test.ts index 922b4eb0..20a17ed7 100644 --- a/apps/web/worker/runner-control-plane.test.ts +++ b/apps/web/worker/runner-control-plane.test.ts @@ -41,6 +41,7 @@ describe("HttpRunnerDispatcher", () => { JSON.stringify({ status: "ready", service: "counterlab-hosted-runner", + generationFilesystemReadIsolation: "OS_ENFORCED", runnerSourceCommit, runnerImageDigest, }), @@ -83,6 +84,7 @@ describe("HttpRunnerDispatcher", () => { ["invalid response", "schema"], ["stale source identity", "source"], ["stale image identity", "image"], + ["partial filesystem isolation", "isolation"], ])("fails Container readiness closed on %s", async (_label, failure) => { const startAndWaitForPorts = vi.fn(async () => { if (failure === "startup") throw new Error("image did not start"); @@ -97,6 +99,8 @@ describe("HttpRunnerDispatcher", () => { status: "ready", service: failure === "schema" ? "wrong-runner" : "counterlab-hosted-runner", + generationFilesystemReadIsolation: + failure === "isolation" ? "PARTIAL" : "OS_ENFORCED", runnerSourceCommit: failure === "source" ? "d".repeat(40) : runnerSourceCommit, runnerImageDigest: @@ -321,6 +325,7 @@ describe("HttpRunnerDispatcher", () => { JSON.stringify({ status: "ready", service: "counterlab-hosted-runner", + generationFilesystemReadIsolation: "OS_ENFORCED", runnerSourceCommit, runnerImageDigest, }), @@ -407,6 +412,7 @@ describe("HttpRunnerDispatcher", () => { JSON.stringify({ status: "ready", service: "counterlab-hosted-runner", + generationFilesystemReadIsolation: "OS_ENFORCED", runnerSourceCommit: source, runnerImageDigest: image, }), diff --git a/apps/web/worker/runner-control-plane.ts b/apps/web/worker/runner-control-plane.ts index 529e5feb..1042e9df 100644 --- a/apps/web/worker/runner-control-plane.ts +++ b/apps/web/worker/runner-control-plane.ts @@ -105,12 +105,20 @@ async function runnerReadinessMatches( !Array.isArray(payload) && JSON.stringify(Object.keys(payload).sort()) === JSON.stringify( - ["runnerImageDigest", "runnerSourceCommit", "service", "status"].sort(), + [ + "generationFilesystemReadIsolation", + "runnerImageDigest", + "runnerSourceCommit", + "service", + "status", + ].sort(), ) && "status" in payload && payload.status === "ready" && "service" in payload && payload.service === "counterlab-hosted-runner" && + "generationFilesystemReadIsolation" in payload && + payload.generationFilesystemReadIsolation === "OS_ENFORCED" && "runnerSourceCommit" in payload && payload.runnerSourceCommit === releaseIdentity.runnerSourceCommit && "runnerImageDigest" in payload && diff --git a/packages/scientific-engine-registry/src/schema.ts b/packages/scientific-engine-registry/src/schema.ts index 03a66c8d..245bebff 100644 --- a/packages/scientific-engine-registry/src/schema.ts +++ b/packages/scientific-engine-registry/src/schema.ts @@ -838,7 +838,7 @@ export const QualifiedRunnerReleaseV3Schema = z export const QUALIFIED_AGGREGATE_LIMIT_MODE = "container-cgroup-and-process-rlimit" as const; -export const QualifiedRunnerReleaseSchema = z +export const QualifiedRunnerReleaseV4Schema = z .strictObject({ schemaVersion: z.literal("4"), status: z.literal("VERIFIED"), @@ -966,6 +966,34 @@ export const QualifiedRunnerReleaseSchema = z } }); +export const QualifiedRunnerReleaseSchema = z + .strictObject({ + ...QualifiedRunnerReleaseV4Schema.shape, + schemaVersion: z.literal("5"), + generationFilesystemReadIsolation: z.literal("OS_ENFORCED"), + verifierVersion: z.literal("counterlab-release-v5"), + }) + .superRefine((release, context) => { + const { + generationFilesystemReadIsolation: _generationFilesystemReadIsolation, + ...legacyRelease + } = release; + const result = QualifiedRunnerReleaseV4Schema.safeParse({ + ...legacyRelease, + schemaVersion: "4", + verifierVersion: "counterlab-release-v4", + }); + if (!result.success) { + for (const issue of result.error.issues) { + context.addIssue({ + code: "custom", + path: issue.path, + message: issue.message, + }); + } + } + }); + export const RELEASE_CHECK_IDS = [ "test-all", "leakage-mutations", @@ -1045,7 +1073,7 @@ export const ReleaseCheckReceiptV1Schema = z } }); -export const ReleaseCheckReceiptSchema = z +export const ReleaseCheckReceiptV2Schema = z .strictObject({ schemaVersion: z.literal("2"), status: z.literal("PASSED"), @@ -1113,6 +1141,34 @@ export const ReleaseCheckReceiptSchema = z } }); +export const ReleaseCheckReceiptSchema = z + .strictObject({ + ...ReleaseCheckReceiptV2Schema.shape, + schemaVersion: z.literal("3"), + generationFilesystemReadIsolation: z.literal("OS_ENFORCED"), + verifierVersion: z.literal("counterlab-release-check-v3"), + }) + .superRefine((receipt, context) => { + const { + generationFilesystemReadIsolation: _generationFilesystemReadIsolation, + ...legacyReceipt + } = receipt; + const result = ReleaseCheckReceiptV2Schema.safeParse({ + ...legacyReceipt, + schemaVersion: "2", + verifierVersion: "counterlab-release-check-v2", + }); + if (!result.success) { + for (const issue of result.error.issues) { + context.addIssue({ + code: "custom", + path: issue.path, + message: issue.message, + }); + } + } + }); + export const DeploymentReceiptV3Schema = z .strictObject({ schemaVersion: z.literal("3"), @@ -1203,7 +1259,7 @@ export const DeploymentReceiptV3Schema = z } }); -export const DeploymentReceiptSchema = z +export const DeploymentReceiptV4Schema = z .strictObject({ schemaVersion: z.literal("4"), status: z.literal("DEPLOYED"), @@ -1306,3 +1362,28 @@ export const DeploymentReceiptSchema = z }); } }); + +export const DeploymentReceiptSchema = z + .strictObject({ + ...DeploymentReceiptV4Schema.shape, + schemaVersion: z.literal("5"), + generationFilesystemReadIsolation: z.literal("OS_ENFORCED"), + verifierVersion: z.literal("counterlab-deployment-v5"), + }) + .superRefine((receipt, context) => { + const result = DeploymentReceiptV4Schema.safeParse({ + ...receipt, + schemaVersion: "4", + generationFilesystemReadIsolation: "PARTIAL", + verifierVersion: "counterlab-deployment-v4", + }); + if (!result.success) { + for (const issue of result.error.issues) { + context.addIssue({ + code: "custom", + path: issue.path, + message: issue.message, + }); + } + } + }); diff --git a/packages/scientific-engine-registry/src/types.ts b/packages/scientific-engine-registry/src/types.ts index 8c5345bc..d062c5ac 100644 --- a/packages/scientific-engine-registry/src/types.ts +++ b/packages/scientific-engine-registry/src/types.ts @@ -5,6 +5,7 @@ import { ContainedRuntimeAttestationV1Schema, DeploymentReceiptSchema, DeploymentReceiptV3Schema, + DeploymentReceiptV4Schema, ScientificEngineDescriptorSchema, ScientificEngineEvidenceCatalogSchema, ScientificEngineEvidenceKindSchema, @@ -17,8 +18,10 @@ import { QualifiedRunnerReleaseV1Schema, QualifiedRunnerReleaseV2Schema, QualifiedRunnerReleaseV3Schema, + QualifiedRunnerReleaseV4Schema, ReleaseCheckReceiptSchema, ReleaseCheckReceiptV1Schema, + ReleaseCheckReceiptV2Schema, SubjectPackAuthorityBindingSchema, SubjectPackEngineBindingSchema, SubjectPackEngineBindingsSchema, @@ -33,6 +36,7 @@ export type ContainedRuntimeAttestationV1 = z.infer< >; export type DeploymentReceipt = z.infer; export type DeploymentReceiptV3 = z.infer; +export type DeploymentReceiptV4 = z.infer; export type ScientificEngineRole = z.infer; export type ScientificEngineDescriptor = z.infer< @@ -77,6 +81,10 @@ export type QualifiedRunnerReleaseV2 = z.infer< export type QualifiedRunnerReleaseV3 = z.infer< typeof QualifiedRunnerReleaseV3Schema >; +export type QualifiedRunnerReleaseV4 = z.infer< + typeof QualifiedRunnerReleaseV4Schema +>; export type TimeoutCleanupReceipt = z.infer; export type ReleaseCheckReceipt = z.infer; export type ReleaseCheckReceiptV1 = z.infer; +export type ReleaseCheckReceiptV2 = z.infer; diff --git a/packages/scientific-engine-registry/test/release-check-receipt.test.ts b/packages/scientific-engine-registry/test/release-check-receipt.test.ts index e62c6db6..76ffe9b7 100644 --- a/packages/scientific-engine-registry/test/release-check-receipt.test.ts +++ b/packages/scientific-engine-registry/test/release-check-receipt.test.ts @@ -1,8 +1,14 @@ import { createHash } from "node:crypto"; -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; +import { + ReleaseCheckReceiptSchema, + ReleaseCheckReceiptV2Schema, +} from "../src/index"; + import { assertReleaseCheckBinding, createDeploymentReleaseIdentity, @@ -15,8 +21,9 @@ const evidenceCommit = "b".repeat(40); function qualifiedReceipt() { return { - schemaVersion: "4", + schemaVersion: "5", status: "VERIFIED", + generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, sourceArchiveSha256: "1".repeat(64), sourceTreeSha256: "2".repeat(64), @@ -57,7 +64,7 @@ function qualifiedReceipt() { registryDigest: `sha256:${"1".repeat(64)}`, registryResolvedAt: "2026-07-19T00:00:00.000Z", qualifiedAt: "2026-07-19T00:01:00.000Z", - verifierVersion: "counterlab-release-v4", + verifierVersion: "counterlab-release-v5", } as const; } @@ -103,11 +110,11 @@ function deploymentReceipt() { const runner = "d".repeat(40); const digest = `sha256:${"a".repeat(64)}`; return { - schemaVersion: "4", + schemaVersion: "5", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", - generationFilesystemReadIsolation: "PARTIAL", + generationFilesystemReadIsolation: "OS_ENFORCED", workerEvidenceCommit: worker, runnerSourceCommit: runner, qualifiedRunnerReceiptSha256: "1".repeat(64), @@ -144,7 +151,7 @@ function deploymentReceipt() { workerVersionSha256: "c".repeat(64), containerStatusSha256: "d".repeat(64), deployedAt: "2026-07-19T00:00:00.000Z", - verifierVersion: "counterlab-deployment-v4", + verifierVersion: "counterlab-deployment-v5", } as const; } @@ -263,13 +270,75 @@ describe("release-check receipt", () => { }); expect(receipt).toMatchObject({ + schemaVersion: "3", status: "PASSED", + generationFilesystemReadIsolation: "OS_ENFORCED", evidenceCommit, - verifierVersion: "counterlab-release-check-v2", + verifierVersion: "counterlab-release-check-v3", }); expect(receipt.checks).toHaveLength(11); }); + it("preserves v2 receipts while requiring OS isolation in v3", () => { + const current = createReleaseCheckReceipt({ + qualifiedReceipt: qualifiedReceipt(), + qualifiedReceiptBytes: qualifiedBytes(), + runtimeAttestation: runtimeAttestation(), + currentCommit: evidenceCommit, + worktreeClean: true, + runnerImageDigest: qualifiedReceipt().localImageDigest, + adapterImageDigest: qualifiedReceipt().adapterImageDigest, + checkedAt: "2026-07-19T00:02:00.000Z", + }); + const { + generationFilesystemReadIsolation: _generationFilesystemReadIsolation, + ...legacy + } = current; + + expect( + ReleaseCheckReceiptV2Schema.parse({ + ...legacy, + schemaVersion: "2", + verifierVersion: "counterlab-release-check-v2", + }), + ).toMatchObject({ schemaVersion: "2" }); + expect(() => + ReleaseCheckReceiptSchema.parse({ + ...current, + generationFilesystemReadIsolation: "PARTIAL", + }), + ).toThrow(); + }); + + it("publishes distinct historical v2 and isolation-enforced v3 schemas", () => { + const v2Path = resolve( + process.cwd(), + "scientific-engines/schemas/release-check-receipt-v2.schema.json", + ); + const v3Path = resolve( + process.cwd(), + "scientific-engines/schemas/release-check-receipt-v3.schema.json", + ); + expect(existsSync(v2Path)).toBe(true); + expect(existsSync(v3Path)).toBe(true); + const v2 = JSON.parse(readFileSync(v2Path, "utf8")) as { + properties?: Record; + required?: string[]; + }; + const v3 = JSON.parse(readFileSync(v3Path, "utf8")) as typeof v2; + + expect(v2.properties?.schemaVersion?.const).toBe("2"); + expect(v2.properties).not.toHaveProperty( + "generationFilesystemReadIsolation", + ); + expect(v2.required).not.toContain("generationFilesystemReadIsolation"); + expect(v3.properties?.schemaVersion?.const).toBe("3"); + expect(v3.properties?.generationFilesystemReadIsolation?.const).toBe( + "OS_ENFORCED", + ); + expect(v3.required).toContain("generationFilesystemReadIsolation"); + }); + it("rejects a different qualified receipt or live runtime", () => { const receipt = createReleaseCheckReceipt({ qualifiedReceipt: qualifiedReceipt(), diff --git a/packages/scientific-engine-registry/test/release-schema.test.ts b/packages/scientific-engine-registry/test/release-schema.test.ts index 903a7a4c..e134cfaa 100644 --- a/packages/scientific-engine-registry/test/release-schema.test.ts +++ b/packages/scientific-engine-registry/test/release-schema.test.ts @@ -5,9 +5,11 @@ import { describe, expect, it } from "vitest"; import { DeploymentReceiptSchema, DeploymentReceiptV3Schema, + DeploymentReceiptV4Schema, QualifiedRunnerReleaseSchema, QualifiedRunnerReleaseV2Schema, QualifiedRunnerReleaseV3Schema, + QualifiedRunnerReleaseV4Schema, } from "../src/index"; import * as releaseTools from "../../../scripts/prepare-qualified-deploy"; @@ -37,7 +39,7 @@ describe("qualified runner release schema", () => { expect(() => QualifiedRunnerReleaseSchema.parse(legacyReceipt())).toThrow(); }); - it("creates a runtime-bound promoted v4 receipt from recomputed evidence", () => { + it("creates a runtime-bound promoted v5 receipt from recomputed evidence", () => { const sourceCommit = "a".repeat(40); const evidenceCommit = "2".repeat(40); const registryImage = `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`; @@ -88,6 +90,7 @@ describe("qualified runner release schema", () => { timeoutRuntimeSessionId: "rt-v61-test1", timeoutVerifiedAt: "2026-07-16T16:24:00.000Z", currentCommit: evidenceCommit, + generationFilesystemReadIsolation: "OS_ENFORCED", sourceIsAncestor: true, changedPaths: ["scientific-engines/snapshot.json"], registryImage, @@ -99,7 +102,8 @@ describe("qualified runner release schema", () => { ); expect(QualifiedRunnerReleaseSchema.parse(receipt)).toMatchObject({ - schemaVersion: "4", + schemaVersion: "5", + generationFilesystemReadIsolation: "OS_ENFORCED", evidenceCommit, registryImage, registryDigest: `sha256:${"3".repeat(64)}`, @@ -115,6 +119,79 @@ describe("qualified runner release schema", () => { ).toThrow(); }); + it("retains the exact historical v4 receipt contract", () => { + const sourceCommit = "a".repeat(40); + const evidenceCommit = "2".repeat(40); + const current = ( + releaseTools as unknown as { + createQualifiedRunnerRelease: ( + observation: Record, + qualifiedAt: string, + ) => Record; + } + ).createQualifiedRunnerRelease( + { + sourceCommit, + sourceArchiveSha256: "b".repeat(64), + sourceTreeSha256: "c".repeat(64), + dockerfileSha256: "d".repeat(64), + localImageTag: `counterlab-runner:git-${sourceCommit}`, + localImageDigest: `sha256:${"e".repeat(64)}`, + ociRevision: sourceCommit, + ociSourceTreeSha256: "c".repeat(64), + engineAuthorityHash: "f".repeat(64), + runtimeManifestHash: "1".repeat(64), + runtimeToolchainSha256: "4".repeat(64), + runtimePolicySha256: "0".repeat(64), + proofDependencyManifestSha256: "1".repeat(64), + toolchainLockSha256: "5".repeat(64), + runtimeAdapterSha256: "6".repeat(64), + buildctlSha256: "7".repeat(64), + buildkitdSha256: "8".repeat(64), + buildkitConfigSha256: "9".repeat(64), + adapterDockerfileSha256: "a".repeat(64), + adapterImageTag: `counterlab-adapter:git-${sourceCommit}`, + adapterImageDigest: `sha256:${"b".repeat(64)}`, + adapterManifestDigest: `sha256:${"c".repeat(64)}`, + adapterOciArchiveSha256: "d".repeat(64), + adapterOciRevision: sourceCommit, + adapterOciSourceTreeSha256: "c".repeat(64), + limitMode: "container-cgroup-and-process-rlimit", + aggregateLimitIntentEnforced: true, + aggregateLimitEvidenceSha256: "e".repeat(64), + timeoutCleanupReceipt: `node_modules/.cache/counterlab-v6.1/releases/timeout-cleanup-${sourceCommit}.json`, + timeoutCleanupReceiptSha256: "e".repeat(64), + timeoutCleanupPayloadSha256: "f".repeat(64), + timeoutRunControlReceiptSha256: "0".repeat(64), + timeoutRootlessReceiptSha256: "1".repeat(64), + timeoutRuntimeSessionId: "rt-v61-test1", + timeoutVerifiedAt: "2026-07-16T16:24:00.000Z", + currentCommit: evidenceCommit, + generationFilesystemReadIsolation: "OS_ENFORCED", + sourceIsAncestor: true, + changedPaths: ["scientific-engines/snapshot.json"], + registryImage: `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`, + registryDigest: `sha256:${"3".repeat(64)}`, + registryResolvedAt: "2026-07-16T16:25:00.000Z", + observedAt: "2026-07-16T16:30:00.000Z", + }, + "2026-07-16T16:30:00.000Z", + ); + const { + generationFilesystemReadIsolation: _generationFilesystemReadIsolation, + ...legacyV4 + } = current; + + expect( + QualifiedRunnerReleaseV4Schema.parse({ + ...legacyV4, + schemaVersion: "4", + verifierVersion: "counterlab-release-v4", + }), + ).toMatchObject({ schemaVersion: "4" }); + expect(() => QualifiedRunnerReleaseSchema.parse(legacyV4)).toThrow(); + }); + it("retains strict parsing for historical v3 receipts", () => { const sourceCommit = "a".repeat(40); expect( @@ -179,18 +256,31 @@ describe("qualified runner release schema", () => { ); }); - it("publishes the timeout-bound v4 receipt as generated JSON Schema", () => { - const path = resolve( + it("publishes distinct historical v4 and isolation-enforced v5 schemas", () => { + const v4Path = resolve( process.cwd(), "scientific-engines/schemas/qualified-runner-release-v4.schema.json", ); - expect(existsSync(path)).toBe(true); - const schema = JSON.parse(readFileSync(path, "utf8")) as { - properties?: { schemaVersion?: { const?: string } }; + const v5Path = resolve( + process.cwd(), + "scientific-engines/schemas/qualified-runner-release-v5.schema.json", + ); + expect(existsSync(v4Path)).toBe(true); + expect(existsSync(v5Path)).toBe(true); + const v4 = JSON.parse(readFileSync(v4Path, "utf8")) as { + properties?: Record; required?: string[]; }; - expect(schema.properties?.schemaVersion?.const).toBe("4"); - expect(schema.required).toEqual( + const v5 = JSON.parse(readFileSync(v5Path, "utf8")) as { + properties?: Record; + required?: string[]; + }; + expect(v4.properties?.schemaVersion?.const).toBe("4"); + expect(v4.properties).not.toHaveProperty( + "generationFilesystemReadIsolation", + ); + expect(v4.required).not.toContain("generationFilesystemReadIsolation"); + expect(v4.required).toEqual( expect.arrayContaining([ "timeoutCleanupReceipt", "timeoutCleanupReceiptSha256", @@ -203,6 +293,11 @@ describe("qualified runner release schema", () => { "proofDependencyManifestSha256", ]), ); + expect(v5.properties?.schemaVersion?.const).toBe("5"); + expect(v5.properties?.generationFilesystemReadIsolation?.const).toBe( + "OS_ENFORCED", + ); + expect(v5.required).toContain("generationFilesystemReadIsolation"); }); }); @@ -212,11 +307,11 @@ describe("deployment receipt schema", () => { const runner = "b".repeat(40); const digest = `sha256:${"c".repeat(64)}`; return { - schemaVersion: "4", + schemaVersion: "5", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", - generationFilesystemReadIsolation: "PARTIAL", + generationFilesystemReadIsolation: "OS_ENFORCED", workerEvidenceCommit: worker, runnerSourceCommit: runner, qualifiedRunnerReceiptSha256: "1".repeat(64), @@ -253,13 +348,13 @@ describe("deployment receipt schema", () => { workerVersionSha256: "b".repeat(64), containerStatusSha256: "c".repeat(64), deployedAt: "2026-07-19T00:03:00.000Z", - verifierVersion: "counterlab-deployment-v4", + verifierVersion: "counterlab-deployment-v5", } as const; } it("binds the deployed Worker, Container, release checks, and exact artifacts", () => { expect(DeploymentReceiptSchema.parse(receipt())).toMatchObject({ - schemaVersion: "4", + schemaVersion: "5", status: "DEPLOYED", workerName: "counterlab", }); @@ -278,11 +373,25 @@ describe("deployment receipt schema", () => { expect(() => DeploymentReceiptSchema.parse({ ...receipt(), - generationFilesystemReadIsolation: "OS_ENFORCED", + generationFilesystemReadIsolation: "PARTIAL", }), ).toThrow(); }); + it("retains the historical v4 PARTIAL isolation contract", () => { + expect( + DeploymentReceiptV4Schema.parse({ + ...receipt(), + schemaVersion: "4", + generationFilesystemReadIsolation: "PARTIAL", + verifierVersion: "counterlab-deployment-v4", + }), + ).toMatchObject({ + schemaVersion: "4", + generationFilesystemReadIsolation: "PARTIAL", + }); + }); + it("rejects a dry-run projection that changes bytes or contains another file", () => { expect(() => DeploymentReceiptSchema.parse({ @@ -316,6 +425,7 @@ describe("deployment receipt schema", () => { DeploymentReceiptV3Schema.parse({ ...historical, schemaVersion: "3", + generationFilesystemReadIsolation: "PARTIAL", verifierVersion: "counterlab-deployment-v3", }), ).toMatchObject({ schemaVersion: "3" }); @@ -347,22 +457,32 @@ describe("deployment receipt schema", () => { ); }); - it("publishes the timeout-bound v4 deployment receipt as generated JSON Schema", () => { - const path = resolve( + it("publishes distinct historical v4 and isolation-enforced v5 deployment schemas", () => { + const v4Path = resolve( process.cwd(), "scientific-engines/schemas/deployment-receipt-v4.schema.json", ); - expect(existsSync(path)).toBe(true); - const schema = JSON.parse(readFileSync(path, "utf8")) as { + const v5Path = resolve( + process.cwd(), + "scientific-engines/schemas/deployment-receipt-v5.schema.json", + ); + expect(existsSync(v4Path)).toBe(true); + expect(existsSync(v5Path)).toBe(true); + const v4 = JSON.parse(readFileSync(v4Path, "utf8")) as { properties?: { schemaVersion?: { const?: string }; + generationFilesystemReadIsolation?: { const?: string }; dryRunFileCount?: { const?: number }; }; required?: string[]; }; - expect(schema.properties?.schemaVersion?.const).toBe("4"); - expect(schema.properties?.dryRunFileCount?.const).toBe(1); - expect(schema.required).toEqual( + const v5 = JSON.parse(readFileSync(v5Path, "utf8")) as typeof v4; + expect(v4.properties?.schemaVersion?.const).toBe("4"); + expect(v4.properties?.generationFilesystemReadIsolation?.const).toBe( + "PARTIAL", + ); + expect(v4.properties?.dryRunFileCount?.const).toBe(1); + expect(v4.required).toEqual( expect.arrayContaining([ "timeoutCleanupReceiptSha256", "aggregateLimitEvidenceSha256", @@ -376,5 +496,9 @@ describe("deployment receipt schema", () => { "wranglerVersion", ]), ); + expect(v5.properties?.schemaVersion?.const).toBe("5"); + expect(v5.properties?.generationFilesystemReadIsolation?.const).toBe( + "OS_ENFORCED", + ); }); }); diff --git a/scientific-engines/schemas/deployment-receipt-v5.schema.json b/scientific-engines/schemas/deployment-receipt-v5.schema.json new file mode 100644 index 00000000..33ab8cf1 --- /dev/null +++ b/scientific-engines/schemas/deployment-receipt-v5.schema.json @@ -0,0 +1,229 @@ +{ + "$id": "https://counterlab.dev/schemas/deployment-receipt-v5.schema.json", + "title": "CounterLab deployment receipt v5", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "5" + }, + "status": { + "type": "string", + "const": "DEPLOYED" + }, + "workerName": { + "type": "string", + "const": "counterlab" + }, + "productionOrigin": { + "type": "string", + "const": "https://counterlab.cserules.workers.dev" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "workerEvidenceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "runnerSourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "qualifiedRunnerReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckCheckedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "timeoutCleanupReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "aggregateLimitEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeToolchainSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimePolicySha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "proofDependencyManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeAdapterSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "workerVersionId": { + "type": "string", + "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" + }, + "workerTag": { + "type": "string", + "pattern": "^git-[a-f0-9]{40}$" + }, + "workerMessage": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "containerApplicationId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,128}$" + }, + "containerApplicationVersion": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "containerImage": { + "type": "string", + "pattern": "^registry\\.cloudflare\\.com\\/[A-Za-z0-9_-]{3,64}\\/counterlab-runner@sha256:[a-f0-9]{64}$" + }, + "containerState": { + "type": "string", + "enum": [ + "active", + "ready" + ] + }, + "containerImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "workerArtifactClassification": { + "type": "string", + "const": "PROCESS_BOUND_PARTIAL" + }, + "workerArtifactManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "deployConfigSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "workerBundleSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientAssetsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientAssetCount": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "clientPublicAssetsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientPublicAssetCount": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "viteVersion": { + "type": "string", + "const": "8.1.4" + }, + "wranglerVersion": { + "type": "string", + "const": "4.110.0" + }, + "dryRunSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "dryRunFileCount": { + "type": "number", + "const": 1 + }, + "deploymentStatusSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "workerVersionSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "containerStatusSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "deployedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-deployment-v5" + } + }, + "required": [ + "schemaVersion", + "status", + "workerName", + "productionOrigin", + "generationFilesystemReadIsolation", + "workerEvidenceCommit", + "runnerSourceCommit", + "qualifiedRunnerReceiptSha256", + "releaseCheckReceiptSha256", + "releaseCheckCheckedAt", + "timeoutCleanupReceiptSha256", + "aggregateLimitEvidenceSha256", + "runtimeToolchainSha256", + "runtimePolicySha256", + "proofDependencyManifestSha256", + "runtimeAdapterSha256", + "adapterImageDigest", + "workerVersionId", + "workerTag", + "workerMessage", + "containerApplicationId", + "containerApplicationVersion", + "containerImage", + "containerState", + "containerImageDigest", + "workerArtifactClassification", + "workerArtifactManifestSha256", + "deployConfigSha256", + "workerBundleSha256", + "clientAssetsSha256", + "clientAssetCount", + "clientPublicAssetsSha256", + "clientPublicAssetCount", + "viteVersion", + "wranglerVersion", + "dryRunSha256", + "dryRunFileCount", + "deploymentStatusSha256", + "workerVersionSha256", + "containerStatusSha256", + "deployedAt", + "verifierVersion" + ], + "additionalProperties": false +} diff --git a/scientific-engines/schemas/qualified-runner-release-v5.schema.json b/scientific-engines/schemas/qualified-runner-release-v5.schema.json new file mode 100644 index 00000000..db58db8a --- /dev/null +++ b/scientific-engines/schemas/qualified-runner-release-v5.schema.json @@ -0,0 +1,235 @@ +{ + "$id": "https://counterlab.dev/schemas/qualified-runner-release-v5.schema.json", + "title": "CounterLab qualified runner release v5", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "5" + }, + "status": { + "type": "string", + "const": "VERIFIED" + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceArchiveSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "sourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "dockerfileSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "localImageTag": { + "type": "string", + "pattern": "^counterlab-runner:git-[a-f0-9]{40}$" + }, + "localImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "ociRevision": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "ociSourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "engineAuthorityHash": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeManifestHash": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeToolchainSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimePolicySha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "proofDependencyManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "toolchainLockSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeAdapterSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "buildctlSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "buildkitdSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "buildkitConfigSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterDockerfileSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterImageTag": { + "type": "string", + "pattern": "^counterlab-adapter:git-[a-f0-9]{40}$" + }, + "adapterImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "adapterManifestDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "adapterOciArchiveSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterOciRevision": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "adapterOciSourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "limitMode": { + "type": "string", + "const": "container-cgroup-and-process-rlimit" + }, + "aggregateLimitIntentEnforced": { + "type": "boolean", + "const": true + }, + "aggregateLimitEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutCleanupReceipt": { + "type": "string", + "minLength": 1, + "maxLength": 300 + }, + "timeoutCleanupReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutCleanupPayloadSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutRunControlReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutRootlessReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutRuntimeSessionId": { + "type": "string", + "pattern": "^rt-[a-z0-9][a-z0-9-]{7,13}$" + }, + "timeoutVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "evidenceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "registryImage": { + "type": "string", + "pattern": "^registry\\.cloudflare\\.com\\/[A-Za-z0-9_-]{3,64}\\/counterlab-runner:git-[a-f0-9]{40}$" + }, + "registryDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "registryResolvedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "qualifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-release-v5" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + } + }, + "required": [ + "schemaVersion", + "status", + "sourceCommit", + "sourceArchiveSha256", + "sourceTreeSha256", + "dockerfileSha256", + "localImageTag", + "localImageDigest", + "ociRevision", + "ociSourceTreeSha256", + "engineAuthorityHash", + "runtimeManifestHash", + "runtimeToolchainSha256", + "runtimePolicySha256", + "proofDependencyManifestSha256", + "toolchainLockSha256", + "runtimeAdapterSha256", + "buildctlSha256", + "buildkitdSha256", + "buildkitConfigSha256", + "adapterDockerfileSha256", + "adapterImageTag", + "adapterImageDigest", + "adapterManifestDigest", + "adapterOciArchiveSha256", + "adapterOciRevision", + "adapterOciSourceTreeSha256", + "limitMode", + "aggregateLimitIntentEnforced", + "aggregateLimitEvidenceSha256", + "timeoutCleanupReceipt", + "timeoutCleanupReceiptSha256", + "timeoutCleanupPayloadSha256", + "timeoutRunControlReceiptSha256", + "timeoutRootlessReceiptSha256", + "timeoutRuntimeSessionId", + "timeoutVerifiedAt", + "evidenceCommit", + "registryImage", + "registryDigest", + "registryResolvedAt", + "qualifiedAt", + "verifierVersion", + "generationFilesystemReadIsolation" + ], + "additionalProperties": false +} diff --git a/scientific-engines/schemas/release-check-receipt-v3.schema.json b/scientific-engines/schemas/release-check-receipt-v3.schema.json new file mode 100644 index 00000000..17da26c8 --- /dev/null +++ b/scientific-engines/schemas/release-check-receipt-v3.schema.json @@ -0,0 +1,144 @@ +{ + "$id": "https://counterlab.dev/schemas/release-check-receipt-v3.schema.json", + "title": "CounterLab release-check receipt v3", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "3" + }, + "status": { + "type": "string", + "const": "PASSED" + }, + "evidenceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "qualifiedRunnerReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "qualifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "runnerImageTag": { + "type": "string", + "pattern": "^counterlab-runner:git-[a-f0-9]{40}$" + }, + "runnerImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "adapterImageTag": { + "type": "string", + "pattern": "^counterlab-adapter:git-[a-f0-9]{40}$" + }, + "adapterImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "registryDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "runtimeToolchainSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimePolicySha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "proofDependencyManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "aggregateLimitEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeAdapterSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "checks": { + "minItems": 11, + "maxItems": 11, + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "test-all", + "leakage-mutations", + "imbalance-mutations", + "held-out", + "adapter-sandbox-smoke", + "scientific-engines", + "web-build", + "achieved-metrics", + "reproduce-leakage", + "replay-patch", + "secret-scan" + ] + }, + "status": { + "type": "string", + "const": "PASSED" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + }, + "checkedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-release-check-v3" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + } + }, + "required": [ + "schemaVersion", + "status", + "evidenceCommit", + "sourceCommit", + "qualifiedRunnerReceiptSha256", + "qualifiedAt", + "runnerImageTag", + "runnerImageDigest", + "adapterImageTag", + "adapterImageDigest", + "registryDigest", + "runtimeToolchainSha256", + "runtimePolicySha256", + "proofDependencyManifestSha256", + "aggregateLimitEvidenceSha256", + "runtimeAdapterSha256", + "checks", + "checkedAt", + "verifierVersion", + "generationFilesystemReadIsolation" + ], + "additionalProperties": false +} diff --git a/scripts/create-deployment-receipt.test.ts b/scripts/create-deployment-receipt.test.ts index 32dca2b3..f3779437 100644 --- a/scripts/create-deployment-receipt.test.ts +++ b/scripts/create-deployment-receipt.test.ts @@ -194,6 +194,7 @@ describe("deployment receipt Worker identity", () => { describe("deployment receipt qualification binding", () => { const qualifiedReceiptBytes = Buffer.from("qualified-receipt\n"); const qualified = { + generationFilesystemReadIsolation: "OS_ENFORCED", evidenceCommit: "a".repeat(40), sourceCommit: "b".repeat(40), registryDigest: `sha256:${"c".repeat(64)}`, @@ -209,6 +210,8 @@ describe("deployment receipt qualification binding", () => { aggregateLimitEvidenceSha256: "5".repeat(64), } as const; const releaseCheck = { + generationFilesystemReadIsolation: + qualified.generationFilesystemReadIsolation, evidenceCommit: qualified.evidenceCommit, sourceCommit: qualified.sourceCommit, qualifiedRunnerReceiptSha256: createHash("sha256") diff --git a/scripts/create-deployment-receipt.ts b/scripts/create-deployment-receipt.ts index da660664..615591df 100644 --- a/scripts/create-deployment-receipt.ts +++ b/scripts/create-deployment-receipt.ts @@ -482,8 +482,7 @@ async function main(): Promise { runnerSourceCommit: args["--source-commit"], runnerImageDigest: args["--registry-digest"], timeoutCleanupReceiptSha256: qualified.timeoutCleanupReceiptSha256, - aggregateLimitEvidenceSha256: - qualified.aggregateLimitEvidenceSha256, + aggregateLimitEvidenceSha256: qualified.aggregateLimitEvidenceSha256, runtimePolicySha256: qualified.runtimePolicySha256, proofDependencyManifestSha256: qualified.proofDependencyManifestSha256, workerArtifactClassification: frozenWorkerRelease.identity.classification, @@ -560,19 +559,19 @@ async function main(): Promise { const dryRun = dryRunWorkerHash(root, paths.dryRun, workerBundleSha256); const configBytes = readFileSync(paths.config); const receipt = DeploymentReceiptSchema.parse({ - schemaVersion: "4", + schemaVersion: "5", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", - generationFilesystemReadIsolation: "PARTIAL", + generationFilesystemReadIsolation: + releaseCheck.generationFilesystemReadIsolation, workerEvidenceCommit: args["--evidence-commit"], runnerSourceCommit: args["--source-commit"], qualifiedRunnerReceiptSha256: sha256(qualifiedBytes), releaseCheckReceiptSha256: sha256(releaseCheckBytes), releaseCheckCheckedAt: releaseCheck.checkedAt, timeoutCleanupReceiptSha256: qualified.timeoutCleanupReceiptSha256, - aggregateLimitEvidenceSha256: - qualified.aggregateLimitEvidenceSha256, + aggregateLimitEvidenceSha256: qualified.aggregateLimitEvidenceSha256, runtimeToolchainSha256: releaseCheck.runtimeToolchainSha256, runtimePolicySha256: releaseCheck.runtimePolicySha256, proofDependencyManifestSha256: releaseCheck.proofDependencyManifestSha256, @@ -602,7 +601,7 @@ async function main(): Promise { workerVersionSha256: sha256(versionBytes), containerStatusSha256: sha256(containersBytes), deployedAt: new Date().toISOString(), - verifierVersion: "counterlab-deployment-v4", + verifierVersion: "counterlab-deployment-v5", }); await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", diff --git a/scripts/generate-contract-schemas.ts b/scripts/generate-contract-schemas.ts index 953b2238..02cac6ec 100644 --- a/scripts/generate-contract-schemas.ts +++ b/scripts/generate-contract-schemas.ts @@ -20,11 +20,14 @@ import { ContainedRuntimeAttestationV1Schema, DeploymentReceiptSchema, DeploymentReceiptV3Schema, + DeploymentReceiptV4Schema, QualifiedRunnerReleaseSchema, QualifiedRunnerReleaseV2Schema, QualifiedRunnerReleaseV3Schema, + QualifiedRunnerReleaseV4Schema, ReleaseCheckReceiptSchema, ReleaseCheckReceiptV1Schema, + ReleaseCheckReceiptV2Schema, ScientificEngineEvidenceCatalogSchema, ScientificEngineRegistrySchema, ScientificEngineRuntimeManifestSchema, @@ -237,7 +240,7 @@ const schemas = [ fileName: "qualified-runner-release-v4.schema.json", id: "https://counterlab.dev/schemas/qualified-runner-release-v4.schema.json", title: "CounterLab qualified runner release v4", - schema: QualifiedRunnerReleaseSchema, + schema: QualifiedRunnerReleaseV4Schema, destinations: [ resolve( root, @@ -245,6 +248,18 @@ const schemas = [ ), ], }, + { + fileName: "qualified-runner-release-v5.schema.json", + id: "https://counterlab.dev/schemas/qualified-runner-release-v5.schema.json", + title: "CounterLab qualified runner release v5", + schema: QualifiedRunnerReleaseSchema, + destinations: [ + resolve( + root, + "scientific-engines/schemas/qualified-runner-release-v5.schema.json", + ), + ], + }, { fileName: "timeout-cleanup-receipt-v1.schema.json", id: "https://counterlab.dev/schemas/timeout-cleanup-receipt-v1.schema.json", @@ -297,7 +312,7 @@ const schemas = [ fileName: "release-check-receipt-v2.schema.json", id: "https://counterlab.dev/schemas/release-check-receipt-v2.schema.json", title: "CounterLab release-check receipt v2", - schema: ReleaseCheckReceiptSchema, + schema: ReleaseCheckReceiptV2Schema, destinations: [ resolve( root, @@ -305,6 +320,18 @@ const schemas = [ ), ], }, + { + fileName: "release-check-receipt-v3.schema.json", + id: "https://counterlab.dev/schemas/release-check-receipt-v3.schema.json", + title: "CounterLab release-check receipt v3", + schema: ReleaseCheckReceiptSchema, + destinations: [ + resolve( + root, + "scientific-engines/schemas/release-check-receipt-v3.schema.json", + ), + ], + }, { fileName: "deployment-receipt-v3.schema.json", id: "https://counterlab.dev/schemas/deployment-receipt-v3.schema.json", @@ -321,7 +348,7 @@ const schemas = [ fileName: "deployment-receipt-v4.schema.json", id: "https://counterlab.dev/schemas/deployment-receipt-v4.schema.json", title: "CounterLab deployment receipt v4", - schema: DeploymentReceiptSchema, + schema: DeploymentReceiptV4Schema, destinations: [ resolve( root, @@ -329,6 +356,18 @@ const schemas = [ ), ], }, + { + fileName: "deployment-receipt-v5.schema.json", + id: "https://counterlab.dev/schemas/deployment-receipt-v5.schema.json", + title: "CounterLab deployment receipt v5", + schema: DeploymentReceiptSchema, + destinations: [ + resolve( + root, + "scientific-engines/schemas/deployment-receipt-v5.schema.json", + ), + ], + }, ]; for (const definition of schemas) { diff --git a/scripts/prepare-qualified-deploy.test.ts b/scripts/prepare-qualified-deploy.test.ts index 4dd1d93e..1105bb4d 100644 --- a/scripts/prepare-qualified-deploy.test.ts +++ b/scripts/prepare-qualified-deploy.test.ts @@ -22,6 +22,7 @@ describe("qualified release evidence-only delta", () => { it("rejects authority schemas, runtime policy, and undeclared evidence files", () => { for (const path of [ "scientific-engines/schemas/qualified-runner-release-v4.schema.json", + "scientific-engines/schemas/qualified-runner-release-v5.schema.json", "services/runner/src/counterlab_runner/contained-runtime-policy.json", "scientific-engines/fixtures/validation/unreviewed.json", "scripts/qualify-runner-release.ts", diff --git a/scripts/prepare-qualified-deploy.ts b/scripts/prepare-qualified-deploy.ts index 89d6601f..9c7be94a 100644 --- a/scripts/prepare-qualified-deploy.ts +++ b/scripts/prepare-qualified-deploy.ts @@ -68,6 +68,7 @@ export type RunnerReleaseObservation = { }; export type QualifiedReleaseObservation = RunnerReleaseObservation & { + generationFilesystemReadIsolation: "OS_ENFORCED"; limitMode: "container-cgroup-and-process-rlimit"; aggregateLimitIntentEnforced: true; aggregateLimitEvidenceSha256: string; @@ -648,6 +649,8 @@ export async function collectQualifiedReleaseObservation(input: { return { ...observation, ...timeout, + generationFilesystemReadIsolation: + receipt.generationFilesystemReadIsolation, }; } @@ -657,8 +660,10 @@ export function createQualifiedRunnerRelease( ): unknown { assertQualifiedObservation(observation); return QualifiedRunnerReleaseSchema.parse({ - schemaVersion: "4", + schemaVersion: "5", status: "VERIFIED", + generationFilesystemReadIsolation: + observation.generationFilesystemReadIsolation, sourceCommit: observation.sourceCommit, sourceArchiveSha256: observation.sourceArchiveSha256, sourceTreeSha256: observation.sourceTreeSha256, @@ -699,7 +704,7 @@ export function createQualifiedRunnerRelease( registryDigest: observation.registryDigest, registryResolvedAt: observation.registryResolvedAt, qualifiedAt, - verifierVersion: "counterlab-release-v4", + verifierVersion: "counterlab-release-v5", }); } diff --git a/scripts/production-smoke.sh b/scripts/production-smoke.sh index c8a19f2c..05fa148b 100755 --- a/scripts/production-smoke.sh +++ b/scripts/production-smoke.sh @@ -481,7 +481,7 @@ stage_started="$(timestamp)" "${CURL_BIN}" --fail --silent --show-error \ --proto '=https' --proto-redir '=https' --max-redirs 0 \ --max-time 30 \ - "${BASE_URL}/api/health" \ + "${BASE_URL}/api/health?readiness=probe" \ >"${WORK_DIR}/health.json" python3 - "${WORK_DIR}/health.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" <<'PY' import json @@ -500,7 +500,7 @@ required = { "liveCodex": "configured", "liveKernel": "configured", "sandbox": "credential-and-privilege-boundary", - "generationFilesystemReadIsolation": "PARTIAL", + "generationFilesystemReadIsolation": "OS_ENFORCED", } for key, expected in required.items(): if health.get(key) != expected: @@ -509,8 +509,8 @@ for key, expected in required.items(): ) if health.get("maintenance") is not False: raise SystemExit("production health reports maintenance mode") -if health.get("readiness") != "not-checked": - raise SystemExit("ordinary production health must not perform a deep readiness probe") +if health.get("readiness") != "ready": + raise SystemExit("production health deep readiness probe did not pass") expected_release = { "status": "bound", "workerVersionId": sys.argv[2], diff --git a/scripts/qualify-runner-release.ts b/scripts/qualify-runner-release.ts index 44e8b312..b4d7a1ca 100644 --- a/scripts/qualify-runner-release.ts +++ b/scripts/qualify-runner-release.ts @@ -733,7 +733,11 @@ async function main(): Promise { adapterManifestDigest: buildReceipt.adapterManifestDigest, adapterOciArchiveSha256: buildReceipt.adapterOciArchiveSha256, }); - const observation = { ...releaseObservation, ...timeoutProof }; + const observation = { + ...releaseObservation, + ...timeoutProof, + generationFilesystemReadIsolation: "OS_ENFORCED" as const, + }; for (const [label, built, observed] of [ [ "source archive", diff --git a/scripts/release-check-receipt.ts b/scripts/release-check-receipt.ts index bb2ee067..30f5daa3 100644 --- a/scripts/release-check-receipt.ts +++ b/scripts/release-check-receipt.ts @@ -188,8 +188,10 @@ export function createReleaseCheckReceipt(input: { ); const checkedAt = input.checkedAt ?? new Date().toISOString(); const receipt = ReleaseCheckReceiptSchema.parse({ - schemaVersion: "2", + schemaVersion: "3", status: "PASSED", + generationFilesystemReadIsolation: + qualified.generationFilesystemReadIsolation, evidenceCommit: qualified.evidenceCommit, sourceCommit: qualified.sourceCommit, qualifiedRunnerReceiptSha256: sha256(input.qualifiedReceiptBytes), @@ -206,7 +208,7 @@ export function createReleaseCheckReceipt(input: { runtimeAdapterSha256: qualified.runtimeAdapterSha256, checks: RELEASE_CHECK_IDS.map((id) => ({ id, status: "PASSED" })), checkedAt, - verifierVersion: "counterlab-release-check-v2", + verifierVersion: "counterlab-release-check-v3", }); return assertReleaseCheckBinding({ qualifiedReceipt: qualified, diff --git a/scripts/test_production_smoke.py b/scripts/test_production_smoke.py index 61b4bdde..8d2c7232 100644 --- a/scripts/test_production_smoke.py +++ b/scripts/test_production_smoke.py @@ -146,11 +146,11 @@ def _deployment_receipt() -> dict[str, object]: runner = "b" * 40 digest = "sha256:" + "c" * 64 return { - "schemaVersion": "4", + "schemaVersion": "5", "status": "DEPLOYED", "workerName": "counterlab", "productionOrigin": "https://counterlab.cserules.workers.dev", - "generationFilesystemReadIsolation": "PARTIAL", + "generationFilesystemReadIsolation": "OS_ENFORCED", "workerEvidenceCommit": worker, "runnerSourceCommit": runner, "qualifiedRunnerReceiptSha256": "1" * 64, @@ -189,7 +189,7 @@ def _deployment_receipt() -> dict[str, object]: "workerVersionSha256": "a" * 64, "containerStatusSha256": "b" * 64, "deployedAt": "2026-07-19T00:00:00.000Z", - "verifierVersion": "counterlab-deployment-v4", + "verifierVersion": "counterlab-deployment-v5", } @@ -314,8 +314,8 @@ def test_control_plane_validators_bind_exact_release_and_maintenance_state() -> "liveCodex": "configured", "liveKernel": "configured", "sandbox": "credential-and-privilege-boundary", - "generationFilesystemReadIsolation": "PARTIAL", - "readiness": "not-checked", + "generationFilesystemReadIsolation": "OS_ENFORCED", + "readiness": "ready", "maintenance": False, "release": release, }, diff --git a/scripts/verify-scientific-engines.sh b/scripts/verify-scientific-engines.sh index 91b4eff4..cf8ae3f0 100755 --- a/scripts/verify-scientific-engines.sh +++ b/scripts/verify-scientific-engines.sh @@ -196,8 +196,9 @@ node -e ' value.status !== "ready" || value.service !== "counterlab-hosted-runner" || value.probe !== "non-root-startup" || + value.generationFilesystemReadIsolation !== "OS_ENFORCED" || JSON.stringify(value.checks) !== - JSON.stringify(["entrypoint", "non-root-user", "immutable-paths", "codex", "python", "setpriv", "writable-roots"]) + JSON.stringify(["entrypoint", "non-root-user", "immutable-paths", "codex", "python", "bubblewrap", "bubblewrap-read-isolation", "setpriv", "writable-roots"]) ) { throw new Error("Runner non-root startup probe returned an invalid sentinel"); } From ce2da766a48b818bff6a3998ca776ae342db5451 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 02:40:34 +0530 Subject: [PATCH 007/143] docs(progress): record isolation release state --- docs/DECISIONS.md | 13 ++++++++ docs/PROGRESS.md | 64 +++++++++++++++++++++++++++++++++++++++ docs/RELEASE_CHECKLIST.md | 6 ++++ 3 files changed, 83 insertions(+) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 654c8cd2..cc603b04 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -771,3 +771,16 @@ New sessions receive a separate 256-bit owner key. - Rehash the manifest, qualification receipt, and release-check receipt before every state-changing deployment step. Any byte drift stops before maintenance mode, migrations, Container rollout, or final Worker upload. + +## 2026-07-20 — Version stronger isolation evidence instead of rewriting receipts + +- Preserve qualified-runner v4 and release-check v2 without a generation + isolation field, and preserve deployment v4 with its exact `PARTIAL` + isolation meaning. +- Use qualified-runner v5, release-check v3, and deployment v5 only for the + strict `OS_ENFORCED` status contract. Never reinterpret historical bytes. +- Treat the status field as necessary but insufficient release proof. A later + version must bind a persisted canonical exact-image isolation sentinel and + its hash through qualification, deployment, production readiness, and smoke. +- Keep `PROCESS_BOUND_PARTIAL` separate: it describes Worker/client artifact + attestation, not the hosted generation filesystem boundary. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 9e8facbb..fded4d12 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2896,3 +2896,67 @@ qualification evidence. submission receipt before final publication. 6. Push the finished feature branch and fast-forward its verified tip into `main` only after the preceding gates are truthfully recorded. + +## Finish Commander isolation checkpoint — 2026-07-20T21:08:25Z + +Deadline: `2026-07-22T00:00:00Z`; **26 hours 51 minutes 35 seconds remained at +this checkpoint**. The feature branch is at committed HEAD `f365132`; the only +worktree entry is the preserved, untracked +`scripts/.fuse_hidden0000a9600000801f`, which was not opened, staged, or +deleted. + +### Newly frozen independent commits + +- `2b94c9d feat(runner): enforce Bubblewrap generation boundary` pins and + verifies Bubblewrap in the runner image, hides non-allowlisted host roots, + keeps only the job workspace writable, validates command/root containment, + and waits for the Codex child to exit before final credential cleanup. +- `f365132 fix(release): version enforced isolation receipts` makes deep runner + readiness require `OS_ENFORCED`, requires that state in production smoke, + preserves historical qualified v4/release-check v2/deployment v4 receipt + semantics byte-for-byte, and introduces strict v5/v3/v5 receipts for the new + isolation claim. + +### Exact local verification + +- Release/schema/contained-runtime focused Vitest: **65/65 passed**. +- Worker/API/client focused Vitest: **186/186 passed**. +- Production-smoke Pytest: **19/19 passed**. +- Reproduction tooling Pytest: **12/12 passed** with both repository Python + source roots supplied explicitly. +- Repository, web, Worker, and hosted-runner TypeScript: **passed**. +- Scoped Prettier, Git whitespace, generated-schema byte comparison, and the + explicit 21-file secret scan: **passed**. +- Broader scientific release-verifier run: **76/79 passed**. The three failures + are expected stale source-bound integrity evidence and remain an explicit + regeneration gate after the final source freeze; they are not relabelled as + passing. +- The first isolated Python commands omitted repository import roots and + failed collection; the correctly scoped invocations above are the recorded + results. + +### Current release truth and ordered queue + +1. Persist a canonical source/tree/image-bound generation-isolation sentinel, + carry its evidence and probe hashes through new receipt versions, and + require the exact probe hash from production runner readiness. The current + v5/v3/v5 status literal is necessary but not yet independently auditable. +2. Obtain immediate owner approval before removing the named FUSE artifact, or + keep release qualification blocked on a non-clean tree. +3. Freeze source, build one exact image, and prove both the new isolation + sentinel and aggregate cgroup-v2 containment. This host still has no + delegated writable aggregate-cgroup subtree, so that gate remains + fail-closed. +4. Regenerate source/image-bound scientific, SBOM, VEX, health, held-out, and + negative-control evidence; issue qualification and release-check receipts + only from that tuple. +5. Verify repository-contained Wrangler identity, deploy only the qualified + tuple, and run public production smoke. No deployment is claimed here. +6. Run the full desktop/mobile/accessibility journey matrix through + CloakBrowser. The current session still has no `CLOAK_CDP_ENDPOINT`; prior + stock-Chromium captures remain design-review evidence only. +7. Keep learner impact `NO_DATA` without consented observations, then align the + exact app, repository, video, Devpost entry, feedback ID, and submission + receipt before publication. +8. Push the completed feature branch, review its exact tip, fast-forward it + into `main`, and push `main` only after the release state is truthful. diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index 0d91baec..9e8c32a9 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -357,3 +357,9 @@ after the final source freeze. the qualified deployed tuple. - [ ] Review and commit each logical slice with the configured user identity, then fast-forward merge `feat/learner-ux-v6.1` into `main`. +- [x] Preserve historical qualified v4/release-check v2/deployment v4 receipt + semantics and introduce strict v5/v3/v5 contracts for `OS_ENFORCED` + generation-filesystem isolation. +- [ ] Persist and hash-bind the exact-image generation-isolation sentinel, + require its exact probe hash from the production runner, and propagate it + through new receipt versions without rewriting v5/v3/v5. From b5d23597708ba67067191fc31a6fbefc4c8351db Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 02:47:12 +0530 Subject: [PATCH 008/143] fix(runner): hash Bubblewrap startup evidence --- .../hosted-runner/src/startup-probe.test.ts | 91 ++++++++++++- services/hosted-runner/src/startup-probe.ts | 127 ++++++++++++++---- 2 files changed, 189 insertions(+), 29 deletions(-) diff --git a/services/hosted-runner/src/startup-probe.test.ts b/services/hosted-runner/src/startup-probe.test.ts index b035039b..700211c5 100644 --- a/services/hosted-runner/src/startup-probe.test.ts +++ b/services/hosted-runner/src/startup-probe.test.ts @@ -1,6 +1,16 @@ import { describe, expect, it, vi } from "vitest"; -import { runHostedRunnerStartupProbe } from "./startup-probe.js"; +import { + hashGenerationIsolationProbe, + runHostedRunnerStartupProbe, +} from "./startup-probe.js"; + +const BUBBLEWRAP_OUTPUT = { + forbiddenHostPathsHidden: true, + parentEnvironmentHidden: true, + workspaceVisible: true, + workspaceWritable: true, +} as const; describe("hosted runner startup probe", () => { it("checks the real runtime chain without propagating credentials", async () => { @@ -14,7 +24,10 @@ describe("hosted runner startup probe", () => { _executable: string, _args: string[], _options: { env: NodeJS.ProcessEnv; timeout: number }, - ) => undefined, + ) => + _executable === "/runtime/bwrap" + ? { stdout: `${JSON.stringify(BUBBLEWRAP_OUTPUT)}\n` } + : { stdout: "" }, ); const writeFile = vi.fn(async () => undefined); const stat = vi.fn(async (path: string) => ({ @@ -67,7 +80,33 @@ describe("hosted runner startup probe", () => { "writable-roots", ], generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationProbe: { + schemaVersion: "1", + probeVersion: "counterlab-generation-isolation-v1", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: [ + "entrypoint", + "non-root-user", + "immutable-paths", + "codex", + "python", + "bubblewrap", + "bubblewrap-read-isolation", + "setpriv", + "writable-roots", + ], + generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrap: BUBBLEWRAP_OUTPUT, + }, + generationIsolationProbeSha256: expect.stringMatching(/^[a-f0-9]{64}$/u), }); + expect(result.generationIsolationProbeSha256).toBe( + hashGenerationIsolationProbe(result.generationIsolationProbe), + ); + expect(result.generationIsolationProbeSha256).toBe( + "cc950c368771f715d7b8807b20c91a04b53065d0ad64ba51024b638f7da329ea", + ); expect(stat.mock.calls.map(([path]) => path)).toEqual([ "/runtime/app", "/runtime/runner.mjs", @@ -147,6 +186,54 @@ describe("hosted runner startup probe", () => { ).rejects.toThrow("not executable"); }); + it.each([ + ["missing stdout", undefined], + ["invalid JSON", { stdout: "not-json" }], + [ + "a false isolation check", + { + stdout: JSON.stringify({ + ...BUBBLEWRAP_OUTPUT, + parentEnvironmentHidden: false, + }), + }, + ], + [ + "an unknown isolation field", + { + stdout: JSON.stringify({ + ...BUBBLEWRAP_OUTPUT, + unverifiedClaim: true, + }), + }, + ], + ])("fails closed when Bubblewrap returns %s", async (_label, probeResult) => { + let invocation = 0; + await expect( + runHostedRunnerStartupProbe({ + bundlePath: "/app/runner.mjs", + access: async () => undefined, + mkdir: async () => undefined as never, + writeFile: async () => undefined, + stat: (async ( + path: Parameters[0], + ) => ({ + uid: 0, + gid: 0, + mode: 0o555, + isDirectory: () => path === "/app", + isFile: () => path === "/app/runner.mjs", + })) as unknown as typeof import("node:fs/promises").stat, + getUid: () => 10001, + getGid: () => 10001, + execute: async () => { + invocation += 1; + return invocation === 4 ? probeResult : { stdout: "" }; + }, + }), + ).rejects.toThrow(); + }); + it("fails closed when PID 1 is not the declared non-root identity", async () => { await expect( runHostedRunnerStartupProbe({ diff --git a/services/hosted-runner/src/startup-probe.ts b/services/hosted-runner/src/startup-probe.ts index a9a3f96b..6c2805fc 100644 --- a/services/hosted-runner/src/startup-probe.ts +++ b/services/hosted-runner/src/startup-probe.ts @@ -1,9 +1,13 @@ import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; import { constants } from "node:fs"; import { access, mkdir, stat, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; +import { canonicalJson } from "@counterlab/session-core"; +import { z } from "zod"; + import { buildContainerBubblewrapProbe } from "./launch-boundary.js"; const execFileAsync = promisify(execFile); @@ -14,6 +18,72 @@ type Execute = ( options: { env: NodeJS.ProcessEnv; timeout: number }, ) => Promise; +const STARTUP_CHECKS = [ + "entrypoint", + "non-root-user", + "immutable-paths", + "codex", + "python", + "bubblewrap", + "bubblewrap-read-isolation", + "setpriv", + "writable-roots", +] as const; + +const BubblewrapReadIsolationOutputSchema = z.strictObject({ + forbiddenHostPathsHidden: z.literal(true), + parentEnvironmentHidden: z.literal(true), + workspaceVisible: z.literal(true), + workspaceWritable: z.literal(true), +}); + +export const GENERATION_ISOLATION_PROBE_VERSION = + "counterlab-generation-isolation-v1" as const; + +export type GenerationIsolationProbePayload = { + schemaVersion: "1"; + probeVersion: typeof GENERATION_ISOLATION_PROBE_VERSION; + service: "counterlab-hosted-runner"; + probe: "non-root-startup"; + checks: typeof STARTUP_CHECKS; + generationFilesystemReadIsolation: "OS_ENFORCED"; + bubblewrap: z.infer; +}; + +function readExecutionStdout(result: unknown): string { + if (typeof result !== "object" || result === null || !("stdout" in result)) { + throw new Error( + "Hosted runner Bubblewrap probe did not return inspectable stdout", + ); + } + const stdout = result.stdout; + if (typeof stdout === "string") return stdout; + if (Buffer.isBuffer(stdout)) return stdout.toString("utf8"); + throw new Error( + "Hosted runner Bubblewrap probe returned an invalid stdout payload", + ); +} + +export function createGenerationIsolationProbePayload( + bubblewrapOutput: unknown, +): GenerationIsolationProbePayload { + return { + schemaVersion: "1", + probeVersion: GENERATION_ISOLATION_PROBE_VERSION, + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: STARTUP_CHECKS, + generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrap: BubblewrapReadIsolationOutputSchema.parse(bubblewrapOutput), + }; +} + +export function hashGenerationIsolationProbe( + payload: GenerationIsolationProbePayload, +): string { + return createHash("sha256").update(canonicalJson(payload)).digest("hex"); +} + export type HostedRunnerStartupProbeOptions = { environment?: NodeJS.ProcessEnv; nodeExecutable?: string; @@ -32,18 +102,10 @@ export type HostedRunnerStartupProbeResult = { status: "ready"; service: "counterlab-hosted-runner"; probe: "non-root-startup"; - checks: [ - "entrypoint", - "non-root-user", - "immutable-paths", - "codex", - "python", - "bubblewrap", - "bubblewrap-read-isolation", - "setpriv", - "writable-roots", - ]; + checks: typeof STARTUP_CHECKS; generationFilesystemReadIsolation: "OS_ENFORCED"; + generationIsolationProbe: GenerationIsolationProbePayload; + generationIsolationProbeSha256: string; }; export async function runHostedRunnerStartupProbe( @@ -59,7 +121,7 @@ export async function runHostedRunnerStartupProbe( const execute = options.execute ?? (async (executable, args, executionOptions) => { - await execFileAsync(executable, args, executionOptions); + return execFileAsync(executable, args, executionOptions); }); const nodeExecutable = options.nodeExecutable ?? process.execPath; const bundlePath = options.bundlePath ?? new URL(import.meta.url).pathname; @@ -158,26 +220,37 @@ export async function runHostedRunnerStartupProbe( codexRoot, workspace: probeWorkspace, }); - await execute(isolationProbe.command, isolationProbe.args, { - env: isolationProbe.environment, - timeout: 30_000, - }); + const isolationProbeExecution = await execute( + isolationProbe.command, + isolationProbe.args, + { + env: isolationProbe.environment, + timeout: 30_000, + }, + ); + let bubblewrapOutput: unknown; + try { + bubblewrapOutput = JSON.parse( + readExecutionStdout(isolationProbeExecution).trim(), + ) as unknown; + } catch (error) { + throw new Error( + "Hosted runner Bubblewrap probe returned invalid JSON evidence", + { cause: error }, + ); + } + const generationIsolationProbe = + createGenerationIsolationProbePayload(bubblewrapOutput); return { status: "ready", service: "counterlab-hosted-runner", probe: "non-root-startup", - checks: [ - "entrypoint", - "non-root-user", - "immutable-paths", - "codex", - "python", - "bubblewrap", - "bubblewrap-read-isolation", - "setpriv", - "writable-roots", - ], + checks: STARTUP_CHECKS, generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationProbe, + generationIsolationProbeSha256: hashGenerationIsolationProbe( + generationIsolationProbe, + ), }; } From fc1c58101e66ecb17346dbf9862bd7d86e9f6834 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 02:50:58 +0530 Subject: [PATCH 009/143] fix(runner): bind pinned Bubblewrap version --- .../hosted-runner/src/startup-probe.test.ts | 32 +++++++++++++++---- services/hosted-runner/src/startup-probe.ts | 24 ++++++++++++-- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/services/hosted-runner/src/startup-probe.test.ts b/services/hosted-runner/src/startup-probe.test.ts index 700211c5..a9068cb6 100644 --- a/services/hosted-runner/src/startup-probe.test.ts +++ b/services/hosted-runner/src/startup-probe.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { + createGenerationIsolationProbePayload, hashGenerationIsolationProbe, runHostedRunnerStartupProbe, } from "./startup-probe.js"; @@ -25,9 +26,11 @@ describe("hosted runner startup probe", () => { _args: string[], _options: { env: NodeJS.ProcessEnv; timeout: number }, ) => - _executable === "/runtime/bwrap" - ? { stdout: `${JSON.stringify(BUBBLEWRAP_OUTPUT)}\n` } - : { stdout: "" }, + _executable === "/runtime/bwrap" && _args[0] === "--version" + ? { stdout: "bubblewrap 0.11.0\n" } + : _executable === "/runtime/bwrap" + ? { stdout: `${JSON.stringify(BUBBLEWRAP_OUTPUT)}\n` } + : { stdout: "" }, ); const writeFile = vi.fn(async () => undefined); const stat = vi.fn(async (path: string) => ({ @@ -97,6 +100,7 @@ describe("hosted runner startup probe", () => { "writable-roots", ], generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrapVersion: "0.11.0", bubblewrap: BUBBLEWRAP_OUTPUT, }, generationIsolationProbeSha256: expect.stringMatching(/^[a-f0-9]{64}$/u), @@ -105,7 +109,7 @@ describe("hosted runner startup probe", () => { hashGenerationIsolationProbe(result.generationIsolationProbe), ); expect(result.generationIsolationProbeSha256).toBe( - "cc950c368771f715d7b8807b20c91a04b53065d0ad64ba51024b638f7da329ea", + "700cc58bedc163846e3854415170f49f55da9fd3ba316cc4967747d5268199dc", ); expect(stat.mock.calls.map(([path]) => path)).toEqual([ "/runtime/app", @@ -137,13 +141,17 @@ describe("hosted runner startup probe", () => { "approved\n", { encoding: "utf8", flag: "w", mode: 0o600 }, ); - expect(execute).toHaveBeenCalledTimes(4); + expect(execute).toHaveBeenCalledTimes(5); for (const [, , options] of execute.mock.calls) { expect(options.env).not.toHaveProperty("CODEX_AUTH_JSON"); expect(options.env).not.toHaveProperty("OPENAI_API_KEY"); } + expect(execute.mock.calls[3]?.slice(0, 2)).toEqual([ + "/runtime/bwrap", + ["--version"], + ]); const [probeExecutable, probeArgs, probeOptions] = - execute.mock.calls[3] ?? []; + execute.mock.calls[4] ?? []; expect(probeExecutable).toBe("/runtime/bwrap"); expect(probeArgs).toContain("--unshare-user"); expect(probeArgs).toContain("--unshare-pid"); @@ -228,12 +236,22 @@ describe("hosted runner startup probe", () => { getGid: () => 10001, execute: async () => { invocation += 1; - return invocation === 4 ? probeResult : { stdout: "" }; + if (invocation === 4) return { stdout: "bubblewrap 0.11.0\n" }; + return invocation === 5 ? probeResult : { stdout: "" }; }, }), ).rejects.toThrow(); }); + it("rejects a Bubblewrap version that differs from the pinned image", () => { + expect(() => + createGenerationIsolationProbePayload( + BUBBLEWRAP_OUTPUT, + "bubblewrap 0.11.1", + ), + ).toThrow("requires bubblewrap 0.11.0; observed bubblewrap 0.11.1"); + }); + it("fails closed when PID 1 is not the declared non-root identity", async () => { await expect( runHostedRunnerStartupProbe({ diff --git a/services/hosted-runner/src/startup-probe.ts b/services/hosted-runner/src/startup-probe.ts index 6c2805fc..c29a8dda 100644 --- a/services/hosted-runner/src/startup-probe.ts +++ b/services/hosted-runner/src/startup-probe.ts @@ -37,6 +37,8 @@ const BubblewrapReadIsolationOutputSchema = z.strictObject({ workspaceWritable: z.literal(true), }); +const EXPECTED_BUBBLEWRAP_VERSION = "bubblewrap 0.11.0" as const; + export const GENERATION_ISOLATION_PROBE_VERSION = "counterlab-generation-isolation-v1" as const; @@ -47,6 +49,7 @@ export type GenerationIsolationProbePayload = { probe: "non-root-startup"; checks: typeof STARTUP_CHECKS; generationFilesystemReadIsolation: "OS_ENFORCED"; + bubblewrapVersion: "0.11.0"; bubblewrap: z.infer; }; @@ -66,7 +69,13 @@ function readExecutionStdout(result: unknown): string { export function createGenerationIsolationProbePayload( bubblewrapOutput: unknown, + bubblewrapVersionOutput: string, ): GenerationIsolationProbePayload { + if (bubblewrapVersionOutput.trim() !== EXPECTED_BUBBLEWRAP_VERSION) { + throw new Error( + `Hosted runner requires ${EXPECTED_BUBBLEWRAP_VERSION}; observed ${bubblewrapVersionOutput.trim() || "no version"}`, + ); + } return { schemaVersion: "1", probeVersion: GENERATION_ISOLATION_PROBE_VERSION, @@ -74,6 +83,7 @@ export function createGenerationIsolationProbePayload( probe: "non-root-startup", checks: STARTUP_CHECKS, generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrapVersion: "0.11.0", bubblewrap: BubblewrapReadIsolationOutputSchema.parse(bubblewrapOutput), }; } @@ -215,6 +225,14 @@ export async function runHostedRunnerStartupProbe( env: childEnvironment, timeout: 30_000, }); + const bubblewrapVersionExecution = await execute( + bwrapExecutable, + ["--version"], + { + env: childEnvironment, + timeout: 30_000, + }, + ); const isolationProbe = buildContainerBubblewrapProbe({ bwrapExecutable, codexRoot, @@ -239,8 +257,10 @@ export async function runHostedRunnerStartupProbe( { cause: error }, ); } - const generationIsolationProbe = - createGenerationIsolationProbePayload(bubblewrapOutput); + const generationIsolationProbe = createGenerationIsolationProbePayload( + bubblewrapOutput, + readExecutionStdout(bubblewrapVersionExecution), + ); return { status: "ready", From 9a15ec4cca27f7bd840bbc4855f0965964cccb67 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 03:03:43 +0530 Subject: [PATCH 010/143] feat(release): define image isolation evidence --- .../scientific-engine-registry/src/schema.ts | 229 ++++++++- .../scientific-engine-registry/src/types.ts | 28 ++ .../test/generation-isolation-schema.test.ts | 169 +++++++ .../schemas/deployment-receipt-v6.schema.json | 245 ++++++++++ ...neration-isolation-evidence-v1.schema.json | 185 ++++++++ .../qualified-runner-release-v6.schema.json | 434 ++++++++++++++++++ .../release-check-receipt-v4.schema.json | 160 +++++++ scripts/generate-contract-schemas.ts | 85 +++- scripts/generation-isolation-evidence.test.ts | 164 +++++++ scripts/generation-isolation-evidence.ts | 182 ++++++++ 10 files changed, 1872 insertions(+), 9 deletions(-) create mode 100644 packages/scientific-engine-registry/test/generation-isolation-schema.test.ts create mode 100644 scientific-engines/schemas/deployment-receipt-v6.schema.json create mode 100644 scientific-engines/schemas/generation-isolation-evidence-v1.schema.json create mode 100644 scientific-engines/schemas/qualified-runner-release-v6.schema.json create mode 100644 scientific-engines/schemas/release-check-receipt-v4.schema.json create mode 100644 scripts/generation-isolation-evidence.test.ts create mode 100644 scripts/generation-isolation-evidence.ts diff --git a/packages/scientific-engine-registry/src/schema.ts b/packages/scientific-engine-registry/src/schema.ts index 245bebff..07b071d6 100644 --- a/packages/scientific-engine-registry/src/schema.ts +++ b/packages/scientific-engine-registry/src/schema.ts @@ -616,6 +616,70 @@ export const TimeoutCleanupReceiptSchema = z } }); +export const GENERATION_ISOLATION_PROBE_VERSION = + "counterlab-generation-isolation-v1" as const; + +export const GENERATION_ISOLATION_MOUNT_POLICY_VERSION = + "counterlab-bwrap-mount-policy-v1" as const; + +export const GenerationIsolationProbePayloadSchema = z.strictObject({ + schemaVersion: z.literal("1"), + probeVersion: z.literal(GENERATION_ISOLATION_PROBE_VERSION), + service: z.literal("counterlab-hosted-runner"), + probe: z.literal("non-root-startup"), + checks: z.tuple([ + z.literal("entrypoint"), + z.literal("non-root-user"), + z.literal("immutable-paths"), + z.literal("codex"), + z.literal("python"), + z.literal("bubblewrap"), + z.literal("bubblewrap-read-isolation"), + z.literal("setpriv"), + z.literal("writable-roots"), + ]), + generationFilesystemReadIsolation: z.literal("OS_ENFORCED"), + bubblewrapVersion: z.literal("0.11.0"), + bubblewrap: z.strictObject({ + forbiddenHostPathsHidden: z.literal(true), + parentEnvironmentHidden: z.literal(true), + workspaceVisible: z.literal(true), + workspaceWritable: z.literal(true), + }), +}); + +export const GenerationIsolationEvidenceV1Schema = z + .strictObject({ + schemaVersion: z.literal("1"), + status: z.literal("VERIFIED"), + generationFilesystemReadIsolation: z.literal("OS_ENFORCED"), + sourceCommit: GitCommitSchema, + sourceTreeSha256: Sha256Schema, + localImageTag: z.string().regex(/^counterlab-runner:git-[a-f0-9]{40}$/), + localImageDigest: OciDigestSchema, + imageUser: z.literal("10001:10001"), + mountPolicyVersion: z.literal(GENERATION_ISOLATION_MOUNT_POLICY_VERSION), + probePayload: GenerationIsolationProbePayloadSchema, + probePayloadSha256: Sha256Schema, + verifiedAt: z.iso.datetime({ offset: true }), + verifierVersion: z.literal("counterlab-generation-isolation-evidence-v1"), + }) + .superRefine((evidence, context) => { + if ( + evidence.localImageTag !== + `counterlab-runner:git-${evidence.sourceCommit}` + ) { + context.addIssue({ + code: "custom", + path: ["localImageTag"], + message: "generation-isolation image tag must bind the source commit", + }); + } + }); + +export const GenerationIsolationEvidenceSchema = + GenerationIsolationEvidenceV1Schema; + export const QualifiedRunnerReleaseV1Schema = z .strictObject({ schemaVersion: z.literal("1"), @@ -966,7 +1030,7 @@ export const QualifiedRunnerReleaseV4Schema = z } }); -export const QualifiedRunnerReleaseSchema = z +export const QualifiedRunnerReleaseV5Schema = z .strictObject({ ...QualifiedRunnerReleaseV4Schema.shape, schemaVersion: z.literal("5"), @@ -994,6 +1058,77 @@ export const QualifiedRunnerReleaseSchema = z } }); +export const QualifiedRunnerReleaseV6Schema = z + .strictObject({ + ...QualifiedRunnerReleaseV5Schema.shape, + schemaVersion: z.literal("6"), + generationIsolationEvidence: GenerationIsolationEvidenceV1Schema, + generationIsolationEvidenceSha256: Sha256Schema, + generationIsolationProbeSha256: Sha256Schema, + generationIsolationVerifiedAt: z.iso.datetime({ offset: true }), + verifierVersion: z.literal("counterlab-release-v6"), + }) + .superRefine((release, context) => { + const { + generationIsolationEvidence: _generationIsolationEvidence, + generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, + generationIsolationProbeSha256: _generationIsolationProbeSha256, + generationIsolationVerifiedAt: _generationIsolationVerifiedAt, + ...legacyRelease + } = release; + const result = QualifiedRunnerReleaseV5Schema.safeParse({ + ...legacyRelease, + schemaVersion: "5", + verifierVersion: "counterlab-release-v5", + }); + if (!result.success) { + for (const issue of result.error.issues) { + context.addIssue({ + code: "custom", + path: issue.path, + message: issue.message, + }); + } + } + if ( + Date.parse(release.generationIsolationVerifiedAt) > + Date.parse(release.qualifiedAt) + ) { + context.addIssue({ + code: "custom", + path: ["generationIsolationVerifiedAt"], + message: "generation-isolation proof must precede qualification", + }); + } + const evidence = release.generationIsolationEvidence; + for (const [field, expected, observed] of [ + ["sourceCommit", release.sourceCommit, evidence.sourceCommit], + ["sourceTreeSha256", release.sourceTreeSha256, evidence.sourceTreeSha256], + ["localImageTag", release.localImageTag, evidence.localImageTag], + ["localImageDigest", release.localImageDigest, evidence.localImageDigest], + [ + "generationIsolationProbeSha256", + release.generationIsolationProbeSha256, + evidence.probePayloadSha256, + ], + [ + "generationIsolationVerifiedAt", + release.generationIsolationVerifiedAt, + evidence.verifiedAt, + ], + ] as const) { + if (expected !== observed) { + context.addIssue({ + code: "custom", + path: [field], + message: `${field} must match embedded generation-isolation evidence`, + }); + } + } + }); + +export const QualifiedRunnerReleaseSchema = QualifiedRunnerReleaseV5Schema; + export const RELEASE_CHECK_IDS = [ "test-all", "leakage-mutations", @@ -1141,7 +1276,7 @@ export const ReleaseCheckReceiptV2Schema = z } }); -export const ReleaseCheckReceiptSchema = z +export const ReleaseCheckReceiptV3Schema = z .strictObject({ ...ReleaseCheckReceiptV2Schema.shape, schemaVersion: z.literal("3"), @@ -1169,6 +1304,50 @@ export const ReleaseCheckReceiptSchema = z } }); +export const ReleaseCheckReceiptV4Schema = z + .strictObject({ + ...ReleaseCheckReceiptV3Schema.shape, + schemaVersion: z.literal("4"), + generationIsolationEvidenceSha256: Sha256Schema, + generationIsolationProbeSha256: Sha256Schema, + generationIsolationVerifiedAt: z.iso.datetime({ offset: true }), + verifierVersion: z.literal("counterlab-release-check-v4"), + }) + .superRefine((receipt, context) => { + const { + generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, + generationIsolationProbeSha256: _generationIsolationProbeSha256, + generationIsolationVerifiedAt: _generationIsolationVerifiedAt, + ...legacyReceipt + } = receipt; + const result = ReleaseCheckReceiptV3Schema.safeParse({ + ...legacyReceipt, + schemaVersion: "3", + verifierVersion: "counterlab-release-check-v3", + }); + if (!result.success) { + for (const issue of result.error.issues) { + context.addIssue({ + code: "custom", + path: issue.path, + message: issue.message, + }); + } + } + if ( + Date.parse(receipt.generationIsolationVerifiedAt) > + Date.parse(receipt.checkedAt) + ) { + context.addIssue({ + code: "custom", + path: ["generationIsolationVerifiedAt"], + message: "generation-isolation proof must precede release checks", + }); + } + }); + +export const ReleaseCheckReceiptSchema = ReleaseCheckReceiptV3Schema; + export const DeploymentReceiptV3Schema = z .strictObject({ schemaVersion: z.literal("3"), @@ -1363,7 +1542,7 @@ export const DeploymentReceiptV4Schema = z } }); -export const DeploymentReceiptSchema = z +export const DeploymentReceiptV5Schema = z .strictObject({ ...DeploymentReceiptV4Schema.shape, schemaVersion: z.literal("5"), @@ -1387,3 +1566,47 @@ export const DeploymentReceiptSchema = z } } }); + +export const DeploymentReceiptV6Schema = z + .strictObject({ + ...DeploymentReceiptV5Schema.shape, + schemaVersion: z.literal("6"), + generationIsolationEvidenceSha256: Sha256Schema, + generationIsolationProbeSha256: Sha256Schema, + generationIsolationVerifiedAt: z.iso.datetime({ offset: true }), + verifierVersion: z.literal("counterlab-deployment-v6"), + }) + .superRefine((receipt, context) => { + const { + generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, + generationIsolationProbeSha256: _generationIsolationProbeSha256, + generationIsolationVerifiedAt: _generationIsolationVerifiedAt, + ...legacyReceipt + } = receipt; + const result = DeploymentReceiptV5Schema.safeParse({ + ...legacyReceipt, + schemaVersion: "5", + verifierVersion: "counterlab-deployment-v5", + }); + if (!result.success) { + for (const issue of result.error.issues) { + context.addIssue({ + code: "custom", + path: issue.path, + message: issue.message, + }); + } + } + if ( + Date.parse(receipt.generationIsolationVerifiedAt) > + Date.parse(receipt.deployedAt) + ) { + context.addIssue({ + code: "custom", + path: ["generationIsolationVerifiedAt"], + message: "generation-isolation proof must precede deployment", + }); + } + }); + +export const DeploymentReceiptSchema = DeploymentReceiptV5Schema; diff --git a/packages/scientific-engine-registry/src/types.ts b/packages/scientific-engine-registry/src/types.ts index d062c5ac..8a801a6a 100644 --- a/packages/scientific-engine-registry/src/types.ts +++ b/packages/scientific-engine-registry/src/types.ts @@ -6,6 +6,11 @@ import { DeploymentReceiptSchema, DeploymentReceiptV3Schema, DeploymentReceiptV4Schema, + DeploymentReceiptV5Schema, + DeploymentReceiptV6Schema, + GenerationIsolationEvidenceSchema, + GenerationIsolationEvidenceV1Schema, + GenerationIsolationProbePayloadSchema, ScientificEngineDescriptorSchema, ScientificEngineEvidenceCatalogSchema, ScientificEngineEvidenceKindSchema, @@ -19,9 +24,13 @@ import { QualifiedRunnerReleaseV2Schema, QualifiedRunnerReleaseV3Schema, QualifiedRunnerReleaseV4Schema, + QualifiedRunnerReleaseV5Schema, + QualifiedRunnerReleaseV6Schema, ReleaseCheckReceiptSchema, ReleaseCheckReceiptV1Schema, ReleaseCheckReceiptV2Schema, + ReleaseCheckReceiptV3Schema, + ReleaseCheckReceiptV4Schema, SubjectPackAuthorityBindingSchema, SubjectPackEngineBindingSchema, SubjectPackEngineBindingsSchema, @@ -37,6 +46,17 @@ export type ContainedRuntimeAttestationV1 = z.infer< export type DeploymentReceipt = z.infer; export type DeploymentReceiptV3 = z.infer; export type DeploymentReceiptV4 = z.infer; +export type DeploymentReceiptV5 = z.infer; +export type DeploymentReceiptV6 = z.infer; +export type GenerationIsolationEvidence = z.infer< + typeof GenerationIsolationEvidenceSchema +>; +export type GenerationIsolationEvidenceV1 = z.infer< + typeof GenerationIsolationEvidenceV1Schema +>; +export type GenerationIsolationProbePayload = z.infer< + typeof GenerationIsolationProbePayloadSchema +>; export type ScientificEngineRole = z.infer; export type ScientificEngineDescriptor = z.infer< @@ -84,7 +104,15 @@ export type QualifiedRunnerReleaseV3 = z.infer< export type QualifiedRunnerReleaseV4 = z.infer< typeof QualifiedRunnerReleaseV4Schema >; +export type QualifiedRunnerReleaseV5 = z.infer< + typeof QualifiedRunnerReleaseV5Schema +>; +export type QualifiedRunnerReleaseV6 = z.infer< + typeof QualifiedRunnerReleaseV6Schema +>; export type TimeoutCleanupReceipt = z.infer; export type ReleaseCheckReceipt = z.infer; export type ReleaseCheckReceiptV1 = z.infer; export type ReleaseCheckReceiptV2 = z.infer; +export type ReleaseCheckReceiptV3 = z.infer; +export type ReleaseCheckReceiptV4 = z.infer; diff --git a/packages/scientific-engine-registry/test/generation-isolation-schema.test.ts b/packages/scientific-engine-registry/test/generation-isolation-schema.test.ts new file mode 100644 index 00000000..33b0170a --- /dev/null +++ b/packages/scientific-engine-registry/test/generation-isolation-schema.test.ts @@ -0,0 +1,169 @@ +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { + DeploymentReceiptV6Schema, + GenerationIsolationEvidenceV1Schema, + QualifiedRunnerReleaseV6Schema, + ReleaseCheckReceiptV4Schema, +} from "../src/index.js"; + +const sourceCommit = "a".repeat(40); + +function probePayload() { + return { + schemaVersion: "1", + probeVersion: "counterlab-generation-isolation-v1", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: [ + "entrypoint", + "non-root-user", + "immutable-paths", + "codex", + "python", + "bubblewrap", + "bubblewrap-read-isolation", + "setpriv", + "writable-roots", + ], + generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrapVersion: "0.11.0", + bubblewrap: { + forbiddenHostPathsHidden: true, + parentEnvironmentHidden: true, + workspaceVisible: true, + workspaceWritable: true, + }, + } as const; +} + +function evidence() { + return { + schemaVersion: "1", + status: "VERIFIED", + generationFilesystemReadIsolation: "OS_ENFORCED", + sourceCommit, + sourceTreeSha256: "b".repeat(64), + localImageTag: `counterlab-runner:git-${sourceCommit}`, + localImageDigest: `sha256:${"c".repeat(64)}`, + imageUser: "10001:10001", + mountPolicyVersion: "counterlab-bwrap-mount-policy-v1", + probePayload: probePayload(), + probePayloadSha256: "d".repeat(64), + verifiedAt: "2026-07-20T21:30:00.000Z", + verifierVersion: "counterlab-generation-isolation-evidence-v1", + } as const; +} + +describe("generation isolation evidence schema", () => { + it("accepts only the pinned, source-bound, all-true probe envelope", () => { + expect(GenerationIsolationEvidenceV1Schema.parse(evidence())).toEqual( + evidence(), + ); + expect(() => + GenerationIsolationEvidenceV1Schema.parse({ + ...evidence(), + localImageTag: `counterlab-runner:git-${"e".repeat(40)}`, + }), + ).toThrow(/bind the source commit/u); + expect(() => + GenerationIsolationEvidenceV1Schema.parse({ + ...evidence(), + probePayload: { + ...probePayload(), + bubblewrap: { + ...probePayload().bubblewrap, + forbiddenHostPathsHidden: false, + }, + }, + }), + ).toThrow(); + expect(() => + GenerationIsolationEvidenceV1Schema.parse({ + ...evidence(), + unverifiedClaim: true, + }), + ).toThrow(); + }); + + it("adds evidence bindings only in the next receipt versions", () => { + for (const [schema, version] of [ + [QualifiedRunnerReleaseV6Schema, "6"], + [ReleaseCheckReceiptV4Schema, "4"], + [DeploymentReceiptV6Schema, "6"], + ] as const) { + const jsonSchema = z.toJSONSchema(schema) as { + properties?: Record; + required?: string[]; + }; + expect(jsonSchema.properties?.schemaVersion?.const).toBe(version); + expect(jsonSchema.required).toEqual( + expect.arrayContaining([ + ...(version === "6" && schema === QualifiedRunnerReleaseV6Schema + ? ["generationIsolationEvidence"] + : []), + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt", + ]), + ); + } + }); + + it("publishes the evidence and next-version receipt schemas", () => { + for (const path of [ + "generation-isolation-evidence-v1.schema.json", + "qualified-runner-release-v6.schema.json", + "release-check-receipt-v4.schema.json", + "deployment-receipt-v6.schema.json", + ]) { + const absolute = resolve( + process.cwd(), + "scientific-engines/schemas", + path, + ); + expect(existsSync(absolute)).toBe(true); + const schema = JSON.parse(readFileSync(absolute, "utf8")) as { + additionalProperties?: boolean; + }; + expect(schema.additionalProperties).toBe(false); + } + + const evidenceSchema = JSON.parse( + readFileSync( + resolve( + process.cwd(), + "scientific-engines/schemas/generation-isolation-evidence-v1.schema.json", + ), + "utf8", + ), + ) as { + properties: { + probePayload: { + properties: { + checks: { + minItems?: number; + maxItems?: number; + items?: boolean; + prefixItems?: unknown[]; + }; + }; + }; + }; + }; + expect( + evidenceSchema.properties.probePayload.properties.checks, + ).toMatchObject({ + minItems: 9, + maxItems: 9, + items: false, + }); + expect( + evidenceSchema.properties.probePayload.properties.checks.prefixItems, + ).toHaveLength(9); + }); +}); diff --git a/scientific-engines/schemas/deployment-receipt-v6.schema.json b/scientific-engines/schemas/deployment-receipt-v6.schema.json new file mode 100644 index 00000000..6ec0f0f8 --- /dev/null +++ b/scientific-engines/schemas/deployment-receipt-v6.schema.json @@ -0,0 +1,245 @@ +{ + "$id": "https://counterlab.dev/schemas/deployment-receipt-v6.schema.json", + "title": "CounterLab deployment receipt v6", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "6" + }, + "status": { + "type": "string", + "const": "DEPLOYED" + }, + "workerName": { + "type": "string", + "const": "counterlab" + }, + "productionOrigin": { + "type": "string", + "const": "https://counterlab.cserules.workers.dev" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "workerEvidenceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "runnerSourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "qualifiedRunnerReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckCheckedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "timeoutCleanupReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "aggregateLimitEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeToolchainSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimePolicySha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "proofDependencyManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeAdapterSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "workerVersionId": { + "type": "string", + "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" + }, + "workerTag": { + "type": "string", + "pattern": "^git-[a-f0-9]{40}$" + }, + "workerMessage": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "containerApplicationId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,128}$" + }, + "containerApplicationVersion": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "containerImage": { + "type": "string", + "pattern": "^registry\\.cloudflare\\.com\\/[A-Za-z0-9_-]{3,64}\\/counterlab-runner@sha256:[a-f0-9]{64}$" + }, + "containerState": { + "type": "string", + "enum": [ + "active", + "ready" + ] + }, + "containerImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "workerArtifactClassification": { + "type": "string", + "const": "PROCESS_BOUND_PARTIAL" + }, + "workerArtifactManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "deployConfigSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "workerBundleSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientAssetsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientAssetCount": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "clientPublicAssetsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientPublicAssetCount": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "viteVersion": { + "type": "string", + "const": "8.1.4" + }, + "wranglerVersion": { + "type": "string", + "const": "4.110.0" + }, + "dryRunSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "dryRunFileCount": { + "type": "number", + "const": 1 + }, + "deploymentStatusSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "workerVersionSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "containerStatusSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "deployedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-deployment-v6" + }, + "generationIsolationEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationProbeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "status", + "workerName", + "productionOrigin", + "generationFilesystemReadIsolation", + "workerEvidenceCommit", + "runnerSourceCommit", + "qualifiedRunnerReceiptSha256", + "releaseCheckReceiptSha256", + "releaseCheckCheckedAt", + "timeoutCleanupReceiptSha256", + "aggregateLimitEvidenceSha256", + "runtimeToolchainSha256", + "runtimePolicySha256", + "proofDependencyManifestSha256", + "runtimeAdapterSha256", + "adapterImageDigest", + "workerVersionId", + "workerTag", + "workerMessage", + "containerApplicationId", + "containerApplicationVersion", + "containerImage", + "containerState", + "containerImageDigest", + "workerArtifactClassification", + "workerArtifactManifestSha256", + "deployConfigSha256", + "workerBundleSha256", + "clientAssetsSha256", + "clientAssetCount", + "clientPublicAssetsSha256", + "clientPublicAssetCount", + "viteVersion", + "wranglerVersion", + "dryRunSha256", + "dryRunFileCount", + "deploymentStatusSha256", + "workerVersionSha256", + "containerStatusSha256", + "deployedAt", + "verifierVersion", + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt" + ], + "additionalProperties": false +} diff --git a/scientific-engines/schemas/generation-isolation-evidence-v1.schema.json b/scientific-engines/schemas/generation-isolation-evidence-v1.schema.json new file mode 100644 index 00000000..8e3d0ddc --- /dev/null +++ b/scientific-engines/schemas/generation-isolation-evidence-v1.schema.json @@ -0,0 +1,185 @@ +{ + "$id": "https://counterlab.dev/schemas/generation-isolation-evidence-v1.schema.json", + "title": "CounterLab generation isolation evidence v1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "1" + }, + "status": { + "type": "string", + "const": "VERIFIED" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "localImageTag": { + "type": "string", + "pattern": "^counterlab-runner:git-[a-f0-9]{40}$" + }, + "localImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "imageUser": { + "type": "string", + "const": "10001:10001" + }, + "mountPolicyVersion": { + "type": "string", + "const": "counterlab-bwrap-mount-policy-v1" + }, + "probePayload": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "1" + }, + "probeVersion": { + "type": "string", + "const": "counterlab-generation-isolation-v1" + }, + "service": { + "type": "string", + "const": "counterlab-hosted-runner" + }, + "probe": { + "type": "string", + "const": "non-root-startup" + }, + "checks": { + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "entrypoint" + }, + { + "type": "string", + "const": "non-root-user" + }, + { + "type": "string", + "const": "immutable-paths" + }, + { + "type": "string", + "const": "codex" + }, + { + "type": "string", + "const": "python" + }, + { + "type": "string", + "const": "bubblewrap" + }, + { + "type": "string", + "const": "bubblewrap-read-isolation" + }, + { + "type": "string", + "const": "setpriv" + }, + { + "type": "string", + "const": "writable-roots" + } + ], + "minItems": 9, + "maxItems": 9, + "items": false + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "bubblewrapVersion": { + "type": "string", + "const": "0.11.0" + }, + "bubblewrap": { + "type": "object", + "properties": { + "forbiddenHostPathsHidden": { + "type": "boolean", + "const": true + }, + "parentEnvironmentHidden": { + "type": "boolean", + "const": true + }, + "workspaceVisible": { + "type": "boolean", + "const": true + }, + "workspaceWritable": { + "type": "boolean", + "const": true + } + }, + "required": [ + "forbiddenHostPathsHidden", + "parentEnvironmentHidden", + "workspaceVisible", + "workspaceWritable" + ], + "additionalProperties": false + } + }, + "required": [ + "schemaVersion", + "probeVersion", + "service", + "probe", + "checks", + "generationFilesystemReadIsolation", + "bubblewrapVersion", + "bubblewrap" + ], + "additionalProperties": false + }, + "probePayloadSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "verifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-generation-isolation-evidence-v1" + } + }, + "required": [ + "schemaVersion", + "status", + "generationFilesystemReadIsolation", + "sourceCommit", + "sourceTreeSha256", + "localImageTag", + "localImageDigest", + "imageUser", + "mountPolicyVersion", + "probePayload", + "probePayloadSha256", + "verifiedAt", + "verifierVersion" + ], + "additionalProperties": false +} diff --git a/scientific-engines/schemas/qualified-runner-release-v6.schema.json b/scientific-engines/schemas/qualified-runner-release-v6.schema.json new file mode 100644 index 00000000..b3a79472 --- /dev/null +++ b/scientific-engines/schemas/qualified-runner-release-v6.schema.json @@ -0,0 +1,434 @@ +{ + "$id": "https://counterlab.dev/schemas/qualified-runner-release-v6.schema.json", + "title": "CounterLab qualified runner release v6", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "6" + }, + "status": { + "type": "string", + "const": "VERIFIED" + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceArchiveSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "sourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "dockerfileSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "localImageTag": { + "type": "string", + "pattern": "^counterlab-runner:git-[a-f0-9]{40}$" + }, + "localImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "ociRevision": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "ociSourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "engineAuthorityHash": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeManifestHash": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeToolchainSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimePolicySha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "proofDependencyManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "toolchainLockSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeAdapterSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "buildctlSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "buildkitdSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "buildkitConfigSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterDockerfileSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterImageTag": { + "type": "string", + "pattern": "^counterlab-adapter:git-[a-f0-9]{40}$" + }, + "adapterImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "adapterManifestDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "adapterOciArchiveSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterOciRevision": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "adapterOciSourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "limitMode": { + "type": "string", + "const": "container-cgroup-and-process-rlimit" + }, + "aggregateLimitIntentEnforced": { + "type": "boolean", + "const": true + }, + "aggregateLimitEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutCleanupReceipt": { + "type": "string", + "minLength": 1, + "maxLength": 300 + }, + "timeoutCleanupReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutCleanupPayloadSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutRunControlReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutRootlessReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "timeoutRuntimeSessionId": { + "type": "string", + "pattern": "^rt-[a-z0-9][a-z0-9-]{7,13}$" + }, + "timeoutVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "evidenceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "registryImage": { + "type": "string", + "pattern": "^registry\\.cloudflare\\.com\\/[A-Za-z0-9_-]{3,64}\\/counterlab-runner:git-[a-f0-9]{40}$" + }, + "registryDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "registryResolvedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "qualifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-release-v6" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "generationIsolationEvidence": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "1" + }, + "status": { + "type": "string", + "const": "VERIFIED" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "localImageTag": { + "type": "string", + "pattern": "^counterlab-runner:git-[a-f0-9]{40}$" + }, + "localImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "imageUser": { + "type": "string", + "const": "10001:10001" + }, + "mountPolicyVersion": { + "type": "string", + "const": "counterlab-bwrap-mount-policy-v1" + }, + "probePayload": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "1" + }, + "probeVersion": { + "type": "string", + "const": "counterlab-generation-isolation-v1" + }, + "service": { + "type": "string", + "const": "counterlab-hosted-runner" + }, + "probe": { + "type": "string", + "const": "non-root-startup" + }, + "checks": { + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "entrypoint" + }, + { + "type": "string", + "const": "non-root-user" + }, + { + "type": "string", + "const": "immutable-paths" + }, + { + "type": "string", + "const": "codex" + }, + { + "type": "string", + "const": "python" + }, + { + "type": "string", + "const": "bubblewrap" + }, + { + "type": "string", + "const": "bubblewrap-read-isolation" + }, + { + "type": "string", + "const": "setpriv" + }, + { + "type": "string", + "const": "writable-roots" + } + ], + "minItems": 9, + "maxItems": 9, + "items": false + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "bubblewrapVersion": { + "type": "string", + "const": "0.11.0" + }, + "bubblewrap": { + "type": "object", + "properties": { + "forbiddenHostPathsHidden": { + "type": "boolean", + "const": true + }, + "parentEnvironmentHidden": { + "type": "boolean", + "const": true + }, + "workspaceVisible": { + "type": "boolean", + "const": true + }, + "workspaceWritable": { + "type": "boolean", + "const": true + } + }, + "required": [ + "forbiddenHostPathsHidden", + "parentEnvironmentHidden", + "workspaceVisible", + "workspaceWritable" + ], + "additionalProperties": false + } + }, + "required": [ + "schemaVersion", + "probeVersion", + "service", + "probe", + "checks", + "generationFilesystemReadIsolation", + "bubblewrapVersion", + "bubblewrap" + ], + "additionalProperties": false + }, + "probePayloadSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "verifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-generation-isolation-evidence-v1" + } + }, + "required": [ + "schemaVersion", + "status", + "generationFilesystemReadIsolation", + "sourceCommit", + "sourceTreeSha256", + "localImageTag", + "localImageDigest", + "imageUser", + "mountPolicyVersion", + "probePayload", + "probePayloadSha256", + "verifiedAt", + "verifierVersion" + ], + "additionalProperties": false + }, + "generationIsolationEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationProbeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "status", + "sourceCommit", + "sourceArchiveSha256", + "sourceTreeSha256", + "dockerfileSha256", + "localImageTag", + "localImageDigest", + "ociRevision", + "ociSourceTreeSha256", + "engineAuthorityHash", + "runtimeManifestHash", + "runtimeToolchainSha256", + "runtimePolicySha256", + "proofDependencyManifestSha256", + "toolchainLockSha256", + "runtimeAdapterSha256", + "buildctlSha256", + "buildkitdSha256", + "buildkitConfigSha256", + "adapterDockerfileSha256", + "adapterImageTag", + "adapterImageDigest", + "adapterManifestDigest", + "adapterOciArchiveSha256", + "adapterOciRevision", + "adapterOciSourceTreeSha256", + "limitMode", + "aggregateLimitIntentEnforced", + "aggregateLimitEvidenceSha256", + "timeoutCleanupReceipt", + "timeoutCleanupReceiptSha256", + "timeoutCleanupPayloadSha256", + "timeoutRunControlReceiptSha256", + "timeoutRootlessReceiptSha256", + "timeoutRuntimeSessionId", + "timeoutVerifiedAt", + "evidenceCommit", + "registryImage", + "registryDigest", + "registryResolvedAt", + "qualifiedAt", + "verifierVersion", + "generationFilesystemReadIsolation", + "generationIsolationEvidence", + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt" + ], + "additionalProperties": false +} diff --git a/scientific-engines/schemas/release-check-receipt-v4.schema.json b/scientific-engines/schemas/release-check-receipt-v4.schema.json new file mode 100644 index 00000000..ef0894f5 --- /dev/null +++ b/scientific-engines/schemas/release-check-receipt-v4.schema.json @@ -0,0 +1,160 @@ +{ + "$id": "https://counterlab.dev/schemas/release-check-receipt-v4.schema.json", + "title": "CounterLab release-check receipt v4", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "4" + }, + "status": { + "type": "string", + "const": "PASSED" + }, + "evidenceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "qualifiedRunnerReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "qualifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "runnerImageTag": { + "type": "string", + "pattern": "^counterlab-runner:git-[a-f0-9]{40}$" + }, + "runnerImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "adapterImageTag": { + "type": "string", + "pattern": "^counterlab-adapter:git-[a-f0-9]{40}$" + }, + "adapterImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "registryDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "runtimeToolchainSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimePolicySha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "proofDependencyManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "aggregateLimitEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeAdapterSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "checks": { + "minItems": 11, + "maxItems": 11, + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "test-all", + "leakage-mutations", + "imbalance-mutations", + "held-out", + "adapter-sandbox-smoke", + "scientific-engines", + "web-build", + "achieved-metrics", + "reproduce-leakage", + "replay-patch", + "secret-scan" + ] + }, + "status": { + "type": "string", + "const": "PASSED" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + }, + "checkedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-release-check-v4" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "generationIsolationEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationProbeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "status", + "evidenceCommit", + "sourceCommit", + "qualifiedRunnerReceiptSha256", + "qualifiedAt", + "runnerImageTag", + "runnerImageDigest", + "adapterImageTag", + "adapterImageDigest", + "registryDigest", + "runtimeToolchainSha256", + "runtimePolicySha256", + "proofDependencyManifestSha256", + "aggregateLimitEvidenceSha256", + "runtimeAdapterSha256", + "checks", + "checkedAt", + "verifierVersion", + "generationFilesystemReadIsolation", + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt" + ], + "additionalProperties": false +} diff --git a/scripts/generate-contract-schemas.ts b/scripts/generate-contract-schemas.ts index 02cac6ec..1d3bd40f 100644 --- a/scripts/generate-contract-schemas.ts +++ b/scripts/generate-contract-schemas.ts @@ -18,16 +18,20 @@ import { import { ContainedRuntimeAttestationSchema, ContainedRuntimeAttestationV1Schema, - DeploymentReceiptSchema, DeploymentReceiptV3Schema, DeploymentReceiptV4Schema, - QualifiedRunnerReleaseSchema, + DeploymentReceiptV5Schema, + DeploymentReceiptV6Schema, + GenerationIsolationEvidenceV1Schema, QualifiedRunnerReleaseV2Schema, QualifiedRunnerReleaseV3Schema, QualifiedRunnerReleaseV4Schema, - ReleaseCheckReceiptSchema, + QualifiedRunnerReleaseV5Schema, + QualifiedRunnerReleaseV6Schema, ReleaseCheckReceiptV1Schema, ReleaseCheckReceiptV2Schema, + ReleaseCheckReceiptV3Schema, + ReleaseCheckReceiptV4Schema, ScientificEngineEvidenceCatalogSchema, ScientificEngineRegistrySchema, ScientificEngineRuntimeManifestSchema, @@ -42,7 +46,36 @@ import { } from "../packages/generative-ui-contracts/src/index.js"; const root = resolve(import.meta.dirname, ".."); + +function closeTupleArrays(value: unknown): void { + if (Array.isArray(value)) { + for (const item of value) closeTupleArrays(item); + return; + } + if (typeof value !== "object" || value === null) return; + const record = value as Record; + if (Array.isArray(record.prefixItems)) { + record.minItems = record.prefixItems.length; + record.maxItems = record.prefixItems.length; + record.items = false; + } + for (const item of Object.values(record)) closeTupleArrays(item); +} + const schemas = [ + { + fileName: "generation-isolation-evidence-v1.schema.json", + id: "https://counterlab.dev/schemas/generation-isolation-evidence-v1.schema.json", + title: "CounterLab generation isolation evidence v1", + schema: GenerationIsolationEvidenceV1Schema, + closeTupleArrays: true, + destinations: [ + resolve( + root, + "scientific-engines/schemas/generation-isolation-evidence-v1.schema.json", + ), + ], + }, { fileName: "boundary-map-result-v1.schema.json", id: "https://counterlab.dev/schemas/boundary-map-result-v1.schema.json", @@ -252,7 +285,7 @@ const schemas = [ fileName: "qualified-runner-release-v5.schema.json", id: "https://counterlab.dev/schemas/qualified-runner-release-v5.schema.json", title: "CounterLab qualified runner release v5", - schema: QualifiedRunnerReleaseSchema, + schema: QualifiedRunnerReleaseV5Schema, destinations: [ resolve( root, @@ -260,6 +293,19 @@ const schemas = [ ), ], }, + { + fileName: "qualified-runner-release-v6.schema.json", + id: "https://counterlab.dev/schemas/qualified-runner-release-v6.schema.json", + title: "CounterLab qualified runner release v6", + schema: QualifiedRunnerReleaseV6Schema, + closeTupleArrays: true, + destinations: [ + resolve( + root, + "scientific-engines/schemas/qualified-runner-release-v6.schema.json", + ), + ], + }, { fileName: "timeout-cleanup-receipt-v1.schema.json", id: "https://counterlab.dev/schemas/timeout-cleanup-receipt-v1.schema.json", @@ -324,7 +370,7 @@ const schemas = [ fileName: "release-check-receipt-v3.schema.json", id: "https://counterlab.dev/schemas/release-check-receipt-v3.schema.json", title: "CounterLab release-check receipt v3", - schema: ReleaseCheckReceiptSchema, + schema: ReleaseCheckReceiptV3Schema, destinations: [ resolve( root, @@ -332,6 +378,18 @@ const schemas = [ ), ], }, + { + fileName: "release-check-receipt-v4.schema.json", + id: "https://counterlab.dev/schemas/release-check-receipt-v4.schema.json", + title: "CounterLab release-check receipt v4", + schema: ReleaseCheckReceiptV4Schema, + destinations: [ + resolve( + root, + "scientific-engines/schemas/release-check-receipt-v4.schema.json", + ), + ], + }, { fileName: "deployment-receipt-v3.schema.json", id: "https://counterlab.dev/schemas/deployment-receipt-v3.schema.json", @@ -360,7 +418,7 @@ const schemas = [ fileName: "deployment-receipt-v5.schema.json", id: "https://counterlab.dev/schemas/deployment-receipt-v5.schema.json", title: "CounterLab deployment receipt v5", - schema: DeploymentReceiptSchema, + schema: DeploymentReceiptV5Schema, destinations: [ resolve( root, @@ -368,10 +426,25 @@ const schemas = [ ), ], }, + { + fileName: "deployment-receipt-v6.schema.json", + id: "https://counterlab.dev/schemas/deployment-receipt-v6.schema.json", + title: "CounterLab deployment receipt v6", + schema: DeploymentReceiptV6Schema, + destinations: [ + resolve( + root, + "scientific-engines/schemas/deployment-receipt-v6.schema.json", + ), + ], + }, ]; for (const definition of schemas) { const generated = z.toJSONSchema(definition.schema); + if ("closeTupleArrays" in definition && definition.closeTupleArrays) { + closeTupleArrays(generated); + } const serialized = `${JSON.stringify( { $id: definition.id, title: definition.title, ...generated }, null, diff --git a/scripts/generation-isolation-evidence.test.ts b/scripts/generation-isolation-evidence.test.ts new file mode 100644 index 00000000..b325d9e4 --- /dev/null +++ b/scripts/generation-isolation-evidence.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, it } from "vitest"; + +import { + createGenerationIsolationEvidence, + verifyGenerationIsolationEvidence, +} from "./generation-isolation-evidence.js"; + +const sourceCommit = "a".repeat(40); +const sourceTreeSha256 = "b".repeat(64); +const localImageTag = `counterlab-runner:git-${sourceCommit}`; +const localImageDigest = `sha256:${"c".repeat(64)}`; +const probePayload = { + schemaVersion: "1", + probeVersion: "counterlab-generation-isolation-v1", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: [ + "entrypoint", + "non-root-user", + "immutable-paths", + "codex", + "python", + "bubblewrap", + "bubblewrap-read-isolation", + "setpriv", + "writable-roots", + ], + generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrapVersion: "0.11.0", + bubblewrap: { + forbiddenHostPathsHidden: true, + parentEnvironmentHidden: true, + workspaceVisible: true, + workspaceWritable: true, + }, +} as const; +const probeSha256 = + "700cc58bedc163846e3854415170f49f55da9fd3ba316cc4967747d5268199dc"; + +function startupProbe() { + return { + status: "ready", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: probePayload.checks, + generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationProbe: probePayload, + generationIsolationProbeSha256: probeSha256, + } as const; +} + +function createEvidence() { + return createGenerationIsolationEvidence({ + sourceCommit, + sourceTreeSha256, + localImageTag, + localImageDigest, + imageUser: "10001:10001", + startupProbe: startupProbe(), + verifiedAt: "2026-07-20T21:30:00.000Z", + }); +} + +describe("generation-isolation evidence", () => { + it("binds canonical exact-image probe evidence to source and image", () => { + const result = createEvidence(); + expect(result.probeSha256).toBe(probeSha256); + expect(result.evidence).toMatchObject({ + sourceCommit, + sourceTreeSha256, + localImageTag, + localImageDigest, + imageUser: "10001:10001", + probePayloadSha256: probeSha256, + }); + expect( + verifyGenerationIsolationEvidence({ + evidence: result.evidence, + evidenceSha256: result.evidenceSha256, + expected: { + sourceCommit, + sourceTreeSha256, + localImageTag, + localImageDigest, + probeSha256, + verifiedAt: "2026-07-20T21:30:00.000Z", + }, + }), + ).toEqual(result); + }); + + it("rejects self-reported, evidence, source, and image hash mutations", () => { + expect(() => + createGenerationIsolationEvidence({ + sourceCommit, + sourceTreeSha256, + localImageTag, + localImageDigest, + imageUser: "10001:10001", + startupProbe: { + ...startupProbe(), + generationIsolationProbeSha256: "d".repeat(64), + }, + }), + ).toThrow(/probe hash/u); + + const result = createEvidence(); + for (const mutation of [ + { evidenceSha256: "d".repeat(64), expected: {} }, + { + evidenceSha256: result.evidenceSha256, + expected: { sourceCommit: "e".repeat(40) }, + }, + { + evidenceSha256: result.evidenceSha256, + expected: { sourceTreeSha256: "f".repeat(64) }, + }, + { + evidenceSha256: result.evidenceSha256, + expected: { localImageDigest: `sha256:${"0".repeat(64)}` }, + }, + { + evidenceSha256: result.evidenceSha256, + expected: { probeSha256: "1".repeat(64) }, + }, + ]) { + expect(() => + verifyGenerationIsolationEvidence({ + evidence: result.evidence, + evidenceSha256: mutation.evidenceSha256, + expected: { + sourceCommit, + sourceTreeSha256, + localImageTag, + localImageDigest, + ...mutation.expected, + }, + }), + ).toThrow(); + } + }); + + it("rejects a modified probe payload even when its outer envelope is intact", () => { + const result = createEvidence(); + expect(() => + verifyGenerationIsolationEvidence({ + evidence: { + ...result.evidence, + probePayload: { + ...result.evidence.probePayload, + bubblewrapVersion: "0.11.1", + }, + }, + evidenceSha256: result.evidenceSha256, + expected: { + sourceCommit, + sourceTreeSha256, + localImageTag, + localImageDigest, + }, + }), + ).toThrow(); + }); +}); diff --git a/scripts/generation-isolation-evidence.ts b/scripts/generation-isolation-evidence.ts new file mode 100644 index 00000000..53bbce0a --- /dev/null +++ b/scripts/generation-isolation-evidence.ts @@ -0,0 +1,182 @@ +import { createHash } from "node:crypto"; + +import { + GENERATION_ISOLATION_MOUNT_POLICY_VERSION, + GenerationIsolationEvidenceV1Schema, + GenerationIsolationProbePayloadSchema, + QualifiedRunnerReleaseV6Schema, + type GenerationIsolationEvidenceV1, + type QualifiedRunnerReleaseV6, +} from "../packages/scientific-engine-registry/src/index.js"; +import { canonicalJson } from "../packages/session-core/src/index.js"; +import { z } from "zod"; + +const Sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u); + +const StartupProbeResultSchema = z + .strictObject({ + status: z.literal("ready"), + service: z.literal("counterlab-hosted-runner"), + probe: z.literal("non-root-startup"), + checks: GenerationIsolationProbePayloadSchema.shape.checks, + generationFilesystemReadIsolation: z.literal("OS_ENFORCED"), + generationIsolationProbe: GenerationIsolationProbePayloadSchema, + generationIsolationProbeSha256: Sha256Schema, + }) + .superRefine((result, context) => { + for (const [field, outer, inner] of [ + ["service", result.service, result.generationIsolationProbe.service], + ["probe", result.probe, result.generationIsolationProbe.probe], + [ + "checks", + canonicalJson(result.checks), + canonicalJson(result.generationIsolationProbe.checks), + ], + [ + "generationFilesystemReadIsolation", + result.generationFilesystemReadIsolation, + result.generationIsolationProbe.generationFilesystemReadIsolation, + ], + ] as const) { + if (outer !== inner) { + context.addIssue({ + code: "custom", + path: [field], + message: `${field} must match the nested isolation probe`, + }); + } + } + }); + +function sha256Canonical(value: unknown): string { + return createHash("sha256").update(canonicalJson(value)).digest("hex"); +} + +export function hashGenerationIsolationEvidence( + evidence: GenerationIsolationEvidenceV1, +): string { + return sha256Canonical(GenerationIsolationEvidenceV1Schema.parse(evidence)); +} + +export function createGenerationIsolationEvidence(input: { + sourceCommit: string; + sourceTreeSha256: string; + localImageTag: string; + localImageDigest: string; + imageUser: string; + startupProbe: unknown; + verifiedAt?: string; +}): { + evidence: GenerationIsolationEvidenceV1; + evidenceSha256: string; + probeSha256: string; +} { + const startupProbe = StartupProbeResultSchema.parse(input.startupProbe); + const probeSha256 = sha256Canonical(startupProbe.generationIsolationProbe); + if (probeSha256 !== startupProbe.generationIsolationProbeSha256) { + throw new Error( + "Generation-isolation probe hash does not match the exact-image payload", + ); + } + const evidence = GenerationIsolationEvidenceV1Schema.parse({ + schemaVersion: "1", + status: "VERIFIED", + generationFilesystemReadIsolation: "OS_ENFORCED", + sourceCommit: input.sourceCommit, + sourceTreeSha256: input.sourceTreeSha256, + localImageTag: input.localImageTag, + localImageDigest: input.localImageDigest, + imageUser: input.imageUser, + mountPolicyVersion: GENERATION_ISOLATION_MOUNT_POLICY_VERSION, + probePayload: startupProbe.generationIsolationProbe, + probePayloadSha256: probeSha256, + verifiedAt: input.verifiedAt ?? new Date().toISOString(), + verifierVersion: "counterlab-generation-isolation-evidence-v1", + }); + return { + evidence, + evidenceSha256: hashGenerationIsolationEvidence(evidence), + probeSha256, + }; +} + +export function verifyGenerationIsolationEvidence(input: { + evidence: unknown; + evidenceSha256: string; + expected: { + sourceCommit: string; + sourceTreeSha256: string; + localImageTag: string; + localImageDigest: string; + imageUser?: "10001:10001"; + probeSha256?: string; + verifiedAt?: string; + }; +}): { + evidence: GenerationIsolationEvidenceV1; + evidenceSha256: string; + probeSha256: string; +} { + const evidence = GenerationIsolationEvidenceV1Schema.parse(input.evidence); + const evidenceSha256 = hashGenerationIsolationEvidence(evidence); + if (evidenceSha256 !== Sha256Schema.parse(input.evidenceSha256)) { + throw new Error("Generation-isolation evidence hash mismatch"); + } + const probeSha256 = sha256Canonical(evidence.probePayload); + if (probeSha256 !== evidence.probePayloadSha256) { + throw new Error("Generation-isolation probe payload hash mismatch"); + } + for (const [field, expected, observed] of [ + ["sourceCommit", input.expected.sourceCommit, evidence.sourceCommit], + [ + "sourceTreeSha256", + input.expected.sourceTreeSha256, + evidence.sourceTreeSha256, + ], + ["localImageTag", input.expected.localImageTag, evidence.localImageTag], + [ + "localImageDigest", + input.expected.localImageDigest, + evidence.localImageDigest, + ], + [ + "imageUser", + input.expected.imageUser ?? "10001:10001", + evidence.imageUser, + ], + [ + "probeSha256", + input.expected.probeSha256 ?? evidence.probePayloadSha256, + evidence.probePayloadSha256, + ], + [ + "verifiedAt", + input.expected.verifiedAt ?? evidence.verifiedAt, + evidence.verifiedAt, + ], + ] as const) { + if (expected !== observed) { + throw new Error(`Generation-isolation ${field} binding mismatch`); + } + } + return { evidence, evidenceSha256, probeSha256 }; +} + +export function parseQualifiedRunnerReleaseV6( + value: unknown, +): QualifiedRunnerReleaseV6 { + const release = QualifiedRunnerReleaseV6Schema.parse(value); + verifyGenerationIsolationEvidence({ + evidence: release.generationIsolationEvidence, + evidenceSha256: release.generationIsolationEvidenceSha256, + expected: { + sourceCommit: release.sourceCommit, + sourceTreeSha256: release.sourceTreeSha256, + localImageTag: release.localImageTag, + localImageDigest: release.localImageDigest, + probeSha256: release.generationIsolationProbeSha256, + verifiedAt: release.generationIsolationVerifiedAt, + }, + }); + return release; +} From 56638e9748f8a6b2996bf61c882594aa27fdeb70 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 03:06:51 +0530 Subject: [PATCH 011/143] feat(release): persist exact-image isolation evidence --- apps/web/worker/deployment-config.test.ts | 7 ++ scripts/generation-isolation-evidence.ts | 113 ++++++++++++++++++++++ scripts/verify-scientific-engines.sh | 36 +++++++ 3 files changed, 156 insertions(+) diff --git a/apps/web/worker/deployment-config.test.ts b/apps/web/worker/deployment-config.test.ts index ccbc49d6..157b258c 100644 --- a/apps/web/worker/deployment-config.test.ts +++ b/apps/web/worker/deployment-config.test.ts @@ -547,6 +547,13 @@ describe("Cloudflare static asset routing", () => { expect(probe).not.toContain("--user"); expect(probe).not.toContain("--entrypoint"); expect(probe).toContain('"${IMAGE}"'); + expect(verifier).toContain("--generation-isolation-report"); + expect(verifier).toContain( + '{{index .Config.Labels "io.counterlab.source-tree-sha256"}}', + ); + expect(verifier).toContain( + "node --import tsx scripts/generation-isolation-evidence.ts", + ); }); it("generates a deploy config from a source-bound qualified image receipt", () => { diff --git a/scripts/generation-isolation-evidence.ts b/scripts/generation-isolation-evidence.ts index 53bbce0a..431c4433 100644 --- a/scripts/generation-isolation-evidence.ts +++ b/scripts/generation-isolation-evidence.ts @@ -1,4 +1,7 @@ import { createHash } from "node:crypto"; +import { lstat, realpath, stat, writeFile } from "node:fs/promises"; +import { dirname, isAbsolute, relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; import { GENERATION_ISOLATION_MOUNT_POLICY_VERSION, @@ -180,3 +183,113 @@ export function parseQualifiedRunnerReleaseV6( }); return release; } + +function isContained(root: string, candidate: string): boolean { + const fromRoot = relative(root, candidate); + return ( + fromRoot === "" || (!fromRoot.startsWith("..") && !isAbsolute(fromRoot)) + ); +} + +async function newContainedOutput(root: string, requested: string) { + const output = resolve(root, requested); + if (!isContained(root, output) || output === root) { + throw new Error( + "Generation-isolation output must remain in the repository", + ); + } + let current = root; + for (const component of relative(root, output).split(sep)) { + current = resolve(current, component); + try { + const metadata = await lstat(current); + if (metadata.isSymbolicLink()) { + throw new Error("Generation-isolation output path contains a symlink"); + } + if (!isContained(root, await realpath(current))) { + throw new Error( + "Generation-isolation output path escapes the repository", + ); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") break; + throw error; + } + } + const parent = await realpath(dirname(output)); + if (!isContained(root, parent) || !(await stat(parent)).isDirectory()) { + throw new Error("Generation-isolation output parent is not contained"); + } + try { + await lstat(output); + throw new Error("Generation-isolation output already exists"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + return output; +} + +function cliArguments(argv: string[]): Record { + const allowed = new Set([ + "--source-commit", + "--source-tree-sha256", + "--local-image-tag", + "--local-image-digest", + "--image-user", + "--startup-probe-json", + "--verified-at", + "--output", + ]); + const values = new Map(); + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index]; + const value = argv[index + 1]; + if ( + flag === undefined || + value === undefined || + !allowed.has(flag) || + values.has(flag) + ) { + throw new Error("Invalid generation-isolation evidence arguments"); + } + values.set(flag, value); + } + if (values.size !== allowed.size) { + throw new Error("Incomplete generation-isolation evidence arguments"); + } + return Object.fromEntries(values); +} + +async function main(): Promise { + const root = await realpath(resolve(import.meta.dirname, "..")); + const values = cliArguments(process.argv.slice(2)); + const output = await newContainedOutput(root, values["--output"] ?? ""); + const result = createGenerationIsolationEvidence({ + sourceCommit: values["--source-commit"] ?? "", + sourceTreeSha256: values["--source-tree-sha256"] ?? "", + localImageTag: values["--local-image-tag"] ?? "", + localImageDigest: values["--local-image-digest"] ?? "", + imageUser: values["--image-user"] ?? "", + startupProbe: JSON.parse(values["--startup-probe-json"] ?? "") as unknown, + verifiedAt: values["--verified-at"] ?? "", + }); + await writeFile(output, `${JSON.stringify(result.evidence, null, 2)}\n`, { + encoding: "utf8", + flag: "wx", + mode: 0o600, + }); + console.log( + JSON.stringify({ + evidenceSha256: result.evidenceSha256, + probeSha256: result.probeSha256, + output: relative(root, output), + }), + ); +} + +if ( + process.argv[1] !== undefined && + fileURLToPath(import.meta.url) === resolve(process.argv[1]) +) { + await main(); +} diff --git a/scripts/verify-scientific-engines.sh b/scripts/verify-scientific-engines.sh index cf8ae3f0..afa379ce 100755 --- a/scripts/verify-scientific-engines.sh +++ b/scripts/verify-scientific-engines.sh @@ -6,6 +6,7 @@ IMAGE="" REGISTRY_ONLY=0 REQUIRE_PRODUCTION=0 RUNTIME_REPORT="" +GENERATION_ISOLATION_REPORT="" ENVIRONMENT_HELPER="${ROOT_DIR}/scripts/prepare-contained-shell-environment.sh" [[ -f "${ENVIRONMENT_HELPER}" && ! -L "${ENVIRONMENT_HELPER}" ]] || { @@ -24,6 +25,8 @@ Options: --registry-only Skip Proof Capsule linkage and permit an omitted runtime image. --require-production Reject a local-candidate runtime manifest. --runtime-report PATH Persist the exact runtime report to a new contained file. + --generation-isolation-report PATH + Persist source/image-bound generation isolation evidence. --help Show this help. The default release gate fails closed unless an image is supplied and Proof Capsule @@ -51,6 +54,11 @@ while [[ $# -gt 0 ]]; do RUNTIME_REPORT="$2" shift 2 ;; + --generation-isolation-report) + [[ $# -ge 2 ]] || { echo "--generation-isolation-report requires a value" >&2; exit 2; } + GENERATION_ISOLATION_REPORT="$2" + shift 2 + ;; --help|-h) usage exit 0 @@ -75,6 +83,16 @@ if [[ -n "${RUNTIME_REPORT}" ]]; then exit 2 } fi +if [[ -n "${GENERATION_ISOLATION_REPORT}" ]]; then + if [[ "${GENERATION_ISOLATION_REPORT}" != /* ]]; then + GENERATION_ISOLATION_REPORT="${ROOT_DIR}/${GENERATION_ISOLATION_REPORT#./}" + fi + node scripts/assert-contained-path.mjs "${GENERATION_ISOLATION_REPORT}" + [[ ! -e "${GENERATION_ISOLATION_REPORT}" && ! -L "${GENERATION_ISOLATION_REPORT}" ]] || { + echo "Generation-isolation output must be a new repository-contained file." >&2 + exit 2 + } +fi [[ -x "./node_modules/.bin/tsx" ]] || { echo "Missing local TypeScript runtime. Install the pinned workspace dependencies first." >&2 @@ -137,6 +155,7 @@ DOCKER_COMMAND=( IMAGE_DIGEST="$("${DOCKER_COMMAND[@]}" image inspect "${IMAGE}" --format '{{.Id}}')" SOURCE_COMMIT="$("${DOCKER_COMMAND[@]}" image inspect "${IMAGE}" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" +SOURCE_TREE_SHA256="$("${DOCKER_COMMAND[@]}" image inspect "${IMAGE}" --format '{{index .Config.Labels "io.counterlab.source-tree-sha256"}}')" IMAGE_USER="$("${DOCKER_COMMAND[@]}" image inspect "${IMAGE}" --format '{{.Config.User}}')" if [[ ! "${IMAGE_DIGEST}" =~ ^sha256:[a-f0-9]{64}$ ]]; then echo "Runner image does not expose a valid sha256 image ID." >&2 @@ -146,6 +165,10 @@ if [[ ! "${SOURCE_COMMIT}" =~ ^[a-f0-9]{40}$ ]]; then echo "Runner image has an unbound or invalid OCI source revision." >&2 exit 1 fi +if [[ ! "${SOURCE_TREE_SHA256}" =~ ^[a-f0-9]{64}$ ]]; then + echo "Runner image has an unbound or invalid OCI source-tree hash." >&2 + exit 1 +fi if [[ "${IMAGE_USER}" != "10001:10001" ]]; then echo "Runner image must declare the non-root user 10001:10001; observed '${IMAGE_USER}'." >&2 exit 1 @@ -204,6 +227,19 @@ node -e ' } ' "${STARTUP_PROBE_OUTPUT}" +if [[ -n "${GENERATION_ISOLATION_REPORT}" ]]; then + VERIFIED_AT="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" + node --import tsx scripts/generation-isolation-evidence.ts \ + --source-commit "${SOURCE_COMMIT}" \ + --source-tree-sha256 "${SOURCE_TREE_SHA256}" \ + --local-image-tag "${IMAGE}" \ + --local-image-digest "${IMAGE_DIGEST}" \ + --image-user "${IMAGE_USER}" \ + --startup-probe-json "${STARTUP_PROBE_OUTPUT}" \ + --verified-at "${VERIFIED_AT}" \ + --output "${GENERATION_ISOLATION_REPORT}" +fi + # The preceding probe executes the real OCI entrypoint as Config.User. This # second run adopts the host identity only so the exact-image verifier can read # the repository evidence mounted read-only. From 151854bb8bef3cc4f58f368dc3be213f1284d183 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 03:08:10 +0530 Subject: [PATCH 012/143] docs(progress): record image isolation evidence --- docs/PROGRESS.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index fded4d12..33880daa 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2960,3 +2960,77 @@ deleted. receipt before publication. 8. Push the completed feature branch, review its exact tip, fast-forward it into `main`, and push `main` only after the release state is truthful. + +## Finish Commander image-isolation evidence checkpoint — 2026-07-20T21:37:26Z + +Deadline: `2026-07-22T00:00:00Z`; **26 hours 22 minutes 34 seconds remained at +this checkpoint**. The feature branch is at committed HEAD `56638e9`; its only +worktree entry is the preserved untracked +`scripts/.fuse_hidden0000a9600000801f`. It remains unopened, unstaged, and +undeleted. + +### Newly frozen independent commits + +- `b5d2359 fix(runner): hash Bubblewrap startup evidence` parses the actual + Bubblewrap stdout, rejects missing, malformed, false, or unknown evidence, + and hashes a canonical startup-probe payload. +- `fc1c581 fix(runner): bind pinned Bubblewrap version` observes and binds the + exact image-pinned Bubblewrap `0.11.0` version into that payload. +- `9a15ec4 feat(release): define image isolation evidence` preserves historical + receipt schemas, adds strict evidence plus qualified v6/release-check + v4/deployment v6 contracts, closes generated tuple semantics, and adds a + canonical hash verifier with mutation coverage. +- `56638e9 feat(release): persist exact-image isolation evidence` lets the + exact-image scientific verifier create a contained, `0600`, non-overwriting + evidence record bound to OCI source, source-tree, image config digest, image + user, and recomputed probe hash. + +### Exact local verification + +- Hosted-runner startup probe: **8/8 passed**; full hosted-runner suite: + **45 passed, 2 skipped** after rerunning the five socket tests with loopback + permission. +- Isolation contracts and canonical verifier: **4 files, 27/27 passed**. +- Worker deployment configuration: **17/17 passed**. +- Contained-runtime source validation: **35/35 passed**. +- Root, Worker, and hosted-runner TypeScript at the affected checkpoints: + **passed**. +- Shell syntax, scoped Prettier, Git whitespace, historical v5/v3/v5 schema + byte preservation, and scoped secret scans: **passed**. +- The image evidence writer has not yet been exercised against a newly built + exact image. Source coverage is not represented as image or production + evidence. + +### Reconciled issue and external-gate truth + +- Internal P0/P1 reconciliation is now five source-pass, four partial or + mitigated, and three open; **zero are production-closed**. +- Black-box product issues are 17 source-covered and one partial; **none are + verified against the current public tuple**. All 11 submission requirements + remain externally unverified. +- This active session exposes no `CLOAK_CDP_ENDPOINT` or Playwright-safe tool + surface. Stock Chromium remains design-review-only under the checked-in + browser policy and cannot close release QA. +- Repository-contained Wrangler `4.110.0` currently reports + `loggedIn:false`; normal-home credentials are not inspected or copied. +- The host still lacks the delegated aggregate cgroup-v2 authority required by + the current qualification contract. This is a genuine external release + blocker and must not be converted into a synthetic receipt. + +### Ordered next work + +1. Consume the exact-image evidence in qualified v6, release-check v4, and + deployment v6; require the same probe hash from Container `/ready`, Worker + deep readiness, public release identity, and production smoke. +2. Resolve the named FUSE artifact only with immediate destructive-action + approval, then freeze source and regenerate exact source/image-bound + scientific and supply-chain evidence. +3. Obtain a real aggregate-containment receipt on a capable authorized host, + authenticate Wrangler inside the repository boundary, qualify, deploy, and + smoke exactly one tuple. +4. Run CloakBrowser desktop/mobile/accessibility journeys in a session that + exposes the required endpoint. Keep learner evidence `NO_DATA` unless real + consented observations occur. +5. Align and verify Devpost, public video, repository, feedback ID, and + submission receipt; push the feature branch and fast-forward its verified + tip into `main` only after the recorded gates are truthful. From e8ae61f3973a71549d6d1341985709d50cc4ef11 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 03:45:06 +0530 Subject: [PATCH 013/143] fix(release): bind qualified isolation identity --- apps/web/worker/api.test.ts | 16 ++ apps/web/worker/api.ts | 34 +++- apps/web/worker/deployment-config.test.ts | 138 +++++++++++++++- apps/web/worker/runner-container-env.test.ts | 4 + apps/web/worker/runner-container-env.ts | 6 + apps/web/worker/runner-control-plane.test.ts | 117 ++++++++++--- apps/web/worker/runner-control-plane.ts | 24 ++- .../scientific-engine-registry/src/schema.ts | 6 +- .../test/release-check-receipt.test.ts | 154 +++++++++++++++++- .../test/release-schema.test.ts | 121 +++++++++++++- scripts/create-deployment-receipt.test.ts | 22 +++ scripts/create-deployment-receipt.ts | 58 +++++-- scripts/deploy-qualified.sh | 19 ++- scripts/prepare-qualified-deploy.ts | 61 ++++++- scripts/production-smoke.sh | 16 +- scripts/production_smoke_report.py | 58 ++++++- scripts/qualify-runner-release.ts | 66 +++++++- scripts/release-check-receipt.ts | 122 +++++++++++--- scripts/release-check.sh | 17 +- scripts/test_production_smoke.py | 63 +++++-- scripts/verify-scientific-engines.sh | 16 +- services/hosted-runner/src/server.test.ts | 39 +++++ services/hosted-runner/src/server.ts | 64 ++++++-- 23 files changed, 1090 insertions(+), 151 deletions(-) diff --git a/apps/web/worker/api.test.ts b/apps/web/worker/api.test.ts index 44c36db3..1fd56a5f 100644 --- a/apps/web/worker/api.test.ts +++ b/apps/web/worker/api.test.ts @@ -10493,6 +10493,8 @@ describe("Cloudflare Worker API", () => { COUNTERLAB_WORKER_EVIDENCE_COMMIT: workerEvidenceCommit, COUNTERLAB_RUNNER_SOURCE_COMMIT: "b".repeat(40), COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "5".repeat(64), + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat(64), COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: "d".repeat(64), COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: "9".repeat(64), COUNTERLAB_RUNTIME_POLICY_SHA256: "e".repeat(64), @@ -10522,6 +10524,8 @@ describe("Cloudflare Worker API", () => { workerEvidenceCommit, runnerSourceCommit: "b".repeat(40), runnerImageDigest: `sha256:${"c".repeat(64)}`, + generationIsolationEvidenceSha256: "5".repeat(64), + generationIsolationProbeSha256: "6".repeat(64), timeoutCleanupReceiptSha256: "d".repeat(64), aggregateLimitEvidenceSha256: "9".repeat(64), runtimePolicySha256: "e".repeat(64), @@ -10543,6 +10547,8 @@ describe("Cloudflare Worker API", () => { COUNTERLAB_WORKER_EVIDENCE_COMMIT: workerEvidenceCommit, COUNTERLAB_RUNNER_SOURCE_COMMIT: "b".repeat(40), COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "5".repeat(64), + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat(64), COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: "d".repeat(64), COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: "9".repeat(64), COUNTERLAB_RUNTIME_POLICY_SHA256: "e".repeat(64), @@ -10583,6 +10589,10 @@ describe("Cloudflare Worker API", () => { for (const invalidQualificationBinding of [ { COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: undefined }, { COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: "0".repeat(63) }, + { COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: undefined }, + { COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "0".repeat(63) }, + { COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: undefined }, + { COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "0".repeat(63) }, { COUNTERLAB_RUNTIME_POLICY_SHA256: undefined }, { COUNTERLAB_RUNTIME_POLICY_SHA256: "0".repeat(63) }, { COUNTERLAB_PROOF_DEPENDENCY_MANIFEST_SHA256: undefined }, @@ -10610,6 +10620,8 @@ describe("Cloudflare Worker API", () => { COUNTERLAB_WORKER_EVIDENCE_COMMIT: workerEvidenceCommit, COUNTERLAB_RUNNER_SOURCE_COMMIT: "b".repeat(40), COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "5".repeat(64), + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat(64), COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: "d".repeat(64), COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: "9".repeat(64), COUNTERLAB_RUNTIME_POLICY_SHA256: "e".repeat(64), @@ -10930,6 +10942,8 @@ describe("Cloudflare Worker API", () => { COUNTERLAB_RUNNER_SIGNING_PRIVATE_KEY: TEST_RUNNER_SIGNING_PRIVATE_KEY, COUNTERLAB_RUNNER_SOURCE_COMMIT: "b".repeat(40), COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "5".repeat(64), + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat(64), } as unknown as Env & Record); expect(response.status).toBe(200); @@ -10947,6 +10961,8 @@ describe("Cloudflare Worker API", () => { COUNTERLAB_RUNNER_BASE_URL: "http://127.0.0.1:8788", COUNTERLAB_RUNNER_SOURCE_COMMIT: "b".repeat(40), COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "5".repeat(64), + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat(64), } as unknown as Env & Record); await expect(unsigned.json()).resolves.toMatchObject({ data: { liveCodex: "local-runner-required" }, diff --git a/apps/web/worker/api.ts b/apps/web/worker/api.ts index 87726907..4c944c40 100644 --- a/apps/web/worker/api.ts +++ b/apps/web/worker/api.ts @@ -243,6 +243,8 @@ type WorkerBindings = Omit & { COUNTERLAB_WORKER_EVIDENCE_COMMIT?: string; COUNTERLAB_RUNNER_SOURCE_COMMIT?: string; COUNTERLAB_RUNNER_IMAGE_DIGEST?: string; + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256?: string; + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256?: string; COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256?: string; COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256?: string; COUNTERLAB_RUNTIME_POLICY_SHA256?: string; @@ -972,9 +974,15 @@ function runnerDispatcher( context.env?.COUNTERLAB_RUNNER_SOURCE_COMMIT?.trim() ?? ""; const runnerImageDigest = context.env?.COUNTERLAB_RUNNER_IMAGE_DIGEST?.trim() ?? ""; + const generationIsolationEvidenceSha256 = + context.env?.COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256?.trim() ?? ""; + const generationIsolationProbeSha256 = + context.env?.COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256?.trim() ?? ""; const exactRunnerIdentity = /^[a-f0-9]{40}$/u.test(runnerSourceCommit) && - /^sha256:[a-f0-9]{64}$/u.test(runnerImageDigest); + /^sha256:[a-f0-9]{64}$/u.test(runnerImageDigest) && + /^[a-f0-9]{64}$/u.test(generationIsolationEvidenceSha256) && + /^[a-f0-9]{64}$/u.test(generationIsolationProbeSha256); const processRunnerURL = context.env?.COUNTERLAB_RUNNER_BASE_URL?.trim(); if ( processRunnerURL !== undefined && @@ -984,7 +992,12 @@ function runnerDispatcher( try { return new HttpRunnerDispatcher({ baseURL: processRunnerURL, - releaseIdentity: { runnerSourceCommit, runnerImageDigest }, + releaseIdentity: { + runnerSourceCommit, + runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, + }, }); } catch { return undefined; @@ -996,7 +1009,12 @@ function runnerDispatcher( ? new CloudflareContainerRunnerDispatcher( context.env.RUNNER, createRunnerContainerEnvVars(context.env), - { runnerSourceCommit, runnerImageDigest }, + { + runnerSourceCommit, + runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, + }, ) : undefined; } @@ -1048,6 +1066,8 @@ function releaseIdentity(context: Context): workerEvidenceCommit: string; runnerSourceCommit: string; runnerImageDigest: string; + generationIsolationEvidenceSha256: string; + generationIsolationProbeSha256: string; timeoutCleanupReceiptSha256: string; aggregateLimitEvidenceSha256: string; runtimePolicySha256: string; @@ -1067,6 +1087,10 @@ function releaseIdentity(context: Context): context.env?.COUNTERLAB_WORKER_EVIDENCE_COMMIT ?? ""; const runnerSourceCommit = context.env?.COUNTERLAB_RUNNER_SOURCE_COMMIT ?? ""; const runnerImageDigest = context.env?.COUNTERLAB_RUNNER_IMAGE_DIGEST ?? ""; + const generationIsolationEvidenceSha256 = + context.env?.COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256 ?? ""; + const generationIsolationProbeSha256 = + context.env?.COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256 ?? ""; const timeoutCleanupReceiptSha256 = context.env?.COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256 ?? ""; const aggregateLimitEvidenceSha256 = @@ -1099,6 +1123,8 @@ function releaseIdentity(context: Context): !/^[a-f0-9]{40}$/u.test(workerEvidenceCommit) || !/^[a-f0-9]{40}$/u.test(runnerSourceCommit) || !/^sha256:[a-f0-9]{64}$/u.test(runnerImageDigest) || + !/^[a-f0-9]{64}$/u.test(generationIsolationEvidenceSha256) || + !/^[a-f0-9]{64}$/u.test(generationIsolationProbeSha256) || !/^[a-f0-9]{64}$/u.test(timeoutCleanupReceiptSha256) || !/^[a-f0-9]{64}$/u.test(aggregateLimitEvidenceSha256) || !/^[a-f0-9]{64}$/u.test(runtimePolicySha256) || @@ -1126,6 +1152,8 @@ function releaseIdentity(context: Context): workerEvidenceCommit, runnerSourceCommit, runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, timeoutCleanupReceiptSha256, aggregateLimitEvidenceSha256, runtimePolicySha256, diff --git a/apps/web/worker/deployment-config.test.ts b/apps/web/worker/deployment-config.test.ts index 157b258c..81865abf 100644 --- a/apps/web/worker/deployment-config.test.ts +++ b/apps/web/worker/deployment-config.test.ts @@ -8,6 +8,7 @@ import { bindFrozenWorkerRelease, qualifiedDeployConfig, } from "../../../scripts/prepare-qualified-deploy"; +import { createGenerationIsolationEvidence } from "../../../scripts/generation-isolation-evidence"; function productionDeployConfig() { return { @@ -141,6 +142,62 @@ function timeoutQualification(sourceCommit: string) { } as const; } +function generationIsolationQualification(input: { + sourceCommit: string; + sourceTreeSha256: string; + localImageTag: string; + localImageDigest: string; + verifiedAt?: string; +}) { + const probePayload = { + schemaVersion: "1", + probeVersion: "counterlab-generation-isolation-v1", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: [ + "entrypoint", + "non-root-user", + "immutable-paths", + "codex", + "python", + "bubblewrap", + "bubblewrap-read-isolation", + "setpriv", + "writable-roots", + ], + generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrapVersion: "0.11.0", + bubblewrap: { + forbiddenHostPathsHidden: true, + parentEnvironmentHidden: true, + workspaceVisible: true, + workspaceWritable: true, + }, + } as const; + const probeSha256 = + "700cc58bedc163846e3854415170f49f55da9fd3ba316cc4967747d5268199dc"; + const result = createGenerationIsolationEvidence({ + ...input, + imageUser: "10001:10001", + verifiedAt: input.verifiedAt ?? "2026-07-16T16:18:00.000Z", + startupProbe: { + status: "ready", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: probePayload.checks, + generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationProbe: probePayload, + generationIsolationProbeSha256: probeSha256, + }, + }); + return { + generationIsolationEvidence: result.evidence, + generationIsolationEvidenceSha256: result.evidenceSha256, + generationIsolationProbeSha256: result.probeSha256, + generationIsolationVerifiedAt: result.evidence.verifiedAt, + } as const; +} + function luminance(color: string): number { const channels = [1, 3, 5].map((offset) => Number.parseInt(color.slice(offset, offset + 2), 16), @@ -219,6 +276,7 @@ describe("Cloudflare static asset routing", () => { expect(verifyIndex).toBeGreaterThan(0); expect(buildIndex).toBeGreaterThan(verifyIndex); + expect(script).toContain('--expected-image-digest "${LOCAL_IMAGE_DIGEST}"'); }); it("gates deployment on immutable authority, migrations, secrets, and 100 percent traffic", () => { @@ -401,6 +459,12 @@ describe("Cloudflare static asset routing", () => { "qualification output already exists; refusing to replace it", ); expect(script).toContain("assertCurrentGrypeReleaseEvidenceBinding"); + expect(script).toContain("input.expectedConfigDigest"); + expect(script).toContain('"--expected-image-digest"'); + expect(script).toContain("buildReceipt.localImageDigest"); + expect(script).toContain( + '"qualified local image digest is unavailable for promotion"', + ); expect(script).toContain('flag: "wx"'); expect( script.match( @@ -546,8 +610,15 @@ describe("Cloudflare static asset routing", () => { expect(probe).toContain("COUNTERLAB_RUNNER_STARTUP_PROBE=1"); expect(probe).not.toContain("--user"); expect(probe).not.toContain("--entrypoint"); - expect(probe).toContain('"${IMAGE}"'); + expect(probe).toContain('"${IMAGE_DIGEST}"'); + expect( + verifier.match(/"\$\{IMAGE_DIGEST\}"/gu)?.length, + ).toBeGreaterThanOrEqual(2); expect(verifier).toContain("--generation-isolation-report"); + expect(verifier).toContain("--expected-image-digest"); + expect(verifier).toContain( + '"${IMAGE_DIGEST}" != "${EXPECTED_IMAGE_DIGEST}"', + ); expect(verifier).toContain( '{{index .Config.Labels "io.counterlab.source-tree-sha256"}}', ); @@ -561,7 +632,7 @@ describe("Cloudflare static asset routing", () => { const evidenceCommit = "2".repeat(40); const image = `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`; const receipt = { - schemaVersion: "5", + schemaVersion: "6", status: "VERIFIED", generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, @@ -570,6 +641,12 @@ describe("Cloudflare static asset routing", () => { dockerfileSha256: "d".repeat(64), localImageTag: `counterlab-runner:git-${sourceCommit}`, localImageDigest: `sha256:${"e".repeat(64)}`, + ...generationIsolationQualification({ + sourceCommit, + sourceTreeSha256: "c".repeat(64), + localImageTag: `counterlab-runner:git-${sourceCommit}`, + localImageDigest: `sha256:${"e".repeat(64)}`, + }), ociRevision: sourceCommit, ociSourceTreeSha256: "c".repeat(64), engineAuthorityHash: "f".repeat(64), @@ -595,10 +672,16 @@ describe("Cloudflare static asset routing", () => { registryDigest: `sha256:${"3".repeat(64)}`, registryResolvedAt: "2026-07-16T16:20:00.000Z", qualifiedAt: "2026-07-16T16:30:00.000Z", - verifierVersion: "counterlab-release-v5", + verifierVersion: "counterlab-release-v6", }; const observation = { generationFilesystemReadIsolation: "OS_ENFORCED" as const, + ...generationIsolationQualification({ + sourceCommit, + sourceTreeSha256: receipt.sourceTreeSha256, + localImageTag: receipt.localImageTag, + localImageDigest: receipt.localImageDigest, + }), sourceCommit, sourceArchiveSha256: receipt.sourceArchiveSha256, sourceTreeSha256: receipt.sourceTreeSha256, @@ -663,6 +746,10 @@ describe("Cloudflare static asset routing", () => { COUNTERLAB_RUNTIME_POLICY_SHA256: receipt.runtimePolicySha256, COUNTERLAB_PROOF_DEPENDENCY_MANIFEST_SHA256: receipt.proofDependencyManifestSha256, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: + receipt.generationIsolationEvidenceSha256, + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: + receipt.generationIsolationProbeSha256, }), ); const generatedFromVite = qualifiedDeployConfig({ @@ -776,6 +863,19 @@ describe("Cloudflare static asset routing", () => { observation, }), ).toThrow(/runtime policy/u); + for (const [field, label] of [ + ["generationIsolationEvidenceSha256", /generation isolation evidence/u], + ["generationIsolationProbeSha256", /generation isolation probe/u], + ] as const) { + expect(() => + qualifiedDeployConfig({ + config: productionDeployConfig(), + receipt, + image, + observation: { ...observation, [field]: "0".repeat(64) }, + }), + ).toThrow(label); + } expect(() => qualifiedDeployConfig({ config: productionDeployConfig(), @@ -805,7 +905,7 @@ describe("Cloudflare static asset routing", () => { const evidenceCommit = "2".repeat(40); const image = `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`; const receipt = { - schemaVersion: "5", + schemaVersion: "6", status: "VERIFIED", generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, @@ -814,6 +914,12 @@ describe("Cloudflare static asset routing", () => { dockerfileSha256: "d".repeat(64), localImageTag: `counterlab-runner:git-${sourceCommit}`, localImageDigest: `sha256:${"e".repeat(64)}`, + ...generationIsolationQualification({ + sourceCommit, + sourceTreeSha256: "c".repeat(64), + localImageTag: `counterlab-runner:git-${sourceCommit}`, + localImageDigest: `sha256:${"e".repeat(64)}`, + }), ociRevision: sourceCommit, ociSourceTreeSha256: "c".repeat(64), engineAuthorityHash: "f".repeat(64), @@ -839,7 +945,7 @@ describe("Cloudflare static asset routing", () => { registryDigest: `sha256:${"3".repeat(64)}`, registryResolvedAt: "2026-07-16T16:20:00.000Z", qualifiedAt: "2026-07-16T16:30:00.000Z", - verifierVersion: "counterlab-release-v5", + verifierVersion: "counterlab-release-v6", }; const config = productionDeployConfig(); config.durable_objects.bindings = [ @@ -853,6 +959,12 @@ describe("Cloudflare static asset routing", () => { image, observation: { generationFilesystemReadIsolation: "OS_ENFORCED", + ...generationIsolationQualification({ + sourceCommit, + sourceTreeSha256: receipt.sourceTreeSha256, + localImageTag: receipt.localImageTag, + localImageDigest: receipt.localImageDigest, + }), sourceCommit, sourceArchiveSha256: receipt.sourceArchiveSha256, sourceTreeSha256: receipt.sourceTreeSha256, @@ -895,7 +1007,7 @@ describe("Cloudflare static asset routing", () => { const sourceCommit = "a".repeat(40); const image = `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`; const receipt = { - schemaVersion: "5", + schemaVersion: "6", status: "VERIFIED", generationFilesystemReadIsolation: "OS_ENFORCED", sourceCommit, @@ -904,6 +1016,12 @@ describe("Cloudflare static asset routing", () => { dockerfileSha256: "d".repeat(64), localImageTag: `counterlab-runner:git-${sourceCommit}`, localImageDigest: `sha256:${"e".repeat(64)}`, + ...generationIsolationQualification({ + sourceCommit, + sourceTreeSha256: "c".repeat(64), + localImageTag: `counterlab-runner:git-${sourceCommit}`, + localImageDigest: `sha256:${"e".repeat(64)}`, + }), ociRevision: sourceCommit, ociSourceTreeSha256: "c".repeat(64), engineAuthorityHash: "f".repeat(64), @@ -929,7 +1047,7 @@ describe("Cloudflare static asset routing", () => { registryDigest: `sha256:${"3".repeat(64)}`, registryResolvedAt: "2026-07-16T15:55:00.000Z", qualifiedAt: "2026-07-16T16:30:00.000Z", - verifierVersion: "counterlab-release-v5", + verifierVersion: "counterlab-release-v6", }; expect(() => @@ -942,6 +1060,12 @@ describe("Cloudflare static asset routing", () => { image, observation: { generationFilesystemReadIsolation: "OS_ENFORCED", + ...generationIsolationQualification({ + sourceCommit, + sourceTreeSha256: receipt.sourceTreeSha256, + localImageTag: receipt.localImageTag, + localImageDigest: receipt.localImageDigest, + }), sourceCommit, sourceArchiveSha256: "9".repeat(64), sourceTreeSha256: receipt.sourceTreeSha256, diff --git a/apps/web/worker/runner-container-env.test.ts b/apps/web/worker/runner-container-env.test.ts index 278ea4b1..652fdb18 100644 --- a/apps/web/worker/runner-container-env.test.ts +++ b/apps/web/worker/runner-container-env.test.ts @@ -17,6 +17,8 @@ describe("runner Container environment", () => { COUNTERLAB_RUNNER_SIGNING_PRIVATE_KEY: signingKeys.privateKey, COUNTERLAB_RUNNER_SOURCE_COMMIT: "b".repeat(40), COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "d".repeat(64), + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "e".repeat(64), }); expect(environment).toEqual({ @@ -37,6 +39,8 @@ describe("runner Container environment", () => { COUNTERLAB_RUNNER_ONE_SHOT: "1", COUNTERLAB_RUNNER_SOURCE_COMMIT: "b".repeat(40), COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "d".repeat(64), + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "e".repeat(64), }); }); diff --git a/apps/web/worker/runner-container-env.ts b/apps/web/worker/runner-container-env.ts index 5d4731fe..22c33d27 100644 --- a/apps/web/worker/runner-container-env.ts +++ b/apps/web/worker/runner-container-env.ts @@ -6,6 +6,8 @@ export type RunnerContainerSecretBindings = { COUNTERLAB_RUNNER_SIGNING_PRIVATE_KEY?: string; COUNTERLAB_RUNNER_SOURCE_COMMIT?: string; COUNTERLAB_RUNNER_IMAGE_DIGEST?: string; + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256?: string; + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256?: string; }; export function createRunnerContainerEnvVars( @@ -37,5 +39,9 @@ export function createRunnerContainerEnvVars( bindings.COUNTERLAB_RUNNER_SOURCE_COMMIT ?? "", COUNTERLAB_RUNNER_IMAGE_DIGEST: bindings.COUNTERLAB_RUNNER_IMAGE_DIGEST ?? "", + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: + bindings.COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256 ?? "", + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: + bindings.COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256 ?? "", }; } diff --git a/apps/web/worker/runner-control-plane.test.ts b/apps/web/worker/runner-control-plane.test.ts index 20a17ed7..8fc315c4 100644 --- a/apps/web/worker/runner-control-plane.test.ts +++ b/apps/web/worker/runner-control-plane.test.ts @@ -32,7 +32,14 @@ const job: RunnerJob = { }; const runnerImageDigest = `sha256:${"c".repeat(64)}`; const runnerSourceCommit = "b".repeat(40); -const runnerReleaseIdentity = { runnerSourceCommit, runnerImageDigest }; +const generationIsolationEvidenceSha256 = "d".repeat(64); +const generationIsolationProbeSha256 = "e".repeat(64); +const runnerReleaseIdentity = { + runnerSourceCommit, + runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, +}; describe("HttpRunnerDispatcher", () => { it("starts and probes the digest-bound Container readiness instance", async () => { @@ -44,6 +51,8 @@ describe("HttpRunnerDispatcher", () => { generationFilesystemReadIsolation: "OS_ENFORCED", runnerSourceCommit, runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, }), { status: 200, headers: { "content-type": "application/json" } }, ); @@ -57,7 +66,7 @@ describe("HttpRunnerDispatcher", () => { await expect(dispatcher.ready()).resolves.toBe(true); expect(getByName).toHaveBeenCalledWith( - `counterlab-readiness-${"c".repeat(64)}`, + `counterlab-readiness-${generationIsolationEvidenceSha256}`, ); expect(startAndWaitForPorts).toHaveBeenCalledWith({ ports: [8080], @@ -84,6 +93,8 @@ describe("HttpRunnerDispatcher", () => { ["invalid response", "schema"], ["stale source identity", "source"], ["stale image identity", "image"], + ["stale isolation evidence identity", "evidence"], + ["stale isolation probe identity", "probe"], ["partial filesystem isolation", "isolation"], ])("fails Container readiness closed on %s", async (_label, failure) => { const startAndWaitForPorts = vi.fn(async () => { @@ -105,6 +116,12 @@ describe("HttpRunnerDispatcher", () => { failure === "source" ? "d".repeat(40) : runnerSourceCommit, runnerImageDigest: failure === "image" ? `sha256:${"e".repeat(64)}` : runnerImageDigest, + generationIsolationEvidenceSha256: + failure === "evidence" + ? "f".repeat(64) + : generationIsolationEvidenceSha256, + generationIsolationProbeSha256: + failure === "probe" ? "f".repeat(64) : generationIsolationProbeSha256, }; return new Response(JSON.stringify(payload), { status: 200 }); }); @@ -120,11 +137,22 @@ describe("HttpRunnerDispatcher", () => { it.each([ [ "invalid source commit", - { runnerSourceCommit: "main", runnerImageDigest }, + { ...runnerReleaseIdentity, runnerSourceCommit: "main" }, ], [ "invalid image digest", - { runnerSourceCommit, runnerImageDigest: "counterlab-runner:latest" }, + { + ...runnerReleaseIdentity, + runnerImageDigest: "counterlab-runner:latest", + }, + ], + [ + "invalid isolation evidence hash", + { ...runnerReleaseIdentity, generationIsolationEvidenceSha256: "short" }, + ], + [ + "invalid isolation probe hash", + { ...runnerReleaseIdentity, generationIsolationProbeSha256: "short" }, ], ])("rejects %s before acquiring a Container", (_label, identity) => { const getByName = vi.fn(); @@ -328,6 +356,8 @@ describe("HttpRunnerDispatcher", () => { generationFilesystemReadIsolation: "OS_ENFORCED", runnerSourceCommit, runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, }), { status: 200 }, ), @@ -400,29 +430,60 @@ describe("HttpRunnerDispatcher", () => { }); it.each([ - ["stale source", "d".repeat(40), runnerImageDigest], - ["stale image", runnerSourceCommit, `sha256:${"e".repeat(64)}`], - ])("fails HTTP readiness closed for %s", async (_label, source, image) => { - const dispatcher = new HttpRunnerDispatcher({ - baseURL: "https://runner.example.test", - releaseIdentity: runnerReleaseIdentity, - fetch: vi.fn( - async () => - new Response( - JSON.stringify({ - status: "ready", - service: "counterlab-hosted-runner", - generationFilesystemReadIsolation: "OS_ENFORCED", - runnerSourceCommit: source, - runnerImageDigest: image, - }), - { status: 200 }, - ), - ), - }); - - await expect(dispatcher.ready()).resolves.toBe(false); - }); + [ + "stale source", + "d".repeat(40), + runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, + ], + [ + "stale image", + runnerSourceCommit, + `sha256:${"e".repeat(64)}`, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, + ], + [ + "stale isolation evidence", + runnerSourceCommit, + runnerImageDigest, + "f".repeat(64), + generationIsolationProbeSha256, + ], + [ + "stale isolation probe", + runnerSourceCommit, + runnerImageDigest, + generationIsolationEvidenceSha256, + "f".repeat(64), + ], + ])( + "fails HTTP readiness closed for %s", + async (_label, source, image, evidence, probe) => { + const dispatcher = new HttpRunnerDispatcher({ + baseURL: "https://runner.example.test", + releaseIdentity: runnerReleaseIdentity, + fetch: vi.fn( + async () => + new Response( + JSON.stringify({ + status: "ready", + service: "counterlab-hosted-runner", + generationFilesystemReadIsolation: "OS_ENFORCED", + runnerSourceCommit: source, + runnerImageDigest: image, + generationIsolationEvidenceSha256: evidence, + generationIsolationProbeSha256: probe, + }), + { status: 200 }, + ), + ), + }); + + await expect(dispatcher.ready()).resolves.toBe(false); + }, + ); it("requires an exact expected identity for an HTTP runner", () => { expect( @@ -432,6 +493,8 @@ describe("HttpRunnerDispatcher", () => { releaseIdentity: { runnerSourceCommit: "main", runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, }, }), ).toThrow(/source commit must be exact/i); diff --git a/apps/web/worker/runner-control-plane.ts b/apps/web/worker/runner-control-plane.ts index 1042e9df..b2d4db3a 100644 --- a/apps/web/worker/runner-control-plane.ts +++ b/apps/web/worker/runner-control-plane.ts @@ -40,6 +40,8 @@ export type RunnerContainerBinding = { export type RunnerReleaseIdentity = { runnerSourceCommit: string; runnerImageDigest: string; + generationIsolationEvidenceSha256: string; + generationIsolationProbeSha256: string; }; export type HttpRunnerDispatcherOptions = { @@ -90,6 +92,18 @@ function assertExactRunnerReleaseIdentity( if (!/^sha256:[a-f0-9]{64}$/u.test(releaseIdentity.runnerImageDigest)) { throw new Error("Runner image digest must be exact for readiness"); } + if (!/^[a-f0-9]{64}$/u.test(releaseIdentity.generationIsolationProbeSha256)) { + throw new Error( + "Generation-isolation probe hash must be exact for readiness", + ); + } + if ( + !/^[a-f0-9]{64}$/u.test(releaseIdentity.generationIsolationEvidenceSha256) + ) { + throw new Error( + "Generation-isolation evidence hash must be exact for readiness", + ); + } } async function runnerReadinessMatches( @@ -107,6 +121,8 @@ async function runnerReadinessMatches( JSON.stringify( [ "generationFilesystemReadIsolation", + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", "runnerImageDigest", "runnerSourceCommit", "service", @@ -119,6 +135,12 @@ async function runnerReadinessMatches( payload.service === "counterlab-hosted-runner" && "generationFilesystemReadIsolation" in payload && payload.generationFilesystemReadIsolation === "OS_ENFORCED" && + "generationIsolationEvidenceSha256" in payload && + payload.generationIsolationEvidenceSha256 === + releaseIdentity.generationIsolationEvidenceSha256 && + "generationIsolationProbeSha256" in payload && + payload.generationIsolationProbeSha256 === + releaseIdentity.generationIsolationProbeSha256 && "runnerSourceCommit" in payload && payload.runnerSourceCommit === releaseIdentity.runnerSourceCommit && "runnerImageDigest" in payload && @@ -209,7 +231,7 @@ export class CloudflareContainerRunnerDispatcher implements RunnerDispatcher { private readonly releaseIdentity: RunnerReleaseIdentity, ) { assertExactRunnerReleaseIdentity(releaseIdentity); - this.readinessInstanceName = `counterlab-readiness-${releaseIdentity.runnerImageDigest.slice("sha256:".length)}`; + this.readinessInstanceName = `counterlab-readiness-${releaseIdentity.generationIsolationEvidenceSha256}`; } async ready(): Promise { diff --git a/packages/scientific-engine-registry/src/schema.ts b/packages/scientific-engine-registry/src/schema.ts index 07b071d6..4efaaa9d 100644 --- a/packages/scientific-engine-registry/src/schema.ts +++ b/packages/scientific-engine-registry/src/schema.ts @@ -1127,7 +1127,7 @@ export const QualifiedRunnerReleaseV6Schema = z } }); -export const QualifiedRunnerReleaseSchema = QualifiedRunnerReleaseV5Schema; +export const QualifiedRunnerReleaseSchema = QualifiedRunnerReleaseV6Schema; export const RELEASE_CHECK_IDS = [ "test-all", @@ -1346,7 +1346,7 @@ export const ReleaseCheckReceiptV4Schema = z } }); -export const ReleaseCheckReceiptSchema = ReleaseCheckReceiptV3Schema; +export const ReleaseCheckReceiptSchema = ReleaseCheckReceiptV4Schema; export const DeploymentReceiptV3Schema = z .strictObject({ @@ -1609,4 +1609,4 @@ export const DeploymentReceiptV6Schema = z } }); -export const DeploymentReceiptSchema = DeploymentReceiptV5Schema; +export const DeploymentReceiptSchema = DeploymentReceiptV6Schema; diff --git a/packages/scientific-engine-registry/test/release-check-receipt.test.ts b/packages/scientific-engine-registry/test/release-check-receipt.test.ts index 76ffe9b7..3abd9ae4 100644 --- a/packages/scientific-engine-registry/test/release-check-receipt.test.ts +++ b/packages/scientific-engine-registry/test/release-check-receipt.test.ts @@ -7,7 +7,9 @@ import { describe, expect, it } from "vitest"; import { ReleaseCheckReceiptSchema, ReleaseCheckReceiptV2Schema, + ReleaseCheckReceiptV3Schema, } from "../src/index"; +import { createGenerationIsolationEvidence } from "../../../scripts/generation-isolation-evidence"; import { assertReleaseCheckBinding, @@ -19,11 +21,72 @@ import { const sourceCommit = "a".repeat(40); const evidenceCommit = "b".repeat(40); +function generationIsolation(verifiedAt = "2026-07-19T00:00:30.000Z") { + const probePayload = { + schemaVersion: "1", + probeVersion: "counterlab-generation-isolation-v1", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: [ + "entrypoint", + "non-root-user", + "immutable-paths", + "codex", + "python", + "bubblewrap", + "bubblewrap-read-isolation", + "setpriv", + "writable-roots", + ], + generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrapVersion: "0.11.0", + bubblewrap: { + forbiddenHostPathsHidden: true, + parentEnvironmentHidden: true, + workspaceVisible: true, + workspaceWritable: true, + }, + } as const; + return createGenerationIsolationEvidence({ + sourceCommit, + sourceTreeSha256: "2".repeat(64), + localImageTag: `counterlab-runner:git-${sourceCommit}`, + localImageDigest: `sha256:${"4".repeat(64)}`, + imageUser: "10001:10001", + verifiedAt, + startupProbe: { + status: "ready", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: probePayload.checks, + generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationProbe: probePayload, + generationIsolationProbeSha256: + "700cc58bedc163846e3854415170f49f55da9fd3ba316cc4967747d5268199dc", + }, + }); +} + +function generationIsolationFields() { + const result = generationIsolation(); + return { + generationIsolationEvidence: result.evidence, + generationIsolationEvidenceSha256: result.evidenceSha256, + generationIsolationProbeSha256: result.probeSha256, + generationIsolationVerifiedAt: result.evidence.verifiedAt, + } as const; +} + +function freshGenerationIsolationEvidence() { + return generationIsolation("2026-07-19T00:01:30.000Z").evidence; +} + function qualifiedReceipt() { return { - schemaVersion: "5", + schemaVersion: "6", status: "VERIFIED", generationFilesystemReadIsolation: "OS_ENFORCED", + ...generationIsolationFields(), sourceCommit, sourceArchiveSha256: "1".repeat(64), sourceTreeSha256: "2".repeat(64), @@ -64,7 +127,7 @@ function qualifiedReceipt() { registryDigest: `sha256:${"1".repeat(64)}`, registryResolvedAt: "2026-07-19T00:00:00.000Z", qualifiedAt: "2026-07-19T00:01:00.000Z", - verifierVersion: "counterlab-release-v5", + verifierVersion: "counterlab-release-v6", } as const; } @@ -110,11 +173,14 @@ function deploymentReceipt() { const runner = "d".repeat(40); const digest = `sha256:${"a".repeat(64)}`; return { - schemaVersion: "5", + schemaVersion: "6", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationEvidenceSha256: "0".repeat(64), + generationIsolationProbeSha256: "1".repeat(64), + generationIsolationVerifiedAt: "2026-07-18T23:58:00.000Z", workerEvidenceCommit: worker, runnerSourceCommit: runner, qualifiedRunnerReceiptSha256: "1".repeat(64), @@ -151,7 +217,7 @@ function deploymentReceipt() { workerVersionSha256: "c".repeat(64), containerStatusSha256: "d".repeat(64), deployedAt: "2026-07-19T00:00:00.000Z", - verifierVersion: "counterlab-deployment-v5", + verifierVersion: "counterlab-deployment-v6", } as const; } @@ -266,20 +332,51 @@ describe("release-check receipt", () => { worktreeClean: true, runnerImageDigest: qualifiedReceipt().localImageDigest, adapterImageDigest: qualifiedReceipt().adapterImageDigest, + generationIsolationEvidence: freshGenerationIsolationEvidence(), checkedAt: "2026-07-19T00:02:00.000Z", }); expect(receipt).toMatchObject({ - schemaVersion: "3", + schemaVersion: "4", status: "PASSED", generationFilesystemReadIsolation: "OS_ENFORCED", evidenceCommit, - verifierVersion: "counterlab-release-check-v3", + verifierVersion: "counterlab-release-check-v4", }); expect(receipt.checks).toHaveLength(11); }); - it("preserves v2 receipts while requiring OS isolation in v3", () => { + it("requires a fresh exact-image probe that matches qualification", () => { + const input = { + qualifiedReceipt: qualifiedReceipt(), + qualifiedReceiptBytes: qualifiedBytes(), + runtimeAttestation: runtimeAttestation(), + currentCommit: evidenceCommit, + worktreeClean: true, + runnerImageDigest: qualifiedReceipt().localImageDigest, + adapterImageDigest: qualifiedReceipt().adapterImageDigest, + checkedAt: "2026-07-19T00:02:00.000Z", + } as const; + expect(() => + createReleaseCheckReceipt({ + ...input, + generationIsolationEvidence: { + ...freshGenerationIsolationEvidence(), + sourceCommit: "0".repeat(40), + }, + }), + ).toThrow(/source commit|sourceCommit/iu); + expect(() => + createReleaseCheckReceipt({ + ...input, + generationIsolationEvidence: generationIsolation( + "2026-07-18T22:00:00.000Z", + ).evidence, + }), + ).toThrow(/not fresh/u); + }); + + it("preserves v3 and v2 receipts while requiring hash binding in v4", () => { const current = createReleaseCheckReceipt({ qualifiedReceipt: qualifiedReceipt(), qualifiedReceiptBytes: qualifiedBytes(), @@ -288,12 +385,26 @@ describe("release-check receipt", () => { worktreeClean: true, runnerImageDigest: qualifiedReceipt().localImageDigest, adapterImageDigest: qualifiedReceipt().adapterImageDigest, + generationIsolationEvidence: freshGenerationIsolationEvidence(), checkedAt: "2026-07-19T00:02:00.000Z", }); + const { + generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, + generationIsolationProbeSha256: _generationIsolationProbeSha256, + generationIsolationVerifiedAt: _generationIsolationVerifiedAt, + ...legacyV3 + } = current; + expect( + ReleaseCheckReceiptV3Schema.parse({ + ...legacyV3, + schemaVersion: "3", + verifierVersion: "counterlab-release-check-v3", + }), + ).toMatchObject({ schemaVersion: "3" }); const { generationFilesystemReadIsolation: _generationFilesystemReadIsolation, ...legacy - } = current; + } = legacyV3; expect( ReleaseCheckReceiptV2Schema.parse({ @@ -348,6 +459,7 @@ describe("release-check receipt", () => { worktreeClean: true, runnerImageDigest: qualifiedReceipt().localImageDigest, adapterImageDigest: qualifiedReceipt().adapterImageDigest, + generationIsolationEvidence: freshGenerationIsolationEvidence(), checkedAt: "2026-07-19T00:02:00.000Z", }); const common = { @@ -405,6 +517,31 @@ describe("release-check receipt", () => { }), ).toThrow(new RegExp(`live ${label.source}`, "u")); } + for (const [field, value, label] of [ + [ + "generationIsolationEvidenceSha256", + "0".repeat(64), + /generation isolation evidence/u, + ], + [ + "generationIsolationProbeSha256", + "0".repeat(64), + /generation isolation probe/u, + ], + [ + "generationIsolationVerifiedAt", + "2026-07-19T00:00:31.000Z", + /generation isolation verification/u, + ], + ] as const) { + expect(() => + assertReleaseCheckBinding({ + ...common, + releaseCheckReceipt: { ...receipt, [field]: value }, + runtimeAttestation: runtimeAttestation(), + }), + ).toThrow(label); + } }); it("rejects stale receipts and incomplete check sets", () => { @@ -416,6 +553,7 @@ describe("release-check receipt", () => { worktreeClean: true, runnerImageDigest: qualifiedReceipt().localImageDigest, adapterImageDigest: qualifiedReceipt().adapterImageDigest, + generationIsolationEvidence: freshGenerationIsolationEvidence(), checkedAt: "2026-07-19T00:02:00.000Z", }); const common = { diff --git a/packages/scientific-engine-registry/test/release-schema.test.ts b/packages/scientific-engine-registry/test/release-schema.test.ts index e134cfaa..626af6e2 100644 --- a/packages/scientific-engine-registry/test/release-schema.test.ts +++ b/packages/scientific-engine-registry/test/release-schema.test.ts @@ -6,11 +6,14 @@ import { DeploymentReceiptSchema, DeploymentReceiptV3Schema, DeploymentReceiptV4Schema, + DeploymentReceiptV5Schema, QualifiedRunnerReleaseSchema, QualifiedRunnerReleaseV2Schema, QualifiedRunnerReleaseV3Schema, QualifiedRunnerReleaseV4Schema, + QualifiedRunnerReleaseV5Schema, } from "../src/index"; +import { createGenerationIsolationEvidence } from "../../../scripts/generation-isolation-evidence"; import * as releaseTools from "../../../scripts/prepare-qualified-deploy"; function legacyReceipt() { @@ -34,12 +37,67 @@ function legacyReceipt() { }; } +function generationIsolationQualification(input: { + sourceCommit: string; + sourceTreeSha256: string; + localImageTag: string; + localImageDigest: string; + verifiedAt?: string; +}) { + const probePayload = { + schemaVersion: "1", + probeVersion: "counterlab-generation-isolation-v1", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: [ + "entrypoint", + "non-root-user", + "immutable-paths", + "codex", + "python", + "bubblewrap", + "bubblewrap-read-isolation", + "setpriv", + "writable-roots", + ], + generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrapVersion: "0.11.0", + bubblewrap: { + forbiddenHostPathsHidden: true, + parentEnvironmentHidden: true, + workspaceVisible: true, + workspaceWritable: true, + }, + } as const; + const result = createGenerationIsolationEvidence({ + ...input, + imageUser: "10001:10001", + verifiedAt: input.verifiedAt ?? "2026-07-16T16:23:00.000Z", + startupProbe: { + status: "ready", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: probePayload.checks, + generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationProbe: probePayload, + generationIsolationProbeSha256: + "700cc58bedc163846e3854415170f49f55da9fd3ba316cc4967747d5268199dc", + }, + }); + return { + generationIsolationEvidence: result.evidence, + generationIsolationEvidenceSha256: result.evidenceSha256, + generationIsolationProbeSha256: result.probeSha256, + generationIsolationVerifiedAt: result.evidence.verifiedAt, + } as const; +} + describe("qualified runner release schema", () => { it("requires an exact registry promotion digest", () => { expect(() => QualifiedRunnerReleaseSchema.parse(legacyReceipt())).toThrow(); }); - it("creates a runtime-bound promoted v5 receipt from recomputed evidence", () => { + it("creates a runtime-bound promoted v6 receipt from recomputed evidence", () => { const sourceCommit = "a".repeat(40); const evidenceCommit = "2".repeat(40); const registryImage = `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`; @@ -91,6 +149,12 @@ describe("qualified runner release schema", () => { timeoutVerifiedAt: "2026-07-16T16:24:00.000Z", currentCommit: evidenceCommit, generationFilesystemReadIsolation: "OS_ENFORCED", + ...generationIsolationQualification({ + sourceCommit, + sourceTreeSha256: "c".repeat(64), + localImageTag: `counterlab-runner:git-${sourceCommit}`, + localImageDigest: `sha256:${"e".repeat(64)}`, + }), sourceIsAncestor: true, changedPaths: ["scientific-engines/snapshot.json"], registryImage, @@ -102,7 +166,7 @@ describe("qualified runner release schema", () => { ); expect(QualifiedRunnerReleaseSchema.parse(receipt)).toMatchObject({ - schemaVersion: "5", + schemaVersion: "6", generationFilesystemReadIsolation: "OS_ENFORCED", evidenceCommit, registryImage, @@ -119,7 +183,7 @@ describe("qualified runner release schema", () => { ).toThrow(); }); - it("retains the exact historical v4 receipt contract", () => { + it("retains the exact historical v5 and v4 receipt contracts", () => { const sourceCommit = "a".repeat(40); const evidenceCommit = "2".repeat(40); const current = ( @@ -168,6 +232,12 @@ describe("qualified runner release schema", () => { timeoutVerifiedAt: "2026-07-16T16:24:00.000Z", currentCommit: evidenceCommit, generationFilesystemReadIsolation: "OS_ENFORCED", + ...generationIsolationQualification({ + sourceCommit, + sourceTreeSha256: "c".repeat(64), + localImageTag: `counterlab-runner:git-${sourceCommit}`, + localImageDigest: `sha256:${"e".repeat(64)}`, + }), sourceIsAncestor: true, changedPaths: ["scientific-engines/snapshot.json"], registryImage: `registry.cloudflare.com/account-1/counterlab-runner:git-${sourceCommit}`, @@ -177,10 +247,24 @@ describe("qualified runner release schema", () => { }, "2026-07-16T16:30:00.000Z", ); + const { + generationIsolationEvidence: _generationIsolationEvidence, + generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, + generationIsolationProbeSha256: _generationIsolationProbeSha256, + generationIsolationVerifiedAt: _generationIsolationVerifiedAt, + ...legacyV5 + } = current; + expect( + QualifiedRunnerReleaseV5Schema.parse({ + ...legacyV5, + schemaVersion: "5", + verifierVersion: "counterlab-release-v5", + }), + ).toMatchObject({ schemaVersion: "5" }); const { generationFilesystemReadIsolation: _generationFilesystemReadIsolation, ...legacyV4 - } = current; + } = legacyV5; expect( QualifiedRunnerReleaseV4Schema.parse({ @@ -307,11 +391,14 @@ describe("deployment receipt schema", () => { const runner = "b".repeat(40); const digest = `sha256:${"c".repeat(64)}`; return { - schemaVersion: "5", + schemaVersion: "6", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationEvidenceSha256: "0".repeat(64), + generationIsolationProbeSha256: "1".repeat(64), + generationIsolationVerifiedAt: "2026-07-19T00:01:00.000Z", workerEvidenceCommit: worker, runnerSourceCommit: runner, qualifiedRunnerReceiptSha256: "1".repeat(64), @@ -348,13 +435,13 @@ describe("deployment receipt schema", () => { workerVersionSha256: "b".repeat(64), containerStatusSha256: "c".repeat(64), deployedAt: "2026-07-19T00:03:00.000Z", - verifierVersion: "counterlab-deployment-v5", + verifierVersion: "counterlab-deployment-v6", } as const; } it("binds the deployed Worker, Container, release checks, and exact artifacts", () => { expect(DeploymentReceiptSchema.parse(receipt())).toMatchObject({ - schemaVersion: "5", + schemaVersion: "6", status: "DEPLOYED", workerName: "counterlab", }); @@ -378,10 +465,23 @@ describe("deployment receipt schema", () => { ).toThrow(); }); - it("retains the historical v4 PARTIAL isolation contract", () => { + it("retains the historical v5 and v4 isolation contracts", () => { + const { + generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, + generationIsolationProbeSha256: _generationIsolationProbeSha256, + generationIsolationVerifiedAt: _generationIsolationVerifiedAt, + ...legacyV5 + } = receipt(); + expect( + DeploymentReceiptV5Schema.parse({ + ...legacyV5, + schemaVersion: "5", + verifierVersion: "counterlab-deployment-v5", + }), + ).toMatchObject({ schemaVersion: "5" }); expect( DeploymentReceiptV4Schema.parse({ - ...receipt(), + ...legacyV5, schemaVersion: "4", generationFilesystemReadIsolation: "PARTIAL", verifierVersion: "counterlab-deployment-v4", @@ -409,6 +509,9 @@ describe("deployment receipt schema", () => { it("retains strict parsing for historical v3 deployment receipts", () => { const { + generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, + generationIsolationProbeSha256: _generationIsolationProbeSha256, + generationIsolationVerifiedAt: _generationIsolationVerifiedAt, timeoutCleanupReceiptSha256: _timeoutCleanupReceiptSha256, aggregateLimitEvidenceSha256: _aggregateLimitEvidenceSha256, runtimePolicySha256: _runtimePolicySha256, diff --git a/scripts/create-deployment-receipt.test.ts b/scripts/create-deployment-receipt.test.ts index f3779437..07ca88cd 100644 --- a/scripts/create-deployment-receipt.test.ts +++ b/scripts/create-deployment-receipt.test.ts @@ -97,6 +97,8 @@ describe("deployment receipt Worker identity", () => { workerEvidenceCommit: "a".repeat(40), runnerSourceCommit: "b".repeat(40), runnerImageDigest: `sha256:${"c".repeat(64)}`, + generationIsolationEvidenceSha256: "5".repeat(64), + generationIsolationProbeSha256: "6".repeat(64), timeoutCleanupReceiptSha256: "d".repeat(64), aggregateLimitEvidenceSha256: "9".repeat(64), runtimePolicySha256: "e".repeat(64), @@ -115,6 +117,14 @@ describe("deployment receipt Worker identity", () => { ["COUNTERLAB_WORKER_EVIDENCE_COMMIT", expected.workerEvidenceCommit], ["COUNTERLAB_RUNNER_SOURCE_COMMIT", expected.runnerSourceCommit], ["COUNTERLAB_RUNNER_IMAGE_DIGEST", expected.runnerImageDigest], + [ + "COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256", + expected.generationIsolationEvidenceSha256, + ], + [ + "COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256", + expected.generationIsolationProbeSha256, + ], [ "COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256", expected.timeoutCleanupReceiptSha256, @@ -159,6 +169,8 @@ describe("deployment receipt Worker identity", () => { for (const name of [ "COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256", + "COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256", + "COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256", "COUNTERLAB_RUNTIME_POLICY_SHA256", "COUNTERLAB_PROOF_DEPENDENCY_MANIFEST_SHA256", "COUNTERLAB_WORKER_ARTIFACT_MANIFEST_SHA256", @@ -208,6 +220,9 @@ describe("deployment receipt qualification binding", () => { proofDependencyManifestSha256: "3".repeat(64), runtimeAdapterSha256: "4".repeat(64), aggregateLimitEvidenceSha256: "5".repeat(64), + generationIsolationEvidenceSha256: "6".repeat(64), + generationIsolationProbeSha256: "7".repeat(64), + generationIsolationVerifiedAt: "2026-07-19T00:00:30.000Z", } as const; const releaseCheck = { generationFilesystemReadIsolation: @@ -228,6 +243,10 @@ describe("deployment receipt qualification binding", () => { proofDependencyManifestSha256: qualified.proofDependencyManifestSha256, runtimeAdapterSha256: qualified.runtimeAdapterSha256, aggregateLimitEvidenceSha256: qualified.aggregateLimitEvidenceSha256, + generationIsolationEvidenceSha256: + qualified.generationIsolationEvidenceSha256, + generationIsolationProbeSha256: qualified.generationIsolationProbeSha256, + generationIsolationVerifiedAt: qualified.generationIsolationVerifiedAt, } as const; const common = { qualified, @@ -247,6 +266,9 @@ describe("deployment receipt qualification binding", () => { ["aggregateLimitEvidenceSha256", /aggregate limit evidence/u], ["runtimePolicySha256", /runtime policy/u], ["proofDependencyManifestSha256", /proof dependency manifest/u], + ["generationIsolationEvidenceSha256", /generation isolation evidence/u], + ["generationIsolationProbeSha256", /generation isolation probe/u], + ["generationIsolationVerifiedAt", /generation isolation verification/u], ] as const) { expect(() => assertDeploymentReceiptBindings({ diff --git a/scripts/create-deployment-receipt.ts b/scripts/create-deployment-receipt.ts index 615591df..d1281e4f 100644 --- a/scripts/create-deployment-receipt.ts +++ b/scripts/create-deployment-receipt.ts @@ -5,14 +5,14 @@ import { dirname, isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { - DeploymentReceiptSchema, - QualifiedRunnerReleaseSchema, - ReleaseCheckReceiptSchema, + DeploymentReceiptV6Schema, + ReleaseCheckReceiptV4Schema, } from "../packages/scientific-engine-registry/src/index.js"; import type { - QualifiedRunnerRelease, - ReleaseCheckReceipt, + QualifiedRunnerReleaseV6, + ReleaseCheckReceiptV4, } from "../packages/scientific-engine-registry/src/index.js"; +import { parseQualifiedRunnerReleaseV6 } from "./generation-isolation-evidence.js"; import { collectFrozenClientAssets, verifyFrozenWorkerReleaseManifest, @@ -222,6 +222,8 @@ export function assertActiveWorkerReleaseBindings( workerEvidenceCommit: string; runnerSourceCommit: string; runnerImageDigest: string; + generationIsolationEvidenceSha256: string; + generationIsolationProbeSha256: string; timeoutCleanupReceiptSha256: string; aggregateLimitEvidenceSha256: string; runtimePolicySha256: string; @@ -244,6 +246,10 @@ export function assertActiveWorkerReleaseBindings( COUNTERLAB_WORKER_EVIDENCE_COMMIT: expected.workerEvidenceCommit, COUNTERLAB_RUNNER_SOURCE_COMMIT: expected.runnerSourceCommit, COUNTERLAB_RUNNER_IMAGE_DIGEST: expected.runnerImageDigest, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: + expected.generationIsolationEvidenceSha256, + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: + expected.generationIsolationProbeSha256, COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: expected.timeoutCleanupReceiptSha256, COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: @@ -277,7 +283,7 @@ export function assertActiveWorkerReleaseBindings( } type QualifiedDeploymentIdentity = Pick< - QualifiedRunnerRelease, + QualifiedRunnerReleaseV6, | "evidenceCommit" | "sourceCommit" | "registryDigest" @@ -291,10 +297,13 @@ type QualifiedDeploymentIdentity = Pick< | "proofDependencyManifestSha256" | "aggregateLimitEvidenceSha256" | "runtimeAdapterSha256" + | "generationIsolationEvidenceSha256" + | "generationIsolationProbeSha256" + | "generationIsolationVerifiedAt" >; type ReleaseCheckDeploymentIdentity = Pick< - ReleaseCheckReceipt, + ReleaseCheckReceiptV4, | "evidenceCommit" | "sourceCommit" | "qualifiedRunnerReceiptSha256" @@ -309,6 +318,9 @@ type ReleaseCheckDeploymentIdentity = Pick< | "proofDependencyManifestSha256" | "aggregateLimitEvidenceSha256" | "runtimeAdapterSha256" + | "generationIsolationEvidenceSha256" + | "generationIsolationProbeSha256" + | "generationIsolationVerifiedAt" >; export function assertDeploymentReceiptBindings(input: { @@ -355,6 +367,21 @@ export function assertDeploymentReceiptBindings(input: { input.releaseCheck.qualifiedAt, input.qualified.qualifiedAt, ], + [ + "generation isolation evidence", + input.releaseCheck.generationIsolationEvidenceSha256, + input.qualified.generationIsolationEvidenceSha256, + ], + [ + "generation isolation probe", + input.releaseCheck.generationIsolationProbeSha256, + input.qualified.generationIsolationProbeSha256, + ], + [ + "generation isolation verification time", + input.releaseCheck.generationIsolationVerifiedAt, + input.qualified.generationIsolationVerifiedAt, + ], [ "runner image tag", input.releaseCheck.runnerImageTag, @@ -474,13 +501,16 @@ async function main(): Promise { } const bindings = version.resources?.bindings; const qualifiedBytes = readFileSync(paths.qualified); - const qualified = QualifiedRunnerReleaseSchema.parse( + const qualified = parseQualifiedRunnerReleaseV6( JSON.parse(qualifiedBytes.toString("utf8")) as unknown, ); assertActiveWorkerReleaseBindings(bindings, { workerEvidenceCommit: args["--evidence-commit"], runnerSourceCommit: args["--source-commit"], runnerImageDigest: args["--registry-digest"], + generationIsolationEvidenceSha256: + qualified.generationIsolationEvidenceSha256, + generationIsolationProbeSha256: qualified.generationIsolationProbeSha256, timeoutCleanupReceiptSha256: qualified.timeoutCleanupReceiptSha256, aggregateLimitEvidenceSha256: qualified.aggregateLimitEvidenceSha256, runtimePolicySha256: qualified.runtimePolicySha256, @@ -507,7 +537,7 @@ async function main(): Promise { } const releaseCheckBytes = readFileSync(paths.releaseCheck); - const releaseCheck = ReleaseCheckReceiptSchema.parse( + const releaseCheck = ReleaseCheckReceiptV4Schema.parse( JSON.parse(releaseCheckBytes.toString("utf8")) as unknown, ); assertDeploymentReceiptBindings({ @@ -558,13 +588,17 @@ async function main(): Promise { } const dryRun = dryRunWorkerHash(root, paths.dryRun, workerBundleSha256); const configBytes = readFileSync(paths.config); - const receipt = DeploymentReceiptSchema.parse({ - schemaVersion: "5", + const receipt = DeploymentReceiptV6Schema.parse({ + schemaVersion: "6", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", generationFilesystemReadIsolation: releaseCheck.generationFilesystemReadIsolation, + generationIsolationEvidenceSha256: + releaseCheck.generationIsolationEvidenceSha256, + generationIsolationProbeSha256: releaseCheck.generationIsolationProbeSha256, + generationIsolationVerifiedAt: releaseCheck.generationIsolationVerifiedAt, workerEvidenceCommit: args["--evidence-commit"], runnerSourceCommit: args["--source-commit"], qualifiedRunnerReceiptSha256: sha256(qualifiedBytes), @@ -601,7 +635,7 @@ async function main(): Promise { workerVersionSha256: sha256(versionBytes), containerStatusSha256: sha256(containersBytes), deployedAt: new Date().toISOString(), - verifierVersion: "counterlab-deployment-v5", + verifierVersion: "counterlab-deployment-v6", }); await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", diff --git a/scripts/deploy-qualified.sh b/scripts/deploy-qualified.sh index 51a0a12a..710e9227 100755 --- a/scripts/deploy-qualified.sh +++ b/scripts/deploy-qualified.sh @@ -189,11 +189,14 @@ process.stdout.write(identity.receipt[field]); EVIDENCE_COMMIT="$(qualified_identity_value evidenceCommit)" SOURCE_COMMIT="$(qualified_identity_value sourceCommit)" LOCAL_IMAGE="$(qualified_identity_value localImageTag)" +LOCAL_IMAGE_DIGEST="$(qualified_identity_value localImageDigest)" REGISTRY_DIGEST="$(qualified_identity_value registryDigest)" TIMEOUT_CLEANUP_RECEIPT_SHA256="$(qualified_identity_value timeoutCleanupReceiptSha256)" AGGREGATE_LIMIT_EVIDENCE_SHA256="$(qualified_identity_value aggregateLimitEvidenceSha256)" RUNTIME_POLICY_SHA256="$(qualified_identity_value runtimePolicySha256)" PROOF_DEPENDENCY_MANIFEST_SHA256="$(qualified_identity_value proofDependencyManifestSha256)" +GENERATION_ISOLATION_EVIDENCE_SHA256="$(qualified_identity_value generationIsolationEvidenceSha256)" +GENERATION_ISOLATION_PROBE_SHA256="$(qualified_identity_value generationIsolationProbeSha256)" QUALIFIED_RECEIPT_SHA256="$( node -e ' const identity = JSON.parse(process.argv[1]); @@ -228,7 +231,9 @@ CLIENT_DIR="$(repo_path "apps/web/dist/client")" WORKER_ARTIFACT_MANIFEST="${RELEASE_DIR}/frozen-worker-release.json" mkdir -p "${RELEASE_DIR}" -./scripts/verify-scientific-engines.sh --image "${LOCAL_IMAGE}" +./scripts/verify-scientific-engines.sh \ + --image "${LOCAL_IMAGE}" \ + --expected-image-digest "${LOCAL_IMAGE_DIGEST}" "${PNPM}" --filter @counterlab/web build [[ -f "apps/web/dist/client/_headers" ]] || { @@ -409,9 +414,9 @@ wait_for_maintenance_health() { if "${CURL_BIN}" --silent --show-error --fail-with-body \ --connect-timeout 10 --max-time 20 \ --output "${candidate}" "${PRODUCTION_ORIGIN}/api/health" && - node - "${candidate}" "${EVIDENCE_COMMIT}" "${SOURCE_COMMIT}" "${REGISTRY_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${expected_version}" <<'NODE' + node - "${candidate}" "${EVIDENCE_COMMIT}" "${SOURCE_COMMIT}" "${REGISTRY_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${expected_version}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" <<'NODE' const fs = require("node:fs"); -const [path, evidenceCommit, sourceCommit, digest, timeoutReceipt, aggregateEvidence, runtimePolicy, proofManifest, artifactClassification, artifactManifest, workerBundle, clientAssets, clientAssetCount, publicAssets, publicAssetCount, viteVersion, wranglerVersion, expectedVersion] = process.argv.slice(2); +const [path, evidenceCommit, sourceCommit, digest, timeoutReceipt, aggregateEvidence, runtimePolicy, proofManifest, artifactClassification, artifactManifest, workerBundle, clientAssets, clientAssetCount, publicAssets, publicAssetCount, viteVersion, wranglerVersion, expectedVersion, isolationEvidence, isolationProbe] = process.argv.slice(2); const payload = JSON.parse(fs.readFileSync(path, "utf8")); const data = payload?.ok === true ? payload.data : undefined; if ( @@ -421,6 +426,8 @@ if ( data.release.workerEvidenceCommit !== evidenceCommit || data.release.runnerSourceCommit !== sourceCommit || data.release.runnerImageDigest !== digest || + data.release.generationIsolationEvidenceSha256 !== isolationEvidence || + data.release.generationIsolationProbeSha256 !== isolationProbe || data.release.timeoutCleanupReceiptSha256 !== timeoutReceipt || data.release.aggregateLimitEvidenceSha256 !== aggregateEvidence || data.release.runtimePolicySha256 !== runtimePolicy || @@ -459,9 +466,9 @@ wait_for_final_readiness() { if "${CURL_BIN}" --silent --show-error --fail-with-body \ --connect-timeout 10 --max-time 20 \ --output "${candidate}" "${PRODUCTION_ORIGIN}/ready" && - node - "${candidate}" "${EVIDENCE_COMMIT}" "${SOURCE_COMMIT}" "${REGISTRY_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${expected_version}" <<'NODE' + node - "${candidate}" "${EVIDENCE_COMMIT}" "${SOURCE_COMMIT}" "${REGISTRY_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${expected_version}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" <<'NODE' const fs = require("node:fs"); -const [path, evidenceCommit, sourceCommit, digest, timeoutReceipt, aggregateEvidence, runtimePolicy, proofManifest, artifactClassification, artifactManifest, workerBundle, clientAssets, clientAssetCount, publicAssets, publicAssetCount, viteVersion, wranglerVersion, expectedVersion] = process.argv.slice(2); +const [path, evidenceCommit, sourceCommit, digest, timeoutReceipt, aggregateEvidence, runtimePolicy, proofManifest, artifactClassification, artifactManifest, workerBundle, clientAssets, clientAssetCount, publicAssets, publicAssetCount, viteVersion, wranglerVersion, expectedVersion, isolationEvidence, isolationProbe] = process.argv.slice(2); const payload = JSON.parse(fs.readFileSync(path, "utf8")); if ( payload?.status !== "ready" || @@ -471,6 +478,8 @@ if ( payload.release.workerEvidenceCommit !== evidenceCommit || payload.release.runnerSourceCommit !== sourceCommit || payload.release.runnerImageDigest !== digest || + payload.release.generationIsolationEvidenceSha256 !== isolationEvidence || + payload.release.generationIsolationProbeSha256 !== isolationProbe || payload.release.timeoutCleanupReceiptSha256 !== timeoutReceipt || payload.release.aggregateLimitEvidenceSha256 !== aggregateEvidence || payload.release.runtimePolicySha256 !== runtimePolicy || diff --git a/scripts/prepare-qualified-deploy.ts b/scripts/prepare-qualified-deploy.ts index 9c7be94a..9a22e277 100644 --- a/scripts/prepare-qualified-deploy.ts +++ b/scripts/prepare-qualified-deploy.ts @@ -9,9 +9,13 @@ import { z } from "zod"; import { ContainedRuntimeAttestationSchema, - QualifiedRunnerReleaseSchema, + type GenerationIsolationEvidenceV1, } from "../packages/scientific-engine-registry/src/index.js"; import { canonicalJson } from "../packages/session-core/src/index.js"; +import { + parseQualifiedRunnerReleaseV6, + verifyGenerationIsolationEvidence, +} from "./generation-isolation-evidence.js"; import { assertReleaseCheckBinding } from "./release-check-receipt.js"; import { containedRuntimeAdapterArguments, @@ -69,6 +73,10 @@ export type RunnerReleaseObservation = { export type QualifiedReleaseObservation = RunnerReleaseObservation & { generationFilesystemReadIsolation: "OS_ENFORCED"; + generationIsolationEvidence: GenerationIsolationEvidenceV1; + generationIsolationEvidenceSha256: string; + generationIsolationProbeSha256: string; + generationIsolationVerifiedAt: string; limitMode: "container-cgroup-and-process-rlimit"; aggregateLimitIntentEnforced: true; aggregateLimitEvidenceSha256: string; @@ -267,6 +275,18 @@ function assertQualifiedObservation( "qualified source commit is not an ancestor of the evidence commit", ); } + verifyGenerationIsolationEvidence({ + evidence: observation.generationIsolationEvidence, + evidenceSha256: observation.generationIsolationEvidenceSha256, + expected: { + sourceCommit: observation.sourceCommit, + sourceTreeSha256: observation.sourceTreeSha256, + localImageTag: observation.localImageTag, + localImageDigest: observation.localImageDigest, + probeSha256: observation.generationIsolationProbeSha256, + verifiedAt: observation.generationIsolationVerifiedAt, + }, + }); assertEvidenceOnlyReleaseDelta(observation.changedPaths); } @@ -607,7 +627,7 @@ export async function collectQualifiedReleaseObservation(input: { root: string; receipt: unknown; }): Promise { - const receipt = QualifiedRunnerReleaseSchema.parse(input.receipt); + const receipt = parseQualifiedRunnerReleaseV6(input.receipt); const runtimeAdapter = process.env.COUNTERLAB_DOCKER_BIN; if (runtimeAdapter === undefined || runtimeAdapter.trim().length === 0) { throw new Error( @@ -651,6 +671,11 @@ export async function collectQualifiedReleaseObservation(input: { ...timeout, generationFilesystemReadIsolation: receipt.generationFilesystemReadIsolation, + generationIsolationEvidence: receipt.generationIsolationEvidence, + generationIsolationEvidenceSha256: + receipt.generationIsolationEvidenceSha256, + generationIsolationProbeSha256: receipt.generationIsolationProbeSha256, + generationIsolationVerifiedAt: receipt.generationIsolationVerifiedAt, }; } @@ -659,11 +684,16 @@ export function createQualifiedRunnerRelease( qualifiedAt = new Date().toISOString(), ): unknown { assertQualifiedObservation(observation); - return QualifiedRunnerReleaseSchema.parse({ - schemaVersion: "5", + return parseQualifiedRunnerReleaseV6({ + schemaVersion: "6", status: "VERIFIED", generationFilesystemReadIsolation: observation.generationFilesystemReadIsolation, + generationIsolationEvidence: observation.generationIsolationEvidence, + generationIsolationEvidenceSha256: + observation.generationIsolationEvidenceSha256, + generationIsolationProbeSha256: observation.generationIsolationProbeSha256, + generationIsolationVerifiedAt: observation.generationIsolationVerifiedAt, sourceCommit: observation.sourceCommit, sourceArchiveSha256: observation.sourceArchiveSha256, sourceTreeSha256: observation.sourceTreeSha256, @@ -704,7 +734,7 @@ export function createQualifiedRunnerRelease( registryDigest: observation.registryDigest, registryResolvedAt: observation.registryResolvedAt, qualifiedAt, - verifierVersion: "counterlab-release-v5", + verifierVersion: "counterlab-release-v6", }); } @@ -1083,7 +1113,7 @@ export function qualifiedDeployConfig(input: { observation: QualifiedReleaseObservation; }): Record { const config = canonicalReleaseConfig(input.config); - const receipt = QualifiedRunnerReleaseSchema.parse(input.receipt); + const receipt = parseQualifiedRunnerReleaseV6(input.receipt); const comparisons: Array<[string, string, string]> = [ ["source commit", receipt.sourceCommit, input.observation.sourceCommit], [ @@ -1231,6 +1261,21 @@ export function qualifiedDeployConfig(input: { receipt.timeoutVerifiedAt, input.observation.timeoutVerifiedAt, ], + [ + "generation isolation evidence", + receipt.generationIsolationEvidenceSha256, + input.observation.generationIsolationEvidenceSha256, + ], + [ + "generation isolation probe", + receipt.generationIsolationProbeSha256, + input.observation.generationIsolationProbeSha256, + ], + [ + "generation isolation verification time", + receipt.generationIsolationVerifiedAt, + input.observation.generationIsolationVerifiedAt, + ], ["registry image", receipt.registryImage, input.observation.registryImage], [ "registry digest", @@ -1338,6 +1383,10 @@ export function qualifiedDeployConfig(input: { COUNTERLAB_WORKER_EVIDENCE_COMMIT: receipt.evidenceCommit, COUNTERLAB_RUNNER_SOURCE_COMMIT: receipt.sourceCommit, COUNTERLAB_RUNNER_IMAGE_DIGEST: receipt.registryDigest, + COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: + receipt.generationIsolationEvidenceSha256, + COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: + receipt.generationIsolationProbeSha256, COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: receipt.timeoutCleanupReceiptSha256, COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: diff --git a/scripts/production-smoke.sh b/scripts/production-smoke.sh index 05fa148b..32496fbc 100755 --- a/scripts/production-smoke.sh +++ b/scripts/production-smoke.sh @@ -98,7 +98,7 @@ esac if [[ -z "${COUNTERLAB_DEPLOYMENT_RECEIPT:-}" ]]; then echo "Production smoke requires COUNTERLAB_DEPLOYMENT_RECEIPT." >&2 - echo "Use the schema-v4 receipt written by scripts/deploy-qualified.sh." >&2 + echo "Use the schema-v6 receipt written by scripts/deploy-qualified.sh." >&2 exit 2 fi DEPLOYMENT_RECEIPT="$(repo_path "${COUNTERLAB_DEPLOYMENT_RECEIPT}")" @@ -156,6 +156,9 @@ TIMEOUT_CLEANUP_RECEIPT_SHA256="$(deployment_identity_value timeoutCleanupReceip AGGREGATE_LIMIT_EVIDENCE_SHA256="$(deployment_identity_value aggregateLimitEvidenceSha256)" RUNTIME_POLICY_SHA256="$(deployment_identity_value runtimePolicySha256)" PROOF_DEPENDENCY_MANIFEST_SHA256="$(deployment_identity_value proofDependencyManifestSha256)" +GENERATION_ISOLATION_EVIDENCE_SHA256="$(deployment_identity_value generationIsolationEvidenceSha256)" +GENERATION_ISOLATION_PROBE_SHA256="$(deployment_identity_value generationIsolationProbeSha256)" +GENERATION_ISOLATION_VERIFIED_AT="$(deployment_identity_value generationIsolationVerifiedAt)" WORKER_ARTIFACT_CLASSIFICATION="$(deployment_identity_value workerArtifactClassification)" WORKER_ARTIFACT_MANIFEST_SHA256="$(deployment_identity_value workerArtifactManifestSha256)" WORKER_BUNDLE_SHA256="$(deployment_identity_value workerBundleSha256)" @@ -401,6 +404,9 @@ init_args+=(--timeout-cleanup-receipt-sha256 "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" init_args+=(--aggregate-limit-evidence-sha256 "${AGGREGATE_LIMIT_EVIDENCE_SHA256}") init_args+=(--runtime-policy-sha256 "${RUNTIME_POLICY_SHA256}") init_args+=(--proof-dependency-manifest-sha256 "${PROOF_DEPENDENCY_MANIFEST_SHA256}") +init_args+=(--generation-isolation-evidence-sha256 "${GENERATION_ISOLATION_EVIDENCE_SHA256}") +init_args+=(--generation-isolation-probe-sha256 "${GENERATION_ISOLATION_PROBE_SHA256}") +init_args+=(--generation-isolation-verified-at "${GENERATION_ISOLATION_VERIFIED_AT}") init_args+=(--worker-artifact-classification "${WORKER_ARTIFACT_CLASSIFICATION}") init_args+=(--worker-artifact-manifest-sha256 "${WORKER_ARTIFACT_MANIFEST_SHA256}") init_args+=(--worker-bundle-sha256 "${WORKER_BUNDLE_SHA256}") @@ -423,7 +429,7 @@ stage_started="$(timestamp)" --max-time 30 \ "${BASE_URL}/ready" \ >"${WORK_DIR}/ready.json" -python3 - "${WORK_DIR}/ready.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" <<'PY' +python3 - "${WORK_DIR}/ready.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" <<'PY' import json import pathlib import sys @@ -454,6 +460,8 @@ expected_release = { "workerEvidenceCommit": sys.argv[3], "runnerSourceCommit": sys.argv[4], "runnerImageDigest": sys.argv[5], + "generationIsolationEvidenceSha256": sys.argv[19], + "generationIsolationProbeSha256": sys.argv[20], "timeoutCleanupReceiptSha256": sys.argv[6], "aggregateLimitEvidenceSha256": sys.argv[7], "runtimePolicySha256": sys.argv[8], @@ -483,7 +491,7 @@ stage_started="$(timestamp)" --max-time 30 \ "${BASE_URL}/api/health?readiness=probe" \ >"${WORK_DIR}/health.json" -python3 - "${WORK_DIR}/health.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" <<'PY' +python3 - "${WORK_DIR}/health.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" <<'PY' import json import pathlib import sys @@ -518,6 +526,8 @@ expected_release = { "workerEvidenceCommit": sys.argv[3], "runnerSourceCommit": sys.argv[4], "runnerImageDigest": sys.argv[5], + "generationIsolationEvidenceSha256": sys.argv[19], + "generationIsolationProbeSha256": sys.argv[20], "timeoutCleanupReceiptSha256": sys.argv[6], "aggregateLimitEvidenceSha256": sys.argv[7], "runtimePolicySha256": sys.argv[8], diff --git a/scripts/production_smoke_report.py b/scripts/production_smoke_report.py index 893f088e..c9c45142 100644 --- a/scripts/production_smoke_report.py +++ b/scripts/production_smoke_report.py @@ -167,7 +167,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "privacy", }: raise ValueError("smoke report has unknown or missing fields") - if report["schemaVersion"] not in {"2", "3", "4"} or report[ + if report["schemaVersion"] not in {"2", "3", "4", "5"} or report[ "status" ] not in _REPORT_STATUSES: raise ValueError("smoke report schema or status is invalid") @@ -186,7 +186,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "containerImageDigest", "deploymentReceiptSha256", } - if report["schemaVersion"] in {"3", "4"}: + if report["schemaVersion"] in {"3", "4", "5"}: deployment_fields.update( { "timeoutCleanupReceiptSha256", @@ -194,7 +194,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "proofDependencyManifestSha256", } ) - if report["schemaVersion"] == "4": + if report["schemaVersion"] in {"4", "5"}: deployment_fields.update( { "aggregateLimitEvidenceSha256", @@ -209,6 +209,14 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "wranglerVersion", } ) + if report["schemaVersion"] == "5": + deployment_fields.update( + { + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt", + } + ) if not isinstance(deployment, dict) or set(deployment) != deployment_fields: raise ValueError("smoke deployment metadata is invalid") worker_version = deployment["workerVersion"] @@ -222,7 +230,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: if commit is not None and not _COMMIT.fullmatch(commit): raise ValueError(f"{field} is invalid") hash_fields = ["deploymentReceiptSha256"] - if report["schemaVersion"] in {"3", "4"}: + if report["schemaVersion"] in {"3", "4", "5"}: hash_fields.extend( [ "timeoutCleanupReceiptSha256", @@ -230,7 +238,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "proofDependencyManifestSha256", ] ) - if report["schemaVersion"] == "4": + if report["schemaVersion"] in {"4", "5"}: hash_fields.extend( [ "aggregateLimitEvidenceSha256", @@ -240,11 +248,18 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "clientPublicAssetsSha256", ] ) + if report["schemaVersion"] == "5": + hash_fields.extend( + [ + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + ] + ) for field in hash_fields: value = deployment[field] if value is not None and not _SHA256.fullmatch(value): raise ValueError(f"{field} is invalid") - if report["schemaVersion"] == "4": + if report["schemaVersion"] in {"4", "5"}: artifact_values = [ deployment[field] for field in ( @@ -283,6 +298,13 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: or public_count > client_count ): raise ValueError("client asset counts are invalid") + if report["schemaVersion"] == "5": + isolation_verified_at = deployment["generationIsolationVerifiedAt"] + if isolation_verified_at is not None: + _timestamp( + isolation_verified_at, + "generationIsolationVerifiedAt", + ) if report["privacy"] != { "containsSecrets": False, "containsRawNotebookBytes": False, @@ -359,6 +381,9 @@ def initialize_report( aggregate_limit_evidence_sha256: str | None = None, runtime_policy_sha256: str | None = None, proof_dependency_manifest_sha256: str | None = None, + generation_isolation_evidence_sha256: str | None = None, + generation_isolation_probe_sha256: str | None = None, + generation_isolation_verified_at: str | None = None, worker_artifact_classification: str | None = None, worker_artifact_manifest_sha256: str | None = None, worker_bundle_sha256: str | None = None, @@ -372,7 +397,7 @@ def initialize_report( ) -> dict[str, Any]: report = validate_report( { - "schemaVersion": "4", + "schemaVersion": "5", "status": "RUNNING", "baseUrl": _base_url(base_url), "startedAt": _timestamp(started_at, "startedAt"), @@ -390,6 +415,13 @@ def initialize_report( "proofDependencyManifestSha256": ( proof_dependency_manifest_sha256 ), + "generationIsolationEvidenceSha256": ( + generation_isolation_evidence_sha256 + ), + "generationIsolationProbeSha256": ( + generation_isolation_probe_sha256 + ), + "generationIsolationVerifiedAt": generation_isolation_verified_at, "workerArtifactClassification": worker_artifact_classification, "workerArtifactManifestSha256": worker_artifact_manifest_sha256, "workerBundleSha256": worker_bundle_sha256, @@ -481,6 +513,9 @@ def _parser() -> argparse.ArgumentParser: initialize.add_argument("--aggregate-limit-evidence-sha256") initialize.add_argument("--runtime-policy-sha256") initialize.add_argument("--proof-dependency-manifest-sha256") + initialize.add_argument("--generation-isolation-evidence-sha256") + initialize.add_argument("--generation-isolation-probe-sha256") + initialize.add_argument("--generation-isolation-verified-at") initialize.add_argument("--worker-artifact-classification") initialize.add_argument("--worker-artifact-manifest-sha256") initialize.add_argument("--worker-bundle-sha256") @@ -522,6 +557,15 @@ def main() -> int: proof_dependency_manifest_sha256=( arguments.proof_dependency_manifest_sha256 ), + generation_isolation_evidence_sha256=( + arguments.generation_isolation_evidence_sha256 + ), + generation_isolation_probe_sha256=( + arguments.generation_isolation_probe_sha256 + ), + generation_isolation_verified_at=( + arguments.generation_isolation_verified_at + ), worker_artifact_classification=( arguments.worker_artifact_classification ), diff --git a/scripts/qualify-runner-release.ts b/scripts/qualify-runner-release.ts index b4d7a1ca..c5b7df18 100644 --- a/scripts/qualify-runner-release.ts +++ b/scripts/qualify-runner-release.ts @@ -18,6 +18,7 @@ import { } from "./contained-runtime-attestation.mjs"; import { ContainedRuntimeAttestationSchema, + GenerationIsolationEvidenceV1Schema, assertCurrentGrypeReleaseEvidenceBinding, } from "../packages/scientific-engine-registry/src/index.js"; import { @@ -25,6 +26,10 @@ import { createQualifiedRunnerRelease, } from "./prepare-qualified-deploy.js"; import { validateTimeoutCleanupProof } from "./timeout-cleanup-receipt.js"; +import { + hashGenerationIsolationEvidence, + verifyGenerationIsolationEvidence, +} from "./generation-isolation-evidence.js"; import { SourceBoundBuildReceiptSchema, type SourceBoundBuildReceipt, @@ -322,6 +327,23 @@ async function promoteImage(input: { "registry image tag must match the built runner source commit", ); } + if ( + commandText( + input.root, + input.runtimeAdapter, + runtimeAdapterArguments([ + "image", + "inspect", + input.expectedConfigDigest, + "--format", + "{{.Id}}", + ]), + ) !== input.expectedConfigDigest + ) { + throw new Error( + "qualified local image digest is unavailable for promotion", + ); + } const wrangler = resolve(input.root, "node_modules/.bin/wrangler"); const credentials = RegistryCredentialsSchema.parse( JSON.parse( @@ -395,7 +417,11 @@ async function promoteImage(input: { ); execFileSync( input.runtimeAdapter, - runtimeAdapterArguments(["tag", input.localImage, input.registryImage]), + runtimeAdapterArguments([ + "tag", + input.expectedConfigDigest, + input.registryImage, + ]), { cwd: input.root, stdio: "inherit", timeout: 30_000 }, ); execFileSync( @@ -453,6 +479,10 @@ async function main(): Promise { throw new Error("runner qualification requires the verified Git root"); } const output = await repositoryOutputPath(root, args.output); + const isolationOutput = await repositoryOutputPath( + root, + `${output}.generation-isolation-${Date.now()}-${process.pid}.json`, + ); const buildReceiptPath = await existingRepositoryFile( root, args.buildReceipt, @@ -637,6 +667,10 @@ async function main(): Promise { "scripts/verify-scientific-engines.sh", "--image", buildReceipt.localImageTag, + "--expected-image-digest", + buildReceipt.localImageDigest, + "--generation-isolation-report", + isolationOutput, ], { cwd: root, @@ -644,6 +678,22 @@ async function main(): Promise { stdio: "inherit", }, ); + const readGenerationIsolation = async () => { + const evidence = GenerationIsolationEvidenceV1Schema.parse( + JSON.parse(await readFile(isolationOutput, "utf8")) as unknown, + ); + return verifyGenerationIsolationEvidence({ + evidence, + evidenceSha256: hashGenerationIsolationEvidence(evidence), + expected: { + sourceCommit: buildReceipt.sourceCommit, + sourceTreeSha256: buildReceipt.sourceTreeSha256, + localImageTag: buildReceipt.localImageTag, + localImageDigest: buildReceipt.localImageDigest, + }, + }); + }; + const initialGenerationIsolation = await readGenerationIsolation(); assertCleanWorktree(root); runtimeAttestation = observeRuntimeAttestation(root, runtimeAdapter); @@ -733,10 +783,24 @@ async function main(): Promise { adapterManifestDigest: buildReceipt.adapterManifestDigest, adapterOciArchiveSha256: buildReceipt.adapterOciArchiveSha256, }); + const generationIsolation = await readGenerationIsolation(); + if ( + generationIsolation.evidenceSha256 !== + initialGenerationIsolation.evidenceSha256 || + generationIsolation.probeSha256 !== initialGenerationIsolation.probeSha256 + ) { + throw new Error( + "generation-isolation evidence changed during qualification", + ); + } const observation = { ...releaseObservation, ...timeoutProof, generationFilesystemReadIsolation: "OS_ENFORCED" as const, + generationIsolationEvidence: generationIsolation.evidence, + generationIsolationEvidenceSha256: generationIsolation.evidenceSha256, + generationIsolationProbeSha256: generationIsolation.probeSha256, + generationIsolationVerifiedAt: generationIsolation.evidence.verifiedAt, }; for (const [label, built, observed] of [ [ diff --git a/scripts/release-check-receipt.ts b/scripts/release-check-receipt.ts index 30f5daa3..8ab77291 100644 --- a/scripts/release-check-receipt.ts +++ b/scripts/release-check-receipt.ts @@ -7,11 +7,18 @@ import { fileURLToPath } from "node:url"; import { ContainedRuntimeAttestationSchema, - DeploymentReceiptSchema, - QualifiedRunnerReleaseSchema, + DeploymentReceiptV5Schema, + DeploymentReceiptV6Schema, + GenerationIsolationEvidenceV1Schema, RELEASE_CHECK_IDS, - ReleaseCheckReceiptSchema, + QualifiedRunnerReleaseV5Schema, + ReleaseCheckReceiptV4Schema, } from "../packages/scientific-engine-registry/src/index.js"; +import { + hashGenerationIsolationEvidence, + parseQualifiedRunnerReleaseV6, + verifyGenerationIsolationEvidence, +} from "./generation-isolation-evidence.js"; import { containedRuntimeAdapterArguments, requireContainedRuntimeSessionId, @@ -34,9 +41,14 @@ function sha256(value: Buffer): string { } export function createQualifiedReleaseIdentity(receiptBytes: Buffer) { - const receipt = QualifiedRunnerReleaseSchema.parse( - JSON.parse(receiptBytes.toString("utf8")) as unknown, - ); + const value = JSON.parse(receiptBytes.toString("utf8")) as unknown; + const receipt = + typeof value === "object" && + value !== null && + "schemaVersion" in value && + value.schemaVersion === "6" + ? parseQualifiedRunnerReleaseV6(value) + : QualifiedRunnerReleaseV5Schema.parse(value); return { identitySchemaVersion: "1", receiptType: "qualified-runner-release", @@ -46,9 +58,14 @@ export function createQualifiedReleaseIdentity(receiptBytes: Buffer) { } export function createDeploymentReleaseIdentity(receiptBytes: Buffer) { - const receipt = DeploymentReceiptSchema.parse( - JSON.parse(receiptBytes.toString("utf8")) as unknown, - ); + const value = JSON.parse(receiptBytes.toString("utf8")) as unknown; + const receipt = + typeof value === "object" && + value !== null && + "schemaVersion" in value && + value.schemaVersion === "6" + ? DeploymentReceiptV6Schema.parse(value) + : DeploymentReceiptV5Schema.parse(value); return { identitySchemaVersion: "1", receiptType: "deployment-receipt", @@ -58,8 +75,8 @@ export function createDeploymentReleaseIdentity(receiptBytes: Buffer) { } export function assertReleaseCheckBinding(input: BindingInput) { - const qualified = QualifiedRunnerReleaseSchema.parse(input.qualifiedReceipt); - const releaseCheck = ReleaseCheckReceiptSchema.parse( + const qualified = parseQualifiedRunnerReleaseV6(input.qualifiedReceipt); + const releaseCheck = ReleaseCheckReceiptV4Schema.parse( input.releaseCheckReceipt, ); const runtime = ContainedRuntimeAttestationSchema.parse( @@ -74,6 +91,21 @@ export function assertReleaseCheckBinding(input: BindingInput) { sha256(input.qualifiedReceiptBytes), ], ["qualification time", releaseCheck.qualifiedAt, qualified.qualifiedAt], + [ + "generation isolation evidence", + releaseCheck.generationIsolationEvidenceSha256, + qualified.generationIsolationEvidenceSha256, + ], + [ + "generation isolation probe", + releaseCheck.generationIsolationProbeSha256, + qualified.generationIsolationProbeSha256, + ], + [ + "generation isolation verification time", + releaseCheck.generationIsolationVerifiedAt, + qualified.generationIsolationVerifiedAt, + ], ["runner image tag", releaseCheck.runnerImageTag, qualified.localImageTag], [ "runner image digest", @@ -180,18 +212,47 @@ export function createReleaseCheckReceipt(input: { worktreeClean: boolean; runnerImageDigest: string; adapterImageDigest: string; + generationIsolationEvidence: unknown; checkedAt?: string; }) { - const qualified = QualifiedRunnerReleaseSchema.parse(input.qualifiedReceipt); + const qualified = parseQualifiedRunnerReleaseV6(input.qualifiedReceipt); const runtime = ContainedRuntimeAttestationSchema.parse( input.runtimeAttestation, ); const checkedAt = input.checkedAt ?? new Date().toISOString(); - const receipt = ReleaseCheckReceiptSchema.parse({ - schemaVersion: "3", + const freshEvidence = GenerationIsolationEvidenceV1Schema.parse( + input.generationIsolationEvidence, + ); + verifyGenerationIsolationEvidence({ + evidence: freshEvidence, + evidenceSha256: hashGenerationIsolationEvidence(freshEvidence), + expected: { + sourceCommit: qualified.sourceCommit, + sourceTreeSha256: qualified.sourceTreeSha256, + localImageTag: qualified.localImageTag, + localImageDigest: qualified.localImageDigest, + probeSha256: qualified.generationIsolationProbeSha256, + }, + }); + const freshVerifiedAt = Date.parse(freshEvidence.verifiedAt); + const checkedAtMilliseconds = Date.parse(checkedAt); + if ( + !Number.isFinite(freshVerifiedAt) || + !Number.isFinite(checkedAtMilliseconds) || + freshVerifiedAt > checkedAtMilliseconds + 5 * 60_000 || + checkedAtMilliseconds - freshVerifiedAt > 30 * 60_000 + ) { + throw new Error("release-check generation-isolation evidence is not fresh"); + } + const receipt = ReleaseCheckReceiptV4Schema.parse({ + schemaVersion: "4", status: "PASSED", generationFilesystemReadIsolation: qualified.generationFilesystemReadIsolation, + generationIsolationEvidenceSha256: + qualified.generationIsolationEvidenceSha256, + generationIsolationProbeSha256: qualified.generationIsolationProbeSha256, + generationIsolationVerifiedAt: qualified.generationIsolationVerifiedAt, evidenceCommit: qualified.evidenceCommit, sourceCommit: qualified.sourceCommit, qualifiedRunnerReceiptSha256: sha256(input.qualifiedReceiptBytes), @@ -208,7 +269,7 @@ export function createReleaseCheckReceipt(input: { runtimeAdapterSha256: qualified.runtimeAdapterSha256, checks: RELEASE_CHECK_IDS.map((id) => ({ id, status: "PASSED" })), checkedAt, - verifierVersion: "counterlab-release-check-v3", + verifierVersion: "counterlab-release-check-v4", }); return assertReleaseCheckBinding({ qualifiedReceipt: qualified, @@ -288,23 +349,29 @@ function argumentsFrom(argv: string[]) { if ( flag === undefined || value === undefined || - !["--qualified", "--runtime-adapter", "--output"].includes(flag) || + ![ + "--qualified", + "--runtime-adapter", + "--generation-isolation-report", + "--output", + ].includes(flag) || values.has(flag) ) { throw new Error( - "Usage: release-check-receipt --qualified FILE --runtime-adapter FILE --output FILE", + "Usage: release-check-receipt --qualified FILE --runtime-adapter FILE --generation-isolation-report FILE --output FILE", ); } values.set(flag, value); } - if (values.size !== 3) { + if (values.size !== 4) { throw new Error( - "Usage: release-check-receipt --qualified FILE --runtime-adapter FILE --output FILE", + "Usage: release-check-receipt --qualified FILE --runtime-adapter FILE --generation-isolation-report FILE --output FILE", ); } return { qualified: values.get("--qualified")!, runtimeAdapter: values.get("--runtime-adapter")!, + generationIsolationReport: values.get("--generation-isolation-report")!, output: values.get("--output")!, }; } @@ -336,16 +403,18 @@ async function main(): Promise { return; } const args = argumentsFrom(process.argv.slice(2)); - const [qualifiedPath, runtimeAdapter, output] = await Promise.all([ - existingRepositoryFile(root, args.qualified), - existingRepositoryFile(root, args.runtimeAdapter), - repositoryOutput(root, args.output), - ]); + const [qualifiedPath, runtimeAdapter, generationIsolationReport, output] = + await Promise.all([ + existingRepositoryFile(root, args.qualified), + existingRepositoryFile(root, args.runtimeAdapter), + existingRepositoryFile(root, args.generationIsolationReport), + repositoryOutput(root, args.output), + ]); if (((await stat(runtimeAdapter)).mode & 0o111) === 0) { throw new Error("release-check runtime adapter is not executable"); } const qualifiedReceiptBytes = readFileSync(qualifiedPath); - const qualifiedReceipt = QualifiedRunnerReleaseSchema.parse( + const qualifiedReceipt = parseQualifiedRunnerReleaseV6( JSON.parse(qualifiedReceiptBytes.toString("utf8")) as unknown, ); const runtimeAttestation = JSON.parse( @@ -390,6 +459,9 @@ async function main(): Promise { ]).length === 0, runnerImageDigest, adapterImageDigest, + generationIsolationEvidence: JSON.parse( + readFileSync(generationIsolationReport, "utf8"), + ) as unknown, }); await writeFile(output, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", diff --git a/scripts/release-check.sh b/scripts/release-check.sh index 86acfa6c..94f68659 100755 --- a/scripts/release-check.sh +++ b/scripts/release-check.sh @@ -134,12 +134,18 @@ process.stdout.write(identity.receiptSha256); RELEASE_RECEIPT_DIR="${CACHE_ROOT}/releases" RELEASE_RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-$$" RELEASE_CHECK_RECEIPT="${COUNTERLAB_RELEASE_CHECK_RECEIPT_OUTPUT:-${RELEASE_RECEIPT_DIR}/release-check-${EVIDENCE_COMMIT}-${RELEASE_RUN_ID}.json}" +GENERATION_ISOLATION_REPORT="${RELEASE_RECEIPT_DIR}/generation-isolation-release-check-${SOURCE_COMMIT}-${RELEASE_RUN_ID}.json" +mkdir -p "${RELEASE_RECEIPT_DIR}" case "${RELEASE_CHECK_RECEIPT}" in /*) ;; *) RELEASE_CHECK_RECEIPT="${ROOT_DIR}/${RELEASE_CHECK_RECEIPT#./}" ;; esac -node scripts/assert-contained-path.mjs "${RELEASE_RECEIPT_DIR}" "${RELEASE_CHECK_RECEIPT}" -[[ ! -e "${RELEASE_CHECK_RECEIPT}" && ! -L "${RELEASE_CHECK_RECEIPT}" ]] || { +node scripts/assert-contained-path.mjs \ + "${RELEASE_RECEIPT_DIR}" \ + "${RELEASE_CHECK_RECEIPT}" \ + "${GENERATION_ISOLATION_REPORT}" +[[ ! -e "${RELEASE_CHECK_RECEIPT}" && ! -L "${RELEASE_CHECK_RECEIPT}" && \ + ! -e "${GENERATION_ISOLATION_REPORT}" && ! -L "${GENERATION_ISOLATION_REPORT}" ]] || { echo "Release-check receipt output already exists; refusing to overwrite it." >&2 exit 2 } @@ -160,7 +166,10 @@ bash scripts/run-mutations.sh leakage bash scripts/run-mutations.sh imbalance "${PNPM}" run held-out:check COUNTERLAB_SANDBOX_IMAGE="${ADAPTER_IMAGE}" bash scripts/sandbox-smoke.sh -bash scripts/verify-scientific-engines.sh --image "${ENGINE_IMAGE}" +bash scripts/verify-scientific-engines.sh \ + --image "${ENGINE_IMAGE}" \ + --expected-image-digest "${EXPECTED_IMAGE_DIGEST}" \ + --generation-isolation-report "${GENERATION_ISOLATION_REPORT}" "${PNPM}" --filter @counterlab/web build .venv/bin/python scripts/secret-scan.py \ apps/web/dist/counterlab/index.js \ @@ -176,11 +185,11 @@ if [[ -n "$(git status --porcelain=v1 --untracked-files=all)" ]]; then exit 2 fi -mkdir -p "${RELEASE_RECEIPT_DIR}" node scripts/assert-contained-path.mjs "${RELEASE_RECEIPT_DIR}" "${RELEASE_CHECK_RECEIPT}" node --import tsx scripts/release-check-receipt.ts \ --qualified "${RECEIPT}" \ --runtime-adapter "${RUNTIME_ADAPTER}" \ + --generation-isolation-report "${GENERATION_ISOLATION_REPORT}" \ --output "${RELEASE_CHECK_RECEIPT}" echo "Release checks passed. No repository secret pattern was detected." diff --git a/scripts/test_production_smoke.py b/scripts/test_production_smoke.py index 8d2c7232..3d73338f 100644 --- a/scripts/test_production_smoke.py +++ b/scripts/test_production_smoke.py @@ -41,6 +41,9 @@ def _deployment_identity(**overrides: object) -> dict[str, object]: "aggregate_limit_evidence_sha256": "9" * 64, "runtime_policy_sha256": "c" * 64, "proof_dependency_manifest_sha256": "d" * 64, + "generation_isolation_evidence_sha256": "5" * 64, + "generation_isolation_probe_sha256": "6" * 64, + "generation_isolation_verified_at": "2026-07-15T11:59:00Z", "worker_artifact_classification": "PROCESS_BOUND_PARTIAL", "worker_artifact_manifest_sha256": "1" * 64, "worker_bundle_sha256": "2" * 64, @@ -118,7 +121,9 @@ def _control_plane_validator_source(kind: str) -> str: '"${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" ' '"${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" ' '"${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" ' - '"${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" <<\'PY\'\n' + '"${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" ' + '"${GENERATION_ISOLATION_EVIDENCE_SHA256}" ' + '"${GENERATION_ISOLATION_PROBE_SHA256}" <<\'PY\'\n' ) start = script.index(marker) + len(marker) return script[start : script.index("\nPY\n", start)] @@ -146,11 +151,14 @@ def _deployment_receipt() -> dict[str, object]: runner = "b" * 40 digest = "sha256:" + "c" * 64 return { - "schemaVersion": "5", + "schemaVersion": "6", "status": "DEPLOYED", "workerName": "counterlab", "productionOrigin": "https://counterlab.cserules.workers.dev", "generationFilesystemReadIsolation": "OS_ENFORCED", + "generationIsolationEvidenceSha256": "6" * 64, + "generationIsolationProbeSha256": "7" * 64, + "generationIsolationVerifiedAt": "2026-07-18T23:58:00.000Z", "workerEvidenceCommit": worker, "runnerSourceCommit": runner, "qualifiedRunnerReceiptSha256": "1" * 64, @@ -189,7 +197,7 @@ def _deployment_receipt() -> dict[str, object]: "workerVersionSha256": "a" * 64, "containerStatusSha256": "b" * 64, "deployedAt": "2026-07-19T00:00:00.000Z", - "verifierVersion": "counterlab-deployment-v5", + "verifierVersion": "counterlab-deployment-v6", } @@ -265,6 +273,8 @@ def test_control_plane_validators_bind_exact_release_and_maintenance_state() -> public_asset_count = 25 vite_version = "8.1.4" wrangler_version = "4.110.0" + generation_isolation_evidence = "5" * 64 + generation_isolation_probe = "6" * 64 release = { "status": "bound", "workerVersionId": version, @@ -272,6 +282,8 @@ def test_control_plane_validators_bind_exact_release_and_maintenance_state() -> "workerEvidenceCommit": worker, "runnerSourceCommit": runner, "runnerImageDigest": digest, + "generationIsolationEvidenceSha256": generation_isolation_evidence, + "generationIsolationProbeSha256": generation_isolation_probe, "timeoutCleanupReceiptSha256": timeout_receipt, "aggregateLimitEvidenceSha256": aggregate_limit_evidence, "runtimePolicySha256": runtime_policy, @@ -347,6 +359,8 @@ def run_validator( str(public_asset_count), vite_version, wrangler_version, + generation_isolation_evidence, + generation_isolation_probe, ], check=False, capture_output=True, @@ -385,6 +399,8 @@ def run_validator( "clientPublicAssetCount", "viteVersion", "wranglerVersion", + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", ): tampered = json.loads(json.dumps(payload)) tampered_release = ( @@ -503,7 +519,7 @@ def test_smoke_report_is_atomic_idempotent_and_secret_free() -> None: **_deployment_identity(), ) assert initialized["status"] == "RUNNING" - assert initialized["schemaVersion"] == "4" + assert initialized["schemaVersion"] == "5" assert initialized["baseUrl"] == "https://counterlab.example.test" assert initialized["stages"] == [] @@ -528,7 +544,7 @@ def test_smoke_report_is_atomic_idempotent_and_secret_free() -> None: } -def test_smoke_report_versions_preserve_v2_v3_and_require_v4_artifact_bindings() -> None: +def test_smoke_report_versions_preserve_v2_v3_v4_and_require_v5_isolation_bindings() -> None: with tempfile.TemporaryDirectory() as destination: path = pathlib.Path(destination) / "production-smoke.json" current = initialize_report( @@ -537,7 +553,13 @@ def test_smoke_report_versions_preserve_v2_v3_and_require_v4_artifact_bindings() started_at="2026-07-15T12:00:00Z", **_deployment_identity(), ) - assert current["schemaVersion"] == "4" + assert current["schemaVersion"] == "5" + + v5_fields = ( + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt", + ) v4_fields = ( "aggregateLimitEvidenceSha256", @@ -552,7 +574,13 @@ def test_smoke_report_versions_preserve_v2_v3_and_require_v4_artifact_bindings() "wranglerVersion", ) - historical_v3 = json.loads(json.dumps(current)) + historical_v4 = json.loads(json.dumps(current)) + historical_v4["schemaVersion"] = "4" + for field in v5_fields: + historical_v4["deployment"].pop(field) + assert validate_report(historical_v4)["schemaVersion"] == "4" + + historical_v3 = json.loads(json.dumps(historical_v4)) historical_v3["schemaVersion"] = "3" for field in v4_fields: historical_v3["deployment"].pop(field) @@ -573,6 +601,11 @@ def test_smoke_report_versions_preserve_v2_v3_and_require_v4_artifact_bindings() with pytest.raises(ValueError, match="deployment metadata"): validate_report(expanded_v2) + expanded_v4 = json.loads(json.dumps(current)) + expanded_v4["schemaVersion"] = "4" + with pytest.raises(ValueError, match="deployment metadata"): + validate_report(expanded_v4) + incomplete_v3 = json.loads(json.dumps(historical_v3)) incomplete_v3["deployment"].pop("runtimePolicySha256") with pytest.raises(ValueError, match="deployment metadata"): @@ -583,10 +616,20 @@ def test_smoke_report_versions_preserve_v2_v3_and_require_v4_artifact_bindings() with pytest.raises(ValueError, match="proofDependencyManifestSha256"): validate_report(malformed_v3) - invalid_v4 = json.loads(json.dumps(current)) - invalid_v4["deployment"]["clientPublicAssetCount"] = 28 + malformed_v5_hash = json.loads(json.dumps(current)) + malformed_v5_hash["deployment"]["generationIsolationProbeSha256"] = "short" + with pytest.raises(ValueError, match="generationIsolationProbeSha256"): + validate_report(malformed_v5_hash) + + malformed_v5_time = json.loads(json.dumps(current)) + malformed_v5_time["deployment"]["generationIsolationVerifiedAt"] = "yesterday" + with pytest.raises(ValueError, match="generationIsolationVerifiedAt"): + validate_report(malformed_v5_time) + + invalid_v5 = json.loads(json.dumps(current)) + invalid_v5["deployment"]["clientPublicAssetCount"] = 28 with pytest.raises(ValueError, match="asset counts"): - validate_report(invalid_v4) + validate_report(invalid_v5) def test_smoke_report_rejects_conflicts_secrets_and_credential_urls() -> None: diff --git a/scripts/verify-scientific-engines.sh b/scripts/verify-scientific-engines.sh index afa379ce..e0f6d799 100755 --- a/scripts/verify-scientific-engines.sh +++ b/scripts/verify-scientific-engines.sh @@ -3,6 +3,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" IMAGE="" +EXPECTED_IMAGE_DIGEST="" REGISTRY_ONLY=0 REQUIRE_PRODUCTION=0 RUNTIME_REPORT="" @@ -22,6 +23,8 @@ Usage: ./scripts/verify-scientific-engines.sh [options] Options: --image IMAGE Verify the exact local runner image from inside its runtime. + --expected-image-digest SHA256 + Refuse to start unless IMAGE resolves to this immutable ID. --registry-only Skip Proof Capsule linkage and permit an omitted runtime image. --require-production Reject a local-candidate runtime manifest. --runtime-report PATH Persist the exact runtime report to a new contained file. @@ -41,6 +44,11 @@ while [[ $# -gt 0 ]]; do IMAGE="$2" shift 2 ;; + --expected-image-digest) + [[ $# -ge 2 ]] || { echo "--expected-image-digest requires a value" >&2; exit 2; } + EXPECTED_IMAGE_DIGEST="$2" + shift 2 + ;; --registry-only) REGISTRY_ONLY=1 shift @@ -161,6 +169,10 @@ if [[ ! "${IMAGE_DIGEST}" =~ ^sha256:[a-f0-9]{64}$ ]]; then echo "Runner image does not expose a valid sha256 image ID." >&2 exit 1 fi +if [[ -n "${EXPECTED_IMAGE_DIGEST}" && "${IMAGE_DIGEST}" != "${EXPECTED_IMAGE_DIGEST}" ]]; then + echo "Runner image does not match the required immutable image ID." >&2 + exit 1 +fi if [[ ! "${SOURCE_COMMIT}" =~ ^[a-f0-9]{40}$ ]]; then echo "Runner image has an unbound or invalid OCI source revision." >&2 exit 1 @@ -212,7 +224,7 @@ STARTUP_PROBE_OUTPUT="$("${DOCKER_COMMAND[@]}" run --rm --name "${STARTUP_CONTAI -e COUNTERLAB_RUNNER_STARTUP_PROBE=1 \ -e COUNTERLAB_RUNNER_WORK_ROOT=/counterlab-runtime/jobs \ -e COUNTERLAB_CODEX_HOME_ROOT=/counterlab-runtime/codex \ - "${IMAGE}")" + "${IMAGE_DIGEST}")" node -e ' const value = JSON.parse(process.argv[1]); if ( @@ -267,7 +279,7 @@ RUNTIME_VERIFICATION_OUTPUT="$("${DOCKER_COMMAND[@]}" run --rm --name "${RUNTIME --mount "type=bind,src=${ROOT_DIR}/scientific-engines,dst=/repo/scientific-engines,readonly" \ --workdir=/repo \ --entrypoint python \ - "${IMAGE}" \ + "${IMAGE_DIGEST}" \ /repo/scripts/verify_scientific_runtime.py \ --root /repo \ --image-digest "${IMAGE_DIGEST}" \ diff --git a/services/hosted-runner/src/server.test.ts b/services/hosted-runner/src/server.test.ts index 1d6046cc..f49e069b 100644 --- a/services/hosted-runner/src/server.test.ts +++ b/services/hosted-runner/src/server.test.ts @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { CODEX_ATTEMPT_TIMEOUT_MS, createHostedRunnerServer, + verifiedHostedRunnerReleaseIdentity, type HostedRunnerServerOptions, } from "./server.js"; @@ -108,9 +109,13 @@ describe("hosted runner HTTP service", () => { const processJob = vi.fn(async () => undefined); const runnerSourceCommit = "b".repeat(40); const runnerImageDigest = `sha256:${"c".repeat(64)}`; + const generationIsolationEvidenceSha256 = "d".repeat(64); + const generationIsolationProbeSha256 = "e".repeat(64); const baseUrl = await start(processJob, undefined, undefined, { runnerSourceCommit, runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, }); const ready = await fetch(`${baseUrl}/ready`); @@ -121,6 +126,8 @@ describe("hosted runner HTTP service", () => { generationFilesystemReadIsolation: "OS_ENFORCED", runnerSourceCommit, runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, }); const accepted = await fetch(`${baseUrl}/jobs`, { @@ -149,6 +156,38 @@ describe("hosted runner HTTP service", () => { }); }); + it("fails release identity closed on partial or stale isolation evidence", () => { + const exact = { + runnerSourceCommit: "b".repeat(40), + runnerImageDigest: `sha256:${"c".repeat(64)}`, + generationIsolationEvidenceSha256: "d".repeat(64), + generationIsolationProbeSha256: "e".repeat(64), + }; + expect(verifiedHostedRunnerReleaseIdentity(exact, "e".repeat(64))).toEqual( + exact, + ); + expect( + verifiedHostedRunnerReleaseIdentity( + { + runnerSourceCommit: "", + runnerImageDigest: "", + generationIsolationEvidenceSha256: "", + generationIsolationProbeSha256: "", + }, + "e".repeat(64), + ), + ).toBeUndefined(); + expect(() => + verifiedHostedRunnerReleaseIdentity( + { ...exact, generationIsolationEvidenceSha256: "" }, + "e".repeat(64), + ), + ).toThrow(/exact source commit/u); + expect(() => + verifiedHostedRunnerReleaseIdentity(exact, "f".repeat(64)), + ).toThrow(/startup probe/u); + }); + it("signals one-shot lifecycle cleanup only after the accepted job settles", async () => { let finishJob: (() => void) | undefined; const processJob = vi.fn( diff --git a/services/hosted-runner/src/server.ts b/services/hosted-runner/src/server.ts index aaa27cba..7b7b69fa 100644 --- a/services/hosted-runner/src/server.ts +++ b/services/hosted-runner/src/server.ts @@ -29,12 +29,16 @@ const DispatchSchema = z }) .strict(); +export type HostedRunnerReleaseIdentity = { + runnerSourceCommit: string; + runnerImageDigest: string; + generationIsolationEvidenceSha256: string; + generationIsolationProbeSha256: string; +}; + export type HostedRunnerServerOptions = { generationFilesystemReadIsolation: "OS_ENFORCED"; - releaseIdentity?: { - runnerSourceCommit: string; - runnerImageDigest: string; - }; + releaseIdentity?: HostedRunnerReleaseIdentity; authorizeToken( token: string, jobId: string, @@ -50,6 +54,30 @@ export type HostedRunnerServerOptions = { onJobSettled?(input: { jobId: string }): Promise | void; }; +export function verifiedHostedRunnerReleaseIdentity( + candidate: HostedRunnerReleaseIdentity, + observedProbeSha256: string, +): HostedRunnerReleaseIdentity | undefined { + const values = Object.values(candidate); + if (values.every((value) => value.length === 0)) return undefined; + if ( + !/^[a-f0-9]{40}$/u.test(candidate.runnerSourceCommit) || + !/^sha256:[a-f0-9]{64}$/u.test(candidate.runnerImageDigest) || + !/^[a-f0-9]{64}$/u.test(candidate.generationIsolationEvidenceSha256) || + !/^[a-f0-9]{64}$/u.test(candidate.generationIsolationProbeSha256) + ) { + throw new Error( + "Runner release identity must contain an exact source commit, image digest, generation-isolation evidence hash, and probe hash", + ); + } + if (candidate.generationIsolationProbeSha256 !== observedProbeSha256) { + throw new Error( + "Runner startup probe does not match the qualified generation-isolation evidence", + ); + } + return candidate; +} + type ActiveJob = { controller: AbortController; }; @@ -230,6 +258,10 @@ async function startProductionServer(): Promise { process.env.COUNTERLAB_RUNNER_SOURCE_COMMIT?.trim() ?? ""; const runnerImageDigest = process.env.COUNTERLAB_RUNNER_IMAGE_DIGEST?.trim() ?? ""; + const generationIsolationEvidenceSha256 = + process.env.COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256?.trim() ?? ""; + const generationIsolationProbeSha256 = + process.env.COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256?.trim() ?? ""; delete process.env.CODEX_AUTH_JSON; delete process.env.COUNTERLAB_RUNNER_VERIFYING_PUBLIC_KEY; if (authJson === undefined || authJson.trim().length === 0) { @@ -243,17 +275,15 @@ async function startProductionServer(): Promise { "COUNTERLAB_RUNNER_VERIFYING_PUBLIC_KEY is required by the hosted runner", ); } - const releaseIdentityMissing = - runnerSourceCommit.length === 0 && runnerImageDigest.length === 0; - if ( - !releaseIdentityMissing && - (!/^[a-f0-9]{40}$/u.test(runnerSourceCommit) || - !/^sha256:[a-f0-9]{64}$/u.test(runnerImageDigest)) - ) { - throw new Error( - "Runner release identity must contain an exact source commit and image digest", - ); - } + const releaseIdentity = verifiedHostedRunnerReleaseIdentity( + { + runnerSourceCommit, + runnerImageDigest, + generationIsolationEvidenceSha256, + generationIsolationProbeSha256, + }, + startupProbe.generationIsolationProbeSha256, + ); const workspaceRoot = process.env.COUNTERLAB_RUNNER_WORK_ROOT ?? "/work/jobs"; const codexHomeRoot = process.env.COUNTERLAB_CODEX_HOME_ROOT ?? "/run/counterlab-codex"; @@ -291,9 +321,7 @@ async function startProductionServer(): Promise { const server = createHostedRunnerServer({ generationFilesystemReadIsolation: startupProbe.generationFilesystemReadIsolation, - ...(releaseIdentityMissing - ? {} - : { releaseIdentity: { runnerSourceCommit, runnerImageDigest } }), + ...(releaseIdentity === undefined ? {} : { releaseIdentity }), async authorizeToken(scopedToken, jobId, purpose, controlPlaneOrigin) { try { await verifyRunnerJobToken(scopedToken, runnerVerifyingPublicKey, { From 9348093a15942e7fb59c2755d3e080a20785b5c0 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 04:19:20 +0530 Subject: [PATCH 014/143] fix(release): bind fresh isolation recheck --- apps/web/worker/api.test.ts | 101 +++-- apps/web/worker/api.ts | 25 ++ apps/web/worker/deployment-config.test.ts | 131 ++++++- .../scientific-engine-registry/src/schema.ts | 178 ++++++++- .../scientific-engine-registry/src/types.ts | 4 + .../test/generation-isolation-schema.test.ts | 24 ++ .../test/release-check-receipt.test.ts | 168 ++++++++- .../test/release-schema.test.ts | 74 +++- .../schemas/deployment-receipt-v7.schema.json | 261 +++++++++++++ .../release-check-receipt-v5.schema.json | 356 ++++++++++++++++++ scripts/create-deployment-receipt.test.ts | 104 ++++- scripts/create-deployment-receipt.ts | 79 +++- scripts/deploy-qualified.sh | 190 +++++++--- scripts/generate-contract-schemas.ts | 26 ++ scripts/prepare-qualified-deploy.ts | 51 ++- scripts/production-smoke.sh | 42 ++- scripts/production_smoke_report.py | 102 ++++- scripts/release-check-receipt.ts | 96 ++++- scripts/test_production_smoke.py | 155 +++++++- 19 files changed, 2015 insertions(+), 152 deletions(-) create mode 100644 scientific-engines/schemas/deployment-receipt-v7.schema.json create mode 100644 scientific-engines/schemas/release-check-receipt-v5.schema.json diff --git a/apps/web/worker/api.test.ts b/apps/web/worker/api.test.ts index 1fd56a5f..d45c96f8 100644 --- a/apps/web/worker/api.test.ts +++ b/apps/web/worker/api.test.ts @@ -10495,6 +10495,14 @@ describe("Cloudflare Worker API", () => { COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "5".repeat(64), COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat(64), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256: "7".repeat( + 64, + ), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat( + 64, + ), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT: + "2026-07-19T05:31:00.000+05:30", COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: "d".repeat(64), COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: "9".repeat(64), COUNTERLAB_RUNTIME_POLICY_SHA256: "e".repeat(64), @@ -10515,32 +10523,35 @@ describe("Cloudflare Worker API", () => { }, } as unknown as Env); - await expect(response.json()).resolves.toMatchObject({ - data: { - release: { - status: "bound", - workerVersionId, - workerVersionTag: `git-${workerEvidenceCommit}`, - workerEvidenceCommit, - runnerSourceCommit: "b".repeat(40), - runnerImageDigest: `sha256:${"c".repeat(64)}`, - generationIsolationEvidenceSha256: "5".repeat(64), - generationIsolationProbeSha256: "6".repeat(64), - timeoutCleanupReceiptSha256: "d".repeat(64), - aggregateLimitEvidenceSha256: "9".repeat(64), - runtimePolicySha256: "e".repeat(64), - proofDependencyManifestSha256: "f".repeat(64), - workerArtifactClassification: "PROCESS_BOUND_PARTIAL", - workerArtifactManifestSha256: "1".repeat(64), - workerBundleSha256: "2".repeat(64), - clientAssetsSha256: "3".repeat(64), - clientAssetCount: 27, - clientPublicAssetsSha256: "4".repeat(64), - clientPublicAssetCount: 25, - viteVersion: "8.1.4", - wranglerVersion: "4.110.0", - }, - }, + const releaseHealth = (await response.json()) as { + data: { release: unknown }; + }; + expect(releaseHealth.data.release).toEqual({ + status: "bound", + workerVersionId, + workerVersionTag: `git-${workerEvidenceCommit}`, + workerEvidenceCommit, + runnerSourceCommit: "b".repeat(40), + runnerImageDigest: `sha256:${"c".repeat(64)}`, + generationIsolationEvidenceSha256: "5".repeat(64), + generationIsolationProbeSha256: "6".repeat(64), + releaseCheckGenerationIsolationEvidenceSha256: "7".repeat(64), + releaseCheckGenerationIsolationProbeSha256: "6".repeat(64), + releaseCheckGenerationIsolationVerifiedAt: + "2026-07-19T05:31:00.000+05:30", + timeoutCleanupReceiptSha256: "d".repeat(64), + aggregateLimitEvidenceSha256: "9".repeat(64), + runtimePolicySha256: "e".repeat(64), + proofDependencyManifestSha256: "f".repeat(64), + workerArtifactClassification: "PROCESS_BOUND_PARTIAL", + workerArtifactManifestSha256: "1".repeat(64), + workerBundleSha256: "2".repeat(64), + clientAssetsSha256: "3".repeat(64), + clientAssetCount: 27, + clientPublicAssetsSha256: "4".repeat(64), + clientPublicAssetCount: 25, + viteVersion: "8.1.4", + wranglerVersion: "4.110.0", }); const mismatchedTag = await api.request("/api/health", undefined, { @@ -10549,6 +10560,14 @@ describe("Cloudflare Worker API", () => { COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "5".repeat(64), COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat(64), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256: "7".repeat( + 64, + ), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat( + 64, + ), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT: + "2026-07-19T05:31:00.000+05:30", COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: "d".repeat(64), COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: "9".repeat(64), COUNTERLAB_RUNTIME_POLICY_SHA256: "e".repeat(64), @@ -10593,6 +10612,29 @@ describe("Cloudflare Worker API", () => { { COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "0".repeat(63) }, { COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: undefined }, { COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "0".repeat(63) }, + { + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256: + undefined, + }, + { + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256: + "0".repeat(63), + }, + { + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256: undefined, + }, + { + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256: "0".repeat( + 64, + ), + }, + { + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT: undefined, + }, + { + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT: + "not-a-timestamp", + }, { COUNTERLAB_RUNTIME_POLICY_SHA256: undefined }, { COUNTERLAB_RUNTIME_POLICY_SHA256: "0".repeat(63) }, { COUNTERLAB_PROOF_DEPENDENCY_MANIFEST_SHA256: undefined }, @@ -10622,6 +10664,13 @@ describe("Cloudflare Worker API", () => { COUNTERLAB_RUNNER_IMAGE_DIGEST: `sha256:${"c".repeat(64)}`, COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256: "5".repeat(64), COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat(64), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256: + "7".repeat(64), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256: "6".repeat( + 64, + ), + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT: + "2026-07-19T05:31:00.000+05:30", COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: "d".repeat(64), COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: "9".repeat(64), COUNTERLAB_RUNTIME_POLICY_SHA256: "e".repeat(64), diff --git a/apps/web/worker/api.ts b/apps/web/worker/api.ts index 4c944c40..98715402 100644 --- a/apps/web/worker/api.ts +++ b/apps/web/worker/api.ts @@ -245,6 +245,9 @@ type WorkerBindings = Omit & { COUNTERLAB_RUNNER_IMAGE_DIGEST?: string; COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256?: string; COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256?: string; + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256?: string; + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256?: string; + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT?: string; COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256?: string; COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256?: string; COUNTERLAB_RUNTIME_POLICY_SHA256?: string; @@ -1068,6 +1071,9 @@ function releaseIdentity(context: Context): runnerImageDigest: string; generationIsolationEvidenceSha256: string; generationIsolationProbeSha256: string; + releaseCheckGenerationIsolationEvidenceSha256: string; + releaseCheckGenerationIsolationProbeSha256: string; + releaseCheckGenerationIsolationVerifiedAt: string; timeoutCleanupReceiptSha256: string; aggregateLimitEvidenceSha256: string; runtimePolicySha256: string; @@ -1091,6 +1097,15 @@ function releaseIdentity(context: Context): context.env?.COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256 ?? ""; const generationIsolationProbeSha256 = context.env?.COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256 ?? ""; + const releaseCheckGenerationIsolationEvidenceSha256 = + context.env + ?.COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256 ?? ""; + const releaseCheckGenerationIsolationProbeSha256 = + context.env?.COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256 ?? + ""; + const releaseCheckGenerationIsolationVerifiedAt = + context.env?.COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT ?? + ""; const timeoutCleanupReceiptSha256 = context.env?.COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256 ?? ""; const aggregateLimitEvidenceSha256 = @@ -1125,6 +1140,13 @@ function releaseIdentity(context: Context): !/^sha256:[a-f0-9]{64}$/u.test(runnerImageDigest) || !/^[a-f0-9]{64}$/u.test(generationIsolationEvidenceSha256) || !/^[a-f0-9]{64}$/u.test(generationIsolationProbeSha256) || + !/^[a-f0-9]{64}$/u.test(releaseCheckGenerationIsolationEvidenceSha256) || + releaseCheckGenerationIsolationProbeSha256 !== + generationIsolationProbeSha256 || + !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test( + releaseCheckGenerationIsolationVerifiedAt, + ) || + !Number.isFinite(Date.parse(releaseCheckGenerationIsolationVerifiedAt)) || !/^[a-f0-9]{64}$/u.test(timeoutCleanupReceiptSha256) || !/^[a-f0-9]{64}$/u.test(aggregateLimitEvidenceSha256) || !/^[a-f0-9]{64}$/u.test(runtimePolicySha256) || @@ -1154,6 +1176,9 @@ function releaseIdentity(context: Context): runnerImageDigest, generationIsolationEvidenceSha256, generationIsolationProbeSha256, + releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt, timeoutCleanupReceiptSha256, aggregateLimitEvidenceSha256, runtimePolicySha256, diff --git a/apps/web/worker/deployment-config.test.ts b/apps/web/worker/deployment-config.test.ts index 81865abf..7bd632d6 100644 --- a/apps/web/worker/deployment-config.test.ts +++ b/apps/web/worker/deployment-config.test.ts @@ -3,10 +3,11 @@ import { existsSync, readdirSync } from "node:fs"; import { relative, resolve } from "node:path"; import { describe, expect, it } from "vitest"; +import { RELEASE_CHECK_IDS } from "../../../packages/scientific-engine-registry/src/index"; import { bindFrozenWorkerRelease, - qualifiedDeployConfig, + qualifiedDeployConfig as generateQualifiedDeployConfig, } from "../../../scripts/prepare-qualified-deploy"; import { createGenerationIsolationEvidence } from "../../../scripts/generation-isolation-evidence"; @@ -198,6 +199,85 @@ function generationIsolationQualification(input: { } as const; } +function releaseCheckReceiptFor(qualified: { + evidenceCommit: string; + sourceCommit: string; + sourceTreeSha256: string; + qualifiedAt: string; + localImageTag: string; + localImageDigest: string; + adapterImageTag: string; + adapterImageDigest: string; + registryDigest: string; + runtimeToolchainSha256: string; + runtimePolicySha256: string; + proofDependencyManifestSha256: string; + aggregateLimitEvidenceSha256: string; + runtimeAdapterSha256: string; + generationIsolationEvidenceSha256: string; + generationIsolationProbeSha256: string; + generationIsolationVerifiedAt: string; +}) { + const fresh = generationIsolationQualification({ + sourceCommit: qualified.sourceCommit, + sourceTreeSha256: qualified.sourceTreeSha256, + localImageTag: qualified.localImageTag, + localImageDigest: qualified.localImageDigest, + verifiedAt: "2026-07-16T16:30:30.000Z", + }); + return { + schemaVersion: "5", + status: "PASSED", + generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationEvidenceSha256: + qualified.generationIsolationEvidenceSha256, + generationIsolationProbeSha256: qualified.generationIsolationProbeSha256, + generationIsolationVerifiedAt: qualified.generationIsolationVerifiedAt, + releaseCheckGenerationIsolationEvidence: fresh.generationIsolationEvidence, + releaseCheckGenerationIsolationEvidenceSha256: + fresh.generationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + fresh.generationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + fresh.generationIsolationVerifiedAt, + evidenceCommit: qualified.evidenceCommit, + sourceCommit: qualified.sourceCommit, + qualifiedRunnerReceiptSha256: "2".repeat(64), + qualifiedAt: qualified.qualifiedAt, + runnerImageTag: qualified.localImageTag, + runnerImageDigest: qualified.localImageDigest, + adapterImageTag: qualified.adapterImageTag, + adapterImageDigest: qualified.adapterImageDigest, + registryDigest: qualified.registryDigest, + runtimeToolchainSha256: qualified.runtimeToolchainSha256, + runtimePolicySha256: qualified.runtimePolicySha256, + proofDependencyManifestSha256: qualified.proofDependencyManifestSha256, + aggregateLimitEvidenceSha256: qualified.aggregateLimitEvidenceSha256, + runtimeAdapterSha256: qualified.runtimeAdapterSha256, + checks: RELEASE_CHECK_IDS.map((id) => ({ id, status: "PASSED" as const })), + checkedAt: "2026-07-16T16:31:00.000Z", + verifierVersion: "counterlab-release-check-v5", + } as const; +} + +function qualifiedDeployConfig( + input: Omit< + Parameters[0], + "releaseCheckReceipt" + > & { + releaseCheckReceipt?: unknown; + }, +) { + return generateQualifiedDeployConfig({ + ...input, + releaseCheckReceipt: + input.releaseCheckReceipt ?? + releaseCheckReceiptFor( + input.receipt as Parameters[0], + ), + }); +} + function luminance(color: string): number { const channels = [1, 3, 5].map((offset) => Number.parseInt(color.slice(offset, offset + 2), 16), @@ -310,6 +390,18 @@ describe("Cloudflare static asset routing", () => { expect(script).toContain("--containers-rollout immediate"); expect(script).toContain("deployments status"); expect(script).toContain("scripts/create-deployment-receipt.ts"); + expect(script).toContain("release-check-identity"); + expect(script).toContain( + "Release-check receipt bytes changed after identity validation.", + ); + expect(script).toContain( + "RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256", + ); + expect(script).toContain("RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT"); + expect(script).toContain( + 'exactKeys(payload, ["status", "service", "checks", "maintenance", "release"])', + ); + expect(script).toContain('exactKeys(payload, ["ok", "data"])'); expect(receiptVerifier).toContain("active.percentage !== 100"); expect(script).toContain("deployment-receipt.json"); expect(script).not.toContain("--containers-rollout gradual"); @@ -357,12 +449,10 @@ describe("Cloudflare static asset routing", () => { expect(script.slice(finalDeploy, finalReadiness)).toContain( "--containers-rollout none", ); - expect(script).toContain( - "data.release.workerVersionId !== expectedVersion", - ); - expect(script).toContain( - "payload.release.workerVersionId !== expectedVersion", - ); + expect(script.match(/workerVersionId: expectedVersion/gu)).toHaveLength(2); + expect( + script.match(/releaseCheckGenerationIsolationEvidenceSha256:/gu), + ).toHaveLength(2); expect(script).toContain( 'rollback "${recovery_version}" \\\n --config "${RECOVERY_CONFIG}"', ); @@ -674,6 +764,7 @@ describe("Cloudflare static asset routing", () => { qualifiedAt: "2026-07-16T16:30:00.000Z", verifierVersion: "counterlab-release-v6", }; + const releaseCheckReceipt = releaseCheckReceiptFor(receipt); const observation = { generationFilesystemReadIsolation: "OS_ENFORCED" as const, ...generationIsolationQualification({ @@ -719,6 +810,7 @@ describe("Cloudflare static asset routing", () => { const generated = qualifiedDeployConfig({ config: productionDeployConfig(), receipt, + releaseCheckReceipt, image, observation, }); @@ -750,8 +842,29 @@ describe("Cloudflare static asset routing", () => { receipt.generationIsolationEvidenceSha256, COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: receipt.generationIsolationProbeSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256: + releaseCheckReceipt.releaseCheckGenerationIsolationEvidenceSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256: + releaseCheckReceipt.releaseCheckGenerationIsolationProbeSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT: + releaseCheckReceipt.releaseCheckGenerationIsolationVerifiedAt, }), ); + expect(() => + qualifiedDeployConfig({ + config: productionDeployConfig(), + receipt, + releaseCheckReceipt: { + ...releaseCheckReceipt, + releaseCheckGenerationIsolationEvidence: { + ...releaseCheckReceipt.releaseCheckGenerationIsolationEvidence, + sourceTreeSha256: "0".repeat(64), + }, + }, + image, + observation, + }), + ).toThrow(/evidence hash mismatch/u); const generatedFromVite = qualifiedDeployConfig({ config: viteGeneratedDeployConfig(), receipt, @@ -947,6 +1060,7 @@ describe("Cloudflare static asset routing", () => { qualifiedAt: "2026-07-16T16:30:00.000Z", verifierVersion: "counterlab-release-v6", }; + const releaseCheckReceipt = releaseCheckReceiptFor(receipt); const config = productionDeployConfig(); config.durable_objects.bindings = [ { name: "RUNNER", class_name: "CounterLabRunner" }, @@ -956,6 +1070,7 @@ describe("Cloudflare static asset routing", () => { qualifiedDeployConfig({ config, receipt, + releaseCheckReceipt, image, observation: { generationFilesystemReadIsolation: "OS_ENFORCED", @@ -1049,6 +1164,7 @@ describe("Cloudflare static asset routing", () => { qualifiedAt: "2026-07-16T16:30:00.000Z", verifierVersion: "counterlab-release-v6", }; + const releaseCheckReceipt = releaseCheckReceiptFor(receipt); expect(() => qualifiedDeployConfig({ @@ -1057,6 +1173,7 @@ describe("Cloudflare static asset routing", () => { containers: [{ class_name: "CounterLabRunner" }], }, receipt, + releaseCheckReceipt, image, observation: { generationFilesystemReadIsolation: "OS_ENFORCED", diff --git a/packages/scientific-engine-registry/src/schema.ts b/packages/scientific-engine-registry/src/schema.ts index 4efaaa9d..3be20819 100644 --- a/packages/scientific-engine-registry/src/schema.ts +++ b/packages/scientific-engine-registry/src/schema.ts @@ -1346,7 +1346,106 @@ export const ReleaseCheckReceiptV4Schema = z } }); -export const ReleaseCheckReceiptSchema = ReleaseCheckReceiptV4Schema; +export const ReleaseCheckReceiptV5Schema = z + .strictObject({ + ...ReleaseCheckReceiptV4Schema.shape, + schemaVersion: z.literal("5"), + releaseCheckGenerationIsolationEvidence: + GenerationIsolationEvidenceV1Schema, + releaseCheckGenerationIsolationEvidenceSha256: Sha256Schema, + releaseCheckGenerationIsolationProbeSha256: Sha256Schema, + releaseCheckGenerationIsolationVerifiedAt: z.iso.datetime({ offset: true }), + verifierVersion: z.literal("counterlab-release-check-v5"), + }) + .superRefine((receipt, context) => { + const { + releaseCheckGenerationIsolationEvidence: + _releaseCheckGenerationIsolationEvidence, + releaseCheckGenerationIsolationEvidenceSha256: + _releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + _releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + _releaseCheckGenerationIsolationVerifiedAt, + ...legacyReceipt + } = receipt; + const result = ReleaseCheckReceiptV4Schema.safeParse({ + ...legacyReceipt, + schemaVersion: "4", + verifierVersion: "counterlab-release-check-v4", + }); + if (!result.success) { + for (const issue of result.error.issues) { + context.addIssue({ + code: "custom", + path: issue.path, + message: issue.message, + }); + } + } + const evidence = receipt.releaseCheckGenerationIsolationEvidence; + for (const [field, expected, observed] of [ + ["sourceCommit", receipt.sourceCommit, evidence.sourceCommit], + ["runnerImageTag", receipt.runnerImageTag, evidence.localImageTag], + [ + "runnerImageDigest", + receipt.runnerImageDigest, + evidence.localImageDigest, + ], + [ + "releaseCheckGenerationIsolationProbeSha256", + receipt.releaseCheckGenerationIsolationProbeSha256, + evidence.probePayloadSha256, + ], + [ + "releaseCheckGenerationIsolationVerifiedAt", + receipt.releaseCheckGenerationIsolationVerifiedAt, + evidence.verifiedAt, + ], + ] as const) { + if (expected !== observed) { + context.addIssue({ + code: "custom", + path: [field], + message: `${field} must match release-check isolation evidence`, + }); + } + } + if ( + Date.parse(receipt.releaseCheckGenerationIsolationVerifiedAt) > + Date.parse(receipt.checkedAt) + ) { + context.addIssue({ + code: "custom", + path: ["releaseCheckGenerationIsolationVerifiedAt"], + message: "release-check isolation proof must precede receipt issuance", + }); + } + if ( + Date.parse(receipt.releaseCheckGenerationIsolationVerifiedAt) < + Date.parse(receipt.qualifiedAt) + ) { + context.addIssue({ + code: "custom", + path: ["releaseCheckGenerationIsolationVerifiedAt"], + message: "release-check isolation proof must follow qualification", + }); + } + if ( + Date.parse(receipt.checkedAt) - + Date.parse(receipt.releaseCheckGenerationIsolationVerifiedAt) > + 30 * 60_000 + ) { + context.addIssue({ + code: "custom", + path: ["releaseCheckGenerationIsolationVerifiedAt"], + message: + "release-check isolation proof must be within 30 minutes of receipt issuance", + }); + } + }); + +export const ReleaseCheckReceiptSchema = ReleaseCheckReceiptV5Schema; export const DeploymentReceiptV3Schema = z .strictObject({ @@ -1609,4 +1708,79 @@ export const DeploymentReceiptV6Schema = z } }); -export const DeploymentReceiptSchema = DeploymentReceiptV6Schema; +export const DeploymentReceiptV7Schema = z + .strictObject({ + ...DeploymentReceiptV6Schema.shape, + schemaVersion: z.literal("7"), + releaseCheckGenerationIsolationEvidenceSha256: Sha256Schema, + releaseCheckGenerationIsolationProbeSha256: Sha256Schema, + releaseCheckGenerationIsolationVerifiedAt: z.iso.datetime({ offset: true }), + verifierVersion: z.literal("counterlab-deployment-v7"), + }) + .superRefine((receipt, context) => { + const { + releaseCheckGenerationIsolationEvidenceSha256: + _releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + _releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + _releaseCheckGenerationIsolationVerifiedAt, + ...legacyReceipt + } = receipt; + const result = DeploymentReceiptV6Schema.safeParse({ + ...legacyReceipt, + schemaVersion: "6", + verifierVersion: "counterlab-deployment-v6", + }); + if (!result.success) { + for (const issue of result.error.issues) { + context.addIssue({ + code: "custom", + path: issue.path, + message: issue.message, + }); + } + } + if ( + receipt.releaseCheckGenerationIsolationProbeSha256 !== + receipt.generationIsolationProbeSha256 + ) { + context.addIssue({ + code: "custom", + path: ["releaseCheckGenerationIsolationProbeSha256"], + message: "release-check isolation probe must match the qualified probe", + }); + } + if ( + Date.parse(receipt.releaseCheckGenerationIsolationVerifiedAt) > + Date.parse(receipt.deployedAt) + ) { + context.addIssue({ + code: "custom", + path: ["releaseCheckGenerationIsolationVerifiedAt"], + message: "release-check isolation proof must precede deployment", + }); + } + if ( + Date.parse(receipt.releaseCheckGenerationIsolationVerifiedAt) > + Date.parse(receipt.releaseCheckCheckedAt) + ) { + context.addIssue({ + code: "custom", + path: ["releaseCheckGenerationIsolationVerifiedAt"], + message: "release-check isolation proof must precede receipt issuance", + }); + } + if ( + Date.parse(receipt.releaseCheckGenerationIsolationVerifiedAt) < + Date.parse(receipt.generationIsolationVerifiedAt) + ) { + context.addIssue({ + code: "custom", + path: ["releaseCheckGenerationIsolationVerifiedAt"], + message: "release-check isolation proof must follow qualification", + }); + } + }); + +export const DeploymentReceiptSchema = DeploymentReceiptV7Schema; diff --git a/packages/scientific-engine-registry/src/types.ts b/packages/scientific-engine-registry/src/types.ts index 8a801a6a..c6d33d54 100644 --- a/packages/scientific-engine-registry/src/types.ts +++ b/packages/scientific-engine-registry/src/types.ts @@ -8,6 +8,7 @@ import { DeploymentReceiptV4Schema, DeploymentReceiptV5Schema, DeploymentReceiptV6Schema, + DeploymentReceiptV7Schema, GenerationIsolationEvidenceSchema, GenerationIsolationEvidenceV1Schema, GenerationIsolationProbePayloadSchema, @@ -31,6 +32,7 @@ import { ReleaseCheckReceiptV2Schema, ReleaseCheckReceiptV3Schema, ReleaseCheckReceiptV4Schema, + ReleaseCheckReceiptV5Schema, SubjectPackAuthorityBindingSchema, SubjectPackEngineBindingSchema, SubjectPackEngineBindingsSchema, @@ -48,6 +50,7 @@ export type DeploymentReceiptV3 = z.infer; export type DeploymentReceiptV4 = z.infer; export type DeploymentReceiptV5 = z.infer; export type DeploymentReceiptV6 = z.infer; +export type DeploymentReceiptV7 = z.infer; export type GenerationIsolationEvidence = z.infer< typeof GenerationIsolationEvidenceSchema >; @@ -116,3 +119,4 @@ export type ReleaseCheckReceiptV1 = z.infer; export type ReleaseCheckReceiptV2 = z.infer; export type ReleaseCheckReceiptV3 = z.infer; export type ReleaseCheckReceiptV4 = z.infer; +export type ReleaseCheckReceiptV5 = z.infer; diff --git a/packages/scientific-engine-registry/test/generation-isolation-schema.test.ts b/packages/scientific-engine-registry/test/generation-isolation-schema.test.ts index 33b0170a..4f72c0f8 100644 --- a/packages/scientific-engine-registry/test/generation-isolation-schema.test.ts +++ b/packages/scientific-engine-registry/test/generation-isolation-schema.test.ts @@ -6,9 +6,11 @@ import { z } from "zod"; import { DeploymentReceiptV6Schema, + DeploymentReceiptV7Schema, GenerationIsolationEvidenceV1Schema, QualifiedRunnerReleaseV6Schema, ReleaseCheckReceiptV4Schema, + ReleaseCheckReceiptV5Schema, } from "../src/index.js"; const sourceCommit = "a".repeat(40); @@ -112,6 +114,26 @@ describe("generation isolation evidence schema", () => { ]), ); } + for (const [schema, version] of [ + [ReleaseCheckReceiptV5Schema, "5"], + [DeploymentReceiptV7Schema, "7"], + ] as const) { + const jsonSchema = z.toJSONSchema(schema) as { + properties?: Record; + required?: string[]; + }; + expect(jsonSchema.properties?.schemaVersion?.const).toBe(version); + expect(jsonSchema.required).toEqual( + expect.arrayContaining([ + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + "releaseCheckGenerationIsolationVerifiedAt", + ...(version === "5" + ? ["releaseCheckGenerationIsolationEvidence"] + : []), + ]), + ); + } }); it("publishes the evidence and next-version receipt schemas", () => { @@ -119,7 +141,9 @@ describe("generation isolation evidence schema", () => { "generation-isolation-evidence-v1.schema.json", "qualified-runner-release-v6.schema.json", "release-check-receipt-v4.schema.json", + "release-check-receipt-v5.schema.json", "deployment-receipt-v6.schema.json", + "deployment-receipt-v7.schema.json", ]) { const absolute = resolve( process.cwd(), diff --git a/packages/scientific-engine-registry/test/release-check-receipt.test.ts b/packages/scientific-engine-registry/test/release-check-receipt.test.ts index 3abd9ae4..049b5e79 100644 --- a/packages/scientific-engine-registry/test/release-check-receipt.test.ts +++ b/packages/scientific-engine-registry/test/release-check-receipt.test.ts @@ -8,6 +8,7 @@ import { ReleaseCheckReceiptSchema, ReleaseCheckReceiptV2Schema, ReleaseCheckReceiptV3Schema, + ReleaseCheckReceiptV4Schema, } from "../src/index"; import { createGenerationIsolationEvidence } from "../../../scripts/generation-isolation-evidence"; @@ -15,6 +16,7 @@ import { assertReleaseCheckBinding, createDeploymentReleaseIdentity, createQualifiedReleaseIdentity, + createReleaseCheckReleaseIdentity, createReleaseCheckReceipt, } from "../../../scripts/release-check-receipt"; @@ -173,7 +175,7 @@ function deploymentReceipt() { const runner = "d".repeat(40); const digest = `sha256:${"a".repeat(64)}`; return { - schemaVersion: "6", + schemaVersion: "7", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", @@ -181,11 +183,14 @@ function deploymentReceipt() { generationIsolationEvidenceSha256: "0".repeat(64), generationIsolationProbeSha256: "1".repeat(64), generationIsolationVerifiedAt: "2026-07-18T23:58:00.000Z", + releaseCheckGenerationIsolationEvidenceSha256: "2".repeat(64), + releaseCheckGenerationIsolationProbeSha256: "1".repeat(64), + releaseCheckGenerationIsolationVerifiedAt: "2026-07-18T23:59:30.000Z", workerEvidenceCommit: worker, runnerSourceCommit: runner, qualifiedRunnerReceiptSha256: "1".repeat(64), releaseCheckReceiptSha256: "2".repeat(64), - releaseCheckCheckedAt: "2026-07-18T23:59:00.000Z", + releaseCheckCheckedAt: "2026-07-18T23:59:45.000Z", timeoutCleanupReceiptSha256: "b".repeat(64), aggregateLimitEvidenceSha256: "6".repeat(64), runtimeToolchainSha256: "3".repeat(64), @@ -217,7 +222,7 @@ function deploymentReceipt() { workerVersionSha256: "c".repeat(64), containerStatusSha256: "d".repeat(64), deployedAt: "2026-07-19T00:00:00.000Z", - verifierVersion: "counterlab-deployment-v6", + verifierVersion: "counterlab-deployment-v7", } as const; } @@ -308,6 +313,103 @@ describe("release-check receipt", () => { ).toThrow(/Worker tag/u); }); + it("reads historical deployment v6 and v5 identities without changing them", () => { + const { + releaseCheckGenerationIsolationEvidenceSha256: + _releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + _releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + _releaseCheckGenerationIsolationVerifiedAt, + ...withoutFreshReleaseCheck + } = deploymentReceipt(); + const v6 = { + ...withoutFreshReleaseCheck, + schemaVersion: "6", + verifierVersion: "counterlab-deployment-v6", + } as const; + expect( + createDeploymentReleaseIdentity(Buffer.from(JSON.stringify(v6))).receipt, + ).toMatchObject({ schemaVersion: "6" }); + + const { + generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, + generationIsolationProbeSha256: _generationIsolationProbeSha256, + generationIsolationVerifiedAt: _generationIsolationVerifiedAt, + ...withoutQualificationIsolation + } = v6; + const v5 = { + ...withoutQualificationIsolation, + schemaVersion: "5", + verifierVersion: "counterlab-deployment-v5", + } as const; + expect( + createDeploymentReleaseIdentity(Buffer.from(JSON.stringify(v5))).receipt, + ).toMatchObject({ schemaVersion: "5" }); + }); + + it("hands current and historical release-check identities across without rewriting bytes", () => { + const current = createReleaseCheckReceipt({ + qualifiedReceipt: qualifiedReceipt(), + qualifiedReceiptBytes: qualifiedBytes(), + runtimeAttestation: runtimeAttestation(), + currentCommit: evidenceCommit, + worktreeClean: true, + runnerImageDigest: qualifiedReceipt().localImageDigest, + adapterImageDigest: qualifiedReceipt().adapterImageDigest, + generationIsolationEvidence: freshGenerationIsolationEvidence(), + checkedAt: "2026-07-19T00:02:00.000Z", + }); + const currentBytes = Buffer.from(`${JSON.stringify(current, null, 2)}\n`); + expect(createReleaseCheckReleaseIdentity(currentBytes)).toMatchObject({ + receiptType: "release-check-receipt", + receiptSha256: createHash("sha256").update(currentBytes).digest("hex"), + receipt: { schemaVersion: "5" }, + }); + + const { + releaseCheckGenerationIsolationEvidence: + _releaseCheckGenerationIsolationEvidence, + releaseCheckGenerationIsolationEvidenceSha256: + _releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + _releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + _releaseCheckGenerationIsolationVerifiedAt, + ...currentWithoutFreshEvidence + } = current; + const historical = ReleaseCheckReceiptV4Schema.parse({ + ...currentWithoutFreshEvidence, + schemaVersion: "4", + verifierVersion: "counterlab-release-check-v4", + }); + expect(() => + ReleaseCheckReceiptSchema.parse(currentWithoutFreshEvidence), + ).toThrow(); + expect(() => + assertReleaseCheckBinding({ + qualifiedReceipt: qualifiedReceipt(), + qualifiedReceiptBytes: qualifiedBytes(), + releaseCheckReceipt: { + ...current, + releaseCheckGenerationIsolationEvidenceSha256: "0".repeat(64), + }, + runtimeAttestation: runtimeAttestation(), + currentCommit: evidenceCommit, + worktreeClean: true, + runnerImageDigest: qualifiedReceipt().localImageDigest, + adapterImageDigest: qualifiedReceipt().adapterImageDigest, + observedAt: "2026-07-19T00:03:00.000Z", + }), + ).toThrow(/evidence hash mismatch/u); + const historicalBytes = Buffer.from(JSON.stringify(historical)); + expect(createReleaseCheckReleaseIdentity(historicalBytes)).toMatchObject({ + receiptType: "release-check-receipt", + receiptSha256: createHash("sha256").update(historicalBytes).digest("hex"), + receipt: { schemaVersion: "4" }, + }); + }); + it("uses named receipt accessors instead of positional release tuples", () => { const releaseCheck = readFileSync("scripts/release-check.sh", "utf8"); const deploy = readFileSync("scripts/deploy-qualified.sh", "utf8"); @@ -337,11 +439,14 @@ describe("release-check receipt", () => { }); expect(receipt).toMatchObject({ - schemaVersion: "4", + schemaVersion: "5", status: "PASSED", generationFilesystemReadIsolation: "OS_ENFORCED", + releaseCheckGenerationIsolationProbeSha256: + generationIsolation().probeSha256, + releaseCheckGenerationIsolationVerifiedAt: "2026-07-19T00:01:30.000Z", evidenceCommit, - verifierVersion: "counterlab-release-check-v4", + verifierVersion: "counterlab-release-check-v5", }); expect(receipt.checks).toHaveLength(11); }); @@ -374,9 +479,17 @@ describe("release-check receipt", () => { ).evidence, }), ).toThrow(/not fresh/u); + expect(() => + createReleaseCheckReceipt({ + ...input, + generationIsolationEvidence: generationIsolation( + "2026-07-19T00:00:45.000Z", + ).evidence, + }), + ).toThrow(/follow qualification/u); }); - it("preserves v3 and v2 receipts while requiring hash binding in v4", () => { + it("preserves v4, v3, and v2 receipts while requiring fresh evidence in v5", () => { const current = createReleaseCheckReceipt({ qualifiedReceipt: qualifiedReceipt(), qualifiedReceiptBytes: qualifiedBytes(), @@ -388,12 +501,30 @@ describe("release-check receipt", () => { generationIsolationEvidence: freshGenerationIsolationEvidence(), checkedAt: "2026-07-19T00:02:00.000Z", }); + const { + releaseCheckGenerationIsolationEvidence: + _releaseCheckGenerationIsolationEvidence, + releaseCheckGenerationIsolationEvidenceSha256: + _releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + _releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + _releaseCheckGenerationIsolationVerifiedAt, + ...legacyV4 + } = current; + expect( + ReleaseCheckReceiptV4Schema.parse({ + ...legacyV4, + schemaVersion: "4", + verifierVersion: "counterlab-release-check-v4", + }), + ).toMatchObject({ schemaVersion: "4" }); const { generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, generationIsolationProbeSha256: _generationIsolationProbeSha256, generationIsolationVerifiedAt: _generationIsolationVerifiedAt, ...legacyV3 - } = current; + } = legacyV4; expect( ReleaseCheckReceiptV3Schema.parse({ ...legacyV3, @@ -421,7 +552,7 @@ describe("release-check receipt", () => { ).toThrow(); }); - it("publishes distinct historical v2 and isolation-enforced v3 schemas", () => { + it("publishes historical schemas and the fresh-evidence v5 schema", () => { const v2Path = resolve( process.cwd(), "scientific-engines/schemas/release-check-receipt-v2.schema.json", @@ -430,13 +561,19 @@ describe("release-check receipt", () => { process.cwd(), "scientific-engines/schemas/release-check-receipt-v3.schema.json", ); + const v5Path = resolve( + process.cwd(), + "scientific-engines/schemas/release-check-receipt-v5.schema.json", + ); expect(existsSync(v2Path)).toBe(true); expect(existsSync(v3Path)).toBe(true); + expect(existsSync(v5Path)).toBe(true); const v2 = JSON.parse(readFileSync(v2Path, "utf8")) as { properties?: Record; required?: string[]; }; const v3 = JSON.parse(readFileSync(v3Path, "utf8")) as typeof v2; + const v5 = JSON.parse(readFileSync(v5Path, "utf8")) as typeof v2; expect(v2.properties?.schemaVersion?.const).toBe("2"); expect(v2.properties).not.toHaveProperty( @@ -448,6 +585,11 @@ describe("release-check receipt", () => { "OS_ENFORCED", ); expect(v3.required).toContain("generationFilesystemReadIsolation"); + expect(v5.properties?.schemaVersion?.const).toBe("5"); + expect(v5.required).toContain( + "releaseCheckGenerationIsolationEvidenceSha256", + ); + expect(v5.required).toContain("releaseCheckGenerationIsolationVerifiedAt"); }); it("rejects a different qualified receipt or live runtime", () => { @@ -573,6 +715,16 @@ describe("release-check receipt", () => { observedAt: "2026-07-20T00:03:01.000Z", }), ).toThrow(/stale/u); + expect(() => + assertReleaseCheckBinding({ + ...common, + releaseCheckReceipt: { + ...receipt, + checkedAt: "2026-07-19T00:32:00.000Z", + }, + observedAt: "2026-07-19T00:33:00.000Z", + }), + ).toThrow(/within 30 minutes/u); expect(() => assertReleaseCheckBinding({ ...common, diff --git a/packages/scientific-engine-registry/test/release-schema.test.ts b/packages/scientific-engine-registry/test/release-schema.test.ts index 626af6e2..5670ee1a 100644 --- a/packages/scientific-engine-registry/test/release-schema.test.ts +++ b/packages/scientific-engine-registry/test/release-schema.test.ts @@ -7,6 +7,7 @@ import { DeploymentReceiptV3Schema, DeploymentReceiptV4Schema, DeploymentReceiptV5Schema, + DeploymentReceiptV6Schema, QualifiedRunnerReleaseSchema, QualifiedRunnerReleaseV2Schema, QualifiedRunnerReleaseV3Schema, @@ -391,7 +392,7 @@ describe("deployment receipt schema", () => { const runner = "b".repeat(40); const digest = `sha256:${"c".repeat(64)}`; return { - schemaVersion: "6", + schemaVersion: "7", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", @@ -399,6 +400,9 @@ describe("deployment receipt schema", () => { generationIsolationEvidenceSha256: "0".repeat(64), generationIsolationProbeSha256: "1".repeat(64), generationIsolationVerifiedAt: "2026-07-19T00:01:00.000Z", + releaseCheckGenerationIsolationEvidenceSha256: "2".repeat(64), + releaseCheckGenerationIsolationProbeSha256: "1".repeat(64), + releaseCheckGenerationIsolationVerifiedAt: "2026-07-19T00:01:30.000Z", workerEvidenceCommit: worker, runnerSourceCommit: runner, qualifiedRunnerReceiptSha256: "1".repeat(64), @@ -435,13 +439,13 @@ describe("deployment receipt schema", () => { workerVersionSha256: "b".repeat(64), containerStatusSha256: "c".repeat(64), deployedAt: "2026-07-19T00:03:00.000Z", - verifierVersion: "counterlab-deployment-v6", + verifierVersion: "counterlab-deployment-v7", } as const; } it("binds the deployed Worker, Container, release checks, and exact artifacts", () => { expect(DeploymentReceiptSchema.parse(receipt())).toMatchObject({ - schemaVersion: "6", + schemaVersion: "7", status: "DEPLOYED", workerName: "counterlab", }); @@ -463,15 +467,55 @@ describe("deployment receipt schema", () => { generationFilesystemReadIsolation: "PARTIAL", }), ).toThrow(); + expect(() => + DeploymentReceiptSchema.parse({ + ...receipt(), + releaseCheckGenerationIsolationProbeSha256: "0".repeat(64), + }), + ).toThrow(/must match the qualified probe/u); + expect(() => + DeploymentReceiptSchema.parse({ + ...receipt(), + releaseCheckGenerationIsolationVerifiedAt: "2026-07-19T00:04:00.000Z", + }), + ).toThrow(/precede deployment/u); + expect(() => + DeploymentReceiptSchema.parse({ + ...receipt(), + releaseCheckGenerationIsolationVerifiedAt: "2026-07-19T00:00:30.000Z", + }), + ).toThrow(/follow qualification/u); + expect(() => + DeploymentReceiptSchema.parse({ + ...receipt(), + releaseCheckGenerationIsolationVerifiedAt: "2026-07-19T00:02:30.000Z", + }), + ).toThrow(/precede receipt issuance/u); }); - it("retains the historical v5 and v4 isolation contracts", () => { + it("retains the historical v6, v5, and v4 isolation contracts", () => { + const { + releaseCheckGenerationIsolationEvidenceSha256: + _releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + _releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + _releaseCheckGenerationIsolationVerifiedAt, + ...legacyV6 + } = receipt(); + expect( + DeploymentReceiptV6Schema.parse({ + ...legacyV6, + schemaVersion: "6", + verifierVersion: "counterlab-deployment-v6", + }), + ).toMatchObject({ schemaVersion: "6" }); const { generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, generationIsolationProbeSha256: _generationIsolationProbeSha256, generationIsolationVerifiedAt: _generationIsolationVerifiedAt, ...legacyV5 - } = receipt(); + } = legacyV6; expect( DeploymentReceiptV5Schema.parse({ ...legacyV5, @@ -509,6 +553,12 @@ describe("deployment receipt schema", () => { it("retains strict parsing for historical v3 deployment receipts", () => { const { + releaseCheckGenerationIsolationEvidenceSha256: + _releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + _releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + _releaseCheckGenerationIsolationVerifiedAt, generationIsolationEvidenceSha256: _generationIsolationEvidenceSha256, generationIsolationProbeSha256: _generationIsolationProbeSha256, generationIsolationVerifiedAt: _generationIsolationVerifiedAt, @@ -569,8 +619,13 @@ describe("deployment receipt schema", () => { process.cwd(), "scientific-engines/schemas/deployment-receipt-v5.schema.json", ); + const v7Path = resolve( + process.cwd(), + "scientific-engines/schemas/deployment-receipt-v7.schema.json", + ); expect(existsSync(v4Path)).toBe(true); expect(existsSync(v5Path)).toBe(true); + expect(existsSync(v7Path)).toBe(true); const v4 = JSON.parse(readFileSync(v4Path, "utf8")) as { properties?: { schemaVersion?: { const?: string }; @@ -580,6 +635,7 @@ describe("deployment receipt schema", () => { required?: string[]; }; const v5 = JSON.parse(readFileSync(v5Path, "utf8")) as typeof v4; + const v7 = JSON.parse(readFileSync(v7Path, "utf8")) as typeof v4; expect(v4.properties?.schemaVersion?.const).toBe("4"); expect(v4.properties?.generationFilesystemReadIsolation?.const).toBe( "PARTIAL", @@ -603,5 +659,13 @@ describe("deployment receipt schema", () => { expect(v5.properties?.generationFilesystemReadIsolation?.const).toBe( "OS_ENFORCED", ); + expect(v7.properties?.schemaVersion?.const).toBe("7"); + expect(v7.required).toEqual( + expect.arrayContaining([ + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + "releaseCheckGenerationIsolationVerifiedAt", + ]), + ); }); }); diff --git a/scientific-engines/schemas/deployment-receipt-v7.schema.json b/scientific-engines/schemas/deployment-receipt-v7.schema.json new file mode 100644 index 00000000..eb886c58 --- /dev/null +++ b/scientific-engines/schemas/deployment-receipt-v7.schema.json @@ -0,0 +1,261 @@ +{ + "$id": "https://counterlab.dev/schemas/deployment-receipt-v7.schema.json", + "title": "CounterLab deployment receipt v7", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "7" + }, + "status": { + "type": "string", + "const": "DEPLOYED" + }, + "workerName": { + "type": "string", + "const": "counterlab" + }, + "productionOrigin": { + "type": "string", + "const": "https://counterlab.cserules.workers.dev" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "workerEvidenceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "runnerSourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "qualifiedRunnerReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckCheckedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "timeoutCleanupReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "aggregateLimitEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeToolchainSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimePolicySha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "proofDependencyManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeAdapterSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "adapterImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "workerVersionId": { + "type": "string", + "pattern": "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" + }, + "workerTag": { + "type": "string", + "pattern": "^git-[a-f0-9]{40}$" + }, + "workerMessage": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "containerApplicationId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,128}$" + }, + "containerApplicationVersion": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "containerImage": { + "type": "string", + "pattern": "^registry\\.cloudflare\\.com\\/[A-Za-z0-9_-]{3,64}\\/counterlab-runner@sha256:[a-f0-9]{64}$" + }, + "containerState": { + "type": "string", + "enum": [ + "active", + "ready" + ] + }, + "containerImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "workerArtifactClassification": { + "type": "string", + "const": "PROCESS_BOUND_PARTIAL" + }, + "workerArtifactManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "deployConfigSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "workerBundleSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientAssetsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientAssetCount": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "clientPublicAssetsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "clientPublicAssetCount": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "viteVersion": { + "type": "string", + "const": "8.1.4" + }, + "wranglerVersion": { + "type": "string", + "const": "4.110.0" + }, + "dryRunSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "dryRunFileCount": { + "type": "number", + "const": 1 + }, + "deploymentStatusSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "workerVersionSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "containerStatusSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "deployedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-deployment-v7" + }, + "generationIsolationEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationProbeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "releaseCheckGenerationIsolationEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckGenerationIsolationProbeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckGenerationIsolationVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "status", + "workerName", + "productionOrigin", + "generationFilesystemReadIsolation", + "workerEvidenceCommit", + "runnerSourceCommit", + "qualifiedRunnerReceiptSha256", + "releaseCheckReceiptSha256", + "releaseCheckCheckedAt", + "timeoutCleanupReceiptSha256", + "aggregateLimitEvidenceSha256", + "runtimeToolchainSha256", + "runtimePolicySha256", + "proofDependencyManifestSha256", + "runtimeAdapterSha256", + "adapterImageDigest", + "workerVersionId", + "workerTag", + "workerMessage", + "containerApplicationId", + "containerApplicationVersion", + "containerImage", + "containerState", + "containerImageDigest", + "workerArtifactClassification", + "workerArtifactManifestSha256", + "deployConfigSha256", + "workerBundleSha256", + "clientAssetsSha256", + "clientAssetCount", + "clientPublicAssetsSha256", + "clientPublicAssetCount", + "viteVersion", + "wranglerVersion", + "dryRunSha256", + "dryRunFileCount", + "deploymentStatusSha256", + "workerVersionSha256", + "containerStatusSha256", + "deployedAt", + "verifierVersion", + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt", + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + "releaseCheckGenerationIsolationVerifiedAt" + ], + "additionalProperties": false +} diff --git a/scientific-engines/schemas/release-check-receipt-v5.schema.json b/scientific-engines/schemas/release-check-receipt-v5.schema.json new file mode 100644 index 00000000..42c52466 --- /dev/null +++ b/scientific-engines/schemas/release-check-receipt-v5.schema.json @@ -0,0 +1,356 @@ +{ + "$id": "https://counterlab.dev/schemas/release-check-receipt-v5.schema.json", + "title": "CounterLab release-check receipt v5", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "5" + }, + "status": { + "type": "string", + "const": "PASSED" + }, + "evidenceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "qualifiedRunnerReceiptSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "qualifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "runnerImageTag": { + "type": "string", + "pattern": "^counterlab-runner:git-[a-f0-9]{40}$" + }, + "runnerImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "adapterImageTag": { + "type": "string", + "pattern": "^counterlab-adapter:git-[a-f0-9]{40}$" + }, + "adapterImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "registryDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "runtimeToolchainSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimePolicySha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "proofDependencyManifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "aggregateLimitEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "runtimeAdapterSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "checks": { + "minItems": 11, + "maxItems": 11, + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "test-all", + "leakage-mutations", + "imbalance-mutations", + "held-out", + "adapter-sandbox-smoke", + "scientific-engines", + "web-build", + "achieved-metrics", + "reproduce-leakage", + "replay-patch", + "secret-scan" + ] + }, + "status": { + "type": "string", + "const": "PASSED" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + }, + "checkedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-release-check-v5" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "generationIsolationEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationProbeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "generationIsolationVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "releaseCheckGenerationIsolationEvidence": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "1" + }, + "status": { + "type": "string", + "const": "VERIFIED" + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "sourceCommit": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "sourceTreeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "localImageTag": { + "type": "string", + "pattern": "^counterlab-runner:git-[a-f0-9]{40}$" + }, + "localImageDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "imageUser": { + "type": "string", + "const": "10001:10001" + }, + "mountPolicyVersion": { + "type": "string", + "const": "counterlab-bwrap-mount-policy-v1" + }, + "probePayload": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "string", + "const": "1" + }, + "probeVersion": { + "type": "string", + "const": "counterlab-generation-isolation-v1" + }, + "service": { + "type": "string", + "const": "counterlab-hosted-runner" + }, + "probe": { + "type": "string", + "const": "non-root-startup" + }, + "checks": { + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "entrypoint" + }, + { + "type": "string", + "const": "non-root-user" + }, + { + "type": "string", + "const": "immutable-paths" + }, + { + "type": "string", + "const": "codex" + }, + { + "type": "string", + "const": "python" + }, + { + "type": "string", + "const": "bubblewrap" + }, + { + "type": "string", + "const": "bubblewrap-read-isolation" + }, + { + "type": "string", + "const": "setpriv" + }, + { + "type": "string", + "const": "writable-roots" + } + ] + }, + "generationFilesystemReadIsolation": { + "type": "string", + "const": "OS_ENFORCED" + }, + "bubblewrapVersion": { + "type": "string", + "const": "0.11.0" + }, + "bubblewrap": { + "type": "object", + "properties": { + "forbiddenHostPathsHidden": { + "type": "boolean", + "const": true + }, + "parentEnvironmentHidden": { + "type": "boolean", + "const": true + }, + "workspaceVisible": { + "type": "boolean", + "const": true + }, + "workspaceWritable": { + "type": "boolean", + "const": true + } + }, + "required": [ + "forbiddenHostPathsHidden", + "parentEnvironmentHidden", + "workspaceVisible", + "workspaceWritable" + ], + "additionalProperties": false + } + }, + "required": [ + "schemaVersion", + "probeVersion", + "service", + "probe", + "checks", + "generationFilesystemReadIsolation", + "bubblewrapVersion", + "bubblewrap" + ], + "additionalProperties": false + }, + "probePayloadSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "verifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "verifierVersion": { + "type": "string", + "const": "counterlab-generation-isolation-evidence-v1" + } + }, + "required": [ + "schemaVersion", + "status", + "generationFilesystemReadIsolation", + "sourceCommit", + "sourceTreeSha256", + "localImageTag", + "localImageDigest", + "imageUser", + "mountPolicyVersion", + "probePayload", + "probePayloadSha256", + "verifiedAt", + "verifierVersion" + ], + "additionalProperties": false + }, + "releaseCheckGenerationIsolationEvidenceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckGenerationIsolationProbeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "releaseCheckGenerationIsolationVerifiedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "status", + "evidenceCommit", + "sourceCommit", + "qualifiedRunnerReceiptSha256", + "qualifiedAt", + "runnerImageTag", + "runnerImageDigest", + "adapterImageTag", + "adapterImageDigest", + "registryDigest", + "runtimeToolchainSha256", + "runtimePolicySha256", + "proofDependencyManifestSha256", + "aggregateLimitEvidenceSha256", + "runtimeAdapterSha256", + "checks", + "checkedAt", + "verifierVersion", + "generationFilesystemReadIsolation", + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt", + "releaseCheckGenerationIsolationEvidence", + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + "releaseCheckGenerationIsolationVerifiedAt" + ], + "additionalProperties": false +} diff --git a/scripts/create-deployment-receipt.test.ts b/scripts/create-deployment-receipt.test.ts index 07ca88cd..e621af21 100644 --- a/scripts/create-deployment-receipt.test.ts +++ b/scripts/create-deployment-receipt.test.ts @@ -9,6 +9,36 @@ import { qualifiedContainerImage, selectQualifiedContainer, } from "./create-deployment-receipt"; +import { createGenerationIsolationEvidence } from "./generation-isolation-evidence"; + +const generationIsolationProbePayload = { + schemaVersion: "1", + probeVersion: "counterlab-generation-isolation-v1", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: [ + "entrypoint", + "non-root-user", + "immutable-paths", + "codex", + "python", + "bubblewrap", + "bubblewrap-read-isolation", + "setpriv", + "writable-roots", + ], + generationFilesystemReadIsolation: "OS_ENFORCED", + bubblewrapVersion: "0.11.0", + bubblewrap: { + forbiddenHostPathsHidden: true, + parentEnvironmentHidden: true, + workspaceVisible: true, + workspaceWritable: true, + }, +} as const; + +const generationIsolationProbeSha256 = + "700cc58bedc163846e3854415170f49f55da9fd3ba316cc4967747d5268199dc"; describe("deployment receipt frozen dry-run projection", () => { const workerBundleSha256 = "a".repeat(64); @@ -99,6 +129,9 @@ describe("deployment receipt Worker identity", () => { runnerImageDigest: `sha256:${"c".repeat(64)}`, generationIsolationEvidenceSha256: "5".repeat(64), generationIsolationProbeSha256: "6".repeat(64), + releaseCheckGenerationIsolationEvidenceSha256: "7".repeat(64), + releaseCheckGenerationIsolationProbeSha256: "6".repeat(64), + releaseCheckGenerationIsolationVerifiedAt: "2026-07-19T00:02:00.000Z", timeoutCleanupReceiptSha256: "d".repeat(64), aggregateLimitEvidenceSha256: "9".repeat(64), runtimePolicySha256: "e".repeat(64), @@ -125,6 +158,18 @@ describe("deployment receipt Worker identity", () => { "COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256", expected.generationIsolationProbeSha256, ], + [ + "COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256", + expected.releaseCheckGenerationIsolationEvidenceSha256, + ], + [ + "COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256", + expected.releaseCheckGenerationIsolationProbeSha256, + ], + [ + "COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT", + expected.releaseCheckGenerationIsolationVerifiedAt, + ], [ "COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256", expected.timeoutCleanupReceiptSha256, @@ -162,7 +207,7 @@ describe("deployment receipt Worker identity", () => { ["COUNTERLAB_MAINTENANCE_MODE", "false"], ].map(([name, text]) => ({ name, text, type: "plain_text" })); - it("requires direct active bindings for both qualification hashes", () => { + it("requires direct active bindings for qualification and release-check evidence", () => { expect(() => assertActiveWorkerReleaseBindings(bindings, expected), ).not.toThrow(); @@ -171,6 +216,9 @@ describe("deployment receipt Worker identity", () => { "COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256", "COUNTERLAB_GENERATION_ISOLATION_EVIDENCE_SHA256", "COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256", + "COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256", + "COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256", + "COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT", "COUNTERLAB_RUNTIME_POLICY_SHA256", "COUNTERLAB_PROOF_DEPENDENCY_MANIFEST_SHA256", "COUNTERLAB_WORKER_ARTIFACT_MANIFEST_SHA256", @@ -201,6 +249,22 @@ describe("deployment receipt Worker identity", () => { ).toThrow(new RegExp(name, "u")); } }); + + it("rejects duplicate release bindings even when one value matches", () => { + expect(() => + assertActiveWorkerReleaseBindings( + [ + ...bindings, + { + name: "COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256", + text: expected.releaseCheckGenerationIsolationEvidenceSha256, + type: "plain_text", + }, + ], + expected, + ), + ).toThrow(/COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256/u); + }); }); describe("deployment receipt qualification binding", () => { @@ -209,6 +273,7 @@ describe("deployment receipt qualification binding", () => { generationFilesystemReadIsolation: "OS_ENFORCED", evidenceCommit: "a".repeat(40), sourceCommit: "b".repeat(40), + sourceTreeSha256: "8".repeat(64), registryDigest: `sha256:${"c".repeat(64)}`, qualifiedAt: "2026-07-19T00:01:00.000Z", localImageTag: `counterlab-runner:git-${"b".repeat(40)}`, @@ -221,9 +286,26 @@ describe("deployment receipt qualification binding", () => { runtimeAdapterSha256: "4".repeat(64), aggregateLimitEvidenceSha256: "5".repeat(64), generationIsolationEvidenceSha256: "6".repeat(64), - generationIsolationProbeSha256: "7".repeat(64), + generationIsolationProbeSha256, generationIsolationVerifiedAt: "2026-07-19T00:00:30.000Z", } as const; + const freshIsolation = createGenerationIsolationEvidence({ + sourceCommit: qualified.sourceCommit, + sourceTreeSha256: qualified.sourceTreeSha256, + localImageTag: qualified.localImageTag, + localImageDigest: qualified.localImageDigest, + imageUser: "10001:10001", + verifiedAt: "2026-07-19T00:02:00.000Z", + startupProbe: { + status: "ready", + service: "counterlab-hosted-runner", + probe: "non-root-startup", + checks: generationIsolationProbePayload.checks, + generationFilesystemReadIsolation: "OS_ENFORCED", + generationIsolationProbe: generationIsolationProbePayload, + generationIsolationProbeSha256, + }, + }); const releaseCheck = { generationFilesystemReadIsolation: qualified.generationFilesystemReadIsolation, @@ -247,6 +329,12 @@ describe("deployment receipt qualification binding", () => { qualified.generationIsolationEvidenceSha256, generationIsolationProbeSha256: qualified.generationIsolationProbeSha256, generationIsolationVerifiedAt: qualified.generationIsolationVerifiedAt, + releaseCheckGenerationIsolationEvidence: freshIsolation.evidence, + releaseCheckGenerationIsolationEvidenceSha256: + freshIsolation.evidenceSha256, + releaseCheckGenerationIsolationProbeSha256: freshIsolation.probeSha256, + releaseCheckGenerationIsolationVerifiedAt: + freshIsolation.evidence.verifiedAt, } as const; const common = { qualified, @@ -269,6 +357,18 @@ describe("deployment receipt qualification binding", () => { ["generationIsolationEvidenceSha256", /generation isolation evidence/u], ["generationIsolationProbeSha256", /generation isolation probe/u], ["generationIsolationVerifiedAt", /generation isolation verification/u], + [ + "releaseCheckGenerationIsolationEvidenceSha256", + /Generation-isolation evidence hash mismatch/u, + ], + [ + "releaseCheckGenerationIsolationProbeSha256", + /release-check generation isolation probe|probeSha256 binding mismatch/u, + ], + [ + "releaseCheckGenerationIsolationVerifiedAt", + /Generation-isolation verifiedAt binding mismatch/u, + ], ] as const) { expect(() => assertDeploymentReceiptBindings({ diff --git a/scripts/create-deployment-receipt.ts b/scripts/create-deployment-receipt.ts index d1281e4f..2fdee7f8 100644 --- a/scripts/create-deployment-receipt.ts +++ b/scripts/create-deployment-receipt.ts @@ -4,15 +4,16 @@ import { realpath, stat, writeFile } from "node:fs/promises"; import { dirname, isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { - DeploymentReceiptV6Schema, - ReleaseCheckReceiptV4Schema, -} from "../packages/scientific-engine-registry/src/index.js"; +import { DeploymentReceiptV7Schema } from "../packages/scientific-engine-registry/src/index.js"; import type { QualifiedRunnerReleaseV6, - ReleaseCheckReceiptV4, + ReleaseCheckReceiptV5, } from "../packages/scientific-engine-registry/src/index.js"; -import { parseQualifiedRunnerReleaseV6 } from "./generation-isolation-evidence.js"; +import { + parseQualifiedRunnerReleaseV6, + verifyGenerationIsolationEvidence, +} from "./generation-isolation-evidence.js"; +import { parseReleaseCheckReceiptV5 } from "./release-check-receipt.js"; import { collectFrozenClientAssets, verifyFrozenWorkerReleaseManifest, @@ -224,6 +225,9 @@ export function assertActiveWorkerReleaseBindings( runnerImageDigest: string; generationIsolationEvidenceSha256: string; generationIsolationProbeSha256: string; + releaseCheckGenerationIsolationEvidenceSha256: string; + releaseCheckGenerationIsolationProbeSha256: string; + releaseCheckGenerationIsolationVerifiedAt: string; timeoutCleanupReceiptSha256: string; aggregateLimitEvidenceSha256: string; runtimePolicySha256: string; @@ -250,6 +254,12 @@ export function assertActiveWorkerReleaseBindings( expected.generationIsolationEvidenceSha256, COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: expected.generationIsolationProbeSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256: + expected.releaseCheckGenerationIsolationEvidenceSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256: + expected.releaseCheckGenerationIsolationProbeSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT: + expected.releaseCheckGenerationIsolationVerifiedAt, COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: expected.timeoutCleanupReceiptSha256, COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: @@ -273,10 +283,12 @@ export function assertActiveWorkerReleaseBindings( COUNTERLAB_MAINTENANCE_MODE: "false", } as const; for (const [name, value] of Object.entries(expectedBindings)) { - const binding = (bindings as Array>).find( + const matchingBindings = ( + bindings as Array> + ).filter( (candidate) => candidate.name === name && candidate.type === "plain_text", ); - if (binding?.text !== value) { + if (matchingBindings.length !== 1 || matchingBindings[0]?.text !== value) { throw new Error(`active Worker binding ${name} is not release-bound`); } } @@ -286,6 +298,7 @@ type QualifiedDeploymentIdentity = Pick< QualifiedRunnerReleaseV6, | "evidenceCommit" | "sourceCommit" + | "sourceTreeSha256" | "registryDigest" | "qualifiedAt" | "localImageTag" @@ -303,7 +316,7 @@ type QualifiedDeploymentIdentity = Pick< >; type ReleaseCheckDeploymentIdentity = Pick< - ReleaseCheckReceiptV4, + ReleaseCheckReceiptV5, | "evidenceCommit" | "sourceCommit" | "qualifiedRunnerReceiptSha256" @@ -321,6 +334,10 @@ type ReleaseCheckDeploymentIdentity = Pick< | "generationIsolationEvidenceSha256" | "generationIsolationProbeSha256" | "generationIsolationVerifiedAt" + | "releaseCheckGenerationIsolationEvidence" + | "releaseCheckGenerationIsolationEvidenceSha256" + | "releaseCheckGenerationIsolationProbeSha256" + | "releaseCheckGenerationIsolationVerifiedAt" >; export function assertDeploymentReceiptBindings(input: { @@ -331,6 +348,19 @@ export function assertDeploymentReceiptBindings(input: { sourceCommit: string; registryDigest: string; }): void { + verifyGenerationIsolationEvidence({ + evidence: input.releaseCheck.releaseCheckGenerationIsolationEvidence, + evidenceSha256: + input.releaseCheck.releaseCheckGenerationIsolationEvidenceSha256, + expected: { + sourceCommit: input.qualified.sourceCommit, + sourceTreeSha256: input.qualified.sourceTreeSha256, + localImageTag: input.qualified.localImageTag, + localImageDigest: input.qualified.localImageDigest, + probeSha256: input.qualified.generationIsolationProbeSha256, + verifiedAt: input.releaseCheck.releaseCheckGenerationIsolationVerifiedAt, + }, + }); const comparisons: ReadonlyArray = [ [ "qualified evidence commit", @@ -382,6 +412,11 @@ export function assertDeploymentReceiptBindings(input: { input.releaseCheck.generationIsolationVerifiedAt, input.qualified.generationIsolationVerifiedAt, ], + [ + "release-check generation isolation probe", + input.releaseCheck.releaseCheckGenerationIsolationProbeSha256, + input.qualified.generationIsolationProbeSha256, + ], [ "runner image tag", input.releaseCheck.runnerImageTag, @@ -504,6 +539,10 @@ async function main(): Promise { const qualified = parseQualifiedRunnerReleaseV6( JSON.parse(qualifiedBytes.toString("utf8")) as unknown, ); + const releaseCheckBytes = readFileSync(paths.releaseCheck); + const releaseCheck = parseReleaseCheckReceiptV5( + JSON.parse(releaseCheckBytes.toString("utf8")) as unknown, + ); assertActiveWorkerReleaseBindings(bindings, { workerEvidenceCommit: args["--evidence-commit"], runnerSourceCommit: args["--source-commit"], @@ -511,6 +550,12 @@ async function main(): Promise { generationIsolationEvidenceSha256: qualified.generationIsolationEvidenceSha256, generationIsolationProbeSha256: qualified.generationIsolationProbeSha256, + releaseCheckGenerationIsolationEvidenceSha256: + releaseCheck.releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + releaseCheck.releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + releaseCheck.releaseCheckGenerationIsolationVerifiedAt, timeoutCleanupReceiptSha256: qualified.timeoutCleanupReceiptSha256, aggregateLimitEvidenceSha256: qualified.aggregateLimitEvidenceSha256, runtimePolicySha256: qualified.runtimePolicySha256, @@ -536,10 +581,6 @@ async function main(): Promise { throw new Error("active Worker lacks version metadata"); } - const releaseCheckBytes = readFileSync(paths.releaseCheck); - const releaseCheck = ReleaseCheckReceiptV4Schema.parse( - JSON.parse(releaseCheckBytes.toString("utf8")) as unknown, - ); assertDeploymentReceiptBindings({ qualified, qualifiedReceiptBytes: qualifiedBytes, @@ -588,8 +629,8 @@ async function main(): Promise { } const dryRun = dryRunWorkerHash(root, paths.dryRun, workerBundleSha256); const configBytes = readFileSync(paths.config); - const receipt = DeploymentReceiptV6Schema.parse({ - schemaVersion: "6", + const receipt = DeploymentReceiptV7Schema.parse({ + schemaVersion: "7", status: "DEPLOYED", workerName: "counterlab", productionOrigin: "https://counterlab.cserules.workers.dev", @@ -599,6 +640,12 @@ async function main(): Promise { releaseCheck.generationIsolationEvidenceSha256, generationIsolationProbeSha256: releaseCheck.generationIsolationProbeSha256, generationIsolationVerifiedAt: releaseCheck.generationIsolationVerifiedAt, + releaseCheckGenerationIsolationEvidenceSha256: + releaseCheck.releaseCheckGenerationIsolationEvidenceSha256, + releaseCheckGenerationIsolationProbeSha256: + releaseCheck.releaseCheckGenerationIsolationProbeSha256, + releaseCheckGenerationIsolationVerifiedAt: + releaseCheck.releaseCheckGenerationIsolationVerifiedAt, workerEvidenceCommit: args["--evidence-commit"], runnerSourceCommit: args["--source-commit"], qualifiedRunnerReceiptSha256: sha256(qualifiedBytes), @@ -635,7 +682,7 @@ async function main(): Promise { workerVersionSha256: sha256(versionBytes), containerStatusSha256: sha256(containersBytes), deployedAt: new Date().toISOString(), - verifierVersion: "counterlab-deployment-v6", + verifierVersion: "counterlab-deployment-v7", }); await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", diff --git a/scripts/deploy-qualified.sh b/scripts/deploy-qualified.sh index 710e9227..08c1d6c5 100755 --- a/scripts/deploy-qualified.sh +++ b/scripts/deploy-qualified.sh @@ -210,7 +210,45 @@ process.stdout.write(identity.receiptSha256); echo "Qualified receipt bytes changed after identity validation." >&2 exit 2 } -RELEASE_CHECK_RECEIPT_SHA256="$(sha256sum "${RELEASE_CHECK_RECEIPT}" | cut -d ' ' -f 1)" +RELEASE_CHECK_IDENTITY_JSON="$( + node --import tsx scripts/release-check-receipt.ts \ + release-check-identity \ + --release-check "${RELEASE_CHECK_RECEIPT}" +)" +release_check_identity_value() { + local field="$1" + node -e ' +const [raw, field] = process.argv.slice(1); +const identity = JSON.parse(raw); +const outerFields = ["identitySchemaVersion", "receipt", "receiptSha256", "receiptType"]; +if ( + Object.keys(identity).sort().join("\n") !== outerFields.sort().join("\n") || + identity.identitySchemaVersion !== "1" || + identity.receiptType !== "release-check-receipt" || + !/^[a-f0-9]{64}$/.test(identity.receiptSha256) +) throw new Error("release-check identity envelope is invalid"); +if (!Object.hasOwn(identity.receipt, field) || typeof identity.receipt[field] !== "string") { + throw new Error(`release-check identity field is unavailable: ${field}`); +} +process.stdout.write(identity.receipt[field]); +' "${RELEASE_CHECK_IDENTITY_JSON}" "${field}" +} +RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256="$(release_check_identity_value releaseCheckGenerationIsolationEvidenceSha256)" +RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256="$(release_check_identity_value releaseCheckGenerationIsolationProbeSha256)" +RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT="$(release_check_identity_value releaseCheckGenerationIsolationVerifiedAt)" +RELEASE_CHECK_RECEIPT_SHA256="$( + node -e ' +const identity = JSON.parse(process.argv[1]); +if (identity?.identitySchemaVersion !== "1" || !/^[a-f0-9]{64}$/.test(identity?.receiptSha256)) { + throw new Error("release-check receipt byte hash is unavailable"); +} +process.stdout.write(identity.receiptSha256); +' "${RELEASE_CHECK_IDENTITY_JSON}" +)" +[[ "$(sha256sum "${RELEASE_CHECK_RECEIPT}" | cut -d ' ' -f 1)" == "${RELEASE_CHECK_RECEIPT_SHA256}" ]] || { + echo "Release-check receipt bytes changed after identity validation." >&2 + exit 2 +} CONTAINER_APPLICATION_NAME="counterlab-counterlabrunner" QUALIFIED_CONTAINER_IMAGE="${IMAGE%:git-*}@${REGISTRY_DIGEST}" WORKER_TAG="git-${EVIDENCE_COMMIT}" @@ -414,33 +452,64 @@ wait_for_maintenance_health() { if "${CURL_BIN}" --silent --show-error --fail-with-body \ --connect-timeout 10 --max-time 20 \ --output "${candidate}" "${PRODUCTION_ORIGIN}/api/health" && - node - "${candidate}" "${EVIDENCE_COMMIT}" "${SOURCE_COMMIT}" "${REGISTRY_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${expected_version}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" <<'NODE' + node - "${candidate}" "${EVIDENCE_COMMIT}" "${SOURCE_COMMIT}" "${REGISTRY_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${expected_version}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT}" <<'NODE' const fs = require("node:fs"); -const [path, evidenceCommit, sourceCommit, digest, timeoutReceipt, aggregateEvidence, runtimePolicy, proofManifest, artifactClassification, artifactManifest, workerBundle, clientAssets, clientAssetCount, publicAssets, publicAssetCount, viteVersion, wranglerVersion, expectedVersion, isolationEvidence, isolationProbe] = process.argv.slice(2); +const [path, evidenceCommit, sourceCommit, digest, timeoutReceipt, aggregateEvidence, runtimePolicy, proofManifest, artifactClassification, artifactManifest, workerBundle, clientAssets, clientAssetCount, publicAssets, publicAssetCount, viteVersion, wranglerVersion, expectedVersion, isolationEvidence, isolationProbe, releaseCheckIsolationEvidence, releaseCheckIsolationProbe, releaseCheckIsolationVerifiedAt] = process.argv.slice(2); const payload = JSON.parse(fs.readFileSync(path, "utf8")); const data = payload?.ok === true ? payload.data : undefined; +const exactKeys = (value, keys) => + value !== null && typeof value === "object" && !Array.isArray(value) && + JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort()); +const expectedRelease = { + status: "bound", + workerVersionId: expectedVersion, + workerVersionTag: `git-${evidenceCommit}`, + workerEvidenceCommit: evidenceCommit, + runnerSourceCommit: sourceCommit, + runnerImageDigest: digest, + generationIsolationEvidenceSha256: isolationEvidence, + generationIsolationProbeSha256: isolationProbe, + releaseCheckGenerationIsolationEvidenceSha256: releaseCheckIsolationEvidence, + releaseCheckGenerationIsolationProbeSha256: releaseCheckIsolationProbe, + releaseCheckGenerationIsolationVerifiedAt: releaseCheckIsolationVerifiedAt, + timeoutCleanupReceiptSha256: timeoutReceipt, + aggregateLimitEvidenceSha256: aggregateEvidence, + runtimePolicySha256: runtimePolicy, + proofDependencyManifestSha256: proofManifest, + workerArtifactClassification: artifactClassification, + workerArtifactManifestSha256: artifactManifest, + workerBundleSha256: workerBundle, + clientAssetsSha256: clientAssets, + clientAssetCount: Number(clientAssetCount), + clientPublicAssetsSha256: publicAssets, + clientPublicAssetCount: Number(publicAssetCount), + viteVersion, + wranglerVersion, +}; +const expectedData = { + platform: "cloudflare-workers", + sample: "available", + replay: "available", + liveGpt: "configured", + liveCodex: "configured", + liveKernel: "configured", + maintenance: true, + readiness: "not-checked", + release: expectedRelease, + sandbox: "credential-and-privilege-boundary", + generationFilesystemReadIsolation: "PARTIAL", +}; if ( - data?.maintenance !== true || - data?.release?.status !== "bound" || - data.release.workerVersionId !== expectedVersion || - data.release.workerEvidenceCommit !== evidenceCommit || - data.release.runnerSourceCommit !== sourceCommit || - data.release.runnerImageDigest !== digest || - data.release.generationIsolationEvidenceSha256 !== isolationEvidence || - data.release.generationIsolationProbeSha256 !== isolationProbe || - data.release.timeoutCleanupReceiptSha256 !== timeoutReceipt || - data.release.aggregateLimitEvidenceSha256 !== aggregateEvidence || - data.release.runtimePolicySha256 !== runtimePolicy || - data.release.proofDependencyManifestSha256 !== proofManifest || - data.release.workerArtifactClassification !== artifactClassification || - data.release.workerArtifactManifestSha256 !== artifactManifest || - data.release.workerBundleSha256 !== workerBundle || - data.release.clientAssetsSha256 !== clientAssets || - data.release.clientAssetCount !== Number(clientAssetCount) || - data.release.clientPublicAssetsSha256 !== publicAssets || - data.release.clientPublicAssetCount !== Number(publicAssetCount) || - data.release.viteVersion !== viteVersion || - data.release.wranglerVersion !== wranglerVersion + !exactKeys(payload, ["ok", "data"]) || + !exactKeys(data, [...Object.keys(expectedData), "requestId"]) || + !exactKeys(data?.release, Object.keys(expectedRelease)) || + typeof data?.requestId !== "string" || + data.requestId.length === 0 || + Object.entries(expectedData).some(([key, value]) => + key === "release" + ? Object.entries(expectedRelease).some(([releaseKey, releaseValue]) => data.release?.[releaseKey] !== releaseValue) + : data?.[key] !== value, + ) ) process.exit(1); NODE then @@ -466,33 +535,58 @@ wait_for_final_readiness() { if "${CURL_BIN}" --silent --show-error --fail-with-body \ --connect-timeout 10 --max-time 20 \ --output "${candidate}" "${PRODUCTION_ORIGIN}/ready" && - node - "${candidate}" "${EVIDENCE_COMMIT}" "${SOURCE_COMMIT}" "${REGISTRY_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${expected_version}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" <<'NODE' + node - "${candidate}" "${EVIDENCE_COMMIT}" "${SOURCE_COMMIT}" "${REGISTRY_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${expected_version}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT}" <<'NODE' const fs = require("node:fs"); -const [path, evidenceCommit, sourceCommit, digest, timeoutReceipt, aggregateEvidence, runtimePolicy, proofManifest, artifactClassification, artifactManifest, workerBundle, clientAssets, clientAssetCount, publicAssets, publicAssetCount, viteVersion, wranglerVersion, expectedVersion, isolationEvidence, isolationProbe] = process.argv.slice(2); +const [path, evidenceCommit, sourceCommit, digest, timeoutReceipt, aggregateEvidence, runtimePolicy, proofManifest, artifactClassification, artifactManifest, workerBundle, clientAssets, clientAssetCount, publicAssets, publicAssetCount, viteVersion, wranglerVersion, expectedVersion, isolationEvidence, isolationProbe, releaseCheckIsolationEvidence, releaseCheckIsolationProbe, releaseCheckIsolationVerifiedAt] = process.argv.slice(2); const payload = JSON.parse(fs.readFileSync(path, "utf8")); +const exactKeys = (value, keys) => + value !== null && typeof value === "object" && !Array.isArray(value) && + JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort()); +const expectedChecks = { + admission: true, + analyst: true, + maintenance: true, + persistence: true, + privateStorage: true, + releaseIdentity: true, + runner: true, + signing: true, +}; +const expectedRelease = { + status: "bound", + workerVersionId: expectedVersion, + workerVersionTag: `git-${evidenceCommit}`, + workerEvidenceCommit: evidenceCommit, + runnerSourceCommit: sourceCommit, + runnerImageDigest: digest, + generationIsolationEvidenceSha256: isolationEvidence, + generationIsolationProbeSha256: isolationProbe, + releaseCheckGenerationIsolationEvidenceSha256: releaseCheckIsolationEvidence, + releaseCheckGenerationIsolationProbeSha256: releaseCheckIsolationProbe, + releaseCheckGenerationIsolationVerifiedAt: releaseCheckIsolationVerifiedAt, + timeoutCleanupReceiptSha256: timeoutReceipt, + aggregateLimitEvidenceSha256: aggregateEvidence, + runtimePolicySha256: runtimePolicy, + proofDependencyManifestSha256: proofManifest, + workerArtifactClassification: artifactClassification, + workerArtifactManifestSha256: artifactManifest, + workerBundleSha256: workerBundle, + clientAssetsSha256: clientAssets, + clientAssetCount: Number(clientAssetCount), + clientPublicAssetsSha256: publicAssets, + clientPublicAssetCount: Number(publicAssetCount), + viteVersion, + wranglerVersion, +}; if ( - payload?.status !== "ready" || - payload?.maintenance !== false || - payload?.release?.status !== "bound" || - payload.release.workerVersionId !== expectedVersion || - payload.release.workerEvidenceCommit !== evidenceCommit || - payload.release.runnerSourceCommit !== sourceCommit || - payload.release.runnerImageDigest !== digest || - payload.release.generationIsolationEvidenceSha256 !== isolationEvidence || - payload.release.generationIsolationProbeSha256 !== isolationProbe || - payload.release.timeoutCleanupReceiptSha256 !== timeoutReceipt || - payload.release.aggregateLimitEvidenceSha256 !== aggregateEvidence || - payload.release.runtimePolicySha256 !== runtimePolicy || - payload.release.proofDependencyManifestSha256 !== proofManifest || - payload.release.workerArtifactClassification !== artifactClassification || - payload.release.workerArtifactManifestSha256 !== artifactManifest || - payload.release.workerBundleSha256 !== workerBundle || - payload.release.clientAssetsSha256 !== clientAssets || - payload.release.clientAssetCount !== Number(clientAssetCount) || - payload.release.clientPublicAssetsSha256 !== publicAssets || - payload.release.clientPublicAssetCount !== Number(publicAssetCount) || - payload.release.viteVersion !== viteVersion || - payload.release.wranglerVersion !== wranglerVersion + !exactKeys(payload, ["status", "service", "checks", "maintenance", "release"]) || + payload.status !== "ready" || + payload.service !== "counterlab-control-plane" || + payload.maintenance !== false || + !exactKeys(payload.checks, Object.keys(expectedChecks)) || + Object.entries(expectedChecks).some(([key, value]) => payload.checks?.[key] !== value) || + !exactKeys(payload.release, Object.keys(expectedRelease)) || + Object.entries(expectedRelease).some(([key, value]) => payload.release?.[key] !== value) ) process.exit(1); NODE then diff --git a/scripts/generate-contract-schemas.ts b/scripts/generate-contract-schemas.ts index 1d3bd40f..39d2d7a3 100644 --- a/scripts/generate-contract-schemas.ts +++ b/scripts/generate-contract-schemas.ts @@ -22,6 +22,7 @@ import { DeploymentReceiptV4Schema, DeploymentReceiptV5Schema, DeploymentReceiptV6Schema, + DeploymentReceiptV7Schema, GenerationIsolationEvidenceV1Schema, QualifiedRunnerReleaseV2Schema, QualifiedRunnerReleaseV3Schema, @@ -32,6 +33,7 @@ import { ReleaseCheckReceiptV2Schema, ReleaseCheckReceiptV3Schema, ReleaseCheckReceiptV4Schema, + ReleaseCheckReceiptV5Schema, ScientificEngineEvidenceCatalogSchema, ScientificEngineRegistrySchema, ScientificEngineRuntimeManifestSchema, @@ -390,6 +392,18 @@ const schemas = [ ), ], }, + { + fileName: "release-check-receipt-v5.schema.json", + id: "https://counterlab.dev/schemas/release-check-receipt-v5.schema.json", + title: "CounterLab release-check receipt v5", + schema: ReleaseCheckReceiptV5Schema, + destinations: [ + resolve( + root, + "scientific-engines/schemas/release-check-receipt-v5.schema.json", + ), + ], + }, { fileName: "deployment-receipt-v3.schema.json", id: "https://counterlab.dev/schemas/deployment-receipt-v3.schema.json", @@ -438,6 +452,18 @@ const schemas = [ ), ], }, + { + fileName: "deployment-receipt-v7.schema.json", + id: "https://counterlab.dev/schemas/deployment-receipt-v7.schema.json", + title: "CounterLab deployment receipt v7", + schema: DeploymentReceiptV7Schema, + destinations: [ + resolve( + root, + "scientific-engines/schemas/deployment-receipt-v7.schema.json", + ), + ], + }, ]; for (const definition of schemas) { diff --git a/scripts/prepare-qualified-deploy.ts b/scripts/prepare-qualified-deploy.ts index 9a22e277..4878ff4d 100644 --- a/scripts/prepare-qualified-deploy.ts +++ b/scripts/prepare-qualified-deploy.ts @@ -16,7 +16,10 @@ import { parseQualifiedRunnerReleaseV6, verifyGenerationIsolationEvidence, } from "./generation-isolation-evidence.js"; -import { assertReleaseCheckBinding } from "./release-check-receipt.js"; +import { + assertReleaseCheckBinding, + parseReleaseCheckReceiptV5, +} from "./release-check-receipt.js"; import { containedRuntimeAdapterArguments, requireContainedRuntimeSessionId, @@ -1109,11 +1112,25 @@ function assertProductionBindings(config: Record): void { export function qualifiedDeployConfig(input: { config: unknown; receipt: unknown; + releaseCheckReceipt: unknown; image: string; observation: QualifiedReleaseObservation; }): Record { const config = canonicalReleaseConfig(input.config); const receipt = parseQualifiedRunnerReleaseV6(input.receipt); + const releaseCheck = parseReleaseCheckReceiptV5(input.releaseCheckReceipt); + verifyGenerationIsolationEvidence({ + evidence: releaseCheck.releaseCheckGenerationIsolationEvidence, + evidenceSha256: releaseCheck.releaseCheckGenerationIsolationEvidenceSha256, + expected: { + sourceCommit: receipt.sourceCommit, + sourceTreeSha256: receipt.sourceTreeSha256, + localImageTag: receipt.localImageTag, + localImageDigest: receipt.localImageDigest, + probeSha256: receipt.generationIsolationProbeSha256, + verifiedAt: releaseCheck.releaseCheckGenerationIsolationVerifiedAt, + }, + }); const comparisons: Array<[string, string, string]> = [ ["source commit", receipt.sourceCommit, input.observation.sourceCommit], [ @@ -1276,6 +1293,31 @@ export function qualifiedDeployConfig(input: { receipt.generationIsolationVerifiedAt, input.observation.generationIsolationVerifiedAt, ], + [ + "release-check evidence commit", + releaseCheck.evidenceCommit, + receipt.evidenceCommit, + ], + [ + "release-check source commit", + releaseCheck.sourceCommit, + receipt.sourceCommit, + ], + [ + "release-check qualification isolation evidence", + releaseCheck.generationIsolationEvidenceSha256, + receipt.generationIsolationEvidenceSha256, + ], + [ + "release-check qualification isolation probe", + releaseCheck.generationIsolationProbeSha256, + receipt.generationIsolationProbeSha256, + ], + [ + "release-check isolation probe", + releaseCheck.releaseCheckGenerationIsolationProbeSha256, + receipt.generationIsolationProbeSha256, + ], ["registry image", receipt.registryImage, input.observation.registryImage], [ "registry digest", @@ -1387,6 +1429,12 @@ export function qualifiedDeployConfig(input: { receipt.generationIsolationEvidenceSha256, COUNTERLAB_GENERATION_ISOLATION_PROBE_SHA256: receipt.generationIsolationProbeSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256: + releaseCheck.releaseCheckGenerationIsolationEvidenceSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256: + releaseCheck.releaseCheckGenerationIsolationProbeSha256, + COUNTERLAB_RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT: + releaseCheck.releaseCheckGenerationIsolationVerifiedAt, COUNTERLAB_TIMEOUT_CLEANUP_RECEIPT_SHA256: receipt.timeoutCleanupReceiptSha256, COUNTERLAB_AGGREGATE_LIMIT_EVIDENCE_SHA256: @@ -1493,6 +1541,7 @@ async function main(): Promise { qualifiedDeployConfig({ config: JSON.parse(configText) as unknown, receipt, + releaseCheckReceipt: JSON.parse(releaseCheckReceiptText) as unknown, image: args.image, observation, }), diff --git a/scripts/production-smoke.sh b/scripts/production-smoke.sh index 32496fbc..d95098dc 100755 --- a/scripts/production-smoke.sh +++ b/scripts/production-smoke.sh @@ -98,7 +98,7 @@ esac if [[ -z "${COUNTERLAB_DEPLOYMENT_RECEIPT:-}" ]]; then echo "Production smoke requires COUNTERLAB_DEPLOYMENT_RECEIPT." >&2 - echo "Use the schema-v6 receipt written by scripts/deploy-qualified.sh." >&2 + echo "Use the schema-v7 receipt written by scripts/deploy-qualified.sh." >&2 exit 2 fi DEPLOYMENT_RECEIPT="$(repo_path "${COUNTERLAB_DEPLOYMENT_RECEIPT}")" @@ -159,6 +159,9 @@ PROOF_DEPENDENCY_MANIFEST_SHA256="$(deployment_identity_value proofDependencyMan GENERATION_ISOLATION_EVIDENCE_SHA256="$(deployment_identity_value generationIsolationEvidenceSha256)" GENERATION_ISOLATION_PROBE_SHA256="$(deployment_identity_value generationIsolationProbeSha256)" GENERATION_ISOLATION_VERIFIED_AT="$(deployment_identity_value generationIsolationVerifiedAt)" +RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256="$(deployment_identity_value releaseCheckGenerationIsolationEvidenceSha256)" +RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256="$(deployment_identity_value releaseCheckGenerationIsolationProbeSha256)" +RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT="$(deployment_identity_value releaseCheckGenerationIsolationVerifiedAt)" WORKER_ARTIFACT_CLASSIFICATION="$(deployment_identity_value workerArtifactClassification)" WORKER_ARTIFACT_MANIFEST_SHA256="$(deployment_identity_value workerArtifactManifestSha256)" WORKER_BUNDLE_SHA256="$(deployment_identity_value workerBundleSha256)" @@ -407,6 +410,9 @@ init_args+=(--proof-dependency-manifest-sha256 "${PROOF_DEPENDENCY_MANIFEST_SHA2 init_args+=(--generation-isolation-evidence-sha256 "${GENERATION_ISOLATION_EVIDENCE_SHA256}") init_args+=(--generation-isolation-probe-sha256 "${GENERATION_ISOLATION_PROBE_SHA256}") init_args+=(--generation-isolation-verified-at "${GENERATION_ISOLATION_VERIFIED_AT}") +init_args+=(--release-check-generation-isolation-evidence-sha256 "${RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256}") +init_args+=(--release-check-generation-isolation-probe-sha256 "${RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256}") +init_args+=(--release-check-generation-isolation-verified-at "${RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT}") init_args+=(--worker-artifact-classification "${WORKER_ARTIFACT_CLASSIFICATION}") init_args+=(--worker-artifact-manifest-sha256 "${WORKER_ARTIFACT_MANIFEST_SHA256}") init_args+=(--worker-bundle-sha256 "${WORKER_BUNDLE_SHA256}") @@ -429,13 +435,17 @@ stage_started="$(timestamp)" --max-time 30 \ "${BASE_URL}/ready" \ >"${WORK_DIR}/ready.json" -python3 - "${WORK_DIR}/ready.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" <<'PY' +python3 - "${WORK_DIR}/ready.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT}" <<'PY' import json import pathlib import sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) +if set(payload) != {"status", "service", "checks", "maintenance", "release"}: + raise SystemExit("public readiness response has unknown or missing fields") if payload.get("status") != "ready": raise SystemExit("public readiness response is not ready") +if payload.get("service") != "counterlab-control-plane": + raise SystemExit("public readiness service identity is invalid") checks = payload.get("checks") expected_checks = { "admission", @@ -462,6 +472,9 @@ expected_release = { "runnerImageDigest": sys.argv[5], "generationIsolationEvidenceSha256": sys.argv[19], "generationIsolationProbeSha256": sys.argv[20], + "releaseCheckGenerationIsolationEvidenceSha256": sys.argv[21], + "releaseCheckGenerationIsolationProbeSha256": sys.argv[22], + "releaseCheckGenerationIsolationVerifiedAt": sys.argv[23], "timeoutCleanupReceiptSha256": sys.argv[6], "aggregateLimitEvidenceSha256": sys.argv[7], "runtimePolicySha256": sys.argv[8], @@ -491,15 +504,33 @@ stage_started="$(timestamp)" --max-time 30 \ "${BASE_URL}/api/health?readiness=probe" \ >"${WORK_DIR}/health.json" -python3 - "${WORK_DIR}/health.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" <<'PY' +python3 - "${WORK_DIR}/health.json" "${WORKER_VERSION_ID}" "${WORKER_EVIDENCE_COMMIT}" "${RUNNER_SOURCE_COMMIT}" "${CONTAINER_IMAGE_DIGEST}" "${TIMEOUT_CLEANUP_RECEIPT_SHA256}" "${AGGREGATE_LIMIT_EVIDENCE_SHA256}" "${RUNTIME_POLICY_SHA256}" "${PROOF_DEPENDENCY_MANIFEST_SHA256}" "${WORKER_ARTIFACT_CLASSIFICATION}" "${WORKER_ARTIFACT_MANIFEST_SHA256}" "${WORKER_BUNDLE_SHA256}" "${CLIENT_ASSETS_SHA256}" "${CLIENT_ASSET_COUNT}" "${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" "${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" "${GENERATION_ISOLATION_EVIDENCE_SHA256}" "${GENERATION_ISOLATION_PROBE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256}" "${RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT}" <<'PY' import json import pathlib import sys payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) -if payload.get("ok") is not True: +if set(payload) != {"ok", "data"} or payload.get("ok") is not True: raise SystemExit("health response is not a success envelope") health = payload.get("data", {}) +expected_health_fields = { + "platform", + "sample", + "replay", + "liveGpt", + "liveCodex", + "liveKernel", + "maintenance", + "readiness", + "release", + "sandbox", + "generationFilesystemReadIsolation", + "requestId", +} +if not isinstance(health, dict) or set(health) != expected_health_fields: + raise SystemExit("health data has unknown or missing fields") +if not isinstance(health.get("requestId"), str) or not health["requestId"]: + raise SystemExit("health request ID is unavailable") required = { "platform": "cloudflare-workers", "sample": "available", @@ -528,6 +559,9 @@ expected_release = { "runnerImageDigest": sys.argv[5], "generationIsolationEvidenceSha256": sys.argv[19], "generationIsolationProbeSha256": sys.argv[20], + "releaseCheckGenerationIsolationEvidenceSha256": sys.argv[21], + "releaseCheckGenerationIsolationProbeSha256": sys.argv[22], + "releaseCheckGenerationIsolationVerifiedAt": sys.argv[23], "timeoutCleanupReceiptSha256": sys.argv[6], "aggregateLimitEvidenceSha256": sys.argv[7], "runtimePolicySha256": sys.argv[8], diff --git a/scripts/production_smoke_report.py b/scripts/production_smoke_report.py index c9c45142..da224064 100644 --- a/scripts/production_smoke_report.py +++ b/scripts/production_smoke_report.py @@ -7,7 +7,7 @@ import re import secrets import urllib.parse -from datetime import datetime +from datetime import datetime, timedelta from typing import Any @@ -167,7 +167,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "privacy", }: raise ValueError("smoke report has unknown or missing fields") - if report["schemaVersion"] not in {"2", "3", "4", "5"} or report[ + if report["schemaVersion"] not in {"2", "3", "4", "5", "6"} or report[ "status" ] not in _REPORT_STATUSES: raise ValueError("smoke report schema or status is invalid") @@ -186,7 +186,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "containerImageDigest", "deploymentReceiptSha256", } - if report["schemaVersion"] in {"3", "4", "5"}: + if report["schemaVersion"] in {"3", "4", "5", "6"}: deployment_fields.update( { "timeoutCleanupReceiptSha256", @@ -194,7 +194,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "proofDependencyManifestSha256", } ) - if report["schemaVersion"] in {"4", "5"}: + if report["schemaVersion"] in {"4", "5", "6"}: deployment_fields.update( { "aggregateLimitEvidenceSha256", @@ -209,7 +209,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "wranglerVersion", } ) - if report["schemaVersion"] == "5": + if report["schemaVersion"] in {"5", "6"}: deployment_fields.update( { "generationIsolationEvidenceSha256", @@ -217,6 +217,14 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "generationIsolationVerifiedAt", } ) + if report["schemaVersion"] == "6": + deployment_fields.update( + { + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + "releaseCheckGenerationIsolationVerifiedAt", + } + ) if not isinstance(deployment, dict) or set(deployment) != deployment_fields: raise ValueError("smoke deployment metadata is invalid") worker_version = deployment["workerVersion"] @@ -230,7 +238,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: if commit is not None and not _COMMIT.fullmatch(commit): raise ValueError(f"{field} is invalid") hash_fields = ["deploymentReceiptSha256"] - if report["schemaVersion"] in {"3", "4", "5"}: + if report["schemaVersion"] in {"3", "4", "5", "6"}: hash_fields.extend( [ "timeoutCleanupReceiptSha256", @@ -238,7 +246,7 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "proofDependencyManifestSha256", ] ) - if report["schemaVersion"] in {"4", "5"}: + if report["schemaVersion"] in {"4", "5", "6"}: hash_fields.extend( [ "aggregateLimitEvidenceSha256", @@ -248,18 +256,25 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: "clientPublicAssetsSha256", ] ) - if report["schemaVersion"] == "5": + if report["schemaVersion"] in {"5", "6"}: hash_fields.extend( [ "generationIsolationEvidenceSha256", "generationIsolationProbeSha256", ] ) + if report["schemaVersion"] == "6": + hash_fields.extend( + [ + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + ] + ) for field in hash_fields: value = deployment[field] if value is not None and not _SHA256.fullmatch(value): raise ValueError(f"{field} is invalid") - if report["schemaVersion"] in {"4", "5"}: + if report["schemaVersion"] in {"4", "5", "6"}: artifact_values = [ deployment[field] for field in ( @@ -298,13 +313,50 @@ def validate_report(report: dict[str, Any]) -> dict[str, Any]: or public_count > client_count ): raise ValueError("client asset counts are invalid") - if report["schemaVersion"] == "5": + if report["schemaVersion"] in {"5", "6"}: isolation_verified_at = deployment["generationIsolationVerifiedAt"] if isolation_verified_at is not None: - _timestamp( + qualification_time = _parsed_timestamp( isolation_verified_at, "generationIsolationVerifiedAt", ) + if qualification_time > report_started: + raise ValueError( + "generationIsolationVerifiedAt follows smoke startedAt" + ) + if report["schemaVersion"] == "6": + fresh_probe = deployment[ + "releaseCheckGenerationIsolationProbeSha256" + ] + qualified_probe = deployment["generationIsolationProbeSha256"] + if ( + fresh_probe is not None + and qualified_probe is not None + and fresh_probe != qualified_probe + ): + raise ValueError( + "release-check isolation probe does not match qualification" + ) + fresh_verified_at = deployment[ + "releaseCheckGenerationIsolationVerifiedAt" + ] + if fresh_verified_at is not None: + fresh_time = _parsed_timestamp( + fresh_verified_at, + "releaseCheckGenerationIsolationVerifiedAt", + ) + if fresh_time > report_started: + raise ValueError( + "releaseCheckGenerationIsolationVerifiedAt follows smoke startedAt" + ) + if report_started - fresh_time > timedelta(hours=24): + raise ValueError( + "releaseCheckGenerationIsolationVerifiedAt is stale" + ) + if isolation_verified_at is not None and fresh_time < qualification_time: + raise ValueError( + "release-check isolation verification precedes qualification" + ) if report["privacy"] != { "containsSecrets": False, "containsRawNotebookBytes": False, @@ -384,6 +436,9 @@ def initialize_report( generation_isolation_evidence_sha256: str | None = None, generation_isolation_probe_sha256: str | None = None, generation_isolation_verified_at: str | None = None, + release_check_generation_isolation_evidence_sha256: str | None = None, + release_check_generation_isolation_probe_sha256: str | None = None, + release_check_generation_isolation_verified_at: str | None = None, worker_artifact_classification: str | None = None, worker_artifact_manifest_sha256: str | None = None, worker_bundle_sha256: str | None = None, @@ -397,7 +452,7 @@ def initialize_report( ) -> dict[str, Any]: report = validate_report( { - "schemaVersion": "5", + "schemaVersion": "6", "status": "RUNNING", "baseUrl": _base_url(base_url), "startedAt": _timestamp(started_at, "startedAt"), @@ -422,6 +477,15 @@ def initialize_report( generation_isolation_probe_sha256 ), "generationIsolationVerifiedAt": generation_isolation_verified_at, + "releaseCheckGenerationIsolationEvidenceSha256": ( + release_check_generation_isolation_evidence_sha256 + ), + "releaseCheckGenerationIsolationProbeSha256": ( + release_check_generation_isolation_probe_sha256 + ), + "releaseCheckGenerationIsolationVerifiedAt": ( + release_check_generation_isolation_verified_at + ), "workerArtifactClassification": worker_artifact_classification, "workerArtifactManifestSha256": worker_artifact_manifest_sha256, "workerBundleSha256": worker_bundle_sha256, @@ -516,6 +580,11 @@ def _parser() -> argparse.ArgumentParser: initialize.add_argument("--generation-isolation-evidence-sha256") initialize.add_argument("--generation-isolation-probe-sha256") initialize.add_argument("--generation-isolation-verified-at") + initialize.add_argument( + "--release-check-generation-isolation-evidence-sha256" + ) + initialize.add_argument("--release-check-generation-isolation-probe-sha256") + initialize.add_argument("--release-check-generation-isolation-verified-at") initialize.add_argument("--worker-artifact-classification") initialize.add_argument("--worker-artifact-manifest-sha256") initialize.add_argument("--worker-bundle-sha256") @@ -566,6 +635,15 @@ def main() -> int: generation_isolation_verified_at=( arguments.generation_isolation_verified_at ), + release_check_generation_isolation_evidence_sha256=( + arguments.release_check_generation_isolation_evidence_sha256 + ), + release_check_generation_isolation_probe_sha256=( + arguments.release_check_generation_isolation_probe_sha256 + ), + release_check_generation_isolation_verified_at=( + arguments.release_check_generation_isolation_verified_at + ), worker_artifact_classification=( arguments.worker_artifact_classification ), diff --git a/scripts/release-check-receipt.ts b/scripts/release-check-receipt.ts index 8ab77291..735b75f8 100644 --- a/scripts/release-check-receipt.ts +++ b/scripts/release-check-receipt.ts @@ -9,10 +9,13 @@ import { ContainedRuntimeAttestationSchema, DeploymentReceiptV5Schema, DeploymentReceiptV6Schema, + DeploymentReceiptV7Schema, GenerationIsolationEvidenceV1Schema, RELEASE_CHECK_IDS, QualifiedRunnerReleaseV5Schema, ReleaseCheckReceiptV4Schema, + ReleaseCheckReceiptV5Schema, + type ReleaseCheckReceiptV5, } from "../packages/scientific-engine-registry/src/index.js"; import { hashGenerationIsolationEvidence, @@ -58,17 +61,57 @@ export function createQualifiedReleaseIdentity(receiptBytes: Buffer) { } export function createDeploymentReleaseIdentity(receiptBytes: Buffer) { + const value = JSON.parse(receiptBytes.toString("utf8")) as unknown; + const schemaVersion = + typeof value === "object" && value !== null && "schemaVersion" in value + ? value.schemaVersion + : undefined; + const receipt = + schemaVersion === "7" + ? DeploymentReceiptV7Schema.parse(value) + : schemaVersion === "6" + ? DeploymentReceiptV6Schema.parse(value) + : DeploymentReceiptV5Schema.parse(value); + return { + identitySchemaVersion: "1", + receiptType: "deployment-receipt", + receiptSha256: sha256(receiptBytes), + receipt, + } as const; +} + +export function parseReleaseCheckReceiptV5( + value: unknown, +): ReleaseCheckReceiptV5 { + const receipt = ReleaseCheckReceiptV5Schema.parse(value); + verifyGenerationIsolationEvidence({ + evidence: receipt.releaseCheckGenerationIsolationEvidence, + evidenceSha256: receipt.releaseCheckGenerationIsolationEvidenceSha256, + expected: { + sourceCommit: receipt.sourceCommit, + sourceTreeSha256: + receipt.releaseCheckGenerationIsolationEvidence.sourceTreeSha256, + localImageTag: receipt.runnerImageTag, + localImageDigest: receipt.runnerImageDigest, + probeSha256: receipt.releaseCheckGenerationIsolationProbeSha256, + verifiedAt: receipt.releaseCheckGenerationIsolationVerifiedAt, + }, + }); + return receipt; +} + +export function createReleaseCheckReleaseIdentity(receiptBytes: Buffer) { const value = JSON.parse(receiptBytes.toString("utf8")) as unknown; const receipt = typeof value === "object" && value !== null && "schemaVersion" in value && - value.schemaVersion === "6" - ? DeploymentReceiptV6Schema.parse(value) - : DeploymentReceiptV5Schema.parse(value); + value.schemaVersion === "5" + ? parseReleaseCheckReceiptV5(value) + : ReleaseCheckReceiptV4Schema.parse(value); return { identitySchemaVersion: "1", - receiptType: "deployment-receipt", + receiptType: "release-check-receipt", receiptSha256: sha256(receiptBytes), receipt, } as const; @@ -76,12 +119,22 @@ export function createDeploymentReleaseIdentity(receiptBytes: Buffer) { export function assertReleaseCheckBinding(input: BindingInput) { const qualified = parseQualifiedRunnerReleaseV6(input.qualifiedReceipt); - const releaseCheck = ReleaseCheckReceiptV4Schema.parse( - input.releaseCheckReceipt, - ); + const releaseCheck = parseReleaseCheckReceiptV5(input.releaseCheckReceipt); const runtime = ContainedRuntimeAttestationSchema.parse( input.runtimeAttestation, ); + verifyGenerationIsolationEvidence({ + evidence: releaseCheck.releaseCheckGenerationIsolationEvidence, + evidenceSha256: releaseCheck.releaseCheckGenerationIsolationEvidenceSha256, + expected: { + sourceCommit: qualified.sourceCommit, + sourceTreeSha256: qualified.sourceTreeSha256, + localImageTag: qualified.localImageTag, + localImageDigest: qualified.localImageDigest, + probeSha256: qualified.generationIsolationProbeSha256, + verifiedAt: releaseCheck.releaseCheckGenerationIsolationVerifiedAt, + }, + }); const comparisons: Array<[string, string, string]> = [ ["evidence commit", releaseCheck.evidenceCommit, qualified.evidenceCommit], ["source commit", releaseCheck.sourceCommit, qualified.sourceCommit], @@ -223,7 +276,7 @@ export function createReleaseCheckReceipt(input: { const freshEvidence = GenerationIsolationEvidenceV1Schema.parse( input.generationIsolationEvidence, ); - verifyGenerationIsolationEvidence({ + const freshVerification = verifyGenerationIsolationEvidence({ evidence: freshEvidence, evidenceSha256: hashGenerationIsolationEvidence(freshEvidence), expected: { @@ -244,8 +297,8 @@ export function createReleaseCheckReceipt(input: { ) { throw new Error("release-check generation-isolation evidence is not fresh"); } - const receipt = ReleaseCheckReceiptV4Schema.parse({ - schemaVersion: "4", + const receipt = parseReleaseCheckReceiptV5({ + schemaVersion: "5", status: "PASSED", generationFilesystemReadIsolation: qualified.generationFilesystemReadIsolation, @@ -253,6 +306,12 @@ export function createReleaseCheckReceipt(input: { qualified.generationIsolationEvidenceSha256, generationIsolationProbeSha256: qualified.generationIsolationProbeSha256, generationIsolationVerifiedAt: qualified.generationIsolationVerifiedAt, + releaseCheckGenerationIsolationEvidence: freshVerification.evidence, + releaseCheckGenerationIsolationEvidenceSha256: + freshVerification.evidenceSha256, + releaseCheckGenerationIsolationProbeSha256: freshVerification.probeSha256, + releaseCheckGenerationIsolationVerifiedAt: + freshVerification.evidence.verifiedAt, evidenceCommit: qualified.evidenceCommit, sourceCommit: qualified.sourceCommit, qualifiedRunnerReceiptSha256: sha256(input.qualifiedReceiptBytes), @@ -269,7 +328,7 @@ export function createReleaseCheckReceipt(input: { runtimeAdapterSha256: qualified.runtimeAdapterSha256, checks: RELEASE_CHECK_IDS.map((id) => ({ id, status: "PASSED" })), checkedAt, - verifierVersion: "counterlab-release-check-v4", + verifierVersion: "counterlab-release-check-v5", }); return assertReleaseCheckBinding({ qualifiedReceipt: qualified, @@ -402,6 +461,21 @@ async function main(): Promise { ); return; } + if (process.argv[2] === "release-check-identity") { + if (process.argv.length !== 5 || process.argv[3] !== "--release-check") { + throw new Error( + "Usage: release-check-receipt release-check-identity --release-check FILE", + ); + } + const releaseCheckPath = await existingRepositoryFile( + root, + process.argv[4]!, + ); + process.stdout.write( + `${JSON.stringify(createReleaseCheckReleaseIdentity(readFileSync(releaseCheckPath)))}\n`, + ); + return; + } const args = argumentsFrom(process.argv.slice(2)); const [qualifiedPath, runtimeAdapter, generationIsolationReport, output] = await Promise.all([ diff --git a/scripts/test_production_smoke.py b/scripts/test_production_smoke.py index 3d73338f..ff2ec167 100644 --- a/scripts/test_production_smoke.py +++ b/scripts/test_production_smoke.py @@ -44,6 +44,11 @@ def _deployment_identity(**overrides: object) -> dict[str, object]: "generation_isolation_evidence_sha256": "5" * 64, "generation_isolation_probe_sha256": "6" * 64, "generation_isolation_verified_at": "2026-07-15T11:59:00Z", + "release_check_generation_isolation_evidence_sha256": "7" * 64, + "release_check_generation_isolation_probe_sha256": "6" * 64, + "release_check_generation_isolation_verified_at": ( + "2026-07-15T11:59:30Z" + ), "worker_artifact_classification": "PROCESS_BOUND_PARTIAL", "worker_artifact_manifest_sha256": "1" * 64, "worker_bundle_sha256": "2" * 64, @@ -123,7 +128,10 @@ def _control_plane_validator_source(kind: str) -> str: '"${CLIENT_PUBLIC_ASSETS_SHA256}" "${CLIENT_PUBLIC_ASSET_COUNT}" ' '"${FROZEN_VITE_VERSION}" "${FROZEN_WRANGLER_VERSION}" ' '"${GENERATION_ISOLATION_EVIDENCE_SHA256}" ' - '"${GENERATION_ISOLATION_PROBE_SHA256}" <<\'PY\'\n' + '"${GENERATION_ISOLATION_PROBE_SHA256}" ' + '"${RELEASE_CHECK_GENERATION_ISOLATION_EVIDENCE_SHA256}" ' + '"${RELEASE_CHECK_GENERATION_ISOLATION_PROBE_SHA256}" ' + '"${RELEASE_CHECK_GENERATION_ISOLATION_VERIFIED_AT}" <<\'PY\'\n' ) start = script.index(marker) + len(marker) return script[start : script.index("\nPY\n", start)] @@ -151,7 +159,7 @@ def _deployment_receipt() -> dict[str, object]: runner = "b" * 40 digest = "sha256:" + "c" * 64 return { - "schemaVersion": "6", + "schemaVersion": "7", "status": "DEPLOYED", "workerName": "counterlab", "productionOrigin": "https://counterlab.cserules.workers.dev", @@ -159,6 +167,11 @@ def _deployment_receipt() -> dict[str, object]: "generationIsolationEvidenceSha256": "6" * 64, "generationIsolationProbeSha256": "7" * 64, "generationIsolationVerifiedAt": "2026-07-18T23:58:00.000Z", + "releaseCheckGenerationIsolationEvidenceSha256": "8" * 64, + "releaseCheckGenerationIsolationProbeSha256": "7" * 64, + "releaseCheckGenerationIsolationVerifiedAt": ( + "2026-07-18T23:58:30.000Z" + ), "workerEvidenceCommit": worker, "runnerSourceCommit": runner, "qualifiedRunnerReceiptSha256": "1" * 64, @@ -197,7 +210,7 @@ def _deployment_receipt() -> dict[str, object]: "workerVersionSha256": "a" * 64, "containerStatusSha256": "b" * 64, "deployedAt": "2026-07-19T00:00:00.000Z", - "verifierVersion": "counterlab-deployment-v6", + "verifierVersion": "counterlab-deployment-v7", } @@ -223,6 +236,34 @@ def test_deployment_receipt_parser_accepts_only_the_exact_release_tuple() -> Non assert identity["receipt"] == receipt assert identity["receiptSha256"] == hashlib.sha256(path.read_bytes()).hexdigest() + historical_v6 = json.loads(json.dumps(receipt)) + historical_v6["schemaVersion"] = "6" + historical_v6["verifierVersion"] = "counterlab-deployment-v6" + for field in ( + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + "releaseCheckGenerationIsolationVerifiedAt", + ): + historical_v6.pop(field) + path.write_text(json.dumps(historical_v6), encoding="utf-8") + parsed_v6 = _parse_deployment_identity(path) + assert parsed_v6.returncode == 0, parsed_v6.stderr + assert json.loads(parsed_v6.stdout)["receipt"]["schemaVersion"] == "6" + + historical_v5 = json.loads(json.dumps(historical_v6)) + historical_v5["schemaVersion"] = "5" + historical_v5["verifierVersion"] = "counterlab-deployment-v5" + for field in ( + "generationIsolationEvidenceSha256", + "generationIsolationProbeSha256", + "generationIsolationVerifiedAt", + ): + historical_v5.pop(field) + path.write_text(json.dumps(historical_v5), encoding="utf-8") + parsed_v5 = _parse_deployment_identity(path) + assert parsed_v5.returncode == 0, parsed_v5.stderr + assert json.loads(parsed_v5.stdout)["receipt"]["schemaVersion"] == "5" + receipt["unexpected"] = True path.write_text(json.dumps(receipt), encoding="utf-8") unknown = _parse_deployment_identity(path) @@ -275,6 +316,9 @@ def test_control_plane_validators_bind_exact_release_and_maintenance_state() -> wrangler_version = "4.110.0" generation_isolation_evidence = "5" * 64 generation_isolation_probe = "6" * 64 + release_check_isolation_evidence = "7" * 64 + release_check_isolation_probe = generation_isolation_probe + release_check_isolation_verified_at = "2026-07-15T11:59:30Z" release = { "status": "bound", "workerVersionId": version, @@ -284,6 +328,15 @@ def test_control_plane_validators_bind_exact_release_and_maintenance_state() -> "runnerImageDigest": digest, "generationIsolationEvidenceSha256": generation_isolation_evidence, "generationIsolationProbeSha256": generation_isolation_probe, + "releaseCheckGenerationIsolationEvidenceSha256": ( + release_check_isolation_evidence + ), + "releaseCheckGenerationIsolationProbeSha256": ( + release_check_isolation_probe + ), + "releaseCheckGenerationIsolationVerifiedAt": ( + release_check_isolation_verified_at + ), "timeoutCleanupReceiptSha256": timeout_receipt, "aggregateLimitEvidenceSha256": aggregate_limit_evidence, "runtimePolicySha256": runtime_policy, @@ -300,6 +353,7 @@ def test_control_plane_validators_bind_exact_release_and_maintenance_state() -> } ready = { "status": "ready", + "service": "counterlab-control-plane", "checks": { key: True for key in ( @@ -330,6 +384,7 @@ def test_control_plane_validators_bind_exact_release_and_maintenance_state() -> "readiness": "ready", "maintenance": False, "release": release, + "requestId": "request-test-1", }, } @@ -361,6 +416,9 @@ def run_validator( wrangler_version, generation_isolation_evidence, generation_isolation_probe, + release_check_isolation_evidence, + release_check_isolation_probe, + release_check_isolation_verified_at, ], check=False, capture_output=True, @@ -401,6 +459,9 @@ def run_validator( "wranglerVersion", "generationIsolationEvidenceSha256", "generationIsolationProbeSha256", + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + "releaseCheckGenerationIsolationVerifiedAt", ): tampered = json.loads(json.dumps(payload)) tampered_release = ( @@ -416,6 +477,31 @@ def run_validator( assert rejected.returncode != 0 assert "release identity does not match" in rejected.stderr + for scope in ("top", "release"): + expanded = json.loads(json.dumps(payload)) + if scope == "top": + expanded["unexpected"] = True + elif kind == "ready": + expanded["release"]["unexpected"] = True + else: + expanded["data"]["release"]["unexpected"] = True + path.write_text(json.dumps(expanded), encoding="utf-8") + rejected = run_validator(kind, path) + assert rejected.returncode != 0 + + if kind == "ready": + expanded = json.loads(json.dumps(payload)) + expanded["checks"]["unexpected"] = True + path.write_text(json.dumps(expanded), encoding="utf-8") + rejected = run_validator(kind, path) + assert rejected.returncode != 0 + else: + expanded = json.loads(json.dumps(payload)) + expanded["data"]["unexpected"] = True + path.write_text(json.dumps(expanded), encoding="utf-8") + rejected = run_validator(kind, path) + assert rejected.returncode != 0 + maintenance = json.loads(json.dumps(payload)) if kind == "ready": maintenance["maintenance"] = True @@ -519,7 +605,7 @@ def test_smoke_report_is_atomic_idempotent_and_secret_free() -> None: **_deployment_identity(), ) assert initialized["status"] == "RUNNING" - assert initialized["schemaVersion"] == "5" + assert initialized["schemaVersion"] == "6" assert initialized["baseUrl"] == "https://counterlab.example.test" assert initialized["stages"] == [] @@ -544,7 +630,7 @@ def test_smoke_report_is_atomic_idempotent_and_secret_free() -> None: } -def test_smoke_report_versions_preserve_v2_v3_v4_and_require_v5_isolation_bindings() -> None: +def test_smoke_report_versions_preserve_v2_through_v5_and_require_v6_fresh_isolation() -> None: with tempfile.TemporaryDirectory() as destination: path = pathlib.Path(destination) / "production-smoke.json" current = initialize_report( @@ -553,7 +639,13 @@ def test_smoke_report_versions_preserve_v2_v3_v4_and_require_v5_isolation_bindin started_at="2026-07-15T12:00:00Z", **_deployment_identity(), ) - assert current["schemaVersion"] == "5" + assert current["schemaVersion"] == "6" + + v6_fields = ( + "releaseCheckGenerationIsolationEvidenceSha256", + "releaseCheckGenerationIsolationProbeSha256", + "releaseCheckGenerationIsolationVerifiedAt", + ) v5_fields = ( "generationIsolationEvidenceSha256", @@ -574,7 +666,13 @@ def test_smoke_report_versions_preserve_v2_v3_v4_and_require_v5_isolation_bindin "wranglerVersion", ) - historical_v4 = json.loads(json.dumps(current)) + historical_v5 = json.loads(json.dumps(current)) + historical_v5["schemaVersion"] = "5" + for field in v6_fields: + historical_v5["deployment"].pop(field) + assert validate_report(historical_v5)["schemaVersion"] == "5" + + historical_v4 = json.loads(json.dumps(historical_v5)) historical_v4["schemaVersion"] = "4" for field in v5_fields: historical_v4["deployment"].pop(field) @@ -616,21 +714,58 @@ def test_smoke_report_versions_preserve_v2_v3_v4_and_require_v5_isolation_bindin with pytest.raises(ValueError, match="proofDependencyManifestSha256"): validate_report(malformed_v3) - malformed_v5_hash = json.loads(json.dumps(current)) + malformed_v5_hash = json.loads(json.dumps(historical_v5)) malformed_v5_hash["deployment"]["generationIsolationProbeSha256"] = "short" with pytest.raises(ValueError, match="generationIsolationProbeSha256"): validate_report(malformed_v5_hash) - malformed_v5_time = json.loads(json.dumps(current)) + malformed_v5_time = json.loads(json.dumps(historical_v5)) malformed_v5_time["deployment"]["generationIsolationVerifiedAt"] = "yesterday" with pytest.raises(ValueError, match="generationIsolationVerifiedAt"): validate_report(malformed_v5_time) - invalid_v5 = json.loads(json.dumps(current)) + invalid_v5 = json.loads(json.dumps(historical_v5)) invalid_v5["deployment"]["clientPublicAssetCount"] = 28 with pytest.raises(ValueError, match="asset counts"): validate_report(invalid_v5) + malformed_v6_hash = json.loads(json.dumps(current)) + malformed_v6_hash["deployment"][ + "releaseCheckGenerationIsolationEvidenceSha256" + ] = "short" + with pytest.raises( + ValueError, + match="releaseCheckGenerationIsolationEvidenceSha256", + ): + validate_report(malformed_v6_hash) + + mismatched_v6_probe = json.loads(json.dumps(current)) + mismatched_v6_probe["deployment"][ + "releaseCheckGenerationIsolationProbeSha256" + ] = "0" * 64 + with pytest.raises(ValueError, match="does not match qualification"): + validate_report(mismatched_v6_probe) + + for verified_at, message in ( + ("2026-07-15T12:00:01Z", "follows smoke startedAt"), + ("2026-07-14T11:59:59Z", "stale"), + ("2026-07-15T11:58:59Z", "precedes qualification"), + ("yesterday", "releaseCheckGenerationIsolationVerifiedAt"), + ): + invalid_time = json.loads(json.dumps(current)) + invalid_time["deployment"][ + "releaseCheckGenerationIsolationVerifiedAt" + ] = verified_at + with pytest.raises(ValueError, match=message): + validate_report(invalid_time) + + future_qualification = json.loads(json.dumps(current)) + future_qualification["deployment"][ + "generationIsolationVerifiedAt" + ] = "2026-07-15T12:00:01Z" + with pytest.raises(ValueError, match="follows smoke startedAt"): + validate_report(future_qualification) + def test_smoke_report_rejects_conflicts_secrets_and_credential_urls() -> None: with tempfile.TemporaryDirectory() as destination: From ea01ca7efa2a47911713e2f3ea8cf4644f4de154 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 04:22:13 +0530 Subject: [PATCH 015/143] docs(progress): record fresh release chain --- docs/PROGRESS.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 33880daa..e0f936f8 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -12,7 +12,18 @@ pass is never presented as a browser or production pass. ### Current checkpoint - **Branch:** `feat/learner-ux-v6.1` -- **Current product checkpoint:** +- **Latest committed checkpoint:** `9348093a15942e7fb59c2755d3e080a20785b5c0`. + The fresh exact-image isolation rerun is independently hash-bound through + release-check receipt v5, deployment receipt v7, Worker release identity, + exact public response validation, and production-smoke report v6. Historical + receipt readers remain compatible. The worktree contains only the preserved, + untracked `scripts/.fuse_hidden0000a9600000801f`, which remains unopened, + unstaged, and undeleted. The complete web suite passes **616/616**; the + correctly contained and socket-enabled root suite passes **667/670** with + **2 skipped**, leaving exactly three stale scientific-evidence failures. + Qualification, deployment, browser qualification, learner evidence, and + submission remain unclaimed. +- **Earlier product checkpoint:** `5259a495948a68024702ba5239bf8057d1785f08`. The owner partitioned the preserved integration delta into nine commits and fast-forwarded both `feat/learner-ux-v6.1` and `main` to `134c22f` before qualification. The lead @@ -3034,3 +3045,61 @@ undeleted. 5. Align and verify Devpost, public video, repository, feedback ID, and submission receipt; push the feature branch and fast-forward its verified tip into `main` only after the recorded gates are truthful. + +## Finish Commander fresh release-chain checkpoint — 2026-07-20T22:50:08Z + +Deadline: `2026-07-22T00:00:00Z`; **25 hours 9 minutes 52 seconds remained at +this checkpoint**. The feature branch is at committed HEAD +`9348093a15942e7fb59c2755d3e080a20785b5c0`; the only worktree entry besides +this progress update is the preserved untracked +`scripts/.fuse_hidden0000a9600000801f`. + +### Newly frozen independent commits + +- `e8ae61f fix(release): bind qualified isolation identity` carries the + qualification-time isolation evidence and exact probe through qualified + receipts, runtime readiness, Worker bindings, deployment receipts, and + public release identity. Active Worker bindings now reject duplicates. +- `9348093 fix(release): bind fresh isolation recheck` separately embeds and + hashes the release-time rerun, introduces release-check receipt v5, + deployment receipt v7, and smoke report v6, validates exact public response + shapes, preserves historical readers, rejects future/reversed/stale proof, + and requires the rerun within 30 minutes of receipt issuance. + +### Exact verification at this checkpoint + +- Fresh receipt/deployment contract Vitest: **4 files, 35/35 passed**. +- Worker API/deployment-config Vitest: **2 files, 123/123 passed**. +- Production-smoke Pytest: **19/19 passed**. +- Complete web Vitest: **78 files, 616/616 passed**. +- Complete root Vitest with an absolute repository-contained temp root and + loopback permission: **58/60 files passed; 667 passed, 3 failed, 2 skipped**. + All three failures are current source-bound scientific-evidence drift in the + release verifier; the earlier relative-temp and socket-denied run is not + treated as product evidence. +- Repository, web, and Worker strict TypeScript: **passed**. +- Production Vite/Worker build: **passed**. The main client remains + **552.03 kB** minified and above the 500 kB warning threshold; Worker output + is **1,862.23 kB**. +- Scoped Prettier, Bash syntax, Git whitespace, generated contract schemas, + and a 19-file secret scan: **passed**. + +### Ordered remaining gates + +1. Refresh the exact source-bound scientific integrity, SBOM tool-lock, VEX, + and vulnerability evidence without weakening the three failing tests. +2. Resolve the named FUSE artifact only under the repository destructive-action + rule; until then, clean-tree release checks remain blocked. +3. Freeze one source commit, build and exercise one exact runner image, prove + the generation-isolation sentinel and real aggregate containment, regenerate + all source/image-bound evidence, and issue qualification/release receipts. +4. Verify repository-contained Wrangler authentication, deploy only the + qualified tuple, and run production smoke against the exact public identity. +5. Run the full desktop/mobile/accessibility/error-path matrix through + CloakBrowser. Stock Chromium remains design-review evidence only. +6. Keep learner evidence `NO_DATA` unless real consented participants are + observed; align Devpost, video, public repository, feedback ID, and every + claim before authorized submission. +7. Push the completed feature branch, verify its remote tip, fast-forward the + exact verified commit into `main`, and push `main` only after the release and + submission records are truthful. From de21f14536302d24f6dc40843632ee3c3b6bfd8d Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 04:29:26 +0530 Subject: [PATCH 016/143] fix(release): verify refreshed image digest --- scripts/refresh-source-bound-scientific-evidence.sh | 1 + scripts/source-bound-evidence.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/refresh-source-bound-scientific-evidence.sh b/scripts/refresh-source-bound-scientific-evidence.sh index fe4eb8b5..89dac7fb 100644 --- a/scripts/refresh-source-bound-scientific-evidence.sh +++ b/scripts/refresh-source-bound-scientific-evidence.sh @@ -500,6 +500,7 @@ node --import tsx scripts/refresh-scientific-engine-bindings.ts --check COUNTERLAB_DOCKER_BIN="${RUNTIME_ADAPTER}" \ ./scripts/verify-scientific-engines.sh \ --image "${IMAGE}" \ + --expected-image-digest "${IMAGE_DIGEST}" \ --runtime-report "${WORK}/runtime-verification.json" RECEIPT="${WORK}/evidence-refresh-receipt.json" diff --git a/scripts/source-bound-evidence.test.ts b/scripts/source-bound-evidence.test.ts index 6080967e..b0ff8e13 100644 --- a/scripts/source-bound-evidence.test.ts +++ b/scripts/source-bound-evidence.test.ts @@ -591,6 +591,7 @@ describe("source-bound release evidence helpers", () => { expect(refresh).toContain( '--runtime-report "${WORK}/runtime-verification.json"', ); + expect(refresh).toContain('--expected-image-digest "${IMAGE_DIGEST}"'); expect( refresh.match(/--manifest-digest "\$\{MANIFEST_DIGEST\}"/gu), ).toHaveLength(2); From 85c5652afc75230a28209883153814ca6cb64c36 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 04:34:48 +0530 Subject: [PATCH 017/143] fix(runtime): preserve supervisor response channel --- scripts/contained-runtime-supervisor.mjs | 2 +- scripts/contained-runtime-supervisor.test.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/contained-runtime-supervisor.mjs b/scripts/contained-runtime-supervisor.mjs index 05abc985..c9e4e034 100644 --- a/scripts/contained-runtime-supervisor.mjs +++ b/scripts/contained-runtime-supervisor.mjs @@ -332,7 +332,7 @@ async function handleRequest(request) { throw new Error("runtime supervisor action is invalid"); } -const controlServer = createServer((socket) => { +const controlServer = createServer({ allowHalfOpen: true }, (socket) => { const chunks = []; let total = 0; socket.setTimeout(35_000); diff --git a/scripts/contained-runtime-supervisor.test.ts b/scripts/contained-runtime-supervisor.test.ts index 5c14317f..ef4bee79 100644 --- a/scripts/contained-runtime-supervisor.test.ts +++ b/scripts/contained-runtime-supervisor.test.ts @@ -55,6 +55,17 @@ function drainReceipt() { } describe("contained runtime supervisor protocol", () => { + it("keeps the response side open after a client finishes its request", () => { + const source = readFileSync( + resolve(process.cwd(), "scripts/contained-runtime-supervisor.mjs"), + "utf8", + ); + + expect(source).toContain( + "const controlServer = createServer({ allowHalfOpen: true }, (socket) => {", + ); + }); + it("keeps a failed owned-child shutdown retryable", () => { const source = readFileSync( resolve(process.cwd(), "scripts/contained-runtime-supervisor.mjs"), From 9ac208d64a13232c76fd4bba8c0a562cee196c4e Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 04:37:18 +0530 Subject: [PATCH 018/143] docs(progress): record runtime startup repair --- docs/PROGRESS.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index e0f936f8..2142b831 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -3103,3 +3103,45 @@ this progress update is the preserved untracked 7. Push the completed feature branch, verify its remote tip, fast-forward the exact verified commit into `main`, and push `main` only after the release and submission records are truthful. + +## Finish Commander runtime-start checkpoint — 2026-07-20T23:06:21Z + +Deadline: `2026-07-22T00:00:00Z`; **24 hours 53 minutes 39 seconds remained at +this checkpoint**. The feature branch is at committed HEAD +`85c5652afc75230a28209883153814ca6cb64c36`; the only visible worktree entry is +the preserved untracked `scripts/.fuse_hidden0000a9600000801f`. + +### Newly frozen independent commits + +- `de21f14 fix(release): verify refreshed image digest` passes the build + receipt's exact image digest into the final scientific verifier instead of + relying on the mutable local tag until later receipt validation. The focused + source-bound evidence suite passed **11/11**; shell syntax, formatting, + whitespace, and the two-file secret scan passed. +- `85c5652 fix(runtime): preserve supervisor response channel` configures the + Unix control server for request half-close semantics. Before the repair, a + real fresh runtime reached its sockets but the attestation request received + an empty response; the added characterization failed **1/5**. After repair, + supervisor, attestation, entry, and contained-runtime suites passed **52/52**, + and a second fresh session reached `Contained runtime session ready`. + +### Current external and release truth + +- The repository-contained Wrangler `4.110.0` identity check failed closed + because its isolated profile reported `loggedIn: false`. A fresh manual OAuth + flow was started with `--browser=false` and timed out without a callback. No + normal-home credential was read or copied, and no Cloudflare resource was + mutated. +- The current shell exposes no `CLOAK_CDP_ENDPOINT`; no CloakBrowser result is + claimed. +- Read-only source audit confirms that the contained rootless OCI path still + removes cgroup resources and records aggregate enforcement as false. The + strict timeout and qualification contracts correctly require genuine, + source-bound Linux cgroup v2 negative-control evidence, so no qualified + receipt can be issued from the current producer. This gate will not be + weakened or replaced with a fabricated receipt. +- The FUSE artifact still blocks the exact build's clean-tree gate. Its removal + remains pending the constitution's immediate explicit approval. +- The three scientific release-verifier failures remain the expected stale + exact-image evidence and can be resolved only after a clean source build and + authoritative full refresh. From 773e8087402713c0aa26587e6b3f982428189f30 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 04:59:25 +0530 Subject: [PATCH 019/143] fix(judge): open verified sample proof directly --- .../src/features/judge/JudgeModeView.test.tsx | 50 ++++++++++++++++++- apps/web/src/features/judge/JudgeModeView.tsx | 16 ++++-- .../src/features/judge/SampleEvidencePack.tsx | 15 +++++- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/apps/web/src/features/judge/JudgeModeView.test.tsx b/apps/web/src/features/judge/JudgeModeView.test.tsx index 0dda267b..bd9f9b30 100644 --- a/apps/web/src/features/judge/JudgeModeView.test.tsx +++ b/apps/web/src/features/judge/JudgeModeView.test.tsx @@ -1,10 +1,15 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import type { CapabilityHealth } from "../../api"; import { JudgeModeView } from "./JudgeModeView"; +const repositoryRoot = resolve(import.meta.dirname, "../../../../.."); + const configuredHealth: CapabilityHealth = { platform: "cloudflare-workers", sample: "available", @@ -45,6 +50,10 @@ const isolatedHealth: CapabilityHealth = { }; describe("JudgeModeView", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + it("distinguishes sample, live, and legacy replay authority", async () => { render( { ); expect( screen.getByRole("link", { - name: /inspect exact values and integrity/i, + name: /inspect verified sample proof/i, }), ).toHaveAttribute("href", "#sample-evidence"); expect(screen.getByText("Sample lesson")).toBeInTheDocument(); @@ -203,6 +212,43 @@ describe("JudgeModeView", () => { expect(onStartSample).toHaveBeenCalledTimes(1); }); + it("opens the verified Sample Proof Capsule from the first fold in one action", async () => { + const capsuleBytes = await readFile( + resolve( + repositoryRoot, + "fixtures/public/leakage_sample_proof_capsule_v1.counterlab", + ), + ); + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(capsuleBytes, { status: 200 })), + ); + const user = userEvent.setup(); + + render( + , + ); + + await screen.findByText(/capsule bytes match the checked-in reference/iu); + await user.click( + screen.getByRole("link", { + name: /inspect verified sample proof/iu, + }), + ); + + const summary = screen.getByText("Inspect Sample Proof Capsule", { + selector: "summary", + }); + expect(summary.closest("details")).toHaveAttribute("open"); + expect(summary).toHaveFocus(); + }); + it("does not offer a live link when deployed authority is unavailable", async () => { const user = userEvent.setup(); const retry = vi.fn(); diff --git a/apps/web/src/features/judge/JudgeModeView.tsx b/apps/web/src/features/judge/JudgeModeView.tsx index 08c11c8f..b0a598f2 100644 --- a/apps/web/src/features/judge/JudgeModeView.tsx +++ b/apps/web/src/features/judge/JudgeModeView.tsx @@ -1,3 +1,5 @@ +import { useState } from "react"; + import type { CapabilityHealth } from "../../api"; import { VerifiedBeliefBreakMechanism } from "../../components/learner/VerifiedBeliefBreakTheater"; import { verifiedReplay } from "../../sampleReplayMetadata"; @@ -40,6 +42,8 @@ export function JudgeModeView({ }) { const liveReady = liveAuthorityReady(health); const release = health?.release?.status === "bound" ? health.release : null; + const [sampleEvidenceInspectionRequest, setSampleEvidenceInspectionRequest] = + useState(0); return (
@@ -92,8 +96,14 @@ export function JudgeModeView({ occurs in this preview.

-
- Inspect exact values and integrity + + setSampleEvidenceInspectionRequest((request) => request + 1) + } + > + Inspect verified sample proof @@ -341,7 +351,7 @@ export function JudgeModeView({ - +
diff --git a/apps/web/src/features/judge/SampleEvidencePack.tsx b/apps/web/src/features/judge/SampleEvidencePack.tsx index aadb8210..0629f08b 100644 --- a/apps/web/src/features/judge/SampleEvidencePack.tsx +++ b/apps/web/src/features/judge/SampleEvidencePack.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { type ValidatedSampleProofCapsuleV1, @@ -69,11 +69,14 @@ function HashValue({ value }: { value: string }) { export function SampleEvidencePack({ verifyCapsule = verifyCheckedInSampleProofCapsule, createDownload = createVerifiedDownload, + inspectionRequest = 0, }: { verifyCapsule?: () => Promise; createDownload?: (bytes: Uint8Array) => VerifiedDownload; + inspectionRequest?: number; }) { const [state, setState] = useState({ status: "checking" }); + const inspectionRef = useRef(null); useEffect(() => { let active = true; @@ -102,6 +105,14 @@ export function SampleEvidencePack({ }; }, [createDownload, verifyCapsule]); + useEffect(() => { + if (inspectionRequest < 1 || state.status !== "ready") return; + const inspection = inspectionRef.current; + if (inspection === null) return; + inspection.open = true; + inspection.querySelector("summary")?.focus({ preventScroll: true }); + }, [inspectionRequest, state.status]); + return (
) : ( Date: Tue, 21 Jul 2026 05:30:48 +0530 Subject: [PATCH 025/143] fix(judge): bind isolation copy to release health --- apps/web/src/features/judge/JudgeModeView.test.tsx | 12 ++++++++++++ apps/web/src/features/judge/JudgeModeView.tsx | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/web/src/features/judge/JudgeModeView.test.tsx b/apps/web/src/features/judge/JudgeModeView.test.tsx index bd9f9b30..6296fb4a 100644 --- a/apps/web/src/features/judge/JudgeModeView.test.tsx +++ b/apps/web/src/features/judge/JudgeModeView.test.tsx @@ -125,6 +125,12 @@ describe("JudgeModeView", () => { expect(screen.getByText("Runtime Codex")).toBeInTheDocument(); expect(screen.getByText("Fixed kernel")).toBeInTheDocument(); expect(screen.getByText("Frozen verifier")).toBeInTheDocument(); + expect(document.body).toHaveTextContent( + /filesystem generation read isolation is OS-enforced/i, + ); + expect(document.body).not.toHaveTextContent( + /filesystem generation read isolation is explicitly PARTIAL/i, + ); expect(document.body).toHaveTextContent( /evidence-first learning for notebook users/i, ); @@ -297,6 +303,12 @@ describe("JudgeModeView", () => { expect(document.body).toHaveTextContent( /release identity is unbound.*live qualification as unproven/i, ); + expect(document.body).toHaveTextContent( + /no exact released filesystem generation read-isolation status is available/i, + ); + expect(document.body).not.toHaveTextContent( + /filesystem generation read isolation is OS-enforced/i, + ); }); it("does not equate configured services with observed readiness", () => { diff --git a/apps/web/src/features/judge/JudgeModeView.tsx b/apps/web/src/features/judge/JudgeModeView.tsx index b0a598f2..48497be8 100644 --- a/apps/web/src/features/judge/JudgeModeView.tsx +++ b/apps/web/src/features/judge/JudgeModeView.tsx @@ -42,6 +42,14 @@ export function JudgeModeView({ }) { const liveReady = liveAuthorityReady(health); const release = health?.release?.status === "bound" ? health.release : null; + const generationIsolationBoundary = + release !== null && + health?.generationFilesystemReadIsolation === "OS_ENFORCED" + ? "Filesystem generation read isolation is OS-enforced for the exact released live runtime." + : release !== null && + health?.generationFilesystemReadIsolation === "PARTIAL" + ? "Filesystem generation read isolation is explicitly PARTIAL, so live authority remains unavailable." + : "No exact released filesystem generation read-isolation status is available, so live authority remains unproven."; const [sampleEvidenceInspectionRequest, setSampleEvidenceInspectionRequest] = useState(0); @@ -401,8 +409,8 @@ export function JudgeModeView({ packages, arbitrary code execution, unsupported patch shapes, and insufficient evidence. A Proof Capsule proves integrity and scoped verification—not global mastery or formal sandbox security. The - hosted Codex launch has a credential-and-privilege boundary; - filesystem generation read isolation is explicitly PARTIAL. + hosted Codex launch has a credential-and-privilege boundary.{" "} + {generationIsolationBoundary}

Date: Tue, 21 Jul 2026 05:32:38 +0530 Subject: [PATCH 026/143] docs(submission): make Devpost fields paste-ready --- docs/DEVPOST_COPY.md | 55 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/DEVPOST_COPY.md b/docs/DEVPOST_COPY.md index eeea2a71..7a23e9bc 100644 --- a/docs/DEVPOST_COPY.md +++ b/docs/DEVPOST_COPY.md @@ -12,12 +12,31 @@ CounterLab — A scientific debugger for beliefs Ask like chat. Prove it like science. +## Submission fields + +- **Category:** Education +- **Primary audience:** ML learners and instructors who need to test what a + notebook result actually supports +- **Live judge URL:** +- **License:** MIT +- **Repository URL:** **PENDING** anonymous-access verification; do not paste a + private or 404 URL +- **Public video URL:** **PENDING** capture from the final qualified release +- **Codex `/feedback` Session ID:** **PENDING** owner-supplied primary build + thread ID +- **Exact release receipt:** **PENDING** final source/Container/Worker + qualification +- **Ready-to-use notebook fixtures:** + `fixtures/notebooks/customer_churn_leakage.ipynb` and + `fixtures/notebooks/fraud_class_imbalance.ipynb` + ## Short description -CounterLab turns a learner's supported notebook claim into two competing -models, an immutable Prediction, the smallest verified discriminating Test, a -signed or integrity-hashed Boundary Map, deterministic transfer, and a minimal -repair unlocked only after transfer passes. +CounterLab helps ML learners and instructors test what a supported notebook +result actually means. It turns the learner's claim into two competing models, +an immutable Prediction, the smallest verified discriminating Test, a signed +or integrity-hashed Boundary Map, deterministic transfer, and a minimal repair +unlocked only after transfer passes. CounterLab is evidence-first learning for notebook users: seal a Prediction, change one condition, and let fixed evidence—not AI prose—release the bounded @@ -51,6 +70,34 @@ The Evidence Verdict is exactly `SUPPORTS`, `INCONCLUSIVE`, or `REJECTED`. CounterLab never labels the learner wrong and releases no result from a rejected test. +## What we built during Build Week + +The build-week implementation turned the earlier evidence engine into one +learner-first, source-bound release candidate: + +- a chat-first Question composer and one six-stage progress model; +- immutable Prediction, evidence story, Model Duel, Fair Test Builder, + Experiment Theater, Boundary Hunt, visual transfer, Repair preview, and + bounded completion; +- a ten-second Judge preview bound to checked-in fixed sample evidence and a + one-click integrity-verified Sample Proof Capsule; +- recoverable reject/insufficient-evidence paths, supported-artifact gating, + pre-Prediction result-leak regressions, stored proof-event hydration, and + capability-link lifecycle handling; +- source/image/release identity contracts, exact-image isolation sentinels, + negative release controls, and fail-closed public readiness; and +- keyboard, responsive, reduced-motion, interaction-evidence, replay, and + download coverage without adding learner names, raw free text, or notebook + content to analytics. + +Codex accelerated repository exploration, test construction, bounded repairs, +release-safety review, and repeated regression runs during the build. Those +engineering contributions are separate from **Runtime Codex inside the +product**, which may propose only schema-valid plans and repairs. Human +decisions fixed the product thesis, learner authority, supported Subject Packs, +scientific invariants, privacy boundary, release claims, and every final +integration decision. + ## What is technically distinct CounterLab separates authority instead of asking one model to propose, compute, From 55788ef1cd2ddfaaee0e7f83d6ce1fbddfb6058b Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 05:34:13 +0530 Subject: [PATCH 027/143] docs(demo): lead with the verified belief break --- docs/DEMO_CAPTIONS.vtt | 31 +++++++++++++++++++ docs/DEMO_SCRIPT.md | 69 +++++++++++++++++++++++++----------------- 2 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 docs/DEMO_CAPTIONS.vtt diff --git a/docs/DEMO_CAPTIONS.vtt b/docs/DEMO_CAPTIONS.vtt new file mode 100644 index 00000000..80315b52 --- /dev/null +++ b/docs/DEMO_CAPTIONS.vtt @@ -0,0 +1,31 @@ +WEBVTT + +00:00.000 --> 00:12.000 +CounterLab is for ML learners and instructors. This fixed sample changes one variable—who is new—and a 98.5% score becomes 59.4%. No model call produced these values. + +00:12.000 --> 00:27.000 +CounterLab begins with the Question. It treats notebook evidence as untrusted data, never runs a cell, and sends no raw rows, notebook file, or local path. + +00:27.000 --> 00:43.000 +GPT-5.6 proposes these competing explanations from only the approved sanitized packet. It cannot run the notebook or decide the result. The learner seals an immutable Prediction before any experimental value appears. + +00:43.000 --> 00:58.000 +Runtime Codex proposes only bounded operation IDs. The fixed scorer selects the test, and the frozen verifier decides whether the fixed kernel may release evidence. + +00:58.000 --> 01:18.000 +The numbers come from the fixed kernel. GPT-5.6 and Codex may explain or propose, but neither computes or verifies them. + +01:18.000 --> 01:34.000 +The learner explores already verified cells; the browser and the model calculate nothing new. + +01:34.000 --> 01:52.000 +No model grades the sentence. The fixed evaluator checks transfer on a different surface. + +01:52.000 --> 02:10.000 +Repair stays locked until deterministic transfer passes, and CounterLab patches only a copy. + +02:10.000 --> 02:30.000 +The Reasoning Diff is for the learner. The Proof Capsule binds the machine-readable evidence, versions, limitations, and event chain. + +02:30.000 --> 02:45.000 +Codex also accelerated the build through repository exploration, tests, and bounded repairs; humans fixed the scope and authority rules. Learner-impact status is NO_DATA, so we claim no measured learning gain. Chatbots explain. CounterLab lets reality answer. diff --git a/docs/DEMO_SCRIPT.md b/docs/DEMO_SCRIPT.md index 72b72a5a..909da168 100644 --- a/docs/DEMO_SCRIPT.md +++ b/docs/DEMO_SCRIPT.md @@ -1,57 +1,62 @@ -# 2:45 demo voiceover +# 2:45 demo voiceover and shot list Record **Live notebook analysis** only after the exact source has passed the Container smoke. A bundled sample is frozen, not a substitute live run. Keep **Verified replay** open as a disclosed backup and never present it as a new -model call. +model call. Use [DEMO_CAPTIONS.vtt](DEMO_CAPTIONS.vtt) word for word so the +spoken roles, on-screen labels, and captions agree. -## 0:00–0:15 — Question +## 0:00–0:12 — The belief break -Start on “What result are you trying to understand?” State that the notebook's -98.5% score is claimed to generalize to unseen customers, then attach the -untouched notebook. Say: “CounterLab begins with the question, not a mode or a -dashboard.” +Open Judge Mode on the disclosed fixed sample. Keep its Sample label visible +and show 98.5% familiar rows beside 59.4% unseen customers. Say: “CounterLab is +for ML learners and instructors. This fixed sample changes one variable—who is +new—and a 98.5% score becomes 59.4%. No model call produced these values.” -## 0:15–0:35 — Evidence and privacy +## 0:12–0:27 — Question, evidence, and privacy -Show the Notebook Evidence Story: the stored metric, exact cell/output -reference, and why it matters. Open the short privacy summary and approve the -exact sanitized packet. Say: “CounterLab reads this evidence as untrusted data; -it never runs a notebook cell, and the model receives no raw rows, file, or -local path.” +Move to an exact-qualified **Live notebook analysis**. Start on “What result are +you trying to understand?”, attach the untouched supported notebook, and show +the exact cell/output reference plus privacy summary. Say: “CounterLab begins +with the Question. It treats notebook evidence as untrusted data, never runs a +cell, and sends no raw rows, notebook file, or local path.” -## 0:35–0:55 — Model Duel and Prediction Seal +## 0:27–0:43 — GPT-5.6 and Prediction Show the learner's current explanation and the alternative with equal visual weight. Confirm that the first captures the learner's meaning. Choose “Remain -near 98%,” set confidence, and seal it. Say: “This is a commitment, not a quiz. -No experimental result is released before the Prediction is immutable.” +near 98%,” set confidence, and seal it. Say: “GPT-5.6 proposes these competing +explanations from only the approved sanitized packet. It cannot run the +notebook or decide the result. The learner seals an immutable Prediction before +any experimental value appears.” -## 0:55–1:15 — Fair Test Builder +## 0:43–0:58 — Runtime Codex and Fair Test Builder Show what changes, what stays fixed, and why whole-customer holdout matches deployment. If this exact live compile is genuinely rejected and repaired, show the learner-language explanation and optionally open Evidence & proof for the sanitized counterexample. If it passes first try, say so. Never stage a failure. -Say: “Codex proposes bounded artifacts; the fixed scorer selects, and the -verifier decides whether the fixed kernel may release evidence.” +Say: “Runtime Codex proposes only bounded operation IDs. The fixed scorer +selects the test, and the frozen verifier decides whether the fixed kernel may +release evidence.” -## 1:15–1:40 — Experiment Theater +## 0:58–1:18 — Experiment Theater After the verified payload arrives, show the pinned Prediction and one dominant comparison: 98.5% random rows versus 59.4% unseen customers, with controlled model and preprocessing. Use the exact table for 67.4% identity ablation, zero group overlap, sample sizes, seed, and result hash. Say: “The numbers come from -the fixed kernel and stay hidden until verification.” +the fixed kernel. GPT-5.6 and Codex may explain or propose, but neither computes +or verifies them.” -## 1:40–1:55 — Boundary +## 1:18–1:34 — Boundary Try one verified Boundary Hunt point, then reveal the full map. Show its non-color symbols, exact-value table, assumptions, and integrity or signature wording. Say: “The learner explores already verified cells; the browser and the model calculate nothing new.” -## 1:55–2:15 — Reflect and apply +## 1:34–1:52 — Reflect and Apply Build: “When rows repeat the same entity, I should hold out whole entities, because random rows can share identity across train and test.” Then use the @@ -59,20 +64,28 @@ timeline to select a time-ordered holdout and identify the feature that crosses NOW. Say: “No model grades the sentence. The fixed evaluator checks transfer on a different surface.” -## 2:15–2:32 — Repair +## 1:52–2:10 — Repair Show the repair preview: random rows become whole-customer holdout, identity is removed, and overlap is reported; target, model family, unrelated cells, and the original notebook remain preserved. Review the verified copied-notebook patch -and use the primary download. Emphasize that the upload was never overwritten. +and use the primary download. Say: “Repair stays locked until deterministic +transfer passes, and CounterLab patches only a copy.” -## 2:32–2:45 — Completion and proof +## 2:10–2:30 — Completion and proof Show: “You completed one verified entity-leakage loop: this fixed task separated performance on familiar rows from performance on new entities. It does not establish mastery.” Then open Evidence & proof for the Reasoning Diff and Proof -Capsule. Close: “Chatbots explain. CounterLab lets reality answer—and only then -helps you fix the code.” +Capsule. Say: “The Reasoning Diff is for the learner. The Proof Capsule binds +the machine-readable evidence, versions, limitations, and event chain.” + +## 2:30–2:45 — Build truth and close + +Show the exact release identity. Say: “Codex also accelerated the build through +repository exploration, tests, and bounded repairs; humans fixed the scope and +authority rules. Learner-impact status is NO_DATA, so we claim no measured +learning gain. Chatbots explain. CounterLab lets reality answer.” ## Backup disclosure From eafd8677a11ba0898ae9fbb9a1918d3515dbc206 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 05:35:45 +0530 Subject: [PATCH 028/143] docs(codex): separate build and runtime authority --- docs/CODEX_USAGE.md | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/CODEX_USAGE.md b/docs/CODEX_USAGE.md index cfce5b5e..eea02fb4 100644 --- a/docs/CODEX_USAGE.md +++ b/docs/CODEX_USAGE.md @@ -1,9 +1,19 @@ -# Runtime Codex usage +# Codex usage: build-time and runtime CounterLab uses Codex as a bounded Plan compiler. Codex proposes how to compose registered experiments or repairs; it never owns numeric truth, learner transfer scoring, or final verification. +## Build-time Codex + +Codex also accelerated the build as an engineering collaborator: repository +mapping, focused test construction, bounded implementation slices, regression +triage, release-safety review, and documentation reconciliation. Those +build-time contributions do not grant Runtime Codex any additional product +authority. Humans fixed the product thesis, learner and model authority, +supported Subject Packs, scientific invariants, privacy boundary, acceptance +criteria, and final integration decisions. + ## Product modes | Mode | Model calls | Meaning | @@ -37,9 +47,11 @@ Codex receives only the approved Belief Spec, sanitized Artifact Manifest, resolved evidence, selected Subject Pack capabilities/schema, resource limits, and permitted outputs in its request packet. That packet excludes notebook bytes, raw rows, secrets, hidden verifier source, mutation implementations, -held-out fixtures, R2/D1 credentials, and unrelated files. This input-minimizing -contract does not claim the hosted process is OS-unable to read every runtime -file; hosted filesystem generation read isolation is `PARTIAL`. +held-out fixtures, R2/D1 credentials, and unrelated files. The historical +recorded replay used an input-minimizing process boundary with `PARTIAL` +filesystem read isolation. Current source admits hosted Live only when the +exact runner proves `OS_ENFORCED` generation read isolation; source logic is not +production evidence until that exact image is qualified and deployed. ### Outputs @@ -92,11 +104,13 @@ model-authored Python, and it does not require nested Docker. Missing binding, credential, timeout, or process capability is a typed failure—not replay or sample success. -The hosted Codex launcher stages and revokes credentials, uses a fixed non-root -UID plus `setpriv --no-new-privs`, and constrains writes. It does not provide a -mount namespace or filesystem read allowlist, so hosted filesystem generation -read isolation is `PARTIAL`. -Source-free plans still have no numerical or verification authority. +The hosted Codex launcher stages and revokes credentials and requires an exact +image sentinel for a fixed non-root UID, `no-new-privs`, a pinned Bubblewrap +boundary, an allowlisted runtime mount set, a writable generation workspace, +and missing repository, held-out, verifier, and unrelated-job paths. Judge Mode +keeps Live unavailable unless that exact `OS_ENFORCED` result is bound to the +public release. Source-free plans still have no numerical or verification +authority. ## Advanced local adapter proof From 45038ce552ce80298af8d5840f6b2c16e27aa917 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 05:36:44 +0530 Subject: [PATCH 029/143] docs(readme): document samples and Codex decisions --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8cfc73d5..7241ef6f 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,14 @@ Required for local reproduction: Node 22+, pnpm 11.13.1, Python 3.12+, and the locked dependencies. A Docker-compatible engine is required for local Cloudflare Container development and for the advanced local adapter proof. +The two ready-to-use supported notebook fixtures are: + +- `fixtures/notebooks/customer_churn_leakage.ipynb` +- `fixtures/notebooks/fraud_class_imbalance.ipynb` + +Use them for the entity-leakage and class-imbalance live paths respectively; +do not treat them as evidence that arbitrary notebooks are supported. + ```bash cp .env.example .env.local ./scripts/clean-demo.sh @@ -322,11 +330,18 @@ See [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md). ## How Codex accelerated the build -Codex helped implement and test the monorepo, then CounterLab used Codex itself -as a bounded runtime compiler. The real rejected trace exposed two product -contract defects: incomplete public SDK documentation and transient bytecode in -an exact-file workspace. The public contract was strengthened; a separate later -run verified successfully. +Build-time Codex helped map the monorepo, turn audit findings into focused +regressions, implement bounded UI and release slices, triage cross-language +failures, review authority drift, and keep the evidence documents synchronized +with executed checks. Changes were integrated as small independently reviewed +commits; a source test, deployment receipt, model call, or learner result was +never treated as successful without direct evidence. + +That engineering role is separate from Runtime Codex inside CounterLab. Runtime +Codex acts only as a bounded Plan compiler. The real rejected trace exposed two +product-contract defects: incomplete public SDK documentation and transient +bytecode in an exact-file workspace. The public contract was strengthened; a +separate later run verified successfully. Human decisions remained the product thesis, learner authority, concept scope, fixture parameters, evidence contracts, verifier invariants, transfer design, From 94a5d51ad33c1672ae32485d9011a70179546129 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 05:39:42 +0530 Subject: [PATCH 030/143] fix(runtime): align timeout rlimit authority --- scripts/timeout-cleanup-receipt.test.ts | 12 +++++++++ scripts/timeout-cleanup-receipt.ts | 16 ++++++------ .../src/counterlab_runner/timeout_proof.py | 2 +- services/runner/tests/test_timeout_proof.py | 25 ++++++++++++++++++- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/scripts/timeout-cleanup-receipt.test.ts b/scripts/timeout-cleanup-receipt.test.ts index fd5f637e..e4b2e375 100644 --- a/scripts/timeout-cleanup-receipt.test.ts +++ b/scripts/timeout-cleanup-receipt.test.ts @@ -6,6 +6,7 @@ import { canonicalJson } from "../packages/session-core/src/index.js"; import { QUALIFIED_AGGREGATE_LIMIT_MODE, + TIMEOUT_ROOTLESS_RLIMIT_TYPES, assertQualifiedAggregateRuntimeLimits, } from "./timeout-cleanup-receipt.js"; @@ -98,6 +99,17 @@ function aggregateInput() { } describe("timeout cleanup aggregate resource authority", () => { + it("matches the five rlimits emitted by the contained runtime", () => { + expect(TIMEOUT_ROOTLESS_RLIMIT_TYPES).toEqual([ + "RLIMIT_AS", + "RLIMIT_CPU", + "RLIMIT_FSIZE", + "RLIMIT_NOFILE", + "RLIMIT_NPROC", + ]); + expect(TIMEOUT_ROOTLESS_RLIMIT_TYPES).not.toContain("RLIMIT_CORE"); + }); + it("rejects process-only or merely declared aggregate limits", () => { expect(() => assertQualifiedAggregateRuntimeLimits({ diff --git a/scripts/timeout-cleanup-receipt.ts b/scripts/timeout-cleanup-receipt.ts index c0af94d4..1fa18dfa 100644 --- a/scripts/timeout-cleanup-receipt.ts +++ b/scripts/timeout-cleanup-receipt.ts @@ -164,15 +164,17 @@ const AggregateLimitEvidenceSchema = z } }); +export const TIMEOUT_ROOTLESS_RLIMIT_TYPES = [ + "RLIMIT_AS", + "RLIMIT_CPU", + "RLIMIT_FSIZE", + "RLIMIT_NOFILE", + "RLIMIT_NPROC", +] as const; + const RlimitSchema = z .strictObject({ - type: z.enum([ - "RLIMIT_AS", - "RLIMIT_CORE", - "RLIMIT_CPU", - "RLIMIT_FSIZE", - "RLIMIT_NPROC", - ]), + type: z.enum(TIMEOUT_ROOTLESS_RLIMIT_TYPES), soft: PositiveSafeIntegerSchema, hard: PositiveSafeIntegerSchema, }) diff --git a/services/runner/src/counterlab_runner/timeout_proof.py b/services/runner/src/counterlab_runner/timeout_proof.py index e67d2edf..e23c099b 100644 --- a/services/runner/src/counterlab_runner/timeout_proof.py +++ b/services/runner/src/counterlab_runner/timeout_proof.py @@ -408,9 +408,9 @@ def validate_rootless_receipt( def _validate_enforced_rlimits(value: object) -> bool: required = { "RLIMIT_AS", - "RLIMIT_CORE", "RLIMIT_CPU", "RLIMIT_FSIZE", + "RLIMIT_NOFILE", "RLIMIT_NPROC", } if not isinstance(value, list) or len(value) != len(required): diff --git a/services/runner/tests/test_timeout_proof.py b/services/runner/tests/test_timeout_proof.py index 156a359f..5d30b655 100644 --- a/services/runner/tests/test_timeout_proof.py +++ b/services/runner/tests/test_timeout_proof.py @@ -171,9 +171,9 @@ def _rootless(control: dict[str, object], build: dict[str, object]) -> dict[str, {"type": name, "soft": 1, "hard": 1} for name in ( "RLIMIT_AS", - "RLIMIT_CORE", "RLIMIT_CPU", "RLIMIT_FSIZE", + "RLIMIT_NOFILE", "RLIMIT_NPROC", ) ], @@ -221,6 +221,29 @@ def test_control_receipt_rejects_result_release_and_hash_mutation() -> None: validate_control_receipt({**control, "receiptPayloadSha256": "f" * 64}) +def test_rootless_rlimit_validator_matches_the_runtime_contract() -> None: + runtime_limits = [ + {"type": name, "soft": 1, "hard": 1} + for name in ( + "RLIMIT_AS", + "RLIMIT_CPU", + "RLIMIT_FSIZE", + "RLIMIT_NOFILE", + "RLIMIT_NPROC", + ) + ] + + assert timeout_proof_module._validate_enforced_rlimits(runtime_limits) + assert not timeout_proof_module._validate_enforced_rlimits( + [ + {**entry, "type": "RLIMIT_CORE"} + if entry["type"] == "RLIMIT_NOFILE" + else entry + for entry in runtime_limits + ] + ) + + def test_rootless_receipt_binds_control_and_exact_adapter_authority() -> None: control = _control() build = { From b7c54fe11f4d365ea93e55841e23027b205a1ce4 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 05:49:23 +0530 Subject: [PATCH 031/143] fix(runtime): retain deterministic cgroup limits --- scripts/contained-rootless-spec.mjs | 12 +++++------- scripts/contained-runtime-run.mjs | 2 +- scripts/contained-runtime.test.ts | 14 +++++++------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/scripts/contained-rootless-spec.mjs b/scripts/contained-rootless-spec.mjs index 5db554d1..e6887295 100644 --- a/scripts/contained-rootless-spec.mjs +++ b/scripts/contained-rootless-spec.mjs @@ -1447,14 +1447,13 @@ export function sanitizeContainedRootlessSpec({ ); if (hasNerdctlHooks) delete parsed.hooks; delete parsed.annotations; - delete linux.cgroupsPath; - delete linux.resources; + linux.cgroupsPath = `${namespace}/${expected.invocationId}`; delete linux.sysctl; if ( - Object.hasOwn(linux, "cgroupsPath") || - Object.hasOwn(linux, "resources") + linux.cgroupsPath !== `${namespace}/${expected.invocationId}` || + !Object.hasOwn(linux, "resources") ) { - throw new Error("contained rootless OCI spec cgroup removal failed"); + throw new Error("contained rootless OCI spec cgroup normalization failed"); } const baseSpecSha256 = sha256(canonicalJson(parsed)); const runtimeAnnotations = { @@ -1527,13 +1526,12 @@ export function sanitizeContainedRootlessSpec({ removedFields: [ ...(hasNerdctlHooks ? ["hooks"] : []), "annotations", - "linux.cgroupsPath", - "linux.resources", "linux.sysctl", "linux.seccomp.restrictedTraceRule", ], normalizedFields: [ "annotations", + "linux.cgroupsPath", "process.terminal", "process.user.additionalGids", "process.env.HOSTNAME", diff --git a/scripts/contained-runtime-run.mjs b/scripts/contained-runtime-run.mjs index 19628956..f414fc01 100644 --- a/scripts/contained-runtime-run.mjs +++ b/scripts/contained-runtime-run.mjs @@ -521,7 +521,7 @@ export function containedRunPlan({ "--runc-root", resolve(sessionRoot, "run/runc"), "--cgroup", - "", + `${namespace}/${invocationId}`, "--platform", "linux/amd64", ], diff --git a/scripts/contained-runtime.test.ts b/scripts/contained-runtime.test.ts index b83dbd10..6a9d9ceb 100644 --- a/scripts/contained-runtime.test.ts +++ b/scripts/contained-runtime.test.ts @@ -1111,7 +1111,7 @@ describe("contained runtime command policy", () => { "--fifo-dir", clientFifoRoot, "--cgroup", - "", + `counterlab-v6.1/${invocationId}`, "--platform", "linux/amd64", ]), @@ -1181,7 +1181,7 @@ describe("contained runtime command policy", () => { expect(containedRuntimeResourceAbsent(missing())).toBe(true); }); - it("removes only unsupported cgroup fields from a bounded OCI spec", () => { + it("retains bounded cgroup intent without claiming observed enforcement", () => { const sessionRoot = resolve(root, ".rt/rt-validator-spec"); const installRoot = resolve( root, @@ -1213,9 +1213,10 @@ describe("contained runtime command policy", () => { source, }); const sanitized = JSON.parse(prepared.config); + const original = JSON.parse(source); - expect(sanitized.linux).not.toHaveProperty("cgroupsPath"); - expect(sanitized.linux).not.toHaveProperty("resources"); + expect(sanitized.linux.cgroupsPath).toBe(`counterlab-v6.1/${invocationId}`); + expect(sanitized.linux.resources).toEqual(original.linux.resources); expect(sanitized.process.rlimits).toEqual(plan.expected.rlimits); expect(sanitized.process.noNewPrivileges).toBe(true); expect(sanitized.process.terminal).toBe(false); @@ -1286,8 +1287,6 @@ describe("contained runtime command policy", () => { removedFields: [ "hooks", "annotations", - "linux.cgroupsPath", - "linux.resources", "linux.sysctl", "linux.seccomp.restrictedTraceRule", ], @@ -1295,6 +1294,7 @@ describe("contained runtime command policy", () => { finalContainerId: prepared.finalContainerId, removedMounts: ["/etc/hostname", "/etc/hosts", "/etc/resolv.conf"], }); + expect(prepared.receipt.normalizedFields).toContain("linux.cgroupsPath"); const missingHookSource = JSON.parse(source); missingHookSource.hooks = null; @@ -2279,7 +2279,7 @@ describe("contained runtime command policy", () => { expect(startCall).toEqual( expect.arrayContaining([ "--cgroup", - "", + `counterlab-v6.1/${invocationId}`, "--label", `io.counterlab.runtime.invocation=${invocationId}`, ]), From fa4261dbd0fb1ebc10655eadb6badbff4000ce4c Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 05:53:02 +0530 Subject: [PATCH 032/143] test(runtime): harden cgroup intent bindings --- scripts/contained-runtime.test.ts | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/scripts/contained-runtime.test.ts b/scripts/contained-runtime.test.ts index 6a9d9ceb..e9bb9308 100644 --- a/scripts/contained-runtime.test.ts +++ b/scripts/contained-runtime.test.ts @@ -1116,6 +1116,10 @@ describe("contained runtime command policy", () => { "linux/amd64", ]), ); + expect( + plan.start.args.filter((argument) => argument === "--cgroup"), + ).toHaveLength(1); + expect(plan.start.args).not.toContain(""); expect(plan.containerName).toBe("counterlab-startup-validator"); expect(plan.start.args).not.toContain("counterlab-startup-validator"); expect(plan.mountImageRootfs).toEqual({ @@ -1217,6 +1221,17 @@ describe("contained runtime command policy", () => { expect(sanitized.linux.cgroupsPath).toBe(`counterlab-v6.1/${invocationId}`); expect(sanitized.linux.resources).toEqual(original.linux.resources); + const canonicalBase = structuredClone(sanitized); + delete canonicalBase.annotations; + expect(prepared.receipt.baseSpecSha256).toBe( + createHash("sha256").update(canonicalJson(canonicalBase)).digest("hex"), + ); + expect(prepared.receipt.sanitizedSpecSha256).toBe( + createHash("sha256").update(canonicalJson(sanitized)).digest("hex"), + ); + expect(prepared.receipt.configFileSha256).toBe( + createHash("sha256").update(prepared.config).digest("hex"), + ); expect(sanitized.process.rlimits).toEqual(plan.expected.rlimits); expect(sanitized.process.noNewPrivileges).toBe(true); expect(sanitized.process.terminal).toBe(false); @@ -1296,6 +1311,42 @@ describe("contained runtime command policy", () => { }); expect(prepared.receipt.normalizedFields).toContain("linux.cgroupsPath"); + const resourceMutations: Array<(spec: typeof original) => void> = [ + (spec) => { + spec.linux.resources.memory.limit += 1; + }, + (spec) => { + spec.linux.resources.memory.swap = -1; + }, + (spec) => { + spec.linux.resources.pids.limit += 1; + }, + (spec) => { + spec.linux.resources.cpu.quota -= 1; + }, + (spec) => { + spec.linux.resources.devices[1].major = 2; + }, + (spec) => { + spec.linux.resources.io = {}; + }, + (spec) => { + spec.linux.cgroupsPath = "/escaped"; + }, + ]; + for (const mutate of resourceMutations) { + const changed = structuredClone(original); + mutate(changed); + expect(() => + sanitizeContainedRootlessSpec({ + containerId, + expected, + metadataSha256: "3".repeat(64), + source: JSON.stringify(changed), + }), + ).toThrow(/cgroup path|resource/u); + } + const missingHookSource = JSON.parse(source); missingHookSource.hooks = null; expect(() => From 22e489ff3d25457de37b95a7b5f8639a18e4e770 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 05:55:10 +0530 Subject: [PATCH 033/143] fix(runtime): request delegated cgroup namespace --- scripts/contained-runtime-supervisor.mjs | 3 +++ scripts/contained-runtime-supervisor.test.ts | 24 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/scripts/contained-runtime-supervisor.mjs b/scripts/contained-runtime-supervisor.mjs index c9e4e034..61c6f096 100644 --- a/scripts/contained-runtime-supervisor.mjs +++ b/scripts/contained-runtime-supervisor.mjs @@ -91,6 +91,9 @@ const containerdRootlesskit = spawn( [ `--state-dir=${resolve(runRoot, "containerd-rootless")}`, "--net=host", + "--pidns", + "--cgroupns", + "--evacuate-cgroup2=containerd", process.execPath, resolve(root, "scripts/contained-runtime-server.mjs"), "--session-id", diff --git a/scripts/contained-runtime-supervisor.test.ts b/scripts/contained-runtime-supervisor.test.ts index ef4bee79..abdc0e30 100644 --- a/scripts/contained-runtime-supervisor.test.ts +++ b/scripts/contained-runtime-supervisor.test.ts @@ -84,6 +84,30 @@ describe("contained runtime supervisor protocol", () => { ); }); + it("requests delegated PID and cgroup namespaces for containerd", () => { + const source = readFileSync( + resolve(process.cwd(), "scripts/contained-runtime-supervisor.mjs"), + "utf8", + ); + const launch = source.slice( + source.indexOf("const containerdRootlesskit = spawn("), + source.indexOf("closeSync(containerdLog)"), + ); + + expect(launch).toContain('"--pidns"'); + expect(launch).toContain('"--cgroupns"'); + expect(launch).toContain('"--evacuate-cgroup2=containerd"'); + expect(launch.indexOf('"--pidns"')).toBeLessThan( + launch.indexOf("scripts/contained-runtime-server.mjs"), + ); + expect(launch.indexOf('"--cgroupns"')).toBeLessThan( + launch.indexOf("scripts/contained-runtime-server.mjs"), + ); + expect(launch.indexOf('"--evacuate-cgroup2=containerd"')).toBeLessThan( + launch.indexOf("scripts/contained-runtime-server.mjs"), + ); + }); + it("accepts only exact hash-bound status, drain, and shutdown requests", () => { expect( parseSupervisorRequest( From d797b4c5245cb7b1d95710fc576932dff2b29c20 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:00:58 +0530 Subject: [PATCH 034/143] feat(runtime): bind aggregate qualification requests --- scripts/contained-runtime-attestation.mjs | 1 + scripts/contained-runtime-client.mjs | 19 ++++-- scripts/contained-runtime-request.d.mts | 27 ++++++++ scripts/contained-runtime-request.mjs | 82 +++++++++++++++++++++++ scripts/contained-runtime-request.test.ts | 74 ++++++++++++++++++++ scripts/contained-runtime-server.mjs | 28 ++------ 6 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 scripts/contained-runtime-request.d.mts create mode 100644 scripts/contained-runtime-request.mjs create mode 100644 scripts/contained-runtime-request.test.ts diff --git a/scripts/contained-runtime-attestation.mjs b/scripts/contained-runtime-attestation.mjs index 4a437924..8beb9da0 100644 --- a/scripts/contained-runtime-attestation.mjs +++ b/scripts/contained-runtime-attestation.mjs @@ -13,6 +13,7 @@ export const RUNTIME_POLICY_PATH = export const RUNTIME_HELPER_PATHS = Object.freeze({ runtimeClient: "scripts/contained-runtime-client.mjs", + runtimeRequest: "scripts/contained-runtime-request.mjs", runtimeRun: "scripts/contained-runtime-run.mjs", rootlessSpec: "scripts/contained-rootless-spec.mjs", imageAuthority: "scripts/contained-image-authority.mjs", diff --git a/scripts/contained-runtime-client.mjs b/scripts/contained-runtime-client.mjs index dcaa8ace..809ab305 100644 --- a/scripts/contained-runtime-client.mjs +++ b/scripts/contained-runtime-client.mjs @@ -13,6 +13,10 @@ import { isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { validateContainedRunControlReceipt } from "./contained-runtime-run.mjs"; +import { + AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + createContainedRuntimeRequest, +} from "./contained-runtime-request.mjs"; const root = realpathSync(resolve(fileURLToPath(import.meta.url), "../..")); const argv = process.argv.slice(2); @@ -91,11 +95,16 @@ if (command[0] === "login") { } } -const request = `${JSON.stringify({ - schemaVersion: "1", - args: command, - stdinBase64: stdin.toString("base64"), -})}\n`; +const request = `${JSON.stringify( + createContainedRuntimeRequest({ + args: command, + qualificationMode: + controlReceiptPath === undefined + ? null + : AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + stdin, + }), +)}\n`; const response = await new Promise((accept, reject) => { const chunks = []; diff --git a/scripts/contained-runtime-request.d.mts b/scripts/contained-runtime-request.d.mts new file mode 100644 index 00000000..2501285d --- /dev/null +++ b/scripts/contained-runtime-request.d.mts @@ -0,0 +1,27 @@ +export const AGGREGATE_TIMEOUT_QUALIFICATION_MODE: "aggregate-timeout-proof-v1"; + +export type ContainedRuntimeQualificationMode = + typeof AGGREGATE_TIMEOUT_QUALIFICATION_MODE | null; + +export interface ContainedRuntimeWireRequest { + schemaVersion: "2"; + args: string[]; + qualificationMode: ContainedRuntimeQualificationMode; + stdinBase64: string; +} + +export interface ContainedRuntimeRequest { + args: string[]; + qualificationMode: ContainedRuntimeQualificationMode; + stdin: Buffer; +} + +export function parseContainedRuntimeRequest( + value: unknown, +): ContainedRuntimeRequest; + +export function createContainedRuntimeRequest(input: { + args: string[]; + qualificationMode: ContainedRuntimeQualificationMode; + stdin: Buffer; +}): ContainedRuntimeWireRequest; diff --git a/scripts/contained-runtime-request.mjs b/scripts/contained-runtime-request.mjs new file mode 100644 index 00000000..27f1e26e --- /dev/null +++ b/scripts/contained-runtime-request.mjs @@ -0,0 +1,82 @@ +export const AGGREGATE_TIMEOUT_QUALIFICATION_MODE = + "aggregate-timeout-proof-v1"; + +const requestKeys = [ + "args", + "qualificationMode", + "schemaVersion", + "stdinBase64", +]; + +function validateArguments(value) { + if ( + !Array.isArray(value) || + value.length === 0 || + value.length > 128 || + value.some( + (argument) => + typeof argument !== "string" || + argument.length === 0 || + argument.length > 8_192, + ) + ) { + throw new Error("contained runtime request argument is invalid"); + } +} + +function validateQualificationMode(value, args) { + if (value !== null && value !== AGGREGATE_TIMEOUT_QUALIFICATION_MODE) { + throw new Error("contained runtime request qualification mode is invalid"); + } + if (value !== null && args[0] !== "run") { + throw new Error("contained runtime qualification requires a run command"); + } +} + +export function parseContainedRuntimeRequest(value) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + JSON.stringify(Object.keys(value).sort()) !== + JSON.stringify([...requestKeys].sort()) || + value.schemaVersion !== "2" + ) { + throw new Error("contained runtime request shape is invalid"); + } + validateArguments(value.args); + validateQualificationMode(value.qualificationMode, value.args); + if (typeof value.stdinBase64 !== "string") { + throw new Error("contained runtime request input is invalid"); + } + const stdin = Buffer.from(value.stdinBase64, "base64"); + if ( + stdin.byteLength > 8_192 || + stdin.toString("base64") !== value.stdinBase64 + ) { + throw new Error("contained runtime request input is invalid"); + } + return { + args: [...value.args], + qualificationMode: value.qualificationMode, + stdin, + }; +} + +export function createContainedRuntimeRequest({ + args, + qualificationMode, + stdin, +}) { + if (!Buffer.isBuffer(stdin)) { + throw new Error("contained runtime request input is invalid"); + } + const request = { + schemaVersion: "2", + args: Array.isArray(args) ? [...args] : args, + qualificationMode, + stdinBase64: stdin.toString("base64"), + }; + parseContainedRuntimeRequest(request); + return request; +} diff --git a/scripts/contained-runtime-request.test.ts b/scripts/contained-runtime-request.test.ts new file mode 100644 index 00000000..adacd0f3 --- /dev/null +++ b/scripts/contained-runtime-request.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; + +import { + AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + createContainedRuntimeRequest, + parseContainedRuntimeRequest, +} from "./contained-runtime-request.mjs"; + +describe("contained runtime request protocol", () => { + it("keeps ordinary commands outside qualification mode", () => { + const request = createContainedRuntimeRequest({ + args: ["version"], + qualificationMode: null, + stdin: Buffer.alloc(0), + }); + + expect(request).toEqual({ + schemaVersion: "2", + args: ["version"], + qualificationMode: null, + stdinBase64: "", + }); + expect(parseContainedRuntimeRequest(request)).toEqual({ + args: ["version"], + qualificationMode: null, + stdin: Buffer.alloc(0), + }); + }); + + it("allows aggregate qualification only for an explicit run", () => { + const request = createContainedRuntimeRequest({ + args: ["run", "bounded-image"], + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + stdin: Buffer.from("bounded input"), + }); + + expect(parseContainedRuntimeRequest(request)).toEqual({ + args: ["run", "bounded-image"], + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + stdin: Buffer.from("bounded input"), + }); + expect(() => + createContainedRuntimeRequest({ + args: ["version"], + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + stdin: Buffer.alloc(0), + }), + ).toThrow(/qualification/u); + }); + + it("rejects unknown, malformed, and non-canonical requests", () => { + const ordinary = createContainedRuntimeRequest({ + args: ["version"], + qualificationMode: null, + stdin: Buffer.alloc(0), + }); + + expect(() => + parseContainedRuntimeRequest({ ...ordinary, extra: true }), + ).toThrow(/shape/u); + expect(() => + parseContainedRuntimeRequest({ + ...ordinary, + qualificationMode: "unknown", + }), + ).toThrow(/qualification/u); + expect(() => + parseContainedRuntimeRequest({ ...ordinary, stdinBase64: "%%%" }), + ).toThrow(/input/u); + expect(() => + parseContainedRuntimeRequest({ ...ordinary, args: ["version", ""] }), + ).toThrow(/argument/u); + }); +}); diff --git a/scripts/contained-runtime-server.mjs b/scripts/contained-runtime-server.mjs index b46a1e42..8c50187d 100644 --- a/scripts/contained-runtime-server.mjs +++ b/scripts/contained-runtime-server.mjs @@ -24,6 +24,7 @@ import { probeContainedShimSocketDirectory, } from "./contained-containerd-config.mjs"; import { createContainedRuntimeEnvironment } from "./contained-runtime-environment.mjs"; +import { parseContainedRuntimeRequest } from "./contained-runtime-request.mjs"; import { verifyPersistedContainedRootlessSpec } from "./contained-rootless-spec.mjs"; const root = realpathSync(resolve(fileURLToPath(import.meta.url), "../..")); @@ -404,28 +405,10 @@ const server = createServer((socket) => { socket.on("end", () => { let response; try { - const request = JSON.parse(Buffer.concat(chunks).toString("utf8")); - if ( - request === null || - typeof request !== "object" || - request.schemaVersion !== "1" || - !Array.isArray(request.args) || - request.args.length === 0 || - request.args.length > 128 || - request.args.some( - (argument) => - typeof argument !== "string" || - argument.length === 0 || - argument.length > 8_192, - ) || - typeof request.stdinBase64 !== "string" - ) { - throw new Error("contained runtime request is invalid"); - } - const stdin = Buffer.from(request.stdinBase64, "base64"); - if (stdin.byteLength > 8_192) { - throw new Error("contained runtime input exceeded its bound"); - } + const request = parseContainedRuntimeRequest( + JSON.parse(Buffer.concat(chunks).toString("utf8")), + ); + const stdin = request.stdin; if (request.args[0] === "counterlab-drain") { const receipt = createDrainReceipt(request.args, stdin); response = { @@ -474,6 +457,7 @@ const server = createServer((socket) => { cwd: root, environment, installRoot, + qualificationMode: request.qualificationMode, sessionRoot, stdin, }) From 79d25babb638dc7726f2031f8a6f6b07b84f989e Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:04:40 +0530 Subject: [PATCH 035/143] feat(runtime): validate aggregate cgroup evidence --- scripts/contained-cgroup-evidence.d.mts | 26 +++ scripts/contained-cgroup-evidence.mjs | 272 ++++++++++++++++++++++ scripts/contained-cgroup-evidence.test.ts | 165 +++++++++++++ scripts/contained-runtime-attestation.mjs | 1 + 4 files changed, 464 insertions(+) create mode 100644 scripts/contained-cgroup-evidence.d.mts create mode 100644 scripts/contained-cgroup-evidence.mjs create mode 100644 scripts/contained-cgroup-evidence.test.ts diff --git a/scripts/contained-cgroup-evidence.d.mts b/scripts/contained-cgroup-evidence.d.mts new file mode 100644 index 00000000..f43f0877 --- /dev/null +++ b/scripts/contained-cgroup-evidence.d.mts @@ -0,0 +1,26 @@ +export const QUALIFIED_AGGREGATE_LIMIT_MODE: "container-cgroup-and-process-rlimit"; + +export function canonicalCgroupJson(value: unknown): string; +export function sha256CgroupBytes(value: Buffer | string): string; +export function createContainedCgroupIdentity(input: { + invocationId: string; + finalContainerId: string; + sanitizedSpecSha256: string; +}): string; +export function validateContainedCgroupEvidence( + value: T, + expected: { + invocationId: string; + finalContainerId: string; + sanitizedSpecSha256: string; + intendedAggregateLimits: { + cpuCount: number; + maxProcesses: number; + memoryBytes: number; + }; + runtimeAttestationSha256: string; + runtimeSessionId: string; + driverCliSha256: string; + driverModuleSha256: string; + }, +): T; diff --git a/scripts/contained-cgroup-evidence.mjs b/scripts/contained-cgroup-evidence.mjs new file mode 100644 index 00000000..6849e8d3 --- /dev/null +++ b/scripts/contained-cgroup-evidence.mjs @@ -0,0 +1,272 @@ +import { createHash } from "node:crypto"; + +export const QUALIFIED_AGGREGATE_LIMIT_MODE = + "container-cgroup-and-process-rlimit"; + +const sha256Pattern = /^[a-f0-9]{64}$/u; +const invocationPattern = /^[a-f0-9]{64}$/u; +const sessionPattern = /^rt-[a-z0-9][a-z0-9-]{7,13}$/u; +const isoTimestampPattern = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})$/u; + +const topLevelKeys = [ + "authority", + "cgroupId", + "cgroupIdentity", + "cgroupPath", + "cgroupVersion", + "cleanup", + "finalContainerId", + "invocationId", + "membership", + "negativeControls", + "observedAt", + "observedLimits", + "observer", + "receiptPayloadSha256", + "runtimeAttestationSha256", + "sanitizedSpecSha256", + "schemaVersion", + "status", +]; + +export function canonicalCgroupJson(value) { + if (Array.isArray(value)) { + return `[${value.map(canonicalCgroupJson).join(",")}]`; + } + if (value !== null && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalCgroupJson(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +export function sha256CgroupBytes(value) { + return createHash("sha256").update(value).digest("hex"); +} + +function object(value, label) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`aggregate cgroup evidence ${label} shape is invalid`); + } + return value; +} + +function exactKeys(value, keys, label) { + if ( + JSON.stringify(Object.keys(value).sort()) !== + JSON.stringify([...keys].sort()) + ) { + throw new Error(`aggregate cgroup evidence ${label} shape is invalid`); + } +} + +function safeInteger(value, { positive = false } = {}) { + return Number.isSafeInteger(value) && value >= (positive ? 1 : 0); +} + +function sha256(value) { + return typeof value === "string" && sha256Pattern.test(value); +} + +function validateExpected(value) { + const expected = object(value, "expected binding"); + exactKeys( + expected, + [ + "driverCliSha256", + "driverModuleSha256", + "finalContainerId", + "intendedAggregateLimits", + "invocationId", + "runtimeAttestationSha256", + "runtimeSessionId", + "sanitizedSpecSha256", + ], + "expected binding", + ); + const intended = object(expected.intendedAggregateLimits, "intended limits"); + exactKeys( + intended, + ["cpuCount", "maxProcesses", "memoryBytes"], + "intended limits", + ); + if ( + !invocationPattern.test(expected.invocationId ?? "") || + !invocationPattern.test(expected.finalContainerId ?? "") || + !sha256(expected.sanitizedSpecSha256) || + !sha256(expected.runtimeAttestationSha256) || + !sessionPattern.test(expected.runtimeSessionId ?? "") || + !sha256(expected.driverCliSha256) || + !sha256(expected.driverModuleSha256) || + typeof intended.cpuCount !== "number" || + !Number.isFinite(intended.cpuCount) || + intended.cpuCount < 0.25 || + intended.cpuCount > 2 || + !safeInteger(intended.maxProcesses, { positive: true }) || + intended.maxProcesses > 32 || + !safeInteger(intended.memoryBytes, { positive: true }) || + intended.memoryBytes < 64 * 1024 * 1024 || + intended.memoryBytes > 1024 * 1024 * 1024 + ) { + throw new Error("aggregate cgroup evidence expected binding is invalid"); + } + return { expected, intended }; +} + +export function createContainedCgroupIdentity({ + invocationId, + finalContainerId, + sanitizedSpecSha256, +}) { + if ( + !invocationPattern.test(invocationId ?? "") || + !invocationPattern.test(finalContainerId ?? "") || + !sha256(sanitizedSpecSha256) + ) { + throw new Error("aggregate cgroup evidence identity input is invalid"); + } + return sha256CgroupBytes( + `counterlab-cgroup-v2\0${invocationId}\0${finalContainerId}\0${sanitizedSpecSha256}`, + ); +} + +export function validateContainedCgroupEvidence(value, expectedValue) { + const evidence = object(value, "top-level"); + exactKeys(evidence, topLevelKeys, "top-level"); + const { expected, intended } = validateExpected(expectedValue); + const observed = object(evidence.observedLimits, "observed limits"); + const membership = object(evidence.membership, "membership"); + const controls = object(evidence.negativeControls, "negative controls"); + const memoryControl = object(controls.memory, "memory control"); + const processControl = object(controls.processes, "process control"); + const cpuControl = object(controls.cpu, "CPU control"); + const cleanup = object(evidence.cleanup, "cleanup"); + const observer = object(evidence.observer, "observer"); + exactKeys( + observed, + [ + "cpuPeriodMicros", + "cpuQuotaMicros", + "memoryMaxBytes", + "memorySwapMaxBytes", + "pidsMax", + ], + "observed limits", + ); + exactKeys( + membership, + [ + "descendantsObserved", + "leaderPid", + "leaderStartTimeTicks", + "memberPids", + "memberSetSha256", + ], + "membership", + ); + exactKeys(controls, ["cpu", "memory", "processes"], "negative controls"); + exactKeys( + memoryControl, + ["enforced", "oomKillAfter", "oomKillBefore", "requestedBytes"], + "memory control", + ); + exactKeys( + processControl, + ["attemptedProcesses", "enforced", "maxEventsAfter", "maxEventsBefore"], + "process control", + ); + exactKeys( + cpuControl, + [ + "busyWindowMs", + "enforced", + "nrThrottledAfter", + "nrThrottledBefore", + "throttledUsecAfter", + "throttledUsecBefore", + ], + "CPU control", + ); + exactKeys(cleanup, ["cgroupAbsentAfterTimeout"], "cleanup"); + exactKeys( + observer, + ["driverCliSha256", "driverModuleSha256", "runtimeSessionId"], + "observer", + ); + + const memberPids = membership.memberPids; + const uniqueMembers = Array.isArray(memberPids) + ? new Set(memberPids) + : new Set(); + const { receiptPayloadSha256, ...payload } = evidence; + if ( + evidence.schemaVersion !== "1" || + evidence.status !== "OBSERVED" || + evidence.authority !== "linux-cgroup-v2" || + evidence.cgroupVersion !== 2 || + evidence.invocationId !== expected.invocationId || + evidence.finalContainerId !== expected.finalContainerId || + evidence.sanitizedSpecSha256 !== expected.sanitizedSpecSha256 || + evidence.runtimeAttestationSha256 !== expected.runtimeAttestationSha256 || + evidence.cgroupId !== `counterlab-v6.1-${expected.invocationId}` || + evidence.cgroupPath !== `counterlab-v6.1/${expected.invocationId}` || + evidence.cgroupIdentity !== createContainedCgroupIdentity(expected) || + observed.memoryMaxBytes !== intended.memoryBytes || + ![0, intended.memoryBytes].includes(observed.memorySwapMaxBytes) || + observed.pidsMax !== intended.maxProcesses || + !safeInteger(observed.cpuQuotaMicros, { positive: true }) || + !safeInteger(observed.cpuPeriodMicros, { positive: true }) || + observed.cpuQuotaMicros / observed.cpuPeriodMicros !== intended.cpuCount || + !safeInteger(membership.leaderPid, { positive: true }) || + typeof membership.leaderStartTimeTicks !== "string" || + !/^[1-9][0-9]*$/u.test(membership.leaderStartTimeTicks) || + !Array.isArray(memberPids) || + memberPids.length < 2 || + memberPids.length > 64 || + memberPids.some((pid) => !safeInteger(pid, { positive: true })) || + uniqueMembers.size !== memberPids.length || + !uniqueMembers.has(membership.leaderPid) || + membership.memberSetSha256 !== + sha256CgroupBytes( + canonicalCgroupJson( + [...memberPids].sort((left, right) => left - right), + ), + ) || + membership.descendantsObserved !== true || + !safeInteger(memoryControl.requestedBytes, { positive: true }) || + memoryControl.requestedBytes <= intended.memoryBytes || + !safeInteger(memoryControl.oomKillBefore) || + !safeInteger(memoryControl.oomKillAfter, { positive: true }) || + memoryControl.oomKillAfter <= memoryControl.oomKillBefore || + memoryControl.enforced !== true || + !safeInteger(processControl.attemptedProcesses, { positive: true }) || + processControl.attemptedProcesses <= intended.maxProcesses || + !safeInteger(processControl.maxEventsBefore) || + !safeInteger(processControl.maxEventsAfter, { positive: true }) || + processControl.maxEventsAfter <= processControl.maxEventsBefore || + processControl.enforced !== true || + !safeInteger(cpuControl.busyWindowMs, { positive: true }) || + !safeInteger(cpuControl.nrThrottledBefore) || + !safeInteger(cpuControl.nrThrottledAfter, { positive: true }) || + cpuControl.nrThrottledAfter <= cpuControl.nrThrottledBefore || + !safeInteger(cpuControl.throttledUsecBefore) || + !safeInteger(cpuControl.throttledUsecAfter, { positive: true }) || + cpuControl.throttledUsecAfter <= cpuControl.throttledUsecBefore || + cpuControl.enforced !== true || + cleanup.cgroupAbsentAfterTimeout !== true || + observer.runtimeSessionId !== expected.runtimeSessionId || + observer.driverCliSha256 !== expected.driverCliSha256 || + observer.driverModuleSha256 !== expected.driverModuleSha256 || + typeof evidence.observedAt !== "string" || + !isoTimestampPattern.test(evidence.observedAt) || + !Number.isFinite(Date.parse(evidence.observedAt)) || + !sha256(receiptPayloadSha256) || + sha256CgroupBytes(canonicalCgroupJson(payload)) !== receiptPayloadSha256 + ) { + throw new Error("aggregate cgroup evidence binding is invalid"); + } + return evidence; +} diff --git a/scripts/contained-cgroup-evidence.test.ts b/scripts/contained-cgroup-evidence.test.ts new file mode 100644 index 00000000..cfc12ee4 --- /dev/null +++ b/scripts/contained-cgroup-evidence.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, it } from "vitest"; + +import { + QUALIFIED_AGGREGATE_LIMIT_MODE, + canonicalCgroupJson, + createContainedCgroupIdentity, + sha256CgroupBytes, + validateContainedCgroupEvidence, +} from "./contained-cgroup-evidence.mjs"; + +const invocationId = "1".repeat(64); +const finalContainerId = "2".repeat(64); +const sanitizedSpecSha256 = "3".repeat(64); +const intendedAggregateLimits = { + cpuCount: 1, + maxProcesses: 16, + memoryBytes: 512 * 1024 * 1024, +}; + +function evidence() { + const memberPids = [100, 101]; + const payload = { + schemaVersion: "1", + status: "OBSERVED", + authority: "linux-cgroup-v2", + cgroupVersion: 2, + cgroupId: `counterlab-v6.1-${invocationId}`, + cgroupPath: `counterlab-v6.1/${invocationId}`, + cgroupIdentity: createContainedCgroupIdentity({ + invocationId, + finalContainerId, + sanitizedSpecSha256, + }), + invocationId, + finalContainerId, + sanitizedSpecSha256, + runtimeAttestationSha256: "4".repeat(64), + observedLimits: { + memoryMaxBytes: intendedAggregateLimits.memoryBytes, + memorySwapMaxBytes: intendedAggregateLimits.memoryBytes, + pidsMax: intendedAggregateLimits.maxProcesses, + cpuQuotaMicros: 100_000, + cpuPeriodMicros: 100_000, + }, + membership: { + leaderPid: 100, + leaderStartTimeTicks: "12345", + memberPids, + memberSetSha256: sha256CgroupBytes(canonicalCgroupJson(memberPids)), + descendantsObserved: true, + }, + negativeControls: { + memory: { + requestedBytes: intendedAggregateLimits.memoryBytes + 64 * 1024 * 1024, + oomKillBefore: 0, + oomKillAfter: 1, + enforced: true, + }, + processes: { + attemptedProcesses: intendedAggregateLimits.maxProcesses + 1, + maxEventsBefore: 0, + maxEventsAfter: 1, + enforced: true, + }, + cpu: { + busyWindowMs: 500, + nrThrottledBefore: 0, + nrThrottledAfter: 1, + throttledUsecBefore: 0, + throttledUsecAfter: 1, + enforced: true, + }, + }, + cleanup: { cgroupAbsentAfterTimeout: true }, + observer: { + runtimeSessionId: "rt-v61-test1", + driverCliSha256: "5".repeat(64), + driverModuleSha256: "6".repeat(64), + }, + observedAt: "2026-07-20T12:00:00.000Z", + }; + return { + ...payload, + receiptPayloadSha256: sha256CgroupBytes(canonicalCgroupJson(payload)), + }; +} + +function expected() { + return { + invocationId, + finalContainerId, + sanitizedSpecSha256, + intendedAggregateLimits, + runtimeAttestationSha256: "4".repeat(64), + runtimeSessionId: "rt-v61-test1", + driverCliSha256: "5".repeat(64), + driverModuleSha256: "6".repeat(64), + }; +} + +describe("contained cgroup evidence", () => { + it("accepts only the qualified aggregate limit mode and complete evidence", () => { + expect(QUALIFIED_AGGREGATE_LIMIT_MODE).toBe( + "container-cgroup-and-process-rlimit", + ); + expect(validateContainedCgroupEvidence(evidence(), expected())).toEqual( + evidence(), + ); + }); + + it("rejects identity, limit, membership, counter, cleanup, and hash drift", () => { + const mutations: Array<(value: ReturnType) => void> = [ + (value) => { + value.cgroupPath = "counterlab-v6.1/escaped"; + }, + (value) => { + value.observedLimits.memoryMaxBytes += 1; + }, + (value) => { + value.membership.memberPids = [100, 100]; + }, + (value) => { + value.negativeControls.memory.oomKillAfter = 0; + }, + (value) => { + value.negativeControls.processes.maxEventsAfter = 0; + }, + (value) => { + value.negativeControls.cpu.throttledUsecAfter = 0; + }, + (value) => { + value.cleanup.cgroupAbsentAfterTimeout = false; + }, + (value) => { + value.observer.driverCliSha256 = "7".repeat(64); + }, + (value) => { + value.receiptPayloadSha256 = "8".repeat(64); + }, + ]; + + for (const mutate of mutations) { + const changed = structuredClone(evidence()); + mutate(changed); + expect(() => + validateContainedCgroupEvidence(changed, expected()), + ).toThrow(/aggregate cgroup evidence/u); + } + }); + + it("rejects unknown fields and stale bindings", () => { + expect(() => + validateContainedCgroupEvidence( + { ...evidence(), unknown: true }, + expected(), + ), + ).toThrow(/shape/u); + expect(() => + validateContainedCgroupEvidence(evidence(), { + ...expected(), + invocationId: "9".repeat(64), + }), + ).toThrow(/binding/u); + }); +}); diff --git a/scripts/contained-runtime-attestation.mjs b/scripts/contained-runtime-attestation.mjs index 8beb9da0..6f64e212 100644 --- a/scripts/contained-runtime-attestation.mjs +++ b/scripts/contained-runtime-attestation.mjs @@ -12,6 +12,7 @@ export const RUNTIME_POLICY_PATH = "services/runner/src/counterlab_runner/contained-runtime-policy.json"; export const RUNTIME_HELPER_PATHS = Object.freeze({ + cgroupEvidence: "scripts/contained-cgroup-evidence.mjs", runtimeClient: "scripts/contained-runtime-client.mjs", runtimeRequest: "scripts/contained-runtime-request.mjs", runtimeRun: "scripts/contained-runtime-run.mjs", From 275dd9ee6ba57c0204a16e42ed2d7977ece21451 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:06:16 +0530 Subject: [PATCH 036/143] docs(progress): record cgroup source checkpoint --- docs/PROGRESS.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 554258ea..13ce8b1e 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -3281,3 +3281,103 @@ not staged or modified. verify Devpost, public video, repository, feedback ID, and submission receipt before pushing the feature branch and fast-forwarding its verified tip into `main`. + +## Finish Commander cgroup-source checkpoint — 2026-07-21T00:34:56Z + +Deadline: `2026-07-22T00:00:00Z`; **23 hours 25 minutes 04 seconds remained at +this checkpoint**. The feature branch is at committed HEAD `79d25ba`; the only +worktree entry besides this progress update is the preserved, already-unlinked +FUSE tombstone `scripts/.fuse_hidden0000a9600000801f`. It was inspected +read-only during ownership diagnosis, remains byte-identical to +`scripts/start-contained-runtime.sh`, and remains unchanged and unstaged. + +### Independently committed work since the consolidated checkpoint + +- `1f3067c perf(learner): defer verified theater payload` replaces the eager + landing import with a locked lightweight preview and a genuine lazy verified + Theater. The main client fell from **552.03 kB to 446.77 kB** minified; the + lazy Theater chunk is **20.78 kB**, and the mixed-import build warning is + gone. +- `6b0bf01 fix(judge): bind isolation copy to release health` makes Judge copy + distinguish release-bound `OS_ENFORCED`, release-bound `PARTIAL`, and + unavailable evidence instead of presenting a fixed isolation claim. +- `0621b79`, `55788ef`, `eafd867`, and `45038ce` make the Devpost fields + paste-ready, move the verified belief break into the demo's opening seconds, + add word-for-word captions, separate build-time Codex from Runtime Codex, + and document both sample notebooks without inventing repository, video, + feedback, release, learner, or submission evidence. +- `94a5d51 fix(runtime): align timeout rlimit authority` aligns TypeScript and + Python on the five limits actually emitted: `RLIMIT_AS`, `RLIMIT_CPU`, + `RLIMIT_FSIZE`, `RLIMIT_NOFILE`, and `RLIMIT_NPROC`. +- `b7c54fe fix(runtime): retain deterministic cgroup limits` preserves the + validated OCI CPU, memory, swap, and PID intent, normalizes the cgroup to + `counterlab-v6.1/`, and stops passing `--cgroup ""`. The + receipt deliberately remains `aggregateLimitIntentEnforced: false` with + `aggregateLimitEvidence: null`. +- `fa4261d test(runtime): harden cgroup intent bindings` adds seven fail-closed + resource mutations, exact non-empty cgroup argument assertions, and explicit + config/base/sanitized hash bindings. +- `22e489f fix(runtime): request delegated cgroup namespace` launches the + pinned RootlessKit with `--pidns`, `--cgroupns`, and + `--evacuate-cgroup2=containerd` before the runtime server. +- `d797b4c feat(runtime): bind aggregate qualification requests` introduces a + strict source-bound request protocol. Only an explicit, already path-checked + timeout control-receipt request may select `aggregate-timeout-proof-v1`; + normal learner calls carry `null`, unknown fields fail, and non-run commands + cannot select qualification. +- `79d25ba feat(runtime): validate aggregate cgroup evidence` adds a pure + runtime validator for exact limits, member identity, memory/PID/CPU counter + deltas, cleanup absence, observer/source identity, timestamp, and self-hash. + It does not observe the host or issue a qualified receipt. + +### Exact verification in these slices + +- Complete web Vitest after the learner performance change: **78 files, + 617/617 passed**. +- Cgroup/runtime adjacent suites: **52/52**, then **39/39**, **46/46**, + **50/50**, and the validator cross-boundary suite **12/12**, all passed at + their respective committed slices. +- Runtime/Python rlimit parity: TypeScript **4/4** and Python **7/7**, passed. +- Repository, web client, generated Worker, and Worker TypeScript: **passed** + after each current runtime slice. +- Production Vite/Worker build: **passed** at the learner-performance slice; + main client **446.77 kB / 128.55 kB gzip**, CSS **200.01 kB / 34.61 kB + gzip**, Worker **1,863.39 kB / 359.59 kB gzip**. +- Scoped Prettier, Git whitespace, and explicit secret scans: **passed** for + every committed slice. + +### Live issue matrix + +| Gate | Source mechanism | Acceptance evidence | Current production proof | +| --- | --- | --- | --- | +| Aggregate containment | Retained OCI limits, delegated RootlessKit request, strict qualification protocol and evidence validator | Real exact-image memory OOM, PID denial, CPU throttling, genuine membership, and post-timeout cgroup absence | **not run**; observer/final receipt and real host proof remain open | +| Clean exact source | Nested ignored worktree under `.rt/` avoids the unlinked FUSE tombstone without deleting it | Completely clean physical checkout at frozen source `S` | **not run** | +| Exact image/evidence | Build receipt, timeout proof, SBOM/VEX/scientific refresh, evidence commit `E` | All source/image/hash-bound receipts verify | **not run** | +| Cloudflare release | Repository-contained Wrangler identity, qualified deploy, public identity, smoke | Exact account, Worker version, Container digest, source `S`, evidence `E` | **not run**; contained Wrangler is not authenticated | +| Browser qualification | CloakBrowser Playwright harness | 32 current tests plus required manual semantic/visual matrix on the exact public tuple | **0 current rendered journeys**; this session has no injected `playwright_safe` tools or `CLOAK_CDP_ENDPOINT` | +| Submission/impact | Paste-ready copy, first-12-second demo script, captions, `NO_DATA` guard | Public repo/video/feedback links, Devpost submission receipt, real consented learner rows or honest `NO_DATA` | **externally unverified** | + +### Ordered remaining work + +1. Add the independent cgroup observer, release-only negative controls, and a + create-once post-cleanup qualified receipt without changing normal learner + execution. +2. Obtain a constitution-compatible way to execute the real cgroup proof. The + runtime necessarily reads its delegated `/sys/fs/cgroup` subtree and owned + `/proc//stat`; the current repository-only filesystem rule does not + authorize the lead to execute that proof, so this gate remains fail-closed. +3. Freeze source `S` in a clean repository-contained nested worktree, build the + runner once, run the timeout and scientific evidence proofs while + `HEAD == S`, commit only allowlisted evidence as `E`, then qualify. +4. Complete repository-contained Wrangler authentication, deploy only the + qualified tuple, and run public identity/smoke without overstating the + currently deployed Worker. +5. Start a fresh Codex session with injected `playwright_safe`; registration + alone is insufficient. Run all **32** current tests and the missing + responsive, semantic, screen-reader, network, console, and Web Vitals + review. Historical stock-Chromium and outside-root runtime metadata do not + qualify this release. +6. Keep learner impact `NO_DATA` without real consented participants; verify + and align the public repository, video, `/feedback` ID, Devpost fields, and + submission receipt before the authorized feature push and fast-forward + merge to `main`. From cb8705a166ecdcea7c5944cf95bb99df94495077 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:09:46 +0530 Subject: [PATCH 037/143] feat(runtime): derive qualified rootless receipts --- ...contained-qualified-rootless-receipt.d.mts | 26 +++ .../contained-qualified-rootless-receipt.mjs | 102 ++++++++++ ...ntained-qualified-rootless-receipt.test.ts | 186 ++++++++++++++++++ scripts/contained-runtime-attestation.mjs | 1 + 4 files changed, 315 insertions(+) create mode 100644 scripts/contained-qualified-rootless-receipt.d.mts create mode 100644 scripts/contained-qualified-rootless-receipt.mjs create mode 100644 scripts/contained-qualified-rootless-receipt.test.ts diff --git a/scripts/contained-qualified-rootless-receipt.d.mts b/scripts/contained-qualified-rootless-receipt.d.mts new file mode 100644 index 00000000..52f73ce4 --- /dev/null +++ b/scripts/contained-qualified-rootless-receipt.d.mts @@ -0,0 +1,26 @@ +export const UNQUALIFIED_AGGREGATE_LIMIT_MODE: "process-address-space-rlimit-with-unenforced-cgroup-intent"; + +export function createQualifiedContainedRootlessReceipt< + TReceipt extends Record, + TEvidence, +>(input: { + aggregateLimitEvidence: TEvidence; + baseReceipt: TReceipt; + observerBindings: { + runtimeAttestationSha256: string; + runtimeSessionId: string; + driverCliSha256: string; + driverModuleSha256: string; + }; +}): Omit< + TReceipt, + | "limitMode" + | "aggregateLimitIntentEnforced" + | "aggregateLimitEvidence" + | "receiptPayloadSha256" +> & { + limitMode: "container-cgroup-and-process-rlimit"; + aggregateLimitIntentEnforced: true; + aggregateLimitEvidence: TEvidence; + receiptPayloadSha256: string; +}; diff --git a/scripts/contained-qualified-rootless-receipt.mjs b/scripts/contained-qualified-rootless-receipt.mjs new file mode 100644 index 00000000..865f2cf4 --- /dev/null +++ b/scripts/contained-qualified-rootless-receipt.mjs @@ -0,0 +1,102 @@ +import { + QUALIFIED_AGGREGATE_LIMIT_MODE, + canonicalCgroupJson, + sha256CgroupBytes, + validateContainedCgroupEvidence, +} from "./contained-cgroup-evidence.mjs"; + +export const UNQUALIFIED_AGGREGATE_LIMIT_MODE = + "process-address-space-rlimit-with-unenforced-cgroup-intent"; + +const rootlessReceiptKeys = [ + "aggregateLimitEvidence", + "aggregateLimitIntentEnforced", + "baseSpecSha256", + "commandSha256", + "configFileSha256", + "enforcedRlimits", + "finalContainerId", + "imageAuthority", + "imageRootfs", + "intendedAggregateLimits", + "internalMountManifest", + "internalMountManifestSha256", + "invocationId", + "limitMode", + "metadataSha256", + "normalizedFields", + "originalSpecSha256", + "readOnlyMountManifest", + "readOnlyMountManifestSha256", + "receiptPayloadSha256", + "removedFields", + "removedMounts", + "sanitizedSpecSha256", + "schemaVersion", + "stagingContainerId", + "status", +]; + +const observerBindingKeys = [ + "driverCliSha256", + "driverModuleSha256", + "runtimeAttestationSha256", + "runtimeSessionId", +]; + +function object(value, label) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`qualified rootless receipt ${label} shape is invalid`); + } + return value; +} + +function exactKeys(value, keys, label) { + if ( + JSON.stringify(Object.keys(value).sort()) !== + JSON.stringify([...keys].sort()) + ) { + throw new Error(`qualified rootless receipt ${label} shape is invalid`); + } +} + +export function createQualifiedContainedRootlessReceipt({ + aggregateLimitEvidence, + baseReceipt: baseValue, + observerBindings: observerValue, +}) { + const baseReceipt = object(baseValue, "base"); + const observerBindings = object(observerValue, "observer binding"); + exactKeys(baseReceipt, rootlessReceiptKeys, "base"); + exactKeys(observerBindings, observerBindingKeys, "observer binding"); + const { receiptPayloadSha256, ...basePayload } = baseReceipt; + if ( + baseReceipt.schemaVersion !== "4" || + baseReceipt.status !== "VALIDATED" || + baseReceipt.limitMode !== UNQUALIFIED_AGGREGATE_LIMIT_MODE || + baseReceipt.aggregateLimitIntentEnforced !== false || + baseReceipt.aggregateLimitEvidence !== null || + typeof receiptPayloadSha256 !== "string" || + !/^[a-f0-9]{64}$/u.test(receiptPayloadSha256) || + sha256CgroupBytes(canonicalCgroupJson(basePayload)) !== receiptPayloadSha256 + ) { + throw new Error("qualified rootless receipt base binding is invalid"); + } + const evidence = validateContainedCgroupEvidence(aggregateLimitEvidence, { + invocationId: baseReceipt.invocationId, + finalContainerId: baseReceipt.finalContainerId, + sanitizedSpecSha256: baseReceipt.sanitizedSpecSha256, + intendedAggregateLimits: baseReceipt.intendedAggregateLimits, + ...observerBindings, + }); + const payload = { + ...basePayload, + limitMode: QUALIFIED_AGGREGATE_LIMIT_MODE, + aggregateLimitIntentEnforced: true, + aggregateLimitEvidence: evidence, + }; + return { + ...payload, + receiptPayloadSha256: sha256CgroupBytes(canonicalCgroupJson(payload)), + }; +} diff --git a/scripts/contained-qualified-rootless-receipt.test.ts b/scripts/contained-qualified-rootless-receipt.test.ts new file mode 100644 index 00000000..e4526b19 --- /dev/null +++ b/scripts/contained-qualified-rootless-receipt.test.ts @@ -0,0 +1,186 @@ +import { describe, expect, it } from "vitest"; + +import { + QUALIFIED_AGGREGATE_LIMIT_MODE, + canonicalCgroupJson, + createContainedCgroupIdentity, + sha256CgroupBytes, +} from "./contained-cgroup-evidence.mjs"; +import { + UNQUALIFIED_AGGREGATE_LIMIT_MODE, + createQualifiedContainedRootlessReceipt, +} from "./contained-qualified-rootless-receipt.mjs"; + +const invocationId = "1".repeat(64); +const finalContainerId = "2".repeat(64); +const sanitizedSpecSha256 = "3".repeat(64); +const intendedAggregateLimits = { + cpuCount: 1, + maxProcesses: 16, + memoryBytes: 512 * 1024 * 1024, +}; +const observerBindings = { + runtimeAttestationSha256: "4".repeat(64), + runtimeSessionId: "rt-v61-test1", + driverCliSha256: "5".repeat(64), + driverModuleSha256: "6".repeat(64), +}; + +function hashed>(payload: T) { + return { + ...payload, + receiptPayloadSha256: sha256CgroupBytes(canonicalCgroupJson(payload)), + }; +} + +function baseReceipt() { + return hashed({ + schemaVersion: "4", + status: "VALIDATED", + limitMode: UNQUALIFIED_AGGREGATE_LIMIT_MODE, + aggregateLimitIntentEnforced: false, + invocationId, + stagingContainerId: "7".repeat(64), + finalContainerId, + metadataSha256: "8".repeat(64), + originalSpecSha256: "9".repeat(64), + baseSpecSha256: "a".repeat(64), + sanitizedSpecSha256, + configFileSha256: "b".repeat(64), + internalMountManifest: [], + internalMountManifestSha256: "c".repeat(64), + readOnlyMountManifest: [], + readOnlyMountManifestSha256: "d".repeat(64), + commandSha256: "e".repeat(64), + imageAuthority: {}, + imageRootfs: {}, + removedFields: [], + normalizedFields: [], + removedMounts: [], + intendedAggregateLimits, + enforcedRlimits: [], + aggregateLimitEvidence: null, + }); +} + +function aggregateEvidence() { + const memberPids = [100, 101]; + return hashed({ + schemaVersion: "1", + status: "OBSERVED", + authority: "linux-cgroup-v2", + cgroupVersion: 2, + cgroupId: `counterlab-v6.1-${invocationId}`, + cgroupPath: `counterlab-v6.1/${invocationId}`, + cgroupIdentity: createContainedCgroupIdentity({ + invocationId, + finalContainerId, + sanitizedSpecSha256, + }), + invocationId, + finalContainerId, + sanitizedSpecSha256, + runtimeAttestationSha256: observerBindings.runtimeAttestationSha256, + observedLimits: { + memoryMaxBytes: intendedAggregateLimits.memoryBytes, + memorySwapMaxBytes: intendedAggregateLimits.memoryBytes, + pidsMax: intendedAggregateLimits.maxProcesses, + cpuQuotaMicros: 100_000, + cpuPeriodMicros: 100_000, + }, + membership: { + leaderPid: 100, + leaderStartTimeTicks: "12345", + memberPids, + memberSetSha256: sha256CgroupBytes(canonicalCgroupJson(memberPids)), + descendantsObserved: true, + }, + negativeControls: { + memory: { + requestedBytes: intendedAggregateLimits.memoryBytes + 1, + oomKillBefore: 0, + oomKillAfter: 1, + enforced: true, + }, + processes: { + attemptedProcesses: intendedAggregateLimits.maxProcesses + 1, + maxEventsBefore: 0, + maxEventsAfter: 1, + enforced: true, + }, + cpu: { + busyWindowMs: 500, + nrThrottledBefore: 0, + nrThrottledAfter: 1, + throttledUsecBefore: 0, + throttledUsecAfter: 1, + enforced: true, + }, + }, + cleanup: { cgroupAbsentAfterTimeout: true }, + observer: { + runtimeSessionId: observerBindings.runtimeSessionId, + driverCliSha256: observerBindings.driverCliSha256, + driverModuleSha256: observerBindings.driverModuleSha256, + }, + observedAt: "2026-07-20T12:00:00.000Z", + }); +} + +describe("qualified contained rootless receipt", () => { + it("derives a new qualified receipt without mutating the conservative base", () => { + const base = baseReceipt(); + const qualified = createQualifiedContainedRootlessReceipt({ + aggregateLimitEvidence: aggregateEvidence(), + baseReceipt: base, + observerBindings, + }); + + expect(base.limitMode).toBe(UNQUALIFIED_AGGREGATE_LIMIT_MODE); + expect(base.aggregateLimitIntentEnforced).toBe(false); + expect(base.aggregateLimitEvidence).toBeNull(); + expect(qualified.limitMode).toBe(QUALIFIED_AGGREGATE_LIMIT_MODE); + expect(qualified.aggregateLimitIntentEnforced).toBe(true); + expect(qualified.aggregateLimitEvidence).toEqual(aggregateEvidence()); + const { receiptPayloadSha256, ...payload } = qualified; + expect(receiptPayloadSha256).toBe( + sha256CgroupBytes(canonicalCgroupJson(payload)), + ); + }); + + it("rejects stale, already asserted, malformed, or unknown base receipts", () => { + const mutations: Array<(value: ReturnType) => void> = [ + (value) => { + value.aggregateLimitIntentEnforced = true; + }, + (value) => { + (value as { limitMode: string }).limitMode = + QUALIFIED_AGGREGATE_LIMIT_MODE; + }, + (value) => { + value.invocationId = "f".repeat(64); + }, + (value) => { + value.receiptPayloadSha256 = "0".repeat(64); + }, + ]; + for (const mutate of mutations) { + const changed = structuredClone(baseReceipt()); + mutate(changed); + expect(() => + createQualifiedContainedRootlessReceipt({ + aggregateLimitEvidence: aggregateEvidence(), + baseReceipt: changed, + observerBindings, + }), + ).toThrow(/rootless receipt/u); + } + expect(() => + createQualifiedContainedRootlessReceipt({ + aggregateLimitEvidence: aggregateEvidence(), + baseReceipt: { ...baseReceipt(), unknown: true }, + observerBindings, + }), + ).toThrow(/shape/u); + }); +}); diff --git a/scripts/contained-runtime-attestation.mjs b/scripts/contained-runtime-attestation.mjs index 6f64e212..c286ec73 100644 --- a/scripts/contained-runtime-attestation.mjs +++ b/scripts/contained-runtime-attestation.mjs @@ -13,6 +13,7 @@ export const RUNTIME_POLICY_PATH = export const RUNTIME_HELPER_PATHS = Object.freeze({ cgroupEvidence: "scripts/contained-cgroup-evidence.mjs", + qualifiedRootlessReceipt: "scripts/contained-qualified-rootless-receipt.mjs", runtimeClient: "scripts/contained-runtime-client.mjs", runtimeRequest: "scripts/contained-runtime-request.mjs", runtimeRun: "scripts/contained-runtime-run.mjs", From 3b7796c909967fddb39eb8ae70b0ce7fd895072f Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:14:19 +0530 Subject: [PATCH 038/143] feat(runtime): persist qualified receipts once --- .../contained-qualified-receipt-store.d.mts | 30 +++ scripts/contained-qualified-receipt-store.mjs | 175 ++++++++++++++ .../contained-qualified-receipt-store.test.ts | 226 ++++++++++++++++++ scripts/contained-runtime-attestation.mjs | 2 + 4 files changed, 433 insertions(+) create mode 100644 scripts/contained-qualified-receipt-store.d.mts create mode 100644 scripts/contained-qualified-receipt-store.mjs create mode 100644 scripts/contained-qualified-receipt-store.test.ts diff --git a/scripts/contained-qualified-receipt-store.d.mts b/scripts/contained-qualified-receipt-store.d.mts new file mode 100644 index 00000000..2613ce0e --- /dev/null +++ b/scripts/contained-qualified-receipt-store.d.mts @@ -0,0 +1,30 @@ +export interface QualifiedRootlessReceiptStoreBinding { + baseReceiptFileSha256: string; + baseReceiptPath: string; + finalContainerId: string; + sessionRoot: string; + observerBindings: { + runtimeAttestationSha256: string; + runtimeSessionId: string; + driverCliSha256: string; + driverModuleSha256: string; + }; +} + +export function persistQualifiedContainedRootlessReceipt( + input: { + aggregateLimitEvidence: unknown; + } & QualifiedRootlessReceiptStoreBinding, +): { + qualifiedReceipt: Record; + qualifiedReceiptFileSha256: string; + qualifiedReceiptPath: string; + qualifiedReceiptPayloadSha256: string; +}; + +export function verifyQualifiedContainedRootlessReceipt( + input: QualifiedRootlessReceiptStoreBinding & { + qualifiedReceiptFileSha256: string; + qualifiedReceiptPath: string; + }, +): Record; diff --git a/scripts/contained-qualified-receipt-store.mjs b/scripts/contained-qualified-receipt-store.mjs new file mode 100644 index 00000000..f038772a --- /dev/null +++ b/scripts/contained-qualified-receipt-store.mjs @@ -0,0 +1,175 @@ +import { lstatSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; +import { isAbsolute, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { sha256CgroupBytes } from "./contained-cgroup-evidence.mjs"; +import { createQualifiedContainedRootlessReceipt } from "./contained-qualified-rootless-receipt.mjs"; + +const repositoryRoot = realpathSync( + resolve(fileURLToPath(import.meta.url), "../.."), +); +const containerIdPattern = /^[a-f0-9]{64}$/u; +const sha256Pattern = /^[a-f0-9]{64}$/u; +const maximumReceiptBytes = 1_048_576; + +function contained(root, candidate) { + const fromRoot = relative(root, candidate); + return ( + fromRoot === "" || (!fromRoot.startsWith("..") && !isAbsolute(fromRoot)) + ); +} + +function privateDirectory(path, label) { + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isDirectory() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() || + (metadata.mode & 0o077) !== 0 + ) { + throw new Error(`qualified rootless receipt ${label} is invalid`); + } +} + +function privateFile(path, label) { + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isFile() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() || + metadata.nlink !== 1 || + (metadata.mode & 0o600) !== 0o600 || + (metadata.mode & 0o077) !== 0 || + metadata.size < 1 || + metadata.size > maximumReceiptBytes + ) { + throw new Error(`qualified rootless receipt ${label} is invalid`); + } +} + +function receiptPaths({ sessionRoot, finalContainerId }) { + if ( + !isAbsolute(sessionRoot) || + !contained(repositoryRoot, sessionRoot) || + !containerIdPattern.test(finalContainerId ?? "") + ) { + throw new Error("qualified rootless receipt path input is invalid"); + } + privateDirectory(sessionRoot, "session root"); + const runRoot = resolve(sessionRoot, "run"); + const specRoot = resolve(runRoot, "rootless-specs"); + if ( + !contained(sessionRoot, runRoot) || + !contained(runRoot, specRoot) || + !contained(repositoryRoot, specRoot) + ) { + throw new Error("qualified rootless receipt path escaped the repository"); + } + privateDirectory(runRoot, "run root"); + privateDirectory(specRoot, "spec root"); + return { + baseReceiptPath: resolve(specRoot, `${finalContainerId}.receipt.json`), + qualifiedReceiptPath: resolve( + specRoot, + `${finalContainerId}.qualified-receipt.json`, + ), + }; +} + +function readBaseReceipt({ + baseReceiptFileSha256, + baseReceiptPath, + finalContainerId, + sessionRoot, +}) { + if (!sha256Pattern.test(baseReceiptFileSha256 ?? "")) { + throw new Error("qualified rootless base receipt hash is invalid"); + } + const expected = receiptPaths({ sessionRoot, finalContainerId }); + if (baseReceiptPath !== expected.baseReceiptPath) { + throw new Error("qualified rootless receipt base path changed"); + } + privateFile(baseReceiptPath, "base receipt"); + const source = readFileSync(baseReceiptPath); + if (sha256CgroupBytes(source) !== baseReceiptFileSha256) { + throw new Error("qualified rootless base receipt changed"); + } + let receipt; + try { + receipt = JSON.parse(source.toString("utf8")); + } catch (error) { + throw new Error("qualified rootless base receipt JSON is invalid", { + cause: error, + }); + } + if (receipt?.finalContainerId !== finalContainerId) { + throw new Error("qualified rootless base receipt identity changed"); + } + return { expected, receipt }; +} + +export function persistQualifiedContainedRootlessReceipt(input) { + const { expected, receipt: baseReceipt } = readBaseReceipt(input); + const qualifiedReceipt = createQualifiedContainedRootlessReceipt({ + aggregateLimitEvidence: input.aggregateLimitEvidence, + baseReceipt, + observerBindings: input.observerBindings, + }); + const source = `${JSON.stringify(qualifiedReceipt, null, 2)}\n`; + try { + writeFileSync(expected.qualifiedReceiptPath, source, { + encoding: "utf8", + flag: "wx", + mode: 0o600, + }); + } catch (error) { + if (error?.code === "EEXIST") { + throw new Error("qualified rootless receipt already exists", { + cause: error, + }); + } + throw error; + } + privateFile(expected.qualifiedReceiptPath, "qualified receipt"); + return { + qualifiedReceipt, + qualifiedReceiptFileSha256: sha256CgroupBytes(source), + qualifiedReceiptPath: expected.qualifiedReceiptPath, + qualifiedReceiptPayloadSha256: qualifiedReceipt.receiptPayloadSha256, + }; +} + +export function verifyQualifiedContainedRootlessReceipt(input) { + const { expected, receipt: baseReceipt } = readBaseReceipt(input); + if ( + input.qualifiedReceiptPath !== expected.qualifiedReceiptPath || + !sha256Pattern.test(input.qualifiedReceiptFileSha256 ?? "") + ) { + throw new Error("qualified rootless receipt path or hash changed"); + } + privateFile(input.qualifiedReceiptPath, "qualified receipt"); + const source = readFileSync(input.qualifiedReceiptPath); + if (sha256CgroupBytes(source) !== input.qualifiedReceiptFileSha256) { + throw new Error("qualified rootless receipt file changed"); + } + let qualifiedReceipt; + try { + qualifiedReceipt = JSON.parse(source.toString("utf8")); + } catch (error) { + throw new Error("qualified rootless receipt JSON is invalid", { + cause: error, + }); + } + const expectedReceipt = createQualifiedContainedRootlessReceipt({ + aggregateLimitEvidence: qualifiedReceipt.aggregateLimitEvidence, + baseReceipt, + observerBindings: input.observerBindings, + }); + const expectedSource = `${JSON.stringify(expectedReceipt, null, 2)}\n`; + if (!source.equals(Buffer.from(expectedSource))) { + throw new Error("qualified rootless receipt binding changed"); + } + return qualifiedReceipt; +} diff --git a/scripts/contained-qualified-receipt-store.test.ts b/scripts/contained-qualified-receipt-store.test.ts new file mode 100644 index 00000000..b0070980 --- /dev/null +++ b/scripts/contained-qualified-receipt-store.test.ts @@ -0,0 +1,226 @@ +import { + mkdirSync, + mkdtempSync, + readFileSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { + canonicalCgroupJson, + createContainedCgroupIdentity, + sha256CgroupBytes, +} from "./contained-cgroup-evidence.mjs"; +import { + persistQualifiedContainedRootlessReceipt, + verifyQualifiedContainedRootlessReceipt, +} from "./contained-qualified-receipt-store.mjs"; +import { UNQUALIFIED_AGGREGATE_LIMIT_MODE } from "./contained-qualified-rootless-receipt.mjs"; + +const root = process.cwd(); +const invocationId = "1".repeat(64); +const finalContainerId = "2".repeat(64); +const sanitizedSpecSha256 = "3".repeat(64); +const intendedAggregateLimits = { + cpuCount: 1, + maxProcesses: 16, + memoryBytes: 512 * 1024 * 1024, +}; +const observerBindings = { + runtimeAttestationSha256: "4".repeat(64), + runtimeSessionId: "rt-v61-test1", + driverCliSha256: "5".repeat(64), + driverModuleSha256: "6".repeat(64), +}; + +function hashed>(payload: T) { + return { + ...payload, + receiptPayloadSha256: sha256CgroupBytes(canonicalCgroupJson(payload)), + }; +} + +function baseReceipt() { + return hashed({ + schemaVersion: "4", + status: "VALIDATED", + limitMode: UNQUALIFIED_AGGREGATE_LIMIT_MODE, + aggregateLimitIntentEnforced: false, + invocationId, + stagingContainerId: "7".repeat(64), + finalContainerId, + metadataSha256: "8".repeat(64), + originalSpecSha256: "9".repeat(64), + baseSpecSha256: "a".repeat(64), + sanitizedSpecSha256, + configFileSha256: "b".repeat(64), + internalMountManifest: [], + internalMountManifestSha256: "c".repeat(64), + readOnlyMountManifest: [], + readOnlyMountManifestSha256: "d".repeat(64), + commandSha256: "e".repeat(64), + imageAuthority: {}, + imageRootfs: {}, + removedFields: [], + normalizedFields: [], + removedMounts: [], + intendedAggregateLimits, + enforcedRlimits: [], + aggregateLimitEvidence: null, + }); +} + +function aggregateEvidence() { + const memberPids = [100, 101]; + return hashed({ + schemaVersion: "1", + status: "OBSERVED", + authority: "linux-cgroup-v2", + cgroupVersion: 2, + cgroupId: `counterlab-v6.1-${invocationId}`, + cgroupPath: `counterlab-v6.1/${invocationId}`, + cgroupIdentity: createContainedCgroupIdentity({ + invocationId, + finalContainerId, + sanitizedSpecSha256, + }), + invocationId, + finalContainerId, + sanitizedSpecSha256, + runtimeAttestationSha256: observerBindings.runtimeAttestationSha256, + observedLimits: { + memoryMaxBytes: intendedAggregateLimits.memoryBytes, + memorySwapMaxBytes: intendedAggregateLimits.memoryBytes, + pidsMax: intendedAggregateLimits.maxProcesses, + cpuQuotaMicros: 100_000, + cpuPeriodMicros: 100_000, + }, + membership: { + leaderPid: 100, + leaderStartTimeTicks: "12345", + memberPids, + memberSetSha256: sha256CgroupBytes(canonicalCgroupJson(memberPids)), + descendantsObserved: true, + }, + negativeControls: { + memory: { + requestedBytes: intendedAggregateLimits.memoryBytes + 1, + oomKillBefore: 0, + oomKillAfter: 1, + enforced: true, + }, + processes: { + attemptedProcesses: intendedAggregateLimits.maxProcesses + 1, + maxEventsBefore: 0, + maxEventsAfter: 1, + enforced: true, + }, + cpu: { + busyWindowMs: 500, + nrThrottledBefore: 0, + nrThrottledAfter: 1, + throttledUsecBefore: 0, + throttledUsecAfter: 1, + enforced: true, + }, + }, + cleanup: { cgroupAbsentAfterTimeout: true }, + observer: { + runtimeSessionId: observerBindings.runtimeSessionId, + driverCliSha256: observerBindings.driverCliSha256, + driverModuleSha256: observerBindings.driverModuleSha256, + }, + observedAt: "2026-07-20T12:00:00.000Z", + }); +} + +function fixture() { + const parent = resolve( + root, + "node_modules/.cache/counterlab-v6.1/tmp/qualified-receipt-store", + ); + mkdirSync(parent, { recursive: true, mode: 0o700 }); + const sessionRoot = mkdtempSync(resolve(parent, "rt-store-")); + const runRoot = resolve(sessionRoot, "run"); + const specRoot = resolve(runRoot, "rootless-specs"); + mkdirSync(specRoot, { recursive: true, mode: 0o700 }); + const baseReceiptPath = resolve(specRoot, `${finalContainerId}.receipt.json`); + const source = `${JSON.stringify(baseReceipt(), null, 2)}\n`; + writeFileSync(baseReceiptPath, source, { mode: 0o600, flag: "wx" }); + return { + baseReceiptFileSha256: sha256CgroupBytes(source), + baseReceiptPath, + sessionRoot, + }; +} + +describe("qualified rootless receipt store", () => { + it("creates and verifies one distinct qualified receipt", () => { + const input = fixture(); + const persisted = persistQualifiedContainedRootlessReceipt({ + ...input, + aggregateLimitEvidence: aggregateEvidence(), + finalContainerId, + observerBindings, + }); + + expect(persisted.qualifiedReceiptPath).toBe( + resolve( + input.sessionRoot, + `run/rootless-specs/${finalContainerId}.qualified-receipt.json`, + ), + ); + expect(readFileSync(input.baseReceiptPath, "utf8")).toBe( + `${JSON.stringify(baseReceipt(), null, 2)}\n`, + ); + expect(() => + verifyQualifiedContainedRootlessReceipt({ + ...input, + finalContainerId, + observerBindings, + qualifiedReceiptFileSha256: persisted.qualifiedReceiptFileSha256, + qualifiedReceiptPath: persisted.qualifiedReceiptPath, + }), + ).not.toThrow(); + expect(() => + persistQualifiedContainedRootlessReceipt({ + ...input, + aggregateLimitEvidence: aggregateEvidence(), + finalContainerId, + observerBindings, + }), + ).toThrow(/already exists/u); + }); + + it("rejects path, hash, and symbolic-link drift", () => { + const wrongHash = fixture(); + expect(() => + persistQualifiedContainedRootlessReceipt({ + ...wrongHash, + aggregateLimitEvidence: aggregateEvidence(), + baseReceiptFileSha256: "0".repeat(64), + finalContainerId, + observerBindings, + }), + ).toThrow(/base receipt/u); + + const linked = fixture(); + const linkedPath = resolve( + linked.sessionRoot, + "run/rootless-specs/link.json", + ); + symlinkSync(linked.baseReceiptPath, linkedPath); + expect(() => + persistQualifiedContainedRootlessReceipt({ + ...linked, + aggregateLimitEvidence: aggregateEvidence(), + baseReceiptPath: linkedPath, + finalContainerId, + observerBindings, + }), + ).toThrow(/path/u); + }); +}); diff --git a/scripts/contained-runtime-attestation.mjs b/scripts/contained-runtime-attestation.mjs index c286ec73..de4de1af 100644 --- a/scripts/contained-runtime-attestation.mjs +++ b/scripts/contained-runtime-attestation.mjs @@ -13,6 +13,8 @@ export const RUNTIME_POLICY_PATH = export const RUNTIME_HELPER_PATHS = Object.freeze({ cgroupEvidence: "scripts/contained-cgroup-evidence.mjs", + qualifiedRootlessReceiptStore: + "scripts/contained-qualified-receipt-store.mjs", qualifiedRootlessReceipt: "scripts/contained-qualified-rootless-receipt.mjs", runtimeClient: "scripts/contained-runtime-client.mjs", runtimeRequest: "scripts/contained-runtime-request.mjs", From 0077f1a7af707cee6f3b502277738f5d04392aca Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:21:28 +0530 Subject: [PATCH 039/143] feat(runtime): add fixed cgroup controls --- scripts/contained-cgroup-control-helper.d.mts | 8 + scripts/contained-cgroup-control-helper.mjs | 217 ++++++++++++++++++ .../contained-cgroup-control-helper.test.ts | 50 ++++ scripts/contained-runtime-attestation.mjs | 1 + 4 files changed, 276 insertions(+) create mode 100644 scripts/contained-cgroup-control-helper.d.mts create mode 100644 scripts/contained-cgroup-control-helper.mjs create mode 100644 scripts/contained-cgroup-control-helper.test.ts diff --git a/scripts/contained-cgroup-control-helper.d.mts b/scripts/contained-cgroup-control-helper.d.mts new file mode 100644 index 00000000..01bdd1ea --- /dev/null +++ b/scripts/contained-cgroup-control-helper.d.mts @@ -0,0 +1,8 @@ +export type ContainedCgroupControl = + | { mode: "memory"; requestedBytes: number } + | { mode: "processes"; attemptedProcesses: number } + | { mode: "cpu"; busyWindowMs: number; workers: number }; + +export function parseContainedCgroupControl( + args: string[], +): ContainedCgroupControl; diff --git a/scripts/contained-cgroup-control-helper.mjs b/scripts/contained-cgroup-control-helper.mjs new file mode 100644 index 00000000..e9092499 --- /dev/null +++ b/scripts/contained-cgroup-control-helper.mjs @@ -0,0 +1,217 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import { realpathSync, writeFileSync } from "node:fs"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath } from "node:url"; + +const minimumMemoryBytes = 64 * 1024 * 1024; +const maximumMemoryBytes = 1280 * 1024 * 1024; + +function integer(value) { + if (typeof value !== "string" || !/^[1-9][0-9]*$/u.test(value)) { + throw new Error("contained cgroup control integer is invalid"); + } + const parsed = Number.parseInt(value, 10); + if (!Number.isSafeInteger(parsed)) { + throw new Error("contained cgroup control integer is invalid"); + } + return parsed; +} + +export function parseContainedCgroupControl(args) { + if (!Array.isArray(args) || args[0] !== "--mode") { + throw new Error("contained cgroup control arguments are invalid"); + } + if ( + args[1] === "memory" && + args.length === 4 && + args[2] === "--requested-bytes" + ) { + const requestedBytes = integer(args[3]); + if ( + requestedBytes < minimumMemoryBytes || + requestedBytes > maximumMemoryBytes + ) { + throw new Error("contained cgroup control memory bound is invalid"); + } + return { mode: "memory", requestedBytes }; + } + if ( + args[1] === "processes" && + args.length === 4 && + args[2] === "--attempted-processes" + ) { + const attemptedProcesses = integer(args[3]); + if (attemptedProcesses < 2 || attemptedProcesses > 64) { + throw new Error("contained cgroup control process bound is invalid"); + } + return { mode: "processes", attemptedProcesses }; + } + if ( + args[1] === "cpu" && + args.length === 6 && + args[2] === "--busy-window-ms" && + args[4] === "--workers" + ) { + const busyWindowMs = integer(args[3]); + const workers = integer(args[5]); + if ( + busyWindowMs < 100 || + busyWindowMs > 5_000 || + workers < 2 || + workers > 8 + ) { + throw new Error("contained cgroup control CPU bound is invalid"); + } + return { mode: "cpu", busyWindowMs, workers }; + } + throw new Error("contained cgroup control mode or fields are invalid"); +} + +async function waitForGo() { + process.stdout.write("READY\n"); + const chunks = []; + let length = 0; + for await (const chunk of process.stdin) { + length += chunk.byteLength; + if (length > 16) { + throw new Error("contained cgroup control handshake is oversized"); + } + chunks.push(chunk); + } + if (Buffer.concat(chunks).toString("utf8") !== "GO\n") { + throw new Error("contained cgroup control handshake is invalid"); + } +} + +function childCompletion(child) { + return new Promise((accept) => { + let started = false; + child.once("spawn", () => { + started = true; + }); + child.once("error", () => accept({ started: false })); + child.once("close", (code, signal) => accept({ started, code, signal })); + }); +} + +async function memoryControl(requestedBytes) { + writeFileSync("/proc/self/oom_score_adj", "1000\n", "utf8"); + const retained = []; + let allocated = 0; + const chunkBytes = 8 * 1024 * 1024; + while (allocated < requestedBytes) { + const chunk = Buffer.allocUnsafe( + Math.min(chunkBytes, requestedBytes - allocated), + ); + for (let offset = 0; offset < chunk.byteLength; offset += 4_096) { + chunk[offset] = 1; + } + retained.push(chunk); + allocated += chunk.byteLength; + } + await new Promise((accept) => setTimeout(accept, 2_000)); + throw new Error( + `contained cgroup memory control escaped its limit at ${retained.length} chunks`, + ); +} + +async function processControl(attemptedProcesses, scriptPath) { + const children = []; + const completions = []; + let denied = false; + for (let index = 0; index < attemptedProcesses; index += 1) { + const child = spawn( + process.execPath, + [scriptPath, "--internal-hold", "5000"], + { stdio: "ignore" }, + ); + children.push(child); + const completion = childCompletion(child); + completions.push(completion); + const state = await Promise.race([ + completion, + new Promise((accept) => + child.once("spawn", () => accept({ started: true })), + ), + ]); + if (!state.started) { + denied = true; + break; + } + } + for (const child of children) { + if (child.exitCode === null && child.signalCode === null) { + child.kill("SIGTERM"); + } + } + await Promise.all(completions); + if (!denied) { + throw new Error("contained cgroup process control escaped its limit"); + } + process.stdout.write( + `${JSON.stringify({ attemptedProcesses, denied: true })}\n`, + ); +} + +async function cpuControl(busyWindowMs, workers, scriptPath) { + const children = Array.from({ length: workers }, () => + spawn( + process.execPath, + [scriptPath, "--internal-busy", String(busyWindowMs)], + { stdio: "ignore" }, + ), + ); + const results = await Promise.all(children.map(childCompletion)); + if (results.some((result) => !result.started || result.code !== 0)) { + throw new Error("contained cgroup CPU control worker failed"); + } + process.stdout.write(`${JSON.stringify({ busyWindowMs, workers })}\n`); +} + +async function main() { + const scriptPath = realpathSync(fileURLToPath(import.meta.url)); + if (process.argv[2] === "--internal-hold" && process.argv.length === 4) { + const holdMs = integer(process.argv[3]); + if (holdMs > 10_000) { + throw new Error("contained cgroup internal hold bound is invalid"); + } + await new Promise((accept) => setTimeout(accept, holdMs)); + return; + } + if (process.argv[2] === "--internal-busy" && process.argv.length === 4) { + const busyMs = integer(process.argv[3]); + if (busyMs > 5_000) { + throw new Error("contained cgroup internal CPU bound is invalid"); + } + const deadline = performance.now() + busyMs; + while (performance.now() < deadline) { + // Fixed release-only CPU load. Kernel counters are authoritative. + } + return; + } + const control = parseContainedCgroupControl(process.argv.slice(2)); + await waitForGo(); + if (control.mode === "memory") { + await memoryControl(control.requestedBytes); + return; + } + if (control.mode === "processes") { + await processControl(control.attemptedProcesses, scriptPath); + return; + } + await cpuControl(control.busyWindowMs, control.workers, scriptPath); +} + +if ( + process.argv[1] !== undefined && + realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) +) { + main().catch((error) => { + process.stderr.write( + `${error instanceof Error ? error.message : "contained cgroup control failed"}\n`, + ); + process.exitCode = 70; + }); +} diff --git a/scripts/contained-cgroup-control-helper.test.ts b/scripts/contained-cgroup-control-helper.test.ts new file mode 100644 index 00000000..d5e04954 --- /dev/null +++ b/scripts/contained-cgroup-control-helper.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; + +import { parseContainedCgroupControl } from "./contained-cgroup-control-helper.mjs"; + +describe("contained cgroup negative-control helper", () => { + it("accepts only bounded release-control modes", () => { + expect( + parseContainedCgroupControl([ + "--mode", + "memory", + "--requested-bytes", + String(576 * 1024 * 1024), + ]), + ).toEqual({ mode: "memory", requestedBytes: 576 * 1024 * 1024 }); + expect( + parseContainedCgroupControl([ + "--mode", + "processes", + "--attempted-processes", + "17", + ]), + ).toEqual({ mode: "processes", attemptedProcesses: 17 }); + expect( + parseContainedCgroupControl([ + "--mode", + "cpu", + "--busy-window-ms", + "500", + "--workers", + "4", + ]), + ).toEqual({ mode: "cpu", busyWindowMs: 500, workers: 4 }); + }); + + it("rejects unknown fields, unsafe bounds, and non-integers", () => { + for (const args of [ + ["--mode", "unknown"], + ["--mode", "memory", "--requested-bytes", "1"], + ["--mode", "processes", "--attempted-processes", "65"], + ["--mode", "cpu", "--busy-window-ms", "99", "--workers", "4"], + ["--mode", "cpu", "--busy-window-ms", "500", "--workers", "1"], + ["--mode", "memory", "--requested-bytes", "100.5"], + ["--mode", "memory", "--requested-bytes", "67108864", "extra"], + ]) { + expect(() => parseContainedCgroupControl(args), args.join(" ")).toThrow( + /cgroup control/u, + ); + } + }); +}); diff --git a/scripts/contained-runtime-attestation.mjs b/scripts/contained-runtime-attestation.mjs index de4de1af..6494dcdb 100644 --- a/scripts/contained-runtime-attestation.mjs +++ b/scripts/contained-runtime-attestation.mjs @@ -12,6 +12,7 @@ export const RUNTIME_POLICY_PATH = "services/runner/src/counterlab_runner/contained-runtime-policy.json"; export const RUNTIME_HELPER_PATHS = Object.freeze({ + cgroupControlHelper: "scripts/contained-cgroup-control-helper.mjs", cgroupEvidence: "scripts/contained-cgroup-evidence.mjs", qualifiedRootlessReceiptStore: "scripts/contained-qualified-receipt-store.mjs", From fbc5ba9cbe1659b3e18ab230ef690cede3dd4bd5 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:31:59 +0530 Subject: [PATCH 040/143] feat(runtime): bind cgroup observer identity --- .../contained-cgroup-observer-bindings.d.mts | 10 + .../contained-cgroup-observer-bindings.mjs | 174 ++++++++++++++++++ ...contained-cgroup-observer-bindings.test.ts | 139 ++++++++++++++ scripts/contained-runtime-attestation.mjs | 1 + 4 files changed, 324 insertions(+) create mode 100644 scripts/contained-cgroup-observer-bindings.d.mts create mode 100644 scripts/contained-cgroup-observer-bindings.mjs create mode 100644 scripts/contained-cgroup-observer-bindings.test.ts diff --git a/scripts/contained-cgroup-observer-bindings.d.mts b/scripts/contained-cgroup-observer-bindings.d.mts new file mode 100644 index 00000000..91616f90 --- /dev/null +++ b/scripts/contained-cgroup-observer-bindings.d.mts @@ -0,0 +1,10 @@ +export interface ContainedCgroupObserverBindings { + driverCliSha256: string; + driverModuleSha256: string; + runtimeAttestationSha256: string; + runtimeSessionId: string; +} + +export function resolveContainedCgroupObserverBindings(input: { + sessionRoot: string; +}): Readonly; diff --git a/scripts/contained-cgroup-observer-bindings.mjs b/scripts/contained-cgroup-observer-bindings.mjs new file mode 100644 index 00000000..fb235661 --- /dev/null +++ b/scripts/contained-cgroup-observer-bindings.mjs @@ -0,0 +1,174 @@ +import { lstatSync, readFileSync, realpathSync } from "node:fs"; +import { basename, isAbsolute, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + canonicalRuntimeJson, + createPublicContainedRuntimeAttestation, + createRuntimeToolchainFingerprint, + requireContainedRuntimeSessionId, + sha256RuntimeBytes, +} from "./contained-runtime-attestation.mjs"; + +const repositoryRoot = realpathSync( + resolve(fileURLToPath(import.meta.url), "../.."), +); +const maximumAttestationBytes = 1_048_576; +const sha256Pattern = /^[a-f0-9]{64}$/u; + +function contained(parent, candidate) { + const fromParent = relative(parent, candidate); + return ( + fromParent === "" || + (!fromParent.startsWith("..") && !isAbsolute(fromParent)) + ); +} + +function privateDirectory(path, label) { + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isDirectory() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() || + (metadata.mode & 0o077) !== 0 + ) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } +} + +function privateFile(path, label) { + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isFile() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() || + metadata.nlink !== 1 || + (metadata.mode & 0o600) !== 0o600 || + (metadata.mode & 0o077) !== 0 || + metadata.size < 1 || + metadata.size > maximumAttestationBytes + ) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } +} + +function exactRuntimeSessionRoot(sessionRoot) { + if (!isAbsolute(sessionRoot) || !contained(repositoryRoot, sessionRoot)) { + throw new Error("contained cgroup observer session root is invalid"); + } + const runtimeSessionId = requireContainedRuntimeSessionId( + basename(sessionRoot), + ); + const expected = resolve(repositoryRoot, ".rt", runtimeSessionId); + if (sessionRoot !== expected) { + throw new Error("contained cgroup observer session root is not exact"); + } + privateDirectory(resolve(repositoryRoot, ".rt"), "runtime root"); + privateDirectory(sessionRoot, "session root"); + return runtimeSessionId; +} + +function equalJson(left, right) { + return canonicalRuntimeJson(left) === canonicalRuntimeJson(right); +} + +function repositoryFile(path, label) { + const metadata = lstatSync(path); + if ( + !contained(repositoryRoot, path) || + metadata.isSymbolicLink() || + !metadata.isFile() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() + ) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } +} + +export function resolveContainedCgroupObserverBindings({ sessionRoot }) { + const runtimeSessionId = exactRuntimeSessionRoot(sessionRoot); + const attestationPath = resolve(sessionRoot, "attestation.json"); + privateFile(attestationPath, "runtime attestation"); + let attestation; + try { + attestation = JSON.parse(readFileSync(attestationPath, "utf8")); + } catch (error) { + throw new Error("contained cgroup observer attestation JSON is invalid", { + cause: error, + }); + } + const sessionPrefix = `.rt/${runtimeSessionId}`; + const expectedPublicPaths = { + containerdRootlesskitApiSocket: `${sessionPrefix}/run/containerd-rootless/api.sock`, + containerdSocket: `${sessionPrefix}/run/containerd.sock`, + runtimeCommandSocket: `${sessionPrefix}/run/runtime-command.sock`, + buildkitSocket: `${sessionPrefix}/run/buildkitd.sock`, + }; + const material = createRuntimeToolchainFingerprint({ + root: repositoryRoot, + adapterPath: resolve( + repositoryRoot, + "scripts/contained-runtime-adapter.sh", + ), + containerdConfigPath: resolve(sessionRoot, "config/containerd.toml"), + buildkitConfigPath: resolve(sessionRoot, "config/buildkitd.toml"), + }); + if ( + attestation?.schemaVersion !== "3" || + attestation?.status !== "READY" || + attestation?.sessionId !== runtimeSessionId || + attestation?.namespace !== "counterlab-v6.1" || + attestation?.paths?.sessionRoot !== sessionPrefix || + Object.entries(expectedPublicPaths).some( + ([name, path]) => attestation?.paths?.[name] !== path, + ) || + attestation?.runtimeToolchainSha256 !== material.runtimeToolchainSha256 || + attestation?.toolchainLockSha256 !== material.toolchainLockSha256 || + attestation?.adapterSha256 !== material.adapterSha256 || + attestation?.runtimePolicySha256 !== material.runtimePolicySha256 || + attestation?.proofDependencyManifestSha256 !== + material.proofDependencyManifestSha256 || + !equalJson(attestation?.helperSha256, material.helperSha256) || + !equalJson(attestation?.fileSha256, { + ...material.fileSha256, + supervisorReady: attestation?.fileSha256?.supervisorReady, + }) || + !sha256Pattern.test(attestation?.fileSha256?.supervisorReady ?? "") + ) { + throw new Error("contained cgroup observer attestation binding changed"); + } + const componentSha256 = Object.fromEntries( + Object.entries(material.lock.components).map(([name, component]) => [ + name, + component.sha256, + ]), + ); + const publicAttestation = createPublicContainedRuntimeAttestation({ + attestation, + componentSha256, + }); + const driverCliPath = resolve( + repositoryRoot, + "scripts/verify-contained-runtime-timeout.py", + ); + const driverModulePath = resolve( + repositoryRoot, + "services/runner/src/counterlab_runner/timeout_proof.py", + ); + for (const [path, label] of [ + [driverCliPath, "proof CLI"], + [driverModulePath, "proof module"], + ]) { + repositoryFile(path, label); + } + return Object.freeze({ + driverCliSha256: sha256RuntimeBytes(readFileSync(driverCliPath)), + driverModuleSha256: sha256RuntimeBytes(readFileSync(driverModulePath)), + runtimeAttestationSha256: sha256RuntimeBytes( + canonicalRuntimeJson(publicAttestation), + ), + runtimeSessionId, + }); +} diff --git a/scripts/contained-cgroup-observer-bindings.test.ts b/scripts/contained-cgroup-observer-bindings.test.ts new file mode 100644 index 00000000..ac6f9499 --- /dev/null +++ b/scripts/contained-cgroup-observer-bindings.test.ts @@ -0,0 +1,139 @@ +import { randomBytes } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { + canonicalRuntimeJson, + createPublicContainedRuntimeAttestation, + createRuntimeToolchainFingerprint, + sha256RuntimeBytes, +} from "./contained-runtime-attestation.mjs"; +import { resolveContainedCgroupObserverBindings } from "./contained-cgroup-observer-bindings.mjs"; + +const root = process.cwd(); + +function fixture() { + const runtimeSessionId = `rt-bind-${randomBytes(4).toString("hex")}`; + const sessionRoot = resolve(root, ".rt", runtimeSessionId); + mkdirSync(resolve(sessionRoot, "config"), { + recursive: true, + mode: 0o700, + }); + writeFileSync(resolve(sessionRoot, "config/containerd.toml"), "version=3\n", { + flag: "wx", + mode: 0o600, + }); + writeFileSync( + resolve(sessionRoot, "config/buildkitd.toml"), + "debug=false\n", + { + flag: "wx", + mode: 0o600, + }, + ); + const material = createRuntimeToolchainFingerprint({ + root, + adapterPath: resolve(root, "scripts/contained-runtime-adapter.sh"), + containerdConfigPath: resolve(sessionRoot, "config/containerd.toml"), + buildkitConfigPath: resolve(sessionRoot, "config/buildkitd.toml"), + }); + const sessionPrefix = `.rt/${runtimeSessionId}`; + const attestation = { + schemaVersion: "3", + status: "READY", + sessionId: runtimeSessionId, + namespace: "counterlab-v6.1", + runtimeToolchainSha256: material.runtimeToolchainSha256, + toolchainLockSha256: material.toolchainLockSha256, + adapterSha256: material.adapterSha256, + helperSha256: material.helperSha256, + runtimePolicySha256: material.runtimePolicySha256, + proofDependencyManifest: material.proofDependencyManifest, + proofDependencyManifestSha256: material.proofDependencyManifestSha256, + createdAt: new Date().toISOString(), + paths: { + sessionRoot: sessionPrefix, + containerdRootlesskitApiSocket: `${sessionPrefix}/run/containerd-rootless/api.sock`, + containerdSocket: `${sessionPrefix}/run/containerd.sock`, + runtimeCommandSocket: `${sessionPrefix}/run/runtime-command.sock`, + buildkitSocket: `${sessionPrefix}/run/buildkitd.sock`, + }, + pids: {}, + fileSha256: { + ...material.fileSha256, + supervisorReady: "f".repeat(64), + }, + }; + writeFileSync( + resolve(sessionRoot, "attestation.json"), + `${JSON.stringify(attestation)}\n`, + { flag: "wx", mode: 0o600 }, + ); + return { attestation, material, runtimeSessionId, sessionRoot }; +} + +describe("contained cgroup observer bindings", () => { + it("derives proof-driver and public-attestation hashes from exact live bytes", () => { + const input = fixture(); + const componentSha256 = Object.fromEntries( + Object.entries(input.material.lock.components).map( + ([name, component]) => [name, component.sha256], + ), + ); + const publicAttestation = createPublicContainedRuntimeAttestation({ + attestation: input.attestation, + componentSha256, + }); + + expect( + resolveContainedCgroupObserverBindings({ + sessionRoot: input.sessionRoot, + }), + ).toEqual({ + driverCliSha256: sha256RuntimeBytes( + readFileSync( + resolve(root, "scripts/verify-contained-runtime-timeout.py"), + ), + ), + driverModuleSha256: sha256RuntimeBytes( + readFileSync( + resolve( + root, + "services/runner/src/counterlab_runner/timeout_proof.py", + ), + ), + ), + runtimeAttestationSha256: sha256RuntimeBytes( + canonicalRuntimeJson(publicAttestation), + ), + runtimeSessionId: input.runtimeSessionId, + }); + }); + + it("rejects a noncanonical session root and stale helper attestation", () => { + const wrongRoot = fixture(); + expect(() => + resolveContainedCgroupObserverBindings({ + sessionRoot: resolve(wrongRoot.sessionRoot, "config"), + }), + ).toThrow(/session/u); + + const stale = fixture(); + stale.attestation.helperSha256 = { + ...stale.attestation.helperSha256, + runtimeRun: "0".repeat(64), + }; + writeFileSync( + resolve(stale.sessionRoot, "attestation.json"), + `${JSON.stringify(stale.attestation)}\n`, + { mode: 0o600 }, + ); + expect(() => + resolveContainedCgroupObserverBindings({ + sessionRoot: stale.sessionRoot, + }), + ).toThrow(/attestation binding/u); + }); +}); diff --git a/scripts/contained-runtime-attestation.mjs b/scripts/contained-runtime-attestation.mjs index 6494dcdb..faafc247 100644 --- a/scripts/contained-runtime-attestation.mjs +++ b/scripts/contained-runtime-attestation.mjs @@ -14,6 +14,7 @@ export const RUNTIME_POLICY_PATH = export const RUNTIME_HELPER_PATHS = Object.freeze({ cgroupControlHelper: "scripts/contained-cgroup-control-helper.mjs", cgroupEvidence: "scripts/contained-cgroup-evidence.mjs", + cgroupObserverBindings: "scripts/contained-cgroup-observer-bindings.mjs", qualifiedRootlessReceiptStore: "scripts/contained-qualified-receipt-store.mjs", qualifiedRootlessReceipt: "scripts/contained-qualified-rootless-receipt.mjs", From 37684dd2424bdf09def57d05cbbad40f2f3d75b3 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:35:18 +0530 Subject: [PATCH 041/143] feat(runtime): define cgroup observer protocol --- .../contained-cgroup-observer-protocol.d.mts | 61 ++++++ .../contained-cgroup-observer-protocol.mjs | 201 ++++++++++++++++++ ...contained-cgroup-observer-protocol.test.ts | 167 +++++++++++++++ scripts/contained-runtime-attestation.mjs | 1 + 4 files changed, 430 insertions(+) create mode 100644 scripts/contained-cgroup-observer-protocol.d.mts create mode 100644 scripts/contained-cgroup-observer-protocol.mjs create mode 100644 scripts/contained-cgroup-observer-protocol.test.ts diff --git a/scripts/contained-cgroup-observer-protocol.d.mts b/scripts/contained-cgroup-observer-protocol.d.mts new file mode 100644 index 00000000..4c76be22 --- /dev/null +++ b/scripts/contained-cgroup-observer-protocol.d.mts @@ -0,0 +1,61 @@ +export interface ContainedCgroupObserverManifest { + schemaVersion: "1"; + status: "REQUESTED"; + qualificationMode: "aggregate-timeout-proof-v1"; + runtimeSessionId: string; + invocationId: string; + finalContainerId: string; + cgroupId: string; + cgroupPath: string; + cgroupIdentity: string; + sanitizedSpecSha256: string; + intendedAggregateLimits: { + cpuCount: number; + maxProcesses: number; + memoryBytes: number; + }; + baseReceiptPath: string; + baseReceiptFileSha256: string; + baseReceiptPayloadSha256: string; + runtimeAttestationSha256: string; + driverCliSha256: string; + driverModuleSha256: string; + requestedAt: string; + receiptPayloadSha256: string; +} + +export function validateContainedCgroupObserverManifest( + value: unknown, + options?: { observedAtMs?: number }, +): ContainedCgroupObserverManifest; + +export function createContainedCgroupObserverManifest(input: { + baseReceiptFileSha256: string; + baseReceiptPayloadSha256: string; + finalContainerId: string; + intendedAggregateLimits: ContainedCgroupObserverManifest["intendedAggregateLimits"]; + invocationId: string; + observerBindings: { + runtimeSessionId: string; + runtimeAttestationSha256: string; + driverCliSha256: string; + driverModuleSha256: string; + }; + requestedAt?: Date; + sanitizedSpecSha256: string; +}): ContainedCgroupObserverManifest; + +export function containedCgroupQualificationPaths(input: { + repositoryRoot: string; + runtimeSessionId: string; + invocationId: string; +}): Readonly<{ + evidencePath: string; + failurePath: string; + invocationRoot: string; + manifestPath: string; + observerDraftPath: string; + observerReadyPath: string; + qualificationRoot: string; + sessionRoot: string; +}>; diff --git a/scripts/contained-cgroup-observer-protocol.mjs b/scripts/contained-cgroup-observer-protocol.mjs new file mode 100644 index 00000000..0988cbfa --- /dev/null +++ b/scripts/contained-cgroup-observer-protocol.mjs @@ -0,0 +1,201 @@ +import { isAbsolute, relative, resolve } from "node:path"; + +import { AGGREGATE_TIMEOUT_QUALIFICATION_MODE } from "./contained-runtime-request.mjs"; +import { + canonicalCgroupJson, + createContainedCgroupIdentity, + sha256CgroupBytes, +} from "./contained-cgroup-evidence.mjs"; + +const sha256Pattern = /^[a-f0-9]{64}$/u; +const sessionPattern = /^rt-[a-z0-9][a-z0-9-]{7,13}$/u; +const maximumAgeMs = 5 * 60_000; + +const manifestKeys = [ + "baseReceiptFileSha256", + "baseReceiptPath", + "baseReceiptPayloadSha256", + "cgroupId", + "cgroupIdentity", + "cgroupPath", + "driverCliSha256", + "driverModuleSha256", + "finalContainerId", + "intendedAggregateLimits", + "invocationId", + "qualificationMode", + "receiptPayloadSha256", + "requestedAt", + "runtimeAttestationSha256", + "runtimeSessionId", + "sanitizedSpecSha256", + "schemaVersion", + "status", +]; + +function object(value, label) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`contained cgroup observer ${label} shape is invalid`); + } + return value; +} + +function exactKeys(value, keys, label) { + if ( + JSON.stringify(Object.keys(value).sort()) !== + JSON.stringify([...keys].sort()) + ) { + throw new Error(`contained cgroup observer ${label} shape is invalid`); + } +} + +function validTimestamp(value, observedAtMs) { + if (typeof value !== "string") return false; + const parsed = Date.parse(value); + return ( + Number.isFinite(parsed) && + new Date(parsed).toISOString() === value && + Math.abs(observedAtMs - parsed) <= maximumAgeMs + ); +} + +function validIntent(value) { + const intent = object(value, "aggregate intent"); + exactKeys( + intent, + ["cpuCount", "maxProcesses", "memoryBytes"], + "aggregate intent", + ); + if ( + typeof intent.cpuCount !== "number" || + !Number.isFinite(intent.cpuCount) || + intent.cpuCount < 0.25 || + intent.cpuCount > 2 || + !Number.isSafeInteger(intent.maxProcesses) || + intent.maxProcesses < 1 || + intent.maxProcesses > 32 || + !Number.isSafeInteger(intent.memoryBytes) || + intent.memoryBytes < 64 * 1024 * 1024 || + intent.memoryBytes > 1024 * 1024 * 1024 + ) { + throw new Error("contained cgroup observer aggregate intent is invalid"); + } + return intent; +} + +export function validateContainedCgroupObserverManifest( + value, + { observedAtMs = Date.now() } = {}, +) { + const manifest = object(value, "manifest"); + exactKeys(manifest, manifestKeys, "manifest"); + const intent = validIntent(manifest.intendedAggregateLimits); + const expectedReceiptPath = `.rt/${manifest.runtimeSessionId}/run/rootless-specs/${manifest.finalContainerId}.receipt.json`; + const { receiptPayloadSha256, ...payload } = manifest; + if ( + manifest.schemaVersion !== "1" || + manifest.status !== "REQUESTED" || + manifest.qualificationMode !== AGGREGATE_TIMEOUT_QUALIFICATION_MODE || + !sessionPattern.test(manifest.runtimeSessionId ?? "") || + !sha256Pattern.test(manifest.invocationId ?? "") || + !sha256Pattern.test(manifest.finalContainerId ?? "") || + !sha256Pattern.test(manifest.sanitizedSpecSha256 ?? "") || + !sha256Pattern.test(manifest.baseReceiptFileSha256 ?? "") || + !sha256Pattern.test(manifest.baseReceiptPayloadSha256 ?? "") || + !sha256Pattern.test(manifest.runtimeAttestationSha256 ?? "") || + !sha256Pattern.test(manifest.driverCliSha256 ?? "") || + !sha256Pattern.test(manifest.driverModuleSha256 ?? "") || + manifest.baseReceiptPath !== expectedReceiptPath || + manifest.cgroupId !== `counterlab-v6.1-${manifest.invocationId}` || + manifest.cgroupPath !== `counterlab-v6.1/${manifest.invocationId}` || + manifest.cgroupIdentity !== createContainedCgroupIdentity(manifest) || + !validTimestamp(manifest.requestedAt, observedAtMs) || + !sha256Pattern.test(receiptPayloadSha256 ?? "") || + sha256CgroupBytes(canonicalCgroupJson(payload)) !== receiptPayloadSha256 + ) { + throw new Error("contained cgroup observer manifest binding is invalid"); + } + return manifest; +} + +export function createContainedCgroupObserverManifest({ + baseReceiptFileSha256, + baseReceiptPayloadSha256, + finalContainerId, + intendedAggregateLimits, + invocationId, + observerBindings, + requestedAt = new Date(), + sanitizedSpecSha256, +}) { + if ( + !(requestedAt instanceof Date) || + !Number.isFinite(requestedAt.getTime()) + ) { + throw new Error("contained cgroup observer request time is invalid"); + } + const payload = { + schemaVersion: "1", + status: "REQUESTED", + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + runtimeSessionId: observerBindings?.runtimeSessionId, + invocationId, + finalContainerId, + cgroupId: `counterlab-v6.1-${invocationId}`, + cgroupPath: `counterlab-v6.1/${invocationId}`, + cgroupIdentity: createContainedCgroupIdentity({ + invocationId, + finalContainerId, + sanitizedSpecSha256, + }), + sanitizedSpecSha256, + intendedAggregateLimits, + baseReceiptPath: `.rt/${observerBindings?.runtimeSessionId}/run/rootless-specs/${finalContainerId}.receipt.json`, + baseReceiptFileSha256, + baseReceiptPayloadSha256, + runtimeAttestationSha256: observerBindings?.runtimeAttestationSha256, + driverCliSha256: observerBindings?.driverCliSha256, + driverModuleSha256: observerBindings?.driverModuleSha256, + requestedAt: requestedAt.toISOString(), + }; + return validateContainedCgroupObserverManifest( + { + ...payload, + receiptPayloadSha256: sha256CgroupBytes(canonicalCgroupJson(payload)), + }, + { observedAtMs: requestedAt.getTime() }, + ); +} + +export function containedCgroupQualificationPaths({ + repositoryRoot, + runtimeSessionId, + invocationId, +}) { + if ( + !isAbsolute(repositoryRoot) || + !sessionPattern.test(runtimeSessionId ?? "") || + !sha256Pattern.test(invocationId ?? "") + ) { + throw new Error("contained cgroup observer path input is invalid"); + } + const sessionRoot = resolve(repositoryRoot, ".rt", runtimeSessionId); + const qualificationRoot = resolve(sessionRoot, "run/cgroup-qualification"); + const invocationRoot = resolve(qualificationRoot, invocationId); + for (const candidate of [sessionRoot, qualificationRoot, invocationRoot]) { + const fromRoot = relative(repositoryRoot, candidate); + if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) { + throw new Error("contained cgroup observer path escaped the repository"); + } + } + return Object.freeze({ + evidencePath: resolve(invocationRoot, "aggregate-evidence.json"), + failurePath: resolve(invocationRoot, "failure.json"), + invocationRoot, + manifestPath: resolve(invocationRoot, "manifest.json"), + observerDraftPath: resolve(invocationRoot, "observer-draft.json"), + observerReadyPath: resolve(invocationRoot, "observer-ready.json"), + qualificationRoot, + sessionRoot, + }); +} diff --git a/scripts/contained-cgroup-observer-protocol.test.ts b/scripts/contained-cgroup-observer-protocol.test.ts new file mode 100644 index 00000000..718cc4af --- /dev/null +++ b/scripts/contained-cgroup-observer-protocol.test.ts @@ -0,0 +1,167 @@ +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { + containedCgroupQualificationPaths, + createContainedCgroupObserverManifest, + validateContainedCgroupObserverManifest, +} from "./contained-cgroup-observer-protocol.mjs"; + +const root = process.cwd(); +const runtimeSessionId = "rt-v61-test1"; +const invocationId = "1".repeat(64); +const finalContainerId = "2".repeat(64); +const sanitizedSpecSha256 = "3".repeat(64); +const requestedAt = new Date("2026-07-20T01:02:03.000Z"); + +function manifest() { + return createContainedCgroupObserverManifest({ + baseReceiptFileSha256: "4".repeat(64), + baseReceiptPayloadSha256: "5".repeat(64), + finalContainerId, + intendedAggregateLimits: { + cpuCount: 1, + maxProcesses: 16, + memoryBytes: 512 * 1024 * 1024, + }, + invocationId, + observerBindings: { + runtimeSessionId, + runtimeAttestationSha256: "6".repeat(64), + driverCliSha256: "7".repeat(64), + driverModuleSha256: "8".repeat(64), + }, + requestedAt, + sanitizedSpecSha256, + }); +} + +describe("contained cgroup observer protocol", () => { + it("creates a source-, session-, base-receipt-, and cgroup-bound request", () => { + const value = manifest(); + + expect( + validateContainedCgroupObserverManifest(value, { + observedAtMs: requestedAt.getTime(), + }), + ).toEqual(value); + expect(value.baseReceiptPath).toBe( + `.rt/${runtimeSessionId}/run/rootless-specs/${finalContainerId}.receipt.json`, + ); + expect(value.cgroupPath).toBe(`counterlab-v6.1/${invocationId}`); + expect(value.receiptPayloadSha256).toMatch(/^[a-f0-9]{64}$/u); + }); + + it("rejects unknown, stale, and drifted requests", () => { + const mutations: Array<(value: ReturnType) => void> = [ + (value) => { + value.finalContainerId = "9".repeat(64); + }, + (value) => { + value.baseReceiptPath = ".rt/escaped/receipt.json"; + }, + (value) => { + value.intendedAggregateLimits.memoryBytes += 1; + }, + (value) => { + value.driverCliSha256 = "a".repeat(64); + }, + (value) => { + value.receiptPayloadSha256 = "b".repeat(64); + }, + ]; + for (const mutate of mutations) { + const changed = structuredClone(manifest()); + mutate(changed); + expect(() => + validateContainedCgroupObserverManifest(changed, { + observedAtMs: requestedAt.getTime(), + }), + ).toThrow(/observer manifest/u); + } + expect(() => + validateContainedCgroupObserverManifest( + { ...manifest(), unknown: true }, + { observedAtMs: requestedAt.getTime() }, + ), + ).toThrow(/shape/u); + expect(() => + validateContainedCgroupObserverManifest(manifest(), { + observedAtMs: requestedAt.getTime() + 5 * 60_000 + 1, + }), + ).toThrow(/binding/u); + }); + + it("derives only exact repository-contained handshake paths", () => { + expect( + containedCgroupQualificationPaths({ + repositoryRoot: root, + runtimeSessionId, + invocationId, + }), + ).toEqual({ + sessionRoot: resolve(root, ".rt", runtimeSessionId), + qualificationRoot: resolve( + root, + ".rt", + runtimeSessionId, + "run/cgroup-qualification", + ), + invocationRoot: resolve( + root, + ".rt", + runtimeSessionId, + "run/cgroup-qualification", + invocationId, + ), + manifestPath: resolve( + root, + ".rt", + runtimeSessionId, + "run/cgroup-qualification", + invocationId, + "manifest.json", + ), + observerReadyPath: resolve( + root, + ".rt", + runtimeSessionId, + "run/cgroup-qualification", + invocationId, + "observer-ready.json", + ), + observerDraftPath: resolve( + root, + ".rt", + runtimeSessionId, + "run/cgroup-qualification", + invocationId, + "observer-draft.json", + ), + evidencePath: resolve( + root, + ".rt", + runtimeSessionId, + "run/cgroup-qualification", + invocationId, + "aggregate-evidence.json", + ), + failurePath: resolve( + root, + ".rt", + runtimeSessionId, + "run/cgroup-qualification", + invocationId, + "failure.json", + ), + }); + expect(() => + containedCgroupQualificationPaths({ + repositoryRoot: resolve(root, ".rt", ".."), + runtimeSessionId: "bad", + invocationId, + }), + ).toThrow(/path input/u); + }); +}); diff --git a/scripts/contained-runtime-attestation.mjs b/scripts/contained-runtime-attestation.mjs index faafc247..03a0801f 100644 --- a/scripts/contained-runtime-attestation.mjs +++ b/scripts/contained-runtime-attestation.mjs @@ -15,6 +15,7 @@ export const RUNTIME_HELPER_PATHS = Object.freeze({ cgroupControlHelper: "scripts/contained-cgroup-control-helper.mjs", cgroupEvidence: "scripts/contained-cgroup-evidence.mjs", cgroupObserverBindings: "scripts/contained-cgroup-observer-bindings.mjs", + cgroupObserverProtocol: "scripts/contained-cgroup-observer-protocol.mjs", qualifiedRootlessReceiptStore: "scripts/contained-qualified-receipt-store.mjs", qualifiedRootlessReceipt: "scripts/contained-qualified-rootless-receipt.mjs", From 771d8bf8f8a33d4c35433d1db1ea4f288ba5e3fc Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:50:01 +0530 Subject: [PATCH 042/143] feat(runtime): observe aggregate cgroup controls --- scripts/contained-cgroup-observer.d.mts | 60 ++ scripts/contained-cgroup-observer.mjs | 942 ++++++++++++++++++++++ scripts/contained-cgroup-observer.test.ts | 267 ++++++ scripts/contained-runtime-attestation.mjs | 1 + 4 files changed, 1270 insertions(+) create mode 100644 scripts/contained-cgroup-observer.d.mts create mode 100644 scripts/contained-cgroup-observer.mjs create mode 100644 scripts/contained-cgroup-observer.test.ts diff --git a/scripts/contained-cgroup-observer.d.mts b/scripts/contained-cgroup-observer.d.mts new file mode 100644 index 00000000..0577ca25 --- /dev/null +++ b/scripts/contained-cgroup-observer.d.mts @@ -0,0 +1,60 @@ +import type { ContainedCgroupObserverManifest } from "./contained-cgroup-observer-protocol.mjs"; + +export function createContainedCgroupObserverReady( + manifest: ContainedCgroupObserverManifest, + options?: { armedAt?: Date; observerPid?: number }, +): Record; + +export function validateContainedCgroupObserverReady( + value: T, + manifest: ContainedCgroupObserverManifest, +): T; + +export function createContainedCgroupObserverFailure( + manifest: ContainedCgroupObserverManifest, + options?: { failedAt?: Date }, +): Record; + +export function validateContainedCgroupObserverFailure( + value: T, + manifest: ContainedCgroupObserverManifest, +): T; + +export function createContainedCgroupObserverDraft( + manifest: ContainedCgroupObserverManifest, + observation: Record, +): Record; + +export function validateContainedCgroupObserverDraft( + value: T, + manifest: ContainedCgroupObserverManifest, +): T; + +export function parseContainedCgroupKeyValues( + source: string, +): Record; +export function parseContainedCgroupMembers(source: string): number[]; +export function parseContainedProcessStat( + source: string, + expectedPid: number, +): { pid: number; parentPid: number; startTimeTicks: string }; +export function selectContainedCgroupMembership( + memberPids: number[], + processStats: Map< + number, + { pid: number; parentPid: number; startTimeTicks: string } + >, +): Record; + +export function observeContainedCgroup( + manifest: ContainedCgroupObserverManifest, + adapter: { + now(): Date; + waitForCgroup(): Promise; + readCgroupFile(name: string): string | Promise; + readProcessStat(pid: number): string | Promise; + runControl(control: Record): Promise; + publishDraft(draft: Record): Promise; + waitForCgroupAbsent(): Promise; + }, +): Promise>; diff --git a/scripts/contained-cgroup-observer.mjs b/scripts/contained-cgroup-observer.mjs new file mode 100644 index 00000000..ccbd3ab0 --- /dev/null +++ b/scripts/contained-cgroup-observer.mjs @@ -0,0 +1,942 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import { + existsSync, + lstatSync, + readFileSync, + realpathSync, + statfsSync, + writeFileSync, +} from "node:fs"; +import { isAbsolute, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + canonicalCgroupJson, + sha256CgroupBytes, + validateContainedCgroupEvidence, +} from "./contained-cgroup-evidence.mjs"; +import { resolveContainedCgroupObserverBindings } from "./contained-cgroup-observer-bindings.mjs"; +import { + containedCgroupQualificationPaths, + validateContainedCgroupObserverManifest, +} from "./contained-cgroup-observer-protocol.mjs"; + +const repositoryRoot = realpathSync( + resolve(fileURLToPath(import.meta.url), "../.."), +); +const cgroupFilesystemRoot = "/sys/fs/cgroup"; +const cgroup2Magic = 0x63677270; +const maximumArtifactBytes = 1_048_576; +const allowedCgroupFiles = new Set([ + "cgroup.procs", + "cpu.max", + "cpu.stat", + "memory.events", + "memory.max", + "memory.swap.max", + "pids.events", + "pids.max", +]); + +function object(value, label) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`contained cgroup observer ${label} shape is invalid`); + } + return value; +} + +function exactKeys(value, keys, label) { + if ( + JSON.stringify(Object.keys(value).sort()) !== + JSON.stringify([...keys].sort()) + ) { + throw new Error(`contained cgroup observer ${label} shape is invalid`); + } +} + +function hashedArtifact(payload) { + return { + ...payload, + receiptPayloadSha256: sha256CgroupBytes(canonicalCgroupJson(payload)), + }; +} + +function validTimestamp(value) { + if (typeof value !== "string") return false; + const parsed = Date.parse(value); + return Number.isFinite(parsed) && new Date(parsed).toISOString() === value; +} + +export function createContainedCgroupObserverReady( + manifest, + { armedAt = new Date(), observerPid = process.pid } = {}, +) { + if ( + !(armedAt instanceof Date) || + !Number.isFinite(armedAt.getTime()) || + !Number.isSafeInteger(observerPid) || + observerPid < 1 + ) { + throw new Error("contained cgroup observer ready input is invalid"); + } + const payload = { + schemaVersion: "1", + status: "ARMED", + manifestPayloadSha256: manifest.receiptPayloadSha256, + invocationId: manifest.invocationId, + finalContainerId: manifest.finalContainerId, + observerPid, + armedAt: armedAt.toISOString(), + }; + return validateContainedCgroupObserverReady( + hashedArtifact(payload), + manifest, + ); +} + +export function validateContainedCgroupObserverReady(value, manifest) { + const ready = object(value, "ready receipt"); + exactKeys( + ready, + [ + "armedAt", + "finalContainerId", + "invocationId", + "manifestPayloadSha256", + "observerPid", + "receiptPayloadSha256", + "schemaVersion", + "status", + ], + "ready receipt", + ); + const { receiptPayloadSha256, ...payload } = ready; + if ( + ready.schemaVersion !== "1" || + ready.status !== "ARMED" || + ready.manifestPayloadSha256 !== manifest.receiptPayloadSha256 || + ready.invocationId !== manifest.invocationId || + ready.finalContainerId !== manifest.finalContainerId || + !Number.isSafeInteger(ready.observerPid) || + ready.observerPid < 1 || + !validTimestamp(ready.armedAt) || + !/^[a-f0-9]{64}$/u.test(receiptPayloadSha256 ?? "") || + sha256CgroupBytes(canonicalCgroupJson(payload)) !== receiptPayloadSha256 + ) { + throw new Error("contained cgroup observer ready binding is invalid"); + } + return ready; +} + +export function createContainedCgroupObserverFailure( + manifest, + { failedAt = new Date() } = {}, +) { + if (!(failedAt instanceof Date) || !Number.isFinite(failedAt.getTime())) { + throw new Error("contained cgroup observer failure input is invalid"); + } + const payload = { + schemaVersion: "1", + status: "FAILED", + code: "OBSERVATION_FAILED", + manifestPayloadSha256: manifest.receiptPayloadSha256, + invocationId: manifest.invocationId, + finalContainerId: manifest.finalContainerId, + failedAt: failedAt.toISOString(), + }; + return validateContainedCgroupObserverFailure( + hashedArtifact(payload), + manifest, + ); +} + +export function validateContainedCgroupObserverFailure(value, manifest) { + const failure = object(value, "failure receipt"); + exactKeys( + failure, + [ + "code", + "failedAt", + "finalContainerId", + "invocationId", + "manifestPayloadSha256", + "receiptPayloadSha256", + "schemaVersion", + "status", + ], + "failure receipt", + ); + const { receiptPayloadSha256, ...payload } = failure; + if ( + failure.schemaVersion !== "1" || + failure.status !== "FAILED" || + failure.code !== "OBSERVATION_FAILED" || + failure.manifestPayloadSha256 !== manifest.receiptPayloadSha256 || + failure.invocationId !== manifest.invocationId || + failure.finalContainerId !== manifest.finalContainerId || + !validTimestamp(failure.failedAt) || + !/^[a-f0-9]{64}$/u.test(receiptPayloadSha256 ?? "") || + sha256CgroupBytes(canonicalCgroupJson(payload)) !== receiptPayloadSha256 + ) { + throw new Error("contained cgroup observer failure binding is invalid"); + } + return failure; +} + +function expectedEvidence(manifest) { + return { + invocationId: manifest.invocationId, + finalContainerId: manifest.finalContainerId, + sanitizedSpecSha256: manifest.sanitizedSpecSha256, + intendedAggregateLimits: manifest.intendedAggregateLimits, + runtimeAttestationSha256: manifest.runtimeAttestationSha256, + runtimeSessionId: manifest.runtimeSessionId, + driverCliSha256: manifest.driverCliSha256, + driverModuleSha256: manifest.driverModuleSha256, + }; +} + +function evidencePayload(manifest, observation) { + return { + schemaVersion: "1", + status: "OBSERVED", + authority: "linux-cgroup-v2", + cgroupVersion: 2, + cgroupId: manifest.cgroupId, + cgroupPath: manifest.cgroupPath, + cgroupIdentity: manifest.cgroupIdentity, + invocationId: manifest.invocationId, + finalContainerId: manifest.finalContainerId, + sanitizedSpecSha256: manifest.sanitizedSpecSha256, + runtimeAttestationSha256: manifest.runtimeAttestationSha256, + observedLimits: observation.observedLimits, + membership: observation.membership, + negativeControls: observation.negativeControls, + cleanup: { cgroupAbsentAfterTimeout: true }, + observer: { + runtimeSessionId: manifest.runtimeSessionId, + driverCliSha256: manifest.driverCliSha256, + driverModuleSha256: manifest.driverModuleSha256, + }, + observedAt: observation.observedAt, + }; +} + +export function createContainedCgroupObserverDraft(manifest, observation) { + const finalEvidence = validateContainedCgroupEvidence( + hashedArtifact(evidencePayload(manifest, observation)), + expectedEvidence(manifest), + ); + const payload = { + schemaVersion: "1", + status: "OBSERVED_ACTIVE", + manifestPayloadSha256: manifest.receiptPayloadSha256, + invocationId: manifest.invocationId, + finalContainerId: manifest.finalContainerId, + candidateLeaderPresentAfterControls: true, + observedLimits: finalEvidence.observedLimits, + membership: finalEvidence.membership, + negativeControls: finalEvidence.negativeControls, + observedAt: finalEvidence.observedAt, + }; + return validateContainedCgroupObserverDraft( + hashedArtifact(payload), + manifest, + ); +} + +export function validateContainedCgroupObserverDraft(value, manifest) { + const draft = object(value, "draft receipt"); + exactKeys( + draft, + [ + "candidateLeaderPresentAfterControls", + "finalContainerId", + "invocationId", + "manifestPayloadSha256", + "membership", + "negativeControls", + "observedAt", + "observedLimits", + "receiptPayloadSha256", + "schemaVersion", + "status", + ], + "draft receipt", + ); + const { receiptPayloadSha256, ...payload } = draft; + if ( + draft.schemaVersion !== "1" || + draft.status !== "OBSERVED_ACTIVE" || + draft.manifestPayloadSha256 !== manifest.receiptPayloadSha256 || + draft.invocationId !== manifest.invocationId || + draft.finalContainerId !== manifest.finalContainerId || + draft.candidateLeaderPresentAfterControls !== true || + !validTimestamp(draft.observedAt) || + !/^[a-f0-9]{64}$/u.test(receiptPayloadSha256 ?? "") || + sha256CgroupBytes(canonicalCgroupJson(payload)) !== receiptPayloadSha256 + ) { + throw new Error("contained cgroup observer draft binding is invalid"); + } + validateContainedCgroupEvidence( + hashedArtifact(evidencePayload(manifest, draft)), + expectedEvidence(manifest), + ); + return draft; +} + +function positiveInteger(value, label) { + if (!/^[1-9][0-9]*$/u.test(value)) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } + const parsed = Number.parseInt(value, 10); + if (!Number.isSafeInteger(parsed)) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } + return parsed; +} + +function nonnegativeInteger(value, label) { + if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } + const parsed = Number.parseInt(value, 10); + if (!Number.isSafeInteger(parsed)) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } + return parsed; +} + +export function parseContainedCgroupKeyValues(source) { + if ( + typeof source !== "string" || + source.length === 0 || + source.length > 8_192 + ) { + throw new Error("contained cgroup observer counter source is invalid"); + } + const values = {}; + for (const line of source.trim().split("\n")) { + const match = line.match(/^([a-z_]+) ([0-9]+)$/u); + if (match === null || Object.hasOwn(values, match[1])) { + throw new Error("contained cgroup observer counter source is invalid"); + } + values[match[1]] = nonnegativeInteger(match[2], "counter value"); + } + return values; +} + +export function parseContainedCgroupMembers(source) { + if ( + typeof source !== "string" || + source.length === 0 || + source.length > 8_192 + ) { + throw new Error("contained cgroup observer membership source is invalid"); + } + const members = source + .trim() + .split("\n") + .map((entry) => positiveInteger(entry, "member PID")) + .sort((left, right) => left - right); + if ( + members.length < 2 || + members.length > 64 || + new Set(members).size !== members.length + ) { + throw new Error("contained cgroup observer membership is invalid"); + } + return members; +} + +export function parseContainedProcessStat(source, expectedPid) { + if ( + typeof source !== "string" || + source.length === 0 || + source.length > 8_192 + ) { + throw new Error("contained cgroup observer process stat is invalid"); + } + const separator = source.lastIndexOf(") "); + const opening = source.indexOf(" ("); + if (opening < 1 || separator <= opening) { + throw new Error("contained cgroup observer process stat is invalid"); + } + const pid = positiveInteger(source.slice(0, opening), "process PID"); + const fields = source + .slice(separator + 2) + .trim() + .split(/\s+/u); + if ( + pid !== expectedPid || + fields.length < 20 || + !/^[A-Z]$/u.test(fields[0]) + ) { + throw new Error("contained cgroup observer process stat is invalid"); + } + return { + pid, + parentPid: nonnegativeInteger(fields[1], "parent PID"), + startTimeTicks: String(positiveInteger(fields[19], "process start time")), + }; +} + +export function selectContainedCgroupMembership(memberPids, processStats) { + const members = new Set(memberPids); + if ( + !(processStats instanceof Map) || + processStats.size !== members.size || + [...members].some((pid) => processStats.get(pid)?.pid !== pid) + ) { + throw new Error("contained cgroup observer process membership is invalid"); + } + const roots = memberPids.filter( + (pid) => !members.has(processStats.get(pid).parentPid), + ); + if (roots.length !== 1) { + throw new Error("contained cgroup observer ancestry is ambiguous"); + } + const leaderPid = roots[0]; + const descendantsObserved = memberPids.some((pid) => { + if (pid === leaderPid) return false; + const visited = new Set(); + let current = pid; + while (members.has(current) && !visited.has(current)) { + if (current === leaderPid) return true; + visited.add(current); + current = processStats.get(current).parentPid; + } + return false; + }); + if (!descendantsObserved) { + throw new Error("contained cgroup observer found no candidate descendant"); + } + return { + leaderPid, + leaderStartTimeTicks: processStats.get(leaderPid).startTimeTicks, + memberPids, + memberSetSha256: sha256CgroupBytes(canonicalCgroupJson(memberPids)), + descendantsObserved: true, + }; +} + +function scalar(source, label) { + return positiveInteger(String(source).trim(), label); +} + +function swapScalar(source) { + const value = String(source).trim(); + return value === "0" ? 0 : positiveInteger(value, "memory swap limit"); +} + +function cpuLimit(source) { + const match = String(source) + .trim() + .match(/^([1-9][0-9]*) ([1-9][0-9]*)$/u); + if (match === null) { + throw new Error("contained cgroup observer CPU limit is invalid"); + } + return { + cpuQuotaMicros: positiveInteger(match[1], "CPU quota"), + cpuPeriodMicros: positiveInteger(match[2], "CPU period"), + }; +} + +async function stableMembership(adapter) { + const memberPids = parseContainedCgroupMembers( + await adapter.readCgroupFile("cgroup.procs"), + ); + const processStats = new Map(); + for (const pid of memberPids) { + processStats.set( + pid, + parseContainedProcessStat(await adapter.readProcessStat(pid), pid), + ); + } + const membership = selectContainedCgroupMembership(memberPids, processStats); + return { memberPids, membership, processStats }; +} + +async function assertCandidateSurvived(adapter, initial) { + const current = new Set( + parseContainedCgroupMembers(await adapter.readCgroupFile("cgroup.procs")), + ); + for (const pid of initial.memberPids) { + if (!current.has(pid)) { + throw new Error("contained cgroup observer candidate membership changed"); + } + const observed = parseContainedProcessStat( + await adapter.readProcessStat(pid), + pid, + ); + if ( + observed.startTimeTicks !== initial.processStats.get(pid).startTimeTicks + ) { + throw new Error("contained cgroup observer candidate PID was reused"); + } + } +} + +export async function observeContainedCgroup(manifest, adapter) { + await adapter.waitForCgroup(); + const observedLimits = { + memoryMaxBytes: scalar( + await adapter.readCgroupFile("memory.max"), + "memory limit", + ), + memorySwapMaxBytes: swapScalar( + await adapter.readCgroupFile("memory.swap.max"), + ), + pidsMax: scalar(await adapter.readCgroupFile("pids.max"), "PID limit"), + ...cpuLimit(await adapter.readCgroupFile("cpu.max")), + }; + const initial = await stableMembership(adapter); + + const cpuBefore = parseContainedCgroupKeyValues( + await adapter.readCgroupFile("cpu.stat"), + ); + await adapter.runControl({ mode: "cpu", busyWindowMs: 500, workers: 4 }); + const cpuAfter = parseContainedCgroupKeyValues( + await adapter.readCgroupFile("cpu.stat"), + ); + + const processesBefore = parseContainedCgroupKeyValues( + await adapter.readCgroupFile("pids.events"), + ); + const attemptedProcesses = observedLimits.pidsMax + 1; + await adapter.runControl({ mode: "processes", attemptedProcesses }); + const processesAfter = parseContainedCgroupKeyValues( + await adapter.readCgroupFile("pids.events"), + ); + + const memoryBefore = parseContainedCgroupKeyValues( + await adapter.readCgroupFile("memory.events"), + ); + const requestedBytes = observedLimits.memoryMaxBytes + 64 * 1024 * 1024; + await adapter.runControl({ mode: "memory", requestedBytes }); + const memoryAfter = parseContainedCgroupKeyValues( + await adapter.readCgroupFile("memory.events"), + ); + + await assertCandidateSurvived(adapter, initial); + const observedAt = adapter.now().toISOString(); + const observation = { + observedLimits, + membership: initial.membership, + negativeControls: { + memory: { + requestedBytes, + oomKillBefore: memoryBefore.oom_kill, + oomKillAfter: memoryAfter.oom_kill, + enforced: memoryAfter.oom_kill > memoryBefore.oom_kill, + }, + processes: { + attemptedProcesses, + maxEventsBefore: processesBefore.max, + maxEventsAfter: processesAfter.max, + enforced: processesAfter.max > processesBefore.max, + }, + cpu: { + busyWindowMs: 500, + nrThrottledBefore: cpuBefore.nr_throttled, + nrThrottledAfter: cpuAfter.nr_throttled, + throttledUsecBefore: cpuBefore.throttled_usec, + throttledUsecAfter: cpuAfter.throttled_usec, + enforced: + cpuAfter.nr_throttled > cpuBefore.nr_throttled && + cpuAfter.throttled_usec > cpuBefore.throttled_usec, + }, + }, + observedAt, + }; + const draft = createContainedCgroupObserverDraft(manifest, observation); + await adapter.publishDraft(draft); + await adapter.waitForCgroupAbsent(); + return validateContainedCgroupEvidence( + hashedArtifact(evidencePayload(manifest, observation)), + expectedEvidence(manifest), + ); +} + +function privateDirectory(path, label) { + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isDirectory() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() || + (metadata.mode & 0o777) !== 0o700 + ) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } +} + +function privateFile(path, label) { + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isFile() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() || + metadata.nlink !== 1 || + (metadata.mode & 0o777) !== 0o600 || + metadata.size < 1 || + metadata.size > maximumArtifactBytes + ) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } +} + +function writeArtifact(path, value) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { + encoding: "utf8", + flag: "wx", + mode: 0o600, + }); + privateFile(path, "artifact"); +} + +function sleep(milliseconds) { + return new Promise((accept) => setTimeout(accept, milliseconds)); +} + +async function waitUntil(predicate, label, timeoutMs) { + const deadline = Date.now() + timeoutMs; + while (Date.now() <= deadline) { + if (predicate()) return; + await sleep(10); + } + throw new Error(`contained cgroup observer ${label} timed out`); +} + +function actualCgroupAdapter(manifest, paths) { + const cgroupRoot = resolve(cgroupFilesystemRoot, manifest.cgroupPath); + if ( + resolve( + cgroupFilesystemRoot, + relative(cgroupFilesystemRoot, cgroupRoot), + ) !== cgroupRoot || + statfsSync(cgroupFilesystemRoot).type !== cgroup2Magic + ) { + throw new Error("contained cgroup observer cgroup v2 root is invalid"); + } + const helperPath = resolve( + repositoryRoot, + "scripts/contained-cgroup-control-helper.mjs", + ); + + function cgroupFile(name) { + if (!allowedCgroupFiles.has(name)) { + throw new Error("contained cgroup observer file is not allowlisted"); + } + const path = resolve(cgroupRoot, name); + if (resolve(cgroupRoot, name) !== path) { + throw new Error("contained cgroup observer file path is invalid"); + } + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isFile() || + realpathSync(path) !== path + ) { + throw new Error("contained cgroup observer file is invalid"); + } + return path; + } + + async function runControl(control) { + const args = + control.mode === "memory" + ? [ + "--mode", + "memory", + "--requested-bytes", + String(control.requestedBytes), + ] + : control.mode === "processes" + ? [ + "--mode", + "processes", + "--attempted-processes", + String(control.attemptedProcesses), + ] + : [ + "--mode", + "cpu", + "--busy-window-ms", + String(control.busyWindowMs), + "--workers", + String(control.workers), + ]; + const child = spawn(process.execPath, [helperPath, ...args], { + cwd: repositoryRoot, + env: process.env, + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + let outputInvalid = false; + const completion = new Promise((accept) => { + child.once("error", (error) => + accept({ code: null, signal: null, error }), + ); + child.once("close", (code, signal) => accept({ code, signal })); + }); + const ready = new Promise((accept, reject) => { + child.stdout.on("data", (chunk) => { + stdout += chunk.toString("utf8"); + if (stdout.length > 8_192) { + outputInvalid = true; + reject(new Error("helper output exceeded bound")); + } + if (stdout.startsWith("READY\n")) accept(); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk.toString("utf8"); + if (stderr.length > 8_192) { + outputInvalid = true; + reject(new Error("helper error exceeded bound")); + } + }); + child.once("close", () => + reject(new Error("helper exited before ready")), + ); + child.once("error", reject); + }); + const bounded = async (promise, timeoutMs, label) => { + let timer; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout( + () => + reject( + new Error(`contained cgroup observer ${label} timed out`), + ), + timeoutMs, + ); + }), + ]); + } finally { + clearTimeout(timer); + } + }; + try { + await bounded(ready, 5_000, "helper readiness"); + if (!Number.isSafeInteger(child.pid) || child.pid < 1 || outputInvalid) { + throw new Error("contained cgroup observer helper PID is invalid"); + } + writeFileSync(cgroupFile("cgroup.procs"), `${child.pid}\n`, "utf8"); + child.stdin.end("GO\n"); + const result = await bounded(completion, 15_000, "helper completion"); + if ( + result.error !== undefined || + outputInvalid || + (control.mode !== "memory" && result.code !== 0) || + (control.mode === "memory" && result.code === 0) + ) { + throw new Error("contained cgroup observer helper result is invalid"); + } + if (control.mode === "processes") { + const lines = stdout.trim().split("\n"); + let report; + try { + report = JSON.parse(lines.at(-1)); + } catch (error) { + throw new Error( + "contained cgroup observer process report is invalid", + { + cause: error, + }, + ); + } + if ( + report?.attemptedProcesses !== control.attemptedProcesses || + report?.denied !== true + ) { + throw new Error( + "contained cgroup observer process report is invalid", + ); + } + } + } finally { + if (child.exitCode === null && child.signalCode === null) { + child.kill("SIGTERM"); + } + child.stdin.destroy(); + try { + await bounded(completion, 2_000, "helper reap"); + } catch { + if (child.exitCode === null && child.signalCode === null) { + child.kill("SIGKILL"); + } + await bounded(completion, 2_000, "forced helper reap"); + } + } + } + + return { + now: () => new Date(), + async waitForCgroup() { + await waitUntil(() => existsSync(cgroupRoot), "cgroup creation", 10_000); + if ( + lstatSync(cgroupRoot).isSymbolicLink() || + !lstatSync(cgroupRoot).isDirectory() || + realpathSync(cgroupRoot) !== cgroupRoot + ) { + throw new Error("contained cgroup observer cgroup path is invalid"); + } + await waitUntil( + () => { + try { + return ( + parseContainedCgroupMembers( + readFileSync(cgroupFile("cgroup.procs"), "utf8"), + ).length >= 2 + ); + } catch { + return false; + } + }, + "candidate membership", + 10_000, + ); + }, + readCgroupFile(name) { + const path = cgroupFile(name); + const source = readFileSync(path, "utf8"); + if (source.length === 0 || source.length > 8_192) { + throw new Error("contained cgroup observer cgroup file is invalid"); + } + return source; + }, + readProcessStat(pid) { + return readFileSync(`/proc/${pid}/stat`, "utf8"); + }, + runControl, + publishDraft(draft) { + writeArtifact(paths.observerDraftPath, draft); + }, + async waitForCgroupAbsent() { + await waitUntil(() => !existsSync(cgroupRoot), "cgroup cleanup", 30_000); + }, + }; +} + +function parseMainArguments(args) { + if ( + args.length !== 4 || + args[0] !== "--session-id" || + args[2] !== "--manifest" || + !/^rt-[a-z0-9][a-z0-9-]{7,13}$/u.test(args[1] ?? "") || + !isAbsolute(args[3] ?? "") + ) { + throw new Error("contained cgroup observer arguments are invalid"); + } + return { runtimeSessionId: args[1], manifestPath: args[3] }; +} + +async function main() { + const args = parseMainArguments(process.argv.slice(2)); + privateDirectory(resolve(repositoryRoot, ".rt"), "runtime root"); + const sessionRoot = resolve(repositoryRoot, ".rt", args.runtimeSessionId); + const manifestRelative = relative(repositoryRoot, args.manifestPath); + const manifestMatch = manifestRelative.match( + new RegExp( + `^\\.rt/${args.runtimeSessionId}/run/cgroup-qualification/([a-f0-9]{64})/manifest\\.json$`, + "u", + ), + ); + if (manifestMatch === null || isAbsolute(manifestRelative)) { + throw new Error("contained cgroup observer manifest path is not exact"); + } + const paths = containedCgroupQualificationPaths({ + repositoryRoot, + runtimeSessionId: args.runtimeSessionId, + invocationId: manifestMatch[1], + }); + if (args.manifestPath !== paths.manifestPath) { + throw new Error("contained cgroup observer manifest path is not exact"); + } + const observerBindings = resolveContainedCgroupObserverBindings({ + sessionRoot, + }); + privateFile(args.manifestPath, "manifest"); + const manifest = validateContainedCgroupObserverManifest( + JSON.parse(readFileSync(args.manifestPath, "utf8")), + ); + try { + if ( + manifest.invocationId !== manifestMatch[1] || + manifest.runtimeSessionId !== args.runtimeSessionId || + Object.entries(observerBindings).some( + ([name, value]) => manifest[name] !== value, + ) + ) { + throw new Error("contained cgroup observer live binding changed"); + } + privateDirectory(paths.qualificationRoot, "qualification root"); + privateDirectory(paths.invocationRoot, "invocation root"); + const baseReceiptPath = resolve(repositoryRoot, manifest.baseReceiptPath); + privateFile(baseReceiptPath, "base receipt"); + const baseReceiptSource = readFileSync(baseReceiptPath); + const baseReceipt = JSON.parse(baseReceiptSource.toString("utf8")); + const { + receiptPayloadSha256: baseReceiptPayloadSha256, + ...baseReceiptPayload + } = baseReceipt; + if ( + sha256CgroupBytes(baseReceiptSource) !== manifest.baseReceiptFileSha256 || + baseReceiptPayloadSha256 !== manifest.baseReceiptPayloadSha256 || + sha256CgroupBytes(canonicalCgroupJson(baseReceiptPayload)) !== + baseReceiptPayloadSha256 || + baseReceipt?.schemaVersion !== "4" || + baseReceipt?.status !== "VALIDATED" || + baseReceipt?.limitMode !== + "process-address-space-rlimit-with-unenforced-cgroup-intent" || + baseReceipt?.aggregateLimitIntentEnforced !== false || + baseReceipt?.aggregateLimitEvidence !== null || + baseReceipt?.finalContainerId !== manifest.finalContainerId || + baseReceipt?.invocationId !== manifest.invocationId || + baseReceipt?.sanitizedSpecSha256 !== manifest.sanitizedSpecSha256 || + canonicalCgroupJson(baseReceipt?.intendedAggregateLimits) !== + canonicalCgroupJson(manifest.intendedAggregateLimits) + ) { + throw new Error("contained cgroup observer base receipt changed"); + } + const adapter = actualCgroupAdapter(manifest, paths); + writeArtifact( + paths.observerReadyPath, + createContainedCgroupObserverReady(manifest), + ); + const evidence = await observeContainedCgroup(manifest, adapter); + writeArtifact(paths.evidencePath, evidence); + } catch (error) { + if (!existsSync(paths.failurePath)) { + try { + writeArtifact( + paths.failurePath, + createContainedCgroupObserverFailure(manifest), + ); + } catch { + // Preserve the original fail-closed observation error. + } + } + throw error; + } +} + +if ( + process.argv[1] !== undefined && + realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)) +) { + main().catch((error) => { + process.stderr.write( + `${error instanceof Error ? error.message : "contained cgroup observer failed"}\n`, + ); + process.exitCode = 70; + }); +} diff --git a/scripts/contained-cgroup-observer.test.ts b/scripts/contained-cgroup-observer.test.ts new file mode 100644 index 00000000..0486970d --- /dev/null +++ b/scripts/contained-cgroup-observer.test.ts @@ -0,0 +1,267 @@ +import { describe, expect, it } from "vitest"; + +import { + canonicalCgroupJson, + sha256CgroupBytes, + validateContainedCgroupEvidence, +} from "./contained-cgroup-evidence.mjs"; +import { + createContainedCgroupObserverDraft, + createContainedCgroupObserverFailure, + createContainedCgroupObserverReady, + observeContainedCgroup, + parseContainedCgroupKeyValues, + parseContainedCgroupMembers, + parseContainedProcessStat, + selectContainedCgroupMembership, + validateContainedCgroupObserverDraft, + validateContainedCgroupObserverFailure, + validateContainedCgroupObserverReady, +} from "./contained-cgroup-observer.mjs"; +import { createContainedCgroupObserverManifest } from "./contained-cgroup-observer-protocol.mjs"; + +const runtimeSessionId = "rt-v61-test1"; +const invocationId = "1".repeat(64); +const finalContainerId = "2".repeat(64); +const sanitizedSpecSha256 = "3".repeat(64); +const now = new Date("2026-07-20T01:02:03.000Z"); + +function manifest() { + return createContainedCgroupObserverManifest({ + baseReceiptFileSha256: "4".repeat(64), + baseReceiptPayloadSha256: "5".repeat(64), + finalContainerId, + intendedAggregateLimits: { + cpuCount: 1, + maxProcesses: 16, + memoryBytes: 512 * 1024 * 1024, + }, + invocationId, + observerBindings: { + runtimeSessionId, + runtimeAttestationSha256: "6".repeat(64), + driverCliSha256: "7".repeat(64), + driverModuleSha256: "8".repeat(64), + }, + requestedAt: now, + sanitizedSpecSha256, + }); +} + +function processStat(pid: number, parentPid: number, startTime: number) { + const fields = [ + "S", + String(parentPid), + ...Array.from({ length: 17 }, () => "0"), + String(startTime), + ]; + return `${pid} (counterlab worker) ${fields.join(" ")}\n`; +} + +function adapter(overrides: Record = {}) { + const counters = { + cpu: { nr_throttled: 0, throttled_usec: 0 }, + processes: { max: 0 }, + memory: { oom_kill: 0 }, + }; + const controls: Array> = []; + const drafts: Array> = []; + return { + controls, + drafts, + now: () => now, + waitForCgroup: async () => undefined, + readCgroupFile(name: string) { + const values: Record = { + "memory.max": String(512 * 1024 * 1024), + "memory.swap.max": "0", + "pids.max": "16", + "cpu.max": "100000 100000", + "cgroup.procs": "100\n101\n", + "cpu.stat": `usage_usec 10\nnr_throttled ${counters.cpu.nr_throttled}\nthrottled_usec ${counters.cpu.throttled_usec}\n`, + "pids.events": `max ${counters.processes.max}\n`, + "memory.events": `low 0\noom_kill ${counters.memory.oom_kill}\n`, + }; + const value = values[name]; + if (value === undefined) + throw new Error(`unexpected cgroup file: ${name}`); + return value; + }, + readProcessStat(pid: number) { + return pid === 100 + ? processStat(100, 1, 12345) + : processStat(101, 100, 12346); + }, + async runControl(control: Record) { + controls.push(control); + if (control.mode === "cpu") { + counters.cpu.nr_throttled += 1; + counters.cpu.throttled_usec += 100; + } else if (control.mode === "processes") { + counters.processes.max += 1; + } else if (control.mode === "memory") { + counters.memory.oom_kill += 1; + } + }, + async publishDraft(draft: Record) { + drafts.push(draft); + }, + waitForCgroupAbsent: async () => undefined, + ...overrides, + }; +} + +describe("contained cgroup observer", () => { + it("parses counters, candidate-only ancestry, and process identity", () => { + expect(parseContainedCgroupKeyValues("max 0\noom_kill 2\n")).toEqual({ + max: 0, + oom_kill: 2, + }); + expect(parseContainedCgroupMembers("101\n100\n")).toEqual([100, 101]); + expect( + parseContainedProcessStat(processStat(101, 100, 12346), 101), + ).toEqual({ pid: 101, parentPid: 100, startTimeTicks: "12346" }); + expect( + selectContainedCgroupMembership( + [100, 101], + new Map([ + [100, { pid: 100, parentPid: 1, startTimeTicks: "12345" }], + [101, { pid: 101, parentPid: 100, startTimeTicks: "12346" }], + ]), + ), + ).toMatchObject({ leaderPid: 100, descendantsObserved: true }); + }); + + it("runs fixed controls in order and emits only complete evidence after cleanup", async () => { + const input = manifest(); + const fake = adapter(); + const evidence = await observeContainedCgroup(input, fake); + + expect(fake.controls).toEqual([ + { mode: "cpu", busyWindowMs: 500, workers: 4 }, + { mode: "processes", attemptedProcesses: 17 }, + { mode: "memory", requestedBytes: 576 * 1024 * 1024 }, + ]); + expect(fake.drafts).toHaveLength(1); + expect(() => + validateContainedCgroupObserverDraft(fake.drafts[0], input), + ).not.toThrow(); + expect(() => + validateContainedCgroupEvidence(evidence, { + invocationId, + finalContainerId, + sanitizedSpecSha256, + intendedAggregateLimits: input.intendedAggregateLimits, + runtimeAttestationSha256: input.runtimeAttestationSha256, + runtimeSessionId, + driverCliSha256: input.driverCliSha256, + driverModuleSha256: input.driverModuleSha256, + }), + ).not.toThrow(); + }); + + it("rejects missing counter enforcement, ambiguous ancestry, and PID reuse", async () => { + const noCpu = adapter({ + runControl: async (control: Record) => { + if (control.mode !== "cpu") return; + }, + }); + await expect(observeContainedCgroup(manifest(), noCpu)).rejects.toThrow( + /evidence binding/u, + ); + + const ambiguous = adapter({ + readProcessStat: (pid: number) => processStat(pid, 1, 12000 + pid), + }); + await expect(observeContainedCgroup(manifest(), ambiguous)).rejects.toThrow( + /ancestry/u, + ); + + let reads = 0; + const reused = adapter({ + readProcessStat(pid: number) { + reads += 1; + return processStat(pid, pid === 100 ? 1 : 100, 12345 + pid + reads); + }, + }); + await expect(observeContainedCgroup(manifest(), reused)).rejects.toThrow( + /reused/u, + ); + }); + + it("binds ready and draft receipts and rejects mutation", () => { + const input = manifest(); + const ready = createContainedCgroupObserverReady(input, { + armedAt: now, + observerPid: 123, + }); + expect(validateContainedCgroupObserverReady(ready, input)).toEqual(ready); + expect(() => + validateContainedCgroupObserverReady( + { ...ready, observerPid: 124 }, + input, + ), + ).toThrow(/ready binding/u); + + const failure = createContainedCgroupObserverFailure(input, { + failedAt: now, + }); + expect(validateContainedCgroupObserverFailure(failure, input)).toEqual( + failure, + ); + expect(() => + validateContainedCgroupObserverFailure( + { ...failure, code: "PRIVATE_ERROR" }, + input, + ), + ).toThrow(/failure binding/u); + + const observation = { + observedLimits: { + memoryMaxBytes: 512 * 1024 * 1024, + memorySwapMaxBytes: 0, + pidsMax: 16, + cpuQuotaMicros: 100_000, + cpuPeriodMicros: 100_000, + }, + membership: { + leaderPid: 100, + leaderStartTimeTicks: "12345", + memberPids: [100, 101], + memberSetSha256: sha256CgroupBytes(canonicalCgroupJson([100, 101])), + descendantsObserved: true, + }, + negativeControls: { + memory: { + requestedBytes: 576 * 1024 * 1024, + oomKillBefore: 0, + oomKillAfter: 1, + enforced: true, + }, + processes: { + attemptedProcesses: 17, + maxEventsBefore: 0, + maxEventsAfter: 1, + enforced: true, + }, + cpu: { + busyWindowMs: 500, + nrThrottledBefore: 0, + nrThrottledAfter: 1, + throttledUsecBefore: 0, + throttledUsecAfter: 1, + enforced: true, + }, + }, + observedAt: now.toISOString(), + }; + const draft = createContainedCgroupObserverDraft(input, observation); + expect(validateContainedCgroupObserverDraft(draft, input)).toEqual(draft); + expect(() => + validateContainedCgroupObserverDraft( + { ...draft, candidateLeaderPresentAfterControls: false }, + input, + ), + ).toThrow(/draft binding/u); + }); +}); diff --git a/scripts/contained-runtime-attestation.mjs b/scripts/contained-runtime-attestation.mjs index 03a0801f..a84184a5 100644 --- a/scripts/contained-runtime-attestation.mjs +++ b/scripts/contained-runtime-attestation.mjs @@ -15,6 +15,7 @@ export const RUNTIME_HELPER_PATHS = Object.freeze({ cgroupControlHelper: "scripts/contained-cgroup-control-helper.mjs", cgroupEvidence: "scripts/contained-cgroup-evidence.mjs", cgroupObserverBindings: "scripts/contained-cgroup-observer-bindings.mjs", + cgroupObserver: "scripts/contained-cgroup-observer.mjs", cgroupObserverProtocol: "scripts/contained-cgroup-observer-protocol.mjs", qualifiedRootlessReceiptStore: "scripts/contained-qualified-receipt-store.mjs", From 317ba44d9f86491b114662e5c557d5aa9de037bf Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 06:58:06 +0530 Subject: [PATCH 043/143] fix(runtime): bind qualified receipts to live sessions --- .../contained-qualified-receipt-store.d.mts | 8 +- scripts/contained-qualified-receipt-store.mjs | 62 +++++-- .../contained-qualified-receipt-store.test.ts | 161 ++++++++++++------ 3 files changed, 170 insertions(+), 61 deletions(-) diff --git a/scripts/contained-qualified-receipt-store.d.mts b/scripts/contained-qualified-receipt-store.d.mts index 2613ce0e..591a2738 100644 --- a/scripts/contained-qualified-receipt-store.d.mts +++ b/scripts/contained-qualified-receipt-store.d.mts @@ -3,7 +3,11 @@ export interface QualifiedRootlessReceiptStoreBinding { baseReceiptPath: string; finalContainerId: string; sessionRoot: string; - observerBindings: { +} + +interface QualifiedReceiptStoreDependencies { + now?: () => number; + resolveObserverBindings?: (input: { sessionRoot: string }) => { runtimeAttestationSha256: string; runtimeSessionId: string; driverCliSha256: string; @@ -15,6 +19,7 @@ export function persistQualifiedContainedRootlessReceipt( input: { aggregateLimitEvidence: unknown; } & QualifiedRootlessReceiptStoreBinding, + dependencies?: QualifiedReceiptStoreDependencies, ): { qualifiedReceipt: Record; qualifiedReceiptFileSha256: string; @@ -27,4 +32,5 @@ export function verifyQualifiedContainedRootlessReceipt( qualifiedReceiptFileSha256: string; qualifiedReceiptPath: string; }, + dependencies?: QualifiedReceiptStoreDependencies, ): Record; diff --git a/scripts/contained-qualified-receipt-store.mjs b/scripts/contained-qualified-receipt-store.mjs index f038772a..9b911998 100644 --- a/scripts/contained-qualified-receipt-store.mjs +++ b/scripts/contained-qualified-receipt-store.mjs @@ -3,6 +3,7 @@ import { isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { sha256CgroupBytes } from "./contained-cgroup-evidence.mjs"; +import { resolveContainedCgroupObserverBindings } from "./contained-cgroup-observer-bindings.mjs"; import { createQualifiedContainedRootlessReceipt } from "./contained-qualified-rootless-receipt.mjs"; const repositoryRoot = realpathSync( @@ -11,6 +12,7 @@ const repositoryRoot = realpathSync( const containerIdPattern = /^[a-f0-9]{64}$/u; const sha256Pattern = /^[a-f0-9]{64}$/u; const maximumReceiptBytes = 1_048_576; +const maximumObservationAgeMs = 5 * 60_000; function contained(root, candidate) { const fromRoot = relative(root, candidate); @@ -26,7 +28,7 @@ function privateDirectory(path, label) { !metadata.isDirectory() || realpathSync(path) !== path || metadata.uid !== process.getuid() || - (metadata.mode & 0o077) !== 0 + (metadata.mode & 0o777) !== 0o700 ) { throw new Error(`qualified rootless receipt ${label} is invalid`); } @@ -49,14 +51,17 @@ function privateFile(path, label) { } } -function receiptPaths({ sessionRoot, finalContainerId }) { +function receiptPaths({ sessionRoot, finalContainerId, runtimeSessionId }) { if ( !isAbsolute(sessionRoot) || !contained(repositoryRoot, sessionRoot) || - !containerIdPattern.test(finalContainerId ?? "") + !containerIdPattern.test(finalContainerId ?? "") || + !/^rt-[a-z0-9][a-z0-9-]{7,13}$/u.test(runtimeSessionId ?? "") || + sessionRoot !== resolve(repositoryRoot, ".rt", runtimeSessionId) ) { throw new Error("qualified rootless receipt path input is invalid"); } + privateDirectory(resolve(repositoryRoot, ".rt"), "runtime root"); privateDirectory(sessionRoot, "session root"); const runRoot = resolve(sessionRoot, "run"); const specRoot = resolve(runRoot, "rootless-specs"); @@ -82,12 +87,17 @@ function readBaseReceipt({ baseReceiptFileSha256, baseReceiptPath, finalContainerId, + observerBindings, sessionRoot, }) { if (!sha256Pattern.test(baseReceiptFileSha256 ?? "")) { throw new Error("qualified rootless base receipt hash is invalid"); } - const expected = receiptPaths({ sessionRoot, finalContainerId }); + const expected = receiptPaths({ + sessionRoot, + finalContainerId, + runtimeSessionId: observerBindings.runtimeSessionId, + }); if (baseReceiptPath !== expected.baseReceiptPath) { throw new Error("qualified rootless receipt base path changed"); } @@ -110,12 +120,35 @@ function readBaseReceipt({ return { expected, receipt }; } -export function persistQualifiedContainedRootlessReceipt(input) { - const { expected, receipt: baseReceipt } = readBaseReceipt(input); +export function persistQualifiedContainedRootlessReceipt( + input, + { + now = () => Date.now(), + resolveObserverBindings = resolveContainedCgroupObserverBindings, + } = {}, +) { + const observerBindings = resolveObserverBindings({ + sessionRoot: input.sessionRoot, + }); + const observedAt = Date.parse(input.aggregateLimitEvidence?.observedAt); + const currentTime = now(); + if ( + !Number.isFinite(currentTime) || + !Number.isFinite(observedAt) || + new Date(observedAt).toISOString() !== + input.aggregateLimitEvidence?.observedAt || + Math.abs(currentTime - observedAt) > maximumObservationAgeMs + ) { + throw new Error("qualified rootless receipt observation is stale"); + } + const { expected, receipt: baseReceipt } = readBaseReceipt({ + ...input, + observerBindings, + }); const qualifiedReceipt = createQualifiedContainedRootlessReceipt({ aggregateLimitEvidence: input.aggregateLimitEvidence, baseReceipt, - observerBindings: input.observerBindings, + observerBindings, }); const source = `${JSON.stringify(qualifiedReceipt, null, 2)}\n`; try { @@ -141,8 +174,17 @@ export function persistQualifiedContainedRootlessReceipt(input) { }; } -export function verifyQualifiedContainedRootlessReceipt(input) { - const { expected, receipt: baseReceipt } = readBaseReceipt(input); +export function verifyQualifiedContainedRootlessReceipt( + input, + { resolveObserverBindings = resolveContainedCgroupObserverBindings } = {}, +) { + const observerBindings = resolveObserverBindings({ + sessionRoot: input.sessionRoot, + }); + const { expected, receipt: baseReceipt } = readBaseReceipt({ + ...input, + observerBindings, + }); if ( input.qualifiedReceiptPath !== expected.qualifiedReceiptPath || !sha256Pattern.test(input.qualifiedReceiptFileSha256 ?? "") @@ -165,7 +207,7 @@ export function verifyQualifiedContainedRootlessReceipt(input) { const expectedReceipt = createQualifiedContainedRootlessReceipt({ aggregateLimitEvidence: qualifiedReceipt.aggregateLimitEvidence, baseReceipt, - observerBindings: input.observerBindings, + observerBindings, }); const expectedSource = `${JSON.stringify(expectedReceipt, null, 2)}\n`; if (!source.equals(Buffer.from(expectedSource))) { diff --git a/scripts/contained-qualified-receipt-store.test.ts b/scripts/contained-qualified-receipt-store.test.ts index b0070980..5f7bbbf5 100644 --- a/scripts/contained-qualified-receipt-store.test.ts +++ b/scripts/contained-qualified-receipt-store.test.ts @@ -1,10 +1,5 @@ -import { - mkdirSync, - mkdtempSync, - readFileSync, - symlinkSync, - writeFileSync, -} from "node:fs"; +import { randomBytes } from "node:crypto"; +import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; @@ -73,7 +68,7 @@ function baseReceipt() { }); } -function aggregateEvidence() { +function aggregateEvidence(bindings = observerBindings) { const memberPids = [100, 101]; return hashed({ schemaVersion: "1", @@ -90,7 +85,7 @@ function aggregateEvidence() { invocationId, finalContainerId, sanitizedSpecSha256, - runtimeAttestationSha256: observerBindings.runtimeAttestationSha256, + runtimeAttestationSha256: bindings.runtimeAttestationSha256, observedLimits: { memoryMaxBytes: intendedAggregateLimits.memoryBytes, memorySwapMaxBytes: intendedAggregateLimits.memoryBytes, @@ -129,21 +124,20 @@ function aggregateEvidence() { }, cleanup: { cgroupAbsentAfterTimeout: true }, observer: { - runtimeSessionId: observerBindings.runtimeSessionId, - driverCliSha256: observerBindings.driverCliSha256, - driverModuleSha256: observerBindings.driverModuleSha256, + runtimeSessionId: bindings.runtimeSessionId, + driverCliSha256: bindings.driverCliSha256, + driverModuleSha256: bindings.driverModuleSha256, }, observedAt: "2026-07-20T12:00:00.000Z", }); } function fixture() { - const parent = resolve( - root, - "node_modules/.cache/counterlab-v6.1/tmp/qualified-receipt-store", - ); + const runtimeSessionId = `rt-store-${randomBytes(4).toString("hex")}`; + const parent = resolve(root, ".rt"); mkdirSync(parent, { recursive: true, mode: 0o700 }); - const sessionRoot = mkdtempSync(resolve(parent, "rt-store-")); + const sessionRoot = resolve(parent, runtimeSessionId); + mkdirSync(sessionRoot, { mode: 0o700 }); const runRoot = resolve(sessionRoot, "run"); const specRoot = resolve(runRoot, "rootless-specs"); mkdirSync(specRoot, { recursive: true, mode: 0o700 }); @@ -153,19 +147,31 @@ function fixture() { return { baseReceiptFileSha256: sha256CgroupBytes(source), baseReceiptPath, + observerBindings: { ...observerBindings, runtimeSessionId }, sessionRoot, }; } +function dependencies(input: ReturnType) { + return { + now: () => Date.parse("2026-07-20T12:01:00.000Z"), + resolveObserverBindings: () => input.observerBindings, + }; +} + describe("qualified rootless receipt store", () => { it("creates and verifies one distinct qualified receipt", () => { const input = fixture(); - const persisted = persistQualifiedContainedRootlessReceipt({ - ...input, - aggregateLimitEvidence: aggregateEvidence(), - finalContainerId, - observerBindings, - }); + const persisted = persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: input.baseReceiptFileSha256, + baseReceiptPath: input.baseReceiptPath, + sessionRoot: input.sessionRoot, + aggregateLimitEvidence: aggregateEvidence(input.observerBindings), + finalContainerId, + }, + dependencies(input), + ); expect(persisted.qualifiedReceiptPath).toBe( resolve( @@ -177,34 +183,45 @@ describe("qualified rootless receipt store", () => { `${JSON.stringify(baseReceipt(), null, 2)}\n`, ); expect(() => - verifyQualifiedContainedRootlessReceipt({ - ...input, - finalContainerId, - observerBindings, - qualifiedReceiptFileSha256: persisted.qualifiedReceiptFileSha256, - qualifiedReceiptPath: persisted.qualifiedReceiptPath, - }), + verifyQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: input.baseReceiptFileSha256, + baseReceiptPath: input.baseReceiptPath, + sessionRoot: input.sessionRoot, + finalContainerId, + qualifiedReceiptFileSha256: persisted.qualifiedReceiptFileSha256, + qualifiedReceiptPath: persisted.qualifiedReceiptPath, + }, + dependencies(input), + ), ).not.toThrow(); expect(() => - persistQualifiedContainedRootlessReceipt({ - ...input, - aggregateLimitEvidence: aggregateEvidence(), - finalContainerId, - observerBindings, - }), + persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: input.baseReceiptFileSha256, + baseReceiptPath: input.baseReceiptPath, + sessionRoot: input.sessionRoot, + aggregateLimitEvidence: aggregateEvidence(input.observerBindings), + finalContainerId, + }, + dependencies(input), + ), ).toThrow(/already exists/u); }); it("rejects path, hash, and symbolic-link drift", () => { const wrongHash = fixture(); expect(() => - persistQualifiedContainedRootlessReceipt({ - ...wrongHash, - aggregateLimitEvidence: aggregateEvidence(), - baseReceiptFileSha256: "0".repeat(64), - finalContainerId, - observerBindings, - }), + persistQualifiedContainedRootlessReceipt( + { + baseReceiptPath: wrongHash.baseReceiptPath, + sessionRoot: wrongHash.sessionRoot, + aggregateLimitEvidence: aggregateEvidence(wrongHash.observerBindings), + baseReceiptFileSha256: "0".repeat(64), + finalContainerId, + }, + dependencies(wrongHash), + ), ).toThrow(/base receipt/u); const linked = fixture(); @@ -214,13 +231,57 @@ describe("qualified rootless receipt store", () => { ); symlinkSync(linked.baseReceiptPath, linkedPath); expect(() => - persistQualifiedContainedRootlessReceipt({ - ...linked, - aggregateLimitEvidence: aggregateEvidence(), - baseReceiptPath: linkedPath, - finalContainerId, - observerBindings, - }), + persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: linked.baseReceiptFileSha256, + sessionRoot: linked.sessionRoot, + aggregateLimitEvidence: aggregateEvidence(linked.observerBindings), + baseReceiptPath: linkedPath, + finalContainerId, + }, + dependencies(linked), + ), ).toThrow(/path/u); }); + + it("rejects stale evidence and a resolver bound to another session", () => { + const stale = fixture(); + expect(() => + persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: stale.baseReceiptFileSha256, + baseReceiptPath: stale.baseReceiptPath, + sessionRoot: stale.sessionRoot, + aggregateLimitEvidence: aggregateEvidence(stale.observerBindings), + finalContainerId, + }, + { + ...dependencies(stale), + now: () => Date.parse("2026-07-20T12:06:00.001Z"), + }, + ), + ).toThrow(/stale/u); + + const mismatched = fixture(); + expect(() => + persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: mismatched.baseReceiptFileSha256, + baseReceiptPath: mismatched.baseReceiptPath, + sessionRoot: mismatched.sessionRoot, + aggregateLimitEvidence: aggregateEvidence( + mismatched.observerBindings, + ), + finalContainerId, + }, + { + ...dependencies(mismatched), + resolveObserverBindings: () => ({ + ...mismatched.observerBindings, + runtimeSessionId: "rt-v61-other1", + }), + }, + ), + ).toThrow(/path input/u); + }); }); From c2c1817326b76a5e3791224c2a2355ccca17190f Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 07:02:17 +0530 Subject: [PATCH 044/143] feat(runtime): define cgroup finalization signal --- .../contained-cgroup-observer-protocol.d.mts | 32 ++++++ .../contained-cgroup-observer-protocol.mjs | 103 ++++++++++++++++++ ...contained-cgroup-observer-protocol.test.ts | 63 +++++++++++ 3 files changed, 198 insertions(+) diff --git a/scripts/contained-cgroup-observer-protocol.d.mts b/scripts/contained-cgroup-observer-protocol.d.mts index 4c76be22..a41f3f54 100644 --- a/scripts/contained-cgroup-observer-protocol.d.mts +++ b/scripts/contained-cgroup-observer-protocol.d.mts @@ -45,6 +45,37 @@ export function createContainedCgroupObserverManifest(input: { sanitizedSpecSha256: string; }): ContainedCgroupObserverManifest; +export interface ContainedCgroupObserverFinalization { + schemaVersion: "1"; + status: "FINALIZE" | "ABORT"; + reason: "AGGREGATE_TIMEOUT_CONFIRMED" | "QUALIFICATION_ABORTED"; + manifestPayloadSha256: string; + invocationId: string; + finalContainerId: string; + timeoutObserved: boolean; + resultReleased: boolean; + cleanupVerified: boolean; + decisionAt: string; + receiptPayloadSha256: string; +} + +export function validateContainedCgroupObserverFinalization( + value: unknown, + manifest: ContainedCgroupObserverManifest, + options?: { observedAtMs?: number }, +): ContainedCgroupObserverFinalization; + +export function createContainedCgroupObserverFinalization( + manifest: ContainedCgroupObserverManifest, + input: { + cleanupVerified: boolean; + decisionAt?: Date; + resultReleased: boolean; + status: "FINALIZE" | "ABORT"; + timeoutObserved: boolean; + }, +): ContainedCgroupObserverFinalization; + export function containedCgroupQualificationPaths(input: { repositoryRoot: string; runtimeSessionId: string; @@ -52,6 +83,7 @@ export function containedCgroupQualificationPaths(input: { }): Readonly<{ evidencePath: string; failurePath: string; + finalizationPath: string; invocationRoot: string; manifestPath: string; observerDraftPath: string; diff --git a/scripts/contained-cgroup-observer-protocol.mjs b/scripts/contained-cgroup-observer-protocol.mjs index 0988cbfa..8859a1e7 100644 --- a/scripts/contained-cgroup-observer-protocol.mjs +++ b/scripts/contained-cgroup-observer-protocol.mjs @@ -10,6 +10,10 @@ import { const sha256Pattern = /^[a-f0-9]{64}$/u; const sessionPattern = /^rt-[a-z0-9][a-z0-9-]{7,13}$/u; const maximumAgeMs = 5 * 60_000; +const finalizationReasonByStatus = Object.freeze({ + ABORT: "QUALIFICATION_ABORTED", + FINALIZE: "AGGREGATE_TIMEOUT_CONFIRMED", +}); const manifestKeys = [ "baseReceiptFileSha256", @@ -167,6 +171,104 @@ export function createContainedCgroupObserverManifest({ ); } +export function validateContainedCgroupObserverFinalization( + value, + manifest, + { observedAtMs = Date.now() } = {}, +) { + const finalization = object(value, "finalization"); + exactKeys( + finalization, + [ + "cleanupVerified", + "decisionAt", + "finalContainerId", + "invocationId", + "manifestPayloadSha256", + "reason", + "receiptPayloadSha256", + "resultReleased", + "schemaVersion", + "status", + "timeoutObserved", + ], + "finalization", + ); + const { receiptPayloadSha256, ...payload } = finalization; + const requestedAtMs = Date.parse(manifest?.requestedAt); + const decisionAtMs = Date.parse(finalization.decisionAt); + const finalizesQualification = + finalization.status === "FINALIZE" && + finalization.timeoutObserved === true && + finalization.resultReleased === false && + finalization.cleanupVerified === true; + const abortsQualification = + finalization.status === "ABORT" && + !( + finalization.timeoutObserved === true && + finalization.resultReleased === false && + finalization.cleanupVerified === true + ); + if ( + finalization.schemaVersion !== "1" || + (!finalizesQualification && !abortsQualification) || + finalization.reason !== finalizationReasonByStatus[finalization.status] || + finalization.manifestPayloadSha256 !== manifest?.receiptPayloadSha256 || + finalization.invocationId !== manifest?.invocationId || + finalization.finalContainerId !== manifest?.finalContainerId || + typeof finalization.timeoutObserved !== "boolean" || + typeof finalization.resultReleased !== "boolean" || + typeof finalization.cleanupVerified !== "boolean" || + !validTimestamp(finalization.decisionAt, observedAtMs) || + !Number.isFinite(requestedAtMs) || + !Number.isFinite(decisionAtMs) || + decisionAtMs < requestedAtMs || + decisionAtMs - requestedAtMs > maximumAgeMs || + !sha256Pattern.test(receiptPayloadSha256 ?? "") || + sha256CgroupBytes(canonicalCgroupJson(payload)) !== receiptPayloadSha256 + ) { + throw new Error( + "contained cgroup observer finalization binding is invalid", + ); + } + return finalization; +} + +export function createContainedCgroupObserverFinalization( + manifest, + { + cleanupVerified, + decisionAt = new Date(), + resultReleased, + status, + timeoutObserved, + }, +) { + if (!(decisionAt instanceof Date) || !Number.isFinite(decisionAt.getTime())) { + throw new Error("contained cgroup observer decision time is invalid"); + } + const payload = { + schemaVersion: "1", + status, + reason: finalizationReasonByStatus[status], + manifestPayloadSha256: manifest?.receiptPayloadSha256, + invocationId: manifest?.invocationId, + finalContainerId: manifest?.finalContainerId, + timeoutObserved, + resultReleased, + cleanupVerified, + decisionAt: decisionAt.toISOString(), + }; + return validateContainedCgroupObserverFinalization( + { + ...payload, + receiptPayloadSha256: sha256CgroupBytes(canonicalCgroupJson(payload)), + }, + manifest, + { observedAtMs: decisionAt.getTime() }, + ); +} + export function containedCgroupQualificationPaths({ repositoryRoot, runtimeSessionId, @@ -191,6 +293,7 @@ export function containedCgroupQualificationPaths({ return Object.freeze({ evidencePath: resolve(invocationRoot, "aggregate-evidence.json"), failurePath: resolve(invocationRoot, "failure.json"), + finalizationPath: resolve(invocationRoot, "finalization.json"), invocationRoot, manifestPath: resolve(invocationRoot, "manifest.json"), observerDraftPath: resolve(invocationRoot, "observer-draft.json"), diff --git a/scripts/contained-cgroup-observer-protocol.test.ts b/scripts/contained-cgroup-observer-protocol.test.ts index 718cc4af..cb7e04a9 100644 --- a/scripts/contained-cgroup-observer-protocol.test.ts +++ b/scripts/contained-cgroup-observer-protocol.test.ts @@ -4,7 +4,9 @@ import { describe, expect, it } from "vitest"; import { containedCgroupQualificationPaths, + createContainedCgroupObserverFinalization, createContainedCgroupObserverManifest, + validateContainedCgroupObserverFinalization, validateContainedCgroupObserverManifest, } from "./contained-cgroup-observer-protocol.mjs"; @@ -93,6 +95,59 @@ describe("contained cgroup observer protocol", () => { ).toThrow(/binding/u); }); + it("accepts only self-bound runtime finalization or an explicit abort", () => { + const input = manifest(); + const finalizedAt = new Date(requestedAt.getTime() + 30_000); + const finalized = createContainedCgroupObserverFinalization(input, { + cleanupVerified: true, + decisionAt: finalizedAt, + resultReleased: false, + status: "FINALIZE", + timeoutObserved: true, + }); + expect( + validateContainedCgroupObserverFinalization(finalized, input, { + observedAtMs: finalizedAt.getTime(), + }), + ).toEqual(finalized); + + const aborted = createContainedCgroupObserverFinalization(input, { + cleanupVerified: false, + decisionAt: finalizedAt, + resultReleased: false, + status: "ABORT", + timeoutObserved: false, + }); + expect( + validateContainedCgroupObserverFinalization(aborted, input, { + observedAtMs: finalizedAt.getTime(), + }), + ).toEqual(aborted); + + const mutations = [ + { ...finalized, resultReleased: true }, + { ...finalized, cleanupVerified: false }, + { ...finalized, timeoutObserved: false }, + { ...finalized, receiptPayloadSha256: "0".repeat(64) }, + { ...aborted, status: "FINALIZE" }, + { ...aborted, timeoutObserved: true, cleanupVerified: true }, + ]; + for (const changed of mutations) { + expect(() => + validateContainedCgroupObserverFinalization(changed, input, { + observedAtMs: finalizedAt.getTime(), + }), + ).toThrow(/finalization binding/u); + } + expect(() => + validateContainedCgroupObserverFinalization( + finalized, + { ...input, invocationId: "9".repeat(64) }, + { observedAtMs: finalizedAt.getTime() }, + ), + ).toThrow(/finalization binding/u); + }); + it("derives only exact repository-contained handshake paths", () => { expect( containedCgroupQualificationPaths({ @@ -139,6 +194,14 @@ describe("contained cgroup observer protocol", () => { invocationId, "observer-draft.json", ), + finalizationPath: resolve( + root, + ".rt", + runtimeSessionId, + "run/cgroup-qualification", + invocationId, + "finalization.json", + ), evidencePath: resolve( root, ".rt", From cf906475cb1c13a33971a59ba4678fe2cf27f5f3 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 07:19:57 +0530 Subject: [PATCH 045/143] fix(runtime): fail closed on cgroup qualification --- scripts/contained-cgroup-control-helper.mjs | 189 ++++++++--- scripts/contained-cgroup-evidence.mjs | 2 +- scripts/contained-cgroup-evidence.test.ts | 12 + .../contained-cgroup-observer-protocol.d.mts | 24 ++ .../contained-cgroup-observer-protocol.mjs | 50 ++- ...contained-cgroup-observer-protocol.test.ts | 38 ++- scripts/contained-cgroup-observer.d.mts | 7 +- scripts/contained-cgroup-observer.mjs | 305 +++++++++++++++--- scripts/contained-cgroup-observer.test.ts | 119 ++++++- .../src/counterlab_runner/timeout_proof.py | 3 +- services/runner/tests/test_timeout_proof.py | 10 +- 11 files changed, 669 insertions(+), 90 deletions(-) diff --git a/scripts/contained-cgroup-control-helper.mjs b/scripts/contained-cgroup-control-helper.mjs index e9092499..7a0b0c7f 100644 --- a/scripts/contained-cgroup-control-helper.mjs +++ b/scripts/contained-cgroup-control-helper.mjs @@ -96,6 +96,71 @@ function childCompletion(child) { }); } +function childRunning(child) { + return child.exitCode === null && child.signalCode === null; +} + +async function bounded(promise, timeoutMs, label) { + let timer; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error(`contained cgroup ${label} timed out`)), + timeoutMs, + ); + }), + ]); + } finally { + clearTimeout(timer); + } +} + +async function reapChildren(children, completions) { + for (const child of children) { + if (childRunning(child)) child.kill("SIGTERM"); + } + try { + await bounded(Promise.all(completions), 2_000, "child reap"); + } catch { + for (const child of children) { + if (childRunning(child)) child.kill("SIGKILL"); + } + await bounded(Promise.all(completions), 2_000, "forced child reap"); + } +} + +function terminationController(activeChildren) { + let requestedSignal; + const handlers = new Map( + ["SIGINT", "SIGTERM"].map((signal) => { + const handler = () => { + requestedSignal ??= signal; + for (const child of activeChildren) { + if (childRunning(child)) child.kill("SIGTERM"); + } + }; + process.on(signal, handler); + return [signal, handler]; + }), + ); + return { + assertActive() { + if (requestedSignal !== undefined) { + throw new Error( + `contained cgroup control interrupted by ${requestedSignal}`, + ); + } + }, + dispose() { + for (const [signal, handler] of handlers) { + process.off(signal, handler); + } + }, + }; +} + async function memoryControl(requestedBytes) { writeFileSync("/proc/self/oom_score_adj", "1000\n", "utf8"); const retained = []; @@ -117,36 +182,48 @@ async function memoryControl(requestedBytes) { ); } -async function processControl(attemptedProcesses, scriptPath) { +async function processControl( + attemptedProcesses, + scriptPath, + activeChildren, + controller, +) { const children = []; const completions = []; let denied = false; - for (let index = 0; index < attemptedProcesses; index += 1) { - const child = spawn( - process.execPath, - [scriptPath, "--internal-hold", "5000"], - { stdio: "ignore" }, - ); - children.push(child); - const completion = childCompletion(child); - completions.push(completion); - const state = await Promise.race([ - completion, - new Promise((accept) => - child.once("spawn", () => accept({ started: true })), - ), - ]); - if (!state.started) { - denied = true; - break; + try { + for (let index = 0; index < attemptedProcesses; index += 1) { + controller.assertActive(); + const child = spawn( + process.execPath, + [scriptPath, "--internal-hold", "5000"], + { + env: { LANG: "C", LC_ALL: "C", TZ: "UTC" }, + stdio: "ignore", + }, + ); + children.push(child); + activeChildren.add(child); + const completion = childCompletion(child); + completions.push(completion); + const state = await Promise.race([ + completion, + new Promise((accept) => + child.once("spawn", () => accept({ started: true })), + ), + ]); + if (!state.started) { + denied = true; + break; + } } - } - for (const child of children) { - if (child.exitCode === null && child.signalCode === null) { - child.kill("SIGTERM"); + } finally { + await reapChildren(children, completions); + for (const child of children) { + activeChildren.delete(child); } } - await Promise.all(completions); + controller.assertActive(); if (!denied) { throw new Error("contained cgroup process control escaped its limit"); } @@ -155,15 +232,37 @@ async function processControl(attemptedProcesses, scriptPath) { ); } -async function cpuControl(busyWindowMs, workers, scriptPath) { +async function cpuControl( + busyWindowMs, + workers, + scriptPath, + activeChildren, + controller, +) { const children = Array.from({ length: workers }, () => spawn( process.execPath, [scriptPath, "--internal-busy", String(busyWindowMs)], - { stdio: "ignore" }, + { + env: { LANG: "C", LC_ALL: "C", TZ: "UTC" }, + stdio: "ignore", + }, ), ); - const results = await Promise.all(children.map(childCompletion)); + children.forEach((child) => activeChildren.add(child)); + const completions = children.map(childCompletion); + let results; + try { + results = await bounded( + Promise.all(completions), + busyWindowMs + 2_000, + "CPU worker completion", + ); + } finally { + await reapChildren(children, completions); + children.forEach((child) => activeChildren.delete(child)); + } + controller.assertActive(); if (results.some((result) => !result.started || result.code !== 0)) { throw new Error("contained cgroup CPU control worker failed"); } @@ -192,16 +291,34 @@ async function main() { return; } const control = parseContainedCgroupControl(process.argv.slice(2)); - await waitForGo(); - if (control.mode === "memory") { - await memoryControl(control.requestedBytes); - return; - } - if (control.mode === "processes") { - await processControl(control.attemptedProcesses, scriptPath); - return; + const activeChildren = new Set(); + const controller = terminationController(activeChildren); + try { + await waitForGo(); + controller.assertActive(); + if (control.mode === "memory") { + await memoryControl(control.requestedBytes); + return; + } + if (control.mode === "processes") { + await processControl( + control.attemptedProcesses, + scriptPath, + activeChildren, + controller, + ); + return; + } + await cpuControl( + control.busyWindowMs, + control.workers, + scriptPath, + activeChildren, + controller, + ); + } finally { + controller.dispose(); } - await cpuControl(control.busyWindowMs, control.workers, scriptPath); } if ( diff --git a/scripts/contained-cgroup-evidence.mjs b/scripts/contained-cgroup-evidence.mjs index 6849e8d3..24a3cdc7 100644 --- a/scripts/contained-cgroup-evidence.mjs +++ b/scripts/contained-cgroup-evidence.mjs @@ -240,7 +240,7 @@ export function validateContainedCgroupEvidence(value, expectedValue) { memoryControl.requestedBytes <= intended.memoryBytes || !safeInteger(memoryControl.oomKillBefore) || !safeInteger(memoryControl.oomKillAfter, { positive: true }) || - memoryControl.oomKillAfter <= memoryControl.oomKillBefore || + memoryControl.oomKillAfter !== memoryControl.oomKillBefore + 1 || memoryControl.enforced !== true || !safeInteger(processControl.attemptedProcesses, { positive: true }) || processControl.attemptedProcesses <= intended.maxProcesses || diff --git a/scripts/contained-cgroup-evidence.test.ts b/scripts/contained-cgroup-evidence.test.ts index cfc12ee4..78cbf3f5 100644 --- a/scripts/contained-cgroup-evidence.test.ts +++ b/scripts/contained-cgroup-evidence.test.ts @@ -162,4 +162,16 @@ describe("contained cgroup evidence", () => { }), ).toThrow(/binding/u); }); + + it("rejects a self-consistent receipt with an ambiguous OOM delta", () => { + const changed = evidence(); + changed.negativeControls.memory.oomKillAfter = 2; + const { receiptPayloadSha256: _ignored, ...payload } = changed; + changed.receiptPayloadSha256 = sha256CgroupBytes( + canonicalCgroupJson(payload), + ); + expect(() => validateContainedCgroupEvidence(changed, expected())).toThrow( + /binding/u, + ); + }); }); diff --git a/scripts/contained-cgroup-observer-protocol.d.mts b/scripts/contained-cgroup-observer-protocol.d.mts index a41f3f54..af516812 100644 --- a/scripts/contained-cgroup-observer-protocol.d.mts +++ b/scripts/contained-cgroup-observer-protocol.d.mts @@ -49,7 +49,25 @@ export interface ContainedCgroupObserverFinalization { schemaVersion: "1"; status: "FINALIZE" | "ABORT"; reason: "AGGREGATE_TIMEOUT_CONFIRMED" | "QUALIFICATION_ABORTED"; + abortCode: + | "CLEANUP_UNVERIFIED" + | "OBSERVER_FAILED" + | "RESULT_RELEASED" + | "RUNTIME_FAILED" + | "TIMEOUT_NOT_OBSERVED" + | null; + cleanup: { + taskAbsent: boolean; + containerAbsent: boolean; + snapshotAbsent: boolean; + invocationAliasAbsent: boolean; + imageRootfsAbsent: boolean; + persistedAuthorityVerified: boolean; + readOnlyMountsUnchanged: boolean; + imageRootfsUnchanged: boolean; + }; manifestPayloadSha256: string; + observerDraftPayloadSha256: string; invocationId: string; finalContainerId: string; timeoutObserved: boolean; @@ -68,8 +86,14 @@ export function validateContainedCgroupObserverFinalization( export function createContainedCgroupObserverFinalization( manifest: ContainedCgroupObserverManifest, input: { + abortCode?: Exclude< + ContainedCgroupObserverFinalization["abortCode"], + null + > | null; + cleanup: ContainedCgroupObserverFinalization["cleanup"]; cleanupVerified: boolean; decisionAt?: Date; + observerDraftPayloadSha256: string; resultReleased: boolean; status: "FINALIZE" | "ABORT"; timeoutObserved: boolean; diff --git a/scripts/contained-cgroup-observer-protocol.mjs b/scripts/contained-cgroup-observer-protocol.mjs index 8859a1e7..489ecce0 100644 --- a/scripts/contained-cgroup-observer-protocol.mjs +++ b/scripts/contained-cgroup-observer-protocol.mjs @@ -14,6 +14,23 @@ const finalizationReasonByStatus = Object.freeze({ ABORT: "QUALIFICATION_ABORTED", FINALIZE: "AGGREGATE_TIMEOUT_CONFIRMED", }); +const allowedAbortCodes = new Set([ + "CLEANUP_UNVERIFIED", + "OBSERVER_FAILED", + "RESULT_RELEASED", + "RUNTIME_FAILED", + "TIMEOUT_NOT_OBSERVED", +]); +const cleanupKeys = [ + "containerAbsent", + "imageRootfsAbsent", + "imageRootfsUnchanged", + "invocationAliasAbsent", + "persistedAuthorityVerified", + "readOnlyMountsUnchanged", + "snapshotAbsent", + "taskAbsent", +]; const manifestKeys = [ "baseReceiptFileSha256", @@ -180,11 +197,14 @@ export function validateContainedCgroupObserverFinalization( exactKeys( finalization, [ + "abortCode", + "cleanup", "cleanupVerified", "decisionAt", "finalContainerId", "invocationId", "manifestPayloadSha256", + "observerDraftPayloadSha256", "reason", "receiptPayloadSha256", "resultReleased", @@ -197,28 +217,36 @@ export function validateContainedCgroupObserverFinalization( const { receiptPayloadSha256, ...payload } = finalization; const requestedAtMs = Date.parse(manifest?.requestedAt); const decisionAtMs = Date.parse(finalization.decisionAt); + const cleanup = object(finalization.cleanup, "finalization cleanup"); + exactKeys(cleanup, cleanupKeys, "finalization cleanup"); + const cleanupValuesAreBoolean = cleanupKeys.every( + (key) => typeof cleanup[key] === "boolean", + ); + const everyCleanupCheckPassed = cleanupKeys.every( + (key) => cleanup[key] === true, + ); const finalizesQualification = finalization.status === "FINALIZE" && finalization.timeoutObserved === true && finalization.resultReleased === false && - finalization.cleanupVerified === true; - const abortsQualification = - finalization.status === "ABORT" && - !( - finalization.timeoutObserved === true && - finalization.resultReleased === false && - finalization.cleanupVerified === true - ); + finalization.cleanupVerified === true && + everyCleanupCheckPassed; + const abortsQualification = finalization.status === "ABORT"; if ( finalization.schemaVersion !== "1" || (!finalizesQualification && !abortsQualification) || finalization.reason !== finalizationReasonByStatus[finalization.status] || + (finalizesQualification + ? finalization.abortCode !== null + : !allowedAbortCodes.has(finalization.abortCode)) || finalization.manifestPayloadSha256 !== manifest?.receiptPayloadSha256 || + !sha256Pattern.test(finalization.observerDraftPayloadSha256 ?? "") || finalization.invocationId !== manifest?.invocationId || finalization.finalContainerId !== manifest?.finalContainerId || typeof finalization.timeoutObserved !== "boolean" || typeof finalization.resultReleased !== "boolean" || typeof finalization.cleanupVerified !== "boolean" || + !cleanupValuesAreBoolean || !validTimestamp(finalization.decisionAt, observedAtMs) || !Number.isFinite(requestedAtMs) || !Number.isFinite(decisionAtMs) || @@ -237,8 +265,11 @@ export function validateContainedCgroupObserverFinalization( export function createContainedCgroupObserverFinalization( manifest, { + abortCode = null, + cleanup, cleanupVerified, decisionAt = new Date(), + observerDraftPayloadSha256, resultReleased, status, timeoutObserved, @@ -251,7 +282,10 @@ export function createContainedCgroupObserverFinalization( schemaVersion: "1", status, reason: finalizationReasonByStatus[status], + abortCode, + cleanup, manifestPayloadSha256: manifest?.receiptPayloadSha256, + observerDraftPayloadSha256, invocationId: manifest?.invocationId, finalContainerId: manifest?.finalContainerId, timeoutObserved, diff --git a/scripts/contained-cgroup-observer-protocol.test.ts b/scripts/contained-cgroup-observer-protocol.test.ts index cb7e04a9..abd82fdf 100644 --- a/scripts/contained-cgroup-observer-protocol.test.ts +++ b/scripts/contained-cgroup-observer-protocol.test.ts @@ -16,6 +16,16 @@ const invocationId = "1".repeat(64); const finalContainerId = "2".repeat(64); const sanitizedSpecSha256 = "3".repeat(64); const requestedAt = new Date("2026-07-20T01:02:03.000Z"); +const verifiedCleanup = { + taskAbsent: true, + containerAbsent: true, + snapshotAbsent: true, + invocationAliasAbsent: true, + imageRootfsAbsent: true, + persistedAuthorityVerified: true, + readOnlyMountsUnchanged: true, + imageRootfsUnchanged: true, +}; function manifest() { return createContainedCgroupObserverManifest({ @@ -99,8 +109,10 @@ describe("contained cgroup observer protocol", () => { const input = manifest(); const finalizedAt = new Date(requestedAt.getTime() + 30_000); const finalized = createContainedCgroupObserverFinalization(input, { + cleanup: verifiedCleanup, cleanupVerified: true, decisionAt: finalizedAt, + observerDraftPayloadSha256: "a".repeat(64), resultReleased: false, status: "FINALIZE", timeoutObserved: true, @@ -112,8 +124,11 @@ describe("contained cgroup observer protocol", () => { ).toEqual(finalized); const aborted = createContainedCgroupObserverFinalization(input, { + abortCode: "TIMEOUT_NOT_OBSERVED", + cleanup: { ...verifiedCleanup, taskAbsent: false }, cleanupVerified: false, decisionAt: finalizedAt, + observerDraftPayloadSha256: "a".repeat(64), resultReleased: false, status: "ABORT", timeoutObserved: false, @@ -124,13 +139,34 @@ describe("contained cgroup observer protocol", () => { }), ).toEqual(aborted); + const failClosedAbort = createContainedCgroupObserverFinalization(input, { + abortCode: "RUNTIME_FAILED", + cleanup: verifiedCleanup, + cleanupVerified: true, + decisionAt: finalizedAt, + observerDraftPayloadSha256: "a".repeat(64), + resultReleased: false, + status: "ABORT", + timeoutObserved: true, + }); + expect( + validateContainedCgroupObserverFinalization(failClosedAbort, input, { + observedAtMs: finalizedAt.getTime(), + }), + ).toEqual(failClosedAbort); + const mutations = [ { ...finalized, resultReleased: true }, { ...finalized, cleanupVerified: false }, { ...finalized, timeoutObserved: false }, + { + ...finalized, + cleanup: { ...finalized.cleanup, snapshotAbsent: false }, + }, { ...finalized, receiptPayloadSha256: "0".repeat(64) }, + { ...finalized, observerDraftPayloadSha256: "0" }, { ...aborted, status: "FINALIZE" }, - { ...aborted, timeoutObserved: true, cleanupVerified: true }, + { ...aborted, abortCode: "UNKNOWN" }, ]; for (const changed of mutations) { expect(() => diff --git a/scripts/contained-cgroup-observer.d.mts b/scripts/contained-cgroup-observer.d.mts index 0577ca25..d561b7c4 100644 --- a/scripts/contained-cgroup-observer.d.mts +++ b/scripts/contained-cgroup-observer.d.mts @@ -1,4 +1,5 @@ import type { ContainedCgroupObserverManifest } from "./contained-cgroup-observer-protocol.mjs"; +import type { ContainedCgroupObserverFinalization } from "./contained-cgroup-observer-protocol.mjs"; export function createContainedCgroupObserverReady( manifest: ContainedCgroupObserverManifest, @@ -53,8 +54,12 @@ export function observeContainedCgroup( waitForCgroup(): Promise; readCgroupFile(name: string): string | Promise; readProcessStat(pid: number): string | Promise; - runControl(control: Record): Promise; + runControl( + control: Record, + baselineMemberPids: number[], + ): Promise; publishDraft(draft: Record): Promise; + waitForFinalization(): Promise; waitForCgroupAbsent(): Promise; }, ): Promise>; diff --git a/scripts/contained-cgroup-observer.mjs b/scripts/contained-cgroup-observer.mjs index ccbd3ab0..5805b05c 100644 --- a/scripts/contained-cgroup-observer.mjs +++ b/scripts/contained-cgroup-observer.mjs @@ -2,7 +2,6 @@ import { spawn } from "node:child_process"; import { - existsSync, lstatSync, readFileSync, realpathSync, @@ -20,6 +19,7 @@ import { import { resolveContainedCgroupObserverBindings } from "./contained-cgroup-observer-bindings.mjs"; import { containedCgroupQualificationPaths, + validateContainedCgroupObserverFinalization, validateContainedCgroupObserverManifest, } from "./contained-cgroup-observer-protocol.mjs"; @@ -444,6 +444,20 @@ function cpuLimit(source) { }; } +async function observedAggregateLimits(adapter) { + return { + memoryMaxBytes: scalar( + await adapter.readCgroupFile("memory.max"), + "memory limit", + ), + memorySwapMaxBytes: swapScalar( + await adapter.readCgroupFile("memory.swap.max"), + ), + pidsMax: scalar(await adapter.readCgroupFile("pids.max"), "PID limit"), + ...cpuLimit(await adapter.readCgroupFile("cpu.max")), + }; +} + async function stableMembership(adapter) { const memberPids = parseContainedCgroupMembers( await adapter.readCgroupFile("cgroup.procs"), @@ -455,18 +469,28 @@ async function stableMembership(adapter) { parseContainedProcessStat(await adapter.readProcessStat(pid), pid), ); } + const confirmedMemberPids = parseContainedCgroupMembers( + await adapter.readCgroupFile("cgroup.procs"), + ); + if ( + canonicalCgroupJson(confirmedMemberPids) !== canonicalCgroupJson(memberPids) + ) { + throw new Error("contained cgroup observer membership did not stabilize"); + } const membership = selectContainedCgroupMembership(memberPids, processStats); return { memberPids, membership, processStats }; } async function assertCandidateSurvived(adapter, initial) { - const current = new Set( - parseContainedCgroupMembers(await adapter.readCgroupFile("cgroup.procs")), + const current = parseContainedCgroupMembers( + await adapter.readCgroupFile("cgroup.procs"), ); + if ( + canonicalCgroupJson(current) !== canonicalCgroupJson(initial.memberPids) + ) { + throw new Error("contained cgroup observer candidate membership changed"); + } for (const pid of initial.memberPids) { - if (!current.has(pid)) { - throw new Error("contained cgroup observer candidate membership changed"); - } const observed = parseContainedProcessStat( await adapter.readProcessStat(pid), pid, @@ -477,27 +501,28 @@ async function assertCandidateSurvived(adapter, initial) { throw new Error("contained cgroup observer candidate PID was reused"); } } + const confirmed = parseContainedCgroupMembers( + await adapter.readCgroupFile("cgroup.procs"), + ); + if ( + canonicalCgroupJson(confirmed) !== canonicalCgroupJson(initial.memberPids) + ) { + throw new Error("contained cgroup observer membership did not stabilize"); + } } export async function observeContainedCgroup(manifest, adapter) { await adapter.waitForCgroup(); - const observedLimits = { - memoryMaxBytes: scalar( - await adapter.readCgroupFile("memory.max"), - "memory limit", - ), - memorySwapMaxBytes: swapScalar( - await adapter.readCgroupFile("memory.swap.max"), - ), - pidsMax: scalar(await adapter.readCgroupFile("pids.max"), "PID limit"), - ...cpuLimit(await adapter.readCgroupFile("cpu.max")), - }; + const observedLimits = await observedAggregateLimits(adapter); const initial = await stableMembership(adapter); const cpuBefore = parseContainedCgroupKeyValues( await adapter.readCgroupFile("cpu.stat"), ); - await adapter.runControl({ mode: "cpu", busyWindowMs: 500, workers: 4 }); + await adapter.runControl( + { mode: "cpu", busyWindowMs: 500, workers: 4 }, + initial.memberPids, + ); const cpuAfter = parseContainedCgroupKeyValues( await adapter.readCgroupFile("cpu.stat"), ); @@ -506,7 +531,10 @@ export async function observeContainedCgroup(manifest, adapter) { await adapter.readCgroupFile("pids.events"), ); const attemptedProcesses = observedLimits.pidsMax + 1; - await adapter.runControl({ mode: "processes", attemptedProcesses }); + await adapter.runControl( + { mode: "processes", attemptedProcesses }, + initial.memberPids, + ); const processesAfter = parseContainedCgroupKeyValues( await adapter.readCgroupFile("pids.events"), ); @@ -515,12 +543,21 @@ export async function observeContainedCgroup(manifest, adapter) { await adapter.readCgroupFile("memory.events"), ); const requestedBytes = observedLimits.memoryMaxBytes + 64 * 1024 * 1024; - await adapter.runControl({ mode: "memory", requestedBytes }); + await adapter.runControl( + { mode: "memory", requestedBytes }, + initial.memberPids, + ); const memoryAfter = parseContainedCgroupKeyValues( await adapter.readCgroupFile("memory.events"), ); await assertCandidateSurvived(adapter, initial); + const confirmedLimits = await observedAggregateLimits(adapter); + if ( + canonicalCgroupJson(confirmedLimits) !== canonicalCgroupJson(observedLimits) + ) { + throw new Error("contained cgroup observer aggregate limits changed"); + } const observedAt = adapter.now().toISOString(); const observation = { observedLimits, @@ -530,7 +567,7 @@ export async function observeContainedCgroup(manifest, adapter) { requestedBytes, oomKillBefore: memoryBefore.oom_kill, oomKillAfter: memoryAfter.oom_kill, - enforced: memoryAfter.oom_kill > memoryBefore.oom_kill, + enforced: memoryAfter.oom_kill === memoryBefore.oom_kill + 1, }, processes: { attemptedProcesses, @@ -553,6 +590,17 @@ export async function observeContainedCgroup(manifest, adapter) { }; const draft = createContainedCgroupObserverDraft(manifest, observation); await adapter.publishDraft(draft); + const finalization = validateContainedCgroupObserverFinalization( + await adapter.waitForFinalization(), + manifest, + { observedAtMs: adapter.now().getTime() }, + ); + if (finalization.status !== "FINALIZE") { + throw new Error("contained cgroup observer qualification was aborted"); + } + if (finalization.observerDraftPayloadSha256 !== draft.receiptPayloadSha256) { + throw new Error("contained cgroup observer finalization draft changed"); + } await adapter.waitForCgroupAbsent(); return validateContainedCgroupEvidence( hashedArtifact(evidencePayload(manifest, observation)), @@ -581,7 +629,8 @@ function privateFile(path, label) { realpathSync(path) !== path || metadata.uid !== process.getuid() || metadata.nlink !== 1 || - (metadata.mode & 0o777) !== 0o600 || + (metadata.mode & 0o600) !== 0o600 || + (metadata.mode & 0o077) !== 0 || metadata.size < 1 || metadata.size > maximumArtifactBytes ) { @@ -602,6 +651,19 @@ function sleep(milliseconds) { return new Promise((accept) => setTimeout(accept, milliseconds)); } +function lstatOrAbsent(path, options) { + try { + return lstatSync(path, options); + } catch (error) { + if (error?.code === "ENOENT") return null; + throw error; + } +} + +function artifactExists(path) { + return lstatOrAbsent(path) !== null; +} + async function waitUntil(predicate, label, timeoutMs) { const deadline = Date.now() + timeoutMs; while (Date.now() <= deadline) { @@ -613,6 +675,7 @@ async function waitUntil(predicate, label, timeoutMs) { function actualCgroupAdapter(manifest, paths) { const cgroupRoot = resolve(cgroupFilesystemRoot, manifest.cgroupPath); + const cgroupParentRoot = resolve(cgroupFilesystemRoot, "counterlab-v6.1"); if ( resolve( cgroupFilesystemRoot, @@ -626,6 +689,78 @@ function actualCgroupAdapter(manifest, paths) { repositoryRoot, "scripts/contained-cgroup-control-helper.mjs", ); + let expectedCgroupIdentity; + let expectedParentIdentity; + let terminationSignal; + const activeHelpers = new Set(); + + function handleTermination(signal) { + terminationSignal ??= signal; + for (const helper of activeHelpers) { + if (helper.exitCode === null && helper.signalCode === null) { + helper.kill("SIGTERM"); + } + } + } + + const signalHandlers = new Map( + ["SIGINT", "SIGTERM"].map((signal) => { + const handler = () => handleTermination(signal); + process.on(signal, handler); + return [signal, handler]; + }), + ); + + function assertNotTerminated() { + if (terminationSignal !== undefined) { + throw new Error( + `contained cgroup observer interrupted by ${terminationSignal}`, + ); + } + } + + function directoryIdentity(path, label) { + const metadata = lstatOrAbsent(path, { bigint: true }); + if (metadata === null) return null; + if ( + metadata.isSymbolicLink() || + !metadata.isDirectory() || + realpathSync(path) !== path || + statfsSync(path).type !== cgroup2Magic + ) { + throw new Error(`contained cgroup observer ${label} is invalid`); + } + return `${metadata.dev}:${metadata.ino}`; + } + + function assertCgroupParent({ allowAbsent = false } = {}) { + const identity = directoryIdentity(cgroupParentRoot, "cgroup parent"); + if (identity === null) { + if (allowAbsent && expectedParentIdentity === undefined) return false; + throw new Error("contained cgroup observer cgroup parent is absent"); + } + if ( + expectedParentIdentity !== undefined && + identity !== expectedParentIdentity + ) { + throw new Error("contained cgroup observer cgroup parent changed"); + } + expectedParentIdentity ??= identity; + return true; + } + + function currentCgroupIdentity({ allowParentAbsent = false } = {}) { + if (!assertCgroupParent({ allowAbsent: allowParentAbsent })) return null; + const identity = directoryIdentity(cgroupRoot, "cgroup path"); + if ( + identity !== null && + expectedCgroupIdentity !== undefined && + identity !== expectedCgroupIdentity + ) { + throw new Error("contained cgroup observer cgroup identity changed"); + } + return identity; + } function cgroupFile(name) { if (!allowedCgroupFiles.has(name)) { @@ -635,6 +770,9 @@ function actualCgroupAdapter(manifest, paths) { if (resolve(cgroupRoot, name) !== path) { throw new Error("contained cgroup observer file path is invalid"); } + if (currentCgroupIdentity() !== expectedCgroupIdentity) { + throw new Error("contained cgroup observer cgroup identity changed"); + } const metadata = lstatSync(path); if ( metadata.isSymbolicLink() || @@ -646,7 +784,8 @@ function actualCgroupAdapter(manifest, paths) { return path; } - async function runControl(control) { + async function runControl(control, baselineMemberPids) { + assertNotTerminated(); const args = control.mode === "memory" ? [ @@ -672,9 +811,10 @@ function actualCgroupAdapter(manifest, paths) { ]; const child = spawn(process.execPath, [helperPath, ...args], { cwd: repositoryRoot, - env: process.env, + env: { LANG: "C", LC_ALL: "C", TZ: "UTC" }, stdio: ["pipe", "pipe", "pipe"], }); + activeHelpers.add(child); let stdout = ""; let stderr = ""; let outputInvalid = false; @@ -729,14 +869,32 @@ function actualCgroupAdapter(manifest, paths) { if (!Number.isSafeInteger(child.pid) || child.pid < 1 || outputInvalid) { throw new Error("contained cgroup observer helper PID is invalid"); } + const helperIdentity = parseContainedProcessStat( + readFileSync(`/proc/${child.pid}/stat`, "utf8"), + child.pid, + ); writeFileSync(cgroupFile("cgroup.procs"), `${child.pid}\n`, "utf8"); + const membersAfterMove = parseContainedCgroupMembers( + readFileSync(cgroupFile("cgroup.procs"), "utf8"), + ); + const movedIdentity = parseContainedProcessStat( + readFileSync(`/proc/${child.pid}/stat`, "utf8"), + child.pid, + ); + if ( + !membersAfterMove.includes(child.pid) || + movedIdentity.startTimeTicks !== helperIdentity.startTimeTicks + ) { + throw new Error("contained cgroup observer helper identity changed"); + } child.stdin.end("GO\n"); const result = await bounded(completion, 15_000, "helper completion"); if ( result.error !== undefined || outputInvalid || (control.mode !== "memory" && result.code !== 0) || - (control.mode === "memory" && result.code === 0) + (control.mode === "memory" && + (result.code !== null || result.signal !== "SIGKILL")) ) { throw new Error("contained cgroup observer helper result is invalid"); } @@ -762,6 +920,23 @@ function actualCgroupAdapter(manifest, paths) { ); } } + await waitUntil( + () => + !parseContainedCgroupMembers( + readFileSync(cgroupFile("cgroup.procs"), "utf8"), + ).includes(child.pid), + "helper membership cleanup", + 2_000, + ); + const remainingMembers = parseContainedCgroupMembers( + readFileSync(cgroupFile("cgroup.procs"), "utf8"), + ); + if ( + canonicalCgroupJson(remainingMembers) !== + canonicalCgroupJson(baselineMemberPids) + ) { + throw new Error("contained cgroup observer helper descendants remain"); + } } finally { if (child.exitCode === null && child.signalCode === null) { child.kill("SIGTERM"); @@ -775,19 +950,30 @@ function actualCgroupAdapter(manifest, paths) { } await bounded(completion, 2_000, "forced helper reap"); } + activeHelpers.delete(child); + assertNotTerminated(); } } return { + dispose() { + for (const [signal, handler] of signalHandlers) { + process.off(signal, handler); + } + }, now: () => new Date(), async waitForCgroup() { - await waitUntil(() => existsSync(cgroupRoot), "cgroup creation", 10_000); - if ( - lstatSync(cgroupRoot).isSymbolicLink() || - !lstatSync(cgroupRoot).isDirectory() || - realpathSync(cgroupRoot) !== cgroupRoot - ) { - throw new Error("contained cgroup observer cgroup path is invalid"); + await waitUntil( + () => { + assertNotTerminated(); + return currentCgroupIdentity({ allowParentAbsent: true }) !== null; + }, + "cgroup creation", + 10_000, + ); + expectedCgroupIdentity = currentCgroupIdentity(); + if (expectedCgroupIdentity === null) { + throw new Error("contained cgroup observer cgroup path disappeared"); } await waitUntil( () => { @@ -806,6 +992,7 @@ function actualCgroupAdapter(manifest, paths) { ); }, readCgroupFile(name) { + assertNotTerminated(); const path = cgroupFile(name); const source = readFileSync(path, "utf8"); if (source.length === 0 || source.length > 8_192) { @@ -814,14 +1001,42 @@ function actualCgroupAdapter(manifest, paths) { return source; }, readProcessStat(pid) { + assertNotTerminated(); return readFileSync(`/proc/${pid}/stat`, "utf8"); }, runControl, publishDraft(draft) { writeArtifact(paths.observerDraftPath, draft); }, + async waitForFinalization() { + await waitUntil( + () => { + assertNotTerminated(); + return artifactExists(paths.finalizationPath); + }, + "runtime finalization", + 30_000, + ); + privateFile(paths.finalizationPath, "finalization"); + return validateContainedCgroupObserverFinalization( + JSON.parse(readFileSync(paths.finalizationPath, "utf8")), + manifest, + ); + }, async waitForCgroupAbsent() { - await waitUntil(() => !existsSync(cgroupRoot), "cgroup cleanup", 30_000); + await waitUntil( + () => { + assertNotTerminated(); + return currentCgroupIdentity() === null; + }, + "cgroup cleanup", + 30_000, + ); + assertCgroupParent(); + await sleep(20); + if (currentCgroupIdentity() !== null) { + throw new Error("contained cgroup observer cgroup path reappeared"); + } }, }; } @@ -908,14 +1123,24 @@ async function main() { throw new Error("contained cgroup observer base receipt changed"); } const adapter = actualCgroupAdapter(manifest, paths); - writeArtifact( - paths.observerReadyPath, - createContainedCgroupObserverReady(manifest), - ); - const evidence = await observeContainedCgroup(manifest, adapter); - writeArtifact(paths.evidencePath, evidence); + try { + writeArtifact( + paths.observerReadyPath, + createContainedCgroupObserverReady(manifest), + ); + const evidence = await observeContainedCgroup(manifest, adapter); + if (artifactExists(paths.failurePath)) { + throw new Error("contained cgroup observer outcome already failed"); + } + writeArtifact(paths.evidencePath, evidence); + } finally { + adapter.dispose(); + } } catch (error) { - if (!existsSync(paths.failurePath)) { + if ( + !artifactExists(paths.evidencePath) && + !artifactExists(paths.failurePath) + ) { try { writeArtifact( paths.failurePath, diff --git a/scripts/contained-cgroup-observer.test.ts b/scripts/contained-cgroup-observer.test.ts index 0486970d..4f5a7768 100644 --- a/scripts/contained-cgroup-observer.test.ts +++ b/scripts/contained-cgroup-observer.test.ts @@ -18,13 +18,26 @@ import { validateContainedCgroupObserverFailure, validateContainedCgroupObserverReady, } from "./contained-cgroup-observer.mjs"; -import { createContainedCgroupObserverManifest } from "./contained-cgroup-observer-protocol.mjs"; +import { + createContainedCgroupObserverFinalization, + createContainedCgroupObserverManifest, +} from "./contained-cgroup-observer-protocol.mjs"; const runtimeSessionId = "rt-v61-test1"; const invocationId = "1".repeat(64); const finalContainerId = "2".repeat(64); const sanitizedSpecSha256 = "3".repeat(64); const now = new Date("2026-07-20T01:02:03.000Z"); +const verifiedCleanup = { + taskAbsent: true, + containerAbsent: true, + snapshotAbsent: true, + invocationAliasAbsent: true, + imageRootfsAbsent: true, + persistedAuthorityVerified: true, + readOnlyMountsUnchanged: true, + imageRootfsUnchanged: true, +}; function manifest() { return createContainedCgroupObserverManifest({ @@ -58,6 +71,14 @@ function processStat(pid: number, parentPid: number, startTime: number) { return `${pid} (counterlab worker) ${fields.join(" ")}\n`; } +function latestDraftHash(drafts: Array>) { + const value = drafts.at(-1)?.receiptPayloadSha256; + if (typeof value !== "string") { + throw new Error("test observer draft hash is unavailable"); + } + return value; +} + function adapter(overrides: Record = {}) { const counters = { cpu: { nr_throttled: 0, throttled_usec: 0 }, @@ -106,6 +127,16 @@ function adapter(overrides: Record = {}) { async publishDraft(draft: Record) { drafts.push(draft); }, + waitForFinalization: async () => + createContainedCgroupObserverFinalization(manifest(), { + cleanup: verifiedCleanup, + cleanupVerified: true, + decisionAt: now, + observerDraftPayloadSha256: latestDraftHash(drafts), + resultReleased: false, + status: "FINALIZE", + timeoutObserved: true, + }), waitForCgroupAbsent: async () => undefined, ...overrides, }; @@ -189,6 +220,92 @@ describe("contained cgroup observer", () => { ); }); + it("rejects aborts, unstable membership and limits, and ambiguous OOM kills", async () => { + const aborted = adapter({ + waitForFinalization: async () => + createContainedCgroupObserverFinalization(manifest(), { + abortCode: "TIMEOUT_NOT_OBSERVED", + cleanupVerified: false, + cleanup: { ...verifiedCleanup, taskAbsent: false }, + decisionAt: now, + observerDraftPayloadSha256: latestDraftHash(aborted.drafts), + resultReleased: false, + status: "ABORT", + timeoutObserved: false, + }), + }); + await expect(observeContainedCgroup(manifest(), aborted)).rejects.toThrow( + /aborted/u, + ); + + const wrongDraft = adapter({ + waitForFinalization: async () => + createContainedCgroupObserverFinalization(manifest(), { + cleanup: verifiedCleanup, + cleanupVerified: true, + decisionAt: now, + observerDraftPayloadSha256: "f".repeat(64), + resultReleased: false, + status: "FINALIZE", + timeoutObserved: true, + }), + }); + await expect( + observeContainedCgroup(manifest(), wrongDraft), + ).rejects.toThrow(/draft changed/u); + + let membershipReads = 0; + const unstableMembership = adapter({ + readCgroupFile(name: string) { + if (name === "cgroup.procs") { + membershipReads += 1; + return membershipReads === 1 ? "100\n101\n" : "100\n101\n102\n"; + } + return adapter().readCgroupFile(name); + }, + readProcessStat(pid: number) { + if (pid === 100) return processStat(100, 1, 12345); + if (pid === 101) return processStat(101, 100, 12346); + return processStat(102, 100, 12347); + }, + }); + await expect( + observeContainedCgroup(manifest(), unstableMembership), + ).rejects.toThrow(/stabilize/u); + + const stable = adapter(); + let memoryLimitReads = 0; + const changedLimits = { + ...stable, + readCgroupFile(name: string) { + if (name === "memory.max") { + memoryLimitReads += 1; + return String( + memoryLimitReads === 1 ? 512 * 1024 * 1024 : 512 * 1024 * 1024 + 1, + ); + } + return stable.readCgroupFile(name); + }, + }; + await expect( + observeContainedCgroup(manifest(), changedLimits), + ).rejects.toThrow(/limits changed/u); + + const ambiguousOomBase = adapter(); + const ambiguousOom = { + ...ambiguousOomBase, + async runControl(control: Record) { + await ambiguousOomBase.runControl(control); + if (control.mode === "memory") { + await ambiguousOomBase.runControl(control); + } + }, + }; + await expect( + observeContainedCgroup(manifest(), ambiguousOom), + ).rejects.toThrow(/evidence binding/u); + }); + it("binds ready and draft receipts and rejects mutation", () => { const input = manifest(); const ready = createContainedCgroupObserverReady(input, { diff --git a/services/runner/src/counterlab_runner/timeout_proof.py b/services/runner/src/counterlab_runner/timeout_proof.py index e23c099b..8c65a769 100644 --- a/services/runner/src/counterlab_runner/timeout_proof.py +++ b/services/runner/src/counterlab_runner/timeout_proof.py @@ -579,7 +579,8 @@ def _validate_aggregate_limit_evidence( for field in ("requestedBytes", "oomKillBefore", "oomKillAfter") ) or memory_control.get("requestedBytes") <= intended["memoryBytes"] - or memory_control.get("oomKillAfter") <= memory_control.get("oomKillBefore") + or memory_control.get("oomKillAfter") + != memory_control.get("oomKillBefore") + 1 or memory_control.get("enforced") is not True or not all( _safe_integer( diff --git a/services/runner/tests/test_timeout_proof.py b/services/runner/tests/test_timeout_proof.py index 5d30b655..e3ca1d47 100644 --- a/services/runner/tests/test_timeout_proof.py +++ b/services/runner/tests/test_timeout_proof.py @@ -297,7 +297,15 @@ def test_rootless_receipt_binds_control_and_exact_adapter_authority() -> None: broken_counter = json.loads(json.dumps(rootless)) broken_counter["aggregateLimitEvidence"]["negativeControls"]["memory"][ "oomKillAfter" - ] = 0 + ] = 2 + evidence_payload = { + key: value + for key, value in broken_counter["aggregateLimitEvidence"].items() + if key != "receiptPayloadSha256" + } + broken_counter["aggregateLimitEvidence"]["receiptPayloadSha256"] = _hash( + evidence_payload + ) payload = { key: value for key, value in broken_counter.items() From f0c9f7bfcca474ed700b51975f03246df9f5158d Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 07:21:13 +0530 Subject: [PATCH 046/143] test(learner): cover recovery and notebook intake --- apps/web/e2e/recovery-and-intake.spec.ts | 430 +++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 apps/web/e2e/recovery-and-intake.spec.ts diff --git a/apps/web/e2e/recovery-and-intake.spec.ts b/apps/web/e2e/recovery-and-intake.spec.ts new file mode 100644 index 00000000..69685e19 --- /dev/null +++ b/apps/web/e2e/recovery-and-intake.spec.ts @@ -0,0 +1,430 @@ +import { expect, test, type Page } from "./cloak-test"; +import { SAMPLE_LEAKAGE_QUESTION } from "../shared/sample-authority"; + +const liveQuestion = + "Does this notebook score show that the model generalizes to customers it has never seen?"; +const supportedNotebookPath = new URL( + "../../../fixtures/notebooks/customer_churn_leakage.ipynb", + import.meta.url, +).pathname; + +const readyHealth = { + ok: true, + data: { + platform: "cloudflare-workers", + sample: "available", + replay: "available", + liveGpt: "configured", + liveCodex: "configured", + liveKernel: "configured", + readiness: "ready", + sandbox: "credential-and-privilege-boundary", + generationFilesystemReadIsolation: "OS_ENFORCED", + release: { + status: "bound", + workerVersionId: "11111111-2222-3333-4444-555555555555", + workerVersionTag: `git-${"a".repeat(40)}`, + workerEvidenceCommit: "a".repeat(40), + runnerSourceCommit: "b".repeat(40), + runnerImageDigest: `sha256:${"c".repeat(64)}`, + timeoutCleanupReceiptSha256: "d".repeat(64), + aggregateLimitEvidenceSha256: "9".repeat(64), + runtimePolicySha256: "e".repeat(64), + proofDependencyManifestSha256: "f".repeat(64), + workerArtifactClassification: "PROCESS_BOUND_PARTIAL", + workerArtifactManifestSha256: "1".repeat(64), + workerBundleSha256: "2".repeat(64), + clientAssetsSha256: "3".repeat(64), + clientAssetCount: 27, + clientPublicAssetsSha256: "4".repeat(64), + clientPublicAssetCount: 25, + viteVersion: "8.1.4", + wranglerVersion: "4.110.0", + }, + requestId: "e2e-ready-intake", + }, +} as const; + +async function reset(page: Page): Promise { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.evaluate(() => { + window.localStorage.clear(); + window.sessionStorage.clear(); + }); + await page.reload({ waitUntil: "domcontentloaded" }); + await expect( + page.getByRole("heading", { + name: "What result are you trying to understand?", + }), + ).toBeVisible(); +} + +async function storedSessionId(page: Page): Promise { + return page.evaluate(() => + window.localStorage.getItem("counterlab.sessionId"), + ); +} + +async function openSampleModelDuel(page: Page): Promise { + await reset(page); + await page.getByRole("button", { name: /Try verified sample/i }).click(); + await expect( + page.getByRole("heading", { name: /What do you think the score means/i }), + ).toBeVisible(); + await page.getByRole("button", { name: /Compare two explanations/i }).click(); + await expect( + page.getByRole("heading", { + name: /Does your current explanation capture what you mean/i, + }), + ).toBeVisible(); +} + +async function installReadyHealth(page: Page): Promise { + await page.route("**/api/health*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(readyHealth), + }); + }); +} + +async function openReadyLiveIntake(page: Page): Promise { + await installReadyHealth(page); + await reset(page); + await page.getByLabel("Your question or claim").fill(liveQuestion); + await page.getByRole("button", { name: /Test this claim/i }).click(); + await expect( + page.getByRole("heading", { + name: /Start with evidence that matches your question/i, + }), + ).toBeVisible(); + await page + .getByRole("button", { name: /Check live notebook tools/i }) + .click(); + await expect( + page.getByRole("heading", { name: "Test my notebook" }), + ).toBeVisible(); + await expect( + page.getByText("Source-bound hosted notebook runner is ready"), + ).toBeVisible(); + await expect(page.getByLabel(/Attach a supported notebook/i)).toBeEnabled(); +} + +function isPostTo(requestUrl: string, method: string, suffix: string): boolean { + return method === "POST" && new URL(requestUrl).pathname.endsWith(suffix); +} + +for (const response of [ + { + action: "Reject", + terminalState: "REJECTED_BY_LEARNER", + }, + { + action: "Not enough evidence", + terminalState: "INSUFFICIENT_EVIDENCE", + }, +] as const) { + test(`${response.action} preserves the claim and resubmits only in a fresh session`, async ({ + page, + }) => { + const proposalSessionIds: string[] = []; + page.on("request", (request) => { + if (!isPostTo(request.url(), request.method(), "/belief-test")) return; + const segments = new URL(request.url()).pathname.split("/"); + const encodedSessionId = segments.at(-2); + if (encodedSessionId !== undefined) { + proposalSessionIds.push(decodeURIComponent(encodedSessionId)); + } + }); + + await openSampleModelDuel(page); + const closedSessionId = await storedSessionId(page); + expect(closedSessionId).toMatch(/^session_/u); + await expect(page).toHaveURL( + new RegExp(`/session/${encodeURIComponent(closedSessionId!)}$`, "u"), + ); + const preservedClaim = ( + await page.locator("blockquote").first().innerText() + ).trim(); + expect(preservedClaim).toBe(SAMPLE_LEAKAGE_QUESTION); + + await page.getByText("More ways to respond", { exact: true }).click(); + await page + .getByRole("button", { name: response.action, exact: true }) + .click(); + await expect( + page.getByText("Your response closed that explanation."), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: /Compare two explanations/i }), + ).toBeDisabled(); + await expect(page.locator("body")).not.toContainText( + /REJECTED_BY_LEARNER|INSUFFICIENT_EVIDENCE/u, + ); + const terminalResponse = await page.request.get( + `/api/sessions/${encodeURIComponent(closedSessionId!)}`, + ); + expect(terminalResponse.ok()).toBe(true); + expect((await terminalResponse.json()).data.state).toBe( + response.terminalState, + ); + + await page.reload({ waitUntil: "domcontentloaded" }); + await expect( + page.getByText("Your response closed that explanation."), + ).toBeVisible(); + await expect( + page.getByText(SAMPLE_LEAKAGE_QUESTION, { exact: true }), + ).toBeVisible(); + expect(await storedSessionId(page)).toBe(closedSessionId); + + await page + .getByRole("button", { name: /Revise in a new investigation/i }) + .click(); + await expect( + page.getByRole("heading", { name: /What do you think the score means/i }), + ).toBeVisible(); + const freshSessionId = await storedSessionId(page); + expect(freshSessionId).toMatch(/^session_/u); + expect(freshSessionId).not.toBe(closedSessionId); + await expect(page).toHaveURL( + new RegExp(`/session/${encodeURIComponent(freshSessionId!)}$`, "u"), + ); + expect( + await page.evaluate(() => + window.localStorage.getItem("counterlab.claim"), + ), + ).toBe(preservedClaim); + await expect( + page.getByRole("button", { name: /Compare two explanations/i }), + ).toBeEnabled(); + + await page + .getByRole("button", { name: /Compare two explanations/i }) + .click(); + await expect( + page.getByRole("heading", { + name: /Does your current explanation capture what you mean/i, + }), + ).toBeVisible(); + expect(proposalSessionIds).toEqual([closedSessionId, freshSessionId]); + + await page.goBack({ waitUntil: "domcontentloaded" }); + await expect(page).toHaveURL( + new RegExp(`/session/${encodeURIComponent(closedSessionId!)}$`, "u"), + ); + await expect( + page.getByText("Your response closed that explanation."), + ).toBeVisible(); + await expect( + page.getByText(SAMPLE_LEAKAGE_QUESTION, { exact: true }), + ).toBeVisible(); + expect(await storedSessionId(page)).toBe(closedSessionId); + + await page.goForward({ waitUntil: "domcontentloaded" }); + await expect(page).toHaveURL( + new RegExp(`/session/${encodeURIComponent(freshSessionId!)}$`, "u"), + ); + await expect( + page.getByRole("heading", { + name: /Does your current explanation capture what you mean/i, + }), + ).toBeVisible(); + expect(await storedSessionId(page)).toBe(freshSessionId); + await page + .getByRole("button", { name: /Yes, this captures my view/i }) + .click(); + await expect( + page.getByRole("heading", { + name: /Seal what you expect before the result appears/i, + }), + ).toBeVisible(); + expect(proposalSessionIds).toEqual([closedSessionId, freshSessionId]); + }); +} + +test("live intake cannot continue without an uploaded supported artifact", async ({ + page, +}) => { + await openReadyLiveIntake(page); + + expect(await storedSessionId(page)).toBeNull(); + await expect(page.getByText(/Notebook intake passed/i)).toHaveCount(0); + await expect( + page.getByRole("button", { name: /Compare two explanations/i }), + ).toHaveCount(0); + await expect(page.locator("body")).not.toContainText(/Preparing artifact/i); +}); + +test("malformed live upload remains at intake and creates no session", async ({ + page, +}) => { + let sessionCreationRequests = 0; + page.on("request", (request) => { + if (isPostTo(request.url(), request.method(), "/api/live/sessions")) { + sessionCreationRequests += 1; + } + }); + await openReadyLiveIntake(page); + + await page.getByLabel(/Attach a supported notebook/i).setInputFiles({ + name: "broken.ipynb", + mimeType: "application/x-ipynb+json", + buffer: Buffer.from("not-json"), + }); + + await expect(page.getByRole("alert")).toContainText(/not valid JSON/i); + await expect( + page.getByRole("heading", { name: "Test my notebook" }), + ).toBeVisible(); + await expect(page.getByLabel(/Attach a supported notebook/i)).toBeEnabled(); + expect(sessionCreationRequests).toBe(0); + expect(await storedSessionId(page)).toBeNull(); +}); + +test("unsupported live upload is refused without creating a session", async ({ + page, +}) => { + let sessionCreationRequests = 0; + page.on("request", (request) => { + if (isPostTo(request.url(), request.method(), "/api/live/sessions")) { + sessionCreationRequests += 1; + } + }); + await openReadyLiveIntake(page); + const unsupportedNotebook = { + cells: [ + { + cell_type: "code", + execution_count: null, + metadata: {}, + outputs: [], + source: ["%load_ext custom_extension\n", "import requests\n"], + }, + ], + metadata: {}, + nbformat: 4, + nbformat_minor: 5, + }; + + await page.getByLabel(/Attach a supported notebook/i).setInputFiles({ + name: "unsupported.ipynb", + mimeType: "application/x-ipynb+json", + buffer: Buffer.from(JSON.stringify(unsupportedNotebook)), + }); + + await expect(page.getByText(/UNSUPPORTED_MAGIC/i)).toBeVisible(); + await expect(page.getByText(/EXTERNAL_NETWORK_DEPENDENCY/i)).toBeVisible(); + await expect( + page.getByRole("button", { name: /Compare two explanations/i }), + ).toBeDisabled(); + expect(sessionCreationRequests).toBe(0); + expect(await storedSessionId(page)).toBeNull(); +}); + +test("a supported live upload creates one source-bound session", async ({ + page, +}) => { + let uploadRequests = 0; + let sessionCreationRequests = 0; + page.on("request", (request) => { + if (isPostTo(request.url(), request.method(), "/api/artifacts")) { + uploadRequests += 1; + } + if (isPostTo(request.url(), request.method(), "/api/live/sessions")) { + sessionCreationRequests += 1; + } + }); + await openReadyLiveIntake(page); + + await page + .getByLabel(/Attach a supported notebook/i) + .setInputFiles(supportedNotebookPath); + + await expect( + page.getByRole("heading", { name: /What do you think the score means/i }), + ).toBeVisible(); + await expect(page.getByText("customer_churn_leakage.ipynb")).toBeVisible(); + await expect( + page.getByRole("button", { name: /Compare two explanations/i }), + ).toBeEnabled(); + expect(await storedSessionId(page)).toMatch(/^session_/u); + expect(uploadRequests).toBe(1); + expect(sessionCreationRequests).toBe(1); +}); + +test("an interrupted upload accepts the same file on retry", async ({ + page, +}) => { + let uploadAttempts = 0; + await page.route("**/api/artifacts", async (route) => { + if (route.request().method() !== "POST") { + await route.continue(); + return; + } + uploadAttempts += 1; + if (uploadAttempts === 1) { + await route.abort("connectionreset"); + return; + } + await route.continue(); + }); + await openReadyLiveIntake(page); + const upload = page.getByLabel(/Attach a supported notebook/i); + + await upload.setInputFiles(supportedNotebookPath); + await expect(page.getByRole("alert")).toContainText( + /could not reach the API/i, + ); + await expect(upload).toBeEnabled(); + await upload.setInputFiles(supportedNotebookPath); + + await expect( + page.getByRole("heading", { name: /What do you think the score means/i }), + ).toBeVisible(); + expect(uploadAttempts).toBe(2); + expect(await storedSessionId(page)).toMatch(/^session_/u); +}); + +test("a lost private-session response retries without re-uploading", async ({ + page, +}) => { + let uploadRequests = 0; + let sessionCreationAttempts = 0; + page.on("request", (request) => { + if (isPostTo(request.url(), request.method(), "/api/artifacts")) { + uploadRequests += 1; + } + }); + await page.route("**/api/live/sessions", async (route) => { + sessionCreationAttempts += 1; + if (sessionCreationAttempts === 1) { + await route.abort("connectionreset"); + return; + } + await route.continue(); + }); + await openReadyLiveIntake(page); + + await page + .getByLabel(/Attach a supported notebook/i) + .setInputFiles(supportedNotebookPath); + await expect(page.getByRole("alert")).toContainText( + /could not reach the API/i, + ); + await expect( + page.getByRole("button", { name: /Retry private session setup/i }), + ).toBeEnabled(); + await expect(page.getByText("customer_churn_leakage.ipynb")).toBeVisible(); + + await page + .getByRole("button", { name: /Retry private session setup/i }) + .click(); + await expect( + page.getByRole("heading", { name: /What do you think the score means/i }), + ).toBeVisible(); + expect(uploadRequests).toBe(1); + expect(sessionCreationAttempts).toBe(2); + expect(await storedSessionId(page)).toMatch(/^session_/u); +}); From 88f96c9cb10665740a66ee6a8296fa9a59a51381 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 07:24:11 +0530 Subject: [PATCH 047/143] docs(progress): record observer and recovery gates --- docs/EVALUATION.md | 13 +++-- docs/PROGRESS.md | 106 +++++++++++++++++++++++++++++++++----- docs/RELEASE_CHECKLIST.md | 2 +- 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/docs/EVALUATION.md b/docs/EVALUATION.md index 59530b87..9313be45 100644 --- a/docs/EVALUATION.md +++ b/docs/EVALUATION.md @@ -103,11 +103,14 @@ They are not browser evidence for branch `feat/learner-ux-v6.1`. ### Current v6.1 local specification -The Cloak-only Playwright suite collected 22 tests from one spec file covering -1440 × 900, 1280 × 720, 390 × 844, keyboard/focus, reduced motion, refresh -phases, sample, replay, mocked live, unsupported intake, rejected verification, -failed transfer, successful patch, downloads, Boundary Hunt, exact tables, no -horizontal overflow, and no model call from local controls. +The current Cloak-only Playwright suite statically collects 40 tests from three +spec files. It covers the desktop/mobile viewports, keyboard/focus, reduced +motion, refresh phases, sample, replay, mocked live, unsupported intake, +rejected verification, failed transfer, successful patch, downloads, Boundary +Hunt, exact tables, no horizontal overflow, no model call from local controls, +fresh-session recovery after both learner terminal responses, browser +back/forward, absent and malformed upload, same-file retry, and lost private +session response recovery without re-upload. Zero of those tests ran in this pass because `CLOAK_CDP_ENDPOINT` was absent. The harness exited with status 1 and refused to launch stock Chromium. No diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 13ce8b1e..c709151c 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,6 +1,6 @@ # Progress -Updated: 2026-07-20 +Updated: 2026-07-21 ## Active v6.1 release board @@ -12,17 +12,17 @@ pass is never presented as a browser or production pass. ### Current checkpoint - **Branch:** `feat/learner-ux-v6.1` -- **Latest committed checkpoint:** `9348093a15942e7fb59c2755d3e080a20785b5c0`. - The fresh exact-image isolation rerun is independently hash-bound through - release-check receipt v5, deployment receipt v7, Worker release identity, - exact public response validation, and production-smoke report v6. Historical - receipt readers remain compatible. The worktree contains only the preserved, - untracked `scripts/.fuse_hidden0000a9600000801f`, which remains unopened, - unstaged, and undeleted. The complete web suite passes **616/616**; the - correctly contained and socket-enabled root suite passes **667/670** with - **2 skipped**, leaving exactly three stale scientific-evidence failures. - Qualification, deployment, browser qualification, learner evidence, and - submission remain unclaimed. +- **Latest committed checkpoint:** `f0c9f7bfcca474ed700b51975f03246df9f5158d`. + The worktree contains only the preserved, untracked + `scripts/.fuse_hidden0000a9600000801f`. The source now has a self-hashed + cgroup request, observer, FINALIZE/ABORT protocol, fixed negative controls, + fail-closed evidence validation, qualified-receipt derivation/store, and 40 + statically collected Playwright journeys. The production runtime still does + not start the observer, write the runtime-owned decision, bind the complete + invocation artifact chain, or select the qualified receipt for timeout + proof. No exact image, real cgroup sentinel, qualification, deployment, + rendered browser journey, learner observation, video, Devpost publication, + or submission receipt is claimed. - **Earlier product checkpoint:** `5259a495948a68024702ba5239bf8057d1785f08`. The owner partitioned the preserved integration delta into nine commits and fast-forwarded both @@ -3381,3 +3381,85 @@ read-only during ownership diagnosis, remains byte-identical to and align the public repository, video, `/feedback` ID, Devpost fields, and submission receipt before the authorized feature push and fast-forward merge to `main`. + +## Finish Commander observer and recovery checkpoint — 2026-07-21T01:51:33Z + +Deadline: `2026-07-22T00:00:00Z`; **22 hours 08 minutes 27 seconds remained at +this checkpoint**. The feature branch is at committed HEAD `f0c9f7b`; the only +worktree entry besides this progress update is the preserved FUSE tombstone +`scripts/.fuse_hidden0000a9600000801f`. It remains unchanged and unstaged. + +### Independently committed source slices + +- `0077f1a feat(runtime): add fixed cgroup controls` adds the release-only + READY-to-GO memory, process, and CPU control helper. +- `fbc5ba9 feat(runtime): bind cgroup observer identity` and `37684dd + feat(runtime): define cgroup observer protocol` bind the observer to the + exact runtime session, attestation, drivers, base receipt, invocation, and + repository-contained handshake paths. +- `771d8bf feat(runtime): observe aggregate cgroup controls` adds the actual + source-attested cgroup-v2 observer and its synthetic characterization tests. +- `317ba44 fix(runtime): bind qualified receipts to live sessions` makes the + receipt store resolve live observer bindings itself, require the exact + `.rt/` path, reject stale observations, and persist once. +- `c2c1817 feat(runtime): define cgroup finalization signal` introduces an + exact self-hashed runtime FINALIZE/ABORT decision. +- `cf90647 fix(runtime): fail closed on cgroup qualification` binds FINALIZE to + the exact draft and eight cleanup facts, keeps ABORT unconditionally + fail-closed, treats only `ENOENT` as absence, pins parent/child cgroup + identities with bigint device/inode values, double-checks limits and + membership, requires exact candidate restoration after each helper, requires + memory `SIGKILL` plus an exact one-event OOM delta, bounds descendant reaping, + minimizes helper environments, handles observer termination, and prevents + evidence/failure coexistence at the observer output boundary. +- `f0c9f7b test(learner): cover recovery and notebook intake` adds eight + Playwright acceptance journeys for Reject and Not enough evidence recovery, + fresh-session resubmission, refresh/back/forward, absent/malformed/ + unsupported/supported upload, same-file retry, and lost-session-response + retry without re-upload. + +### Exact verification in these slices + +- Complete cgroup observer/protocol/helper/evidence/qualified-receipt Vitest: + **7 files, 22/22 passed**. +- Complete Python timeout-proof file: **7/7 passed**. +- Repository, web client, generated Worker, and Worker TypeScript: **passed**. +- Recovery/intake E2E TypeScript: **passed**. +- Playwright static collection with an inert Cloak endpoint: **40 tests in 3 + files**. The endpoint was not contacted, no browser launched, and this is not + rendered qualification evidence. +- Scoped Prettier, Python compilation, Node syntax, Git whitespace, and secret + scan across the 11 runtime files plus the isolated E2E file: **passed**. + +### Live issue matrix + +| Gate | Current source status | Required production evidence | +| --- | --- | --- | +| Observer semantics | **source implemented and locally tested** | Real delegated cgroup-v2 memory OOM, PID denial, CPU throttling, exact membership, identity, limit, FINALIZE, and cleanup observations | +| Runtime integration | **open** | Runtime starts observer before the candidate, waits ARMED/draft, writes FINALIZE only after timeout/no-result/complete cleanup and ABORT otherwise, then waits for the exclusive terminal artifact | +| Qualified receipt chain | **partial** | Store reads and validates exact manifest/draft/finalization/evidence files, requires `failure.json` to be absent, binds finalization into evidence, and supplies the qualified receipt hashes to TypeScript and Python timeout proof | +| Exact release | **not run** | Clean source `S`, one exact image, real sentinel, refreshed SBOM/VEX/scientific evidence `E`, qualification and release-check receipts | +| Cloudflare | **not run** | Contained Wrangler account verification, qualified Worker/Container deployment, exact public identity, D1 migrations, production smoke | +| Browser and accessibility | **40 tests statically collected; 0 rendered** | All tests plus the manual responsive, keyboard, screen-reader, reduced-motion, console/network, download, reconnect, and Web Vitals matrix through CloakBrowser on the exact public tuple | +| Learner impact and submission | **`NO_DATA`; externally unverified** | Real consented aggregates or unchanged `NO_DATA`, public repo/video/feedback ID, aligned Devpost entry, logged-out link audit, and submission receipt | + +### Ordered remaining work + +1. Bind finalization into aggregate evidence and make the qualified store verify + the complete exclusive invocation artifact chain. +2. Wire qualification mode through `executeContainedRun`, observer startup, + ARMED/draft waits, runtime FINALIZE/ABORT creation, qualified receipt + persistence, control-receipt hashes, and Python consumers. Ordinary learner + execution must remain unchanged. +3. Execute the real sentinel only if the repository constitution permits the + required delegated `/sys/fs/cgroup` and owned `/proc//stat` reads; + otherwise preserve an explicit no-go instead of synthesizing evidence. +4. Freeze clean source `S`, build once, regenerate exact evidence, qualify, run + every deterministic gate, verify contained Wrangler identity, and deploy + only the qualified tuple. +5. Execute all 40 current journeys and the manual accessibility/performance + matrix through an injected CloakBrowser endpoint against the exact public + release. +6. Keep learner evidence honest, finish and verify the public submission + package, then push the feature branch, verify the remote tip, fast-forward + `main`, and push/verify `main` only after every ship gate is factual. diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index 9e8c32a9..f4d138a8 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -348,7 +348,7 @@ after the final source freeze. no normal-home credential read is permitted. - [ ] Deploy the exact qualified Container/Worker tuple and bind the active version to the release source, image, Worker, client, and receipt domains. -- [ ] Execute production smoke plus all 31 CloakBrowser journeys, including +- [ ] Execute production smoke plus all 40 CloakBrowser journeys, including desktop/mobile sample, replay, supported live, malformed/unsupported, refusal, transfer fail/pass, patch, Capsule, refresh/reconnect, back/forward, accessibility, console/network, and Web Vitals checks. From bb1cc0b3e5da8215577dc9f12cf93305cccad08e Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 07:29:08 +0530 Subject: [PATCH 048/143] feat(runtime): bind cgroup finalization evidence --- scripts/contained-cgroup-evidence.mjs | 4 ++- scripts/contained-cgroup-evidence.test.ts | 3 +- scripts/contained-cgroup-observer.mjs | 14 ++++++-- scripts/contained-cgroup-observer.test.ts | 34 +++++++++++++------ .../contained-qualified-receipt-store.test.ts | 3 +- ...ntained-qualified-rootless-receipt.test.ts | 3 +- scripts/timeout-cleanup-receipt.test.ts | 16 ++++++++- scripts/timeout-cleanup-receipt.ts | 9 ++--- .../src/counterlab_runner/timeout_proof.py | 4 ++- services/runner/tests/test_timeout_proof.py | 3 +- 10 files changed, 69 insertions(+), 24 deletions(-) diff --git a/scripts/contained-cgroup-evidence.mjs b/scripts/contained-cgroup-evidence.mjs index 24a3cdc7..f877d77b 100644 --- a/scripts/contained-cgroup-evidence.mjs +++ b/scripts/contained-cgroup-evidence.mjs @@ -17,6 +17,7 @@ const topLevelKeys = [ "cgroupVersion", "cleanup", "finalContainerId", + "finalizationPayloadSha256", "invocationId", "membership", "negativeControls", @@ -203,7 +204,7 @@ export function validateContainedCgroupEvidence(value, expectedValue) { : new Set(); const { receiptPayloadSha256, ...payload } = evidence; if ( - evidence.schemaVersion !== "1" || + evidence.schemaVersion !== "2" || evidence.status !== "OBSERVED" || evidence.authority !== "linux-cgroup-v2" || evidence.cgroupVersion !== 2 || @@ -211,6 +212,7 @@ export function validateContainedCgroupEvidence(value, expectedValue) { evidence.finalContainerId !== expected.finalContainerId || evidence.sanitizedSpecSha256 !== expected.sanitizedSpecSha256 || evidence.runtimeAttestationSha256 !== expected.runtimeAttestationSha256 || + !sha256(evidence.finalizationPayloadSha256) || evidence.cgroupId !== `counterlab-v6.1-${expected.invocationId}` || evidence.cgroupPath !== `counterlab-v6.1/${expected.invocationId}` || evidence.cgroupIdentity !== createContainedCgroupIdentity(expected) || diff --git a/scripts/contained-cgroup-evidence.test.ts b/scripts/contained-cgroup-evidence.test.ts index 78cbf3f5..11bd3371 100644 --- a/scripts/contained-cgroup-evidence.test.ts +++ b/scripts/contained-cgroup-evidence.test.ts @@ -20,7 +20,7 @@ const intendedAggregateLimits = { function evidence() { const memberPids = [100, 101]; const payload = { - schemaVersion: "1", + schemaVersion: "2", status: "OBSERVED", authority: "linux-cgroup-v2", cgroupVersion: 2, @@ -33,6 +33,7 @@ function evidence() { }), invocationId, finalContainerId, + finalizationPayloadSha256: "f".repeat(64), sanitizedSpecSha256, runtimeAttestationSha256: "4".repeat(64), observedLimits: { diff --git a/scripts/contained-cgroup-observer.mjs b/scripts/contained-cgroup-observer.mjs index 5805b05c..a5c28f69 100644 --- a/scripts/contained-cgroup-observer.mjs +++ b/scripts/contained-cgroup-observer.mjs @@ -198,9 +198,9 @@ function expectedEvidence(manifest) { }; } -function evidencePayload(manifest, observation) { +function evidencePayload(manifest, observation, finalization) { return { - schemaVersion: "1", + schemaVersion: "2", status: "OBSERVED", authority: "linux-cgroup-v2", cgroupVersion: 2, @@ -209,6 +209,8 @@ function evidencePayload(manifest, observation) { cgroupIdentity: manifest.cgroupIdentity, invocationId: manifest.invocationId, finalContainerId: manifest.finalContainerId, + finalizationPayloadSha256: + finalization?.receiptPayloadSha256 ?? "0".repeat(64), sanitizedSpecSha256: manifest.sanitizedSpecSha256, runtimeAttestationSha256: manifest.runtimeAttestationSha256, observedLimits: observation.observedLimits, @@ -602,8 +604,14 @@ export async function observeContainedCgroup(manifest, adapter) { throw new Error("contained cgroup observer finalization draft changed"); } await adapter.waitForCgroupAbsent(); + const completedObservation = { + ...observation, + observedAt: adapter.now().toISOString(), + }; return validateContainedCgroupEvidence( - hashedArtifact(evidencePayload(manifest, observation)), + hashedArtifact( + evidencePayload(manifest, completedObservation, finalization), + ), expectedEvidence(manifest), ); } diff --git a/scripts/contained-cgroup-observer.test.ts b/scripts/contained-cgroup-observer.test.ts index 4f5a7768..f2a14d15 100644 --- a/scripts/contained-cgroup-observer.test.ts +++ b/scripts/contained-cgroup-observer.test.ts @@ -87,9 +87,13 @@ function adapter(overrides: Record = {}) { }; const controls: Array> = []; const drafts: Array> = []; + const finalizations: Array< + ReturnType + > = []; return { controls, drafts, + finalizations, now: () => now, waitForCgroup: async () => undefined, readCgroupFile(name: string) { @@ -127,16 +131,22 @@ function adapter(overrides: Record = {}) { async publishDraft(draft: Record) { drafts.push(draft); }, - waitForFinalization: async () => - createContainedCgroupObserverFinalization(manifest(), { - cleanup: verifiedCleanup, - cleanupVerified: true, - decisionAt: now, - observerDraftPayloadSha256: latestDraftHash(drafts), - resultReleased: false, - status: "FINALIZE", - timeoutObserved: true, - }), + waitForFinalization: async () => { + const finalization = createContainedCgroupObserverFinalization( + manifest(), + { + cleanup: verifiedCleanup, + cleanupVerified: true, + decisionAt: now, + observerDraftPayloadSha256: latestDraftHash(drafts), + resultReleased: false, + status: "FINALIZE", + timeoutObserved: true, + }, + ); + finalizations.push(finalization); + return finalization; + }, waitForCgroupAbsent: async () => undefined, ...overrides, }; @@ -174,6 +184,10 @@ describe("contained cgroup observer", () => { { mode: "memory", requestedBytes: 576 * 1024 * 1024 }, ]); expect(fake.drafts).toHaveLength(1); + expect(fake.finalizations).toHaveLength(1); + expect(evidence.finalizationPayloadSha256).toBe( + fake.finalizations.at(0)?.receiptPayloadSha256, + ); expect(() => validateContainedCgroupObserverDraft(fake.drafts[0], input), ).not.toThrow(); diff --git a/scripts/contained-qualified-receipt-store.test.ts b/scripts/contained-qualified-receipt-store.test.ts index 5f7bbbf5..a19f625c 100644 --- a/scripts/contained-qualified-receipt-store.test.ts +++ b/scripts/contained-qualified-receipt-store.test.ts @@ -71,7 +71,7 @@ function baseReceipt() { function aggregateEvidence(bindings = observerBindings) { const memberPids = [100, 101]; return hashed({ - schemaVersion: "1", + schemaVersion: "2", status: "OBSERVED", authority: "linux-cgroup-v2", cgroupVersion: 2, @@ -84,6 +84,7 @@ function aggregateEvidence(bindings = observerBindings) { }), invocationId, finalContainerId, + finalizationPayloadSha256: "f".repeat(64), sanitizedSpecSha256, runtimeAttestationSha256: bindings.runtimeAttestationSha256, observedLimits: { diff --git a/scripts/contained-qualified-rootless-receipt.test.ts b/scripts/contained-qualified-rootless-receipt.test.ts index e4526b19..29ae5b10 100644 --- a/scripts/contained-qualified-rootless-receipt.test.ts +++ b/scripts/contained-qualified-rootless-receipt.test.ts @@ -66,7 +66,7 @@ function baseReceipt() { function aggregateEvidence() { const memberPids = [100, 101]; return hashed({ - schemaVersion: "1", + schemaVersion: "2", status: "OBSERVED", authority: "linux-cgroup-v2", cgroupVersion: 2, @@ -79,6 +79,7 @@ function aggregateEvidence() { }), invocationId, finalContainerId, + finalizationPayloadSha256: "f".repeat(64), sanitizedSpecSha256, runtimeAttestationSha256: observerBindings.runtimeAttestationSha256, observedLimits: { diff --git a/scripts/timeout-cleanup-receipt.test.ts b/scripts/timeout-cleanup-receipt.test.ts index e4b2e375..958329d8 100644 --- a/scripts/timeout-cleanup-receipt.test.ts +++ b/scripts/timeout-cleanup-receipt.test.ts @@ -26,7 +26,7 @@ function aggregateLimitEvidence() { const sanitizedSpecSha256 = "5".repeat(64); const memberPids = [100, 101]; const payload = { - schemaVersion: "1" as const, + schemaVersion: "2" as const, status: "OBSERVED" as const, authority: "linux-cgroup-v2" as const, cgroupVersion: 2 as const, @@ -37,6 +37,7 @@ function aggregateLimitEvidence() { ), invocationId, finalContainerId, + finalizationPayloadSha256: "f".repeat(64), sanitizedSpecSha256, runtimeAttestationSha256: "6".repeat(64), observedLimits: { @@ -159,5 +160,18 @@ describe("timeout cleanup aggregate resource authority", () => { limitMode: QUALIFIED_AGGREGATE_LIMIT_MODE, }), ).toThrow(); + + const ambiguousOom = aggregateLimitEvidence(); + ambiguousOom.negativeControls.memory.oomKillAfter = 2; + const { receiptPayloadSha256: _ignored, ...payload } = ambiguousOom; + ambiguousOom.receiptPayloadSha256 = sha256(canonicalJson(payload)); + expect(() => + assertQualifiedAggregateRuntimeLimits({ + ...aggregateInput(), + aggregateLimitEvidence: ambiguousOom, + aggregateLimitIntentEnforced: true, + limitMode: QUALIFIED_AGGREGATE_LIMIT_MODE, + }), + ).toThrow(/negative-control/u); }); }); diff --git a/scripts/timeout-cleanup-receipt.ts b/scripts/timeout-cleanup-receipt.ts index 1fa18dfa..8a4e7a6c 100644 --- a/scripts/timeout-cleanup-receipt.ts +++ b/scripts/timeout-cleanup-receipt.ts @@ -37,7 +37,7 @@ const AggregateLimitIntentSchema = z.strictObject({ const AggregateLimitEvidenceSchema = z .strictObject({ - schemaVersion: z.literal("1"), + schemaVersion: z.literal("2"), status: z.literal("OBSERVED"), authority: z.literal("linux-cgroup-v2"), cgroupVersion: z.literal(2), @@ -46,6 +46,7 @@ const AggregateLimitEvidenceSchema = z cgroupIdentity: Sha256Schema, invocationId: Sha256Schema, finalContainerId: Sha256Schema, + finalizationPayloadSha256: Sha256Schema, sanitizedSpecSha256: Sha256Schema, runtimeAttestationSha256: Sha256Schema, observedLimits: z.strictObject({ @@ -139,8 +140,8 @@ const AggregateLimitEvidenceSchema = z }); } if ( - evidence.negativeControls.memory.oomKillAfter <= - evidence.negativeControls.memory.oomKillBefore || + evidence.negativeControls.memory.oomKillAfter !== + evidence.negativeControls.memory.oomKillBefore + 1 || evidence.negativeControls.processes.maxEventsAfter <= evidence.negativeControls.processes.maxEventsBefore || evidence.negativeControls.cpu.nrThrottledAfter <= @@ -151,7 +152,7 @@ const AggregateLimitEvidenceSchema = z context.addIssue({ code: "custom", path: ["negativeControls"], - message: "aggregate negative-control counters must increase", + message: "aggregate negative-control counters are invalid", }); } const { receiptPayloadSha256, ...payload } = evidence; diff --git a/services/runner/src/counterlab_runner/timeout_proof.py b/services/runner/src/counterlab_runner/timeout_proof.py index 8c65a769..014c47a1 100644 --- a/services/runner/src/counterlab_runner/timeout_proof.py +++ b/services/runner/src/counterlab_runner/timeout_proof.py @@ -457,6 +457,7 @@ def _validate_aggregate_limit_evidence( "cgroupIdentity", "invocationId", "finalContainerId", + "finalizationPayloadSha256", "sanitizedSpecSha256", "runtimeAttestationSha256", "observedLimits", @@ -509,12 +510,13 @@ def _validate_aggregate_limit_evidence( cpu_control = controls.get("cpu") member_pids = membership.get("memberPids") if ( - value.get("schemaVersion") != "1" + value.get("schemaVersion") != "2" or value.get("status") != "OBSERVED" or value.get("authority") != "linux-cgroup-v2" or value.get("cgroupVersion") != 2 or value.get("invocationId") != invocation_id or value.get("finalContainerId") != final_container_id + or not _SHA256.fullmatch(str(value.get("finalizationPayloadSha256", ""))) or value.get("cgroupId") != f"counterlab-v6.1-{invocation_id}" or value.get("cgroupPath") != f"counterlab-v6.1/{invocation_id}" or value.get("sanitizedSpecSha256") != sanitized_spec_sha256 diff --git a/services/runner/tests/test_timeout_proof.py b/services/runner/tests/test_timeout_proof.py index e3ca1d47..8e4f7c6f 100644 --- a/services/runner/tests/test_timeout_proof.py +++ b/services/runner/tests/test_timeout_proof.py @@ -69,7 +69,7 @@ def _rootless(control: dict[str, object], build: dict[str, object]) -> dict[str, sanitized_spec_sha256 = "a" * 64 member_pids = [100, 101] aggregate_payload: dict[str, object] = { - "schemaVersion": "1", + "schemaVersion": "2", "status": "OBSERVED", "authority": "linux-cgroup-v2", "cgroupVersion": 2, @@ -83,6 +83,7 @@ def _rootless(control: dict[str, object], build: dict[str, object]) -> dict[str, ).hexdigest(), "invocationId": invocation_id, "finalContainerId": final_container_id, + "finalizationPayloadSha256": "7" * 64, "sanitizedSpecSha256": sanitized_spec_sha256, "runtimeAttestationSha256": "6" * 64, "observedLimits": { From 3cdf38b2626e7341fd7bcb8af33fcda23a6ea131 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 07:39:45 +0530 Subject: [PATCH 049/143] fix(runtime): verify qualification artifact chain --- .../contained-qualified-receipt-store.d.mts | 20 +- scripts/contained-qualified-receipt-store.mjs | 262 ++++++++++++++++-- .../contained-qualified-receipt-store.test.ts | 229 +++++++++++++-- 3 files changed, 462 insertions(+), 49 deletions(-) diff --git a/scripts/contained-qualified-receipt-store.d.mts b/scripts/contained-qualified-receipt-store.d.mts index 591a2738..1f915503 100644 --- a/scripts/contained-qualified-receipt-store.d.mts +++ b/scripts/contained-qualified-receipt-store.d.mts @@ -15,16 +15,30 @@ interface QualifiedReceiptStoreDependencies { }; } +interface QualifiedReceiptObservationArtifacts { + evidence: Record; + evidenceFileSha256: string; + evidencePath: string; + finalizationFileSha256: string; + finalizationPath: string; + finalizationPayloadSha256: string; + manifestFileSha256: string; + manifestPath: string; + observerDraftFileSha256: string; + observerDraftPath: string; + observerReadyFileSha256: string; + observerReadyPath: string; +} + export function persistQualifiedContainedRootlessReceipt( - input: { - aggregateLimitEvidence: unknown; - } & QualifiedRootlessReceiptStoreBinding, + input: QualifiedRootlessReceiptStoreBinding, dependencies?: QualifiedReceiptStoreDependencies, ): { qualifiedReceipt: Record; qualifiedReceiptFileSha256: string; qualifiedReceiptPath: string; qualifiedReceiptPayloadSha256: string; + qualificationArtifacts: QualifiedReceiptObservationArtifacts; }; export function verifyQualifiedContainedRootlessReceipt( diff --git a/scripts/contained-qualified-receipt-store.mjs b/scripts/contained-qualified-receipt-store.mjs index 9b911998..0a480271 100644 --- a/scripts/contained-qualified-receipt-store.mjs +++ b/scripts/contained-qualified-receipt-store.mjs @@ -2,8 +2,21 @@ import { lstatSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; import { isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { sha256CgroupBytes } from "./contained-cgroup-evidence.mjs"; +import { + canonicalCgroupJson, + sha256CgroupBytes, + validateContainedCgroupEvidence, +} from "./contained-cgroup-evidence.mjs"; import { resolveContainedCgroupObserverBindings } from "./contained-cgroup-observer-bindings.mjs"; +import { + validateContainedCgroupObserverDraft, + validateContainedCgroupObserverReady, +} from "./contained-cgroup-observer.mjs"; +import { + containedCgroupQualificationPaths, + validateContainedCgroupObserverFinalization, + validateContainedCgroupObserverManifest, +} from "./contained-cgroup-observer-protocol.mjs"; import { createQualifiedContainedRootlessReceipt } from "./contained-qualified-rootless-receipt.mjs"; const repositoryRoot = realpathSync( @@ -51,6 +64,29 @@ function privateFile(path, label) { } } +function lstatOrAbsent(path) { + try { + return lstatSync(path); + } catch (error) { + if (error?.code === "ENOENT") return null; + throw error; + } +} + +function readArtifact(path, label) { + privateFile(path, label); + const source = readFileSync(path); + let value; + try { + value = JSON.parse(source.toString("utf8")); + } catch (error) { + throw new Error(`qualified rootless receipt ${label} JSON is invalid`, { + cause: error, + }); + } + return { source, value }; +} + function receiptPaths({ sessionRoot, finalContainerId, runtimeSessionId }) { if ( !isAbsolute(sessionRoot) || @@ -117,7 +153,170 @@ function readBaseReceipt({ if (receipt?.finalContainerId !== finalContainerId) { throw new Error("qualified rootless base receipt identity changed"); } - return { expected, receipt }; + return { expected, receipt, source }; +} + +function exactTimestamp(value, label) { + const parsed = Date.parse(value); + if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) { + throw new Error(`qualified rootless receipt ${label} is invalid`); + } + return parsed; +} + +function readObservationChain({ + baseReceipt, + baseReceiptSource, + currentTime, + enforceFreshness, + observerBindings, + sessionRoot, +}) { + const paths = containedCgroupQualificationPaths({ + repositoryRoot, + runtimeSessionId: observerBindings.runtimeSessionId, + invocationId: baseReceipt.invocationId, + }); + privateDirectory(paths.qualificationRoot, "qualification root"); + privateDirectory(paths.invocationRoot, "qualification invocation root"); + if (lstatOrAbsent(paths.failurePath) !== null) { + throw new Error("qualified rootless receipt observation failed"); + } + + const manifestArtifact = readArtifact(paths.manifestPath, "manifest"); + const manifestRequestedAt = exactTimestamp( + manifestArtifact.value?.requestedAt, + "manifest timestamp", + ); + const manifest = validateContainedCgroupObserverManifest( + manifestArtifact.value, + { observedAtMs: manifestRequestedAt }, + ); + const expectedBaseReceiptPath = `.rt/${observerBindings.runtimeSessionId}/run/rootless-specs/${baseReceipt.finalContainerId}.receipt.json`; + if ( + manifest.baseReceiptPath !== expectedBaseReceiptPath || + manifest.baseReceiptFileSha256 !== sha256CgroupBytes(baseReceiptSource) || + manifest.baseReceiptPayloadSha256 !== baseReceipt.receiptPayloadSha256 || + manifest.finalContainerId !== baseReceipt.finalContainerId || + manifest.invocationId !== baseReceipt.invocationId || + manifest.sanitizedSpecSha256 !== baseReceipt.sanitizedSpecSha256 || + canonicalCgroupJson(manifest.intendedAggregateLimits) !== + canonicalCgroupJson(baseReceipt.intendedAggregateLimits) || + Object.entries(observerBindings).some( + ([name, value]) => manifest[name] !== value, + ) || + paths.sessionRoot !== sessionRoot + ) { + throw new Error("qualified rootless receipt manifest binding changed"); + } + + const readyArtifact = readArtifact(paths.observerReadyPath, "observer ready"); + const ready = validateContainedCgroupObserverReady( + readyArtifact.value, + manifest, + ); + const draftArtifact = readArtifact(paths.observerDraftPath, "observer draft"); + const draft = validateContainedCgroupObserverDraft( + draftArtifact.value, + manifest, + ); + const finalizationArtifact = readArtifact( + paths.finalizationPath, + "finalization", + ); + const finalizationDecisionAt = exactTimestamp( + finalizationArtifact.value?.decisionAt, + "finalization timestamp", + ); + const finalization = validateContainedCgroupObserverFinalization( + finalizationArtifact.value, + manifest, + { observedAtMs: finalizationDecisionAt }, + ); + if ( + finalization.status !== "FINALIZE" || + finalization.observerDraftPayloadSha256 !== draft.receiptPayloadSha256 + ) { + throw new Error("qualified rootless receipt finalization did not qualify"); + } + + const evidenceArtifact = readArtifact( + paths.evidencePath, + "aggregate evidence", + ); + const evidence = validateContainedCgroupEvidence(evidenceArtifact.value, { + invocationId: baseReceipt.invocationId, + finalContainerId: baseReceipt.finalContainerId, + sanitizedSpecSha256: baseReceipt.sanitizedSpecSha256, + intendedAggregateLimits: baseReceipt.intendedAggregateLimits, + ...observerBindings, + }); + const armedAt = exactTimestamp(ready.armedAt, "observer ready timestamp"); + const draftObservedAt = exactTimestamp( + draft.observedAt, + "observer draft timestamp", + ); + const evidenceObservedAt = exactTimestamp( + evidence.observedAt, + "aggregate evidence timestamp", + ); + if ( + evidence.finalizationPayloadSha256 !== finalization.receiptPayloadSha256 || + canonicalCgroupJson(evidence.observedLimits) !== + canonicalCgroupJson(draft.observedLimits) || + canonicalCgroupJson(evidence.membership) !== + canonicalCgroupJson(draft.membership) || + canonicalCgroupJson(evidence.negativeControls) !== + canonicalCgroupJson(draft.negativeControls) || + manifestRequestedAt > armedAt || + armedAt > draftObservedAt || + draftObservedAt > finalizationDecisionAt || + finalizationDecisionAt > evidenceObservedAt || + evidenceObservedAt - manifestRequestedAt > maximumObservationAgeMs + ) { + throw new Error("qualified rootless receipt observation chain changed"); + } + if ( + enforceFreshness && + (!Number.isFinite(currentTime) || + evidenceObservedAt > currentTime || + currentTime - evidenceObservedAt > maximumObservationAgeMs) + ) { + throw new Error("qualified rootless receipt observation is stale"); + } + if (lstatOrAbsent(paths.failurePath) !== null) { + throw new Error("qualified rootless receipt terminal artifacts conflict"); + } + return { + evidence, + evidenceFileSha256: sha256CgroupBytes(evidenceArtifact.source), + evidencePath: paths.evidencePath, + finalizationFileSha256: sha256CgroupBytes(finalizationArtifact.source), + finalizationPath: paths.finalizationPath, + finalizationPayloadSha256: finalization.receiptPayloadSha256, + manifestFileSha256: sha256CgroupBytes(manifestArtifact.source), + manifestPath: paths.manifestPath, + observerDraftFileSha256: sha256CgroupBytes(draftArtifact.source), + observerDraftPath: paths.observerDraftPath, + observerReadyFileSha256: sha256CgroupBytes(readyArtifact.source), + observerReadyPath: paths.observerReadyPath, + }; +} + +function observationArtifactIdentity(observation) { + return canonicalCgroupJson({ + evidenceFileSha256: observation.evidenceFileSha256, + evidencePath: observation.evidencePath, + finalizationFileSha256: observation.finalizationFileSha256, + finalizationPath: observation.finalizationPath, + finalizationPayloadSha256: observation.finalizationPayloadSha256, + manifestFileSha256: observation.manifestFileSha256, + manifestPath: observation.manifestPath, + observerDraftFileSha256: observation.observerDraftFileSha256, + observerDraftPath: observation.observerDraftPath, + observerReadyFileSha256: observation.observerReadyFileSha256, + observerReadyPath: observation.observerReadyPath, + }); } export function persistQualifiedContainedRootlessReceipt( @@ -127,26 +326,45 @@ export function persistQualifiedContainedRootlessReceipt( resolveObserverBindings = resolveContainedCgroupObserverBindings, } = {}, ) { + if (Object.hasOwn(input, "aggregateLimitEvidence")) { + throw new Error("qualified rootless receipt caller evidence is forbidden"); + } const observerBindings = resolveObserverBindings({ sessionRoot: input.sessionRoot, }); - const observedAt = Date.parse(input.aggregateLimitEvidence?.observedAt); const currentTime = now(); - if ( - !Number.isFinite(currentTime) || - !Number.isFinite(observedAt) || - new Date(observedAt).toISOString() !== - input.aggregateLimitEvidence?.observedAt || - Math.abs(currentTime - observedAt) > maximumObservationAgeMs - ) { - throw new Error("qualified rootless receipt observation is stale"); - } - const { expected, receipt: baseReceipt } = readBaseReceipt({ + const { + expected, + receipt: baseReceipt, + source: baseReceiptSource, + } = readBaseReceipt({ ...input, observerBindings, }); + const initialObservation = readObservationChain({ + baseReceipt, + baseReceiptSource, + currentTime, + enforceFreshness: true, + observerBindings, + sessionRoot: input.sessionRoot, + }); + const observation = readObservationChain({ + baseReceipt, + baseReceiptSource, + currentTime: now(), + enforceFreshness: true, + observerBindings, + sessionRoot: input.sessionRoot, + }); + if ( + observationArtifactIdentity(initialObservation) !== + observationArtifactIdentity(observation) + ) { + throw new Error("qualified rootless receipt terminal artifacts changed"); + } const qualifiedReceipt = createQualifiedContainedRootlessReceipt({ - aggregateLimitEvidence: input.aggregateLimitEvidence, + aggregateLimitEvidence: observation.evidence, baseReceipt, observerBindings, }); @@ -171,6 +389,7 @@ export function persistQualifiedContainedRootlessReceipt( qualifiedReceiptFileSha256: sha256CgroupBytes(source), qualifiedReceiptPath: expected.qualifiedReceiptPath, qualifiedReceiptPayloadSha256: qualifiedReceipt.receiptPayloadSha256, + qualificationArtifacts: observation, }; } @@ -181,7 +400,11 @@ export function verifyQualifiedContainedRootlessReceipt( const observerBindings = resolveObserverBindings({ sessionRoot: input.sessionRoot, }); - const { expected, receipt: baseReceipt } = readBaseReceipt({ + const { + expected, + receipt: baseReceipt, + source: baseReceiptSource, + } = readBaseReceipt({ ...input, observerBindings, }); @@ -204,8 +427,15 @@ export function verifyQualifiedContainedRootlessReceipt( cause: error, }); } + const observation = readObservationChain({ + baseReceipt, + baseReceiptSource, + enforceFreshness: false, + observerBindings, + sessionRoot: input.sessionRoot, + }); const expectedReceipt = createQualifiedContainedRootlessReceipt({ - aggregateLimitEvidence: qualifiedReceipt.aggregateLimitEvidence, + aggregateLimitEvidence: observation.evidence, baseReceipt, observerBindings, }); diff --git a/scripts/contained-qualified-receipt-store.test.ts b/scripts/contained-qualified-receipt-store.test.ts index a19f625c..40b58e31 100644 --- a/scripts/contained-qualified-receipt-store.test.ts +++ b/scripts/contained-qualified-receipt-store.test.ts @@ -9,6 +9,16 @@ import { createContainedCgroupIdentity, sha256CgroupBytes, } from "./contained-cgroup-evidence.mjs"; +import { + createContainedCgroupObserverDraft, + createContainedCgroupObserverFailure, + createContainedCgroupObserverReady, +} from "./contained-cgroup-observer.mjs"; +import { + containedCgroupQualificationPaths, + createContainedCgroupObserverFinalization, + createContainedCgroupObserverManifest, +} from "./contained-cgroup-observer-protocol.mjs"; import { persistQualifiedContainedRootlessReceipt, verifyQualifiedContainedRootlessReceipt, @@ -30,6 +40,16 @@ const observerBindings = { driverCliSha256: "5".repeat(64), driverModuleSha256: "6".repeat(64), }; +const verifiedCleanup = { + taskAbsent: true, + containerAbsent: true, + snapshotAbsent: true, + invocationAliasAbsent: true, + imageRootfsAbsent: true, + persistedAuthorityVerified: true, + readOnlyMountsUnchanged: true, + imageRootfsUnchanged: true, +}; function hashed>(payload: T) { return { @@ -68,25 +88,9 @@ function baseReceipt() { }); } -function aggregateEvidence(bindings = observerBindings) { +function aggregateObservation(observedAt = "2026-07-20T12:00:00.000Z") { const memberPids = [100, 101]; - return hashed({ - schemaVersion: "2", - status: "OBSERVED", - authority: "linux-cgroup-v2", - cgroupVersion: 2, - cgroupId: `counterlab-v6.1-${invocationId}`, - cgroupPath: `counterlab-v6.1/${invocationId}`, - cgroupIdentity: createContainedCgroupIdentity({ - invocationId, - finalContainerId, - sanitizedSpecSha256, - }), - invocationId, - finalContainerId, - finalizationPayloadSha256: "f".repeat(64), - sanitizedSpecSha256, - runtimeAttestationSha256: bindings.runtimeAttestationSha256, + return { observedLimits: { memoryMaxBytes: intendedAggregateLimits.memoryBytes, memorySwapMaxBytes: intendedAggregateLimits.memoryBytes, @@ -123,17 +127,50 @@ function aggregateEvidence(bindings = observerBindings) { enforced: true, }, }, + observedAt, + }; +} + +function aggregateEvidence( + bindings: typeof observerBindings, + finalizationPayloadSha256: string, + observedAt = "2026-07-20T12:00:20.000Z", +) { + return hashed({ + schemaVersion: "2", + status: "OBSERVED", + authority: "linux-cgroup-v2", + cgroupVersion: 2, + cgroupId: `counterlab-v6.1-${invocationId}`, + cgroupPath: `counterlab-v6.1/${invocationId}`, + cgroupIdentity: createContainedCgroupIdentity({ + invocationId, + finalContainerId, + sanitizedSpecSha256, + }), + invocationId, + finalContainerId, + finalizationPayloadSha256, + sanitizedSpecSha256, + runtimeAttestationSha256: bindings.runtimeAttestationSha256, + ...aggregateObservation(observedAt), cleanup: { cgroupAbsentAfterTimeout: true }, observer: { runtimeSessionId: bindings.runtimeSessionId, driverCliSha256: bindings.driverCliSha256, driverModuleSha256: bindings.driverModuleSha256, }, - observedAt: "2026-07-20T12:00:00.000Z", }); } -function fixture() { +function writeArtifact(path: string, value: unknown) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { + mode: 0o600, + flag: "wx", + }); +} + +function fixture({ writeEvidence = true, writeFailure = false } = {}) { const runtimeSessionId = `rt-store-${randomBytes(4).toString("hex")}`; const parent = resolve(root, ".rt"); mkdirSync(parent, { recursive: true, mode: 0o700 }); @@ -143,12 +180,73 @@ function fixture() { const specRoot = resolve(runRoot, "rootless-specs"); mkdirSync(specRoot, { recursive: true, mode: 0o700 }); const baseReceiptPath = resolve(specRoot, `${finalContainerId}.receipt.json`); - const source = `${JSON.stringify(baseReceipt(), null, 2)}\n`; + const base = baseReceipt(); + const source = `${JSON.stringify(base, null, 2)}\n`; writeFileSync(baseReceiptPath, source, { mode: 0o600, flag: "wx" }); + const liveObserverBindings = { ...observerBindings, runtimeSessionId }; + const paths = containedCgroupQualificationPaths({ + repositoryRoot: root, + runtimeSessionId, + invocationId, + }); + mkdirSync(paths.invocationRoot, { recursive: true, mode: 0o700 }); + const manifest = createContainedCgroupObserverManifest({ + baseReceiptFileSha256: sha256CgroupBytes(source), + baseReceiptPayloadSha256: base.receiptPayloadSha256, + finalContainerId, + intendedAggregateLimits, + invocationId, + observerBindings: liveObserverBindings, + requestedAt: new Date("2026-07-20T11:59:50.000Z"), + sanitizedSpecSha256, + }); + const ready = createContainedCgroupObserverReady(manifest, { + armedAt: new Date("2026-07-20T11:59:55.000Z"), + observerPid: 123, + }); + const draft = createContainedCgroupObserverDraft( + manifest, + aggregateObservation(), + ); + if (typeof draft.receiptPayloadSha256 !== "string") { + throw new Error("observer draft fixture hash is invalid"); + } + const finalization = createContainedCgroupObserverFinalization(manifest, { + cleanup: verifiedCleanup, + cleanupVerified: true, + decisionAt: new Date("2026-07-20T12:00:10.000Z"), + observerDraftPayloadSha256: draft.receiptPayloadSha256, + resultReleased: false, + status: "FINALIZE", + timeoutObserved: true, + }); + if (typeof finalization.receiptPayloadSha256 !== "string") { + throw new Error("observer finalization fixture hash is invalid"); + } + const evidence = aggregateEvidence( + liveObserverBindings, + finalization.receiptPayloadSha256, + ); + writeArtifact(paths.manifestPath, manifest); + writeArtifact(paths.observerReadyPath, ready); + writeArtifact(paths.observerDraftPath, draft); + writeArtifact(paths.finalizationPath, finalization); + if (writeEvidence) writeArtifact(paths.evidencePath, evidence); + if (writeFailure) { + writeArtifact( + paths.failurePath, + createContainedCgroupObserverFailure(manifest, { + failedAt: new Date("2026-07-20T12:00:21.000Z"), + }), + ); + } return { baseReceiptFileSha256: sha256CgroupBytes(source), baseReceiptPath, - observerBindings: { ...observerBindings, runtimeSessionId }, + evidence, + finalization, + observerBindings: liveObserverBindings, + paths, sessionRoot, }; } @@ -168,7 +266,6 @@ describe("qualified rootless receipt store", () => { baseReceiptFileSha256: input.baseReceiptFileSha256, baseReceiptPath: input.baseReceiptPath, sessionRoot: input.sessionRoot, - aggregateLimitEvidence: aggregateEvidence(input.observerBindings), finalContainerId, }, dependencies(input), @@ -180,6 +277,12 @@ describe("qualified rootless receipt store", () => { `run/rootless-specs/${finalContainerId}.qualified-receipt.json`, ), ); + expect(persisted.qualificationArtifacts.evidencePath).toBe( + input.paths.evidencePath, + ); + expect(persisted.qualificationArtifacts.finalizationPayloadSha256).toBe( + input.finalization.receiptPayloadSha256, + ); expect(readFileSync(input.baseReceiptPath, "utf8")).toBe( `${JSON.stringify(baseReceipt(), null, 2)}\n`, ); @@ -202,7 +305,6 @@ describe("qualified rootless receipt store", () => { baseReceiptFileSha256: input.baseReceiptFileSha256, baseReceiptPath: input.baseReceiptPath, sessionRoot: input.sessionRoot, - aggregateLimitEvidence: aggregateEvidence(input.observerBindings), finalContainerId, }, dependencies(input), @@ -217,7 +319,6 @@ describe("qualified rootless receipt store", () => { { baseReceiptPath: wrongHash.baseReceiptPath, sessionRoot: wrongHash.sessionRoot, - aggregateLimitEvidence: aggregateEvidence(wrongHash.observerBindings), baseReceiptFileSha256: "0".repeat(64), finalContainerId, }, @@ -236,7 +337,6 @@ describe("qualified rootless receipt store", () => { { baseReceiptFileSha256: linked.baseReceiptFileSha256, sessionRoot: linked.sessionRoot, - aggregateLimitEvidence: aggregateEvidence(linked.observerBindings), baseReceiptPath: linkedPath, finalContainerId, }, @@ -253,7 +353,6 @@ describe("qualified rootless receipt store", () => { baseReceiptFileSha256: stale.baseReceiptFileSha256, baseReceiptPath: stale.baseReceiptPath, sessionRoot: stale.sessionRoot, - aggregateLimitEvidence: aggregateEvidence(stale.observerBindings), finalContainerId, }, { @@ -270,9 +369,6 @@ describe("qualified rootless receipt store", () => { baseReceiptFileSha256: mismatched.baseReceiptFileSha256, baseReceiptPath: mismatched.baseReceiptPath, sessionRoot: mismatched.sessionRoot, - aggregateLimitEvidence: aggregateEvidence( - mismatched.observerBindings, - ), finalContainerId, }, { @@ -285,4 +381,77 @@ describe("qualified rootless receipt store", () => { ), ).toThrow(/path input/u); }); + + it("rejects caller-supplied evidence and conflicting terminal artifacts", () => { + const callerEvidence = fixture(); + expect(() => + persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: callerEvidence.baseReceiptFileSha256, + baseReceiptPath: callerEvidence.baseReceiptPath, + sessionRoot: callerEvidence.sessionRoot, + finalContainerId, + aggregateLimitEvidence: callerEvidence.evidence, + } as unknown as Parameters< + typeof persistQualifiedContainedRootlessReceipt + >[0], + dependencies(callerEvidence), + ), + ).toThrow(/caller evidence is forbidden/u); + + const conflict = fixture({ writeFailure: true }); + expect(() => + persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: conflict.baseReceiptFileSha256, + baseReceiptPath: conflict.baseReceiptPath, + sessionRoot: conflict.sessionRoot, + finalContainerId, + }, + dependencies(conflict), + ), + ).toThrow(/observation failed/u); + }); + + it("rejects missing evidence and evidence that diverges from the draft", () => { + const missing = fixture({ writeEvidence: false }); + expect(() => + persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: missing.baseReceiptFileSha256, + baseReceiptPath: missing.baseReceiptPath, + sessionRoot: missing.sessionRoot, + finalContainerId, + }, + dependencies(missing), + ), + ).toThrow(); + + const changed = fixture(); + const changedEvidence = aggregateEvidence( + changed.observerBindings, + changed.finalization.receiptPayloadSha256, + ); + changedEvidence.membership.memberPids = [100, 102]; + changedEvidence.membership.memberSetSha256 = sha256CgroupBytes( + canonicalCgroupJson(changedEvidence.membership.memberPids), + ); + const { receiptPayloadSha256: ignored, ...payload } = changedEvidence; + writeFileSync( + changed.paths.evidencePath, + `${JSON.stringify(hashed(payload), null, 2)}\n`, + { mode: 0o600 }, + ); + expect(() => + persistQualifiedContainedRootlessReceipt( + { + baseReceiptFileSha256: changed.baseReceiptFileSha256, + baseReceiptPath: changed.baseReceiptPath, + sessionRoot: changed.sessionRoot, + finalContainerId, + }, + dependencies(changed), + ), + ).toThrow(/observation chain changed/u); + }); }); From 1ea2ed83530bd469e3689226d4dca19b896bedae Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 07:43:15 +0530 Subject: [PATCH 050/143] fix(runtime): permit honest pre-draft aborts --- .../contained-cgroup-observer-protocol.d.mts | 24 ++++++++++++------- .../contained-cgroup-observer-protocol.mjs | 6 ++++- ...contained-cgroup-observer-protocol.test.ts | 18 ++++++++++++++ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/scripts/contained-cgroup-observer-protocol.d.mts b/scripts/contained-cgroup-observer-protocol.d.mts index af516812..5e119e37 100644 --- a/scripts/contained-cgroup-observer-protocol.d.mts +++ b/scripts/contained-cgroup-observer-protocol.d.mts @@ -67,7 +67,7 @@ export interface ContainedCgroupObserverFinalization { imageRootfsUnchanged: boolean; }; manifestPayloadSha256: string; - observerDraftPayloadSha256: string; + observerDraftPayloadSha256: string | null; invocationId: string; finalContainerId: string; timeoutObserved: boolean; @@ -86,18 +86,26 @@ export function validateContainedCgroupObserverFinalization( export function createContainedCgroupObserverFinalization( manifest: ContainedCgroupObserverManifest, input: { - abortCode?: Exclude< - ContainedCgroupObserverFinalization["abortCode"], - null - > | null; cleanup: ContainedCgroupObserverFinalization["cleanup"]; cleanupVerified: boolean; decisionAt?: Date; - observerDraftPayloadSha256: string; resultReleased: boolean; - status: "FINALIZE" | "ABORT"; timeoutObserved: boolean; - }, + } & ( + | { + abortCode?: null; + observerDraftPayloadSha256: string; + status: "FINALIZE"; + } + | { + abortCode: Exclude< + ContainedCgroupObserverFinalization["abortCode"], + null + >; + observerDraftPayloadSha256: string | null; + status: "ABORT"; + } + ), ): ContainedCgroupObserverFinalization; export function containedCgroupQualificationPaths(input: { diff --git a/scripts/contained-cgroup-observer-protocol.mjs b/scripts/contained-cgroup-observer-protocol.mjs index 489ecce0..345169e1 100644 --- a/scripts/contained-cgroup-observer-protocol.mjs +++ b/scripts/contained-cgroup-observer-protocol.mjs @@ -232,6 +232,10 @@ export function validateContainedCgroupObserverFinalization( finalization.cleanupVerified === true && everyCleanupCheckPassed; const abortsQualification = finalization.status === "ABORT"; + const draftBindingIsValid = finalizesQualification + ? sha256Pattern.test(finalization.observerDraftPayloadSha256 ?? "") + : finalization.observerDraftPayloadSha256 === null || + sha256Pattern.test(finalization.observerDraftPayloadSha256 ?? ""); if ( finalization.schemaVersion !== "1" || (!finalizesQualification && !abortsQualification) || @@ -240,7 +244,7 @@ export function validateContainedCgroupObserverFinalization( ? finalization.abortCode !== null : !allowedAbortCodes.has(finalization.abortCode)) || finalization.manifestPayloadSha256 !== manifest?.receiptPayloadSha256 || - !sha256Pattern.test(finalization.observerDraftPayloadSha256 ?? "") || + !draftBindingIsValid || finalization.invocationId !== manifest?.invocationId || finalization.finalContainerId !== manifest?.finalContainerId || typeof finalization.timeoutObserved !== "boolean" || diff --git a/scripts/contained-cgroup-observer-protocol.test.ts b/scripts/contained-cgroup-observer-protocol.test.ts index abd82fdf..a7829202 100644 --- a/scripts/contained-cgroup-observer-protocol.test.ts +++ b/scripts/contained-cgroup-observer-protocol.test.ts @@ -155,6 +155,22 @@ describe("contained cgroup observer protocol", () => { }), ).toEqual(failClosedAbort); + const preDraftAbort = createContainedCgroupObserverFinalization(input, { + abortCode: "OBSERVER_FAILED", + cleanup: verifiedCleanup, + cleanupVerified: true, + decisionAt: finalizedAt, + observerDraftPayloadSha256: null, + resultReleased: false, + status: "ABORT", + timeoutObserved: false, + }); + expect( + validateContainedCgroupObserverFinalization(preDraftAbort, input, { + observedAtMs: finalizedAt.getTime(), + }), + ).toEqual(preDraftAbort); + const mutations = [ { ...finalized, resultReleased: true }, { ...finalized, cleanupVerified: false }, @@ -165,6 +181,8 @@ describe("contained cgroup observer protocol", () => { }, { ...finalized, receiptPayloadSha256: "0".repeat(64) }, { ...finalized, observerDraftPayloadSha256: "0" }, + { ...finalized, observerDraftPayloadSha256: null }, + { ...aborted, observerDraftPayloadSha256: "0" }, { ...aborted, status: "FINALIZE" }, { ...aborted, abortCode: "UNKNOWN" }, ]; From 40db9bc842aa0206241c7ce7168f932bff0c731c Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:12:48 +0530 Subject: [PATCH 051/143] feat(runtime): coordinate aggregate qualification --- ...ned-cgroup-qualification-coordinator.d.mts | 118 +++ ...ained-cgroup-qualification-coordinator.mjs | 719 ++++++++++++++++++ ...d-cgroup-qualification-coordinator.test.ts | 582 ++++++++++++++ 3 files changed, 1419 insertions(+) create mode 100644 scripts/contained-cgroup-qualification-coordinator.d.mts create mode 100644 scripts/contained-cgroup-qualification-coordinator.mjs create mode 100644 scripts/contained-cgroup-qualification-coordinator.test.ts diff --git a/scripts/contained-cgroup-qualification-coordinator.d.mts b/scripts/contained-cgroup-qualification-coordinator.d.mts new file mode 100644 index 00000000..2ac886aa --- /dev/null +++ b/scripts/contained-cgroup-qualification-coordinator.d.mts @@ -0,0 +1,118 @@ +import type { ContainedCgroupObserverManifest } from "./contained-cgroup-observer-protocol.mjs"; + +export interface ContainedCgroupQualificationHandle { + readonly finalContainerId: string; + readonly invocationId: string; +} + +export interface ContainedCgroupQualificationInput { + baseReceiptFileSha256: string; + baseReceiptPath: string; + baseReceiptPayloadSha256: string; + finalContainerId: string; + intendedAggregateLimits: { + cpuCount: number; + maxProcesses: number; + memoryBytes: number; + }; + invocationId: string; + sanitizedSpecSha256: string; + sessionRoot: string; +} + +export interface ContainedCgroupQualificationOutcome { + cleanup: { + containerAbsent: boolean; + imageRootfsAbsent: boolean; + imageRootfsUnchanged: boolean; + invocationAliasAbsent: boolean; + persistedAuthorityVerified: boolean; + readOnlyMountsUnchanged: boolean; + snapshotAbsent: boolean; + taskAbsent: boolean; + }; + resultReleased: boolean; + runtimeFailed: boolean; + timeoutObserved: boolean; +} + +export interface ContainedCgroupQualifiedReceipt { + qualifiedReceipt: Record; + qualifiedReceiptFileSha256: string; + qualifiedReceiptPath: string; + qualifiedReceiptPayloadSha256: string; + qualificationArtifacts: Record; +} + +export interface ContainedCgroupQualificationCoordinator { + begin( + input: ContainedCgroupQualificationInput, + ): Promise; + waitForDraft( + handle: ContainedCgroupQualificationHandle, + ): Promise>; + complete( + handle: ContainedCgroupQualificationHandle, + outcome: ContainedCgroupQualificationOutcome, + ): Promise; +} + +interface CoordinatorDependencies { + now?: () => Date; + persistQualifiedReceipt?: ( + input: Pick< + ContainedCgroupQualificationInput, + | "baseReceiptFileSha256" + | "baseReceiptPath" + | "finalContainerId" + | "sessionRoot" + >, + ) => ContainedCgroupQualifiedReceipt; + resolveObserverBindings?: (input: { sessionRoot: string }) => { + driverCliSha256: string; + driverModuleSha256: string; + runtimeAttestationSha256: string; + runtimeSessionId: string; + }; + sleep?: (milliseconds: number) => Promise; + startObserver?: (input: { + manifest: ContainedCgroupObserverManifest; + manifestPath: string; + paths: { + evidencePath: string; + failurePath: string; + finalizationPath: string; + invocationRoot: string; + manifestPath: string; + observerDraftPath: string; + observerReadyPath: string; + qualificationRoot: string; + sessionRoot: string; + }; + runtimeSessionId: string; + }) => { + completion: Promise<{ + code: number | null; + error?: Error; + outputExceeded: boolean; + signal: NodeJS.Signals | null; + }>; + isRunning(): boolean; + pid: number; + terminate(signal: NodeJS.Signals): boolean; + }; + verifyQualifiedReceipt?: (input: { + baseReceiptFileSha256: string; + baseReceiptPath: string; + finalContainerId: string; + qualifiedReceiptFileSha256: string; + qualifiedReceiptPath: string; + sessionRoot: string; + }) => Record; +} + +export function createContainedCgroupQualificationCoordinator( + dependencies?: CoordinatorDependencies, +): ContainedCgroupQualificationCoordinator; + +export const containedCgroupQualificationCoordinator: ContainedCgroupQualificationCoordinator; diff --git a/scripts/contained-cgroup-qualification-coordinator.mjs b/scripts/contained-cgroup-qualification-coordinator.mjs new file mode 100644 index 00000000..d4742381 --- /dev/null +++ b/scripts/contained-cgroup-qualification-coordinator.mjs @@ -0,0 +1,719 @@ +import { spawn } from "node:child_process"; +import { + lstatSync, + mkdirSync, + readFileSync, + realpathSync, + writeFileSync, +} from "node:fs"; +import { isAbsolute, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + canonicalCgroupJson, + sha256CgroupBytes, +} from "./contained-cgroup-evidence.mjs"; +import { resolveContainedCgroupObserverBindings } from "./contained-cgroup-observer-bindings.mjs"; +import { + validateContainedCgroupObserverDraft, + validateContainedCgroupObserverFailure, + validateContainedCgroupObserverReady, +} from "./contained-cgroup-observer.mjs"; +import { + containedCgroupQualificationPaths, + createContainedCgroupObserverFinalization, + createContainedCgroupObserverManifest, +} from "./contained-cgroup-observer-protocol.mjs"; +import { + persistQualifiedContainedRootlessReceipt, + verifyQualifiedContainedRootlessReceipt, +} from "./contained-qualified-receipt-store.mjs"; +import { UNQUALIFIED_AGGREGATE_LIMIT_MODE } from "./contained-qualified-rootless-receipt.mjs"; + +const repositoryRoot = realpathSync( + resolve(fileURLToPath(import.meta.url), "../.."), +); +const maximumArtifactBytes = 1_048_576; +const maximumObserverOutputBytes = 8_192; +const observerArtifactTimeoutMs = 30_000; +const observerExitTimeoutMs = 30_000; +const observerForcedExitTimeoutMs = 2_000; +const maximumObservationAgeMs = 5 * 60_000; +const sha256Pattern = /^[a-f0-9]{64}$/u; +const cleanupKeys = [ + "containerAbsent", + "imageRootfsAbsent", + "imageRootfsUnchanged", + "invocationAliasAbsent", + "persistedAuthorityVerified", + "readOnlyMountsUnchanged", + "snapshotAbsent", + "taskAbsent", +]; + +function contained(parent, candidate) { + const fromParent = relative(parent, candidate); + return ( + fromParent === "" || + (!fromParent.startsWith("..") && !isAbsolute(fromParent)) + ); +} + +function privateDirectory(path, label) { + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isDirectory() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() || + (metadata.mode & 0o777) !== 0o700 + ) { + throw new Error(`contained cgroup coordinator ${label} is invalid`); + } +} + +function privateFile(path, label) { + const metadata = lstatSync(path); + if ( + metadata.isSymbolicLink() || + !metadata.isFile() || + realpathSync(path) !== path || + metadata.uid !== process.getuid() || + metadata.nlink !== 1 || + (metadata.mode & 0o600) !== 0o600 || + (metadata.mode & 0o077) !== 0 || + metadata.size < 1 || + metadata.size > maximumArtifactBytes + ) { + throw new Error(`contained cgroup coordinator ${label} is invalid`); + } +} + +function lstatOrAbsent(path) { + try { + return lstatSync(path); + } catch (error) { + if (error?.code === "ENOENT") return null; + throw error; + } +} + +function writeArtifact(path, value) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { + encoding: "utf8", + flag: "wx", + mode: 0o600, + }); + privateFile(path, "artifact"); +} + +function readArtifact(path, label) { + privateFile(path, label); + try { + return JSON.parse(readFileSync(path, "utf8")); + } catch (error) { + throw new Error(`contained cgroup coordinator ${label} JSON is invalid`, { + cause: error, + }); + } +} + +function exactDate(value, label) { + if (!(value instanceof Date) || !Number.isFinite(value.getTime())) { + throw new Error(`contained cgroup coordinator ${label} is invalid`); + } + return value; +} + +function chronologicalTimestamp(value, label) { + const parsed = Date.parse(value); + if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) { + throw new Error(`contained cgroup coordinator ${label} is invalid`); + } + return parsed; +} + +function defaultSleep(milliseconds) { + return new Promise((accept) => setTimeout(accept, milliseconds)); +} + +async function settleCompletionState() { + await Promise.resolve(); + await Promise.resolve(); +} + +function defaultStartObserver({ manifestPath, runtimeSessionId }) { + const observerPath = resolve( + repositoryRoot, + "scripts/contained-cgroup-observer.mjs", + ); + const child = spawn( + process.execPath, + [ + observerPath, + "--session-id", + runtimeSessionId, + "--manifest", + manifestPath, + ], + { + cwd: repositoryRoot, + env: { LANG: "C", LC_ALL: "C", TZ: "UTC" }, + stdio: ["ignore", "ignore", "pipe"], + }, + ); + let stderrBytes = 0; + let outputExceeded = false; + child.stderr.on("data", (chunk) => { + stderrBytes += Buffer.byteLength(chunk); + if (stderrBytes > maximumObserverOutputBytes) { + outputExceeded = true; + child.kill("SIGTERM"); + } + }); + let settled = false; + const completion = new Promise((accept) => { + const finish = (outcome) => { + if (settled) return; + settled = true; + accept({ ...outcome, outputExceeded }); + }; + child.once("error", (error) => finish({ code: null, signal: null, error })); + child.once("close", (code, signal) => + finish({ code, signal, error: undefined }), + ); + }); + return { + completion, + isRunning: () => child.exitCode === null && child.signalCode === null, + pid: child.pid, + terminate: (signal) => child.kill(signal), + }; +} + +function validateBaseReceipt(input, observerBindings) { + if ( + !isAbsolute(input.sessionRoot) || + !contained(repositoryRoot, input.sessionRoot) || + !sha256Pattern.test(input.baseReceiptFileSha256 ?? "") || + !sha256Pattern.test(input.baseReceiptPayloadSha256 ?? "") || + !sha256Pattern.test(input.finalContainerId ?? "") || + !sha256Pattern.test(input.invocationId ?? "") || + !sha256Pattern.test(input.sanitizedSpecSha256 ?? "") + ) { + throw new Error("contained cgroup coordinator base input is invalid"); + } + const expectedPath = resolve( + input.sessionRoot, + `run/rootless-specs/${input.finalContainerId}.receipt.json`, + ); + if ( + input.baseReceiptPath !== expectedPath || + input.sessionRoot !== + resolve(repositoryRoot, ".rt", observerBindings.runtimeSessionId) + ) { + throw new Error("contained cgroup coordinator base path is invalid"); + } + privateFile(input.baseReceiptPath, "base receipt"); + const source = readFileSync(input.baseReceiptPath); + let receipt; + try { + receipt = JSON.parse(source.toString("utf8")); + } catch (error) { + throw new Error("contained cgroup coordinator base JSON is invalid", { + cause: error, + }); + } + const { receiptPayloadSha256, ...payload } = receipt; + if ( + sha256CgroupBytes(source) !== input.baseReceiptFileSha256 || + receiptPayloadSha256 !== input.baseReceiptPayloadSha256 || + sha256CgroupBytes(canonicalCgroupJson(payload)) !== receiptPayloadSha256 || + receipt.schemaVersion !== "4" || + receipt.status !== "VALIDATED" || + receipt.limitMode !== UNQUALIFIED_AGGREGATE_LIMIT_MODE || + receipt.aggregateLimitIntentEnforced !== false || + receipt.aggregateLimitEvidence !== null || + receipt.invocationId !== input.invocationId || + receipt.finalContainerId !== input.finalContainerId || + receipt.sanitizedSpecSha256 !== input.sanitizedSpecSha256 || + canonicalCgroupJson(receipt.intendedAggregateLimits) !== + canonicalCgroupJson(input.intendedAggregateLimits) + ) { + throw new Error("contained cgroup coordinator base binding changed"); + } + return receipt; +} + +function prepareQualificationDirectories(paths) { + privateDirectory(resolve(repositoryRoot, ".rt"), "runtime root"); + privateDirectory(paths.sessionRoot, "session root"); + try { + mkdirSync(paths.qualificationRoot, { mode: 0o700 }); + } catch (error) { + if (error?.code !== "EEXIST") throw error; + } + privateDirectory(paths.qualificationRoot, "qualification root"); + mkdirSync(paths.invocationRoot, { mode: 0o700 }); + privateDirectory(paths.invocationRoot, "invocation root"); +} + +function processOutcomeSucceeded(outcome) { + return ( + outcome?.code === 0 && + outcome?.signal === null && + outcome?.error === undefined && + outcome?.outputExceeded === false + ); +} + +function exactOutcome(value) { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + JSON.stringify(Object.keys(value).sort()) !== + JSON.stringify( + [ + "cleanup", + "resultReleased", + "runtimeFailed", + "timeoutObserved", + ].sort(), + ) + ) { + throw new Error("contained cgroup coordinator outcome is invalid"); + } + const cleanup = value.cleanup; + if ( + cleanup === null || + typeof cleanup !== "object" || + Array.isArray(cleanup) || + JSON.stringify(Object.keys(cleanup).sort()) !== + JSON.stringify([...cleanupKeys].sort()) || + cleanupKeys.some((key) => typeof cleanup[key] !== "boolean") || + typeof value.resultReleased !== "boolean" || + typeof value.runtimeFailed !== "boolean" || + typeof value.timeoutObserved !== "boolean" + ) { + throw new Error("contained cgroup coordinator outcome is invalid"); + } + return value; +} + +function finalizationDecision(state, outcome) { + if (state.draft === undefined) { + return { abortCode: "OBSERVER_FAILED", status: "ABORT" }; + } + if (outcome.runtimeFailed) { + return { abortCode: "RUNTIME_FAILED", status: "ABORT" }; + } + if (!outcome.timeoutObserved) { + return { abortCode: "TIMEOUT_NOT_OBSERVED", status: "ABORT" }; + } + if (outcome.resultReleased) { + return { abortCode: "RESULT_RELEASED", status: "ABORT" }; + } + if (cleanupKeys.some((key) => outcome.cleanup[key] !== true)) { + return { abortCode: "CLEANUP_UNVERIFIED", status: "ABORT" }; + } + return { abortCode: null, status: "FINALIZE" }; +} + +export function createContainedCgroupQualificationCoordinator({ + now = () => new Date(), + persistQualifiedReceipt = persistQualifiedContainedRootlessReceipt, + resolveObserverBindings = resolveContainedCgroupObserverBindings, + sleep = defaultSleep, + startObserver = defaultStartObserver, + verifyQualifiedReceipt = verifyQualifiedContainedRootlessReceipt, +} = {}) { + const states = new WeakMap(); + + function normalizeCompletion(completion) { + return completion.then( + (outcome) => outcome, + (error) => ({ + code: null, + error: + error instanceof Error + ? error + : new Error("contained cgroup observer completion failed"), + outputExceeded: false, + signal: null, + }), + ); + } + + async function boundedCompletion(state, timeoutMs = observerExitTimeoutMs) { + if ( + state.processOutcome !== undefined && + state.process.isRunning() === false + ) { + return state.processOutcome; + } + let timer; + try { + const outcome = await Promise.race([ + state.process.completion, + new Promise((_, reject) => { + timer = setTimeout( + () => + reject( + new Error("contained cgroup coordinator observer timed out"), + ), + timeoutMs, + ); + }), + ]); + if (state.process.isRunning() === true) { + throw new Error( + "contained cgroup coordinator completion preceded observer exit", + ); + } + return outcome; + } finally { + clearTimeout(timer); + } + } + + async function waitForStoppedProcess(state, timeoutMs) { + const deadline = Date.now() + timeoutMs; + while (state.process.isRunning() === true && Date.now() <= deadline) { + await sleep(10); + } + if (state.process.isRunning() === true) { + throw new Error("contained cgroup coordinator observer did not exit"); + } + return boundedCompletion(state, Math.max(1, deadline - Date.now())); + } + + async function stopObserver(state) { + if (state.process.isRunning() === false) { + return boundedCompletion(state, observerForcedExitTimeoutMs); + } + let terminationError; + try { + state.process.terminate("SIGTERM"); + } catch (error) { + terminationError = error; + } + try { + return await waitForStoppedProcess(state, observerForcedExitTimeoutMs); + } catch (graceError) { + try { + state.process.terminate("SIGKILL"); + } catch (error) { + throw new Error( + "contained cgroup coordinator could not terminate observer", + { + cause: new AggregateError( + [terminationError, graceError, error].filter(Boolean), + ), + }, + ); + } + try { + return await waitForStoppedProcess(state, observerForcedExitTimeoutMs); + } catch (forcedError) { + throw new Error( + "contained cgroup coordinator could not reap observer", + { + cause: new AggregateError( + [terminationError, graceError, forcedError].filter(Boolean), + ), + }, + ); + } + } + } + + async function stopAfterFailure(state, originalError, label) { + try { + await stopObserver(state); + } catch (terminationError) { + throw new Error( + `contained cgroup coordinator ${label} and observer termination failed`, + { + cause: new AggregateError([originalError, terminationError]), + }, + ); + } + throw originalError; + } + + async function waitForArtifact(state, path, label, validator) { + const deadline = Date.now() + observerArtifactTimeoutMs; + while (Date.now() <= deadline) { + if (lstatOrAbsent(state.paths.failurePath) !== null) { + validateContainedCgroupObserverFailure( + readArtifact(state.paths.failurePath, "observer failure"), + state.manifest, + ); + throw new Error("contained cgroup coordinator observer failed"); + } + if ( + state.processOutcome !== undefined || + state.process.isRunning() !== true + ) { + throw new Error( + `contained cgroup coordinator observer exited before ${label}`, + ); + } + if (lstatOrAbsent(path) !== null) { + return validator(readArtifact(path, label), state.manifest); + } + await sleep(10); + } + throw new Error(`contained cgroup coordinator ${label} timed out`); + } + + async function begin(input) { + const observerBindings = resolveObserverBindings({ + sessionRoot: input.sessionRoot, + }); + const baseReceipt = validateBaseReceipt(input, observerBindings); + const paths = containedCgroupQualificationPaths({ + repositoryRoot, + runtimeSessionId: observerBindings.runtimeSessionId, + invocationId: input.invocationId, + }); + prepareQualificationDirectories(paths); + const requestedAt = exactDate(now(), "request time"); + const manifest = createContainedCgroupObserverManifest({ + baseReceiptFileSha256: input.baseReceiptFileSha256, + baseReceiptPayloadSha256: input.baseReceiptPayloadSha256, + finalContainerId: input.finalContainerId, + intendedAggregateLimits: input.intendedAggregateLimits, + invocationId: input.invocationId, + observerBindings, + requestedAt, + sanitizedSpecSha256: input.sanitizedSpecSha256, + }); + writeArtifact(paths.manifestPath, manifest); + let rawProcessHandle; + try { + rawProcessHandle = startObserver({ + manifest, + manifestPath: paths.manifestPath, + paths, + runtimeSessionId: observerBindings.runtimeSessionId, + }); + if ( + !Number.isSafeInteger(rawProcessHandle?.pid) || + rawProcessHandle.pid < 1 || + !(rawProcessHandle.completion instanceof Promise) || + typeof rawProcessHandle.isRunning !== "function" || + typeof rawProcessHandle.terminate !== "function" + ) { + throw new Error("contained cgroup coordinator process is invalid"); + } + } catch (error) { + if (typeof rawProcessHandle?.terminate === "function") { + try { + if ( + rawProcessHandle.completion instanceof Promise && + typeof rawProcessHandle.isRunning === "function" + ) { + const partialState = { + process: { + ...rawProcessHandle, + completion: normalizeCompletion(rawProcessHandle.completion), + }, + processOutcome: undefined, + }; + partialState.process.completion.then((outcome) => { + partialState.processOutcome = outcome; + }); + await stopObserver(partialState); + } else { + rawProcessHandle.terminate("SIGTERM"); + rawProcessHandle.terminate("SIGKILL"); + throw new Error("partial observer exit could not be verified"); + } + } catch (terminationError) { + throw new Error( + "contained cgroup coordinator observer start and termination failed", + { + cause: new AggregateError([error, terminationError]), + }, + ); + } + } + throw new Error("contained cgroup coordinator observer start failed", { + cause: error, + }); + } + const processHandle = { + ...rawProcessHandle, + completion: normalizeCompletion(rawProcessHandle.completion), + }; + const handle = Object.freeze({ + finalContainerId: input.finalContainerId, + invocationId: input.invocationId, + }); + const state = { + baseReceipt, + input, + manifest, + observerBindings, + paths, + process: processHandle, + processOutcome: undefined, + }; + states.set(handle, state); + processHandle.completion.then((outcome) => { + state.processOutcome = outcome; + }); + try { + const ready = await waitForArtifact( + state, + paths.observerReadyPath, + "observer readiness", + validateContainedCgroupObserverReady, + ); + await settleCompletionState(); + const readinessCheckedAt = exactDate(now(), "readiness check time"); + const requestedAtMs = chronologicalTimestamp( + manifest.requestedAt, + "request timestamp", + ); + const armedAtMs = chronologicalTimestamp( + ready.armedAt, + "readiness timestamp", + ); + if ( + state.processOutcome !== undefined || + processHandle.isRunning() !== true || + ready.observerPid !== processHandle.pid || + armedAtMs < requestedAtMs || + armedAtMs > readinessCheckedAt.getTime() || + readinessCheckedAt.getTime() - armedAtMs > maximumObservationAgeMs + ) { + throw new Error( + "contained cgroup coordinator observer readiness binding changed", + ); + } + state.ready = ready; + return handle; + } catch (error) { + return stopAfterFailure(state, error, "readiness failed"); + } + } + + async function waitForDraft(handle) { + const state = states.get(handle); + if (state === undefined || state.ready === undefined || state.finalized) { + throw new Error("contained cgroup coordinator handle is invalid"); + } + if (state.draft !== undefined) return state.draft; + try { + const draft = await waitForArtifact( + state, + state.paths.observerDraftPath, + "observer draft", + validateContainedCgroupObserverDraft, + ); + await settleCompletionState(); + const draftCheckedAt = exactDate(now(), "draft check time"); + const armedAtMs = chronologicalTimestamp( + state.ready.armedAt, + "readiness timestamp", + ); + const observedAtMs = chronologicalTimestamp( + draft.observedAt, + "draft timestamp", + ); + if ( + state.processOutcome !== undefined || + state.process.isRunning() !== true || + observedAtMs < armedAtMs || + observedAtMs > draftCheckedAt.getTime() || + draftCheckedAt.getTime() - observedAtMs > maximumObservationAgeMs + ) { + throw new Error( + "contained cgroup coordinator draft chronology changed", + ); + } + state.draft = draft; + return draft; + } catch (error) { + return stopAfterFailure(state, error, "draft validation failed"); + } + } + + async function complete(handle, rawOutcome) { + const state = states.get(handle); + if (state === undefined || state.ready === undefined || state.finalized) { + throw new Error("contained cgroup coordinator handle is invalid"); + } + const outcome = exactOutcome(rawOutcome); + const decision = finalizationDecision(state, outcome); + const cleanupVerified = cleanupKeys.every( + (key) => outcome.cleanup[key] === true, + ); + const finalization = createContainedCgroupObserverFinalization( + state.manifest, + { + abortCode: decision.abortCode, + cleanup: outcome.cleanup, + cleanupVerified, + decisionAt: exactDate(now(), "decision time"), + observerDraftPayloadSha256: state.draft?.receiptPayloadSha256 ?? null, + resultReleased: outcome.resultReleased, + status: decision.status, + timeoutObserved: outcome.timeoutObserved, + }, + ); + writeArtifact(state.paths.finalizationPath, finalization); + state.finalized = true; + if (decision.status === "ABORT") { + const abortError = new Error( + `contained cgroup coordinator qualification aborted: ${decision.abortCode}`, + ); + return stopAfterFailure(state, abortError, "qualification aborted"); + } + + let processOutcome; + try { + processOutcome = await boundedCompletion(state); + } catch (error) { + return stopAfterFailure(state, error, "completion failed"); + } + if (!processOutcomeSucceeded(processOutcome)) { + return stopAfterFailure( + state, + new Error("contained cgroup coordinator observer failed"), + "observer failed", + ); + } + if ( + lstatOrAbsent(state.paths.failurePath) !== null || + lstatOrAbsent(state.paths.evidencePath) === null + ) { + throw new Error( + "contained cgroup coordinator terminal outcome is invalid", + ); + } + const qualified = persistQualifiedReceipt({ + baseReceiptFileSha256: state.input.baseReceiptFileSha256, + baseReceiptPath: state.input.baseReceiptPath, + finalContainerId: state.input.finalContainerId, + sessionRoot: state.input.sessionRoot, + }); + verifyQualifiedReceipt({ + baseReceiptFileSha256: state.input.baseReceiptFileSha256, + baseReceiptPath: state.input.baseReceiptPath, + finalContainerId: state.input.finalContainerId, + qualifiedReceiptFileSha256: qualified.qualifiedReceiptFileSha256, + qualifiedReceiptPath: qualified.qualifiedReceiptPath, + sessionRoot: state.input.sessionRoot, + }); + return qualified; + } + + return Object.freeze({ begin, complete, waitForDraft }); +} + +export const containedCgroupQualificationCoordinator = + createContainedCgroupQualificationCoordinator(); diff --git a/scripts/contained-cgroup-qualification-coordinator.test.ts b/scripts/contained-cgroup-qualification-coordinator.test.ts new file mode 100644 index 00000000..f79ceaf6 --- /dev/null +++ b/scripts/contained-cgroup-qualification-coordinator.test.ts @@ -0,0 +1,582 @@ +import { randomBytes } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { + canonicalCgroupJson, + createContainedCgroupIdentity, + sha256CgroupBytes, +} from "./contained-cgroup-evidence.mjs"; +import { + createContainedCgroupObserverDraft, + createContainedCgroupObserverFailure, + createContainedCgroupObserverReady, +} from "./contained-cgroup-observer.mjs"; +import { createContainedCgroupQualificationCoordinator } from "./contained-cgroup-qualification-coordinator.mjs"; +import type { ContainedCgroupObserverManifest } from "./contained-cgroup-observer-protocol.mjs"; +import { UNQUALIFIED_AGGREGATE_LIMIT_MODE } from "./contained-qualified-rootless-receipt.mjs"; + +const root = process.cwd(); +const invocationId = "1".repeat(64); +const finalContainerId = "2".repeat(64); +const sanitizedSpecSha256 = "3".repeat(64); +const intendedAggregateLimits = { + cpuCount: 1, + maxProcesses: 16, + memoryBytes: 512 * 1024 * 1024, +}; +const clean = { + containerAbsent: true, + imageRootfsAbsent: true, + imageRootfsUnchanged: true, + invocationAliasAbsent: true, + persistedAuthorityVerified: true, + readOnlyMountsUnchanged: true, + snapshotAbsent: true, + taskAbsent: true, +}; +type QualificationPaths = { + evidencePath: string; + failurePath: string; + finalizationPath: string; + invocationRoot: string; + manifestPath: string; + observerDraftPath: string; + observerReadyPath: string; + qualificationRoot: string; + sessionRoot: string; +}; + +function hashed>(payload: T) { + return { + ...payload, + receiptPayloadSha256: sha256CgroupBytes(canonicalCgroupJson(payload)), + }; +} + +function baseReceipt() { + return hashed({ + schemaVersion: "4", + status: "VALIDATED", + limitMode: UNQUALIFIED_AGGREGATE_LIMIT_MODE, + aggregateLimitIntentEnforced: false, + invocationId, + stagingContainerId: "4".repeat(64), + finalContainerId, + metadataSha256: "5".repeat(64), + originalSpecSha256: "6".repeat(64), + baseSpecSha256: "7".repeat(64), + sanitizedSpecSha256, + configFileSha256: "8".repeat(64), + internalMountManifest: [], + internalMountManifestSha256: "9".repeat(64), + readOnlyMountManifest: [], + readOnlyMountManifestSha256: "a".repeat(64), + commandSha256: "b".repeat(64), + imageAuthority: {}, + imageRootfs: {}, + removedFields: [], + normalizedFields: [], + removedMounts: [], + intendedAggregateLimits, + enforcedRlimits: [], + aggregateLimitEvidence: null, + }); +} + +function observation(observedAt = "2026-07-20T12:00:10.000Z") { + const memberPids = [100, 101]; + return { + observedLimits: { + cpuPeriodMicros: 100_000, + cpuQuotaMicros: 100_000, + memoryMaxBytes: intendedAggregateLimits.memoryBytes, + memorySwapMaxBytes: intendedAggregateLimits.memoryBytes, + pidsMax: intendedAggregateLimits.maxProcesses, + }, + membership: { + descendantsObserved: true, + leaderPid: 100, + leaderStartTimeTicks: "12345", + memberPids, + memberSetSha256: sha256CgroupBytes(canonicalCgroupJson(memberPids)), + }, + negativeControls: { + cpu: { + busyWindowMs: 500, + enforced: true, + nrThrottledAfter: 1, + nrThrottledBefore: 0, + throttledUsecAfter: 1, + throttledUsecBefore: 0, + }, + memory: { + enforced: true, + oomKillAfter: 1, + oomKillBefore: 0, + requestedBytes: intendedAggregateLimits.memoryBytes + 1, + }, + processes: { + attemptedProcesses: intendedAggregateLimits.maxProcesses + 1, + enforced: true, + maxEventsAfter: 1, + maxEventsBefore: 0, + }, + }, + observedAt, + }; +} + +function aggregateEvidence( + observerBindings: ReturnType["observerBindings"], + finalizationPayloadSha256: string, +) { + return hashed({ + schemaVersion: "2", + status: "OBSERVED", + authority: "linux-cgroup-v2", + cgroupVersion: 2, + cgroupId: `counterlab-v6.1-${invocationId}`, + cgroupPath: `counterlab-v6.1/${invocationId}`, + cgroupIdentity: createContainedCgroupIdentity({ + invocationId, + finalContainerId, + sanitizedSpecSha256, + }), + invocationId, + finalContainerId, + finalizationPayloadSha256, + sanitizedSpecSha256, + runtimeAttestationSha256: observerBindings.runtimeAttestationSha256, + ...observation("2026-07-20T12:00:30.000Z"), + cleanup: { cgroupAbsentAfterTimeout: true }, + observer: { + driverCliSha256: observerBindings.driverCliSha256, + driverModuleSha256: observerBindings.driverModuleSha256, + runtimeSessionId: observerBindings.runtimeSessionId, + }, + }); +} + +function writeArtifact(path: string, value: unknown) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { + flag: "wx", + mode: 0o600, + }); +} + +function deferred() { + let resolvePromise!: (value: T) => void; + let rejectPromise!: (reason?: unknown) => void; + let settled = false; + const promise = new Promise((accept, reject) => { + resolvePromise = accept; + rejectPromise = reject; + }); + return { + get settled() { + return settled; + }, + promise, + reject(reason?: unknown) { + settled = true; + rejectPromise(reason); + }, + resolve(value: T) { + settled = true; + resolvePromise(value); + }, + }; +} + +function fixture() { + const runtimeSessionId = `rt-coord-${randomBytes(4).toString("hex")}`; + const sessionRoot = resolve(root, ".rt", runtimeSessionId); + const specRoot = resolve(sessionRoot, "run/rootless-specs"); + mkdirSync(specRoot, { recursive: true, mode: 0o700 }); + const receipt = baseReceipt(); + const source = `${JSON.stringify(receipt, null, 2)}\n`; + const baseReceiptPath = resolve(specRoot, `${finalContainerId}.receipt.json`); + writeFileSync(baseReceiptPath, source, { flag: "wx", mode: 0o600 }); + return { + input: { + baseReceiptFileSha256: sha256CgroupBytes(source), + baseReceiptPath, + baseReceiptPayloadSha256: receipt.receiptPayloadSha256, + finalContainerId, + intendedAggregateLimits, + invocationId, + sanitizedSpecSha256, + sessionRoot, + }, + observerBindings: { + driverCliSha256: "c".repeat(64), + driverModuleSha256: "d".repeat(64), + runtimeAttestationSha256: "e".repeat(64), + runtimeSessionId, + }, + }; +} + +function harness({ + keepRunningAfterCompletion = false, + processPid = 321, + readyPid = 321, +} = {}) { + const value = fixture(); + const processCompletion = deferred<{ + code: number | null; + error?: Error; + outputExceeded: boolean; + signal: NodeJS.Signals | null; + }>(); + let captured: + | { + manifest: ContainedCgroupObserverManifest; + paths: QualificationPaths; + } + | undefined; + const terminations: NodeJS.Signals[] = []; + let forcedStopped = false; + const persisted: Array> = []; + const verified: Array> = []; + const times = [ + new Date("2026-07-20T12:00:00.000Z"), + new Date("2026-07-20T12:00:20.000Z"), + ]; + const qualified = { + qualificationArtifacts: {}, + qualifiedReceipt: {}, + qualifiedReceiptFileSha256: "f".repeat(64), + qualifiedReceiptPath: resolve( + value.input.sessionRoot, + `run/rootless-specs/${finalContainerId}.qualified-receipt.json`, + ), + qualifiedReceiptPayloadSha256: "0".repeat(64), + }; + const coordinator = createContainedCgroupQualificationCoordinator({ + now: () => times.shift() ?? new Date("2026-07-20T12:00:20.000Z"), + persistQualifiedReceipt: (input) => { + persisted.push(input); + return qualified; + }, + resolveObserverBindings: () => value.observerBindings, + sleep: async () => undefined, + startObserver: ({ manifest, paths }) => { + captured = { manifest, paths }; + writeArtifact( + paths.observerReadyPath, + createContainedCgroupObserverReady(manifest, { + armedAt: new Date("2026-07-20T12:00:05.000Z"), + observerPid: readyPid, + }), + ); + return { + completion: processCompletion.promise, + isRunning: () => + !forcedStopped && + (!processCompletion.settled || keepRunningAfterCompletion), + pid: processPid, + terminate: (signal: NodeJS.Signals) => { + terminations.push(signal); + forcedStopped = true; + processCompletion.resolve({ + code: null, + outputExceeded: false, + signal, + }); + return true; + }, + }; + }, + verifyQualifiedReceipt: (input) => { + verified.push(input); + return {}; + }, + }); + return { + coordinator, + get captured() { + if (captured === undefined) throw new Error("observer was not started"); + return captured; + }, + persisted, + processCompletion, + qualified, + terminations, + value, + verified, + }; +} + +async function preparedHarness() { + const value = harness(); + const handle = await value.coordinator.begin(value.value.input); + const draft = createContainedCgroupObserverDraft( + value.captured.manifest, + observation(), + ); + writeArtifact(value.captured.paths.observerDraftPath, draft); + await value.coordinator.waitForDraft(handle); + return { ...value, draft, handle }; +} + +describe("contained cgroup qualification coordinator", () => { + it("orders readiness, draft, finalization, process exit, and persistence", async () => { + const value = await preparedHarness(); + const completing = value.coordinator.complete(value.handle, { + cleanup: clean, + resultReleased: false, + runtimeFailed: false, + timeoutObserved: true, + }); + const finalization = JSON.parse( + readFileSync(value.captured.paths.finalizationPath, "utf8"), + ); + expect(finalization.status).toBe("FINALIZE"); + expect(finalization.observerDraftPayloadSha256).toBe( + value.draft.receiptPayloadSha256, + ); + writeArtifact( + value.captured.paths.evidencePath, + aggregateEvidence( + value.value.observerBindings, + finalization.receiptPayloadSha256, + ), + ); + value.processCompletion.resolve({ + code: 0, + outputExceeded: false, + signal: null, + }); + + await expect(completing).resolves.toEqual(value.qualified); + expect(value.persisted).toEqual([ + { + baseReceiptFileSha256: value.value.input.baseReceiptFileSha256, + baseReceiptPath: value.value.input.baseReceiptPath, + finalContainerId, + sessionRoot: value.value.input.sessionRoot, + }, + ]); + expect(value.verified).toHaveLength(1); + expect(value.terminations).toEqual([]); + }); + + it("rejects a ready receipt from a different process", async () => { + const value = harness({ readyPid: 322 }); + await expect(value.coordinator.begin(value.value.input)).rejects.toThrow( + /readiness binding changed/u, + ); + expect(value.terminations).toEqual(["SIGTERM"]); + expect(value.persisted).toEqual([]); + }); + + it("terminates a partially invalid observer handle", async () => { + const value = harness({ processPid: 0 }); + await expect(value.coordinator.begin(value.value.input)).rejects.toThrow( + /observer start failed/u, + ); + expect(value.terminations).toEqual(["SIGTERM"]); + expect(value.persisted).toEqual([]); + }); + + it("rejects an observer that exits or rejects before readiness", async () => { + const exited = harness(); + exited.processCompletion.resolve({ + code: 0, + outputExceeded: false, + signal: null, + }); + await expect(exited.coordinator.begin(exited.value.input)).rejects.toThrow( + /readiness|exited/u, + ); + expect(exited.persisted).toEqual([]); + + const rejected = harness(); + rejected.processCompletion.reject(new Error("observer spawn failed")); + await expect( + rejected.coordinator.begin(rejected.value.input), + ).rejects.toThrow(/readiness|exited/u); + expect(rejected.persisted).toEqual([]); + + const rejectedButRunning = harness({ + keepRunningAfterCompletion: true, + }); + rejectedButRunning.processCompletion.reject( + new Error("observer completion channel failed"), + ); + await expect( + rejectedButRunning.coordinator.begin(rejectedButRunning.value.input), + ).rejects.toThrow(/readiness|exited/u); + expect(rejectedButRunning.terminations).toEqual(["SIGTERM"]); + }); + + it("terminates the observer when draft validation fails", async () => { + const value = harness(); + const handle = await value.coordinator.begin(value.value.input); + writeArtifact(value.captured.paths.observerDraftPath, {}); + await expect(value.coordinator.waitForDraft(handle)).rejects.toThrow( + /draft/u, + ); + expect(value.terminations).toEqual(["SIGTERM"]); + expect(value.persisted).toEqual([]); + }); + + it("records an honest pre-draft abort after observer failure", async () => { + const value = harness(); + const handle = await value.coordinator.begin(value.value.input); + writeArtifact( + value.captured.paths.failurePath, + createContainedCgroupObserverFailure(value.captured.manifest, { + failedAt: new Date("2026-07-20T12:00:10.000Z"), + }), + ); + value.processCompletion.resolve({ + code: 70, + outputExceeded: false, + signal: null, + }); + await expect(value.coordinator.waitForDraft(handle)).rejects.toThrow( + /observer failed/u, + ); + await expect( + value.coordinator.complete(handle, { + cleanup: clean, + resultReleased: false, + runtimeFailed: true, + timeoutObserved: false, + }), + ).rejects.toThrow(/OBSERVER_FAILED/u); + const finalization = JSON.parse( + readFileSync(value.captured.paths.finalizationPath, "utf8"), + ); + expect(finalization).toMatchObject({ + abortCode: "OBSERVER_FAILED", + observerDraftPayloadSha256: null, + status: "ABORT", + }); + expect(value.persisted).toEqual([]); + }); + + it.each([ + { + expected: "TIMEOUT_NOT_OBSERVED", + outcome: { + cleanup: clean, + resultReleased: false, + runtimeFailed: false, + timeoutObserved: false, + }, + }, + { + expected: "RESULT_RELEASED", + outcome: { + cleanup: clean, + resultReleased: true, + runtimeFailed: false, + timeoutObserved: true, + }, + }, + { + expected: "CLEANUP_UNVERIFIED", + outcome: { + cleanup: { ...clean, snapshotAbsent: false }, + resultReleased: false, + runtimeFailed: false, + timeoutObserved: true, + }, + }, + ])("fails closed with $expected", async ({ expected, outcome }) => { + const value = await preparedHarness(); + await expect( + value.coordinator.complete(value.handle, outcome), + ).rejects.toThrow(expected); + const finalization = JSON.parse( + readFileSync(value.captured.paths.finalizationPath, "utf8"), + ); + expect(finalization.abortCode).toBe(expected); + expect(finalization.observerDraftPayloadSha256).toBe( + value.draft.receiptPayloadSha256, + ); + expect(value.persisted).toEqual([]); + }); + + it("rejects conflicting evidence and failure after observer exit", async () => { + const value = await preparedHarness(); + const completing = value.coordinator.complete(value.handle, { + cleanup: clean, + resultReleased: false, + runtimeFailed: false, + timeoutObserved: true, + }); + const finalization = JSON.parse( + readFileSync(value.captured.paths.finalizationPath, "utf8"), + ); + writeArtifact( + value.captured.paths.evidencePath, + aggregateEvidence( + value.value.observerBindings, + finalization.receiptPayloadSha256, + ), + ); + writeArtifact( + value.captured.paths.failurePath, + createContainedCgroupObserverFailure(value.captured.manifest, { + failedAt: new Date("2026-07-20T12:00:30.000Z"), + }), + ); + value.processCompletion.resolve({ + code: 0, + outputExceeded: false, + signal: null, + }); + + await expect(completing).rejects.toThrow(/terminal outcome/u); + expect(value.persisted).toEqual([]); + }); + + it("rejects missing evidence and a nonzero terminal exit", async () => { + const missing = await preparedHarness(); + const missingCompletion = missing.coordinator.complete(missing.handle, { + cleanup: clean, + resultReleased: false, + runtimeFailed: false, + timeoutObserved: true, + }); + missing.processCompletion.resolve({ + code: 0, + outputExceeded: false, + signal: null, + }); + await expect(missingCompletion).rejects.toThrow(/terminal outcome/u); + expect(missing.persisted).toEqual([]); + + const failed = await preparedHarness(); + const failedCompletion = failed.coordinator.complete(failed.handle, { + cleanup: clean, + resultReleased: false, + runtimeFailed: false, + timeoutObserved: true, + }); + const finalization = JSON.parse( + readFileSync(failed.captured.paths.finalizationPath, "utf8"), + ); + writeArtifact( + failed.captured.paths.evidencePath, + aggregateEvidence( + failed.value.observerBindings, + finalization.receiptPayloadSha256, + ), + ); + failed.processCompletion.resolve({ + code: 70, + outputExceeded: false, + signal: null, + }); + await expect(failedCompletion).rejects.toThrow(/observer failed/u); + expect(failed.persisted).toEqual([]); + }); +}); From eaf4a60e0c9654e6673fb15c289729474041815e Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:31:52 +0530 Subject: [PATCH 052/143] feat(runtime): wire aggregate qualification --- scripts/contained-runtime-linearizer.d.mts | 5 + scripts/contained-runtime-linearizer.mjs | 19 + scripts/contained-runtime-linearizer.test.ts | 53 +++ scripts/contained-runtime-run.d.mts | 21 +- scripts/contained-runtime-run.mjs | 123 +++++- scripts/contained-runtime-server.mjs | 222 +++++----- scripts/contained-runtime.test.ts | 431 ++++++++++++++++++- 7 files changed, 755 insertions(+), 119 deletions(-) create mode 100644 scripts/contained-runtime-linearizer.d.mts create mode 100644 scripts/contained-runtime-linearizer.mjs create mode 100644 scripts/contained-runtime-linearizer.test.ts diff --git a/scripts/contained-runtime-linearizer.d.mts b/scripts/contained-runtime-linearizer.d.mts new file mode 100644 index 00000000..6b4f51d4 --- /dev/null +++ b/scripts/contained-runtime-linearizer.d.mts @@ -0,0 +1,5 @@ +export interface ContainedRuntimeLinearizer { + run(task: () => T | Promise): Promise; +} + +export function createContainedRuntimeLinearizer(): ContainedRuntimeLinearizer; diff --git a/scripts/contained-runtime-linearizer.mjs b/scripts/contained-runtime-linearizer.mjs new file mode 100644 index 00000000..14213402 --- /dev/null +++ b/scripts/contained-runtime-linearizer.mjs @@ -0,0 +1,19 @@ +export function createContainedRuntimeLinearizer() { + let tail = Promise.resolve(); + + return Object.freeze({ + run(task) { + if (typeof task !== "function") { + return Promise.reject( + new Error("contained runtime linearized task is invalid"), + ); + } + const current = tail.then(task, task); + tail = current.then( + () => undefined, + () => undefined, + ); + return current; + }, + }); +} diff --git a/scripts/contained-runtime-linearizer.test.ts b/scripts/contained-runtime-linearizer.test.ts new file mode 100644 index 00000000..40e00f50 --- /dev/null +++ b/scripts/contained-runtime-linearizer.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +import { createContainedRuntimeLinearizer } from "./contained-runtime-linearizer.mjs"; + +describe("contained runtime request linearizer", () => { + it("does not begin a drain until an accepted run reaches its terminal response", async () => { + const linearizer = createContainedRuntimeLinearizer(); + const events: string[] = []; + let releaseRun!: () => void; + const runCanFinish = new Promise((resolve) => { + releaseRun = resolve; + }); + + const run = linearizer.run(async () => { + events.push("run-started"); + await runCanFinish; + events.push("run-terminal-response"); + }); + const drain = linearizer.run(() => { + events.push("drain-started"); + }); + + await Promise.resolve(); + expect(events).toEqual(["run-started"]); + releaseRun(); + await Promise.all([run, drain]); + expect(events).toEqual([ + "run-started", + "run-terminal-response", + "drain-started", + ]); + }); + + it("continues serializing after a failed request", async () => { + const linearizer = createContainedRuntimeLinearizer(); + const failed = linearizer.run(() => { + throw new Error("request failed"); + }); + const next = linearizer.run(() => "next response"); + + await expect(failed).rejects.toThrow("request failed"); + await expect(next).resolves.toBe("next response"); + }); + + it("rejects invalid tasks without poisoning the queue", async () => { + const linearizer = createContainedRuntimeLinearizer(); + + await expect(linearizer.run(null as never)).rejects.toThrow( + "linearized task is invalid", + ); + await expect(linearizer.run(() => "ready")).resolves.toBe("ready"); + }); +}); diff --git a/scripts/contained-runtime-run.d.mts b/scripts/contained-runtime-run.d.mts index fcdba8a8..90c4b336 100644 --- a/scripts/contained-runtime-run.d.mts +++ b/scripts/contained-runtime-run.d.mts @@ -1,3 +1,6 @@ +import type { ContainedCgroupQualificationCoordinator } from "./contained-cgroup-qualification-coordinator.mjs"; +import type { ContainedRuntimeQualificationMode } from "./contained-runtime-request.mjs"; + export interface ContainedRuntimeCommand { program: string; args: string[]; @@ -56,6 +59,7 @@ export type ContainedRuntimeRunContext = Omit< > & { cwd: string; environment: NodeJS.ProcessEnv; + qualificationMode: ContainedRuntimeQualificationMode; stdin: Buffer; }; @@ -66,8 +70,7 @@ export interface ContainedRuntimeSpawnResult { error?: Error & { code?: string }; } -export interface ContainedRunControlReceipt { - schemaVersion: "2"; +interface ContainedRunControlReceiptBase { status: "TIMED_OUT_CLEAN" | "TIMED_OUT_UNCLEAN"; timeoutKind: "WALL_CLOCK"; runtimePolicySha256: string; @@ -92,6 +95,15 @@ export interface ContainedRunControlReceipt { receiptPayloadSha256: string; } +export type ContainedRunControlReceipt = + | (ContainedRunControlReceiptBase & { + schemaVersion: "2"; + }) + | (ContainedRunControlReceiptBase & { + schemaVersion: "3"; + qualificationMode: "aggregate-timeout-proof-v1"; + }); + export type ContainedRuntimeSpawn = ( program: string, args: string[], @@ -161,9 +173,10 @@ export declare function executeContainedRun( }) => void, createInvocationId?: () => string, now?: () => number, -): { + qualificationCoordinator?: ContainedCgroupQualificationCoordinator, +): Promise<{ status: number; stdout: Buffer; stderr: Buffer; controlReceipt?: ContainedRunControlReceipt; -}; +}>; diff --git a/scripts/contained-runtime-run.mjs b/scripts/contained-runtime-run.mjs index f414fc01..c22aee13 100644 --- a/scripts/contained-runtime-run.mjs +++ b/scripts/contained-runtime-run.mjs @@ -13,6 +13,7 @@ import { isAbsolute, relative, resolve } from "node:path"; import { performance } from "node:perf_hooks"; import { fileURLToPath } from "node:url"; +import { containedCgroupQualificationCoordinator } from "./contained-cgroup-qualification-coordinator.mjs"; import { persistContainedRootlessSpec, sanitizeContainedRootlessSpec, @@ -28,6 +29,7 @@ import { validateContainedImageAliasTarget, verifyContainedReadOnlyMounts, } from "./contained-image-authority.mjs"; +import { AGGREGATE_TIMEOUT_QUALIFICATION_MODE } from "./contained-runtime-request.mjs"; const repositoryRoot = realpathSync( resolve(fileURLToPath(import.meta.url), "../.."), @@ -99,7 +101,7 @@ function canonicalJson(value) { return JSON.stringify(value); } -const runControlReceiptKeys = [ +const runControlReceiptV2Keys = [ "candidateWallSeconds", "cleanupReserveMs", "commandSha256", @@ -124,14 +126,24 @@ const runControlReceiptKeys = [ "timeoutKind", "runtimePolicySha256", ]; +const runControlReceiptV3Keys = [ + ...runControlReceiptV2Keys, + "qualificationMode", +]; export function validateContainedRunControlReceipt(value) { if ( value === null || typeof value !== "object" || Array.isArray(value) || - JSON.stringify(Object.keys(value).sort()) !== - JSON.stringify([...runControlReceiptKeys].sort()) + !( + (value.schemaVersion === "2" && + JSON.stringify(Object.keys(value).sort()) === + JSON.stringify([...runControlReceiptV2Keys].sort())) || + (value.schemaVersion === "3" && + JSON.stringify(Object.keys(value).sort()) === + JSON.stringify([...runControlReceiptV3Keys].sort())) + ) ) { throw new Error("contained runtime control receipt shape is invalid"); } @@ -148,7 +160,9 @@ export function validateContainedRunControlReceipt(value) { "timeoutObserved", ]; if ( - value.schemaVersion !== "2" || + !["2", "3"].includes(value.schemaVersion) || + (value.schemaVersion === "3" && + value.qualificationMode !== AGGREGATE_TIMEOUT_QUALIFICATION_MODE) || !["TIMED_OUT_CLEAN", "TIMED_OUT_UNCLEAN"].includes(value.status) || !invocationIdPattern.test(value.invocationId ?? "") || !containerIdPattern.test(value.finalContainerId ?? "") || @@ -988,14 +1002,23 @@ function resultWasReleased(plan, started) { ); } -export function executeContainedRun( +export async function executeContainedRun( context, spawn = spawnSync, persistSpec = persistContainedRootlessSpec, verifySpec = verifyPersistedContainedRootlessSpec, createInvocationId = () => randomBytes(32).toString("hex"), now = () => performance.now(), + qualificationCoordinator = containedCgroupQualificationCoordinator, ) { + if ( + context.qualificationMode !== null && + context.qualificationMode !== AGGREGATE_TIMEOUT_QUALIFICATION_MODE + ) { + return failedResult("contained runtime qualification mode is invalid"); + } + const qualificationRequested = + context.qualificationMode === AGGREGATE_TIMEOUT_QUALIFICATION_MODE; const startedAtMs = now(); const invocationId = createInvocationId(); if (!invocationIdPattern.test(invocationId)) { @@ -1408,6 +1431,26 @@ export function executeContainedRun( ); } + let qualificationHandle; + if (qualificationRequested) { + try { + qualificationHandle = await qualificationCoordinator.begin({ + baseReceiptFileSha256: persisted.receiptFileSha256, + baseReceiptPath: persisted.receiptPath, + baseReceiptPayloadSha256: prepared.receipt.receiptPayloadSha256, + finalContainerId: prepared.finalContainerId, + intendedAggregateLimits: prepared.receipt.intendedAggregateLimits, + invocationId, + sanitizedSpecSha256: prepared.receipt.sanitizedSpecSha256, + sessionRoot: context.sessionRoot, + }); + } catch { + return finish( + failedResult("contained runtime qualification readiness failed"), + ); + } + } + const candidateStartedAt = now(); const started = executeSpawn( plan.start.program, @@ -1424,6 +1467,14 @@ export function executeContainedRun( { ...runtimeOptions, input: context.stdin }, ); const timeoutObserved = started.error?.code === "ETIMEDOUT"; + let qualificationDraftFailed = false; + if (qualificationRequested) { + try { + await qualificationCoordinator.waitForDraft(qualificationHandle); + } catch { + qualificationDraftFailed = true; + } + } let imageRootfsUnchanged = true; try { const afterSha256 = snapshotDiffSha256( @@ -1548,6 +1599,68 @@ export function executeContainedRun( rootfsCleaned.diagnostics, ), }); + if (qualificationRequested) { + let resultReleased = true; + try { + resultReleased = resultWasReleased(plan, started); + } catch { + resultReleased = true; + } + const taskAbsent = cleaned.state.valid && !cleaned.state.taskPresent; + const containerAbsent = + cleaned.state.valid && !cleaned.state.containerPresent; + const snapshotAbsent = + cleaned.state.valid && !cleaned.state.snapshotPresent; + let qualified; + try { + qualified = await qualificationCoordinator.complete(qualificationHandle, { + cleanup: { + taskAbsent, + containerAbsent, + snapshotAbsent, + invocationAliasAbsent: lastAliasCleanup.absent === true, + imageRootfsAbsent: lastRootfsCleanup.absent === true, + persistedAuthorityVerified: persistedAfterRun, + readOnlyMountsUnchanged: readOnlyMountsAfterRun, + imageRootfsUnchanged, + }, + resultReleased, + runtimeFailed: qualificationDraftFailed, + timeoutObserved, + }); + } catch { + return failedResult("contained runtime aggregate qualification failed"); + } + const controlPayload = { + schemaVersion: "3", + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + status: "TIMED_OUT_CLEAN", + timeoutKind: "WALL_CLOCK", + runtimePolicySha256: CONTAINED_RUNTIME_POLICY_SHA256, + invocationId, + finalContainerId: prepared.finalContainerId, + commandSha256: imageAuthority.commandSha256, + rootlessReceiptFileSha256: qualified.qualifiedReceiptFileSha256, + rootlessReceiptPayloadSha256: qualified.qualifiedReceiptPayloadSha256, + timeoutObserved: true, + candidateWallSeconds: cpuLimit, + elapsedMs: Math.max(1, Math.floor(now() - candidateStartedAt)), + cleanupReserveMs, + taskAbsent, + containerAbsent, + snapshotAbsent, + invocationAliasAbsent: lastAliasCleanup.absent === true, + imageRootfsAbsent: lastRootfsCleanup.absent === true, + persistedAuthorityVerified: persistedAfterRun, + readOnlyMountsUnchanged: readOnlyMountsAfterRun, + imageRootfsUnchanged, + resultReleased, + }; + return { + ...finished, + controlReceipt: createRunControlReceipt(controlPayload), + }; + } if (!timeoutObserved || plan.outputDirectory === undefined) return finished; let resultReleased = true; diff --git a/scripts/contained-runtime-server.mjs b/scripts/contained-runtime-server.mjs index 8c50187d..446c3b04 100644 --- a/scripts/contained-runtime-server.mjs +++ b/scripts/contained-runtime-server.mjs @@ -24,6 +24,7 @@ import { probeContainedShimSocketDirectory, } from "./contained-containerd-config.mjs"; import { createContainedRuntimeEnvironment } from "./contained-runtime-environment.mjs"; +import { createContainedRuntimeLinearizer } from "./contained-runtime-linearizer.mjs"; import { parseContainedRuntimeRequest } from "./contained-runtime-request.mjs"; import { verifyPersistedContainedRootlessSpec } from "./contained-rootless-spec.mjs"; @@ -108,6 +109,7 @@ const environment = createContainedRuntimeEnvironment({ let runtimeState = "ACTIVE"; let drainAttestationSha256; let drainReceipt; +const requestLinearizer = createContainedRuntimeLinearizer(); function lines(value) { return String(value ?? "") @@ -403,119 +405,129 @@ const server = createServer((socket) => { socket.on("timeout", () => socket.destroy()); socket.on("error", () => undefined); socket.on("end", () => { - let response; - try { - const request = parseContainedRuntimeRequest( - JSON.parse(Buffer.concat(chunks).toString("utf8")), - ); - const stdin = request.stdin; - if (request.args[0] === "counterlab-drain") { - const receipt = createDrainReceipt(request.args, stdin); - response = { - schemaVersion: "1", - exitCode: 0, - stdoutBase64: Buffer.from( - `${canonicalJson(receipt)}\n`, - "utf8", - ).toString("base64"), - stderrBase64: "", - }; - } else { - if (runtimeState !== "ACTIVE") { - throw new Error( - runtimeState === "DRAINED" - ? "contained runtime is drained" - : "contained runtime is draining", - ); - } - const validation = spawnSync( - process.execPath, - [ - resolve(root, "scripts/validate-contained-runtime-command.mjs"), - ...request.args, - ], - { - cwd: root, - env: environment, - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - timeout: 10_000, - }, + void requestLinearizer.run(async () => { + let response; + try { + const request = parseContainedRuntimeRequest( + JSON.parse(Buffer.concat(chunks).toString("utf8")), ); - if (validation.status !== 0) { - throw new Error( - "contained runtime command failed independent validation", + const stdin = request.stdin; + if (request.args[0] === "counterlab-drain") { + let receipt; + try { + receipt = createDrainReceipt(request.args, stdin); + } catch (error) { + if (runtimeState === "DRAINING" && drainReceipt === undefined) { + runtimeState = "ACTIVE"; + } + throw error; + } + response = { + schemaVersion: "1", + exitCode: 0, + stdoutBase64: Buffer.from( + `${canonicalJson(receipt)}\n`, + "utf8", + ).toString("base64"), + stderrBase64: "", + }; + } else { + if (runtimeState !== "ACTIVE") { + throw new Error( + runtimeState === "DRAINED" + ? "contained runtime is drained" + : "contained runtime is draining", + ); + } + const validation = spawnSync( + process.execPath, + [ + resolve(root, "scripts/validate-contained-runtime-command.mjs"), + ...request.args, + ], + { + cwd: root, + env: environment, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 10_000, + }, ); - } - const result = - request.args[0] === "run" - ? executeContainedRun({ - args: request.args, - binRoot, - clientFifoRoot, - containerdSocket, - cwd: root, - environment, - installRoot, - qualificationMode: request.qualificationMode, - sessionRoot, - stdin, - }) - : spawnSync( - resolve(binRoot, "nerdctl"), - [ - "--address", + if (validation.status !== 0) { + throw new Error( + "contained runtime command failed independent validation", + ); + } + const result = + request.args[0] === "run" + ? await executeContainedRun({ + args: request.args, + binRoot, + clientFifoRoot, containerdSocket, - "--namespace", - "counterlab-v6.1", - "--snapshotter", - "native", - "--data-root", - resolve(sessionRoot, "data/nerdctl"), - "--cgroup-manager", - "cgroupfs", - "--cni-path", - resolve(installRoot, "libexec/cni"), - "--cni-netconfpath", - resolve(sessionRoot, "config/cni"), - "--hosts-dir", - resolve(sessionRoot, "config/certs.d"), - "--experimental=false", - ...request.args, - ], - { cwd: root, - env: environment, - input: stdin, - encoding: null, - stdio: ["pipe", "pipe", "pipe"], - timeout: 1_800_000, - maxBuffer: 32 * 1024 * 1024, - }, - ); + environment, + installRoot, + qualificationMode: request.qualificationMode, + sessionRoot, + stdin, + }) + : spawnSync( + resolve(binRoot, "nerdctl"), + [ + "--address", + containerdSocket, + "--namespace", + "counterlab-v6.1", + "--snapshotter", + "native", + "--data-root", + resolve(sessionRoot, "data/nerdctl"), + "--cgroup-manager", + "cgroupfs", + "--cni-path", + resolve(installRoot, "libexec/cni"), + "--cni-netconfpath", + resolve(sessionRoot, "config/cni"), + "--hosts-dir", + resolve(sessionRoot, "config/certs.d"), + "--experimental=false", + ...request.args, + ], + { + cwd: root, + env: environment, + input: stdin, + encoding: null, + stdio: ["pipe", "pipe", "pipe"], + timeout: 1_800_000, + maxBuffer: 32 * 1024 * 1024, + }, + ); + response = { + schemaVersion: "1", + exitCode: result.status ?? 1, + stdoutBase64: Buffer.from(result.stdout ?? "").toString("base64"), + stderrBase64: Buffer.from(result.stderr ?? "").toString("base64"), + ...(result.controlReceipt === undefined + ? {} + : { runControlReceipt: result.controlReceipt }), + }; + } + } catch (error) { response = { schemaVersion: "1", - exitCode: result.status ?? 1, - stdoutBase64: Buffer.from(result.stdout ?? "").toString("base64"), - stderrBase64: Buffer.from(result.stderr ?? "").toString("base64"), - ...(result.controlReceipt === undefined - ? {} - : { runControlReceipt: result.controlReceipt }), + exitCode: 1, + stdoutBase64: "", + stderrBase64: Buffer.from( + error instanceof Error + ? `${error.message}\n` + : "contained runtime failure\n", + ).toString("base64"), }; } - } catch (error) { - response = { - schemaVersion: "1", - exitCode: 1, - stdoutBase64: "", - stderrBase64: Buffer.from( - error instanceof Error - ? `${error.message}\n` - : "contained runtime failure\n", - ).toString("base64"), - }; - } - socket.end(JSON.stringify(response)); + socket.end(JSON.stringify(response)); + }); }); }); diff --git a/scripts/contained-runtime.test.ts b/scripts/contained-runtime.test.ts index e9bb9308..f446ed42 100644 --- a/scripts/contained-runtime.test.ts +++ b/scripts/contained-runtime.test.ts @@ -44,6 +44,7 @@ import { renderContainedContainerdConfig, } from "./contained-containerd-config.mjs"; import { createContainedRuntimeEnvironment } from "./contained-runtime-environment.mjs"; +import { AGGREGATE_TIMEOUT_QUALIFICATION_MODE } from "./contained-runtime-request.mjs"; const root = process.cwd(); const validator = resolve( @@ -778,6 +779,207 @@ function absentInspectionResponses() { return [successful(), missing(), missing()]; } +function qualifiedRuntimeHarness() { + const sessionRoot = resolve(root, ".rt/rt-validator-qualified"); + const installRoot = resolve( + root, + "node_modules/.cache/counterlab-v6.1/rootless-tools/install-v2.3.1", + ); + const command = boundedAdapterCommand(); + const plan = containedRunPlan({ + args: command, + binRoot: resolve(installRoot, "bin"), + clientFifoRoot: resolve(sessionRoot, "run/client-fifo"), + containerdSocket: resolve(sessionRoot, "run/containerd.sock"), + installRoot, + invocationId, + sessionRoot, + }); + const fixture = imageFixture(command); + const stagingContainerId = "d".repeat(64); + let finalContainerId = ""; + const alias = plan.imageAlias; + const imageRootfsPath = resolve( + sessionRoot, + `run/rootless-specs/${invocationId}.image-rootfs`, + ); + const events: string[] = []; + let aliasPresent = false; + let imageRootfsMounted = false; + let stagingPresent = true; + let taskPresent = false; + let containerPresent = false; + let baseSpecSha256 = ""; + + writeFileSync(resolve(output, "public-tests.stdout"), "", { mode: 0o600 }); + writeFileSync(resolve(output, "public-tests.stderr"), "", { mode: 0o600 }); + + const fakeSpawn = ( + _program: string, + args: string[], + _options: { timeout: number }, + ) => { + const joined = args.join(" "); + if (joined.includes("image inspect docker.io/library/counterlab-adapter")) { + return successful(fixture.targetSource); + } + if (joined.includes(`content get ${fixture.manifestDigest}`)) { + return successful(fixture.manifestSource); + } + if (joined.includes(`content get ${fixture.configDigest}`)) { + return successful(fixture.configSource); + } + if (joined.includes(`images inspect ${alias}`)) { + return aliasPresent ? successful("{}") : missing(); + } + if (joined.includes(`image inspect ${alias}`)) { + return successful(aliasMetadata(alias, fixture)); + } + if (joined.includes("images tag")) { + aliasPresent = true; + return successful(); + } + if (joined.includes(`images remove ${alias}`)) { + events.push("alias-cleaned"); + aliasPresent = false; + return successful(); + } + if (joined.includes("images mount")) { + imageRootfsMounted = true; + return successful( + `${fixture.authority.rootfsChainId}\n${imageRootfsPath}\n`, + ); + } + if (joined.includes("images unmount")) { + events.push("rootfs-cleaned"); + imageRootfsMounted = false; + return successful(`${imageRootfsPath}\n`); + } + if (joined.includes(" create ")) { + return successful(`${stagingContainerId}\n`); + } + if (args.includes("--spec")) { + return successful( + rootlessSpec(stagingContainerId, plan.expected, fixture.authority), + ); + } + if (joined.includes("containers info")) { + const id = args.at(-1); + if (id === stagingContainerId) { + return stagingPresent + ? successful(containerMetadata(stagingContainerId, alias)) + : missing(); + } + if (id === finalContainerId) { + return containerPresent + ? successful( + configContainerMetadata(finalContainerId, baseSpecSha256), + ) + : missing(); + } + return missing(); + } + if (joined.includes("tasks list")) { + return successful(taskPresent ? `${finalContainerId}\n` : ""); + } + if (joined.includes("snapshots") && joined.includes(" diff ")) { + return successful("stable-rootfs-diff"); + } + if (joined.includes("snapshots") && joined.includes(" info ")) { + const id = args.at(-1); + if (id === imageRootfsPath) { + return imageRootfsMounted + ? successful( + imageRootfsSnapshotMetadata( + imageRootfsPath, + fixture.authority.rootfsChainId, + ), + ) + : missing(); + } + return missing(); + } + if (joined.includes(" rm --force ")) { + stagingPresent = false; + return successful(); + } + if (joined.includes(" run ")) { + events.push("candidate-started"); + taskPresent = true; + containerPresent = true; + const label = args.find((entry) => + entry.startsWith("io.counterlab.runtime.base-spec-sha256="), + ); + baseSpecSha256 = label!.split("=", 2)[1]!; + const timeoutError = Object.assign(new Error("candidate timed out"), { + code: "ETIMEDOUT", + }); + return { + status: null, + stdout: Buffer.alloc(0), + stderr: Buffer.alloc(0), + error: timeoutError, + }; + } + if (joined.includes("tasks delete")) { + events.push("task-cleaned"); + taskPresent = false; + return successful(); + } + if (joined.includes("containers delete")) { + events.push("container-cleaned"); + containerPresent = false; + return successful(); + } + throw new Error(`unexpected mock command: ${joined}`); + }; + + return { + context: { + args: command, + binRoot: resolve(installRoot, "bin"), + clientFifoRoot: resolve(sessionRoot, "run/client-fifo"), + containerdSocket: resolve(sessionRoot, "run/containerd.sock"), + cwd: root, + environment: process.env, + installRoot, + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + sessionRoot, + stdin: Buffer.alloc(0), + }, + events, + fakeSpawn, + get finalContainerId() { + return finalContainerId; + }, + imageRootfsPath, + persistSpec: ({ + finalContainerId: persistedFinalContainerId, + receipt, + sessionRoot: persistedSessionRoot, + }: { + finalContainerId: string; + receipt: Record; + sessionRoot: string; + }) => { + finalContainerId = persistedFinalContainerId; + return { + configFileSha256: String(receipt.configFileSha256), + configPath: resolve( + persistedSessionRoot, + `run/rootless-specs/${finalContainerId}.config.json`, + ), + imageRootfsPath, + receiptFileSha256: "4".repeat(64), + receiptPath: resolve( + persistedSessionRoot, + `run/rootless-specs/${finalContainerId}.receipt.json`, + ), + }; + }, + }; +} + describe("contained runtime command policy", () => { it("renders a bounded containerd shim manager configuration", () => { const config = renderContainedContainerdConfig(root); @@ -834,7 +1036,7 @@ describe("contained runtime command policy", () => { symlinkSync(logTarget, runcLogSymlink); }); - it("accepts only a self-hashed clean timeout control receipt", () => { + it("accepts only self-hashed timeout controls with exact v2 or qualified v3 shape", () => { const payload = { schemaVersion: "2", status: "TIMED_OUT_CLEAN", @@ -867,6 +1069,32 @@ describe("contained runtime command policy", () => { }; expect(validateContainedRunControlReceipt(receipt)).toEqual(receipt); + const qualifiedPayload = { + ...payload, + schemaVersion: "3", + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + } as const; + const qualifiedReceipt = { + ...qualifiedPayload, + receiptPayloadSha256: createHash("sha256") + .update(canonicalJson(qualifiedPayload)) + .digest("hex"), + }; + expect(validateContainedRunControlReceipt(qualifiedReceipt)).toEqual( + qualifiedReceipt, + ); + expect(() => + validateContainedRunControlReceipt({ + ...qualifiedReceipt, + qualificationMode: "unknown", + }), + ).toThrow(/qualification|binding/u); + expect(() => + validateContainedRunControlReceipt({ + ...receipt, + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + }), + ).toThrow(/shape/u); expect(() => validateContainedRunControlReceipt({ ...receipt, @@ -884,6 +1112,184 @@ describe("contained runtime command policy", () => { ).toThrow(/status/u); }); + it("rejects an unknown qualification mode before invoking the runtime", async () => { + let spawned = false; + const result = await executeContainedRun( + { + args: startupCommand(), + binRoot: resolve(root, "node_modules/.cache/unused/bin"), + clientFifoRoot: resolve(root, ".rt/unused/client-fifo"), + containerdSocket: resolve(root, ".rt/unused/containerd.sock"), + cwd: root, + environment: process.env, + installRoot: resolve(root, "node_modules/.cache/unused"), + qualificationMode: "unknown" as never, + sessionRoot: resolve(root, ".rt/unused"), + stdin: Buffer.alloc(0), + }, + () => { + spawned = true; + throw new Error("invalid qualification mode must not reach spawn"); + }, + ); + + expect(result.status).toBe(1); + expect(result.stderr.toString("utf8")).toContain( + "qualification mode is invalid", + ); + expect(spawned).toBe(false); + }); + + it("binds a clean timed-out run to the aggregate-qualified receipt", async () => { + const harness = qualifiedRuntimeHarness(); + let completionOutcome: unknown; + const qualifiedReceiptFileSha256 = "7".repeat(64); + const qualifiedReceiptPayloadSha256 = "8".repeat(64); + const coordinator = { + async begin(input: { finalContainerId: string; invocationId: string }) { + harness.events.push("qualification-ready"); + expect(input).toMatchObject({ + finalContainerId: harness.finalContainerId, + invocationId, + }); + return { + finalContainerId: input.finalContainerId, + invocationId: input.invocationId, + }; + }, + async waitForDraft() { + harness.events.push("qualification-draft"); + return { receiptPayloadSha256: "6".repeat(64) }; + }, + async complete(_handle: unknown, outcome: unknown) { + harness.events.push("qualification-complete"); + completionOutcome = outcome; + return { + qualificationArtifacts: {}, + qualifiedReceipt: {}, + qualifiedReceiptFileSha256, + qualifiedReceiptPath: resolve( + harness.context.sessionRoot, + `run/rootless-specs/${harness.finalContainerId}.qualified-receipt.json`, + ), + qualifiedReceiptPayloadSha256, + }; + }, + }; + + const result = await executeContainedRun( + harness.context, + harness.fakeSpawn, + harness.persistSpec, + () => undefined, + () => invocationId, + undefined, + coordinator, + ); + + expect(result.status).toBe(1); + expect(result.stdout).toHaveLength(0); + expect( + result.controlReceipt, + JSON.stringify({ + events: harness.events, + stderr: result.stderr.toString("utf8"), + }), + ).toMatchObject({ + schemaVersion: "3", + qualificationMode: AGGREGATE_TIMEOUT_QUALIFICATION_MODE, + status: "TIMED_OUT_CLEAN", + timeoutObserved: true, + resultReleased: false, + rootlessReceiptFileSha256: qualifiedReceiptFileSha256, + rootlessReceiptPayloadSha256: qualifiedReceiptPayloadSha256, + }); + expect(completionOutcome).toEqual({ + cleanup: { + taskAbsent: true, + containerAbsent: true, + snapshotAbsent: true, + invocationAliasAbsent: true, + imageRootfsAbsent: true, + persistedAuthorityVerified: true, + readOnlyMountsUnchanged: true, + imageRootfsUnchanged: true, + }, + resultReleased: false, + runtimeFailed: false, + timeoutObserved: true, + }); + expect(harness.events).toEqual([ + "qualification-ready", + "candidate-started", + "qualification-draft", + "task-cleaned", + "container-cleaned", + "rootfs-cleaned", + "alias-cleaned", + "qualification-complete", + ]); + expect(() => + validateContainedRunControlReceipt(result.controlReceipt), + ).not.toThrow(); + }); + + it.each(["begin", "draft", "complete"] as const)( + "fails closed when aggregate qualification %s fails", + async (failurePoint) => { + const harness = qualifiedRuntimeHarness(); + const coordinator = { + async begin() { + harness.events.push("qualification-ready"); + if (failurePoint === "begin") throw new Error("begin failed"); + return { + finalContainerId: harness.finalContainerId, + invocationId, + }; + }, + async waitForDraft() { + harness.events.push("qualification-draft"); + if (failurePoint === "draft") throw new Error("draft failed"); + return { receiptPayloadSha256: "6".repeat(64) }; + }, + async complete() { + harness.events.push("qualification-complete"); + throw new Error("completion failed"); + }, + }; + + const result = await executeContainedRun( + harness.context, + harness.fakeSpawn, + harness.persistSpec, + () => undefined, + () => invocationId, + undefined, + coordinator, + ); + + expect(result.status).toBe(1); + expect(result.stdout).toHaveLength(0); + expect(result.controlReceipt).toBeUndefined(); + expect(result.stderr.toString("utf8")).toMatch( + failurePoint === "begin" + ? /qualification readiness failed/u + : /aggregate qualification failed/u, + ); + if (failurePoint === "begin") { + expect(harness.events).toEqual([ + "qualification-ready", + "rootfs-cleaned", + "alias-cleaned", + ]); + expect(harness.events).not.toContain("candidate-started"); + } else { + expect(harness.events).toContain("candidate-started"); + expect(harness.events.at(-1)).toBe("qualification-complete"); + } + }, + ); + it("maps only the pinned runc state root into the runtime session", () => { const environment = { ...process.env, @@ -2113,7 +2519,7 @@ describe("contained runtime command policy", () => { ); }); - it("fails closed when owned final cleanup fails", () => { + it("fails closed when owned final cleanup fails", async () => { const sessionRoot = resolve(root, ".rt/rt-validator-cleanup"); const installRoot = resolve( root, @@ -2280,7 +2686,18 @@ describe("contained runtime command policy", () => { throw new Error(`unexpected mock command: ${joined}`); }; - const result = executeContainedRun( + const qualificationCoordinator = { + begin: () => { + throw new Error("normal mode must not start qualification"); + }, + waitForDraft: () => { + throw new Error("normal mode must not wait for qualification"); + }, + complete: () => { + throw new Error("normal mode must not complete qualification"); + }, + }; + const result = await executeContainedRun( { args: command, binRoot: resolve(installRoot, "bin"), @@ -2289,6 +2706,7 @@ describe("contained runtime command policy", () => { cwd: root, environment: process.env, installRoot, + qualificationMode: null, sessionRoot, stdin: Buffer.alloc(0), }, @@ -2308,6 +2726,8 @@ describe("contained runtime command policy", () => { }), () => undefined, () => invocationId, + undefined, + qualificationCoordinator, ); expect(result.status).toBe(1); @@ -2347,7 +2767,7 @@ describe("contained runtime command policy", () => { ).toBe(false); }); - it("rejects an invalid generated container ID without deleting by name", () => { + it("rejects an invalid generated container ID without deleting by name", async () => { const sessionRoot = resolve(root, ".rt/rt-validator-id"); const installRoot = resolve( root, @@ -2358,7 +2778,7 @@ describe("contained runtime command policy", () => { const alias = `docker.io/library/counterlab-runtime-invocation:${invocationId}`; const calls: string[][] = []; let aliasPresent = false; - const result = executeContainedRun( + const result = await executeContainedRun( { args: command, binRoot: resolve(installRoot, "bin"), @@ -2367,6 +2787,7 @@ describe("contained runtime command policy", () => { cwd: root, environment: process.env, installRoot, + qualificationMode: null, sessionRoot, stdin: Buffer.alloc(0), }, From 75dfb6543c89b66aa55d013e27872a3c53862f15 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:33:02 +0530 Subject: [PATCH 053/143] fix(runner): consume qualified timeout receipts --- .../runner/src/counterlab_runner/docker.py | 79 +++++++----- .../src/counterlab_runner/timeout_proof.py | 91 +++++++++----- services/runner/tests/test_docker_runner.py | 114 ++++++++++++++++++ services/runner/tests/test_timeout_proof.py | 44 ++++++- 4 files changed, 270 insertions(+), 58 deletions(-) diff --git a/services/runner/src/counterlab_runner/docker.py b/services/runner/src/counterlab_runner/docker.py index 280e8c7d..cd6cc3b2 100644 --- a/services/runner/src/counterlab_runner/docker.py +++ b/services/runner/src/counterlab_runner/docker.py @@ -30,31 +30,35 @@ _SHA256 = re.compile(r"^[a-f0-9]{64}$") _CONTAINED_RUNTIME_SESSION_ID = re.compile(r"^rt-[a-z0-9][a-z0-9-]{7,13}$") _CONTAINED_RUNTIME_SUBPROCESS_ENVIRONMENT = {"PATH": "/usr/bin:/bin"} -_CONTROL_RECEIPT_KEYS = { - "schemaVersion", - "status", - "timeoutKind", - "runtimePolicySha256", - "invocationId", - "finalContainerId", - "commandSha256", - "rootlessReceiptFileSha256", - "rootlessReceiptPayloadSha256", - "timeoutObserved", - "candidateWallSeconds", - "elapsedMs", - "cleanupReserveMs", - "taskAbsent", - "containerAbsent", - "snapshotAbsent", - "invocationAliasAbsent", - "imageRootfsAbsent", - "persistedAuthorityVerified", - "readOnlyMountsUnchanged", - "imageRootfsUnchanged", - "resultReleased", - "receiptPayloadSha256", -} +_AGGREGATE_TIMEOUT_QUALIFICATION_MODE = "aggregate-timeout-proof-v1" +_CONTROL_RECEIPT_V2_KEYS = frozenset( + { + "schemaVersion", + "status", + "timeoutKind", + "runtimePolicySha256", + "invocationId", + "finalContainerId", + "commandSha256", + "rootlessReceiptFileSha256", + "rootlessReceiptPayloadSha256", + "timeoutObserved", + "candidateWallSeconds", + "elapsedMs", + "cleanupReserveMs", + "taskAbsent", + "containerAbsent", + "snapshotAbsent", + "invocationAliasAbsent", + "imageRootfsAbsent", + "persistedAuthorityVerified", + "readOnlyMountsUnchanged", + "imageRootfsUnchanged", + "resultReleased", + "receiptPayloadSha256", + } +) +_CONTROL_RECEIPT_V3_KEYS = _CONTROL_RECEIPT_V2_KEYS | {"qualificationMode"} _CONTROL_CLEANUP_FIELDS = ( "taskAbsent", "containerAbsent", @@ -273,7 +277,16 @@ def _validate_contained_runtime_control_receipt( value = json.loads(control_bytes) except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: raise DockerExecutionError("runtime_control_invalid") from exc - if not isinstance(value, dict) or set(value) != _CONTROL_RECEIPT_KEYS: + if not isinstance(value, dict): + raise DockerExecutionError("runtime_control_invalid") + schema_version = value.get("schemaVersion") + if schema_version == "2": + expected_keys = _CONTROL_RECEIPT_V2_KEYS + elif schema_version == "3": + expected_keys = _CONTROL_RECEIPT_V3_KEYS + else: + raise DockerExecutionError("runtime_control_invalid") + if set(value) != expected_keys: raise DockerExecutionError("runtime_control_invalid") payload = { key: entry for key, entry in value.items() if key != "receiptPayloadSha256" @@ -282,15 +295,21 @@ def _validate_contained_runtime_control_receipt( value.get("resultReleased") is False ) if ( - value.get("schemaVersion") != "2" + ( + schema_version == "3" + and value.get("qualificationMode") + != _AGGREGATE_TIMEOUT_QUALIFICATION_MODE + ) or value.get("status") not in {"TIMED_OUT_CLEAN", "TIMED_OUT_UNCLEAN"} or value.get("timeoutKind") != "WALL_CLOCK" or value.get("runtimePolicySha256") != CONTAINED_RUNTIME_POLICY_SHA256 or not isinstance(value.get("candidateWallSeconds"), int) + or isinstance(value.get("candidateWallSeconds"), bool) or value["candidateWallSeconds"] != expected_wall_seconds or value.get("cleanupReserveMs") != int(_CONTAINED_RUNTIME_POLICY["cleanupReserveMs"]) or not isinstance(value.get("elapsedMs"), int) + or isinstance(value.get("elapsedMs"), bool) or value["elapsedMs"] < 1 or value["elapsedMs"] > 600_000 or value.get("timeoutObserved") is not True @@ -314,7 +333,11 @@ def _validate_contained_runtime_control_receipt( / ".rt" / session_id / "run/rootless-specs" - / f"{value['finalContainerId']}.receipt.json", + / ( + f"{value['finalContainerId']}.qualified-receipt.json" + if schema_version == "3" + else f"{value['finalContainerId']}.receipt.json" + ), label="rootless_receipt", must_exist=True, ) diff --git a/services/runner/src/counterlab_runner/timeout_proof.py b/services/runner/src/counterlab_runner/timeout_proof.py index 014c47a1..a3d55aa8 100644 --- a/services/runner/src/counterlab_runner/timeout_proof.py +++ b/services/runner/src/counterlab_runner/timeout_proof.py @@ -26,6 +26,7 @@ _SOURCE_COMMIT = re.compile(r"^[a-f0-9]{40}$") _SESSION_ID = re.compile(r"^rt-[a-z0-9][a-z0-9-]{7,13}$") _QUALIFIED_AGGREGATE_LIMIT_MODE = "container-cgroup-and-process-rlimit" +_AGGREGATE_TIMEOUT_QUALIFICATION_MODE = "aggregate-timeout-proof-v1" _BUILD_KEYS = { "schemaVersion", "status", @@ -56,31 +57,34 @@ "buildkitConfigSha256", "builtAt", } -_CONTROL_KEYS = { - "schemaVersion", - "status", - "timeoutKind", - "runtimePolicySha256", - "invocationId", - "finalContainerId", - "commandSha256", - "rootlessReceiptFileSha256", - "rootlessReceiptPayloadSha256", - "timeoutObserved", - "candidateWallSeconds", - "elapsedMs", - "cleanupReserveMs", - "taskAbsent", - "containerAbsent", - "snapshotAbsent", - "invocationAliasAbsent", - "imageRootfsAbsent", - "persistedAuthorityVerified", - "readOnlyMountsUnchanged", - "imageRootfsUnchanged", - "resultReleased", - "receiptPayloadSha256", -} +_CONTROL_V2_KEYS = frozenset( + { + "schemaVersion", + "status", + "timeoutKind", + "runtimePolicySha256", + "invocationId", + "finalContainerId", + "commandSha256", + "rootlessReceiptFileSha256", + "rootlessReceiptPayloadSha256", + "timeoutObserved", + "candidateWallSeconds", + "elapsedMs", + "cleanupReserveMs", + "taskAbsent", + "containerAbsent", + "snapshotAbsent", + "invocationAliasAbsent", + "imageRootfsAbsent", + "persistedAuthorityVerified", + "readOnlyMountsUnchanged", + "imageRootfsUnchanged", + "resultReleased", + "receiptPayloadSha256", + } +) +_CONTROL_V3_KEYS = _CONTROL_V2_KEYS | {"qualificationMode"} _ATTESTATION_KEYS = { "schemaVersion", "status", @@ -302,11 +306,24 @@ def validate_control_receipt( expected_wall_seconds: int = 1, expected_runtime_policy_sha256: str = CONTAINED_RUNTIME_POLICY_SHA256, ) -> dict[str, Any]: - if not isinstance(value, dict) or set(value) != _CONTROL_KEYS: + if not isinstance(value, dict): + raise RuntimeError("timeout control receipt shape is invalid") + schema_version = value.get("schemaVersion") + if schema_version == "2": + expected_keys = _CONTROL_V2_KEYS + elif schema_version == "3": + expected_keys = _CONTROL_V3_KEYS + else: + raise RuntimeError("timeout control receipt shape is invalid") + if set(value) != expected_keys: raise RuntimeError("timeout control receipt shape is invalid") payload = {key: entry for key, entry in value.items() if key != "receiptPayloadSha256"} if ( - value.get("schemaVersion") != "2" + ( + schema_version == "3" + and value.get("qualificationMode") + != _AGGREGATE_TIMEOUT_QUALIFICATION_MODE + ) or value.get("status") != "TIMED_OUT_CLEAN" or value.get("timeoutKind") != "WALL_CLOCK" or value.get("runtimePolicySha256") @@ -318,10 +335,14 @@ def validate_control_receipt( or not _SHA256.fullmatch(str(value.get("rootlessReceiptPayloadSha256", ""))) or value.get("timeoutObserved") is not True or value.get("candidateWallSeconds") != expected_wall_seconds + or not isinstance(value.get("candidateWallSeconds"), int) + or isinstance(value.get("candidateWallSeconds"), bool) or not isinstance(value.get("elapsedMs"), int) + or isinstance(value.get("elapsedMs"), bool) or value["elapsedMs"] < 1 or value["elapsedMs"] > 600_000 or not isinstance(value.get("cleanupReserveMs"), int) + or isinstance(value.get("cleanupReserveMs"), bool) or value["cleanupReserveMs"] < 1 or any(value.get(field) is not True for field in _CLEANUP_FIELDS) or value.get("resultReleased") is not False @@ -333,6 +354,20 @@ def validate_control_receipt( return value +def _rootless_receipt_name(control: dict[str, Any]) -> str: + if control.get("schemaVersion") == "2": + suffix = "receipt.json" + elif ( + control.get("schemaVersion") == "3" + and control.get("qualificationMode") + == _AGGREGATE_TIMEOUT_QUALIFICATION_MODE + ): + suffix = "qualified-receipt.json" + else: + raise RuntimeError("timeout control receipt qualification is invalid") + return f"{control['finalContainerId']}.{suffix}" + + def validate_rootless_receipt( value: object, *, @@ -886,7 +921,7 @@ def run_timeout_cleanup_proof( or _sha256_file(rootless_path) != expected_rootless_sha256 ): raise RuntimeError("timeout proof exception receipt hashes changed") - if rootless_path.name != f"{control['finalContainerId']}.receipt.json": + if rootless_path.name != _rootless_receipt_name(control): raise RuntimeError("timeout control and rootless receipt IDs disagree") if _sha256_file(rootless_path) != control["rootlessReceiptFileSha256"]: raise RuntimeError("rootless receipt file hash changed") diff --git a/services/runner/tests/test_docker_runner.py b/services/runner/tests/test_docker_runner.py index 786630d4..8bd083c1 100644 --- a/services/runner/tests/test_docker_runner.py +++ b/services/runner/tests/test_docker_runner.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import json import os import stat @@ -38,6 +39,47 @@ def _artifacts(workspace: Path, limits: RunnerLimits) -> SimpleNamespace: ) +def _canonical_hash(value: object) -> str: + source = json.dumps( + value, + ensure_ascii=False, + allow_nan=False, + sort_keys=True, + separators=(",", ":"), + ) + return hashlib.sha256(source.encode()).hexdigest() + + +def _runtime_control_receipt(schema_version: str) -> dict[str, object]: + payload: dict[str, object] = { + "schemaVersion": schema_version, + "status": "TIMED_OUT_CLEAN", + "timeoutKind": "WALL_CLOCK", + "runtimePolicySha256": docker_module.CONTAINED_RUNTIME_POLICY_SHA256, + "invocationId": "1" * 64, + "finalContainerId": "2" * 64, + "commandSha256": "3" * 64, + "rootlessReceiptFileSha256": hashlib.sha256(b"{}\n").hexdigest(), + "rootlessReceiptPayloadSha256": "4" * 64, + "timeoutObserved": True, + "candidateWallSeconds": 1, + "elapsedMs": 1_001, + "cleanupReserveMs": 120_000, + "taskAbsent": True, + "containerAbsent": True, + "snapshotAbsent": True, + "invocationAliasAbsent": True, + "imageRootfsAbsent": True, + "persistedAuthorityVerified": True, + "readOnlyMountsUnchanged": True, + "imageRootfsUnchanged": True, + "resultReleased": False, + } + if schema_version == "3": + payload["qualificationMode"] = "aggregate-timeout-proof-v1" + return {**payload, "receiptPayloadSha256": _canonical_hash(payload)} + + def test_docker_command_applies_fixed_isolation_and_only_public_mounts( tmp_path: Path, ) -> None: @@ -179,6 +221,78 @@ def test_trusted_repository_root_rejects_relative_and_nested_callers( require_trusted_repository_root(nested) +@pytest.mark.parametrize( + ("schema_version", "rootless_suffix"), + [("2", ".receipt.json"), ("3", ".qualified-receipt.json")], +) +def test_runtime_control_receipt_binds_schema_to_exact_rootless_path( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + schema_version: str, + rootless_suffix: str, +) -> None: + session_id = "rt-entry123" + receipt = _runtime_control_receipt(schema_version) + rootless_root = tmp_path / ".rt" / session_id / "run/rootless-specs" + rootless_root.mkdir(parents=True) + rootless_path = rootless_root / f"{'2' * 64}{rootless_suffix}" + rootless_path.write_text("{}\n", encoding="utf-8") + control_path = tmp_path / f"control-v{schema_version}.json" + control_path.write_text(json.dumps(receipt), encoding="utf-8") + monkeypatch.setattr( + docker_module, + "_trusted_repository_root", + lambda: tmp_path.resolve(strict=True), + ) + + value, _, observed_rootless_path, observed_rootless_sha256 = ( + docker_module._validate_contained_runtime_control_receipt( + control_path, + session_id=session_id, + expected_wall_seconds=1, + ) + ) + + assert value == receipt + assert observed_rootless_path == rootless_path + assert observed_rootless_sha256 == receipt["rootlessReceiptFileSha256"] + + +@pytest.mark.parametrize( + "mutation", + [ + {"qualificationMode": "unknown"}, + {"candidateWallSeconds": True}, + ], +) +def test_runtime_control_receipt_rejects_invalid_v3_fields( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + mutation: dict[str, object], +) -> None: + receipt = {**_runtime_control_receipt("3"), **mutation} + payload = { + key: value + for key, value in receipt.items() + if key != "receiptPayloadSha256" + } + receipt["receiptPayloadSha256"] = _canonical_hash(payload) + control_path = tmp_path / "control-v3.json" + control_path.write_text(json.dumps(receipt), encoding="utf-8") + monkeypatch.setattr( + docker_module, + "_trusted_repository_root", + lambda: tmp_path.resolve(strict=True), + ) + + with pytest.raises(DockerExecutionError, match="runtime_control_invalid"): + docker_module._validate_contained_runtime_control_receipt( + control_path, + session_id="rt-entry123", + expected_wall_seconds=1, + ) + + def test_execution_snapshot_is_the_exact_validated_read_only_source( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/services/runner/tests/test_timeout_proof.py b/services/runner/tests/test_timeout_proof.py index 8e4f7c6f..807b4751 100644 --- a/services/runner/tests/test_timeout_proof.py +++ b/services/runner/tests/test_timeout_proof.py @@ -30,9 +30,9 @@ def _hash(value: object) -> str: return hashlib.sha256(_canonical(value).encode()).hexdigest() -def _control() -> dict[str, object]: +def _control(schema_version: str = "2") -> dict[str, object]: payload: dict[str, object] = { - "schemaVersion": "2", + "schemaVersion": schema_version, "status": "TIMED_OUT_CLEAN", "timeoutKind": "WALL_CLOCK", "runtimePolicySha256": CONTAINED_RUNTIME_POLICY_SHA256, @@ -55,6 +55,8 @@ def _control() -> dict[str, object]: "imageRootfsUnchanged": True, "resultReleased": False, } + if schema_version == "3": + payload["qualificationMode"] = "aggregate-timeout-proof-v1" return {**payload, "receiptPayloadSha256": _hash(payload)} @@ -222,6 +224,44 @@ def test_control_receipt_rejects_result_release_and_hash_mutation() -> None: validate_control_receipt({**control, "receiptPayloadSha256": "f" * 64}) +def test_control_receipt_accepts_only_exact_v2_and_qualified_v3_shapes() -> None: + assert validate_control_receipt(_control("2"))["schemaVersion"] == "2" + assert validate_control_receipt(_control("3"))["schemaVersion"] == "3" + + for mutated in ( + {**_control("2"), "qualificationMode": "aggregate-timeout-proof-v1"}, + { + key: value + for key, value in _control("3").items() + if key != "qualificationMode" + }, + {**_control("3"), "qualificationMode": "unknown"}, + {**_control("2"), "schemaVersion": []}, + {**_control("2"), "candidateWallSeconds": True}, + ): + payload = { + key: value + for key, value in mutated.items() + if key != "receiptPayloadSha256" + } + mutated["receiptPayloadSha256"] = _hash(payload) + with pytest.raises(RuntimeError, match="shape|not clean"): + validate_control_receipt(mutated) + + +def test_control_schema_selects_the_exact_rootless_receipt_name() -> None: + assert timeout_proof_module._rootless_receipt_name(_control("2")) == ( + f"{'2' * 64}.receipt.json" + ) + assert timeout_proof_module._rootless_receipt_name(_control("3")) == ( + f"{'2' * 64}.qualified-receipt.json" + ) + with pytest.raises(RuntimeError, match="qualification"): + timeout_proof_module._rootless_receipt_name( + {**_control("3"), "qualificationMode": "unknown"} + ) + + def test_rootless_rlimit_validator_matches_the_runtime_contract() -> None: runtime_limits = [ {"type": name, "soft": 1, "hard": 1} From f19387b1ab214571ea27022efdc0b9432655cc9f Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:33:52 +0530 Subject: [PATCH 054/143] test(learner): align live journey selectors --- apps/web/e2e/judged-flow.spec.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/web/e2e/judged-flow.spec.ts b/apps/web/e2e/judged-flow.spec.ts index 39fdc06b..2517ef29 100644 --- a/apps/web/e2e/judged-flow.spec.ts +++ b/apps/web/e2e/judged-flow.spec.ts @@ -2184,7 +2184,7 @@ test("configured reasoning cannot start without a qualified hosted runner", asyn page.getByText(/qualified hosted runner is needed/i), ).toBeVisible(); await expect( - page.getByRole("button", { name: /Continue with my notebook/i }), + page.getByLabel(/Attach a supported notebook/i), ).not.toBeVisible(); await expect(page.locator("body")).not.toContainText( /OPENAI|GPT-|https?:\/\//i, @@ -2423,10 +2423,8 @@ test("a configured hosted runner completes an untouched leakage notebook", async page.getByText(/Hosted notebook runner is ready/i), ).toBeVisible(); await page - .getByRole("button", { name: /Continue with my notebook/i }) - .click(); - - await page.locator('input[type="file"]').setInputFiles(leakageNotebookPath); + .getByLabel(/Attach a supported notebook/i) + .setInputFiles(leakageNotebookPath); await expect( page.getByText(/leakage-rows-pipeline.ipynb/i).first(), ).toBeVisible(); @@ -2665,10 +2663,8 @@ test("a configured hosted runner completes an untouched class-imbalance notebook page.getByText(/Hosted notebook runner is ready/i), ).toBeVisible(); await page - .getByRole("button", { name: /Continue with my notebook/i }) - .click(); - - await page.locator('input[type="file"]').setInputFiles(imbalanceNotebookPath); + .getByLabel(/Attach a supported notebook/i) + .setInputFiles(imbalanceNotebookPath); await expect( page.getByText(/fraud_class_imbalance.ipynb/i).first(), ).toBeVisible(); @@ -2732,10 +2728,10 @@ test("a configured hosted runner completes an untouched class-imbalance notebook "When positive cases are rare, compare against the majority baseline and choose class-specific metrics and a threshold that match deployment cost.", ); await page.getByRole("button", { name: /Try it on defects/i }).click(); - await page.getByLabel(/Lower threshold based on missed-defect cost/i).check(); - await page.getByLabel(/Missing a defect is the costly error/i).check(); - await page.getByLabel(/Confusion matrix shows misses/i).check(); - await page.getByLabel(/Prevalence changes precision/i).check(); + await page.getByLabel(/Reject the accuracy-only conclusion/i).check(); + await page.getByLabel(/Defect recall and PR-AUC/i).check(); + await page.getByLabel(/zero true positives/i).check(); + await page.getByLabel(/Defects are only 1%/i).check(); await page.getByRole("button", { name: /Check transfer/i }).click(); await expect(page.locator(".imbalance-transfer-pass .eyebrow")).toHaveText( "Transfer passed", From e4dc33eb44962b972c0157e5ecb4ed9760c16ee4 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:35:15 +0530 Subject: [PATCH 055/143] docs(progress): record aggregate runtime integration --- docs/PROGRESS.md | 83 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index c709151c..0904c7ec 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -12,17 +12,16 @@ pass is never presented as a browser or production pass. ### Current checkpoint - **Branch:** `feat/learner-ux-v6.1` -- **Latest committed checkpoint:** `f0c9f7bfcca474ed700b51975f03246df9f5158d`. +- **Latest committed checkpoint:** `f19387b1ab214571ea27022efdc0b9432655cc9f`. The worktree contains only the preserved, untracked `scripts/.fuse_hidden0000a9600000801f`. The source now has a self-hashed cgroup request, observer, FINALIZE/ABORT protocol, fixed negative controls, - fail-closed evidence validation, qualified-receipt derivation/store, and 40 - statically collected Playwright journeys. The production runtime still does - not start the observer, write the runtime-owned decision, bind the complete - invocation artifact chain, or select the qualified receipt for timeout - proof. No exact image, real cgroup sentinel, qualification, deployment, - rendered browser journey, learner observation, video, Devpost publication, - or submission receipt is claimed. + fail-closed evidence validation, qualified-receipt derivation/store, runtime + observer orchestration, serialized run/drain handling, exact v2/v3 Python + consumers, and 40 statically collected Playwright journeys. Source tests do + not constitute a real cgroup sentinel: no exact image, aggregate host proof, + qualification, deployment, rendered browser journey, learner observation, + video, Devpost publication, or submission receipt is claimed. - **Earlier product checkpoint:** `5259a495948a68024702ba5239bf8057d1785f08`. The owner partitioned the preserved integration delta into nine commits and fast-forwarded both @@ -3463,3 +3462,71 @@ worktree entry besides this progress update is the preserved FUSE tombstone 6. Keep learner evidence honest, finish and verify the public submission package, then push the feature branch, verify the remote tip, fast-forward `main`, and push/verify `main` only after every ship gate is factual. + +## Finish Commander aggregate integration checkpoint — 2026-07-21T03:04:15Z + +Deadline: `2026-07-22T00:00:00Z`; **20 hours 55 minutes 45 seconds remained at +this checkpoint**. The feature branch is at committed HEAD `f19387b`; the only +worktree entry besides this progress update is the preserved FUSE tombstone +`scripts/.fuse_hidden0000a9600000801f`. It remains unchanged and unstaged. + +### Independently committed source slices + +- `3cdf38b fix(runtime): verify qualification artifact chain` verifies the + manifest, readiness, draft, finalization, and exclusive evidence chain before + deriving a qualified receipt. +- `1ea2ed8 fix(runtime): permit honest pre-draft aborts` permits only the + explicit null-draft ABORT case while retaining a mandatory draft hash for + FINALIZE. +- `40db9bc feat(runtime): coordinate aggregate qualification` owns observer + startup, readiness/draft chronology, FINALIZE/ABORT emission, observer exit, + and qualified-receipt persistence. +- `eaf4a60 feat(runtime): wire aggregate qualification` connects the release-only + qualification mode to the runtime, binds control-receipt v3 to the qualified + receipt, serializes run and drain requests, and restores `ACTIVE` after a + failed drain. +- `75dfb65 fix(runner): consume qualified timeout receipts` preserves legacy v2 + replay while requiring the exact qualification mode and + `.qualified-receipt.json` path for v3 in both Python consumers. +- `f19387b test(learner): align live journey selectors` updates the two live + upload journeys and imbalance transfer choices to current accessible names. + +### Exact verification in these slices + +- Runtime, request, observer, coordinator, and qualified-receipt Vitest: + **7 files, 68/68 passed**. +- Complete runner Pytest with kernel and runner source roots: **82/82 passed**. + One preceding command omitted the kernel source root and failed collection; + it executed no product tests and was corrected before this reported pass. +- Repository, web client, generated Worker, and Worker TypeScript: **passed**. +- E2E strict TypeScript: **passed**. +- Playwright static collection under the explicit non-rendering design-review + authority: **40 tests in 3 files**. No browser launched and no rendered + qualification is claimed. +- Scoped Prettier, Python compilation, Node syntax, Git whitespace, and secret + scans: **passed**. +- Independent runtime integration review: **GO** after the initial review found + and the implementation repaired the async run/drain race and missing positive + qualification-path coverage. + +### Current remaining gates + +1. The source-level observer/runtime/consumer chain is complete, but no real + delegated-cgroup sentinel or exact-image qualification has run. The current + repository-only filesystem constitution prohibits the required `/sys` and + `/proc` reads, so this gate remains honestly blocked unless the checked-in + constitution changes. +2. The preserved FUSE tombstone prevents a clean physical checkout. It is not + deleted, hidden, staged, or modified. A separately contained clean checkout + or the natural exit of its proven owner remains required. +3. Prompt-first Landing and Judge hierarchy, mobile textual controls, and the + shared pre-seal forbidden-text regression remain the next source slice. +4. Final source freeze, one exact image, refreshed source/image-bound evidence, + qualification, the complete deterministic release gate, contained Wrangler + identity, and Cloudflare deployment remain **not run**. +5. This session still has no `CLOAK_CDP_ENDPOINT`; rendered desktop/mobile, + accessibility, console/network, reconnect, download, and Web Vitals + qualification remain **0 current journeys**. +6. Learner evidence remains **`NO_DATA`**. Devpost, video, public-repository + verification, feedback Session ID, logged-out link audit, publication, and + submission receipt remain externally unverified. From bd6e5c32077905894e722b4313c9e09957d16637 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:41:46 +0530 Subject: [PATCH 056/143] docs(release): map qualification dependencies --- .../19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 docs/audits/counterlab-first-prize/19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md diff --git a/docs/audits/counterlab-first-prize/19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md b/docs/audits/counterlab-first-prize/19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md new file mode 100644 index 00000000..5158aeec --- /dev/null +++ b/docs/audits/counterlab-first-prize/19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md @@ -0,0 +1,263 @@ +# Release dependency and review graph + +- Checkpoint: `2026-07-21T03:07:00Z` +- Branch: `feat/learner-ux-v6.1` +- Committed HEAD: `e4dc33eb44962b972c0157e5ecb4ed9760c16ee4` +- Deadline: `2026-07-22T00:00:00Z` +- Time remaining at this checkpoint: **20 hours 53 minutes** + +## Purpose and evidence boundary + +This document is the operational dependency graph for freezing, qualifying, +deploying, browser-testing, submitting, and merging one exact CounterLab +release. A downstream node cannot become green from source code or unit tests +alone when it requires image, cloud, browser, learner, or submission evidence. + +The worktree at this checkpoint has one preserved untracked entry: +`scripts/.fuse_hidden0000a9600000801f`. It was not read, changed, hidden, +staged, or deleted for this review. Every current build, qualification, +release-check, deployment, and production-smoke script that requires a clean +worktree will reject this state. + +No current deployment, rendered CloakBrowser journey, learner outcome, video, +Devpost submission, branch push, or merge is claimed here. + +## Release dependency graph + +```mermaid +flowchart TD + A["Source slices
atomic commits + focused tests
SOURCE COMPLETE for aggregate runtime chain"] + B["Review gate
independent review + lead diff review
PASS for aggregate runtime integration"] + C["Clean source freeze S
one committed physical tree
BLOCKED by preserved FUSE entry"] + D["Exact runner and adapter image
build receipt v4 bound to S
NOT RUN for current source"] + E["Real aggregate timeout sentinel
observer + controls + FINALIZE + cleanup
BLOCKED by /sys and /proc authority"] + F["Source/image evidence refresh
SBOM + VEX + engine evidence
NOT RUN"] + G["Evidence commit E
reviewed allowlisted evidence only
NOT CREATED"] + H["Qualification
receipt v6 + immutable registry image
NOT RUN; Wrangler identity unverified"] + I["Release check
receipt v5 + deterministic full gate
NOT RUN"] + J["Cloudflare deployment
maintenance + D1 + Container + Worker
NOT RUN"] + K["Public identity and smoke
deployment receipt v7 + /ready + health + live loops
NOT RUN"] + L["CloakBrowser qualification
40 journeys + manual accessibility/performance matrix
0 rendered journeys"] + M["Submission package
public app + repo + video + feedback + honest NO_DATA
EXTERNALLY UNVERIFIED"] + N["Git publication
push feature -> verify remote -> fast-forward main -> push main
NOT RUN"] + + A --> B --> C --> D --> E --> F --> G --> H --> I --> J --> K --> L --> M --> N + + classDef pass fill:#d9f3ee,stroke:#176b5b,color:#123b34; + classDef blocked fill:#fff0d0,stroke:#9a6500,color:#4c3500; + classDef pending fill:#eef2f7,stroke:#596579,color:#252b36; + classDef external fill:#f7e8ef,stroke:#95506d,color:#482636; + class A,B pass; + class C,E blocked; + class D,F,G,H,I,J,K,L pending; + class M,N external; +``` + +## Gate ledger + +| Gate | Required inputs | Required evidence | Current status | Failure rule | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| Source slices | Reviewed implementation and tests | Atomic commits, focused tests, type checks, formatting, secret scan, whitespace check | Aggregate observer/runtime/consumer chain is source-complete | Repair in a new independent commit; do not weaken tests | +| Review | Complete source slice and focused evidence | Independent review plus lead inspection of complete diff | Aggregate runtime review is `GO`; its initial async run/drain race and missing positive path were repaired | Review findings remain open until implementation and regression tests pass | +| Clean source `S` | Final intended source and no tracked or untracked drift | `git status` empty and exact source commit recorded | **Blocked** by preserved FUSE entry | Do not hide, delete, or sweep the entry; use its proven owner's natural exit or a separately contained clean checkout | +| Exact image | Clean `S`, source-matching contained runtime, pinned BuildKit | Build receipt v4, source/tree labels, exact runner and adapter digests | **Not run** for current source; cached receipts belong to older commits | Never reuse or retag an earlier image as current | +| Real sentinel | Exact image, source-matching runtime, release-only qualification mode | Genuine memory OOM, PID denial, CPU throttling, exact membership, FINALIZE, eight cleanup facts, cgroup absence, exclusive evidence artifact | **Blocked** by the checked-in repository-only filesystem rule, which does not permit required `/sys/fs/cgroup` and owned `/proc//stat` reads | Preserve `NO-GO`; never synthesize or inject aggregate evidence | +| Evidence refresh | Exact image and successful real sentinel while `HEAD == S` | Fresh normalized SBOM, VEX and negative control, scientific-engine bindings, runtime and isolation reports | **Not run** | Any image/source mismatch or stale binding fails closed | +| Evidence commit `E` | Reviewed allowlisted evidence delta | One clean commit containing only regenerated evidence | **Not created** | Do not include unrelated source or cache artifacts | +| Qualification | Clean `HEAD == E`, build receipt, timeout receipt, source-matching runtime, authenticated Cloudflare account | Qualified runner receipt v6 and exact registry manifest/config digest | **Not run**; contained Wrangler identity is unverified | Do not promote when account, source, image, timeout, runtime, or evidence differs | +| Release check | Clean `E`, qualified receipt, exact local images and runtime | Release-check receipt v5 after formatting, tests, mutations, held-out, sandbox, scientific, build, reproduction, patch replay, and secret gates | **Not run** | Any failure blocks deployment | +| Cloudflare deployment | Qualified receipt, release-check receipt, registry image, required secrets, recovery target, working CloakBrowser endpoint | Exact maintenance/final Worker versions, D1 migration log, exact Container digest, deployment receipt v7 | **Not run** | Stop before mutation when preflight is red; after mutation, automated Worker recovery does not reverse D1 or Container state | +| Public identity and smoke | Active exact Worker/Container tuple | `/ready`, `/api/health?readiness=probe`, Worker 100% traffic, Container identity, public asset scan, Sample, Replay, leakage and imbalance live evidence, Capsule/replay evidence | **Not run** | Source, image, Worker, client, receipt, or capability mismatch is a release failure | +| CloakBrowser | Exact public tuple and injected `CLOAK_CDP_ENDPOINT` | All 40 current tests plus responsive, keyboard, screen-reader, 200% zoom, reduced-motion, console/network, downloads, reconnect, and Web Vitals evidence | **40 statically collected; 0 rendered** | Stock Chromium is design-review evidence only and cannot close this gate | +| Devpost and impact | Qualified public tuple, public repo/video/feedback links, factual copy | Logged-out link audit, public video from exact tuple, Devpost receipt, real consented learner aggregates or explicit `NO_DATA` | **Externally unverified; learner evidence remains `NO_DATA`** | Never infer a submission or learner result | +| Push and merge | Every ship gate factual and final source reviewed | Verified feature remote tip, fast-forward-only `main`, verified main remote tip | **Not run** | Do not push or merge a `NO-GO` release | + +## Code-to-evidence graph + +```mermaid +flowchart LR + subgraph Source["Source authority"] + R1["scripts/contained-cgroup-*
observer, protocol, controls"] + R2["scripts/contained-runtime-*
request, run, server, supervisor"] + R3["services/runner/...
docker.py + timeout_proof.py"] + U1["apps/web/src + worker
learner and authority UI"] + end + + subgraph Verification["Pre-release verification"] + T1["Vitest + TypeScript"] + T2["Pytest + cross-runtime parity"] + T3["Playwright source + static collection"] + Q1["Independent review"] + end + + subgraph Release["Release evidence"] + B1["build-source-bound-runner.sh"] + S1["real timeout/cgroup sentinel"] + E1["refresh-source-bound-scientific-evidence.sh"] + Q2["qualify-runner-release.ts"] + R4["release-check.sh"] + end + + subgraph Production["Production evidence"] + C1["deploy-qualified.sh"] + C2["deployment receipt + /ready + health"] + C3["production-smoke.sh"] + P1["CloakBrowser full matrix"] + end + + R1 --> T1 + R2 --> T1 + R3 --> T2 + U1 --> T1 + U1 --> T3 + T1 --> Q1 + T2 --> Q1 + T3 --> Q1 + Q1 --> B1 --> S1 --> E1 --> Q2 --> R4 --> C1 --> C2 --> C3 --> P1 +``` + +Source tests demonstrate behavior; they do not manufacture the observation at +the real-sentinel, Cloudflare, public-identity, or browser nodes. + +## Commit and review chain at this checkpoint + +The current aggregate release chain is split into independent commits: + +- `0077f1a` — fixed cgroup controls; +- `fbc5ba9` — observer identity bindings; +- `37684dd` — observer protocol; +- `771d8bf` — aggregate observer; +- `317ba44` — live-session qualified-receipt binding; +- `c2c1817` — finalization signal; +- `cf90647` — fail-closed cgroup qualification; +- `bb1cc0b` — finalization-bound aggregate evidence; +- `3cdf38b` — exclusive qualification artifact-chain verification; +- `1ea2ed8` — honest pre-draft ABORT handling; +- `40db9bc` — observer/runtime qualification coordinator; +- `eaf4a60` — release-only runtime integration and control-receipt v3; +- `75dfb65` — Python v3 qualified-receipt consumption with v2 replay + compatibility; +- `f19387b` — current live-browser selector alignment; +- `e4dc33e` — factual progress checkpoint. + +Reported verification for the integrated aggregate slice is: + +- runtime/request/observer/coordinator/receipt Vitest: **7 files, 68/68 + passed**; +- complete runner Pytest with both required source roots: **82/82 passed**; +- repository, web client, generated Worker, and Worker TypeScript: **passed**; +- E2E strict TypeScript: **passed**; +- Playwright static collection: **40 tests in 3 files**, with no browser + launched; +- scoped Prettier, Python compilation, Node syntax, Git whitespace, and secret + scans: **passed**; +- independent runtime integration review: **GO** after the implementation + repaired its initial async run/drain race and missing positive + qualification-path coverage. + +These results are source evidence only. The real sentinel and every downstream +release node remain open. + +## File ownership and reviewer graph + +```mermaid +flowchart TD + Lead["Lead integrator
sole committer and release owner"] + Runtime["Runtime/release implementer
contained runtime + timeout consumer"] + UI["Learner UI implementer
isolated learner components/tests"] + Boundary["Boundary implementer
bounded Boundary files/tests"] + Browser["Browser QA reviewer
Playwright evidence only"] + Access["Accessibility reviewer
semantic/responsive findings"] + Docs["Documentation reviewer
copy and evidence drafts"] + Independent["Independent runtime reviewer
race, binding, failure-path audit"] + + Runtime --> Independent --> Lead + UI --> Access --> Lead + UI --> Browser --> Lead + Boundary --> Access + Boundary --> Browser + Docs --> Lead + Lead --> Commit["Selective staged diff
atomic local commit"] + Commit --> Gate["Relevant tests and delivery gate"] +``` + +| Area | Writable owner | Required reviewer/evidence | Integration owner | +| ----------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------- | ----------------- | +| `apps/web/src/App.tsx`, global routes/state/CSS | Lead only | React/Vitest, accessibility and browser review | Lead | +| `apps/web/src/components/learner/`, `features/learner/` | Assigned learner implementer when isolated | Focused component tests, accessibility review, lead diff review | Lead | +| Boundary Hunt files | Assigned Boundary implementer | Boundary invariants, keyboard/table/non-colour review | Lead | +| `scripts/contained-*`, release receipts and release scripts | Lead or one explicitly assigned runtime owner at a time | Runtime Vitest, Python parity, independent review, lead diff review | Lead | +| `services/runner/.../docker.py` and `timeout_proof.py` | One runtime owner at a time | Runner Pytest, cross-language receipt parity, failure-path review | Lead | +| Worker/API/contracts/session transitions | Lead only | TypeScript, Worker tests, authority-boundary review | Lead | +| Scientific/SBOM tracked evidence | Lead during exact refresh | Hash/schema verification and evidence-only diff review | Lead | +| Playwright specifications and evidence | Browser QA owner on assigned files | CloakBrowser execution, console/network and accessibility evidence | Lead | +| Root release/submission documentation | Lead integrates reviewer drafts | Claim-to-evidence review | Lead | +| Git commits, push, deployment and merge | Lead only | Clean staged diff and final delivery gate | Lead | + +No two writers may own the same shared file or receipt registry concurrently. +Only the lead stages, commits, deploys, pushes, or fast-forwards `main`. + +## Disciplines applied directly + +The following disciplines are applied directly from the checked-in repository +constitution, code, tests, and release scripts. This document does **not** claim +that home-directory skill files were read. + +| Discipline | Direct application in this graph | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Repository safety | All repository work and generated state stay inside the verified root; the FUSE entry is preserved; no destructive cleanup, stash, reset, or outside-root filesystem operation is accepted. | +| Code review | Each logical slice has focused characterization/regression tests, an isolated diff, independent review where assigned, and lead integration before an atomic commit. | +| Accessibility | Browser closure requires semantic names, keyboard completion, visible focus, exact-value tables, non-colour meaning, reduced motion, zoom, responsive viewports, touch targets, and screen-reader inspection. | +| React testing | Component and App behavior is checked with Vitest and strict TypeScript; Playwright covers composed learner journeys, authority labels, refresh, navigation, downloads, and no-result-before-Prediction behavior. | +| Playwright and CloakBrowser | Static collection is not browser evidence; release qualification requires the injected CloakBrowser CDP authority, accessibility snapshots, screenshots where visual judgment matters, and console/network review. Stock Chromium cannot close the gate. | +| Cloudflare and Wrangler | The repository-pinned Wrangler, contained account identity, exact registry digest, maintenance freeze, remote D1 migration, Container rollout, final Worker, recovery target, and public identity receipt form one ordered gate. | +| Delivery gate | Clean `S` -> exact image -> real sentinel -> evidence `E` -> qualification -> release check -> deployment -> public smoke -> full browser matrix -> submission -> push/fast-forward merge. No later node may compensate for an earlier red node. | + +## Current blockers and shortest honest path + +1. The source-level aggregate runtime chain is implemented and reviewed, but a + real delegated-cgroup sentinel has not run. +2. The current constitution prohibits the sentinel's necessary `/sys/fs/cgroup` + and owned `/proc//stat` reads. The release must remain `NO-GO` unless the + checked-in authority changes or another constitution-compatible evidence + path is established. +3. The preserved FUSE entry prevents the required clean physical source tree. +4. No current-source image, scientific-evidence refresh, evidence commit, + qualified receipt, or release-check receipt exists. +5. Contained Wrangler authentication and the intended Cloudflare account have + not been verified for this release. +6. `CLOAK_CDP_ENDPOINT` is absent. `deploy-qualified.sh` performs remote Worker, + D1, and Container mutations before invoking the smoke script, while + `production-smoke.sh` requires CloakBrowser. CloakBrowser must therefore be + preflighted before deployment starts. +7. Learner evidence remains `NO_DATA`; public repository, video, feedback ID, + Devpost publication, and submission receipt remain externally unverified. + +The shortest honest path is to resolve the real-sentinel authority and clean +source blockers, freeze `S`, build once, produce the genuine sentinel, refresh +and commit evidence as `E`, qualify, release-check, preflight CloakBrowser and +Wrangler, deploy once, verify the exact public tuple, execute the full browser +matrix, finalize the factual submission, then push and fast-forward merge. + +## Final release review questions + +- Does every result-bearing claim name its exact signed or integrity-hashed + source? +- Are Sample, Replay, and Live labels persistent and non-interchangeable? +- Is `S` the exact runner source and an ancestor of evidence commit `E`? +- Do the image, aggregate evidence, qualification, release-check, deployment, + Worker, client, and public-health hashes agree? +- Did a real observer produce the aggregate evidence, with no caller-supplied + substitute? +- Did all deterministic gates pass without weakening or skipping a test? +- Did the exact public tuple complete both untouched supported live flows? +- Did CloakBrowser execute the full responsive, accessibility, failure, + reconnect, download, console/network, and performance matrix? +- Is learner impact either backed by consented rows or still explicitly + `NO_DATA`? +- Do the app, repository, video, screenshots, feedback ID, Devpost copy, and + submission receipt describe the same exact release? +- Was the feature branch pushed and verified before a fast-forward-only merge + to `main`? + +Any `no`, unknown, or unavailable answer keeps the corresponding gate open. From e80b390a7007a6d360afe6c88bae26c2b8fbd4f5 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:42:33 +0530 Subject: [PATCH 057/143] fix(judge): tighten first-fold belief break --- .../features/judge/JudgeModeView.module.css | 52 ++++++++++++------- .../src/features/judge/JudgeModeView.test.tsx | 50 +++++++++++++++++- apps/web/src/features/judge/JudgeModeView.tsx | 37 ++++++------- 3 files changed, 100 insertions(+), 39 deletions(-) diff --git a/apps/web/src/features/judge/JudgeModeView.module.css b/apps/web/src/features/judge/JudgeModeView.module.css index c9da4676..4e6d24b4 100644 --- a/apps/web/src/features/judge/JudgeModeView.module.css +++ b/apps/web/src/features/judge/JudgeModeView.module.css @@ -118,10 +118,10 @@ grid-template-areas: "copy proof" "after proof"; - gap: clamp(42px, 7vw, 96px); + gap: clamp(32px, 5vw, 68px); align-items: center; - min-height: 680px; - padding-block: 76px; + min-height: 620px; + padding-block: 52px; } .heroCopy { @@ -157,25 +157,25 @@ max-width: 720px; margin: 0; font-family: var(--display); - font-size: clamp(58px, 7.5vw, 106px); + font-size: clamp(52px, 6vw, 82px); font-weight: 520; - letter-spacing: -0.055em; - line-height: 0.88; + letter-spacing: -0.05em; + line-height: 0.94; } .lede { max-width: 650px; - margin: 30px 0 0; + margin: 22px 0 0; color: var(--ink); - font-size: clamp(17px, 1.6vw, 21px); - line-height: 1.55; + font-size: clamp(17px, 1.4vw, 19px); + line-height: 1.5; } .heroActions { display: flex; flex-wrap: wrap; gap: 12px; - margin-top: 32px; + margin-top: 24px; } .heroActions button, @@ -217,7 +217,7 @@ .scopeLine { max-width: 600px; - margin-top: 24px; + margin-top: 16px; color: var(--judge-muted); font-size: 13px; line-height: 1.5; @@ -225,10 +225,10 @@ .twentySecondProof { position: relative; - padding: clamp(26px, 4vw, 46px); + padding: clamp(22px, 2.6vw, 32px); color: var(--judge-panel-ink); background: var(--judge-panel); - box-shadow: 20px 20px 0 rgba(46, 196, 160, 0.6); + box-shadow: 14px 14px 0 rgba(46, 196, 160, 0.6); } .twentySecondProof::before { @@ -248,7 +248,7 @@ display: flex; justify-content: space-between; gap: 20px; - padding-bottom: 18px; + padding-bottom: 12px; border-bottom: 1px solid rgb(255 255 255 / 18%); font-size: 12px; letter-spacing: 0.08em; @@ -256,13 +256,20 @@ } .twentySecondProof blockquote { - margin: 28px 0 18px; + margin: 18px 0 12px; color: var(--gold-bright); font-family: var(--display); font-size: clamp(21px, 2.4vw, 31px); line-height: 1.18; } +.previewMeta { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; +} + .previewAuthority { margin: 0 0 14px; color: var(--judge-panel-muted); @@ -274,7 +281,7 @@ display: inline-flex; min-height: 44px; align-items: center; - margin-top: 8px; + padding-inline: 4px; color: var(--judge-panel-ink); font-size: 13px; font-weight: 720; @@ -786,7 +793,8 @@ } .hero h1 { - font-size: clamp(34px, 10vw, 46px); + font-size: clamp(32px, 9.5vw, 40px); + line-height: 0.98; } .hero .kicker, @@ -804,12 +812,16 @@ } .twentySecondProof { - padding: 18px; - box-shadow: 10px 10px 0 rgba(46, 196, 160, 0.6); + padding: 14px; + box-shadow: 8px 8px 0 rgba(46, 196, 160, 0.6); } .twentySecondProof blockquote { - margin-block: 20px 12px; + margin-block: 14px 10px; + } + + .previewMeta { + gap: 8px; } .authoritySection, diff --git a/apps/web/src/features/judge/JudgeModeView.test.tsx b/apps/web/src/features/judge/JudgeModeView.test.tsx index 6296fb4a..11a8a6f4 100644 --- a/apps/web/src/features/judge/JudgeModeView.test.tsx +++ b/apps/web/src/features/judge/JudgeModeView.test.tsx @@ -1,7 +1,7 @@ import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; -import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -54,6 +54,54 @@ describe("JudgeModeView", () => { vi.unstubAllGlobals(); }); + it("keeps the tightened first fold learner-facing with proof one action away", async () => { + render( + , + ); + + expect( + screen.getByText( + "Seal a Prediction. Change one condition. Fixed evidence—not AI prose—releases one bounded result.", + ), + ).toBeVisible(); + expect( + screen.queryByText(/Question → Prediction → Test/u), + ).not.toBeInTheDocument(); + + const proof = screen.getByRole("complementary", { + name: /ten second fixed sample preview/i, + }); + expect(proof).toHaveTextContent( + /completed fixed sample.*not a live result/i, + ); + expect(within(proof).getByText("Integrity checked")).toBeVisible(); + expect(proof).toHaveTextContent( + /This score proves the model works for customers it has never seen/i, + ); + expect(proof).not.toHaveTextContent(/\b[a-f0-9]{64}\b/iu); + + const proofLink = within(proof).getByRole("link", { + name: /inspect verified sample proof/i, + }); + const mechanism = within(proof).getByRole("region", { + name: /verified sample belief-break mechanism/i, + }); + expect( + proofLink.compareDocumentPosition(mechanism) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).not.toBe(0); + expect(await within(mechanism).findByText("98.5%")).toBeVisible(); + expect(within(mechanism).getByText("59.4%")).toBeVisible(); + expect(within(mechanism).getByText("Boundary consequence")).toBeVisible(); + expect(within(mechanism).getByText("Learner benefit")).toBeVisible(); + }); + it("distinguishes sample, live, and legacy replay authority", async () => { render(

- Seal a Prediction, change one condition, and let fixed evidence—not - AI prose—release the bounded result. CounterLab turns the - learner's notebook claim into a checkable Question → Prediction - → Test → Boundary → Apply → Repair record. + Seal a Prediction. Change one condition. Fixed evidence—not AI + prose—releases one bounded result.

@@ -94,25 +92,28 @@ export function JudgeModeView({ >
Completed fixed sample · not a live result - Hashes checked before values + Integrity checked
“This score proves the model works for customers it has never seen.”
-

- Approved fixed sample framing. No GPT-5.6, Codex, or runner call - occurs in this preview. -

+
+

+ Approved fixed sample framing. No GPT-5.6, Codex, or runner call + occurs here. +

+ + setSampleEvidenceInspectionRequest((request) => request + 1) + } + > + Inspect proof + +
- - setSampleEvidenceInspectionRequest((request) => request + 1) - } - > - Inspect verified sample proof -
From 830d61b3f6d6ea765a11b8cbce96682b487ab7f0 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:47:12 +0530 Subject: [PATCH 058/143] fix(learner): restore prompt-first landing hierarchy --- apps/web/src/App.test.tsx | 37 +++--- apps/web/src/App.tsx | 30 ++--- .../learner/AccessibilityStyles.test.ts | 18 +-- .../LockedBeliefBreakPreview.module.css | 109 ++++++++++++++++++ .../learner/LockedBeliefBreakPreview.test.tsx | 33 ++++++ .../learner/LockedBeliefBreakPreview.tsx | 39 ++++++- .../learner/QuestionComposer.module.css | 44 +++---- apps/web/src/styles.css | 99 +++------------- 8 files changed, 254 insertions(+), 155 deletions(-) create mode 100644 apps/web/src/components/learner/LockedBeliefBreakPreview.module.css create mode 100644 apps/web/src/components/learner/LockedBeliefBreakPreview.test.tsx diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index 467fde5c..d0ca015a 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -21,6 +21,9 @@ import { activeRunnerRegistryKey } from "./features/learner/activeRunnerRegistry import { requireBundledSampleResult, sampleArtifact } from "./sample"; const sampleResult = requireBundledSampleResult(); +const PRE_PREDICTION_RESULT_LANGUAGE = + /59\.4%|\bdeceptive\b|\bfairer test\b|\bverified result\b|\bevidence verdict\b|\bsupported hypothesis\b|\bthe fix\b|\bremove customer(?:_| )id\b|\bkeep each customer's rows together\b|\bproves?\b/i; +const LANDING_FIXED_SAMPLE_VALUES = /98\.5%|59\.4%/i; const verifiedImbalanceResult = VerifiedResultSetSchema.parse( JSON.parse(imbalanceResultText), @@ -1073,12 +1076,7 @@ describe("CounterLab judged flow", () => { ).toBeInTheDocument(); expect( screen.getByText( - /for learners testing whether a notebook result means what they think it means/i, - ), - ).toBeInTheDocument(); - expect( - screen.getByText( - /evidence-first learning.*seal a Prediction.*fixed evidence.*not AI prose/i, + /ask a question or attach a supported notebook.*reads the evidence and never runs its cells/i, ), ).toBeInTheDocument(); expect( @@ -1110,7 +1108,7 @@ describe("CounterLab judged flow", () => { screen.getByText(/fixed kernels calculate the result/i), ).toBeInTheDocument(); expect( - screen.getByText(/read the evidence and never run the cells/i), + screen.getByText(/reads the evidence and never runs its cells/i), ).toBeInTheDocument(); expect( screen.getByRole("navigation", { @@ -1126,21 +1124,16 @@ describe("CounterLab judged flow", () => { expect( screen.getByRole("button", { name: /test this claim/i }), ).toBeDisabled(); - expect( - screen.getByText(/result hidden until your Prediction is sealed/i), - ).toBeInTheDocument(); - expect( - screen.getByText( - /name the claim.*lock your expectation.*change one thing/i, - ), - ).toBeInTheDocument(); - expect( - screen.getByLabelText(/fair-test mechanism with result locked/i), - ).toHaveTextContent(/familiar-row test.*unseen-entity test/i); - expect(document.body).not.toHaveTextContent(/98\.5%|59\.4%/i); - expect( - screen.getByLabelText(/fair-test mechanism with result locked/i), - ).toHaveAttribute("data-result-visibility", "locked"); + const lockedPreview = screen.getByLabelText( + /fair-test preview with result locked/i, + ); + expect(lockedPreview).toHaveTextContent( + /familiar rows.*change who counts as new.*unseen customers/i, + ); + expect(lockedPreview).toHaveAttribute("data-presentation", "strip"); + expect(lockedPreview).toHaveAttribute("data-result-visibility", "locked"); + expect(document.body).not.toHaveTextContent(LANDING_FIXED_SAMPLE_VALUES); + expect(document.body).not.toHaveTextContent(PRE_PREDICTION_RESULT_LANGUAGE); const proofSummary = screen.getByText("Evidence & proof"); await user.click(screen.getByRole("link", { name: /how proof works/i })); expect(proofSummary.closest("details")).toHaveAttribute("open"); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 30646c11..cc4988ad 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1098,14 +1098,9 @@ function Landing({

What result are you trying to understand?

-

- For learners testing whether a notebook result means what they - think it means. -

-

- Evidence-first learning: seal a Prediction, change one - condition, and let fixed evidence—not AI prose—release the - bounded result. +

+ Ask a question or attach a supported notebook. CounterLab reads + the evidence and never runs its cells.

@@ -1130,19 +1125,9 @@ function Landing({

- No account needed Supported - Python/scikit-learn notebooks - We read the evidence and never run the cells + No account needed Sample, live, + and replay stay clearly labelled

{ expect(theaterStyles).toContain("animation: none"); }); - it("keeps the composer dominant and collapses the landing before fixed tracks overflow", () => { - expect(globalStyles).toContain( + it("keeps the composer dominant in one centered responsive column", () => { + expect(globalStyles).not.toContain( "grid-template-columns: minmax(520px, 1.08fr) minmax(480px, 0.92fr)", ); expect(globalStyles).toContain("@media (max-width: 1240px)"); expect(globalStyles).toContain( - ".question-first-layout {\n width: 100%;\n max-width: 1220px;\n min-width: 0;", + ".question-first-layout {\n width: 100%;\n max-width: 820px;\n min-width: 0;", ); expect(globalStyles).toContain( - ".question-first-layout {\n width: 100%;\n max-width: 780px;\n min-width: 0;", + ".question-first-layout {\n width: 100%;\n max-width: 820px;\n min-width: 0;", ); expect(questionComposerStyles).toContain( ".composer {\n width: 100%;\n max-width: 780px;\n min-width: 0;", @@ -128,11 +128,15 @@ describe("learner-facing responsive style safeguards", () => { expect(questionComposerStyles).toContain( ".promptGroup {\n width: 100%;\n max-width: 640px;\n min-width: 0;", ); - expect(globalStyles).toContain( - ".landing-belief-break blockquote {\n display: none;", + expect(globalStyles).toContain("grid-template-columns: minmax(0, 1fr)"); + expect(questionComposerStyles).toContain( + ".form {\n min-height: 122px;\n padding-bottom: 58px;", ); expect(questionComposerStyles).toContain( - "@media (max-width: 560px) {\n .promptGroup {\n display: none;", + ".submitText {\n position: static;", + ); + expect(questionComposerStyles).not.toContain( + ".promptGroup {\n display: none;", ); }); }); diff --git a/apps/web/src/components/learner/LockedBeliefBreakPreview.module.css b/apps/web/src/components/learner/LockedBeliefBreakPreview.module.css new file mode 100644 index 00000000..b8877ea1 --- /dev/null +++ b/apps/web/src/components/learner/LockedBeliefBreakPreview.module.css @@ -0,0 +1,109 @@ +.strip { + width: 100%; + min-width: 0; + padding: 14px 16px; + display: grid; + gap: 11px; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 14px; + color: var(--ink); + background: color-mix(in srgb, var(--night-raised) 84%, transparent); +} + +.status { + margin: 0; + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + color: var(--ink-soft); + font-size: 13px; + line-height: 1.4; +} + +.status strong { + color: var(--aqua-bright); + font-size: 13px; + font-weight: 720; +} + +.sequence { + margin: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + gap: 10px; + font-size: 15px; + line-height: 1.4; + text-align: center; +} + +.sequence > span:first-child, +.sequence > span:last-child { + min-width: 0; + padding: 9px 10px; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--paper); +} + +.change { + display: inline-grid; + justify-items: center; + gap: 2px; + color: var(--ink-soft); + font-size: 13px; + white-space: nowrap; +} + +.change > span[aria-hidden="true"] { + color: var(--aqua-bright); + font-size: 18px; + line-height: 1; +} + +.heldFixed { + margin: 0; + color: var(--ink-soft); + font-size: 13px; + line-height: 1.45; + text-align: center; +} + +.heldFixed > span { + margin-right: 5px; + color: var(--aqua-bright); +} + +@media (max-width: 560px) { + .strip { + padding: 12px; + gap: 9px; + } + + .status { + align-items: flex-start; + flex-direction: column; + gap: 2px; + } + + .sequence { + grid-template-columns: 1fr; + gap: 5px; + text-align: left; + } + + .change { + grid-template-columns: auto 1fr; + justify-items: start; + padding-inline: 9px; + } +} + +@media (prefers-reduced-motion: reduce) { + .strip, + .strip * { + transition: none; + } +} diff --git a/apps/web/src/components/learner/LockedBeliefBreakPreview.test.tsx b/apps/web/src/components/learner/LockedBeliefBreakPreview.test.tsx new file mode 100644 index 00000000..c318a4d4 --- /dev/null +++ b/apps/web/src/components/learner/LockedBeliefBreakPreview.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { LockedBeliefBreakPreview } from "./LockedBeliefBreakPreview"; + +describe("LockedBeliefBreakPreview", () => { + it("offers a compact landing strip without revealing the bundled result", () => { + render(); + + const preview = screen.getByLabelText( + /fair-test preview with result locked/i, + ); + expect(preview).toHaveAttribute("data-presentation", "strip"); + expect(preview).toHaveAttribute("data-result-visibility", "locked"); + expect(preview).toHaveTextContent( + /familiar rows.*change who counts as new.*unseen customers/i, + ); + expect(preview).toHaveTextContent( + /same model, features, preprocessing, sample size, metric, and seed/i, + ); + expect(preview).not.toHaveTextContent(/98\.5%|59\.4%|supports|rejected/i); + }); + + it("retains the complete locked mechanism for non-landing surfaces", () => { + render(); + + const preview = screen.getByLabelText( + /belief-break fair-test mechanism with result locked/i, + ); + expect(preview).toHaveAttribute("data-presentation", "compact"); + expect(preview).toHaveTextContent(/familiar-row test.*unseen-entity test/i); + }); +}); diff --git a/apps/web/src/components/learner/LockedBeliefBreakPreview.tsx b/apps/web/src/components/learner/LockedBeliefBreakPreview.tsx index 67366e69..ee9d1923 100644 --- a/apps/web/src/components/learner/LockedBeliefBreakPreview.tsx +++ b/apps/web/src/components/learner/LockedBeliefBreakPreview.tsx @@ -1,4 +1,5 @@ import styles from "./VerifiedBeliefBreakTheater.module.css"; +import previewStyles from "./LockedBeliefBreakPreview.module.css"; export function LockedBeliefBreakEvidence() { return ( @@ -69,7 +70,43 @@ export function LockedBeliefBreakEvidence() { ); } -export function LockedBeliefBreakPreview() { +export function LockedBeliefBreakPreview({ + density = "full", +}: { + density?: "full" | "strip"; +}) { + if (density === "strip") { + return ( +
+

+ Fair-test preview + Result locked until Prediction +

+

+ Familiar rows + + + change who counts as new + + Unseen customers +

+

+ + Same model, features, preprocessing, sample size, metric, and seed. +

+
+ ); + } + return (
h1:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 6px; @@ -6678,38 +6670,9 @@ body { .landing-belief-break { grid-area: belief-break; - align-self: center; + width: min(780px, 100%); + justify-self: center; min-width: 0; - padding: clamp(20px, 2.5vw, 30px); - overflow: hidden; - border: 1px solid var(--line); - border-radius: 18px; - background: var(--night-raised); - box-shadow: 0 24px 70px rgba(0, 0, 0, 0.34); -} - -.landing-belief-break > header > p { - margin: 0; - color: var(--aqua-bright); - font-size: 12px; - font-weight: 760; - line-height: 1.4; -} - -.landing-belief-break > header > h2 { - margin: 9px 0 0; - font-family: var(--display); - font-size: clamp(25px, 2.4vw, 36px); - font-weight: 520; - letter-spacing: -0.035em; - line-height: 1.05; -} - -.landing-belief-break blockquote { - margin: 13px 0 0; - color: var(--ink-soft); - font-size: 15px; - line-height: 1.5; } .landing-comparison-preview { @@ -6963,15 +6926,8 @@ body { .question-first-layout { width: 100%; - max-width: 780px; + max-width: 820px; min-width: 0; - grid-template-columns: 1fr; - grid-template-areas: - "question" - "belief-break" - "support" - "recent"; - gap: 36px; } } @@ -7119,7 +7075,7 @@ body { } .question-first-layout { - gap: 16px; + gap: 14px; } .landing-question-column { @@ -7130,32 +7086,13 @@ body { font-size: 24px; } - .landing-audience { + .landing-trust-summary { margin-top: 8px; font-size: 15px; } - .landing-category { - margin-top: 6px; - font-size: 14px; - } - .landing-belief-break { - padding: 14px; - border-radius: 17px; - } - - .landing-belief-break > header > p { - font-size: 12px; - } - - .landing-belief-break > header > h2 { - margin-top: 6px; - font-size: 21px; - } - - .landing-belief-break blockquote { - display: none; + width: 100%; } .landing-comparison-preview { From dd7b372fb15048e11224979157d3bf29f9beedc9 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:50:02 +0530 Subject: [PATCH 059/143] test(learner): guard prompt-first landing fold --- apps/web/e2e/judged-flow.spec.ts | 46 ++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/apps/web/e2e/judged-flow.spec.ts b/apps/web/e2e/judged-flow.spec.ts index 2517ef29..d19ce8ee 100644 --- a/apps/web/e2e/judged-flow.spec.ts +++ b/apps/web/e2e/judged-flow.spec.ts @@ -30,6 +30,8 @@ const claim = "The 98 percent random split accuracy proves this model generalizes to customers it has never seen."; const revision = "When rows repeat an entity, hold out whole entities and remove identity-derived features before claiming generalization."; +const prePredictionResultLanguage = + /59\.4%|\bdeceptive\b|\bfairer test\b|\bverified result\b|\bevidence verdict\b|\bsupported hypothesis\b|\bthe fix\b|\bremove customer(?:_| )id\b|\bkeep each customer's rows together\b|\bproves?\b/i; const imbalanceNotebookPath = new URL( "../../../fixtures/notebooks/fraud_class_imbalance.ipynb", import.meta.url, @@ -1332,29 +1334,57 @@ for (const viewport of beliefBreakViewports) { composer, "question composer before secondary paths", ); + await expectEntirelyInFirstViewport( + page, + page.getByRole("heading", { + name: /What result are you trying to understand/i, + }), + "Question-first heading", + ); await expectEntirelyInFirstViewport( page, page.getByText( - /For learners testing whether a notebook result means what they think it means/i, + /Ask a question or attach a supported notebook.*never runs its cells/i, ), - "learner audience statement", + "artifact safety summary", ); - const preview = page.getByRole("complementary", { - name: /One changed variable.*Everything else held fixed/i, - }); + const submit = page.getByRole("button", { name: /Test this claim/i }); + await expect(submit).toBeDisabled(); + await expectEntirelyInFirstViewport( + page, + submit, + "primary Question action", + ); + await expectEntirelyInFirstViewport( + page, + page.getByRole("group", { name: /Prompt starters/i }), + "learner prompt starters", + ); + if (viewport.width <= 620) { + const submitBox = await submit.boundingBox(); + expect(submitBox, "mobile primary Question action bounds").not.toBeNull(); + expect(submitBox!.width).toBeGreaterThanOrEqual(130); + } + + const preview = page.locator( + '[data-presentation="strip"][data-result-visibility="locked"]', + ); await expectEntirelyInFirstViewport( page, preview, "pre-Prediction fair-test promise", ); + await expect(preview).toContainText(/Result locked until Prediction/i); await expect(preview).toContainText( - /Result hidden until your Prediction is sealed/i, + /Familiar rows.*change who counts as new.*Unseen customers/i, ); await expect(preview).toContainText( - /Name the claim.*Lock your expectation.*Change one thing/i, + /Same model, features, preprocessing, sample size, metric, and seed/i, + ); + await expect(page.locator("body")).not.toContainText( + prePredictionResultLanguage, ); - await expect(page.locator("body")).not.toContainText("59.4%"); await expect( page.getByRole("region", { name: "Verified sample belief-break mechanism", From dd99800ab7c00ab6951aca618b82b4f6a584b621 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 08:51:37 +0530 Subject: [PATCH 060/143] docs(progress): record prompt-first release checkpoint --- docs/PROGRESS.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 0904c7ec..56f48b7c 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -12,14 +12,15 @@ pass is never presented as a browser or production pass. ### Current checkpoint - **Branch:** `feat/learner-ux-v6.1` -- **Latest committed checkpoint:** `f19387b1ab214571ea27022efdc0b9432655cc9f`. +- **Latest committed checkpoint:** `dd7b372fb15048e11224979157d3bf29f9beedc9`. The worktree contains only the preserved, untracked `scripts/.fuse_hidden0000a9600000801f`. The source now has a self-hashed cgroup request, observer, FINALIZE/ABORT protocol, fixed negative controls, fail-closed evidence validation, qualified-receipt derivation/store, runtime observer orchestration, serialized run/drain handling, exact v2/v3 Python - consumers, and 40 statically collected Playwright journeys. Source tests do - not constitute a real cgroup sentinel: no exact image, aggregate host proof, + consumers, a tightened Judge first fold, a centered prompt-first Landing, + and 40 statically collected Playwright journeys. Source tests do not + constitute a real cgroup sentinel: no exact image, aggregate host proof, qualification, deployment, rendered browser journey, learner observation, video, Devpost publication, or submission receipt is claimed. - **Earlier product checkpoint:** @@ -3530,3 +3531,70 @@ worktree entry besides this progress update is the preserved FUSE tombstone 6. Learner evidence remains **`NO_DATA`**. Devpost, video, public-repository verification, feedback Session ID, logged-out link audit, publication, and submission receipt remain externally unverified. + +## Prompt-first release-presentation checkpoint — 2026-07-21T03:20:32Z + +Deadline: `2026-07-22T00:00:00Z`; **20 hours 39 minutes 28 seconds remained at +this checkpoint**. The feature branch is at committed HEAD `dd7b372`; the only +worktree entry besides this progress update is the preserved FUSE tombstone +`scripts/.fuse_hidden0000a9600000801f`. It remains unchanged and unstaged. + +### Independently committed slices + +- `bd6e5c3 docs(release): map qualification dependencies` records the source, + release, reviewer, browser, Cloudflare, submission, and merge dependency + graph without representing local source checks as production closure. +- `e80b390 fix(judge): tighten first-fold belief break` reduces Judge first-fold + copy and visual density, preserves the completed-fixed-sample authority + label, and keeps proof one action away before the verified mechanism. +- `830d61b fix(learner): restore prompt-first landing hierarchy` makes the + Question composer the centered dominant action, keeps prompt starters and + textual mobile actions visible, and replaces the large pre-result card with + a compact locked fair-test strip. The Landing strip contains no fixed sample + values or verdict/fix language. +- `dd7b372 test(learner): guard prompt-first landing fold` updates the + desktop/mobile Playwright contract for the new locked strip, visible primary + action and prompts, first-viewport containment, mobile action width, + horizontal overflow, and the shared pre-Prediction forbidden-text corpus. + +### Exact verification in these slices + +- Focused Landing component and integration Vitest: **4 files, 85/85 passed**. +- Complete web Vitest: **79 files, 620/620 passed**. +- Web client TypeScript and E2E TypeScript: **passed**. +- Production Vite/Worker build: **passed**. Main client + **447.26 kB / 128.68 kB gzip**, client CSS + **200.63 kB / 34.69 kB gzip**, Worker + **1,863.39 kB / 359.59 kB gzip**. +- Playwright static collection under the explicit non-rendering design-review + authority: **40 tests in 3 files**. No browser launched and no rendered + qualification is claimed. +- Scoped Prettier, Git whitespace, and explicit secret scans: **passed**. +- One package-manager wrapper retry stopped before test collection with + `unable to open database file`; the same checks passed through the + repository-local Vitest and TypeScript binaries. One initial Playwright + collection command used the wrong repo-local binary path, and one correctly + located command failed closed because no browser authority was supplied; + explicit non-rendering design-review collection then passed. None of these + setup attempts executed product tests or a browser before their corrected + passes. + +### Current remaining gates + +1. The source-level aggregate-cgroup chain remains locally tested but has no + real delegated-cgroup sentinel. The checked-in repository-only filesystem + constitution prohibits the necessary `/sys` and `/proc` reads, so exact + image qualification remains fail-closed. +2. The preserved FUSE tombstone still prevents a clean physical worktree and + is not deleted, hidden, staged, or modified. +3. Final source freeze, one exact runner image, refreshed source/image-bound + evidence, qualification, release checks, contained Wrangler identity, and + Cloudflare deployment remain **not run**. +4. This session still has no `CLOAK_CDP_ENDPOINT`; desktop/mobile rendering, + keyboard, screen-reader, reduced-motion, console/network, reconnect, + downloads, screenshots, and Web Vitals remain **0 current journeys**. +5. Learner evidence remains **`NO_DATA`**. Public repository/video/feedback + alignment, Devpost completion, logged-out verification, publication, and a + submission receipt remain externally unverified. +6. The feature branch has not been pushed and has not been merged into `main`. + Push and fast-forward merge remain downstream of factual release gates. From 2986046cd24e74a799d4494fc84f716f47e15341 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:14:22 +0530 Subject: [PATCH 061/143] test(e2e): authorize private session assertions --- apps/web/e2e/judged-flow.spec.ts | 26 ++++++++++++++++++------ apps/web/e2e/private-session.ts | 20 ++++++++++++++++++ apps/web/e2e/recovery-and-intake.spec.ts | 4 ++++ 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 apps/web/e2e/private-session.ts diff --git a/apps/web/e2e/judged-flow.spec.ts b/apps/web/e2e/judged-flow.spec.ts index d19ce8ee..f0d9c7d9 100644 --- a/apps/web/e2e/judged-flow.spec.ts +++ b/apps/web/e2e/judged-flow.spec.ts @@ -25,6 +25,7 @@ import { snapshotBrowserPerformanceEvidence, type BrowserPerformanceEvidence, } from "./performance-evidence"; +import { privateSessionHeaders } from "./private-session"; const claim = "The 98 percent random split accuracy proves this model generalizes to customers it has never seen."; @@ -152,9 +153,11 @@ async function writeLiveSmokeEvidence( if (sessionId === null) { throw new Error("Live smoke evidence requires a persisted session ID"); } + const ownerHeaders = await privateSessionHeaders(page, sessionId); const completedSessionResponse = await page.request.get( `/api/sessions/${encodeURIComponent(sessionId)}`, + { headers: ownerHeaders }, ); expect(completedSessionResponse.ok()).toBe(true); const completedSessionPayload = (await completedSessionResponse.json()) as { @@ -272,7 +275,7 @@ async function writeLiveSmokeEvidence( const duplicatePublicationResponse = await page.request.post( `/api/sessions/${encodeURIComponent(sessionId)}/replays`, - { data: {} }, + { data: {}, headers: ownerHeaders }, ); expect(duplicatePublicationResponse.status()).toBe(200); const duplicatePublicationPayload = @@ -287,6 +290,7 @@ async function writeLiveSmokeEvidence( const activeStatusResponse = await page.request.get( `/api/sessions/${encodeURIComponent(sessionId)}/replays/status`, + { headers: ownerHeaders }, ); expect(activeStatusResponse.ok()).toBe(true); const activeStatusPayload = (await activeStatusResponse.json()) as { @@ -511,6 +515,7 @@ async function writeLiveSmokeEvidence( const revokedStatusResponse = await page.request.get( `/api/sessions/${encodeURIComponent(sessionId)}/replays/status`, + { headers: ownerHeaders }, ); expect(revokedStatusResponse.ok()).toBe(true); await expect(revokedStatusResponse.json()).resolves.toMatchObject({ @@ -522,7 +527,7 @@ async function writeLiveSmokeEvidence( const duplicateRevocationResponse = await page.request.post( `/api/sessions/${encodeURIComponent(sessionId)}/replays/revoke`, - { data: {} }, + { data: {}, headers: ownerHeaders }, ); expect(duplicateRevocationResponse.status()).toBe(200); await expect(duplicateRevocationResponse.json()).resolves.toMatchObject({ @@ -531,7 +536,7 @@ async function writeLiveSmokeEvidence( const republishRevokedResponse = await page.request.post( `/api/sessions/${encodeURIComponent(sessionId)}/replays`, - { data: {} }, + { data: {}, headers: ownerHeaders }, ); expect(republishRevokedResponse.status()).toBe(409); await expect(republishRevokedResponse.json()).resolves.toMatchObject({ @@ -1838,8 +1843,11 @@ test("prediction is immutable and results do not exist before commitment", async window.localStorage.getItem("counterlab.sessionId"), ); expect(sessionId).not.toBeNull(); + const ownerHeaders = await privateSessionHeaders(page, sessionId!); - const before = await page.request.get(`/api/sessions/${sessionId}`); + const before = await page.request.get(`/api/sessions/${sessionId}`, { + headers: ownerHeaders, + }); expect(before.ok()).toBe(true); expect((await before.json()).data.verifiedResult).toBeUndefined(); @@ -1854,12 +1862,15 @@ test("prediction is immutable and results do not exist before commitment", async `/api/sessions/${sessionId}/prediction`, { data: { choice: "Accuracy remains near 98%", confidence: 100 }, + headers: ownerHeaders, }, ); expect(overwrite.status()).toBe(409); expect((await overwrite.json()).error.code).toBe("ILLEGAL_TRANSITION"); - const committed = await page.request.get(`/api/sessions/${sessionId}`); + const committed = await page.request.get(`/api/sessions/${sessionId}`, { + headers: ownerHeaders, + }); expect(committed.ok()).toBe(true); const committedPayload = await committed.json(); expect(committedPayload.data.prediction.confidence).toBe(88); @@ -2015,7 +2026,10 @@ test("a rejected test releases no result and remains recoverable after refresh", const sessionId = await page.evaluate(() => window.localStorage.getItem("counterlab.sessionId"), ); - const stored = await page.request.get(`/api/sessions/${sessionId}`); + expect(sessionId).not.toBeNull(); + const stored = await page.request.get(`/api/sessions/${sessionId}`, { + headers: await privateSessionHeaders(page, sessionId!), + }); expect(stored.ok()).toBe(true); expect((await stored.json()).data.verifiedResult).toBeUndefined(); diff --git a/apps/web/e2e/private-session.ts b/apps/web/e2e/private-session.ts new file mode 100644 index 00000000..5c452904 --- /dev/null +++ b/apps/web/e2e/private-session.ts @@ -0,0 +1,20 @@ +import type { Page } from "./cloak-test"; + +const ownerCapabilityPattern = /^cl_owner_[A-Za-z0-9_-]{43}$/u; + +export async function privateSessionHeaders( + page: Page, + sessionId: string, +): Promise<{ authorization: string }> { + const storageKey = `counterlab.ownerCapability.${encodeURIComponent(sessionId)}`; + const capability = await page.evaluate( + (key) => window.localStorage.getItem(key), + storageKey, + ); + if (capability === null || !ownerCapabilityPattern.test(capability)) { + throw new Error( + `Private session ${sessionId} has no valid browser-held owner capability`, + ); + } + return { authorization: `Bearer ${capability}` }; +} diff --git a/apps/web/e2e/recovery-and-intake.spec.ts b/apps/web/e2e/recovery-and-intake.spec.ts index 69685e19..fcdd3195 100644 --- a/apps/web/e2e/recovery-and-intake.spec.ts +++ b/apps/web/e2e/recovery-and-intake.spec.ts @@ -1,4 +1,5 @@ import { expect, test, type Page } from "./cloak-test"; +import { privateSessionHeaders } from "./private-session"; import { SAMPLE_LEAKAGE_QUESTION } from "../shared/sample-authority"; const liveQuestion = @@ -164,6 +165,9 @@ for (const response of [ ); const terminalResponse = await page.request.get( `/api/sessions/${encodeURIComponent(closedSessionId!)}`, + { + headers: await privateSessionHeaders(page, closedSessionId!), + }, ); expect(terminalResponse.ok()).toBe(true); expect((await terminalResponse.json()).data.state).toBe( From e9529b0a4da7beaacf33e3a753158647e9b93146 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:17:01 +0530 Subject: [PATCH 062/143] test(e2e): await fresh recovery sessions --- apps/web/e2e/recovery-and-intake.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/web/e2e/recovery-and-intake.spec.ts b/apps/web/e2e/recovery-and-intake.spec.ts index fcdd3195..0c33c2d6 100644 --- a/apps/web/e2e/recovery-and-intake.spec.ts +++ b/apps/web/e2e/recovery-and-intake.spec.ts @@ -186,6 +186,11 @@ for (const response of [ await page .getByRole("button", { name: /Revise in a new investigation/i }) .click(); + await expect + .poll(() => storedSessionId(page), { + message: "the replacement investigation should install a fresh session", + }) + .not.toBe(closedSessionId); await expect( page.getByRole("heading", { name: /What do you think the score means/i }), ).toBeVisible(); From 52b40ef59e86a92e1d6e3a5fc2cb3bf385d204d0 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:23:49 +0530 Subject: [PATCH 063/143] fix(learner): activate session routes before render --- apps/web/e2e/judged-flow.spec.ts | 3 +++ apps/web/src/App.test.tsx | 35 ++++++++++++++++++++++++ apps/web/src/App.tsx | 46 +++++++++++++++++++------------- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/apps/web/e2e/judged-flow.spec.ts b/apps/web/e2e/judged-flow.spec.ts index f0d9c7d9..bfffe219 100644 --- a/apps/web/e2e/judged-flow.spec.ts +++ b/apps/web/e2e/judged-flow.spec.ts @@ -1907,6 +1907,9 @@ test("refresh restores the question and confirmed Prediction phases", async ({ }) => { await reset(page); await page.getByRole("button", { name: /Try verified sample/i }).click(); + await expect( + page.getByRole("heading", { name: /What do you think the score means/i }), + ).toBeVisible(); await page.reload(); await expect( page.getByRole("heading", { name: /What do you think the score means/i }), diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index d0ca015a..f3cc74ac 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -1503,6 +1503,41 @@ describe("CounterLab judged flow", () => { expect(story.getByText(/sha-256/i)).toBeInTheDocument(); }); + it("installs the sample session route before exposing the Question stage", async () => { + const user = userEvent.setup(); + const originalPushState = window.history.pushState.bind(window.history); + const questionVisibilityAtSessionActivation: boolean[] = []; + const pushStateSpy = vi + .spyOn(window.history, "pushState") + .mockImplementation((data, unused, url) => { + if (String(url).startsWith("/session/")) { + questionVisibilityAtSessionActivation.push( + screen.queryByRole("heading", { + name: /what do you think the score means/i, + }) !== null, + ); + } + originalPushState(data, unused, url); + }); + + try { + render(); + await user.click( + screen.getByRole("button", { name: /try verified sample/i }), + ); + + expect( + await screen.findByRole("heading", { + name: /what do you think the score means/i, + }), + ).toBeInTheDocument(); + expect(window.location.pathname).toBe("/session/session_ui"); + expect(questionVisibilityAtSessionActivation).toEqual([false]); + } finally { + pushStateSpy.mockRestore(); + } + }); + it("opens one deterministic stage hint and records only its fixed identity", async () => { const user = userEvent.setup(); const fetcher = installApi(); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index cc4988ad..c9a5d3da 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -554,6 +554,13 @@ const storageKeys = { activeRunnerJobKind: "counterlab.activeRunnerJobKind", } as const; +function activateSessionRoute(sessionId: string): void { + const path = `/session/${encodeURIComponent(sessionId)}`; + if (window.location.pathname !== path) { + window.history.pushState({}, "", path); + } +} + function SkipLink() { return ( @@ -5434,14 +5441,15 @@ export function App() { sampleId: "leakage-01", }); request.assertCurrent(); - setArtifact(sample); - setSession(created); window.localStorage.setItem(storageKeys.sessionId, created.sessionId); window.localStorage.setItem(storageKeys.claim, SAMPLE_LEAKAGE_QUESTION); window.localStorage.setItem( storageKeys.claimSessionId, created.sessionId, ); + activateSessionRoute(created.sessionId); + setArtifact(sample); + setSession(created); setStage("claim"); }); }; @@ -5539,7 +5547,6 @@ export function App() { artifactId: uploaded.artifactId, }); request.assertCurrent(); - setSession(created); window.localStorage.setItem(storageKeys.sessionId, created.sessionId); if (claim.trim().length > 0) { window.localStorage.setItem(storageKeys.claim, claim); @@ -5548,6 +5555,8 @@ export function App() { created.sessionId, ); } + activateSessionRoute(created.sessionId); + setSession(created); setStage("claim"); }; @@ -5587,21 +5596,6 @@ export function App() { request.assertCurrent(); const restartedClaim = sessionBeliefPresentation(restarted)?.claim ?? sourceClaim; - setSession(restarted); - setMode(presentationMode(restarted.mode)); - setClaim(restartedClaim); - setConfirmed( - restarted.state !== "INGESTED" && - restarted.state !== "BELIEF_TEST_PROPOSED", - ); - setPrediction( - restarted.prediction === undefined - ? null - : predictionChoiceFromReceipt(restarted.prediction.choice), - ); - setConfidence(restarted.prediction?.confidence ?? 72); - setAnalysisPreview(null); - setSensitiveContentApproved(false); try { window.localStorage.setItem(storageKeys.sessionId, restarted.sessionId); window.localStorage.setItem( @@ -5622,6 +5616,22 @@ export function App() { // The reconciled server session remains authoritative when this // browser blocks optional local recovery storage. } + activateSessionRoute(restarted.sessionId); + setSession(restarted); + setMode(presentationMode(restarted.mode)); + setClaim(restartedClaim); + setConfirmed( + restarted.state !== "INGESTED" && + restarted.state !== "BELIEF_TEST_PROPOSED", + ); + setPrediction( + restarted.prediction === undefined + ? null + : predictionChoiceFromReceipt(restarted.prediction.choice), + ); + setConfidence(restarted.prediction?.confidence ?? 72); + setAnalysisPreview(null); + setSensitiveContentApproved(false); setStage(restoredStageForSession(restarted)); }); }; From 6142e5edca0a983e70c3cb4a9df911176ebb3e4b Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:29:24 +0530 Subject: [PATCH 064/143] test(e2e): align current learner journeys --- apps/web/e2e/judged-flow.spec.ts | 71 ++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/apps/web/e2e/judged-flow.spec.ts b/apps/web/e2e/judged-flow.spec.ts index bfffe219..6a6f49ca 100644 --- a/apps/web/e2e/judged-flow.spec.ts +++ b/apps/web/e2e/judged-flow.spec.ts @@ -1527,7 +1527,7 @@ for (const viewport of beliefBreakViewports) { } test("Judge Mode distinguishes every authority path", async ({ page }) => { - const healthResponse = await page.request.get("/api/health"); + const healthResponse = await page.request.get("/api/health?readiness=probe"); expect(healthResponse.ok()).toBe(true); const health = (await healthResponse.json()) as { data?: { @@ -1555,7 +1555,8 @@ test("Judge Mode distinguishes every authority path", async ({ page }) => { name: /see a verified belief break in ten seconds/i, }), ).toBeVisible(); - await expect(page.getByText("Sample lesson")).toBeVisible(); + await page.getByRole("button", { name: /Check live readiness/i }).click(); + await expect(page.getByText("Sample lesson", { exact: true })).toBeVisible(); await expect(page.getByText("Live notebook analysis")).toBeVisible(); await expect( page.getByText("Verified replay", { exact: true }), @@ -1567,7 +1568,10 @@ test("Judge Mode distinguishes every authority path", async ({ page }) => { ); } else { await expect( - page.getByText(/live authority is unavailable/i), + page.getByRole("status").filter({ + hasText: + /live authority (?:is|remains) unavailable|live authority did not pass the latest readiness check/i, + }), ).toBeVisible(); await expect(page.getByRole("link", { name: /run live/i })).toHaveCount(0); } @@ -1614,7 +1618,12 @@ test("the first visit explains the lesson before asking for technical knowledge" }), ).toBeVisible(); await expect( - page.getByText(/No account needed.*never run the cells/i), + page.getByText(/reads the evidence and never runs its cells/i), + ).toBeVisible(); + await expect( + page.getByText( + /No account needed.*Sample, live, and replay stay clearly labelled/i, + ), ).toBeVisible(); await expect(page.getByLabel("Your question or claim")).toBeInViewport(); await expect(page.getByLabel("Attach notebook")).toBeVisible(); @@ -1819,7 +1828,9 @@ test("the lesson keeps one learner decision in focus at a time", async ({ await page.getByRole("button", { name: /Check transfer/i }).click(); await expect( - page.getByRole("heading", { name: /You applied the rule correctly/i }), + page.getByRole("heading", { + name: /This fixed forecasting transfer passed/i, + }), ).toBeVisible(); expect(await page.evaluate(() => window.scrollY)).toBeLessThan(24); await expect( @@ -1831,7 +1842,10 @@ test("the lesson keeps one learner decision in focus at a time", async ({ expect(await page.evaluate(() => window.scrollY)).toBeLessThan(24); await expect(page.locator("pre.diff")).not.toBeVisible(); await expect( - page.getByRole("button", { name: "Download proof record", exact: true }), + page.getByRole("button", { + name: "Download repaired notebook", + exact: true, + }), ).toBeVisible(); }); @@ -1983,7 +1997,9 @@ test("refresh restores the current lesson and the committed prediction", async ( ).toBeVisible(); await page.reload(); await expect( - page.getByRole("heading", { name: /You applied the rule correctly/i }), + page.getByRole("heading", { + name: /This fixed forecasting transfer passed/i, + }), ).toBeVisible(); await page.getByRole("button", { name: /Verify notebook patch/i }).click(); await waitForSamplePatch(page); @@ -2073,9 +2089,11 @@ test("local hints and Theater views never request a model or new result", async "true", ); await expect( - page.getByRole("img", { - name: /Verified accuracy comparison.*familiar rows.*new customers/i, - }), + page + .locator('[data-trusted-visual-id="verified_sample_belief_break_v1"]') + .getByRole("region", { + name: "Verified sample belief-break mechanism", + }), ).toBeVisible(); const hint = page.getByLabel("Contextual help"); @@ -2136,10 +2154,21 @@ test("Replay remains visibly labelled and read-only after refresh", async ({ }) => { await reset(page); await page.getByRole("button", { name: /Watch verified replay/i }).click(); - const replayBanner = page.getByLabel("Legacy replay status"); + const replayBanner = page.getByLabel("Replay status"); await expect(replayBanner).toContainText( "Verified replay · read-only stored evidence", ); + await expect( + page.getByRole("heading", { name: /Replay verified session/i }), + ).toBeVisible(); + const continueReplay = page.getByRole("button", { + name: /Continue replay/i, + }); + await expect(continueReplay).toBeVisible(); + await continueReplay.click(); + await expect(page.getByLabel("Legacy replay status")).toContainText( + "Verified replay · read-only stored evidence", + ); await expect( page.getByRole("heading", { name: /Inspect the result without changing its history/i, @@ -2148,9 +2177,7 @@ test("Replay remains visibly labelled and read-only after refresh", async ({ await expect( page.getByRole("table", { name: /Stored fixed-kernel comparison/i }), ).toBeVisible(); - await expect( - page.getByRole("button", { name: /Continue replay/i }), - ).toHaveCount(0); + await expect(continueReplay).toHaveCount(0); await expect( page.getByRole("button", { name: /Show me what happened/i }), ).toHaveCount(0); @@ -2169,7 +2196,7 @@ test("Replay remains visibly labelled and read-only after refresh", async ({ test("missing live capabilities are stated without claiming a model call", async ({ page, }) => { - await page.route("**/api/health", async (route) => { + await page.route("**/api/health*", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", @@ -2202,7 +2229,7 @@ test("missing live capabilities are stated without claiming a model call", async test("configured reasoning cannot start without a qualified hosted runner", async ({ page, }) => { - await page.route("**/api/health", async (route) => { + await page.route("**/api/health*", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", @@ -2241,7 +2268,7 @@ test("configured reasoning cannot start without a qualified hosted runner", asyn test("partial generation isolation cannot expose live notebook upload", async ({ page, }) => { - await page.route("**/api/health", async (route) => { + await page.route("**/api/health*", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", @@ -2380,9 +2407,11 @@ test("the judged path is keyboard operable with reduced motion", async ({ page.getByText(/Interpretation recorded locally/i), ).toBeVisible(); - const comparison = page.getByRole("img", { - name: /Verified accuracy comparison.*familiar rows.*new customers/i, - }); + const comparison = page + .locator('[data-trusted-visual-id="verified_sample_belief_break_v1"]') + .getByRole("region", { + name: "Verified sample belief-break mechanism", + }); await expect(comparison).toBeVisible(); const observeTab = page.getByRole("tab", { name: /Observe/i }); await observeTab.focus(); @@ -2391,7 +2420,7 @@ test("the judged path is keyboard operable with reduced motion", async ({ await page.keyboard.press("ArrowRight"); await expect(page.getByRole("tab", { name: /Boundary/i })).toBeFocused(); await expect(page.getByRole("tabpanel")).toContainText( - /Verified sample boundary/i, + /Verified sample exploration/i, ); await page.getByRole("button", { name: /Reveal the map/i }).click(); await expect( From d184e5a79f0d80b01891c7595a095442925bd58c Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:38:56 +0530 Subject: [PATCH 065/143] test(e2e): configure local intake authority --- apps/web/e2e/recovery-and-intake.spec.ts | 7 ++++--- apps/web/playwright.config.ts | 10 ++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/web/e2e/recovery-and-intake.spec.ts b/apps/web/e2e/recovery-and-intake.spec.ts index 0c33c2d6..43569eac 100644 --- a/apps/web/e2e/recovery-and-intake.spec.ts +++ b/apps/web/e2e/recovery-and-intake.spec.ts @@ -5,9 +5,10 @@ import { SAMPLE_LEAKAGE_QUESTION } from "../shared/sample-authority"; const liveQuestion = "Does this notebook score show that the model generalizes to customers it has never seen?"; const supportedNotebookPath = new URL( - "../../../fixtures/notebooks/customer_churn_leakage.ipynb", + "../../../evals/held-out/notebooks/leakage-rows-pipeline.ipynb", import.meta.url, ).pathname; +const supportedNotebookName = "leakage-rows-pipeline.ipynb"; const readyHealth = { ok: true, @@ -354,7 +355,7 @@ test("a supported live upload creates one source-bound session", async ({ await expect( page.getByRole("heading", { name: /What do you think the score means/i }), ).toBeVisible(); - await expect(page.getByText("customer_churn_leakage.ipynb")).toBeVisible(); + await expect(page.getByText(supportedNotebookName)).toBeVisible(); await expect( page.getByRole("button", { name: /Compare two explanations/i }), ).toBeEnabled(); @@ -425,7 +426,7 @@ test("a lost private-session response retries without re-uploading", async ({ await expect( page.getByRole("button", { name: /Retry private session setup/i }), ).toBeEnabled(); - await expect(page.getByText("customer_churn_leakage.ipynb")).toBeVisible(); + await expect(page.getByText(supportedNotebookName)).toBeVisible(); await page .getByRole("button", { name: /Retry private session setup/i }) diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 6bf51bb8..610eddd2 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -90,6 +90,9 @@ const baseURL = remoteBaseURL ?? `http://127.0.0.1:${port}`; const staticDesignReview = browserAuthority.kind === "stock-chromium-design-review" && process.env.COUNTERLAB_E2E_STATIC_CLIENT === "true"; +const localAdmissionKey = + process.env.COUNTERLAB_ADMISSION_KEY?.trim() || + ["counterlab", "local", "e2e", "admission", "only", "000000"].join("-"); export default defineConfig({ testDir: "./e2e", @@ -129,6 +132,13 @@ export default defineConfig({ ? "node ../../scripts/serve-built-client.mjs" : `./node_modules/.bin/vite --host 127.0.0.1 --port ${port}`, cwd: import.meta.dirname, + ...(staticDesignReview + ? {} + : { + env: { + COUNTERLAB_ADMISSION_KEY: localAdmissionKey, + }, + }), url: baseURL, reuseExistingServer: false, timeout: 120_000, From cdbe153f0f3bde97caffcd6535bac6820dfc234c Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:42:34 +0530 Subject: [PATCH 066/143] fix(learner): distinguish notebook evidence keys --- apps/web/e2e/recovery-and-intake.spec.ts | 10 +++++++ apps/web/src/App.test.tsx | 35 +++++++++++++++++++++++- apps/web/src/App.tsx | 6 ++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/apps/web/e2e/recovery-and-intake.spec.ts b/apps/web/e2e/recovery-and-intake.spec.ts index 43569eac..bc72ea35 100644 --- a/apps/web/e2e/recovery-and-intake.spec.ts +++ b/apps/web/e2e/recovery-and-intake.spec.ts @@ -338,6 +338,15 @@ test("a supported live upload creates one source-bound session", async ({ }) => { let uploadRequests = 0; let sessionCreationRequests = 0; + const duplicateKeyWarnings: string[] = []; + page.on("console", (message) => { + if ( + message.type() === "error" && + /same key|keys should be unique/i.test(message.text()) + ) { + duplicateKeyWarnings.push(message.text()); + } + }); page.on("request", (request) => { if (isPostTo(request.url(), request.method(), "/api/artifacts")) { uploadRequests += 1; @@ -362,6 +371,7 @@ test("a supported live upload creates one source-bound session", async ({ expect(await storedSessionId(page)).toMatch(/^session_/u); expect(uploadRequests).toBe(1); expect(sessionCreationRequests).toBe(1); + expect(duplicateKeyWarnings).toEqual([]); }); test("an interrupted upload accepts the same file on retry", async ({ diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index f3cc74ac..81090245 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { + ArtifactManifestSchema, migrateBeliefTestV1ToV2, VerifiedResultSetSchema, type EvidenceEvent, @@ -11,7 +12,7 @@ import { import imbalanceResultText from "../../../fixtures/held-out/imbalance_epistemic_competing_v2.json?raw"; import { SAMPLE_LEAKAGE_QUESTION } from "../shared/sample-authority"; -import { App } from "./App"; +import { App, notebookEvidenceReferences } from "./App"; import { SessionViewSchema } from "./api"; import { publicReplayFixture, @@ -1019,6 +1020,38 @@ async function openSampleModelDuel(user: ReturnType) { } describe("CounterLab judged flow", () => { + it("assigns distinct evidence identities to metrics from one output", () => { + const outputHash = "d".repeat(64); + const references = notebookEvidenceReferences( + ArtifactManifestSchema.parse({ + ...uploadedArtifact, + cells: [ + { + index: 1, + type: "code", + sourceSha256: "b".repeat(64), + sourceExcerpt: "accuracy and roc_auc", + executionCount: 1, + outputHashes: [outputHash], + symbols: [], + metricCandidates: [ + { name: "accuracy", value: 0.98, outputIndex: 0 }, + { name: "roc_auc", value: 0.97, outputIndex: 0 }, + ], + }, + ], + }), + ); + + expect(references.map(({ id }) => id)).toEqual([ + `${outputHash}-0-accuracy`, + `${outputHash}-0-roc_auc`, + ]); + expect(new Set(references.map(({ id }) => id)).size).toBe( + references.length, + ); + }); + it("keeps Judge Mode on its own refresh-safe route", async () => { const user = userEvent.setup(); const fetcher = installApi({ diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index c9a5d3da..90df385d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -708,7 +708,7 @@ function notebookScoreDisplay( }; } -function notebookEvidenceReferences( +export function notebookEvidenceReferences( artifact: ArtifactView | null, ): readonly NotebookEvidenceReference[] { if (artifact?.fileSha256 === sampleArtifact.fileSha256) { @@ -733,9 +733,7 @@ function notebookEvidenceReferences( if (artifact === null) return []; const metricReferences = artifact.cells.flatMap((cell) => cell.metricCandidates.map((metric) => ({ - id: - cell.outputHashes[metric.outputIndex] ?? - `${cell.sourceSha256}-${metric.outputIndex}-${metric.name}`, + id: `${cell.outputHashes[metric.outputIndex] ?? cell.sourceSha256}-${metric.outputIndex}-${metric.name}`, reference: `Cell ${cell.index} · output ${metric.outputIndex}`, relevance: `${metric.name} is displayed notebook evidence attached to this question.`, excerpt: `${metric.name}: ${metric.value.toLocaleString()}`, From 4f7bbda6e980855b02641f384ca1ae6859c72dc3 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:50:37 +0530 Subject: [PATCH 067/143] test(perf): scope first-fold browser budgets --- apps/web/e2e/judged-flow.spec.ts | 94 +++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/apps/web/e2e/judged-flow.spec.ts b/apps/web/e2e/judged-flow.spec.ts index 6a6f49ca..d063e16f 100644 --- a/apps/web/e2e/judged-flow.spec.ts +++ b/apps/web/e2e/judged-flow.spec.ts @@ -667,12 +667,13 @@ async function resetWithBrowserFailureObservation( return failures; } -function expectWithinComprehensionBudget( - startedAt: number, +async function expectWithinComprehensionBudget( + page: Page, maximumMs: number, label: string, ) { - expect(Date.now() - startedAt, label).toBeLessThanOrEqual(maximumMs); + const navigationElapsedMs = await page.evaluate(() => performance.now()); + expect(navigationElapsedMs, label).toBeLessThanOrEqual(maximumMs); } function expectNoBrowserFailures(failures: BrowserFailureLog) { @@ -711,7 +712,6 @@ async function expectBeliefBreakInFirstViewport( page: Page, surface: Locator, modeLabel: RegExp, - startedAt: number, ) { const mechanism = surface.getByRole("region", { name: "Verified sample belief-break mechanism", @@ -799,6 +799,14 @@ async function expectBeliefBreakInFirstViewport( await expect(mechanism).toBeVisible(); for (const stage of comprehensionStages) { + await Promise.all( + stage.required.map(({ locator }) => expect(locator).toBeVisible()), + ); + await expectWithinComprehensionBudget( + page, + stage.budgetMs, + `Judge ${stage.label} must be inspectable within ${stage.budgetMs / 1_000} seconds`, + ); for (const required of stage.required) { await expectEntirelyInFirstViewport( page, @@ -806,15 +814,19 @@ async function expectBeliefBreakInFirstViewport( required.label, ); } - expectWithinComprehensionBudget( - startedAt, - stage.budgetMs, - `Judge ${stage.label} must be inspectable within ${stage.budgetMs / 1_000} seconds`, - ); } } -async function captureBeliefBreakScreenshot(page: Page, fileName: string) { +async function captureBeliefBreakScreenshot( + page: Page, + fileName: string, + options: { + performanceBudgetScope?: + "navigation-first-fold" | "post-seal-spa-observation"; + } = {}, +) { + const performanceBudgetScope = + options.performanceBudgetScope ?? "navigation-first-fold"; const configuredDirectory = process.env.COUNTERLAB_E2E_BELIEF_BREAK_EVIDENCE_DIR ?? resolve(requiredRuntimeRoot(), "evidence", "belief-break"); @@ -823,7 +835,9 @@ async function captureBeliefBreakScreenshot(page: Page, fileName: string) { ); await page.screenshot({ path: destination, fullPage: false }); const performance = await snapshotBrowserPerformanceEvidence(page); - expectFirstFoldPerformanceBudgets(performance); + if (performanceBudgetScope === "navigation-first-fold") { + expectFirstFoldPerformanceBudgets(performance); + } const evidenceDestination = await ensureRuntimeParent( resolve(configuredDirectory, fileName.replace(/\.png$/u, ".json")), ); @@ -834,6 +848,10 @@ async function captureBeliefBreakScreenshot(page: Page, fileName: string) { browserAuthority: currentBrowserAuthorityLabel(), capturedAt: new Date().toISOString(), performance, + performanceBudget: { + enforced: performanceBudgetScope === "navigation-first-fold", + scope: performanceBudgetScope, + }, screenshot: fileName, }, null, @@ -1322,17 +1340,45 @@ for (const viewport of beliefBreakViewports) { test(`${viewport.name} Landing keeps the unprimed Question and fair-test promise in the first viewport`, async ({ page, }) => { - const navigationStartedAt = Date.now(); await page.setViewportSize(viewport); const failures = await resetWithBrowserFailureObservation(page); + const heading = page.getByRole("heading", { + name: /What result are you trying to understand/i, + }); + const artifactSafetySummary = page.getByText( + /Ask a question or attach a supported notebook.*never runs its cells/i, + ); + const composer = page.getByLabel("Your question or claim"); + const submit = page.getByRole("button", { name: /Test this claim/i }); + const promptStarters = page.getByRole("group", { + name: /Prompt starters/i, + }); + const preview = page.locator( + '[data-presentation="strip"][data-result-visibility="locked"]', + ); + await Promise.all( + [ + heading, + artifactSafetySummary, + composer, + submit, + promptStarters, + preview, + ].map((locator) => expect(locator).toBeVisible()), + ); + await expectWithinComprehensionBudget( + page, + 10_000, + "Landing Question and fair-test promise must become inspectable within ten seconds", + ); + const skipLink = page.getByRole("link", { name: /Skip to main content/i, }); await skipLink.focus(); await expect(skipLink).toBeFocused(); await page.keyboard.press("Tab"); - const composer = page.getByLabel("Your question or claim"); await expect(composer).toBeFocused(); await expectEntirelyInFirstViewport( page, @@ -1341,20 +1387,15 @@ for (const viewport of beliefBreakViewports) { ); await expectEntirelyInFirstViewport( page, - page.getByRole("heading", { - name: /What result are you trying to understand/i, - }), + heading, "Question-first heading", ); await expectEntirelyInFirstViewport( page, - page.getByText( - /Ask a question or attach a supported notebook.*never runs its cells/i, - ), + artifactSafetySummary, "artifact safety summary", ); - const submit = page.getByRole("button", { name: /Test this claim/i }); await expect(submit).toBeDisabled(); await expectEntirelyInFirstViewport( page, @@ -1363,7 +1404,7 @@ for (const viewport of beliefBreakViewports) { ); await expectEntirelyInFirstViewport( page, - page.getByRole("group", { name: /Prompt starters/i }), + promptStarters, "learner prompt starters", ); if (viewport.width <= 620) { @@ -1372,9 +1413,6 @@ for (const viewport of beliefBreakViewports) { expect(submitBox!.width).toBeGreaterThanOrEqual(130); } - const preview = page.locator( - '[data-presentation="strip"][data-result-visibility="locked"]', - ); await expectEntirelyInFirstViewport( page, preview, @@ -1395,11 +1433,6 @@ for (const viewport of beliefBreakViewports) { name: "Verified sample belief-break mechanism", }), ).toHaveCount(0); - expectWithinComprehensionBudget( - navigationStartedAt, - 10_000, - "Landing Question and fair-test promise must become inspectable within ten seconds", - ); await expectNoHorizontalOverflow(page); await captureBeliefBreakScreenshot( page, @@ -1412,7 +1445,6 @@ for (const viewport of beliefBreakViewports) { test(`${viewport.name} Judge Mode shows the honest fixed-sample belief break in the first viewport`, async ({ page, }) => { - const navigationStartedAt = Date.now(); const failures = observeBrowserFailures(page); await installBrowserPerformanceEvidence(page); await page.setViewportSize(viewport); @@ -1426,7 +1458,6 @@ for (const viewport of beliefBreakViewports) { page, preview, /Completed fixed sample.*not a live result/i, - navigationStartedAt, ); await expect( preview.getByText( @@ -1520,6 +1551,7 @@ for (const viewport of beliefBreakViewports) { await captureBeliefBreakScreenshot( page, `belief-break-sample-post-seal-${viewport.name}.png`, + { performanceBudgetScope: "post-seal-spa-observation" }, ); await page.waitForLoadState("networkidle"); expectNoBrowserFailures(failures); From f5155e0cb19fec74330e1d3df9ab0bce0feca52b Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:52:29 +0530 Subject: [PATCH 068/143] docs(progress): record browser harness recovery --- docs/PROGRESS.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 56f48b7c..50f0f2eb 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -3598,3 +3598,86 @@ worktree entry besides this progress update is the preserved FUSE tombstone submission receipt remain externally unverified. 6. The feature branch has not been pushed and has not been merged into `main`. Push and fast-forward merge remain downstream of factual release gates. + +## Browser-harness recovery checkpoint — 2026-07-21T04:20:55Z + +Deadline: `2026-07-22T00:00:00Z`; **19 hours 39 minutes 05 seconds remained at +this checkpoint**. The feature branch is at committed HEAD `4f7bbda`; the only +worktree entry besides this progress update is the preserved FUSE tombstone +`scripts/.fuse_hidden0000a9600000801f`. It remains unread, unchanged, and +unstaged. + +### Independently committed slices + +- `2986046 test(e2e): authorize private session assertions` sends the + browser-held owner capability on direct private-session E2E requests. +- `e9529b0 test(e2e): await fresh recovery sessions` verifies that Reject and + Not enough evidence recovery creates a fresh session before resubmission. +- `52b40ef fix(learner): activate session routes before render` persists the + session and route before exposing learner state, closing refresh and browser + history races. +- `6142e5e test(e2e): align current learner journeys` updates stale accessible + names and route expectations without changing production authority. +- `d184e5a test(e2e): configure local intake authority` supplies a clearly + local-only admission key to the local Playwright server and uses a distinct + supported live notebook fixture rather than sample evidence. +- `cdbe153 fix(learner): distinguish notebook evidence keys` gives repeated + notebook evidence references stable unique React keys and adds unit/browser + console regression coverage. +- `4f7bbda test(perf): scope first-fold browser budgets` measures current-page + visible readiness instead of harness setup time, checks required content in + parallel before geometry assertions, and records post-seal SPA metrics as + observation-only rather than mislabelling navigation LCP as post-interaction + qualification. + +### Exact verification in these slices + +- Full App integration suite before the evidence-key characterization: + **71/71 passed**; the added evidence-key unit test then passed **1/1**. +- Recovery and route-regression browser runs: **2/2** and **1/1 passed**. +- Updated learner-selector browser slices: **7/10 passed** on the first run; + the corrected three cases then passed **3/3**. +- Intake browser matrix on fresh local D1 state: malformed/unsupported cases + **3/3 passed** and supported/retry cases **3/3 passed**. +- Duplicate-key supported-upload browser regression: **1/1 passed** with no + React duplicate-key console error. +- Desktop/mobile Landing, Judge first fold, and post-seal sample browser + matrix: **6/6 passed in 22.5 seconds**. +- Current first-fold design-review metrics: Landing LCP **684–740 ms**, Judge + LCP **1,216–1,352 ms**, CLS **0–0.02145**, and TTFB **6.2–28.7 ms**. +- E2E strict TypeScript, scoped Prettier, Git whitespace, and scoped secret + scans: **passed**. +- A prior complete stock-browser run collected **40 tests** and produced + **10 passed, 25 failed, 5 skipped**. All 25 failures were classified as + harness authorization, stale selectors, route ordering, duplicate evidence + identity, or performance-scope problems. Targeted repairs now pass, but the + complete 40-test matrix has not yet been rerun, so it is not claimed green. + +All rendered checks in this checkpoint used explicitly labelled +`stock-chromium-design-review` authority. They are useful implementation and +visual-review evidence, but do not satisfy the required CloakBrowser public +release qualification. + +### Current remaining gates + +1. Run the complete 40-test stock-browser matrix against fresh local D1 state, + repair any remaining product or harness failures, and record exact results. +2. Run the final deterministic TypeScript, Vitest, Pytest, build, formatting, + secret, whitespace, mutation, and held-out gates at frozen source. +3. Obtain a clean exact source `S`, execute the real aggregate-cgroup sentinel, + build one exact runner image, regenerate source/image-bound SBOM, VEX, and + scientific evidence, create evidence commit `E`, and qualify the tuple. + The preserved FUSE entry and the constitution's prohibition on required + `/sys` and `/proc` reads currently keep this chain fail-closed. +4. Establish repository-contained Wrangler identity, deploy only the qualified + tuple, and verify exact public Worker/Container/source identity and complete + production smoke evidence. +5. Run all public desktop/mobile, keyboard, screen-reader, reduced-motion, + reconnect, download, console/network, and Web Vitals journeys through an + injected CloakBrowser endpoint. This session still has no + `CLOAK_CDP_ENDPOINT`. +6. Keep learner evidence as `NO_DATA` unless real consented observations occur. + Public repository/video/feedback alignment, Devpost publication, logged-out + verification, and submission receipt remain externally unverified. +7. Push the feature branch and fast-forward `main` only after the ship gates + above are factual; neither publication action has occurred. From e0313101952e2e1b66421bfb2c38adae383f7fad Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 09:58:17 +0530 Subject: [PATCH 069/143] docs(release): record local browser matrix --- docs/PROGRESS.md | 17 +++++++++++++ .../19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md | 24 ++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 50f0f2eb..654960f5 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -3681,3 +3681,20 @@ release qualification. verification, and submission receipt remain externally unverified. 7. Push the feature branch and fast-forward `main` only after the ship gates above are factual; neither publication action has occurred. + +## Complete local browser-matrix checkpoint — 2026-07-21T04:26:26Z + +- The complete Playwright collection executed against fresh repository-local + D1 state under explicit `stock-chromium-design-review` authority: **40 tests + in 3 files, 35 passed, 5 intentionally skipped, 0 failed in 2.7 minutes**. +- The five skips are exact and expected at this gate: two public mobile replay + journeys require `COUNTERLAB_E2E_BASE_URL`, one public asset scan requires a + frozen release manifest and public origin, and two untouched supported live + journeys require `COUNTERLAB_E2E_LIVE=1` with a genuinely qualified analyst + and hosted runner. +- All local Sample, Replay, intake, recovery, refresh/history, authority, + responsive, keyboard, reduced-motion, transfer, patch-lock, first-fold, + console/network, and no-result-before-Prediction journeys passed. +- This closes the local stock-browser regression gate only. It does not close + the exact-public, hosted-live, CloakBrowser, real screen-reader, deployment, + or submission gates. diff --git a/docs/audits/counterlab-first-prize/19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md b/docs/audits/counterlab-first-prize/19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md index 5158aeec..618ecd9c 100644 --- a/docs/audits/counterlab-first-prize/19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md +++ b/docs/audits/counterlab-first-prize/19_RELEASE_DEPENDENCY_AND_REVIEW_GRAPH.md @@ -1,10 +1,10 @@ # Release dependency and review graph -- Checkpoint: `2026-07-21T03:07:00Z` +- Checkpoint: `2026-07-21T04:26:26Z` - Branch: `feat/learner-ux-v6.1` -- Committed HEAD: `e4dc33eb44962b972c0157e5ecb4ed9760c16ee4` +- Committed HEAD: `f5155e0cb19fec74330e1d3df9ab0bce0feca52b` - Deadline: `2026-07-22T00:00:00Z` -- Time remaining at this checkpoint: **20 hours 53 minutes** +- Time remaining at this checkpoint: **19 hours 33 minutes 34 seconds** ## Purpose and evidence boundary @@ -22,6 +22,12 @@ worktree will reject this state. No current deployment, rendered CloakBrowser journey, learner outcome, video, Devpost submission, branch push, or merge is claimed here. +The complete local `stock-chromium-design-review` matrix now passes every +locally executable journey: **35 passed, 5 intentionally skipped, 0 failed in +2.7 minutes**. The five skips are the two exact-public mobile replay journeys, +one exact-public asset scan, and two qualified hosted-runner journeys. This is +useful source/design evidence; it is not public or CloakBrowser qualification. + ## Release dependency graph ```mermaid @@ -37,7 +43,7 @@ flowchart TD I["Release check
receipt v5 + deterministic full gate
NOT RUN"] J["Cloudflare deployment
maintenance + D1 + Container + Worker
NOT RUN"] K["Public identity and smoke
deployment receipt v7 + /ready + health + live loops
NOT RUN"] - L["CloakBrowser qualification
40 journeys + manual accessibility/performance matrix
0 rendered journeys"] + L["Browser qualification
local stock 35 pass / 5 public-live skips
public CloakBrowser: 0 journeys"] M["Submission package
public app + repo + video + feedback + honest NO_DATA
EXTERNALLY UNVERIFIED"] N["Git publication
push feature -> verify remote -> fast-forward main -> push main
NOT RUN"] @@ -68,7 +74,7 @@ flowchart TD | Release check | Clean `E`, qualified receipt, exact local images and runtime | Release-check receipt v5 after formatting, tests, mutations, held-out, sandbox, scientific, build, reproduction, patch replay, and secret gates | **Not run** | Any failure blocks deployment | | Cloudflare deployment | Qualified receipt, release-check receipt, registry image, required secrets, recovery target, working CloakBrowser endpoint | Exact maintenance/final Worker versions, D1 migration log, exact Container digest, deployment receipt v7 | **Not run** | Stop before mutation when preflight is red; after mutation, automated Worker recovery does not reverse D1 or Container state | | Public identity and smoke | Active exact Worker/Container tuple | `/ready`, `/api/health?readiness=probe`, Worker 100% traffic, Container identity, public asset scan, Sample, Replay, leakage and imbalance live evidence, Capsule/replay evidence | **Not run** | Source, image, Worker, client, receipt, or capability mismatch is a release failure | -| CloakBrowser | Exact public tuple and injected `CLOAK_CDP_ENDPOINT` | All 40 current tests plus responsive, keyboard, screen-reader, 200% zoom, reduced-motion, console/network, downloads, reconnect, and Web Vitals evidence | **40 statically collected; 0 rendered** | Stock Chromium is design-review evidence only and cannot close this gate | +| CloakBrowser | Exact public tuple and injected `CLOAK_CDP_ENDPOINT` | All 40 current tests plus responsive, keyboard, screen-reader, 200% zoom, reduced-motion, console/network, downloads, reconnect, and Web Vitals evidence | Local stock design review: **35 passed, 5 public/live skips, 0 failed**; public CloakBrowser: **0 journeys** | Stock Chromium is design-review evidence only and cannot close this gate | | Devpost and impact | Qualified public tuple, public repo/video/feedback links, factual copy | Logged-out link audit, public video from exact tuple, Devpost receipt, real consented learner aggregates or explicit `NO_DATA` | **Externally unverified; learner evidence remains `NO_DATA`** | Never infer a submission or learner result | | Push and merge | Every ship gate factual and final source reviewed | Verified feature remote tip, fast-forward-only `main`, verified main remote tip | **Not run** | Do not push or merge a `NO-GO` release | @@ -86,7 +92,7 @@ flowchart LR subgraph Verification["Pre-release verification"] T1["Vitest + TypeScript"] T2["Pytest + cross-runtime parity"] - T3["Playwright source + static collection"] + T3["Playwright source + local stock matrix"] Q1["Independent review"] end @@ -147,8 +153,10 @@ Reported verification for the integrated aggregate slice is: - complete runner Pytest with both required source roots: **82/82 passed**; - repository, web client, generated Worker, and Worker TypeScript: **passed**; - E2E strict TypeScript: **passed**; -- Playwright static collection: **40 tests in 3 files**, with no browser - launched; +- Complete local stock-browser matrix: **40 tests in 3 files**, **35 passed, + 5 intentionally skipped, 0 failed in 2.7 minutes**. The public replay, + public asset, and real hosted-runner cases remain deferred to the exact + release tuple; - scoped Prettier, Python compilation, Node syntax, Git whitespace, and secret scans: **passed**; - independent runtime integration review: **GO** after the implementation From 9229c8ef4447063f8f6eea717da970e633c36d7e Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 10:08:05 +0530 Subject: [PATCH 070/143] style(tests): normalize scientific UI fixtures --- .../TrustedLabSceneRenderer.test.tsx | 18 ++++++++---------- .../src/scientific-candidate-v5.test.ts | 3 ++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/generative-ui/TrustedLabSceneRenderer.test.tsx b/apps/web/src/components/generative-ui/TrustedLabSceneRenderer.test.tsx index 7aea5009..91e2af99 100644 --- a/apps/web/src/components/generative-ui/TrustedLabSceneRenderer.test.tsx +++ b/apps/web/src/components/generative-ui/TrustedLabSceneRenderer.test.tsx @@ -222,16 +222,14 @@ describe("TrustedLabSceneRenderer", () => { ) - } + signedResult={signedResult({ + integrity: { + mode: "hmac-signed", + contentHash: resultHash, + signature: digest("f"), + keyId: "counterlab-result-v1", + }, + } as unknown as Partial)} />, ); expect(screen.getByRole("alert")).toBeVisible(); diff --git a/packages/plan-verifier/src/scientific-candidate-v5.test.ts b/packages/plan-verifier/src/scientific-candidate-v5.test.ts index 6aa4c189..dbf908b1 100644 --- a/packages/plan-verifier/src/scientific-candidate-v5.test.ts +++ b/packages/plan-verifier/src/scientific-candidate-v5.test.ts @@ -561,7 +561,8 @@ describe("scientific v5 candidate verification", () => { label: "an invented result claim in the scene title", mutate: (scene: LabSceneV2) => ({ ...scene, - title: "The repeated-identity explanation won after accuracy collapsed.", + title: + "The repeated-identity explanation won after accuracy collapsed.", }), }, { From 6a92e06b543769164d5ea5a67adfcf5b0e99894b Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 10:13:42 +0530 Subject: [PATCH 071/143] docs(readme): reconcile judge link and evidence --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7241ef6f..70f9d9c4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ misconceptions in supported Jupyter notebooks. Its released Subject Packs cover entity leakage and class imbalance/metric choice; it is not a generic notebook copilot or an unrestricted code runner. -**Live judge surface:** +**Live judge surface:** The v6.1 learner UX is implemented locally on `feat/learner-ux-v6.1`; it has not been pushed, deployed, or browser-qualified. @@ -274,10 +274,10 @@ The current measured artifact is - random accuracy 0.984722, group accuracy 0.594444, ablation accuracy 0.673611; - group entity overlap 0; - canonical result hash - `2501654264b9aa85b39fca944e585ff9b04263b83e182bc186d1f16464fee3b0`; -- 12/12 published critical mutations detected; + `a6ae7652e04e4d70196f991c63b8f7bcb3b76f8c4ab833d3ce2b626df0ab6c94`; +- 14/14 published leakage critical mutations detected; - class-imbalance majority accuracy 0.989333 with 0 rare-class recall, and - 12/12 imbalance mutations detected; + 15/15 published imbalance mutations detected; - held-out intake/routing 10/10 and fixed full-loop completion 7/8; the Random Forest patch is explicitly outside the registered non-sample patch contract; From 1aefd208ecbf3a05c1708d1b3242864a2ae837e8 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 10:17:06 +0530 Subject: [PATCH 072/143] docs(submission): align fields demo and screenshot evidence --- docs/DEMO_CAPTIONS.vtt | 4 ++-- docs/DEMO_SCRIPT.md | 31 +++++++++++++++------------- docs/DEVPOST_COPY.md | 31 +++++++++++++++++----------- docs/SCREENSHOT_PLAN.md | 45 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 80 insertions(+), 31 deletions(-) diff --git a/docs/DEMO_CAPTIONS.vtt b/docs/DEMO_CAPTIONS.vtt index 80315b52..24026779 100644 --- a/docs/DEMO_CAPTIONS.vtt +++ b/docs/DEMO_CAPTIONS.vtt @@ -7,10 +7,10 @@ CounterLab is for ML learners and instructors. This fixed sample changes one var CounterLab begins with the Question. It treats notebook evidence as untrusted data, never runs a cell, and sends no raw rows, notebook file, or local path. 00:27.000 --> 00:43.000 -GPT-5.6 proposes these competing explanations from only the approved sanitized packet. It cannot run the notebook or decide the result. The learner seals an immutable Prediction before any experimental value appears. +GPT-5.6 proposes these competing explanations from only the approved sanitized packet. It cannot run the notebook or decide the result. The learner seals an immutable Prediction before any experimental value appears in this live session. 00:43.000 --> 00:58.000 -Runtime Codex proposes only bounded operation IDs. The fixed scorer selects the test, and the frozen verifier decides whether the fixed kernel may release evidence. +Runtime Codex composes registered operation IDs into bounded plans. The fixed scorer selects the test, and the frozen verifier decides whether the fixed kernel may release evidence. 00:58.000 --> 01:18.000 The numbers come from the fixed kernel. GPT-5.6 and Codex may explain or propose, but neither computes or verifies them. diff --git a/docs/DEMO_SCRIPT.md b/docs/DEMO_SCRIPT.md index 909da168..1952f556 100644 --- a/docs/DEMO_SCRIPT.md +++ b/docs/DEMO_SCRIPT.md @@ -1,10 +1,12 @@ # 2:45 demo voiceover and shot list -Record **Live notebook analysis** only after the exact source has passed the -Container smoke. A bundled sample is frozen, not a substitute live run. Keep -**Verified replay** open as a disclosed backup and never present it as a new -model call. Use [DEMO_CAPTIONS.vtt](DEMO_CAPTIONS.vtt) word for word so the -spoken roles, on-screen labels, and captions agree. +Record **Live notebook analysis** only after the exact source commit, runner +image digest, Worker version, deployment ID, qualification receipt, and +release-check receipt agree and that deployed tuple passes production smoke. A +bundled sample is frozen, not a substitute live run. Keep **Verified replay** +open as a disclosed backup and never present it as a new model call. Use +[DEMO_CAPTIONS.vtt](DEMO_CAPTIONS.vtt) word for word so the spoken roles, +on-screen labels, and captions agree. ## 0:00–0:12 — The belief break @@ -28,7 +30,7 @@ weight. Confirm that the first captures the learner's meaning. Choose “Remain near 98%,” set confidence, and seal it. Say: “GPT-5.6 proposes these competing explanations from only the approved sanitized packet. It cannot run the notebook or decide the result. The learner seals an immutable Prediction before -any experimental value appears.” +any experimental value appears in this live session.” ## 0:43–0:58 — Runtime Codex and Fair Test Builder @@ -36,9 +38,9 @@ Show what changes, what stays fixed, and why whole-customer holdout matches deployment. If this exact live compile is genuinely rejected and repaired, show the learner-language explanation and optionally open Evidence & proof for the sanitized counterexample. If it passes first try, say so. Never stage a failure. -Say: “Runtime Codex proposes only bounded operation IDs. The fixed scorer -selects the test, and the frozen verifier decides whether the fixed kernel may -release evidence.” +Say: “Runtime Codex composes registered operation IDs into bounded plans. The +fixed scorer selects the test, and the frozen verifier decides whether the +fixed kernel may release evidence.” ## 0:58–1:18 — Experiment Theater @@ -74,11 +76,12 @@ transfer passes, and CounterLab patches only a copy.” ## 2:10–2:30 — Completion and proof -Show: “You completed one verified entity-leakage loop: this fixed task separated -performance on familiar rows from performance on new entities. It does not -establish mastery.” Then open Evidence & proof for the Reasoning Diff and Proof -Capsule. Say: “The Reasoning Diff is for the learner. The Proof Capsule binds -the machine-readable evidence, versions, limitations, and event chain.” +Show: “You completed one verified entity-leakage loop.” Then show “good on +familiar rows” was compared with “performance on new entities in this fixed +task,” followed by “Fixed transfer task passed” and the one-task/no-mastery +limitation. Open Evidence & proof for the Reasoning Diff and Proof Capsule. +Say: “The Reasoning Diff is for the learner. The Proof Capsule binds the +machine-readable evidence, versions, limitations, and event chain.” ## 2:30–2:45 — Build truth and close diff --git a/docs/DEVPOST_COPY.md b/docs/DEVPOST_COPY.md index 7a23e9bc..00372542 100644 --- a/docs/DEVPOST_COPY.md +++ b/docs/DEVPOST_COPY.md @@ -15,13 +15,16 @@ Ask like chat. Prove it like science. ## Submission fields - **Category:** Education +- **Submitter type:** **PENDING** owner confirmation in the official form +- **Country:** **PENDING** owner confirmation in the official form - **Primary audience:** ML learners and instructors who need to test what a notebook result actually supports - **Live judge URL:** - **License:** MIT -- **Repository URL:** **PENDING** anonymous-access verification; do not paste a - private or 404 URL -- **Public video URL:** **PENDING** capture from the final qualified release +- **Repository URL:** ; + **PENDING** anonymous-access verification +- **Public YouTube video URL:** **PENDING** capture from the final qualified + release - **Codex `/feedback` Session ID:** **PENDING** owner-supplied primary build thread ID - **Exact release receipt:** **PENDING** final source/Container/Worker @@ -79,8 +82,9 @@ learner-first, source-bound release candidate: - immutable Prediction, evidence story, Model Duel, Fair Test Builder, Experiment Theater, Boundary Hunt, visual transfer, Repair preview, and bounded completion; -- a ten-second Judge preview bound to checked-in fixed sample evidence and a - one-click integrity-verified Sample Proof Capsule; +- an automated ten-second first-viewport inspectability preview bound to + checked-in fixed sample evidence and a one-click integrity-verified Sample + Proof Capsule; this is not a learner-comprehension measurement; - recoverable reject/insufficient-evidence paths, supported-artifact gating, pre-Prediction result-leak regressions, stored proof-event hydration, and capability-link lifecycle handling; @@ -189,10 +193,11 @@ claim should be copied into the submission until a measured study exists. 1. Open and confirm the public build identifier matches the exact production receipt linked by the final release evidence. -2. Choose **Start sample** for the credential-free six-stage learner loop. In - Boundary, make one classification or reveal/skip the map before Apply unlocks. -3. Choose **Watch verified replay** to inspect the persistent read-only label and - genuine reject/repair history without a new model call. +2. Choose **Open disclosed walkthrough** for the credential-free six-stage + learner loop. In Boundary, make one classification or reveal/skip the map + before Apply unlocks. +3. Choose **Watch replay** to inspect the persistent read-only label and genuine + reject/repair history without a new model call. 4. Choose **Run live** only when Judge Mode reports deployed live authority. Upload a supported notebook, approve the exact outbound packet, and follow reconnect, transfer, repaired-copy download, and Proof Capsule export. @@ -200,9 +205,11 @@ claim should be copied into the submission until a measured study exists. schema-v7 deployment receipt for the exact commit, Worker version, Container digest, asset hashes, and smoke outcomes. If those identifiers do not agree, treat live qualification as not proven. -6. Run the repository-contained verification commands documented in `README.md` - for local reproduction. Historical replay evidence remains visibly distinct - from the exact deployed release. +6. Use the [MIT-licensed CounterLab repository](https://github.com/ALikesToCode/CounterLab) + and run the repository-contained verification commands documented in + `README.md` for local reproduction. Historical replay evidence remains + visibly distinct from the exact deployed release. If anonymous repository + access is not verified, omit these instructions from the submitted entry. ## Submission consistency rule diff --git a/docs/SCREENSHOT_PLAN.md b/docs/SCREENSHOT_PLAN.md index 685df6ad..a7fa7569 100644 --- a/docs/SCREENSHOT_PLAN.md +++ b/docs/SCREENSHOT_PLAN.md @@ -1,13 +1,52 @@ # Screenshot plan -Status: planned, not captured for the v6.1 local pass. The required -`CLOAK_CDP_ENDPOINT` was unavailable, and stock Chromium is forbidden. Do not -reuse historical screenshots as evidence of this source tree. +Status: planned, not captured for the final release. The required +`CLOAK_CDP_ENDPOINT` is unavailable. Stock Chromium may produce explicitly +labelled design-review artifacts, but it cannot qualify or supply the final +submission images. Do not reuse historical screenshots as evidence of the +frozen release. Capture the current commit through CloakBrowser at 1440 × 900 and 390 × 844. Add a 1280 × 720 overflow check even if it is not a submission image. Use real product state and omit browser chrome where the submission permits. +## Curated Devpost package + +Create exactly four primary assets plus one thumbnail from the same qualified +public tuple: + +1. `01-judge-belief-break-1440x900.png` — the primary Judge image: learner + claim, persistent Fixed sample label, one changed evaluation unit, held + controls, 98.5% familiar rows, 59.4% unseen customers, Boundary consequence, + and learner benefit in the first viewport. +2. `02-live-prediction-and-authority-1440x900.png` — supported live notebook + evidence, approved privacy packet, equal-weight models, sealed Prediction, + and truthful GPT-5.6/Codex/kernel/verifier provenance. Capture only after a + genuine live call and exact release binding. +3. `03-theater-boundary-1440x900.png` — pinned Prediction, one verified result, + controlled variables, Boundary Hunt/full-map handoff, non-colour symbols, + and the exact-value table. +4. `04-transfer-repair-proof-1440x900.png` — deterministic transfer outcome, + minimal repair preview, copied-notebook download, Reasoning Diff, and Proof + Capsule action. +5. `thumbnail-judge-belief-break.png` — a legible crop derived from asset 1; + do not introduce different values, claims, or release identity. + +Store final images under +`docs/submission-assets//`. Beside every PNG, write a +same-basename JSON sidecar containing: source commit, runner image digest, +Worker version, deployment ID, deployment receipt hash, public URL and route, +viewport, capture timestamp, browser authority, PNG SHA-256, console errors, +and failed requests. A missing or mismatched field keeps the asset out of the +submission. + +Capture desktop and mobile variants for qualification even when only the five +curated files are uploaded. The 390 × 844 Judge capture must prove that the +claim, comparison, Boundary consequence, and learner benefit remain visible +without horizontal overflow. + +## Audit coverage matrix + 1. **Question-first landing** — “What result are you trying to understand?”, the claim field, notebook attachment, sample prompt chips, support sentence, and secondary Judge Mode action. Do not reveal a result above the fold. From 62cf3c95d1d783327bec548e879ddc11664b3e8f Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 10:18:50 +0530 Subject: [PATCH 073/143] docs(progress): record deterministic and submission gates --- docs/PROGRESS.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 654960f5..065b9aa2 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -3698,3 +3698,92 @@ release qualification. - This closes the local stock-browser regression gate only. It does not close the exact-public, hosted-live, CloakBrowser, real screen-reader, deployment, or submission gates. + +## Deterministic gate and submission checkpoint — 2026-07-21T04:47:19Z + +Deadline: `2026-07-22T00:00:00Z`; **19 hours 12 minutes 41 seconds remained at +this checkpoint**. The feature branch is at committed HEAD +`1aefd208ecbf3a05c1708d1b3242864a2ae837e8`. The only worktree entry besides +this progress update is the preserved, unstaged FUSE tombstone. + +### Completed and committed + +- `9229c8e style(tests): normalize scientific UI fixtures` repairs the only two + repository-wide Prettier drifts without behavior changes; focused tests pass + **25/25** for the scientific candidate and **9/9** for the trusted renderer. +- `6a92e06 docs(readme): reconcile judge link and evidence` points the Judge URL + to `/judge` and aligns the published kernel hash and mutation counts with + `docs/ACHIEVED_METRICS.json`. +- `1aefd20 docs(submission): align fields demo and screenshot evidence` adds the + missing administrative placeholders, current Judge controls, public YouTube + field, repository/license rule, exact-tuple video gate, precise Runtime Codex + role, current completion shot, synchronized captions, and a four-image plus + thumbnail package with per-asset provenance sidecars. + +### Current exact deterministic results + +- Complete local Playwright matrix: **35 passed, 5 exact-public/live skips, + 0 failed** across **40 tests in 3 files**. +- Web Vitest: **79 files, 622/622 passed**. +- Root Vitest: **70 files, 716 passed, 3 failed, 2 skipped**. All three failures + are the documented stale scientific-release bindings; no other test failed. +- Kernel and runner Pytest: **304/304 passed**. Release tooling Pytest: + **27/27 passed**. +- Repository, web client, Worker, and E2E strict TypeScript: **passed**. Worker + runtime types regenerated successfully through Wrangler 4.110.0. +- Production build: **passed**. Main client **447.43 kB / 128.75 kB gzip**, + CSS **200.63 kB / 34.69 kB gzip**, Worker + **1,863.39 kB / 359.59 kB gzip**. +- Critical mutations: leakage **14/14**, imbalance **19/19**. Tracked achieved + metrics check passed for its published **14/14** and **15/15** benchmark + scopes. +- Held-out intake/routing **10/10**; fixed full-loop completion **7/8**; tracked + evidence matched. Patch replay passed with zero group overlap. +- Sample Boundary and Sample Proof Capsule deterministic checks: **passed**. + Contract schemas and release-capability manifest regenerated with no drift. +- Repository secret scan: **passed across 1,450 files**. Built client/Worker + secret scan: **passed across 31 files**. Repository-wide Prettier and Git + whitespace: **passed**. +- Independent P0/P1 source re-audit: **13 focused files, 359/359 tests passed**. + +### Explicit red and blocked gates + +- Scientific binding check remains red across 10 generated binding files; + registry verification reports 14 stale source/vulnerability findings after + current source changes. These must be regenerated once from the frozen exact + image; no test or hash was weakened. +- Contained Wrangler `whoami --json` returned `{"loggedIn":false}`. No + repository-contained Cloudflare token or account ID is present. Global home + login state is outside the permitted filesystem boundary and was not read. +- The real aggregate-cgroup sentinel remains constitutionally blocked because + its required `/sys/fs/cgroup` and owned `/proc//stat` reads are outside + the repository-only boundary. The preserved FUSE entry also keeps the main + physical worktree unclean. +- CloakBrowser remains unavailable; qualifying exact-public and real hosted-live + journeys remain unexecuted. Learner impact remains `NO_DATA`. + +### Ordered work left + +1. Finish the two remaining locally actionable source gaps: render named + verified Sample proof details from validated Capsule fields (`MB-005`) and + add a concise learner-audience/differentiation sentence to Landing + (`MB-014`), with focused tests and separate commits. +2. Add a fail-closed submission-package metadata validator and mark the old + submission-readiness audit clearly historical. Verify anonymous repository + access without claiming it prematurely. +3. Resolve repository-contained Wrangler authentication and the clean-source / + real-sentinel authority blockers. +4. Freeze exact source `S`; build one runner image; execute genuine containment + and negative controls; regenerate SBOM, VEX, scientific evidence, Sample + Capsule, and evidence commit `E`; then rerun the complete release gate. +5. Deploy only the qualified source/image tuple. Record exact Worker version, + deployment ID, Container digest, migrations, public identity, and complete + sample/replay/live production smoke. +6. Run every public desktop/mobile, accessibility, reconnect, download, + console/network, and Web Vitals journey through CloakBrowser. Capture the + five provenance-bound submission assets from that exact tuple. +7. Keep learner evidence `NO_DATA` unless consented observations occur; obtain + the public video, `/feedback` ID, Devpost slug and `submitted_at`, and verify + every link logged out. +8. Push and verify the feature branch, then fast-forward-only merge and push + `main` after every ship gate above is factual. No push or merge has occurred. From e4f723b78e397e6b5d1012dc9993c15c68b463dc Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 10:23:48 +0530 Subject: [PATCH 074/143] feat(learner): clarify landing audience and method --- apps/web/e2e/judged-flow.spec.ts | 4 ++++ apps/web/src/App.test.tsx | 5 +++++ apps/web/src/App.tsx | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/web/e2e/judged-flow.spec.ts b/apps/web/e2e/judged-flow.spec.ts index d063e16f..ecab0fbf 100644 --- a/apps/web/e2e/judged-flow.spec.ts +++ b/apps/web/e2e/judged-flow.spec.ts @@ -1349,6 +1349,9 @@ for (const viewport of beliefBreakViewports) { const artifactSafetySummary = page.getByText( /Ask a question or attach a supported notebook.*never runs its cells/i, ); + const audienceDifference = page.getByText( + /Built for ML learners: seal a Prediction, then let one controlled test answer—not AI prose/i, + ); const composer = page.getByLabel("Your question or claim"); const submit = page.getByRole("button", { name: /Test this claim/i }); const promptStarters = page.getByRole("group", { @@ -1361,6 +1364,7 @@ for (const viewport of beliefBreakViewports) { [ heading, artifactSafetySummary, + audienceDifference, composer, submit, promptStarters, diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index 81090245..d5ca14e3 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -1112,6 +1112,11 @@ describe("CounterLab judged flow", () => { /ask a question or attach a supported notebook.*reads the evidence and never runs its cells/i, ), ).toBeInTheDocument(); + expect( + screen.getByText( + /built for ml learners: seal a prediction, then let one controlled test answer—not ai prose/i, + ), + ).toBeInTheDocument(); expect( screen.getByRole("link", { name: /skip to main content/i }), ).toHaveAttribute("href", "#main-content"); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 90df385d..808af02c 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1105,7 +1105,9 @@ function Landing({

Ask a question or attach a supported notebook. CounterLab reads - the evidence and never runs its cells. + the evidence and never runs its cells. Built for ML learners: + seal a Prediction, then let one controlled test answer—not AI + prose.

From 9441166c2ddde9e74210c46db4c3415386aa9033 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 10:34:01 +0530 Subject: [PATCH 075/143] feat(proof): expose validated session evidence --- apps/web/src/app/CounterLabStudio.test.tsx | 78 +++++++- .../src/components/studio/ProofConsole.tsx | 183 +++++++++++++++++- apps/web/src/styles/studio.css | 52 +++++ 3 files changed, 310 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/CounterLabStudio.test.tsx b/apps/web/src/app/CounterLabStudio.test.tsx index 09df3baa..a6caf122 100644 --- a/apps/web/src/app/CounterLabStudio.test.tsx +++ b/apps/web/src/app/CounterLabStudio.test.tsx @@ -1,9 +1,10 @@ import { fireEvent, render, screen, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; +import type { StudioContext } from "../components/studio/types"; import { CounterLabStudio } from "./CounterLabStudio"; -const context = { +const context: StudioContext = { mode: "live" as const, stage: "live-compile" as const, artifact: null, @@ -11,6 +12,44 @@ const context = { events: [], }; +const sampleProofBundle = { + schemaVersion: "1", + experimentPlan: { + concept: "entity_leakage", + runs: [ + { id: "random_row_split" }, + { id: "customer_group_split" }, + { id: "identity_ablation" }, + ], + }, + generatedAdapter: { + sha256: "a".repeat(64), + commitHash: "b".repeat(40), + }, + publicTests: { + passed: 1, + failed: 0, + command: "python /workspace/public_tests.py", + reportHash: "c".repeat(64), + }, + externalVerifier: { + status: "VERIFIED", + verifiedInvariants: ["baseline_overlap_exists", "canonical_result_hash"], + mutations: ["swapped split labels"], + reportHash: "d".repeat(64), + }, + patchResult: { + status: "VERIFIED", + modifiedCells: [3], + diff: "--- cell-3-before.py\n+++ cell-3-after.py", + verification: { + passed: true, + invariants: ["EVALUATION_CELL_ONLY", "GROUP_OVERLAP_ZERO"], + unchangedCellHashes: ["e".repeat(64)], + }, + }, +} as unknown as NonNullable["proofBundle"]; + describe("CounterLabStudio", () => { it("closes proof after compile when it was opened automatically", () => { const actions = { @@ -184,6 +223,43 @@ describe("CounterLabStudio", () => { ); }); + it("shows validated sample proof details in their evidence tabs", () => { + render( + , + }} + actions={{ + newAnalysis: vi.fn(), + showEvidence: vi.fn(), + startOver: vi.fn(), + openRecent: vi.fn(), + }} + > +
Completed sample
+
, + ); + + fireEvent.click(screen.getByRole("tab", { name: "Tests" })); + expect(screen.getByText("1 passed · 0 failed")).toBeInTheDocument(); + expect(screen.getByText("Public test report SHA-256")).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: "Verifier" })); + expect(screen.getByText("baseline_overlap_exists")).toBeInTheDocument(); + expect(screen.getByText("canonical_result_hash")).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: "Diff" })); + expect(screen.getByText("Changed notebook cells: 3")).toBeInTheDocument(); + expect(screen.getByText(/cell-3-before\.py/iu)).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: "Plan" })); + expect(screen.getByText("customer_group_split")).toBeInTheDocument(); + expect(screen.getByText("Generated adapter SHA-256")).toBeInTheDocument(); + }); + it("exposes private-session revocation only on demand in provenance", () => { const revokeSessionAccess = vi.fn(); render( diff --git a/apps/web/src/components/studio/ProofConsole.tsx b/apps/web/src/components/studio/ProofConsole.tsx index 05e23f5d..f2c41d83 100644 --- a/apps/web/src/components/studio/ProofConsole.tsx +++ b/apps/web/src/components/studio/ProofConsole.tsx @@ -1,6 +1,10 @@ import type { KeyboardEvent as ReactKeyboardEvent } from "react"; -import type { EvidenceEvent, PublicCompilerEvent } from "../../api"; +import type { + EvidenceEvent, + ProofBundle, + PublicCompilerEvent, +} from "../../api"; import { CapabilityLinkDisclosure } from "../learner/CapabilityLinkDisclosure"; import { GeneratedProofView } from "./GeneratedProofView"; import { ProvenanceLedger } from "./ProvenanceLedger"; @@ -133,6 +137,171 @@ function EvidenceEventList({ events }: { events: readonly EvidenceEvent[] }) { ); } +function proofBundleHasTab( + proofBundle: ProofBundle | undefined, + tab: ProofTab, +): boolean { + if (proofBundle === undefined) return false; + if (tab === "Plan" || tab === "Diff" || tab === "Verifier") return true; + return tab === "Tests" && proofBundle.schemaVersion === "1"; +} + +function ProofBundleEvidence({ + proofBundle, + tab, +}: { + proofBundle: ProofBundle; + tab: ProofTab; +}) { + if (tab === "Plan") { + const operationIds = + proofBundle.schemaVersion === "1" + ? proofBundle.experimentPlan.runs.map((run) => run.id) + : [ + proofBundle.experimentPlan.baseline.runId, + ...proofBundle.experimentPlan.interventions.map((run) => run.runId), + ]; + return ( +
+

Validated plan

+

+ Registered run identifiers from the session's validated Proof + Bundle. +

+
    + {operationIds.map((operationId) => ( +
  • + {operationId} +
  • + ))} +
+ {proofBundle.schemaVersion === "1" ? ( +
+
+
Generated adapter SHA-256
+
+ {proofBundle.generatedAdapter.sha256} +
+
+
+
Recorded source commit
+
+ {proofBundle.generatedAdapter.commitHash} +
+
+
+ ) : ( +

+ Plan verifier: {proofBundle.planVerification.status} ·{" "} + {proofBundle.planVerification.invariantCount} invariants +

+ )} +
+ ); + } + + if (tab === "Diff") { + return ( +
+

Verified patch

+

+ Changed notebook cells:{" "} + {proofBundle.patchResult.modifiedCells.join(", ")} +

+

+ Patch verification: {proofBundle.patchResult.status} ·{" "} + {proofBundle.patchResult.verification.invariants.length} named + invariants +

+
+          {proofBundle.patchResult.diff}
+        
+
+ ); + } + + if (tab === "Tests" && proofBundle.schemaVersion === "1") { + return ( +
+

Public tests

+

+ {proofBundle.publicTests.passed} passed ·{" "} + {proofBundle.publicTests.failed} failed +

+
+
+
Public test report SHA-256
+
+ {proofBundle.publicTests.reportHash} +
+
+
+
+ ); + } + + if (tab === "Verifier") { + if (proofBundle.schemaVersion === "1") { + return ( +
+

External verifier · {proofBundle.externalVerifier.status}

+

Accepted named invariants

+
    + {proofBundle.externalVerifier.verifiedInvariants.map( + (invariant) => ( +
  • + {invariant} +
  • + ), + )} +
+

+ Mutation probes recorded:{" "} + {proofBundle.externalVerifier.mutations.length} +

+
+
+
External verifier report SHA-256
+
+ {proofBundle.externalVerifier.reportHash} +
+
+
+
+ ); + } + const reports = [ + ["Experiment plan", proofBundle.planVerification], + ["Patch plan", proofBundle.patchPlanVerification], + ] as const; + return ( +
+

Verifier reports

+ {reports.map(([label, report]) => ( +
+

+ {label} · {report.status} +

+
    + {report.invariants.map((invariant) => ( +
  • + {invariant.name} ·{" "} + {invariant.passed ? "passed" : "rejected"} +
  • + ))} +
+
+ ))} +
+ ); + } + + return null; +} + function CompilerEventList({ events, tab, @@ -184,6 +353,7 @@ export function ProofConsole({ const evidenceEvents = context.evidenceEvents ?? []; const proofEventIssues = context.proofEventIssues ?? []; const proofEventStatus = context.proofEventStatus ?? "idle"; + const proofBundle = context.session?.proofBundle; const filteredCompilerEvents = compilerEventsForTab( context.events, activeTab, @@ -193,7 +363,9 @@ export function ProofConsole({ activeTab, ); const hasTabEvents = - filteredEvidenceEvents.length > 0 || filteredCompilerEvents.length > 0; + filteredEvidenceEvents.length > 0 || + filteredCompilerEvents.length > 0 || + proofBundleHasTab(proofBundle, activeTab); const moveTab = ( event: ReactKeyboardEvent, index: number, @@ -323,6 +495,13 @@ export function ProofConsole({ ) : ( <> + {proofBundle !== undefined && + proofBundleHasTab(proofBundle, activeTab) ? ( + + ) : null} {filteredEvidenceEvents.length > 0 ? ( ) : null} diff --git a/apps/web/src/styles/studio.css b/apps/web/src/styles/studio.css index 62a5842f..960df7fe 100644 --- a/apps/web/src/styles/studio.css +++ b/apps/web/src/styles/studio.css @@ -506,6 +506,53 @@ kbd { padding: 12px 16px; } +.proof-bundle-evidence { + display: grid; + gap: 0.65rem; + margin-bottom: 1rem; + padding: 0.85rem; + border: 1px solid var(--line); + border-radius: 0.8rem; + background: var(--paper-raised); +} + +.proof-bundle-evidence h3, +.proof-bundle-evidence h4, +.proof-bundle-evidence p, +.proof-bundle-evidence ul, +.proof-bundle-evidence dl { + margin: 0; +} + +.proof-bundle-evidence ul { + padding-inline-start: 1.25rem; +} + +.proof-bundle-evidence dl { + display: grid; + gap: 0.45rem; +} + +.proof-bundle-evidence dl > div { + display: grid; + grid-template-columns: minmax(150px, 0.35fr) minmax(0, 1fr); + gap: 0.75rem; +} + +.proof-bundle-evidence code { + overflow-wrap: anywhere; +} + +.proof-bundle-evidence pre { + max-height: 14rem; + margin: 0; + overflow: auto; + padding: 0.75rem; + border-radius: 0.55rem; + background: var(--paper); + white-space: pre-wrap; +} + .generated-proof-sequence { display: grid; gap: 0.75rem; @@ -859,6 +906,11 @@ kbd { height: 236px; } + .proof-bundle-evidence dl > div { + grid-template-columns: 1fr; + gap: 0.2rem; + } + .command-palette footer span { display: none; } From 783581f01b7f7c030320bed09653be778d81a2e8 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan <22f3001492@ds.study.iitm.ac.in> Date: Tue, 21 Jul 2026 10:37:22 +0530 Subject: [PATCH 076/143] fix(judge): gate inaccessible source instructions --- apps/web/src/features/judge/JudgeModeView.module.css | 9 ++++----- apps/web/src/features/judge/JudgeModeView.test.tsx | 9 +++++++++ apps/web/src/features/judge/JudgeModeView.tsx | 12 +++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/web/src/features/judge/JudgeModeView.module.css b/apps/web/src/features/judge/JudgeModeView.module.css index 4e6d24b4..204c80bb 100644 --- a/apps/web/src/features/judge/JudgeModeView.module.css +++ b/apps/web/src/features/judge/JudgeModeView.module.css @@ -676,16 +676,15 @@ align-items: end; } -.reproduceSection pre { +.reproductionGate { max-width: 100%; - overflow-x: auto; margin: 0; padding: 28px; - color: var(--aqua-bright); + color: var(--judge-panel-ink); background: var(--judge-panel); border-radius: 2px; - font-size: 13px; - line-height: 1.9; + font-size: 15px; + line-height: 1.65; } .footer { diff --git a/apps/web/src/features/judge/JudgeModeView.test.tsx b/apps/web/src/features/judge/JudgeModeView.test.tsx index 11a8a6f4..ea34c280 100644 --- a/apps/web/src/features/judge/JudgeModeView.test.tsx +++ b/apps/web/src/features/judge/JudgeModeView.test.tsx @@ -219,6 +219,15 @@ describe("JudgeModeView", () => { expect(document.body).toHaveTextContent( "11111111-2222-3333-4444-555555555555", ); + expect( + screen.getByRole("heading", { name: /source reproduction is gated/i }), + ).toBeInTheDocument(); + expect(document.body).toHaveTextContent( + /commands are withheld until the repository and MIT license are anonymously accessible/i, + ); + expect( + screen.queryByLabelText("CounterLab reproduction commands"), + ).not.toBeInTheDocument(); }); it("does not offer live authority for partial generation read isolation", () => { diff --git a/apps/web/src/features/judge/JudgeModeView.tsx b/apps/web/src/features/judge/JudgeModeView.tsx index ad16cf62..57079e3e 100644 --- a/apps/web/src/features/judge/JudgeModeView.tsx +++ b/apps/web/src/features/judge/JudgeModeView.tsx @@ -529,12 +529,14 @@ export function JudgeModeView({ aria-labelledby="reproduce-title" >
-

Reproduce locally

-

The demo has commands, not hand-waving.

+

Source & license

+

Source reproduction is gated.

-
-          {`./scripts/test-all.sh\n./scripts/run-mutations.sh leakage\n./scripts/reproduce-session.sh leakage-01\n./scripts/replay-patch.sh leakage-01`}
-        
+

+ Local reproduction commands are withheld until the repository and MIT + License are anonymously accessible. CounterLab will not present + unactionable source instructions as public evidence. +