mirror of
https://github.com/ruvnet/RuView
synced 2026-08-11 20:41:44 +00:00
docs: ADR-292..296 — remediation ADRs from Aug-2026 external review
Turns the review's code-implementable P0/P1 items into ADRs: source provenance state machine (synthetic never presents as live), UDP data-plane bind hardening (loopback default + allowlist, step one), multi-node semantic correctness (per-node inference, node-keyed rate limiter, stale state), model release sanity gates (block degenerate/mislabeled heads), and CSI data-incident repo controls. Also fixes the stale .gitignore rule so the active recordings directories and CSI globs are covered going forward. Tree removal of existing recordings, history rewrite, and withdrawal of the published presence head are gated on maintainer sign-off (outward-facing / destructive) and intentionally not done here. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
This commit is contained in:
+6
-1
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user