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:
2026-11-09 08:57:35 -05:00
parent e0780f58e0
commit d629a0ff51
45 changed files with 2884 additions and 161 deletions
+7 -5
View File
@@ -49,10 +49,10 @@ may have a board attached that nobody is watching, and a second machine for
| claim | command | expected observation | outcome |
|---|---|---|---|
| keystrokes reach the target | `python3 tools/watch_target.py --seconds 60` **on the target**, driven from the page or `tools/relayctl.py` | one press **and** release per key with timestamps; `still held` absent from the summary | skipped (no passphrase) — task 04, 2026-11-09: the board is reachable on `:443` and the TLS/`wss://` transport rows below are measured, but this board's control passphrase is not the default (`status` says `dflt:0`) and a verification run will not change a credential on a device it cannot put back. To close it: `console.py token <known>` on a board you are willing to re-provision, then drive `wss://` |
| a chord is the keys its label claims | `python3 tools/relayctl.py --host keyrelay.local chord 04 2b` while `watch_target.py` runs | `LAlt press` then `Tab press` — and **not** `LCtrl`. The watcher is the only thing that says what the target received (README: Alt+Tab was `Ctrl+Alt+A` on the wire once) | |
| a chord is the keys its label claims | `python3 tools/relayctl.py --host keyrelay.local chord 04 2b` while `watch_target.py` runs | `LAlt press` then `Tab press` — and **not** `LCtrl`. The watcher is the only thing that says what the target received (README: Alt+Tab was `Ctrl+Alt+A` on the wire once) | skipped (no device run) — phase-03 close, 2026-11-09. This row is still open for the same standing reason as every row in the phase-03 table below: the board is a live device this pass neither reflashed nor armed (arming means typing into whatever this operator's console has focus, and the passphrase is the operator's). Host-side the claim is pinned instead: `test_proto` asserts every labelled chord button's `data-chord` bytes against the combination its label promises, so `04:2b` is Alt+Tab or the suite fails. To close: reflash `bridge`, arm, run the command above with the watcher open |
| served page == built page | `curl -s http://keyrelay.local/ \| wc -c` (after phase 02 lands: `curl -sk https://keyrelay.local/ \| wc -c`) | byte-equal to the `page bytes:` line the gate prints. A mismatch means the page in flash is stale — reflash **and** reload any open tab | ok — re-measured 2026-11-09 **over TLS after the phase-02 flash**: `curl -sk https://10.2.0.22/` = 48598 and `curl -s http://keyrelay.local/` = 48598, both = the gate's count. **Task 04** re-ran it over TLS three times in a row: 48 598 bytes each time with `sha256` equal to the raw string in `src/web_page.h` (`f7aae9e5…c6f7`) — the page now leaves through `kr::Conn` at 1460 B per write and 5840 B per tick (34 chunks over ~9 `loop()` iterations) instead of one `WiFiClient::print`, so byte-equality across a multi-tick send is the transport claim. Original — measured 2026-11-09: `curl -s http://keyrelay.local/ \| wc -c` = 48598 = gate's count (board on the LAN, pre-TLS, serving over http as expected). Re-run by the phase 01 final pass, one capped attempt each: 200 / 48 598 bytes, 404 on `/nope`, 401 on `/status` without the token, zero `src=`/`href=` in the served bytes. The first cold probe returned nothing (`http_code=000`, glibc does not cache mDNS) and the retry worked — that retry is the documented behaviour, not a second attempt at the check |
| silence disarms | arm, kill the controller process without a clean close (or close the tab), wait `kNoClientTimeoutMs` (8 s), `python3 tools/relayctl.py --host keyrelay.local status` | `armed: 0`, and `watch_target.py` shows releases for anything held. Test the *timer*, not the socket close (README bug 10) | skipped (no passphrase), same reason as the row above. What task 04 *did* pin is the property this row depends on and that the transport rewrite could silently have moved: `_lastInboundMs` is stamped in exactly one place, gated on `_phase == WebSocket && _authed`, and neither the TLS handshake, a `/status` poll nor a viewer refresh reaches it — asserted structurally in `test_relayctl` (`the controller clock is stamped in exactly one place`, `no HTTP path can refresh the controller clock`) |
| nothing outlives the link | hold a key and a mouse button, then drop the link | every key up, every button up; mouse summary shows no `still held` | |
| nothing outlives the link | hold a key and a mouse button, then drop the link | every key up, every button up; mouse summary shows no `still held` | skipped (no device run) — phase-03 close, 2026-11-09, same standing as the row above. Phase 03 is the phase that makes this row sharper (a *handover* is a link teardown with a new occupant waiting, and the claim path must not keep anything alive), so the host-side pins are the ones named in the phase-03 table: `test_output`'s release-funnel suite and `test_relayctl`'s funnel-runs-before-adoption pin. To close: the run described in the phase-03 table |
### Phase 02 — HTTPS + `wss://`
@@ -84,9 +84,11 @@ identity. The instrumentation stays worth having — it is what makes the next c
| claim | command | expected observation | outcome |
|---|---|---|---|
| takeover releases the incumbent's held keys | two controllers on one board: incumbent holds a key, newcomer takes over (`relayctl.py --take-over`), all while `watch_target.py` runs **on the target** | `watch_target.py` shows the key **up** *before* the new session's first key-down — the ordering is the whole claim | |
| the board comes back disarmed | `python3 tools/relayctl.py --host keyrelay.local status` immediately after the handover | `armed: 0` across the handover; the newcomer must arm again on purpose; nothing in the handover path arms | |
| a third controller is still refused | a third `/ws` upgrade while two sessions exist | `E3` — takeover consumes the one spare slot, it does not open the door | |
| a second `/ws` upgrade arrives as a pending claim, not a 503; a third socket is still refused (task 01) | after reflashing `bridge` from this tree: controller A connects and authenticates; a second socket does a raw `/ws` upgrade (`openssl s_client -connect keyrelay.local:443` + upgrade headers, or a python one-liner) and sends **one** deliberately-wrong `T`; a third connection opens in parallel; `watch_target.py` runs on the target throughout | second upgrade answers **101** (not 503), then exactly `E1:wrong passphrase` and close (one attempt only — the claim shares the board-wide 5-fail lockout); the third connection is refused (fatal TLS alert / close) and `slotrefused`/`rejected` move while both listeners keep serving; A keeps typing throughout and the silence clock never moves | **skipped (not run on device), 2026-11-09** — the live board runs the phase-02 firmware; the claim path is not flashed, and the authenticating half needs the operator's passphrase, which verification runs must not guess (the same call phase 02 recorded, and more pointed here: a wrong claim token feeds the shared lockout this task wires). Host-side the gate is green: 95 new `test_claim` checks (whitelist matrix over every `proto::Cmd` × auth × role, both deadline clocks at the boundary and across the `millis()` rollover, oversized-frame refusal through the real codec), 4921 checks in both suite runs, all three firmware builds; bridge RAM 99 728 B vs the phase-02 99 616 B — **+112 B**, the claim's whole footprint (64 B buffer + reader state + role/auth bytes + the 4 B clock, matching the comment in `control_server.h`) |
| takeover releases the incumbent's held keys | after reflashing `bridge` from this tree, two controllers on one board: incumbent holds a key, newcomer takes over (`relayctl.py --take-over` — landed in task 03; or a raw `/ws` upgrade sending `T<passphrase>` then `X`), all while `watch_target.py` runs **on the target** | `watch_target.py` shows the key **up** *before* the new session's first key-down — the ordering is the whole claim | **skipped (no device run), 2026-11-09** — same standing as the task-01 row directly above: the live board still runs the phase-02 firmware (no claim path), and both controllers need the operator's passphrase, which a verification run must not guess into the shared 5-fail lockout. The ordering is what the run will observe; on the host it is *pinned*: `test_relayctl.py` fails if `closeLink("taken over")` ever moves after `_authed = true` in `handOverClaim` (demonstrated failing on an inverted tree, then reverted), and the funnel it runs is `test_output`'s release-funnel suite. To close: reflash `bridge`, run the two-controller procedure above, capture the transcript |
| the board comes back disarmed | `python3 tools/relayctl.py --host keyrelay.local status` immediately after the handover | `armed: 0` across the handover; the newcomer must arm again on purpose; nothing in the handover path arms | **skipped (no device run), 2026-11-09** — same reason as the row above. Host-side the sentence is the (b) pin of `test_relayctl.py`'s **bridge handover** section: no `setArmed` / `armed = true` / `arm(` exists in `handOverClaim`'s code lines nor in the controller-side `X` case (demonstrated failing when a `…_rearm();` line was inserted, then reverted); `forgetPendingWrite()` is the one TLS state the adoption touches, and it writes no arming state. To close: the same reflash and run, `armed: 0` in the status read between handover and the newcomer's first `A1` |
| a third controller is still refused | a third `/ws` upgrade while two sessions exist | `E3` — takeover consumes the one spare slot, it does not open the door | **skipped (no device run), 2026-11-09 (phase close)** — the live board still runs the pre-phase-03 firmware, so the second session would be the only claim-shaped thing on it, and opening the two sessions first needs the operator's passphrase. Host-side: `test_claim` pins `claim::acceptsUpgrade()` (a slot that already carries a Claim takes no second upgrade) and `test_tls_session` walks the admission table (`controller`, `spare`, `refused`) with the cap checked before the free slot. Note the mechanism, as the README's Security model states it: a third *socket* never reaches HTTP, so what it gets is the slot refusal — a fatal TLS alert record on `:443`, a close on `:80`, counted in `slotrefused` — and `503`/`E3` is the sentence for an upgrade that reaches the spare slot and cannot be a claim (a malformed upgrade, or that slot already holding one). To close: reflash `bridge`, hold controller + claim, open the third connection, read `slotrefused` moving while both listeners keep serving |
| the locked-out operator sees `held by another controller` and takes the bridge with one click; the incumbent's page says it was taken over, disarmed, in its log (task 03) | reflash `bridge` from this tree (check the build's own exit status first); laptop tab holds the bridge; on the phone open `https://keyrelay.local/`, type the passphrase, connect — the pill must read `held by another controller`, the **take over** button appears, and nothing further leaves the phone until it is clicked; press it while `watch_target.py` runs **on the target**; then read the laptop tab | pill `held by another controller`; one click sends `T` then `X` and nothing else; the target shows every key and button up before the phone's next key; the phone is DISARMED after the handover; the laptop tab shows pill `taken over` and a log line naming "another controller took over", and does not reconnect (no second `/ws` from it); `--take-over` from the terminal produces the same frames and states | **skipped (no device run), 2026-11-09** — same standing as the rows above: the live board still runs pre-phase-03 firmware, the gesture needs the operator's passphrase on two devices (guessing feeds the shared 5-fail lockout), and a phone-and-laptop dance is not a headless attempt. Host-side the gate is green: `test_page.js` executes exactly this flow (claim-close → button appears → nothing sent until the click → click sends `T` then `X` and nothing else → first status logged as the DISARMED start; `E12` disarms, releases the mouse, ends a typing run, marks `taken over`, and stays down through the close; `E3` keeps its own "no room" sentence), and `test_relayctl.py` pins the CLI's `T,X,H` ordering, the default refusal naming `--take-over`, and the free-board no-op. To close: the reflash and two-device run above, transcript captured |
### Phase 04 — responsive page
@@ -0,0 +1,18 @@
**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/`.
@@ -0,0 +1,213 @@
== host suites: bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
keystate 40 checks, 0 failures
proto 2379 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 280 checks, 0 failures
test_claim 102 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
the bridge handover (phase 03)
one routing path, two transports
the client side of pinning
the busy board and --take-over
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 308 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
the busy board and the take-over
element ids
page 142 checks, 0 failures
RESULT: all suites passed
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
sanitizers: on
keystate 40 checks, 0 failures
proto 2379 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 280 checks, 0 failures
test_claim 102 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
the bridge handover (phase 03)
one routing path, two transports
the client side of pinning
the busy board and --take-over
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 308 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
the busy board and the take-over
element ids
page 142 checks, 0 failures
RESULT: all suites passed
== firmware: platformio run -e bridge -e console -e rollback ==
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/bridge/partitions.bin
Retrieving maximum program size .pio/build/bridge/firmware.elf
Checking size .pio/build/bridge/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 30.4% (used 99744 bytes from 327680 bytes)
Flash: [==== ] 40.0% (used 1257933 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.84 seconds =========================
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/console/partitions.bin
Retrieving maximum program size .pio/build/console/firmware.elf
Checking size .pio/build/console/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [== ] 20.6% (used 67352 bytes from 327680 bytes)
Flash: [==== ] 37.5% (used 1178173 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.85 seconds =========================
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/rollback/partitions.bin
Retrieving maximum program size .pio/build/rollback/firmware.elf
Checking size .pio/build/rollback/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
Flash: [==== ] 36.8% (used 1157609 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.84 seconds =========================
Environment Status Duration
------------- -------- ------------
bridge SUCCESS 00:00:00.838
console SUCCESS 00:00:00.848
rollback SUCCESS 00:00:00.837
========================= 3 succeeded in 00:00:02.523 =========================
== page size: src/web_page.h raw strings ==
page bytes: 59286 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
== README/AGENTS claims vs what this run measured ==
README/AGENTS claims: OK (5 026 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 59 286 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
== repo hygiene ==
repo hygiene: OK (109 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
== hardware context (informational; cannot change the gate result) ==
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
hw-probe node /usr/bin/node — test_page.js runs
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
+ the numbers README/AGENTS quote + hygiene)
@@ -0,0 +1,15 @@
All green. Report:
**Implemented (task `03_session_takeover/01_pending_claim_socket.md`)**
- `src/claim.h` (new pure module): `ViewerRole`, `acceptsUpgrade()` (one claim at a time), the **one visible whitelist** `honours()` — allowed: `T<token>` (unauth+auth) and `K` (auth); refused by name: `A1`/`A0`, `S=`, `C`, `Y=`, `P=`, `M=`, `R`, `H`, `Bad`/`None`; `X` reserved for task 02 on one marked line; two-clock `expired()` deadline arithmetic (wrap-safe).
- `control_server.{h,cpp}`: spare slot gained `_viewerRole`/`_viewerPhase`/`_claimAuthed`/`_claimAtMs`/`_claimMsg[64]`+`_claimReader` (~110 B, commented); `/ws` upgrade on the spare is accepted into Claim (same `ws::acceptFor` + one shared 101 writer — the "exactly one status line" pin holds); claim auth = same constant-time compare, same shared `_authFails`/`_lockUntilMs`, E1/E2 + drop on failure; whitelist violation = E6 + drop; claim never touches `_lastInboundMs`/`_controllerSeen`/`_onMessage`; `_claimAtMs` silence/auth timeouts drop and free the session; stopViewer/both accept paths reset claim state; third socket still refused at the slot table.
- `config.h`: `kClaimMsgCap = 64`, `kClaimTimeoutMs = 15000` with reasoning.
- `tests/test_claim.cpp` (95 checks) registered in `run_tests.sh`.
**Verification** — `bash .agents/validate.sh` → **validation OK**: 4921 checks plain and sanitised (12 Tests-table rows match; `test_proto` 2359 and `test_relayctl` 271 **untouched and green** — no wire change, `X` still parses `Bad`); bridge/console/rollback all build; page 51 626 B unchanged; hygiene OK. Bridge RAM 99 728 B vs 99 616 B baseline (**+112 B**, matching the field comment); rollback +112 B likewise.
**Docs** — README: check counts (intro + quick start), new `test_claim` Tests-table row, `claim.h` in the Architecture map (nine→ten pure), rewrote the stale "second upgrade refused 503/E3" Security-model paragraph and the Wire-protocol refusals paragraph. AGENTS.md: invariant 8 restated (claim cannot drive the keyboard; dispatcher never sees a claim frame) + count. `.agents/HW_VERIFICATION.md`: task-01 device row recorded as **skipped (not run on device)** — live board still runs phase-02 firmware and the authenticating half needs the operator's passphrase (guessing feeds the shared 5-fail lockout this task wires); exact close-out commands written into the row.
**Deviations** — task text said the claim buffer is "a sixteenth" of the controller's; 64 vs `kWsMessageCap` 256 is a **quarter** — code, tests and README carry the measured ratio and say so. `ViewerRole` lives in `claim.h` with a `using` alias in `ControlServer` so the host suite walks the same type. No live changes made.
**Next pending task:** `03_session_takeover/02_takeover_funnel.md` (`X` claims the bridge; `E12`; funnel before adoption).
@@ -0,0 +1,207 @@
== host suites: bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
keystate 40 checks, 0 failures
proto 2359 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 268 checks, 0 failures
test_claim 95 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
one routing path, two transports
the client side of pinning
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 271 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
element ids
page 113 checks, 0 failures
RESULT: all suites passed
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
sanitizers: on
keystate 40 checks, 0 failures
proto 2359 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 268 checks, 0 failures
test_claim 95 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
one routing path, two transports
the client side of pinning
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 271 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
element ids
page 113 checks, 0 failures
RESULT: all suites passed
== firmware: platformio run -e bridge -e console -e rollback ==
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/bridge/partitions.bin
Retrieving maximum program size .pio/build/bridge/firmware.elf
Checking size .pio/build/bridge/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 30.4% (used 99728 bytes from 327680 bytes)
Flash: [==== ] 39.7% (used 1249045 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.87 seconds =========================
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/console/partitions.bin
Retrieving maximum program size .pio/build/console/firmware.elf
Checking size .pio/build/console/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [== ] 20.5% (used 67336 bytes from 327680 bytes)
Flash: [==== ] 37.2% (used 1169305 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.86 seconds =========================
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/rollback/partitions.bin
Retrieving maximum program size .pio/build/rollback/firmware.elf
Checking size .pio/build/rollback/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
Flash: [==== ] 36.5% (used 1148945 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.86 seconds =========================
Environment Status Duration
------------- -------- ------------
bridge SUCCESS 00:00:00.870
console SUCCESS 00:00:00.864
rollback SUCCESS 00:00:00.863
========================= 3 succeeded in 00:00:02.597 =========================
== page size: src/web_page.h raw strings ==
page bytes: 51626 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
== README/AGENTS claims vs what this run measured ==
README/AGENTS claims: OK (4 921 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 51 626 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
== repo hygiene ==
repo hygiene: OK (109 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
== hardware context (informational; cannot change the gate result) ==
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
hw-probe node /usr/bin/node — test_page.js runs
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
+ the numbers README/AGENTS quote + hygiene)
@@ -0,0 +1,15 @@
Everything is 100 % green. Final report:
**Implemented (`03_session_takeover/02_takeover_funnel.md`)**
- `proto.h`: `Cmd::Takeover` for a lone `X` (trailing bytes → `Bad`), header direction table + `E12` error-list entry; `formatError` untouched.
- `control_server.{h,cpp}`: `handOverClaim()` — exact order `E12`→incumbent (best-effort) → `closeLink("taken over")` (the bug-10 funnel: disarm + release) → transport transplant → adopt (`_authed/_controllerSeen/_lastInboundMs/_connectedAtMs`, reader reset, `OK`+`sendStatus()`, `_onLinkChange(true)`) → claim slot fully vacated (state + buffers zeroed). `kErrTakenOver=12`, `_takeovers` counter + `takeovers()`, `sendOk()`; `arm()` never called. TLS slot transplant done as a *pointer swap* over two `Session` storage objects (no crypto state moves) + new `Session::forgetPendingWrite()` (documented: mbed TLS resumes half-records from its own buffer; without it the adopted TX path would wedge).
- `claim.h`: `X` joins the whitelist on its marked line, authenticated branch only; `main.cpp`: controller-side `X` = no-op answered `OK` (not `E6`); serial `link … taken=N`.
- Defect fixed (pre-existing, serves `E12`): the page read error codes as one character — `E10`/`E11`/`E12` misread as `E1`; now parsed to the first `:` (page 51 626 → 52 041 B, all pins moved with it).
**Verification** — `bash .agents/validate.sh` → **validation OK**: 4 954 checks plain + sanitised, 0 failures (`test_proto` 2 369, `test_claim` 102, `test_relayctl` 287 incl. the three handover pins — each demonstrated FAILING on an inverted tree, then reverted); `bridge/console/rollback` all build (bridge RAM 99 744 B = +16 B over task 01); page 52 041 B; hygiene OK. Device: **skipped (no device run)** recorded in `.agents/HW_VERIFICATION.md` — live board still runs phase-02 firmware and the procedure needs the operator's passphrase (guessing feeds the shared lockout); no live changes made; close-out commands written into the rows.
**Docs** — README: wire table `X` row, `E12` error code, "Single controller" paragraph now states takeover runs through the funnel and **cannot arm**, refusals/troubleshooting "no takeover" sentences corrected, Tests-table rows + totals, page-size quotes. AGENTS.md: check count + invariant 8's now-false "does nothing else" sentence only.
**Deviations** — the status-frame `takeovers`/JSON field is left to task 03 (whose file explicitly assigns "StatusSnapshot gains … `uint32_t takeovers`"); here the counter is published via the getter and the serial `status` line. Frames pipelined behind `X` become ordinary controller frames after adoption (documented — the handover itself still never arms).
**Next pending task:** `03_session_takeover/03_status_page_takeover_ui.md` (`busy`, TAKE OVER affordance, `E12` page handling).
@@ -0,0 +1,209 @@
== host suites: bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
keystate 40 checks, 0 failures
proto 2369 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 268 checks, 0 failures
test_claim 102 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
the bridge handover (phase 03)
one routing path, two transports
the client side of pinning
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 287 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
element ids
page 113 checks, 0 failures
RESULT: all suites passed
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
sanitizers: on
keystate 40 checks, 0 failures
proto 2369 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 268 checks, 0 failures
test_claim 102 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
the bridge handover (phase 03)
one routing path, two transports
the client side of pinning
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 287 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
element ids
page 113 checks, 0 failures
RESULT: all suites passed
== firmware: platformio run -e bridge -e console -e rollback ==
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/bridge/partitions.bin
Retrieving maximum program size .pio/build/bridge/firmware.elf
Checking size .pio/build/bridge/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 30.4% (used 99744 bytes from 327680 bytes)
Flash: [==== ] 39.8% (used 1250533 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.86 seconds =========================
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/console/partitions.bin
Retrieving maximum program size .pio/build/console/firmware.elf
Checking size .pio/build/console/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [== ] 20.6% (used 67352 bytes from 327680 bytes)
Flash: [==== ] 37.2% (used 1170789 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.87 seconds =========================
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/rollback/partitions.bin
Retrieving maximum program size .pio/build/rollback/firmware.elf
Checking size .pio/build/rollback/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
Flash: [==== ] 36.6% (used 1150201 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.87 seconds =========================
Environment Status Duration
------------- -------- ------------
bridge SUCCESS 00:00:00.859
console SUCCESS 00:00:00.866
rollback SUCCESS 00:00:00.870
========================= 3 succeeded in 00:00:02.595 =========================
== page size: src/web_page.h raw strings ==
page bytes: 52041 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
== README/AGENTS claims vs what this run measured ==
README/AGENTS claims: OK (4 954 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 52 041 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
== repo hygiene ==
repo hygiene: OK (109 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
== hardware context (informational; cannot change the gate result) ==
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
hw-probe node /usr/bin/node — test_page.js runs
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
+ the numbers README/AGENTS quote + hygiene)
@@ -0,0 +1,13 @@
Everything is 100 % green. Final report:
**Implemented (`03_session_takeover/03_status_page_takeover_ui.md`)**
- `status.h` + `main.cpp`: `StatusSnapshot` gains `busy` (= `control.linked()`) and `takeovers`; `statusJson()` emits `"busy":u` / `"tkov":u` — a link state and an event counter, no address/agent/identity anywhere (pinned). Longest legal frame measured at **349 B**; `kStatusJsonCap` stayed 384 because the test-enforced ≥32-byte headroom rule holds (35 left) — `config.h` comment rewritten with the new arithmetic (measured, not grown by guess; the task's "grow" instruction was conditional on the numbers, which the suite re-measures).
- `web_page.h`: link pill's third state `held by another controller` (from `s.busy` / claim non-adoption); **take over** button appears only after the board says held; one click sends `T` then `X` and nothing else, page lands DISARMED with a log line; `E12` disarms + releases mouse + aborts typing + pill `taken over` + auto-reconnect off (no re-claim fight); `E3` keeps its "no room for a claim" meaning with its own wording; Connection-card hint states takeover cannot arm.
- `relayctl.py`: `--take-over`; adoption probe (`OK`-then-status vs `OK`-then-silence = the board reporting busy; no probing frame sent); default = refusal naming `--take-over` with no `X` on the wire; flag on a free board = stated no-op; `_poll` now drains the whole batch (a status riding behind `OK` is kept, not abandoned — bug found and fixed).
- `claim.h`: the `H`-refusal comment corrected — the claimant's answer *is* its own non-adoption; `busy` serves pollers and post-handover frames.
**Verification** — `bash .agents/validate.sh` → **validation OK, exit 0**: 5 026 checks plain + sanitised, 0 failures (`test_proto` 2 379, `test_page` 142, `test_relayctl` 308 incl. T→X ordering / default refusal / free-board no-op / `T,X,H` through `main()`); `bridge/console/rollback` all build (bridge RAM 99 744 B — unchanged; Flash 1 257 933 B, page +7 245 B); page 59 286 B, `test_conn` chunk math updated (41 chunks / 11 ticks); hygiene OK. Device: **skipped (no device run)** recorded in `.agents/HW_VERIFICATION.md` — a board is reachable but runs pre-phase-03 firmware, and the two-device gesture needs the operator's passphrase (guessing feeds the shared lockout); **no live changes made**; close-out commands written into the row.
**Docs** — README: check count ×2, page 59 286 B, S-frame row (`busy`/`tkov`), trust-table row "someone holding the passphrase … can take the bridge over, cannot arm it by taking it", Controller-page pill/take-over rows, Security-model busy-is-not-identity sentence, CLI `--take-over` docs + example, Troubleshooting page/CLI rows, Tests-table rows/descriptions. AGENTS.md: check count only (its stale-fact test).
**Next pending task:** `03_session_takeover/04_takeover_tests_docs.md` (`relayctl`/frame test completion, pin-inversion demonstration, README final pass).
@@ -0,0 +1,213 @@
== host suites: bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
keystate 40 checks, 0 failures
proto 2379 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 280 checks, 0 failures
test_claim 102 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
the bridge handover (phase 03)
one routing path, two transports
the client side of pinning
the busy board and --take-over
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 308 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
the busy board and the take-over
element ids
page 142 checks, 0 failures
RESULT: all suites passed
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
sanitizers: on
keystate 40 checks, 0 failures
proto 2379 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 280 checks, 0 failures
test_claim 102 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
the bridge handover (phase 03)
one routing path, two transports
the client side of pinning
the busy board and --take-over
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 308 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
the busy board and the take-over
element ids
page 142 checks, 0 failures
RESULT: all suites passed
== firmware: platformio run -e bridge -e console -e rollback ==
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/bridge/partitions.bin
Retrieving maximum program size .pio/build/bridge/firmware.elf
Checking size .pio/build/bridge/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 30.4% (used 99744 bytes from 327680 bytes)
Flash: [==== ] 40.0% (used 1257933 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.87 seconds =========================
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/console/partitions.bin
Retrieving maximum program size .pio/build/console/firmware.elf
Checking size .pio/build/console/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [== ] 20.6% (used 67352 bytes from 327680 bytes)
Flash: [==== ] 37.5% (used 1178173 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.85 seconds =========================
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/rollback/partitions.bin
Retrieving maximum program size .pio/build/rollback/firmware.elf
Checking size .pio/build/rollback/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
Flash: [==== ] 36.8% (used 1157609 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.86 seconds =========================
Environment Status Duration
------------- -------- ------------
bridge SUCCESS 00:00:00.866
console SUCCESS 00:00:00.855
rollback SUCCESS 00:00:00.858
========================= 3 succeeded in 00:00:02.579 =========================
== page size: src/web_page.h raw strings ==
page bytes: 59286 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
== README/AGENTS claims vs what this run measured ==
README/AGENTS claims: OK (5 026 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 59 286 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
== repo hygiene ==
repo hygiene: OK (109 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
== hardware context (informational; cannot change the gate result) ==
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
hw-probe node /usr/bin/node — test_page.js runs
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
+ the numbers README/AGENTS quote + hygiene)
@@ -0,0 +1,12 @@
**Report — task 03/04 (`04_takeover_tests_docs.md`)**
- Implemented:
- Verified the three takeover source pins in `tests/test_relayctl.py` (funnel-before-adoption; no `arm(`/`setArmed`/`armed = true` in `handOverClaim` or the controller-side `X` case; whitelist consulted before the Takeover branch and `X` only in `claim.h`'s authenticated branch; `E12` named code + incumbent send) — all present and re-read against `control_server.cpp`/`claim.h`/`main.cpp`.
- Re-read bug 10's three existing pins + the 4-writer `_lastInboundMs` census against the phase-03-touched code: sentences still true (not just green).
- README: takeover bullet in "Disarmed is the only default"; end-to-end takeover procedure (`watch_target` + `key KeyA --hold 30000` + `--take-over status`, key-up-before-first-newcomer-key ordering) + evidence-table row; bugs 12 (page read `E<code>` as one char — `E10/E11/E12` misread as `E1`) and 13 (relayctl `_poll` abandoned the batch behind `OK`, breaking the adoption probe) added; section → "Thirteen bugs", all 6 cross-refs (README ×3, AGENTS.md ×2, `tls_pem.h` comment) updated.
- Page safety sentences confirmed in `test_page.js` (disarm/mouse-release/run-end/`taken over`/no-auto-reclaim on `E12`; nothing sent before the click; `E3` distinct). AGENTS.md: stale bug-count titles fixed only; invariant 8 already described the claim.
- Tests/lint: `bash .agents/validate.sh` → **validation OK**: 5 026 checks ×2 (plain + `SANITIZE=1`), 12 Tests-table rows match suites, page 59 286 B, Backlog contiguous, ws:// sentences clean; `bridge`/`console`/`rollback` builds SUCCESS; hygiene OK.
- Pin inversions proven (then reverted, no leftovers): (a) `closeLink` moved after adoption → exactly 1 FAIL ("funnel runs BEFORE…"); (b) `rearm();` inserted → 1 FAIL ("no `arm(`…"); (c) `claim.h` admits `X` unauthed → pin FAIL + `test_claim` matrix cell FAIL.
- README/AGENTS changes: as above; AGENTS.md gained no prose, only stale-fact fixes.
- Live infra: **no live changes.** A board is reachable but runs pre-phase-03 firmware; the two-controller takeover needs the operator's passphrase (guessing feeds the shared 5-fail lockout) and a reflash is a device side effect — the takeover rows in `.agents/HW_VERIFICATION.md` stand as `skipped (no device run)` with the exact close-out commands.
- Next pending: phase-03 final pass; then `04_controller_page_responsive/01_responsive_layout.md`.
@@ -0,0 +1,213 @@
== host suites: bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
keystate 40 checks, 0 failures
proto 2379 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 280 checks, 0 failures
test_claim 102 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
the bridge handover (phase 03)
one routing path, two transports
the client side of pinning
the busy board and --take-over
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 308 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
the busy board and the take-over
element ids
page 142 checks, 0 failures
RESULT: all suites passed
== host suites: SANITIZE=1 bash tests/run_tests.sh ==
compiler: g++ inside localhost/ai-sandbox:ubuntu-9a08b1d500b0 (repo mounted at /work)
sanitizers: on
keystate 40 checks, 0 failures
proto 2379 checks, 0 failures
text 459 checks, 0 failures
mouse 48 checks, 0 failures
output 153 checks, 0 failures
ws_codec 135 checks, 0 failures
test_tls_pem 862 checks, 0 failures
test_tls_session 118 checks, 0 failures
test_conn 280 checks, 0 failures
test_claim 102 checks, 0 failures
handshake
frame encoding
frame decoding
key table
pasted text
mouse
protocol strings
the fail-safe wiring
the bridge handover (phase 03)
one routing path, two transports
the client side of pinning
the busy board and --take-over
the target-side verifier
t= 0.00s >>> Ctrl+Alt+Del <<<
relayctl 308 checks, 0 failures
the log stays readable
characters
key names in the log
sending text
refusing what it cannot type
stopping
the mouse
the connect path
the channel pill and the certificate line
the busy board and the take-over
element ids
page 142 checks, 0 failures
RESULT: all suites passed
== firmware: platformio run -e bridge -e console -e rollback ==
Processing bridge (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/bridge/partitions.bin
Retrieving maximum program size .pio/build/bridge/firmware.elf
Checking size .pio/build/bridge/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 30.4% (used 99744 bytes from 327680 bytes)
Flash: [==== ] 40.0% (used 1257933 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.88 seconds =========================
Processing console (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/console/partitions.bin
Retrieving maximum program size .pio/build/console/firmware.elf
Checking size .pio/build/console/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [== ] 20.6% (used 67352 bytes from 327680 bytes)
Flash: [==== ] 37.5% (used 1178173 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.87 seconds =========================
Processing rollback (platform: espressif32@8.0.0; board: esp32-s3-devkitc-1; framework: arduino)
--------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (8.0.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 4.30312.261024 (3.3.12)
- framework-arduinoespressif32-esp32s3-libs @ 3.3.12
- tool-esptool @ 2.50400.0 (5.4.0)
- tool-riscv32-esp-elf-gdb @ 12.1.0+20221002
- tool-xtensa-esp-elf-gdb @ 12.1.0+20221002
- toolchain-riscv32-esp @ 14.2.0+20260121
- toolchain-xtensa-esp-elf @ 14.2.0+20260121
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 45 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Adafruit NeoPixel @ 1.15.5
|-- ESPmDNS @ 3.3.12
|-- USB @ 3.3.12
|-- Preferences @ 3.3.12
|-- WiFi @ 3.3.12
Building in release mode
Generating partitions .pio/build/rollback/partitions.bin
Retrieving maximum program size .pio/build/rollback/firmware.elf
Checking size .pio/build/rollback/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [=== ] 28.6% (used 93568 bytes from 327680 bytes)
Flash: [==== ] 36.8% (used 1157609 bytes from 3145728 bytes)
========================= [SUCCESS] Took 0.88 seconds =========================
Environment Status Duration
------------- -------- ------------
bridge SUCCESS 00:00:00.879
console SUCCESS 00:00:00.867
rollback SUCCESS 00:00:00.882
========================= 3 succeeded in 00:00:02.628 =========================
== page size: src/web_page.h raw strings ==
page bytes: 59286 (a reflashed device must serve exactly this many; kIndexHtmlLen is sizeof(kIndexHtml)-1 of these same bytes, both builds)
== README/AGENTS claims vs what this run measured ==
README/AGENTS claims: OK (5 026 checks quoted and measured in both runs; 12 Tests-table rows match their suites; 59 286 page bytes quoted; Backlog contiguous; every ws:// sentence names the rollback)
== repo hygiene ==
repo hygiene: OK (109 tracked files, no ignored-tracked files, no key-material shapes, no PII hits)
== hardware context (informational; cannot change the gate result) ==
hw-probe serial /dev/ttyACM0 present, readable (NOT opened: opening toggles DTR/RTS and resets the board)
hw-probe mdns keyrelay.local resolves -> 10.2.0.22
hw-probe https https://keyrelay.local/ -> HTTP 200 (TLS listener is up)
hw-probe http http://keyrelay.local/ -> HTTP 301, redirects to https://keyrelay.local/ (redirect-only: the post-phase-02 shape)
hw-probe tls 10.2.0.22:443 negotiated TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 (a self-signed verify failure is the expected shape; the fingerprint to compare it against is on the serial console)
hw-probe uv /home/ducoterra/.local/bin/uv — firmware builds available
hw-probe node /usr/bin/node — test_page.js runs
hw-probe evdev board 046d:c33f visible: keyboard=/dev/input/event19 (readable) mouse=/dev/input/event20 (readable) — python3 tools/watch_target.py runs ON THIS machine (the target)
validation OK (suites + sanitised suites + bridge/console/rollback builds + page size
+ the numbers README/AGENTS quote + hygiene)