phase: 01_verification_gate
All green. Report: **Implemented / verified (phase 01 final pass)** - `.agents/validate.sh` re-verified as the gate: ①`tests/run_tests.sh` ②`SANITIZE=1` run ③`uv … platformio run -e bridge -e console` ④page byte count ⑤hygiene, then the informational `tools/hw_probe.sh`. Added an explicit `== repo hygiene ==` header + `repo hygiene: OK (57 tracked files…)` verdict line so every check prints one (previously hygiene passed in silence). - `.agents/HW_VERIFICATION.md`: baseline (bridge 26.7 %/87 592 B, 36.1 %/1 135 293 B; console 16.8 %/33.6 %; 3406 checks; page 48 598 B) re-confirmed; served-page row extended with this pass's one-attempt measurements. **Tests / gate (100 % green)** - `bash .agents/validate.sh` → **exit 0** (also from `/tmp`): `RESULT: all suites passed` twice (3406 = 3162 C++ + 160 relayctl + 84 page), `2 succeeded`, `page bytes: 48598`, `repo hygiene: OK`, `validation OK`. - Faults re-demonstrated then reverted: (a) inverted `test_keystate` assert → cold-copy gate `RESULT: FAILED`, **GATE_EXIT=1**; (b) markup id `pArmed`→`pArmedZ` → page suite rc 1; (c) type error in `src/main.cpp` → `bridge FAILED / console FAILED`, rc 1; (d) hygiene regex matches `certs/test.pem` (git index commands are forbidden in this pass — task 01's report holds the original live `git add` demo). - Lint: no linter is configured for this repo; compiler is the linter — `-Wall -Wextra` host suites + both firmware builds: **0 warnings**; `bash -n` clean; `py_compile` clean. `ruff check` (this machine's broad ruleset) reports 21 pre-existing *preview-rule* style nits in untouched `tools/*.py` + `tests/test_relayctl.py` (SIM/UP/ISC/EXE/RUF/FURB), zero correctness findings — not churned: `watch_target.py`/`relayctl.py` are hardware-verified artifacts outside this phase. `shellcheck` is absent here and not in the local container image (fetching it would violate the gate's no-network rule). Coverage: n/a (shell + docs; no new Python). **Completion criteria** — all met: exit 0 with per-check output ✓; SANITIZE in-gate ✓; no `-t upload`/device curl/network fetch (grep: only in comments; `run_tests.sh` checks `podman image exists` first, so never pulls) ✓; faults bite ✓; HW_VERIFICATION.md names command+expected observation for every 02–05 device claim + one-attempt rule ✓; full suite green, `git status` = phase artifacts + mandated doc edits, nothing stray ✓. **Documentation** — README inconsistencies found & fixed: `test_proto` 193→**2327** (table now sums to the headline 3406); duplicate stale `test_page 47` row deleted (element-id clause merged into the 84 row); "last six checks"/"fails 3 checks" claims replaced by measurement (5 fail-safe source pins; `linked()` gate → 2, accept-stamp → 1, both → 3); console line formats corrected to what the code prints (`mouse interface=present target=report protocol buttons=0x00 …`, `queue 0 keys pending…`, `target=BOOT protocol (refused)`); status-JSON sample gained `typed/typing/mouse/mbtn/mframes`; footprint 87 400/35.5 %→87 592/36.1 %; page delivery `Content-Length: 35164`→**48 598** (re-measured live, one capped curl: 200/48 598, 404 on `/nope`, 401 on `/status`); stale Backlog item "Mouse." removed and renumbered; "~35 KB"→"~48 KB"; page-button names matched to the real markup; `validate.sh`/`HW_VERIFICATION.md`/`hw_probe.sh` added to Architecture, Tests and End-to-end. AGENTS.md: only stale statements fixed — "one commit, `Initial import`… predates the mouse and `output.h`" and the pure-module list missing `mouse.h`/`output.h`; not expanded. **Deviations** — Did not run the keystroke/mouse device rows: the board's target is this very machine, so injecting input during an unattended pass is the side effect the README's "hazard nobody warns you about" forbids; those rows stay blank per the contract (read-only GETs only). No live infra changed. **Next pending phase:** `02_tls_control_channel` (tasks `01_tls_identity_module` … `06_tls_docs_rollback`).
This commit is contained in:
Executable
+160
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
# tools/hw_probe.sh — what device context exists RIGHT NOW. It informs; it does
|
||||
# not gate. Always exits 0, and .agents/validate.sh deliberately does NOT fold
|
||||
# its exit status into the gate's rc.
|
||||
#
|
||||
# Why this exists (AGENTS.md "Verification standard", PLAN.md "No unit blocks on
|
||||
# hardware"): device checks are the half of this roadmap's verification the host
|
||||
# gate cannot see. They are best effort — one attempt, wrapped in a timeout, the
|
||||
# outcome recorded in .agents/HW_VERIFICATION.md. This probe is the cheap way to
|
||||
# find out whether attempting one is even plausible, so a unit running unattended
|
||||
# never discovers the absence of a board by hanging on it.
|
||||
#
|
||||
# Rules this script obeys — the same rules the hardware contract states:
|
||||
# * No side effects. It writes nothing and NEVER opens a serial port: opening
|
||||
# /dev/ttyACM* asserts DTR/RTS and resets the board. `test -e`/`test -r` and
|
||||
# read-only sysfs metadata only. HTTP(S) probes are idempotent GETs.
|
||||
# * Never blocks. Every probe that could wait is capped with `timeout` or curl
|
||||
# -m. getent CAN hang when no mDNS responder answers, so it runs only under
|
||||
# `timeout` — if `timeout` itself is missing, the lookup is skipped with a
|
||||
# stated reason rather than risked.
|
||||
# * Always exits 0. Every fact is printed; none of them is a verdict. The
|
||||
# gate result comes from .agents/validate.sh; device verdicts come from the
|
||||
# outcome table in .agents/HW_VERIFICATION.md, filled in by whoever ran the
|
||||
# actual command — never inferred from these lines.
|
||||
#
|
||||
# One line per fact, prefix "hw-probe", so the output stays greppable:
|
||||
# serial nodes / keyrelay.local resolution / https / http(+redirect) /
|
||||
# uv / node / watch_target.py-capable evdev nodes.
|
||||
set -uo pipefail
|
||||
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# to <secs> <cmd...> — run capped; rc 124 = the cap fired, 125 = cannot cap, so
|
||||
# the caller must NOT run the command (an uncapped blocking probe is exactly
|
||||
# what this file's rule forbids).
|
||||
to() {
|
||||
local secs="$1"
|
||||
shift
|
||||
if have timeout; then
|
||||
timeout "$secs" "$@"
|
||||
else
|
||||
return 125
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 1. serial nodes (existence and readability ONLY — never opened) ----------
|
||||
serials=()
|
||||
for candidate in /dev/ttyACM* /dev/ttyUSB*; do
|
||||
[[ -e "$candidate" ]] && serials+=("$candidate")
|
||||
done
|
||||
if ((${#serials[@]} > 0)); then
|
||||
for s in "${serials[@]}"; do
|
||||
if [[ -r "$s" ]]; then
|
||||
echo "hw-probe serial $s present, readable (NOT opened: opening toggles DTR/RTS and resets the board)"
|
||||
else
|
||||
echo "hw-probe serial $s present, NOT readable (serial group membership? console.py would fail)"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "hw-probe serial no /dev/ttyACM* or /dev/ttyUSB* here — no board on a serial port"
|
||||
fi
|
||||
|
||||
# --- 2. keyrelay.local (mDNS via the resolver; may legitimately be slow) ------
|
||||
mdns_rc=0
|
||||
mdns_out="$(to 4 getent hosts keyrelay.local 2>/dev/null)" || mdns_rc=$?
|
||||
if ! have getent && ((mdns_rc != 0)); then
|
||||
echo "hw-probe mdns unknown — getent not installed; resolution state not checkable here"
|
||||
elif ((mdns_rc == 0)); then
|
||||
echo "hw-probe mdns keyrelay.local resolves -> ${mdns_out%%[[:space:]]*}"
|
||||
elif ((mdns_rc == 124)); then
|
||||
echo "hw-probe mdns keyrelay.local did not resolve within 4 s (no responder? glibc does not cache mDNS — a retry may still work; the IP is the fallback)"
|
||||
elif ((mdns_rc == 125)); then
|
||||
echo "hw-probe mdns unknown — 'timeout' is not installed and an uncapped resolver lookup can hang; skipped on principle"
|
||||
else
|
||||
echo "hw-probe mdns keyrelay.local did not resolve"
|
||||
fi
|
||||
|
||||
# --- 3./4. reachability of the two listeners -----------------------------------
|
||||
# Expectations shift when phase 02 lands: until then :80 serves the page and
|
||||
# :443 is closed; after it, :443 serves TLS and :80 is redirect-only. Both
|
||||
# halves print what IS, and name the expectation, so neither state reads as a
|
||||
# failure — this probe cannot fail, only inform.
|
||||
if have curl; then
|
||||
https_rc=0
|
||||
https_code="$(curl -sk -m 2 -o /dev/null -w '%{http_code}' https://keyrelay.local/ 2>/dev/null)" || https_rc=$?
|
||||
if [[ "$https_rc" == 0 && "$https_code" != 000 ]]; then
|
||||
echo "hw-probe https https://keyrelay.local/ -> HTTP $https_code (TLS listener is up)"
|
||||
else
|
||||
echo "hw-probe https https://keyrelay.local/ unreachable (curl rc $https_rc, code ${https_code:-000}) — expected until phase 02 (TLS) lands"
|
||||
fi
|
||||
|
||||
http_rc=0
|
||||
http_fact="$(curl -s -m 2 -o /dev/null -w '%{http_code} %{redirect_url}' http://keyrelay.local/ 2>/dev/null)" || http_rc=$?
|
||||
http_code="${http_fact%% *}"
|
||||
http_redirect="${http_fact#* }"
|
||||
if [[ "$http_rc" == 0 && "$http_code" == 3* && -n "$http_redirect" ]]; then
|
||||
echo "hw-probe http http://keyrelay.local/ -> HTTP $http_code, redirects to $http_redirect (redirect-only: the post-phase-02 shape)"
|
||||
elif [[ "$http_rc" == 0 && "$http_code" != 000 ]]; then
|
||||
echo "hw-probe http http://keyrelay.local/ -> HTTP $http_code, no redirect (plaintext page still served: the pre-phase-02 shape)"
|
||||
else
|
||||
echo "hw-probe http http://keyrelay.local/ unreachable (curl rc $http_rc) — board off, asleep, or this host is not on its network"
|
||||
fi
|
||||
else
|
||||
echo "hw-probe http skipped — curl not installed; https/http reachability unknown"
|
||||
fi
|
||||
|
||||
# --- 5./6. the two tools the gate and the suites pin ---------------------------
|
||||
if have uv; then
|
||||
echo "hw-probe uv $(command -v uv) — firmware builds available"
|
||||
else
|
||||
echo "hw-probe uv MISSING — the gate's firmware check will FAIL (AGENTS.md: pio on PATH is broken; fallback probe: find / -maxdepth 6 -name platformio -type d -path '*site-packages*')"
|
||||
fi
|
||||
if have node; then
|
||||
echo "hw-probe node $(command -v node) — test_page.js runs"
|
||||
else
|
||||
echo "hw-probe node MISSING — tests/run_tests.sh would skip the page suite and the gate pins it; the gate will FAIL"
|
||||
fi
|
||||
|
||||
# --- 7. watch_target.py-capable evdev nodes (informational, read-only) ---------
|
||||
# Same discovery rule watch_target.py uses: USB devices with the board's ids
|
||||
# (Logitech G815 identity, Espressif default accepted), nodes classified by the
|
||||
# kernel's capabilities/ev mask, never by node order — the same reasoning that
|
||||
# keeps the verifier honest. sysfs reads only; no /dev/input node is opened.
|
||||
kb=""
|
||||
ms=""
|
||||
board_ids=""
|
||||
for dev in /sys/bus/usb/devices/*/; do
|
||||
[[ -r "${dev}idVendor" && -r "${dev}idProduct" ]] || continue
|
||||
vid="$(<"${dev}idVendor")"
|
||||
pid="$(<"${dev}idProduct")"
|
||||
case "$vid:$pid" in
|
||||
046d:c33f | 303a:1001) : ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
for ev in "$dev"*/input/input*/event* "$dev"*/*/input/input*/event*; do
|
||||
[[ -e "$ev" ]] || continue
|
||||
caps_file="${ev%/*}/capabilities/ev" # built-in, not `dirname` — works with any PATH
|
||||
[[ -r "$caps_file" ]] || continue
|
||||
mask_hex="$(<"$caps_file")"
|
||||
[[ "$mask_hex" =~ ^[0-9a-fA-F]+$ ]] || continue
|
||||
mask=$((16#$mask_hex))
|
||||
node="/dev/input/${ev##*/}"
|
||||
readable="readable"
|
||||
[[ -r "$node" ]] || readable="NOT readable (input group?)"
|
||||
if ((mask & 4)); then # EV_REL — the pointer interface
|
||||
[[ -z "$ms" ]] && ms="$node ($readable)"
|
||||
elif ((mask & 1)); then # EV_KEY — the keyboard interface
|
||||
[[ -z "$kb" ]] && kb="$node ($readable)"
|
||||
fi
|
||||
board_ids="$vid:$pid"
|
||||
done
|
||||
done
|
||||
if [[ -n "$kb$ms" ]]; then
|
||||
echo "hw-probe evdev board $board_ids visible: keyboard=${kb:-none} mouse=${ms:-none} — python3 tools/watch_target.py runs ON THIS machine (the target)"
|
||||
else
|
||||
echo "hw-probe evdev no USB device with the board's ids (046d:c33f, 303a:1001) here — target-side claims are 'skipped (no device)' unless the board is plugged into another machine"
|
||||
fi
|
||||
|
||||
# Deliberate, unconditional: this script informs and must never decide anything.
|
||||
exit 0
|
||||
Reference in New Issue
Block a user