diff --git a/.gitignore b/.gitignore index beac4bcf..8b5c3912 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,13 @@ firmware/esp32-csi-node/test/*.obj # Claude Flow swarm runtime state .swarm/ -# CSI recordings (local training data, machine-specific) +# CSI recordings (local training/capture data — CSI is person data per +# CLAUDE.md; never commit). Covers current and legacy layouts. See ADR-296. +data/recordings/ +v2/data/recordings/ rust-port/wifi-densepose-rs/data/recordings/ +**/*.csi.jsonl +**/*.csi.meta.json # NVS partition images and CSVs (contain WiFi credentials) nvs.bin diff --git a/docs/adr/ADR-292-source-provenance-state-machine.md b/docs/adr/ADR-292-source-provenance-state-machine.md new file mode 100644 index 00000000..ef4acf5f --- /dev/null +++ b/docs/adr/ADR-292-source-provenance-state-machine.md @@ -0,0 +1,64 @@ +# ADR-292: Source provenance state machine — synthetic can never present as live + +- **Status**: Accepted — initial implementation (this PR) +- **Date**: 2026-08-11 +- **Deciders**: ruv +- **Tags**: provenance, honesty, ui, sensing-server, security + +## Context + +An August 2026 external review found two provenance defects on the release +path: + +1. The pose-fusion simulator starts in demo mode; on any page port other than + 3000 the WebSocket target falls back to `localhost:8765`, and if the + connection fails the simulator keeps running while the status still reads + "ready" — producing a convincing moving visualization with no live CSI + (issue 1557). +2. The main sensing client labels the source **live** when the authenticated + status endpoint returns an error for lack of authorization, until a real + frame happens to correct it (issue 1526). + +The common root cause: source state is a boolean (live vs not), so "unknown" +collapses to "live". CLAUDE.md requires MEASURED/CLAIMED/SYNTHETIC labeling +and forbids presenting synthetic output as real. + +## Decision + +Define one canonical, mutually exclusive `SourceState` enum shared by the +sensing server and every UI/client that renders a source: + +- `Synthetic` — generated data (simulator/replay of synthetic fixtures). +- `LiveVerified` — frames from an authenticated, attested source. +- `LiveUnverified` — frames arriving but provenance not yet confirmed. +- `Stale` — last frame older than a configured freshness window. +- `Disconnected` — no source. + +Rules enforced structurally: + +- **`Unknown` is not a state.** Any ambiguous condition resolves to + `LiveUnverified`, `Stale`, or `Disconnected` — never `LiveVerified`. +- A status-endpoint error resolves to `Disconnected`/`LiveUnverified`, never + live-verified. +- The simulator constructs `Synthetic` and cannot transition to any `Live*` + state without a verified frame. +- `Synthetic` is watermarked in every view and every export. +- Transitions are a pure function of (last-frame-age, auth-status, + source-kind) so they are unit-testable without a clock or a socket. + +Scope of this PR: the shared `SourceState` type + transition function + tests +in the sensing server, and wiring of the two identified surfaces (pose-fusion +simulator status, sensing client source label). Broader UI adoption follows. + +## Consequences + +- Closes the "synthetic shown as live" and "unknown shown as live" classes. +- A small breaking change to any consumer currently reading a boolean source + flag; mitigated by exposing a compatibility accessor during migration. + +## Validation + +- Unit tests for every transition, especially: auth-error → not-live; + simulator → never live without a verified frame; freshness expiry → `Stale`; + watermark present on synthetic export. +- `cargo test -p wifi-densepose-sensing-server`. diff --git a/docs/adr/ADR-293-sensor-data-plane-bind-hardening.md b/docs/adr/ADR-293-sensor-data-plane-bind-hardening.md new file mode 100644 index 00000000..a29db97f --- /dev/null +++ b/docs/adr/ADR-293-sensor-data-plane-bind-hardening.md @@ -0,0 +1,59 @@ +# ADR-293: Sensor data-plane hardening — UDP bind control and source allowlist (step one) + +- **Status**: Accepted — initial implementation (this PR) +- **Date**: 2026-08-11 +- **Deciders**: ruv +- **Tags**: security, udp, sensor-ingest, sensing-server + +## Context + +The CSI UDP receiver binds `0.0.0.0:{udp_port}` unconditionally +(`main.rs:5706`), with no equivalent of the HTTP `--bind-addr` flag (which +correctly defaults to `127.0.0.1`), no source allowlist, no message +authentication, no device identity, and no replay defense. Any host that can +reach the UDP port can inject a valid-shaped frame, flip an auto-detecting +server into a live source state, and influence presence/vital/automation +outputs (issue 1394). + +An IP allowlist does not stop LAN spoofing, but bind control plus an allowlist +is the correct, shippable first step; per-device keys + authenticated +encryption + monotonic sequence + freshness window + replay rejection is the +full fix and is larger. + +## Decision + +**This PR (step one):** + +- Add `--udp-bind` (env `RUVIEW_UDP_BIND`), **defaulting to `127.0.0.1`**. + Binding to a routable address is now an explicit operator choice, mirroring + the HTTP path. Desktop/appliance defaults stay loopback. +- Add an optional source IP/CIDR allowlist (`--udp-allow`); when set, frames + from other sources are dropped and counted. Loopback is always allowed. +- Emit a startup security log line stating the bind scope and whether an + allowlist is active; refuse a routable bind without an allowlist unless an + explicit `--udp-insecure-lan` override is passed (parallel to the existing + Docker HTTP refusal). +- Publish a `SECURITY.md`/advisory note describing the threat model and safe + deployment. + +**Explicitly deferred to a follow-up ADR (step two):** per-device provisioned +keys, MAC/AEAD, device identifiers, monotonic sequence numbers, freshness +window, and replay rejection. This ADR documents that gap rather than +implying the data plane is authenticated. + +## Consequences + +- Removes the default open-to-LAN exposure with a one-line-safe default. +- Not spoof-proof on a trusted LAN — the advisory says so plainly, and the + override name (`--udp-insecure-lan`) makes the residual risk legible. +- A behavior change for anyone relying on the old implicit `0.0.0.0` default; + called out in the changelog and the startup log. + +## Validation + +- Unit tests: default bind is loopback; routable bind without allowlist is + refused unless overridden; allowlist accept/drop with counting; loopback + always allowed. +- `cargo test -p wifi-densepose-sensing-server`. +- Real-silicon validation of the LAN path remains required before any + deployment claim. diff --git a/docs/adr/ADR-294-multi-node-semantic-correctness.md b/docs/adr/ADR-294-multi-node-semantic-correctness.md new file mode 100644 index 00000000..d7914580 --- /dev/null +++ b/docs/adr/ADR-294-multi-node-semantic-correctness.md @@ -0,0 +1,58 @@ +# ADR-294: Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state + +- **Status**: Accepted — initial implementation (this PR) +- **Date**: 2026-08-11 +- **Deciders**: ruv +- **Tags**: multi-node, mqtt, home-assistant, correctness, sensing-server + +## Context + +The external review confirmed three defects on the multi-node path — the core +mechanism RuView uses to reduce blind spots and room dependence: + +1. The active `NodeInfo` payload carries RSSI/position/subcarrier/sync but **no + per-node classification**; the MQTT mapper reads `node.classification` and + falls back to the room aggregate when absent, so every node can publish the + same aggregate presence value (issues 1540, 1554). +2. The MQTT `RateLimiter` is keyed by `EntityKind` only + (`mqtt/state.rs:65`), so one node consumes the numeric publish slot and the + others are suppressed until the interval expires, while availability still + says online (issue 1541). +3. In the UDP vital path, top-level classification is taken from the + latest-arriving node while other features are fused, so with disagreeing + nodes room presence can flip at packet frequency (issue 1555). + +## Decision + +- **Separate the types.** Introduce `NodeInference` (per-node classification + + confidence + freshness) distinct from `RoomInference` (the fused room + aggregate). `NodeInfo` carries a `NodeInference`; the room aggregate is + computed explicitly and never overwrites node state. No silent fallback from + node to room. +- **Key the rate limiter by (node, entity).** `RateLimiter` becomes keyed on + `(NodeId, EntityKind)` so nodes no longer starve each other; per-entity + behavior per node is preserved. +- **Deterministic fusion.** Room classification is a pure function of the set + of current per-node inferences (e.g. freshness-weighted vote), not + last-writer-wins; identical inputs yield identical room state. +- **Stale entities cannot stay online.** An entity whose backing node has not + reported within N expected publish intervals transitions to unavailable/ + stale rather than holding a frozen value while availability says online. + +## Consequences + +- Multi-node HA/MQTT output becomes semantically correct; distinct nodes + report distinct state and no longer suppress one another. +- Schema change to `NodeInfo`/the MQTT contract; existing single-node + deployments keep working (one node = one inference). Consumers reading the + old aggregate-only shape need the migration accessor. +- Aligns with ADR-292 (freshness) and the review's call for one canonical + `NodeInference`/`RoomInference` contract. + +## Validation + +- Unit/integration tests: per-node classification round-trips through the MQTT + mapper with no room fallback; two nodes with different rates both publish + (no starvation); disagreeing nodes produce deterministic, non-flapping room + state; a silent node's entities go stale, not frozen-online. +- `cargo test -p wifi-densepose-sensing-server`. diff --git a/docs/adr/ADR-295-model-release-sanity-gates.md b/docs/adr/ADR-295-model-release-sanity-gates.md new file mode 100644 index 00000000..86bbd6fe --- /dev/null +++ b/docs/adr/ADR-295-model-release-sanity-gates.md @@ -0,0 +1,60 @@ +# ADR-295: Model release sanity gates — block degenerate and mislabeled model artifacts + +- **Status**: Accepted — initial implementation (this PR) +- **Date**: 2026-08-11 +- **Deciders**: ruv +- **Tags**: models, evaluation, release-gate, honesty, presence + +## Context + +The external review (corroborating issue 1521) showed the published presence +head is mathematically degenerate: with L2-normalized embeddings, a weight +norm ≈ 3.67 against a bias ≈ 8.19 makes the smallest possible logit positive, +so predicted presence probability is ≥ ~0.989 for every valid input — the +decision boundary is unreachable and the head is effectively constant. The +README then labeled a temporal-triplet accuracy (a representation-ordering +metric) as "presence accuracy" — a category error. + +Nothing in the release path catches a constant classifier, an unreachable +boundary, or a metric-name mismatch. A machine check would have. + +## Decision + +Add a `model_gates` module (in `wifi-densepose-train`) plus a CI gate that, +for any classifier artifact proposed for release, fails on: + +- **Constant output** — output variance below a threshold across a diverse + probe set (including the degenerate-embedding probe from issue 1521). +- **Unreachable decision boundary** — for a normalized-embedding linear head, + check whether `bias` sign dominates `‖weight‖` so the logit cannot change + sign; fail if the boundary is analytically unreachable. +- **Degenerate class balance** — predicted-positive rate at/above a ceiling + (e.g. > 99%) on a balanced probe set. +- **Missing/blank baseline** — a report without a paired mean-pose/majority + baseline (ties into ADR-288 `EvaluationReport`). +- **Metric-name provenance** — a metric may not be surfaced under a task name + that does not match its computed kind (temporal-triplet ≠ presence); + enforced by making the metric carry its kind and the label derive from it. + +Each gate emits a structured, human-readable failure explaining the defect and +the offending numbers. + +## Consequences + +- The specific degenerate presence head cannot ship again, and the + temporal-triplet-as-presence mislabel is structurally prevented. +- Some existing artifacts will fail the gate on introduction — intended; they + should fail. +- The gate is heuristic, not a correctness proof; it catches the known + failure shapes, not all bad models. + +## Validation + +- Unit tests: the issue-1521 weights fail the unreachable-boundary and + constant-output gates; a healthy synthetic head passes; a temporal-triplet + metric cannot be constructed with a presence label. +- `cargo test -p wifi-densepose-train`; the CI gate runs in the model-check + workflow. +- This ADR does **not** withdraw the already-published artifact (an + outward-facing action requiring maintainer sign-off) — it prevents + recurrence and documents the model-card correction. diff --git a/docs/adr/ADR-296-csi-data-incident-repo-controls.md b/docs/adr/ADR-296-csi-data-incident-repo-controls.md new file mode 100644 index 00000000..72c563ff --- /dev/null +++ b/docs/adr/ADR-296-csi-data-incident-repo-controls.md @@ -0,0 +1,50 @@ +# ADR-296: Repository CSI data-incident controls — ignore rules and a pre-commit/CI policy check + +- **Status**: Accepted — controls implemented; tree remediation gated on owner sign-off +- **Date**: 2026-08-11 +- **Deciders**: ruv +- **Tags**: privacy, data-governance, ci, security, incident + +## Context + +The external review found ~64.6 MB of tracked raw CSI recordings under +`data/recordings/` and `v2/data/recordings/` (largest an ~61.8 MB overnight +capture). CLAUDE.md explicitly prohibits committing CSI or person data. The +`.gitignore` rule pointed only at a pre-rename path +(`rust-port/wifi-densepose-rs/data/recordings/`) and did not cover the active +directories, which is how the captures were committed. Raw CSI is person data +(it encodes breathing, movement, presence), so this is a data incident, not a +formatting nit. + +## Decision + +**Implemented now (mechanical, no data-ownership judgment):** + +- Fix `.gitignore` to cover `data/recordings/`, `v2/data/recordings/`, the + legacy path, and `*.csi.jsonl` / `*.csi.meta.json` globs (done in this PR). +- Add a policy check (pre-commit hook + CI job) that fails when CSI-format + files (`*.csi.jsonl`, `*.csi.meta.json`) or large JSONL captures are staged + or present as tracked files, with a message pointing here. Tests may use + only synthetic or expressly-consented minimal fixtures. + +**Explicitly gated on data-owner sign-off (NOT done autonomously):** + +- Removing the existing recordings from the tree, and any history rewrite, are + outward-facing/destructive and require the data owner to first establish + provenance, consent, purpose, retention authority, and redistribution + rights. The review is correct that rewriting `origin` does not erase forks + and clones; coordination is required. This ADR records the controls and the + required follow-up; it does not delete the data. + +## Consequences + +- No new CSI captures can be committed (ignore + policy check). +- The existing tracked recordings remain until the owner decides; the incident + is documented and the guard prevents worsening it. +- CI gains one fast policy job; contributors get a local pre-commit check. + +## Validation + +- Policy-check unit tests: a staged `*.csi.jsonl` fails; a synthetic fixture + under an allowed test path passes; the check is deterministic and offline. +- Manual confirmation that the new ignore globs cover both active directories. diff --git a/docs/adr/README.md b/docs/adr/README.md index 07b72ba0..4e243606 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -149,6 +149,11 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme | [ADR-289](ADR-289-wideband-80211ax-csi-ingest.md) | Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter, subcarrier-agnostic plumbing | Accepted (initial implementation) | | [ADR-290](ADR-290-vitals-ground-truth-rig.md) | Vitals ground-truth rig — reference ingest, alignment, agreement metrics | Accepted (initial implementation) | | [ADR-291](ADR-291-wifi-veil-integration.md) | WiFi Veil integration — emission-shaping countermeasure as advisory BFLD dependency | Accepted (initial implementation) | +| [ADR-292](ADR-292-source-provenance-state-machine.md) | Source provenance state machine — synthetic can never present as live | Accepted (initial implementation) | +| [ADR-293](ADR-293-sensor-data-plane-bind-hardening.md) | Sensor data-plane hardening — UDP bind control and source allowlist (step one) | Accepted (initial implementation) | +| [ADR-294](ADR-294-multi-node-semantic-correctness.md) | Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state | Accepted (initial implementation) | +| [ADR-295](ADR-295-model-release-sanity-gates.md) | Model release sanity gates — block degenerate and mislabeled model artifacts | Accepted (initial implementation) | +| [ADR-296](ADR-296-csi-data-incident-repo-controls.md) | Repository CSI data-incident controls — ignore rules and pre-commit/CI policy check | Accepted (controls implemented; tree remediation gated) | ---