phase: 03_session_takeover
**Phase 03 `session_takeover` — final verification pass. All green.**
All four task files were already in `.agents/phases/complete/03_session_takeover/` with per-task reports, so this was the phase-level close, not new implementation.
**Completion criteria**
1. *2nd controller authenticates + takes the bridge; a 3rd is refused* — met host-side: `test_claim` 102 (whitelist matrix, `acceptsUpgrade` one-claim-at-a-time, both deadline clocks across `millis()` rollover, 64-byte cap through the real codec), `test_relayctl` 308 (`--take-over` = `T`,`X`,`H`; default refusal names the flag and sends no `X`), `test_page` 142 (claim→button→one click→`T` then `X` only). Refusal mechanism stated precisely: a second claim gets `503`/`E3`, a third *socket* gets the slot refusal (`slotrefused`).
2. *Handover releases then adopts, lands disarmed* — pinned: `closeLink("taken over")` before adoption (inverting it fails exactly 1 check, reverted); no `arm`/`setArmed`/`armed = true` in the `X` path (inserting one fails exactly 1 check, reverted); un-authenticated `X` admitted in `claim.h` fails `test_claim`+`test_relayctl` (reverted). Also verified by reading: `serveClaim` never writes `_lastInboundMs`/`_controllerSeen`, so a parked claim cannot hold the board armed past 8 s.
3. *Incumbent page says taken over, disarmed, no auto-reclaim* — `onE12()` + `test_page` (disarms, releases mouse, ends typing run, pill `taken over`, reconnect loop off).
4. *Three source pins fail on inversion* — re-demonstrated in this pass, all reverted (`src/claim.h`, `src/control_server.cpp` byte-identical to pre-inversion).
5. *Gate green + README/AGENTS say takeover cannot arm* — yes.
**Validation** — `bash .agents/validate.sh` → **exit 0**: 12 suites **5026 checks, 0 failures** (normal and `SANITIZE=1`), `bridge`/`console`/`rollback` all SUCCESS, page bytes 59 286, README/AGENTS claims OK, hygiene OK. Extra lint: `ruff --select F,E9` clean, `bash -n` clean, `py_compile` clean. Coverage on new/changed logic: `claim.h` 100 % (11 lines), `status.h` 100 % (29), `proto.h` 94.8 % (229), relayctl take-over lines 14/15 (the miss is a `def` continuation trace never records).
**Defects fixed (docs only — no code defect found)**: bug 6 claimed a 2nd `/ws` upgrade "is still refused"; Measured-behaviour samples lacked `taken=`/`busy`/`tkov`; footprint + TLS-cost rows quoted phase-02 numbers while claiming to be what the gate prints (now 99 744/1 257 933, 67 352/1 178 173, 93 568/1 157 609 → TLS = +6 176 B RAM/+100 324 B flash); page-delivery/handshake rows dated; `AGENTS.md` pure-module list was missing `claim.h`.
**Hardware**: no device run. The board still runs pre-phase-03 firmware, reflash/arming mutates a live device (the target is this machine), and both controllers need the operator's passphrase — guessing feeds the shared 5-fail lockout. The 3 blank rows (third-controller refusal, chord labels, nothing-outlives-the-link) are now recorded `skipped (no device run)` with the exact closing commands in `.agents/HW_VERIFICATION.md`.
**Tree left for the harness**: index untouched (0 staged), no history changes; `todo/03_session_takeover/00_phase.md` moved to `complete/` (matches the phase-02 rename pattern); untracked files the commit must include: `.agents/validate.sh`, `src/claim.h`, `tests/test_claim.cpp`, `.agents/phases/complete/03_session_takeover/`, `.agents/reports/03_session_takeover/`. Next phase: `todo/04_controller_page_responsive/`.
This commit is contained in:
@@ -10,7 +10,7 @@ The README is the single source of truth for behaviour, protocol, safety and mea
|
||||
This file is the operating manual: what must not break, how to build and verify, and where
|
||||
the traps are. **Do not fork the protocol or the safety model into `docs/`** — a second copy
|
||||
drifts, and two paths that can disagree about arming is the exact failure this project keeps
|
||||
having to fix (see README, "Eleven bugs").
|
||||
having to fix (see README, "Thirteen bugs").
|
||||
|
||||
## Phased execution
|
||||
|
||||
@@ -57,9 +57,17 @@ This repository runs work through `.agents/phases/`:
|
||||
VT it reboots the target. Same reasoning for the VT-switch chords.
|
||||
7. **One frame is one command.** ASCII, no JSON inbound, ≤ `kWsMessageCap`. Long payloads are
|
||||
the *controller's* problem to split, with the board reporting how much it still holds.
|
||||
8. **The keyboard is exclusive; the board is not.** A second `/ws` upgrade is refused (`E3`);
|
||||
read-only HTTP is served even while a controller holds the bridge. Keep both routes going
|
||||
through `serveReadonly()` so they cannot drift apart.
|
||||
8. **The keyboard is exclusive; the board is not.** A second `/ws` upgrade cannot drive
|
||||
the keyboard: it may only stand in the spare slot as a pending claim that
|
||||
authenticates (same passphrase, same shared lockout) and may then send nothing but
|
||||
`K` and the deliberate `X` — `X` hands the bridge over *through the release funnel*
|
||||
(incumbent told with `E12`, every key and button released) and the board lands
|
||||
**disarmed**; nothing reachable from `X` may arm. Every other frame is refused by
|
||||
the one whitelist in `src/claim.h`, which drops the claim, and no claim frame ever
|
||||
reaches the command dispatcher. A second claim, or a third socket, is
|
||||
refused (`503`/`E3`, or the slot refusal); read-only HTTP is served even while a
|
||||
controller holds the bridge. Keep both routes going through `serveReadonly()` so they
|
||||
cannot drift apart.
|
||||
|
||||
## Tables that must agree
|
||||
|
||||
@@ -90,7 +98,7 @@ the page and `TEXT_FRAME_MAX` / `MAX_PASTE_CHARS` in `relayctl.py` are hand-copi
|
||||
## Build, test, flash
|
||||
|
||||
```bash
|
||||
tests/run_tests.sh # 4826 checks, no hardware
|
||||
tests/run_tests.sh # 5026 checks, no hardware
|
||||
SANITIZE=1 tests/run_tests.sh # + ASan/UBSan — use it for anything touching buffers
|
||||
# PlatformIO. The venv below runs Python 3.15, which esptool REFUSES ("Python version must
|
||||
# be 3.10..3.14") -- builds fine, dies at upload. Use uv's 3.14 for anything with -t upload:
|
||||
@@ -159,7 +167,7 @@ wired now — but ask, of every green check, *what would still pass if this were
|
||||
## Editing rules
|
||||
|
||||
- **Pure modules** (`keystate.h`, `mouse.h`, `output.h`, `proto.h`, `status.h`, `text.h`,
|
||||
`ws_codec.h`, `sha1.h`, `tls_pem.h`) include no Arduino or ESP-IDF headers. That is what lets the
|
||||
`ws_codec.h`, `sha1.h`, `tls_pem.h`, `claim.h`) include no Arduino or ESP-IDF headers. That is what lets the
|
||||
identical code run on a host under sanitisers. Keep it — and keep the *crypto library*
|
||||
out too: `tls_pem.h` handles the identity's strings, `tls.h`/`tls.cpp` handle the
|
||||
library, and the host suite includes the first and never the second. `tls_session.h`
|
||||
@@ -194,7 +202,7 @@ wired now — but ask, of every green check, *what would still pass if this were
|
||||
nothing may print it, hash-and-print it in a way that reads as the key, or be added to the
|
||||
tree as a `.pem`. Fingerprint yes, key never.
|
||||
- `TODO.md` holds **open items only**; clear it when they are done. The durable record is the
|
||||
README: a fixed bug belongs in "Eleven bugs this verification caught" with its root cause and
|
||||
README: a fixed bug belongs in "Thirteen bugs this verification caught" with its root cause and
|
||||
how it was proven, a new capability in the section that describes it, and a rule worth
|
||||
repeating here. A TODO that has accumulated resolved items tells the next reader nothing about
|
||||
what is left to do.
|
||||
|
||||
Reference in New Issue
Block a user