From bf88d18f0dd507c5dba8508d5fb8024ea9424c2f Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Sun, 23 Aug 2026 11:29:20 -0400 Subject: [PATCH] HAC-341: the root URL served a blank page, and prepare the cold-read kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## `/` and `/cockpit` rendered nothing `vercel.json` rewrites `/` and `/cockpit` onto `media/hac-341/cockpit.html`. A rewrite serves those bytes without changing the request URL, and a document-relative specifier resolves against the *request* URL — so import { armView } from './lib/arm-view.mjs' resolved to `/lib/arm-view.mjs` and aborted. Same for `./lib/guide.mjs` and the `./evidence/view-model.json` fetch. Both entry points returned HTTP 200 and rendered a blank page with the body text "Skip to the run". Confirmed against the live deployment: `/media/hac-341/cockpit.html` renders 1628 characters, `/` and `/cockpit` render 15. These are the two URLs a judge visits. #27 made `/` stop 404ing; it did not make it work. Nothing caught it because every gate loads the file path directly, which is the one path where the relative form happens to resolve. The three references are now anchored at `/media/hac-341/`, and the cockpit gate refuses a document-relative import or fetch statically — no server, no deployment, and it fails on the specifier rather than on the blank page. One negative proof. ## Cold-read kit `media/hac-341/cold-read/` — moderator script and per-reader response sheet. Status is READY_FOR_HUMAN_TEST: prepared, not run, nothing claimed. It names the public custom domain, because every `*.vercel.app` URL for this project is behind Vercel SSO (`ssoProtection: all_except_custom_domains`) and would send a cold reader to a login page. All six tester URLs verified 200 logged out. The sheet records verbatim answers rather than scores, and lists the automatic failures — inferring Agent Runtime or Agent Gateway, collapsing the two proof classes into one experiment, reading ALLOW as authorized or EXECUTED as OBSERVED — as overriding any score. pnpm run check exit 0, 645 tests, typecheck clean. --- media/hac-335/evidence/capture-manifest.json | 4 +- media/hac-341/README.md | 5 + media/hac-341/bin/verify-cockpit.mjs | 21 +++- media/hac-341/cockpit.html | 6 +- media/hac-341/cold-read/README.md | 100 +++++++++++++++ media/hac-341/cold-read/response-sheet.md | 125 +++++++++++++++++++ test/hac-341-identity-gates.test.mjs | 16 ++- 7 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 media/hac-341/cold-read/README.md create mode 100644 media/hac-341/cold-read/response-sheet.md diff --git a/media/hac-335/evidence/capture-manifest.json b/media/hac-335/evidence/capture-manifest.json index f31b9ce..a9a7f37 100644 --- a/media/hac-335/evidence/capture-manifest.json +++ b/media/hac-335/evidence/capture-manifest.json @@ -2,11 +2,11 @@ "manifestId": "HAC-335-capture-manifest", "issue": "HAC-335", "generator": "media/hac-335/bin/capture-cockpit.mjs", - "capturedFromSha": "7d62f6fe169cf354a548447dae0ce1ea7e210a08", + "capturedFromSha": "07e1a93f8d575b7bb03db1195b4a26594a8aad32", "capturedSurface": "media/hac-341/cockpit.html (merged executable surface)", "servedFrom": "repository root — the cockpit resolves shared identity from /assets", "viewport": "1440x900", - "captureSourceDigest": "24924971fd814b5f1baa460e420ebe3f9c9c9d09ec125ee616354f01d557819a", + "captureSourceDigest": "718d9c9e3f80a1633093df74131787509910fae14fac2ed6126cd9008c9d7879", "captureSourceFiles": [ "assets/fonts/geist-mono-variable.woff2", "assets/fonts/geist-variable.woff2", diff --git a/media/hac-341/README.md b/media/hac-341/README.md index 83d044f..cb21650 100644 --- a/media/hac-341/README.md +++ b/media/hac-341/README.md @@ -664,6 +664,11 @@ The issue's acceptance requires real human falsification. **No human cold-read has been performed and none is claimed.** The protocol below is prepared; the results are not. +The runnable kit is [`cold-read/`](./cold-read/) — a moderator script to read +verbatim and a per-reader response sheet. Use the **public custom domain** +`https://interlock.marcellelabs.io`; every `*.vercel.app` URL is behind Vercel +SSO and will send a cold reader to a login page. + Deterministic URLs for testers: ``` diff --git a/media/hac-341/bin/verify-cockpit.mjs b/media/hac-341/bin/verify-cockpit.mjs index b169146..9f705ae 100644 --- a/media/hac-341/bin/verify-cockpit.mjs +++ b/media/hac-341/bin/verify-cockpit.mjs @@ -205,7 +205,9 @@ if (defaultArm && perturbedArm) { } // The cockpit must consume that derivation rather than re-deriving it inline. -if (!/from '\.\/lib\/arm-view\.mjs'/.test(cockpit)) { +// Matched on the module, not the specifier prefix, so the rewrite fix below +// does not read as the cockpit having dropped the import. +if (!/from '\/media\/hac-341\/lib\/arm-view\.mjs'/.test(cockpit)) { fail('cockpit does not consume the shared arm-view derivation'); } if (/environmentEvidence\[0\][^\n]*basisRevision|basisRevision[^\n]*environmentEvidence\[0\]/.test(cockpit)) { @@ -1067,6 +1069,23 @@ if (!/aria-pressed/.test(cockpit)) fail('toggle state is not exposed to assistiv if (!/aria-live/.test(cockpit)) fail('state changes are not announced'); if (!/data-glyph/.test(cockpit)) fail('state is encoded by colour alone; no glyph channel'); +/* Every module import and evidence fetch resolves from the site root. + * + * `vercel.json` rewrites `/` and `/cockpit` onto this file. A rewrite serves + * these bytes without changing the request URL, and a document-relative + * specifier resolves against the *request* URL — so `./lib/guide.mjs` became + * `/lib/guide.mjs` and aborted, and the two URLs a judge actually visits + * rendered a blank page while still returning HTTP 200. + * + * The visual gate could not see it: it loads `/media/hac-341/cockpit.html` + * directly, which is the one path where the relative form happens to work. + * This is a static check for that reason — it needs no server and no + * deployment, and it fails on the specifier rather than on the symptom. + */ +for (const m of cockpit.matchAll(/(?:^|\s)(?:import\b[^;]*?from|await\s+fetch\(|import\()\s*['"](\.[^'"]*)['"]/g)) { + fail(`document-relative reference "${m[1]}" breaks under the vercel.json rewrite; anchor it at /media/hac-341/`); +} + const checksLabel = local.checks.label; if (errors.length) { process.stderr.write(`HAC-341 cockpit contract violated:\n${errors.map((e) => ` - ${e}`).join('\n')}\n`); diff --git a/media/hac-341/cockpit.html b/media/hac-341/cockpit.html index 3a528d0..fca3bab 100644 --- a/media/hac-341/cockpit.html +++ b/media/hac-341/cockpit.html @@ -704,14 +704,14 @@ from frozen evidence. A field that is absent there is absent here. What the *selected arm* shows is derived in lib/arm-view.mjs, so the binding between an arm and its evidence can be asserted without a browser. */ -import { armView } from './lib/arm-view.mjs'; +import { armView } from '/media/hac-341/lib/arm-view.mjs'; /* The guided layer's derivation. It lives beside arm-view.mjs for the same reason: step 5 claims that four things were held constant and four changed, and a claim like that has to be checked against the frozen arms rather than typed into markup. `ablationDelta` reads both arms and reports what actually moved, so a marker that has stopped being true fails the gate. */ import { guideView, guideRoute, stateOfStep, stepOfState, GUIDE_STEPS, - GUIDE_FREE_STATE } from './lib/guide.mjs'; + GUIDE_FREE_STATE } from '/media/hac-341/lib/guide.mjs'; const app = document.getElementById('app'); const drawer = document.getElementById('drawer'); @@ -1717,7 +1717,7 @@

