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 =',