Repository navigation
Conversation
A run clears a known interruption (session warning, banner, tour) itself only when it matches a class by text and control, the dismissal is Escape or a closed non-committing label, no write watcher sees a write (browser write fence, or the system-of-record read-back on any backend), and the step's normal identity check passes afterwards. Classes are per-app data (runtime.interruptions_path) or learned from a halt. Each dismissal is in the run report.
…ands An interruption class now matches on the whole dialog (full text, title, owning window), never its button word: a dialog mentioning a patient, a record, or saving/applying/changing anything is a business question and halts, as does a surface that can't read the whole dialog. Every web structural click arms a witness of where its trusted pointer events land; input that reached anything but the verified element (a popup that appeared mid-click) is delivery uncertainty, reconciled and never retried.
|
From: Grok, To: coordinator. Review of #1902 at 732fdcb: CHANGES REQUESTED (one blocking gap, on the exact property you asked me to check). Blocking: the non-committing label check isn't bound to the control that gets clicked. Probe (headless Chromium, the real replayer, using the The network fence is the only thing between this and a POST in a browser. On a desktop run, the record read-back is the only watcher. An Suggested fix: when the class is learned and again at dismissal time, read the resolved element's live accessible name or text (the DOM name in the browser; the UIA/AX name on desktop). Require it to equal the declared name and to match What holds (checked):
Tests:
|
|
From: Grok, To: coordinator. A follow-up for the new head 4e99647, which landed while I was posting. The blocking point above still stands. The new whole-dialog check would stop my first probe, because "Delete record" contains a business word. It still doesn't compare the clicked control's live name with the declared Re-probe on 4e99647, using the same fixture and headless Chromium (the probe was deleted afterwards):
"Sign out", "Send", "OK", "Accept" and "Continue" are not business words, and the leftover text is well under 240 characters, so the button word still decides here. It is simply the wrong button word. The fix is the same as before: read the resolved element's live accessible name, at learn time and at dismissal. Require it to equal the declared I have not rerun the full suite on 4e99647. |
# Conflicts: # packages/flow/CHANGELOG.md
Independent adversarial review: BLOCKBLOCK remains at a7672c3. The main merge does not address Grok's live-label binding blocker.
The policy module and all three new test modules are byte-identical to the previously reviewed 4e99647, as are the selector and dialog-reader methods' ASTs. I checked the current source and prior measured browser evidence; I did not rerun unchanged probes, and make no new real-app or parent execution claim. Require the live control identity and allowed label to agree at learning and delivery, with negative coverage for conflicting selector/name identities. Keep the existing watcher and uncertainty checks. This is a generic declared-versus-delivered identity defect, with no per-app engine change requested. Held-out real-app evidence is not established by this review. Review by Codex. |
An interruption class checked the declared name against the closed non-committing set, but a selector or automation id resolves first and may point at any control. A class declaring "Close" whose selector found "Sign out" was cleared. Now a control class must declare role and accessible name, and before the click (and again at the last no-input boundary) the backend must show the bound element is the one control with that role and exact name (structural_control_is, Playwright get_by_role exact). A backend that can't say, a non-structural resolution, or any error halts. Learning applies the same check before its click.
|
From: Codex BLOCK at Reproduced with genuine headless Chromium, native input and a local HTTP server through both replay and All three unchanged author modules pass: 61 cases / 183 phases on head and on literal parent Please preserve the declared non-committing control identity through actual delivery, including external accessible-name dependencies, across replay and learning. Keep the paired positive and refusal controls. No per-app engine code: yes. Fix the class: not yet; external-name mutation still accepts through both entrypoints. Held-out app evidence: not established by these synthetic browser/server cases. Own cloud run: 42554787250. Combined138 cases/414 phases,134pass/4semantic failures. |
The live-name check passed on Close before the click, but a trusted pointerover could change an external aria-labelledby label to Sign out and the press still landed (GET /logout-everywhere passes the fence). A click guard is now installed by an init script at the start of every document, with the per-context WebSocket guard, so its window capture-phase listeners run ahead of page listeners. Armed per click on the bound control (structural_label_guard), it watches the control, its aria-labelledby/describedby nodes, its labels and stylesheets with a MutationObserver (flushed with takeRecords) and a snapshot, and cancels any press or click that arrives after a change or off the control. A blocked input halts with LabelChangedDuringClick delivery uncertainty, the dismissal reported as delivered. A document without the guard, a guard that can't arm, or one whose listeners are gone afterwards halts. Learning uses the same guard. The write fence now logs every request (requests_seen), reports writes its check let through, and any write in the log that no attempt names becomes an attempt, so the report can't claim no writes the fence saw.
|
From: Codex BLOCK: CSS-generated accessible names can still change between validation and trusted input. In both replay and learn, a button initially named “Close” changes to “Sign out” through CSSOM The original eight independent cases now pass at this head; the parent fails the two external-label-change cases. All author cases pass (head73, parent61), as do two head cases verifying that normal and exceptional dismissal retain genuine fence evidence. Ruff, format, Python3.10 syntax checks and scoped Mypy pass. Broader setup variants failed before establishing their initial control, and provide no product-failure evidence. This review accounted for168 cases/504 phases; the run is not all green. Generality: no per-app engine code added. The class is accessible-name continuity across input delivery, including generated labels. Held-out real-app evidence has not been shown by this review. |
A stylesheet rule added through insertRule or adoptedStyleSheets could change a label's ::before/::after content with no DOM mutation, and the click guard let the press through. The guard's snapshot, re-taken at every guarded input, now also reads the computed ::before, ::after and own content, display, visibility and content-visibility of the control, its aria-labelledby/describedby nodes, its labels and their first descendants, using built-ins captured at install. Any difference blocks the input.
|
From: Codex BLOCK — incomplete CSSOM snapshots can still authorize a differently named control ( The new guard snapshots only the first64 nodes of a label subtree. Its total-node count stays unchanged when CSSOM changes generated text on an existing65th node, so both snapshots compare equal even though Chromium's actual accessible name changes. Independent headless Chromium cases start with exactly one button named “Close.” Its external label has65 total nodes, empty textContent and generated The identical65-node cases also fail on literal parent9c0e96dabeeb6f76096a20ad5cf4661959fb85bf. The new patch correctly refuses the64-node changing cases and the original smaller CSSOM cases; stable64-node controls pass. This is the same incomplete-observation class at the snapshot boundary. A finite cap should refuse when it cannot cover the label evidence, or use an observation that covers all contributing nodes. Raising the cap alone leaves the class open. Validation: head97 cases =95 passed/2 failed; parent93 =87 passed/6 failed; total190 cases/570 phase reports, with no skips, errors or deselections. These include all77 head author cases,73 parent author cases, the immutable original8 probes per ref, four prior CSSOM probes per ref, two genuine normal/exception fencing cases per ref and six new boundary cases per ref. Scoped Ruff, format, Python3.10 syntax and Mypy checks pass. The original CSSOM repair is demonstrated by two causal parent failures becoming passes on head. New boundary failures are not a regression claim. Earlier document.open cases were not selected in this targeted re-check and receive no product credit. Actual Python3.10 execution and held-out real-app qualification were not run. Generality: no per-app engine code is added. The requested fix covers complete observation of authorization evidence across replay and learn. These are independent synthetic browser cases, with no held-out real-app claim. |
The guard's style snapshot read only the first 64 nodes of a label subtree, so a CSSOM relabel on a 65th node compared equal and the click landed on a control Chromium then named Sign out. The snapshot now reads every node of the control's and each label node's subtree, and is null when one has more than LABEL_GUARD_MAX_NODES (64, the node included) or its walk fails. Arming then refuses and nothing is clicked; at an input, a null snapshot (a label that grew past the cap) is a change and the input is blocked. The node count of each subtree is part of the snapshot.
|
From: Codex BLOCK: an exception while refreshing label evidence permits a changed-control click. A genuine headless-browser case installs an own button.value getter on trusted hover. The getter throws while CSSOM changes the accessible name from Close to Sign out. In both replay and learning, the head and its literal parent read the getter eight times and receive eight errors, then deliver trusted pointerdown and click events to Sign out. The server receives GET /logout-everywhere. Replay also continues to POST /save; its action reports ok=true, delivered=true and no label violations. The stable getter controls pass. The direct value read at playwright_backend.py:2273 escapes the snapshot refresh at line2302. The capturing handler calls flush at line2315 before cancellation at line2324, so that exception prevents cancellation; finishing the guard can still accept its empty violation list. Make the complete evidence refresh fail closed and ensure unreadable evidence blocks native input. Cover sibling snapshot-read failures with the same model. Validation: 207 cases, 621 phases, 201 passed and 6 semantic assertion failures; no errors, skips or deselections. Head: 82 author cases and all20 earlier probes passed; the new4 cases had2 stable passes and2 unsafe-click failures. Parent:77 author passes,2 earlier65-node failures and the same2 getter failures. The65-node fix is causally confirmed: head refuses before input while parent clicks the changed control. The getter defect is inherited, rather than introduced by this delta. Ruff, formatting, scoped Mypy and Python3.10 syntax checks passed. Item/JUnit/phase records, source and named dependencies were reconciled independently; no actual3.10 compilation or real-app acceptance is claimed. No per-app engine code: yes. Fix the class: evidence-refresh exceptions must taint authorization across replay and learning. Held-out real-app evidence: not established; these are synthetic genuine-browser cases. |
A page getter that threw on a property the guard's snapshot read (an own button.value getter, installed on hover) aborted the capture listener before it cancelled the input, so a CSSOM-relabeled click landed. Each guarded listener now blocks unless the whole check finishes and passes: any exception blocks the input and records a violation. The guard reads only through built-ins captured at install (brand-checked getters for value, labels, tagName and the like, NodeList and MutationRecord getters, index loops, string-built snapshots), so a getter or method the page defines later never runs. arm and finish answer with strings the guard built, parsed in Python, so a patched toJSON or Array method can't shape the answer.
|
BLOCK — the delivery guard still invokes page-provided prototype getters From: Codex The throwing-value-getter hole from my previous review is fixed: the two changing cases now refuse, with zero page getter reads and no wrong-route request. One callback surface remains in the guard. Measured with the genuine backend through replay and learning: an inherited Use a closed prototype-free representation for descriptors and audit the other browser option dictionaries under the same rule. Add controls that verify page prototype getters are never invoked during arm, snapshot, guard and finish, while retaining the current refusal and successful-dismissal cases. This closes the callback class across entry points. Validation: whole author modules passed (93 head, 82 parent). The earlier comparison recorded 235 cases/705 phases, 231 passes and four failed assertions: two retained head assertions require page getter reads that this fix bypasses, and two parent changing cases reproduce the old hole. The new exact-event companion recorded 12 passes/36 phases. Ruff, formatting, Python 3.10 syntax and scoped Mypy passed; actual Python 3.10/native/real-app execution was not run. Original assertions and failed results remain intact. Generality: no per-app engine code. Fix the class: descriptor and option-dictionary conversion must preserve the guard's trust boundary. Held-out evidence: synthetic software cases only; real-app/M1 acceptance remains unestablished. |
The guard passed ordinary objects to built-ins: property descriptors in its list helper and MutationObserver options. Converting them reads absent fields through Object.prototype, so a page getter for configurable, writable or attributeOldValue ran from inside the guard. Every dictionary the guard hands a built-in now has a null prototype and only own data fields, the observer's attributeFilter is a closed iterable (own iterator, own next, null-prototype results), the heartbeat event type is random, and the listener blocks before it records. The evaluate wrappers no longer destructure their arguments. The init script carries a sourceURL so a test can attribute frames to it: the new browser tests trap Object.prototype keys and the built-ins the guard could reach, and require that no call from the guard reaches them, for a stable click and a relabel, in replay and learning.
|
APPROVE — scoped descriptor/dictionary callback repair From: Codex The null-prototype descriptors, options and iterator results close the page-owned callback paths measured in my earlier BLOCK. Four independent stable/relabel × replay/learning controls armed 79 transparent traps. The head registered the actual guard script, passed both detector self-checks and invoked zero trapped page callbacks. Literal parent a5313ce invoked 84 callbacks per stable case and 50 per relabel case, with actual guard frames and trusted target-event witnesses. Stable clicks reached the owned dismissal route; relabelled controls refused before pointerdown/click delivery, with no write or logout request. This demonstrates callback closure for the tested surfaces; it does not claim an observed unsafe delivery on the parent. Both complete author cohorts passed: 97 head cases and 93 parent cases. Full scoped accounting: 230 cases/690 phases, 222 passes/eight failures. Four failures are the intended parent callback assertions. Four are unchanged document.open fixture-premise failures on both refs, before the requested control exists; they establish no regression or repair. The old function-name detector also earns no callback-closure credit on the renamed head. The overall review workflow remains red; no all-green claim. Ruff, format, syntax and scoped Mypy passed. All eight groups have complete source/dependency/collection/JUnit checks in isolated revision environments and final source parity. I independently rehashed 398 loaded source paths across the two refs. No actual Python 3.10 execution or held-out application qualification in this run. No per-app engine code: yes. |
|
Retriggering CI: the last run tested a merge ref from before #2122 fixed main's gitleaks. |
# Conflicts: # packages/flow/CHANGELOG.md
|
Retriggering CI now that #1956 turned main green. |
|
Retriggering CI: the last run used a merge ref computed before #1956 landed; the import-layer tests pass on the current merge ref. |
Compiled runs stop for a person whenever a harmless interruption appears. This PR lets a run clear a known interruption itself, but only when the dismissal is provably read-only. Any other interruption stops the run as it does today.
The policy (
docs/INTERRUPTION_RECOVERY.md)A run clears an interruption only when all four of these hold:
runtime/effect_watch.py. In a browser, that is the write fence: the save's existing wire check with nothing allowed through, so every write request, WebSocket, worker and socket message is stopped before it leaves the page. On any backend, including desktop and RDP/Citrix, the run's system-of-record read-back is read before and after. A run with no watcher does not dismiss. A watcher that sees a write causes a safety halt.Classes are data. The deployment points
runtime.interruptions_pathat a per-app YAML file, andlearn_interruptionlearns a class from a halt; learning goes through the same watchers on the live screen. Each dismissal is recorded ininterstitial_actionsin the run report, with the class, the control's label, the frames,effect_watchersandwrite_attempts. This builds on the existingInterstitialpath and on the fence idea from draft #1449. Existing interstitials serialize byte for byte as before.The whole dialog decides, and where the click lands
Whole-dialog classification. Before any input, the backend reads the whole interruption: its full text, title and owning window. The run halts in any of these cases:
interruption_text.A class whose own texts carry a business word is refused when it loads.
Click witness. Every web structural click arms a witness just before dispatch. If any trusted pointer event of the click (press, release or click) reaches something other than the verified element, the click raises
ActionDeliveryUncertainwith causeInputLandedElsewhere, and the runtime reconciles and never retries. A popup that covers the target before the input is already refused by the pre-dispatch trial.Tests (cases registered before the runs)
tests/test_interruptions_browser.py: headless Chromium, the real replayer and real OCR, against a local synthetic app:InputLandedElsewhere, with no retry and no POST.tests/test_interruptions.py(34 tests): the closed label set, refusal of position-only classes, the data-file round trip, the read-back watcher on a desktop-style run, a read-back change halting the run, no watcher or a refused fence meaning no dismissal, a dismissal onto an unexpected screen halting at identity, and learning that keeps or refuses a class.tests/test_write_fence.py: fetch POST, beacon, form post and WebSocket are all blocked in a real browser, and a page loaded before the guard is refused.Not in this PR
The same check after dispatch on native and RDP/Citrix backends, session re-authentication with resume from the last verified checkpoint, a bounded re-resolve with an identity re-check after late hydration in the browser (fresh-frame reacquisition exists for RDP only), an "offer to learn" prompt in the halt UI, and real-app runs on the zen lab's OpenEMR and Dolibarr, including behind the chaos proxy.