${esc(title)}

addEventListener('popstate', () => { closeDrawer(); resolve(); }); try { - const res = await fetch('./evidence/view-model.json', { cache: 'no-store' }); + const res = await fetch('/media/hac-341/evidence/view-model.json', { cache: 'no-store' }); if (!res.ok) throw new Error(String(res.status)); MODEL = await res.json(); resolve(); diff --git a/media/hac-341/cold-read/README.md b/media/hac-341/cold-read/README.md new file mode 100644 index 0000000..61ba2a2 --- /dev/null +++ b/media/hac-341/cold-read/README.md @@ -0,0 +1,100 @@ +# HAC-341 cold read — moderator kit + +**Status: READY_FOR_HUMAN_TEST. Not run. No result is claimed.** + +Three unfamiliar readers, tested one at a time, no narration. This kit is the +whole protocol: the URLs, what you say, and where the answers go. Budget 15 +minutes per reader. + +A reader is *unfamiliar* if they have not seen the cockpit, the README, the +Devpost copy or the video, and have not had Interlock explained to them. A +colleague who has heard the pitch is not a cold reader. + +## Before you start + +Open [`response-sheet.md`](./response-sheet.md), one copy per reader. + +Check the surface is live and public — in a **logged-out / private window**: + +``` +https://interlock.marcellelabs.io/cockpit?run=hac330-local&proof=local&state=run.local.treatment +``` + +If that redirects to a login page, stop: you are on a protected URL. The +`*.vercel.app` deployment URLs are SSO-protected and **must not be used for the +cold read**. Only the custom domain is public. + +## The URLs + +| Purpose | URL | +| --- | --- | +| L1 default (start here) | `https://interlock.marcellelabs.io/cockpit?run=hac330-local&proof=local&state=run.local.treatment` | +| Baseline arm | `…/cockpit?run=hac330-local&proof=local&state=run.local.baseline` | +| Perturbed arm | `…/cockpit?run=hac330-local&proof=local&state=run.local.perturbed` | +| Cloud overview | `…/cockpit?run=hac340-cloud&proof=cloud&state=run.cloud.overview` | +| Reduced motion / static | append `&static=1` to any of the above | +| Missing run (must show unavailable, never substitute) | `…/cockpit?run=nope&proof=local&state=run.local.treatment` | + +## What you say + +Read these aloud verbatim. **Do not explain anything, do not answer questions, +do not point at the screen.** If a reader asks what something means, say: *"Tell +me what you think it means."* + +> I'm going to show you a web page. I'm not going to tell you what it is. I'll +> ask you a few questions as you look at it. There are no wrong answers — I'm +> testing the page, not you. Please think out loud. + +Open the L1 default URL. Start the timer. + +1. **At 5 seconds** — cover the screen. *"What problem is this page about?"* +2. **At 30 seconds** — *"What changed, and why did it change?"* +3. **At 60 seconds** — *"If you didn't believe this, how would you check it + yourself?"* +4. **Switch to the cloud overview URL. At 120 seconds** — *"Is what you're + looking at now the same experiment as before, or a different one?"* +5. **Open-ended** — *"Is there anything here you think the page is claiming that + it hasn't actually shown you?"* + +Then repeat steps 1–4 with `&static=1` and **keyboard only** (no mouse). The +semantic answers must match; presentation may differ. + +## Targets + +| # | Question | Target | +| --- | --- | --- | +| 1 | Identifies the core problem at 5 s | **3/3** | +| 2 | Explains the causal delta at 30 s | **3/3** | +| 3 | Finds verifiable evidence by 60 s | **≥2/3** | +| 4 | Distinguishes local proof from Google Cloud participation by 120 s | **≥2/3** | + +## Automatic failures + +These override every score above. If any occurs, the cold read **fails** +regardless of questions 1–4: + +- a reader infers that **Agent Runtime** or **Agent Gateway** participated; +- a reader believes the local experiment and the cloud run are **one combined + experiment**, or that the cloud run reproduced the 140/120 counterfactual; +- a reader reads `ALLOW` as *verified*, *authorized* or *safe*, or reads + `EXECUTED` as `OBSERVED`; +- a reader concludes Interlock is **universally** safe, production-ready, or + that it prevents collisions in general; +- the keyboard/reduced-motion pass yields a **different semantic answer**. + +## Recording the result + +Write **verbatim answers** on the response sheet. Do not summarise into a score +without them — the wording is the finding. A reader who says "it stops two +robots fighting over the same file" has understood something different from one +who says "it decides whether two jobs can run at once", and only the verbatim +text preserves that. + +Only comprehension failures demonstrated by this protocol justify further UX +change. A reader being slow is data; a moderator's hunch is not. + +## After the run + +Record the outcome on HAC-341 with the verbatim sheets attached. Until three +real readers have completed this, HAC-341 stays open and every surface that +mentions the cold read must continue to say it has not been run. diff --git a/media/hac-341/cold-read/response-sheet.md b/media/hac-341/cold-read/response-sheet.md new file mode 100644 index 0000000..d197c7f --- /dev/null +++ b/media/hac-341/cold-read/response-sheet.md @@ -0,0 +1,125 @@ +# HAC-341 cold read — response sheet + +One copy per reader. Write what they said, not what they meant. + +``` +Reader #: ______ Date: ____________ Moderator: ____________ +Surface: https://interlock.marcellelabs.io/cockpit commit: ____________ +Browser / OS: ____________________ Logged out / private window? Y / N +Unfamiliar with Interlock (has not seen cockpit, README, Devpost or video)? Y / N +``` + +--- + +## Pass 1 — default, mouse allowed + +**Q1 · 5 s — "What problem is this page about?"** + +``` +verbatim: + + + +``` +Identified the core problem? **Y / N** + +--- + +**Q2 · 30 s — "What changed, and why did it change?"** + +``` +verbatim: + + + +``` +Explained the causal delta? **Y / N** + +--- + +**Q3 · 60 s — "How would you check it yourself?"** + +``` +verbatim: + + + +``` +Located verifiable evidence? **Y / N** + +--- + +**Q4 · 120 s, after switching to the cloud URL — "Same experiment, or different?"** + +``` +verbatim: + + + +``` +Distinguished the two proof classes? **Y / N** + +--- + +**Q5 · open — "Anything the page claims but hasn't shown you?"** + +``` +verbatim: + + + +``` + +--- + +## Pass 2 — `&static=1`, keyboard only + +``` +Q1 verbatim: + +Q2 verbatim: + +Q3 verbatim: + +Q4 verbatim: +``` + +Semantic answers match pass 1? **Y / N** +If N, which diverged and how: + +``` + +``` + +--- + +## Automatic failures — tick any that occurred + +``` +[ ] inferred Agent Runtime participated +[ ] inferred Agent Gateway participated +[ ] believed local + cloud are one combined experiment +[ ] believed the cloud run reproduced the 140/120 counterfactual +[ ] read ALLOW as verified / authorized / safe +[ ] read EXECUTED as OBSERVED +[ ] concluded Interlock is universally safe / production-ready +[ ] concluded Interlock prevents collisions in general +[ ] keyboard or reduced-motion pass gave a different semantic answer +``` + +Any tick = this reader is a **FAIL**, whatever the Y/N answers above. + +--- + +## Moderator notes + +Where did they look first? Where did they get stuck? What did they scroll past? + +``` + + + +``` + +Did the moderator explain, prompt, or point at any stage? **Y / N** +(If Y, this reader's result is void — the read was not cold.) diff --git a/test/hac-341-identity-gates.test.mjs b/test/hac-341-identity-gates.test.mjs index 0286228..1d9f014 100644 --- a/test/hac-341-identity-gates.test.mjs +++ b/test/hac-341-identity-gates.test.mjs @@ -329,12 +329,26 @@ describe('the selected arm drives its own evidence', () => { it('fails when the cockpit stops consuming the shared derivation', () => { const r = perturbed((a) => { - a.edit('media/hac-341/cockpit.html', "import { armView } from './lib/arm-view.mjs';", ''); + a.edit('media/hac-341/cockpit.html', "import { armView } from '/media/hac-341/lib/arm-view.mjs';", ''); }, gate); expect(r.code).not.toBe(0); expect(r.out).toMatch(/does not consume the shared arm-view derivation/); }); + it('fails when a reference goes document-relative and breaks the rewrite', () => { + // vercel.json rewrites `/` and `/cockpit` onto cockpit.html without + // changing the request URL, so a `./` specifier resolves against the + // request and 404s. The page still returns 200 and renders blank, and the + // visual gate cannot see it because it loads the file path directly. + const r = perturbed((a) => { + a.edit('media/hac-341/cockpit.html', + "await fetch('/media/hac-341/evidence/view-model.json'", + "await fetch('./evidence/view-model.json'"); + }, gate); + expect(r.code).not.toBe(0); + expect(r.out).toMatch(/document-relative reference .* breaks under the vercel\.json rewrite/); + }); + it('fails when the cockpit reads a basis off the environment again', () => { const r = perturbed((a) => { a.edit('media/hac-341/cockpit.html', 'const short =',