fix: require passing evidence for supported claims - #371
Merged
Conversation
abrichr
force-pushed
the
codex/claims-passing-evidence
branch
from
August 18, 2026 19:57
0da7b34 to
bf7ccd5
Compare
abrichr
marked this pull request as ready for review
August 18, 2026 20:29
This was referenced Aug 18, 2026
abrichr
added a commit
that referenced
this pull request
Aug 18, 2026
…after rebase The claims gate from PR #371 emits ci_job for each cited evidence node, and the browser attach claim entry is new. Regenerate the derived report and the reviewed public artifact inventory so both match the registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 19, 2026
…after rebase The claims gate from PR #371 emits ci_job for each cited evidence node, and the browser attach claim entry is new. Regenerate the derived report and the reviewed public artifact inventory so both match the registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 19, 2026
…after rebase The claims gate from PR #371 emits ci_job for each cited evidence node, and the browser attach claim entry is new. Regenerate the derived report and the reviewed public artifact inventory so both match the registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 19, 2026
…after rebase The claims gate from PR #371 emits ci_job for each cited evidence node, and the browser attach claim entry is new. Regenerate the derived report and the reviewed public artifact inventory so both match the registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 19, 2026
…after rebase The claims gate from PR #371 emits ci_job for each cited evidence node, and the browser attach claim entry is new. Regenerate the derived report and the reviewed public artifact inventory so both match the registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 19, 2026
…after rebase The claims gate from PR #371 emits ci_job for each cited evidence node, and the browser attach claim entry is new. Regenerate the derived report and the reviewed public artifact inventory so both match the registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 20, 2026
* feat(browser): attach recorder to existing sessions * docs: align generated verification timestamp * fix(browser): harden attached recording boundaries * fix(browser): harden attached recording finalization * fix(browser): close attach evidence gaps * fix(browser): latch attached recording races * fix(browser): close attached finalization gaps * fix(browser): bind same-task secret mutations * fix(browser): retain masks through final capture * docs: keep generated verification report in sync * Harden browser attach secret boundaries * Refresh browser attach verification artifacts * Use deterministic claims report timestamp * fix(browser): make secret classification sticky across DOM replacement Close the four reproduced source-time secret leaks the independent review found in the attach recorder, plus two adjacent races, and repair the finalization lifecycle latch that the fix exposed. Secret boundaries: - A page can replace a declared secret field before its first input event. Attribute records are now applied ahead of rewrite matching, and the removed node's sticky state transfers to its single proven replacement. - A field can lose its name/id while typing. Classification is sticky to the DOM element and its input session; an attribute change cannot remove secrecy. - A multi-node rewrite has no proven field mapping. Every candidate is masked AND its first input refuses the recording, instead of assigning one removed field's identity (and input session) to all of them. - A cached field label can reproduce the current secret value. Trusted labels are scrubbed against that field's live value. - ARIA textboxes read innerText/textContent, so their values enter the scrub set; printable keydowns on a bound secret field no longer emit hotkeys; native non-text controls no longer trigger a false refusal. Screenshot and CDP boundaries: - The closed-shadow inventory runs as a screenshot guard, so a root that appears during screenshot preparation is bound or refused before any bytes exist. Launched mode gets the same initial inventory. - The origin guard reads only location.origin instead of the full URL, so a reflected secret in the query string cannot cross into Python. - The privacy scan scopes its resolved nodes to one protocol object group and releases it after each scan. - Marker cleanup pierces open shadow roots, and a session-keyed cleanup handle survives a page that replaces the global recorder object. Finalization: - finish() runs the privacy guard BEFORE arming the finalization latch. The guard's page round-trips deliver lifecycle events Chromium queued during recording; arming first made an ordinary same-origin history.replaceState from the recording itself alias a post-evidence frame change and refuse every valid recording that reflects state. - _handle_frame_navigation refuses on the armed latch without another page evaluate, which would re-enter dispatch while finalizing. Tests: - Live Chromium regressions for pre-input replacement, same-task identity loss, ambiguous multi-node rewrite, label/value equality, ARIA textbox, AltGr keydown, static and late unbound closed shadow roots, and a replaced page privacy guard. - The five finalization tests use a fake privacy CDP session, since the guard now runs on the finish path. - The live trial sequence reactivates the app tab after each popup or new-tab refusal. Chromium throttles a backgrounded tab, which made the next trial's first evidence screenshot time out on a loaded runner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(claims): regenerate verification report and artifact inventory after rebase The claims gate from PR #371 emits ci_job for each cited evidence node, and the browser attach claim entry is new. Regenerate the derived report and the reviewed public artifact inventory so both match the registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browser): bind secret redaction to the element, never to keystroke prefixes The page closure retained the value of a declared secret field on EVERY input event, so it kept each intermediate keystroke prefix, then substring-replaced every retained value across later URL, title, label, selector and structural text -- iterating over the already-substituted string, so it also rewrote the inside of its own `[secret]` placeholder. An independent review reproduced three consequences in real Chromium: 1. Silent identity degradation in every mode. With `charlie1` typed into a password field, a later click on `#chart-save` recorded `{"selector": null, "name": "Save [se[secret]ret]t"}`. `uniqueSelector` returned null whenever scrubbing altered an element id, so the DOM identity tier disarmed for any element whose id/name/data-testid contained one character of the prefix. Nothing refused; the bundle compiled and looked healthy. 2. A false abort in attach mode. `emit` wrote the scrubbed `location.href` into every event and the origin guard parsed that as the origin, so on `http://host.test/app` with secret `hunter2` the first keystroke produced `"[secret]ttp://[secret]ost.test/app"` and refused the whole recording with "a browser event came from outside the declared application origin". 3. Corrupted URL evidence on disk in launched mode (`"url_after": "h[secret][secret]p://127.0.0.1:65479/"`). What changed: - Redaction is bound to the ELEMENT, not to a growing value set. The closure keeps the bound element and reads its CURRENT value at scrub time; it never retains a keystroke prefix. Values are committed only where the field can no longer take a keystroke (change, focusout, submit, pagehide), so a value stays scrubbable after the page clears the field. - Redaction makes one left-to-right pass over the original text and never re-reads its own output, so a placeholder can no longer be rewritten. - A URL is redacted one whole token at a time (path segment, query name, query value, fragment). The scheme, host and port are never rewritten. - `emit` sends `location.origin` beside the scrubbed URL and the origin guard reads that, so redaction can never refuse a valid recording. - A value too short to tell a real reflection from a coincidence FAILS CLOSED: Flow withholds the whole URL token, title, label or structural text instead of keeping a partially rewritten copy. A short secret still never persists. - Identity refusal is visible, never a bare null selector: the action carries `identity_withheld`, `meta.json` carries `identity_withheld_events`, and `record` prints how many actions carry no DOM selector and why. - The trusted field-label cache is scrubbed against every declared value, not only the field's own value. Document order meant a field bound before another declared field cached that other field's pre-filled literal, which reached `events.jsonl` as a plain `field_label`. - A declared value no longer survives a same-origin GET form submit. Each document builds its own closure, so a document that does not hold the value cannot scrub it: Flow withholds the URL and title for such a document, notes it in `meta.json`, and reports it. A later document that DOES hold the value scrubs itself and keeps its evidence. - `meta.json` is stamped with the recorded surface BEFORE the atomic publish, so a crash can no longer publish a surface-unbound recording; the CLI stamp is now a no-op when the recorder already stamped it. - The `record` secret line states what Flow actually guarantees. Tests (all fail on the parent commit, pass here): - `test_page_closure_keeps_url_and_identity_evidence_for_a_lowercase_secret` - `test_page_closure_scrubs_a_cached_label_holding_another_declared_secret` - `test_launched_recording_withholds_url_evidence_after_a_get_form_submit` - `test_attached_recorder_reads_the_origin_the_page_reports` - `test_structural_text_is_withheld_after_a_secret_leaves_its_document` - `test_recording_privacy_notices_report_what_flow_withheld` - `test_stamping_a_recorded_surface_does_not_rewrite_a_published_recording` Trial 1 of the live CDP campaign now types a lowercase secret whose characters occur in the attach URL. Every live secret in this file was an uppercase phrase sharing no character with `http://127.0.0.1:<port>/`, and that blind spot hid all three defects. The browser claim no longer says the live campaign covers "URL/title reflection" without qualification: both reflection cases use `history.replaceState`, so the coverage is same-document. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(browser): assert the refused DOM identity states its reason A withheld selector must never read as an element that simply had no stable identity. Lock the reason field on the live case whose button id IS the declared secret value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browser): recognise a keystroke prefix per declared field, not globally A controlled input that swaps its DOM node on every change made every commit point fire in the middle of typing: replacing the focused element fires focusout, so the closure committed `c`, `ch`, `cha`, … as if each were a declared value. Those short values then made every later scrub ambiguous, so the recorder withheld the URL, the title, and the DOM identity of unrelated elements for the rest of the recording — fail closed, but it destroyed evidence a correct recording should keep. Committed values are now filed under their DECLARED FIELD (falling back to the input session when a field has no declared name). At scrub time a value that another value of the SAME field continues is dropped: it is a keystroke prefix of that field's value, not a declared value of its own. The complete value is still redacted, and two declared fields never share a key, so one field's value can never suppress another's. Regression: `test_page_closure_keeps_evidence_when_a_secret_input_swaps_its_node` types a lowercase secret into a field that replaces its node on every keystroke, then clicks an unrelated button and requires the exact selector, the exact accessible name, and the exact URL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(browser): correct the commit-point comment A commit point can fire in the middle of typing, because replacing a focused element fires focusout. Say so, and point at the per-field prefix rule that handles it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browser): never drop a value the field holds or a commit point recorded The prefix rule from 5a871d4 inferred a keystroke prefix from string shape alone, so a longer COMMITTED value suppressed the field's own CURRENT value. An independent re-review reproduced the leak end to end: type `hunter2`, blur (which commits it), return and press Backspace once, and the live value `hunter` was dropped as a "prefix" and never redacted -- `"url_after": "http://127.0.0.1:54721/charts/hunter"`, `"title_after": "session for hunter"`, no withheld marker, and the CLI printing a promise that was false there. The parent commit corrupts that evidence but does not leak it, so the rule was strictly worse in that shape. Retention now tracks WHERE a value came from instead of guessing from its shape: - Committed values are filed under the ELEMENT that held them at the commit point, not under the field. `change` and `focusout` commit only the element they name, so replacing a focused node no longer commits the prefix that its successor happens to hold at that instant. - A value is droppable only when a controlled input REPLACED the node that holds it, the page no longer holds that node, and another value of the same field continues it. `supersededSecretElements` records that replacement where it is proven: the single-node mutation transfer, and the focus transfer to a replacement of a disconnected active element. - A value the field currently holds is never dropped. - Each element keeps the last value it held, and every earlier value long enough to identify. A page that reflects a field as the operator types writes the PREVIOUS value into the title or the URL, and that value must still be redacted. Two more findings from the same review: - A password field with no `name` and no `id` had no stable key: discovery derives a new input session per replacement, so a swapping node looked like a new declared field on every keystroke, no prefix was ever recognised, and every later scrub became ambiguous -- the "fail closed but useless" state. A replacement now inherits the state, and the input session, of the node it replaced, whether the mutation batch or a focus transfer proves the replacement. - A withheld accessible name, role, or clicked-row identity disarmed an identity check with no marker at all; only a withheld identity ATTRIBUTE was marked. `scrubIdentityText` now records the reason, `structuralTarget` reports it in `identity_withheld`, a withheld row identity travels as `sid_withheld`, and the recorder counts the action once for the operator. Also: `scrubSecretUrl` returns the URL the page reports when it redacted nothing, instead of a rebuilt URL with normalised percent-encoding. Regression tests (all four fail on c8f2aa5, pass here): - `test_page_closure_redacts_a_value_the_field_still_holds_after_a_commit` - `test_page_closure_keeps_evidence_when_an_unnamed_password_swaps_its_node` - `test_page_closure_marks_a_withheld_name_and_row_identity` - `test_withheld_row_identity_is_counted_for_the_operator` Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(browser): state the retention rule and every withheld identity Say which values redaction keeps -- everything the field holds and everything a commit point recorded -- and which single case it drops. Extend the never-disarm-silently statement to the accessible name, the role, and the clicked-row identity, which now carry their reason too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browser): replace secret value retention with capture-time withholding Three independent reviews each found a different blocking defect in the recorder's secret-redaction mechanism, and each fix moved the defect into the same retention rule. The literature says why: removing a value from text that was already captured is a known-unsolved problem. Englehardt, Acar and Narayanan measured every major session-replay vendor in 2017 and found that none redacts displayed content automatically and that all of it leaked; PostHog and Sentry still carry open issues for secrets in replay URLs. Production tools solve it at capture time instead, with element-bound, deny-by-default masking. This replaces the retention rule with that model. One rule now governs every text the recorder produces: report it exactly, or withhold it and say why. There is no placeholder substitution left anywhere in the recorder. Root cause. `emit()` sampled `location.href` and `document.title` inside a CAPTURE-phase listener, before the page's own handlers ran, so the values it read described the state before the action. The value history existed only to repair that staleness, and the history produced both open P1s. Python never read those event fields: the recorded URL and title already came from `_read_scrubbed_page_state()` at the settled boundary. The events now carry no reflected text at all, and that boundary is the only sampling point. Deleted: `observedSecretValues`, `committedSecretValues`, `retainedByKey` and its superseded/prefix-drop ladder, `supersededSecretElements`, the `change`/`focusout`/`submit`/`pagehide` commit points, `redactSecretOccurrences` and every scrub-and-rewrite path (`scrubSecretText`, `scrubSecretUrl`, `redactUrlToken`, `SECRET_PLACEHOLDER`). Added: - `liveSecretValues()` -- values that CONNECTED bound elements hold at match time, read live. A node the page detached is not a source of values, which is what a controlled input leaves behind on every keystroke. - `identityTextOrNull()` -- identity evidence (selector, role, accessible name, clicked-row identity, receiving field name) is exact or withheld with a reason. Replay compares identity against the live page, so a rewrite would compare against text the page never held, invisibly. - `safePageState()` -- reflected evidence is reported only while it has not changed since before the document held any declared value, and so cannot be a reflection of it. Anything else is withheld whole: origin-only URL, empty title. The baseline is seeded at install, before the document can reflect anything typed into it. - Every distinct withholding reason reaches `meta.json` and the CLI notice. Regression tests. Each fails at 7acd716 and passes here: - a password beginning with a common word leaves the URL, title, row identity, accessible name and an unrelated button id exact (was: silently rewritten) - a node-swapping input that reflects as you type no longer leaks the prefix the field stopped holding (was: emitted verbatim) - identity holding a declared value is withheld and marked (was: rewritten with no marker, so replay would compare against text the page never showed) - no event carries a URL or a title - the three prior reproductions stay closed - the stated limit -- text that predates the value -- is pinned The ambiguous multi-node replacement refusal, `input[type=password]` auto-detection, `stateFromPriorDeclaration` re-binding, and the screenshot mask are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(browser): re-pin the public artifact inventory after the claims update check_release_consistency.py pins a reviewed SHA-256 of every public artifact. The redaction redesign changed claims.yaml and its generated docs/verification.json, so those two hashes move. Reviewed: exactly those two paths change, none added, none removed. docs/VERIFICATION.md and docs/verification.json are regenerated with the timestamp string the validate-claims workflow uses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(browser): prefer the definite withhold reason over the ambiguous one When a document holds both a short declared value and a long one, and a piece of identity text contains both, the operator should be told the text held a declared value, not that it could have held one by chance. Either result withholds the text; only the reason changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(browser): correct comments that still described the removed scrubber Several comments described a mechanism this branch deleted: a scrubbed URL on the event, values retained at scrub time, and a later document 'scrubbing' text. Nothing is scrubbed any more -- text is reported exactly or withheld. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(browser): close three round-4 blockers and redact URLs by structure Round 4 judged the retention replacement sound and found three bounded defects, two of them pre-existing rather than regressions. It also found the whole-URL refusal too broad: a single-page application that routes after a login lost its URL evidence for the whole session. P1-A, pre-existing. A same-origin GET submit carries the declared value under the field's own NAME, because that is how an HTML form works. The results document seeded its baseline from the URL it loaded with -- which already held the value -- and Python stopped applying the cross-document rule as soon as that document held a declared value of its own, so the URL was reported. Closed by STRUCTURE, not by matching. The URL is now parsed instead of treated as one opaque string: * The origin and the PATH are reported. A path change is the single-page application case and is app structure, not operator input. This is the evidence the old refusal destroyed. * Every parameter NAME survives. The VALUE of a parameter whose name is a declared secret field name, or the name or id of any bound field (which includes every auto-detected input[type=password]), is dropped -- always, deterministically, with no reference to the value. Sentry and Datadog redact URLs the same way and for the same reason. * A parameter Flow cannot prove predates the moment the document first held a declared value loses only ITS value. The whole URL is not withheld for it. * Nothing is invented. A dropped value becomes empty. Flow removes characters; it never adds characters the page did not show. * A document born after some earlier document received a declared value has no trustworthy baseline, so it proves no parameter value. Python supplies that recording-wide fact, which the closure cannot know. The net, a DETECTION and never a rewrite: if the URL Flow is about to report still holds a value it can see, the whole URL is withheld and the operator is warned that the application put a declared secret into it -- an application defect that exists with or without Flow, as OWASP notes. The net runs on the path in both containment directions, so a page that writes the field into its path as the operator types cannot leave a segment behind. Matching is sound here and was not sound before: this function now runs only from Python at the settled boundary, so it needs no history. The title has no structure to exploit, so it keeps the unchanged-or-withhold rule plus the same net. P1-B, a regression against 7acd716. Deleting the commit points lost the ability to match a value after the page removes the field: an SPA wizard that replaces its form with a summary row leaked the value into the clicked row's identity. Commit-point retention is restored for ONE purpose -- deciding whether to WITHHOLD identity text -- and never for the URL, the title, or any rewrite. A spurious match can only withhold, which costs evidence and cannot corrupt or leak, so the rule that failed three reviews is not re-created. A commit is decided at the microtask checkpoint, so a controlled input that fires focusout on the node it just replaced commits no keystroke prefix. Across documents the same case is closed by name: a document recovers the value of any inbound query or fragment parameter whose NAME is declared, long enough to identify, and uses it to withhold identity text. P2-A, pre-existing. targetName() returned null for a bound secret field with no reason, so a declared field with an aria-label produced a silent null. It now states secret-field-name-not-read and is counted. Tests: nine new cases, each failing at ff5d71e and passing here. The four reviewer reproduction scripts all pass. tests/test_browser_attach.py: 74 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(browser): state the structured URL rule and what it still costs The reflected-evidence contract changed from a whole-URL refusal to a structural reduction, so README, docs/BROWSER_RECORDING.md and claims.yaml no longer described what the code does. They now state: origin and path reported, parameter names kept, declared-parameter values dropped by name, unproven values dropped one at a time, nothing invented, and the detection net with its OWASP warning. The costs and the stated residual are written down beside them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(browser): apply the same proof to a bare URL fragment A fragment that names nothing -- `#section` rather than `#a=1` -- has no structure to reduce, so it was passed through unreduced and could carry a value the proof rule would have dropped from a named parameter. Treat the whole fragment as one unnamed value and apply the same proof. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(browser): withhold a later document's URL; the path is not structural Round 5 reproduced a declared secret reaching events.jsonl through the URL PATH. An ordinary REST redirect answers a GET submit with `/results/<value>`. The new document is a fresh closure: no bound element holds the value, nothing was committed there, and no parameter NAME identifies a path segment, so the detection net is empty. `requireProof` never applied to the path, and round 4 had removed the Python backstop, so the literal was reported while meta.json told the operator the URL was protected. Regression against ff5d71e. Restore the cross-document rule. Only the FIRST document to hold a declared value reports its own reflected text; every later document's URL and title are withheld. A document that receives a value of its own is NOT exempt, because holding a value says nothing about whether it loaded with an earlier document's value in its path. This does not cost the single-page-application evidence the structural rule recovered, and there is now a test for that claim rather than an argument: history.pushState and history.replaceState do not build a new document, so the closure that held the value is the closure being sampled, and its route change is still reported exactly. The rule bites only on a real navigation, which is where the leak lives. Cross-document URL evidence IS lost again, and the docs say so. Second reproduction, same document: a scanner input that writes the value into the path and CLEARS its own field inside its own `input` handler. Nothing in the DOM holds the value by the time Python samples, and `change`/`focusout` then fire on an empty field, so every value source was empty. The capture-phase `input` listener runs BEFORE the page's handler, which is the last moment the DOM still holds the value, so it now records the last non-empty value per bound element -- ONE value, REPLACED on every keystroke, never a ladder, never used to rewrite. It applies only when nothing in the document holds a value any more, so a live field's value still wins and a detached node's keystroke prefix never withholds unrelated evidence. This also closes the reviewer's P3, where the page removes the field rather than clearing it. Also, one call: identityRefusal used liveSecretValues() while identityTextOrNull and labelTextOrNull used identityMatchValues(), so a results document refused the accessible name and the row identity but emitted `#row-<value>`. All identity paths now use the same set, which can only withhold more. Value comparison is now case-insensitive. Upper-casing an identifier before showing it is normalisation, not an application-defined transform, and widening a withhold test cannot leak. A dropped parameter is recorded only for a URL Flow actually reports, so meta.json never names a parameter of a URL that was withheld whole. Two dead bindings removed. Tests: four new cases failing at 07372d5 and passing here, plus one that must pass at BOTH heads because it proves the SPA evidence was not lost. All twelve reviewer round-5 cases pass; six of them fail at 07372d5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(browser): treat a page that consumes its own field as having held a value Round 6 reproduced four P1 leaks to events.jsonl, all silent, all present at 07372d5 as well. A scanner input that writes the badge into the URL and then clears its own field inside its own `input` handler holds nothing at any moment Python samples, and two rules keyed off what the DOM holds at that instant. CAUSE A -- the last-value cache was consulted only when NOTHING in the document held a value, while committed values were unioned unconditionally. There was no reason for the asymmetry. The test is now per element: a bound element that holds nothing right now contributes its cached value. It must stay per element rather than unconditional. A controlled input that swaps its node on every keystroke leaves detached elements still reporting `c`, `ch`, `cha`; unioning those in withheld any text containing a one-letter match and nulled `#chart-save` in three existing cases. A detached element is added only when no connected bound field holds anything, which covers a page that REMOVED its field while excluding the swap trail. That alone did not close F3: the cache holds ONE value per element, and a second scan displaced the first badge while the first was still in the URL. A value the next one does not CONTINUE was not edited away by the operator -- the page took it and started the field over -- so it is promoted into the withhold-only committed set, which is not per element. While the operator types, each value continues the one before it, so no keystroke prefix can be promoted. It is checked at the next input event, not at a microtask checkpoint after the current one: a checkpoint runs BETWEEN listeners, so it observes the field before the page's own handler has cleared it. CAUSE B -- `documentHeldSecretValue` was derived from live values, so it stayed false forever for a consuming page. The title net sits inside that flag while the URL net does not, and `state.secret` never became true, so Python never learned the document had received a value. It is now armed in the capture-phase `input` handler, the moment the document provably held one, which is what the documentation already said: "once a declared secret field RECEIVES INPUT". Python had the same asymmetry: `_track_secret_document` added to `_secret_doc_ids` from the input event but set `_first_secret_doc_id` only from the settled read, and the cross-document rule keys off the second. Both markers now move together through `_mark_secret_document`. Also corrected the cache comment: it is literally the last `input` event's value, not "the value the operator stopped on". Tests: four new cases, each failing at d1a762e and passing here. All round-4 and round-5 reviewer files pass (31). Eleven of thirteen round-6 reviewer cases pass; two assert the pre-fix behaviour and contradict F3/F4 -- see the PR body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This draft replaces #368. GitHub closed #368 when its merged stack base branch was deleted, and GitHub does not permit a closed pull request to change its base.
A
supportedclaim no longer passes because a cited test file exists. The required CI job must supply real JUnit evidence that each cited file ran and passed.Root cause
The former gate had four fail-open paths:
--junitwas optional.fileattribute that Pytest xUnit2 does not emit, so real Pytest output could produce an empty status map.Change
testjob emits unit JUnit and validates unit-backed claims.e2e-browserjob emits browser JUnit and validates browser-backed claims.This reuses the existing required test executions. It does not add a duplicate test suite or a new expensive CI job.
Independent review fixes
oa-vmendpoint.openadapt-parallelsrunner label and uses one global concurrency group across branches and dispatches. GitHub can supersede only the read-only pull-request gate, not a physical run during cleanup.prlctl statusandsnapshot-listnow require a zero exit status. Plausible stdout from a failed command cannot satisfy the final-state or current-snapshot proof.Rebase
973e21c458de1bccce67ba23b24ce58ce2cbf7abonmain.357df9324c33263d35a6b13db1b955597e0600da.Local validation
actionlint: passed.git diff --check: passed.Merge order
Do not merge this draft until exact-head CI and independent review pass. The physical Parallels job intentionally skips on pull requests; a passing structure gate is not live Parallels evidence.