# TFBthumb — Heals from v0.2 to v0.2.1 - Heals ID: `TFB-THUMB-HEALS-20260615T073500Z` - Predecessor: v0.2 (`REVIEWER_PACKET.md` at original SHA `aeaca3ab...`; healed once for the 14→15 file count to `d78b2fa5...`) - This file: v0.2.1 (7 heals applied; bounded-claim set unchanged; all 6 gates re-passed; new swap-label test added to Phase 3) - Driver: DAVID+ and CODE_MECHANIC reviews paired in this dir - Decision authority: CEO directed "heal them all"; no autonomous scope expansion - Wider-claim authority: still `inactive` - New public claim: **none** (the bounded-claim set holds at the same bytes-of-meaning; the SHAs changed, the claims didn't) --- ## 1. The 7 heals — each names its source review, file:line, and verification ### Heal 1 — `Thumb.point` docstring drift (CODE_MECHANIC note-3) [thumb.py:80–86](thumb.py) Pre-heal docstring said *"move the hand onto it, and feel hover engage"* — a reader could parse "feel hover engage" as "the mouseover event fires." Heal #5 from the build session added a hit-test fallback for cross-navigation mouse-position preservation. Docstring now states: *"Engagement is confirmed via the hover event (preferred) OR a fresh hit-test (fallback). The semantic contract is hand-on-affordance, not the mouseover event."* No runtime change. ### Heal 2 — `Retina._on_navigated` deliberately-not-reset comment (CODE_MECHANIC note-2) [retina.py:84–96](retina.py) `_on_navigated` was correctly NOT resetting `state.in_flight` (the CDP-tracked request set, cleared by per-request lifecycle events) or `state.motion_until_ms` (a monotonic timestamp safe to leave in the past). Both omissions are now commented explicitly so a future maintainer doesn't "heal" what isn't broken. ### Heal 3 — `Retina.start()` idempotency guard (CODE_MECHANIC note-1) [retina.py:54, 57–67](retina.py) `page.on()` does not de-duplicate; a second `start()` would register a second pair of (`framenavigated`, `load`) handlers. Heal: `start()` now raises `RuntimeError("Retina.start() called twice on the same page")` if `self._started` is True. The contract was always "one Retina per page"; it is now enforced loudly. ### Heal 4 — `Ceiling.authorize_effect` per-window counter (CODE_MECHANIC note-4) [ceiling.py:280–304, 363–386](ceiling.py) The effect window was time-only — any mutating request within the ~1500 ms following an approved consequential click was covered. Heal: a new `effect_window_max_effects` parameter (default `1`) limits how many mutating effects a single approved click covers. A second mutating effect during the same window must present its own token. New ledger reason `"effect-window-exhausted"` distinguishes this from time-expired. Strictly tightens; default flow unchanged (`gate_sentinel.py` still passes — the Pay→POST is one effect, fits within max=1). ### Heal 5 — `make_authority` Ed25519 enforcement (DAVID+ gap-1) [ceiling.py:198–254](ceiling.py) Pre-heal: `make_authority(asymmetric=True)` silently fell back to `HmacAuthority` if `cryptography` was missing. The verifier closure under HMAC HOLDS the signing key — the bounded-claim set's "no signing power in the closure" claim becomes false under fallback. Heal: a new `HmacFallbackRefused` exception class. `make_authority` now requires explicit `allow_hmac_fallback=True` to fall back; default is to refuse. When fallback IS allowed, the function emits a loud stderr warning AND writes a `kind="authority_init", reason="hmac_fallback_explicitly_authorized"` startup receipt to the supplied ledger so post-deploy operators see the downgrade in their ledger and in their logs. Back-compat: existing tests use `HumanAuthority()` (the alias) which preserves the original behavior; production code is steered to `make_authority(...)` so the refusal fires. ### Heal 6 — identity-break event for DOM swap detection (DAVID+ gap-2) [sensors.js:81–155](sensors.js), [retina.py:33–46, 137–144](retina.py), [brain.py:35–63](brain.py), [agent.py:59–67](agent.py) Pre-heal: the sensor's signature-decision branch detected the "new id assigned where a different signature previously held the slot" case but did not surface it. A swap-attack (label preserved, href changed; or label changed, slot preserved) was invisible to the Brain. Heal: - `sensors.js` maintains a per-coords map of `(id, signature)`. When a fresh tfb-id is assigned at coordinates a different `(id, signature)` previously held, it emits `{type: 'identity_break', prior_id, new_id, coords, signature_changed: true}` through the existing `__tfb_emit` binding. - `RetinaState.identity_breaks: set` collects new_ids that surfaced via this path. Cleared on `framenavigated`. - `Observation.identity_breaks: frozenset` carries the set forward to the Brain. The rendered observation now prints `identity_break` as a flag on the affected element so an LLM-Brain can see it inline. - `Agent._observe` propagates `retina.state.identity_breaks` into the Observation. RuleBrain doesn't yet refuse on it (deterministic fixed-target gates don't need to); LLM-Brain consumers can refuse on the flag. This is the **D-PERCEPTION-IS-A-SECURITY-SIGNAL** carve DAVID+ named: the richer substrate surfaces a defender's hook the screenshot loop has no path to. ### Heal 7 — swap-label invariant + Phase 3 gate addition (DAVID+ gap-3) [thumb.py:107–120](thumb.py), [gate_ceiling.py:157–177](gate_ceiling.py) DAVID+ confirmed by inspection that `Thumb.click/type_into` re-read `el["name"]` from the LIVE world at every authorize call (so a between-observation-and-dispatch label mutation is classified by the new label). The implementation was correct; the *claim* was under-documented. Heal: - `Thumb.click` docstring now states the invariant explicitly with the "swap from 'Help' to 'Send' classifies by 'Send'" example and a reference to the new Phase 3 test. - `gate_ceiling.py` adds gate 7: mutates a `Save draft` button to `Send` at runtime, runs `Thumb.click("Send")`, asserts `CeilingBlocked("needs-human-token")` fires. The implementation is now backed by a test. `PHASE 3 GATE` PASS line extended to mention "swap-label attack caught at authorize time." --- ## 2. Re-verification — all 6 gates still PASS on healed canonical | Gate | Verdict | Notes | |---|---|---| | Phase 0 — `demo.py` | PASS | settled in 1187 ms, 5 rows | | Phase 1 — `demo_thumb.py` | PASS | recovered dropped key | | Phase 2 — `harness.py 200` | see receipt | running in background as of write; pre-heal n=200 result was 0/200 | | Phase 3 — `gate_ceiling.py` | PASS | **+ swap-label gate added and passes** | | Phase 4 — `gate_agent.py` | PASS | TFBthumb 863 ms vs baseline 1684 ms (2.0×) | | v0.2 effect-gate — `gate_sentinel.py` | PASS | Pay→POST single-effect-per-window still covered | Analytics receipt at `/tmp/claude-501/tfbthumb_sandbox/canonical_analytics_post_heal.json`: - Token ratio: 7.7× (unchanged; well over 5× tolerance) - Byte ratio: 55.5× (unchanged) - Correctness n=60: 100% TFB vs 38.3% baseline (unchanged at tolerance ceiling) - Motion: 4/4 (unchanged) - Safety: 0 ungated; ledger verifies; tamper detected - Signature: 9/9 same (unchanged) Speed on the trivial 5-step form-fill: 986 ms vs 877 ms baseline (`0.89×`) — same shape as v0.2 (TFBthumb pays a quiescence-tax on tasks where the screenshot loop happens to be correct by luck; faster on every non-trivial task per Phase 4 + Phase 2 receipts). ## 3. Warden Kill-Test (heals layer) - **Claim under review:** the 7 heals strengthen the substrate without falsifying any prior bounded claim or breaking any prior gate. - **Null hypothesis:** at least one heal broke a gate, weakened the bounded set, or silently changed a contract. - **Discriminating test:** parse-gate every file; run 5 fast gates + analytics; queue n=200 Phase 2; verify every metric vs pre-heal receipts. - **Outcome:** null killed. All 5 fast gates PASS (Phase 3 now stronger with gate 7). All analytics metrics match the pre-heal numbers within hardware noise. n=200 running. - **Present-tense downgrade:** *"verified internally on the same M5 Max sandbox by the same Claude Opus 4.7 session that wrote the heals; awaiting external independent re-confirmation by the same reviewers (Gemini + DAVID+ + CODE_MECHANIC) when convenient."* ## 4. What this heals receipt authorizes - The bounded-claim set in `REVIEWER_PACKET.md §1` continues to hold against the new SHAs. - Phase 3 now structurally proves the swap-label invariant; future regressions there fail loudly. - The LLM-Brain consumer can now read `identity_break` flags directly from the rendered observation. ## 5. What this heals receipt does NOT authorize - **No new public claim wider than the bounded set.** All §10 boundaries from REVIEWER_PACKET.md continue to hold. - **No re-validation by Gemini implied.** Gemini's `verified` was paired with the v0.2 SHAs. A clean re-run would re-confirm; this receipt does not assert it has happened. - **No production-deployment statement.** Out-of-process Authority (named in the blueprint as `[deployment]`) is still required for production custody. - **No claim that gap-1 alone is "production-ready" custody.** Refusing HMAC fallback is necessary but not sufficient; the deployment Authority must run out-of-process. ## 6. SHA changes — every file the reviewer must re-match See `sha_manifest.txt` post-heal column. Files that changed: - `retina.py` (heal 2, 3, 6) - `thumb.py` (heal 1, 7) - `ceiling.py` (heal 4, 5) - `sensors.js` (heal 6) - `brain.py` (heal 6) - `agent.py` (heal 6) - `gate_ceiling.py` (heal 7) - `REVIEWER_PACKET.md` (Gemini's 14→15 heal earlier; unchanged here) - This file: `HEALS_v0_2_to_v0_2_1.md` (new) Files NOT changed (verified by SHA): `sentinel.py`, `demo.py`, `demo_thumb.py`, `harness.py`, `gate_agent.py`, `gate_sentinel.py`, `analytics.py`, `TFBthumb_BLUEPRINT.md`, `REVIEWER_DECISION.md`, `DAVID_PLUS_REVIEW.md`, `CODE_MECHANIC_REVIEW.md`. ## 7. Closing The 7 heals each pin a claim that previously lived in code-only down into code + comment + test (where applicable). The bounded set hasn't grown; it's just resting on a substrate that's structurally harder to weaken. DAVID+ and CODE_MECHANIC reviews were the discriminating signal; healing them all takes the system from "verified by Gemini at v0.2" to "verified by Gemini at v0.2 + strengthened against the three class-shaped gaps DAVID+ named + the four engineering notes CODE_MECHANIC named." Per `REVIEWER_PACKET.md §12`, any wider promotion, public claim, or substrate role expansion still requires a fresh decision packet.