Compare commits

...

39 Commits

Author SHA1 Message Date
ruv
0370d49e4a feat: add physics-constrained pose refinement 2026-08-15 14:29:07 -04:00
rUv
de27336fa1 Merge pull request #1588 from ruvnet/fix/issue-triage-batch-1
fix: issue triage batch (#1521-1557) + wire the certificate spine together
2026-08-11 19:37:31 -04:00
ruv
73e82313ac fix: issue triage batch (#1521-1557) + wire the certificate spine together
Reviews and fixes RuView's 10 most recent substantive issues, plus closes
the wiring gap flagged in the perception-substrate review (docs/adr,
gist, release notes from the PR #1579 review).

## Fixed

- #1526: UI falsely claimed "LIVE — ESP32 Hardware Connected" whenever the
  unauthenticated /api/v1/status probe 401'd. Now sends the bearer token
  on the probe (the fallback direction was already fixed by ADR-295).
- #1554: top-level `classification` (GET /api/v1/sensing/latest) was the
  last UDP packet's single node, not the fused room aggregate, so it
  flapped at packet rate with 2+ disagreeing nodes. Now derived from
  `RoomInference` (ADR-297's `fuse_room`) at both call sites.
- #1541: per-node MQTT `presence`/`presence_score` read a `classification`
  JSON key that does not exist on `NodeInfo` (the real field is
  `node_inference`), silently falling back to the room aggregate for
  every node — reproducing the exact "lockstep publish" the field report
  measured. Also fixed the *existing* regression test for this bug,
  which used the same wrong key in its own fixture and so never caught it.
- #1557: pose-fusion's `wsPortMap` only knew port 3000, so a remapped host
  port fell through to `localhost:8765` (nothing there for a remote
  viewer). WS port is now derived from `location.port` instead of a
  2-entry lookup table (the "never render simulated as live" half was
  already fixed by ADR-295's `onVerifiedFrame` gate).
- #1525: the no-model pose path already clamps keypoint confidence to a
  0.1 floor, but the renderer's own threshold is also 0.1 compared with
  `<=`/`>` — a keypoint at exactly the floor was still invisible. Floor
  raised to 0.15 to clear the client's gate.
- #1556: `--mqtt-ca-file`/`--mqtt-client-cert`/`--mqtt-client-key` were
  parsed and stored but never applied — TLS always used the system trust
  store, so a self-signed broker always failed UnknownIssuer. Now builds
  `rumqttc::TlsConfiguration::Simple` from the real PEM files (no new TLS
  dependency needed). A file that can't be read logs why and falls back
  to system trust instead of failing opaquely later.
- #1555: the MQTT availability heartbeat asserted "online" for every known
  node on a fixed 30s timer regardless of whether that node's data was
  still arriving. Now tracks each node's last-seen broadcast snapshot and
  only reports "online" within a 10s freshness window, otherwise
  "offline" — a frozen sensor can no longer look available. (The other
  half — restarting a publisher that goes permanently silent — needs a
  reproduction the reporter themselves weren't certain of; left for a
  follow-up rather than guessing at the trigger.)
- #1540: already fixed on main (node-keyed RateLimiter, ADR-297).
- #1521/#1522: not fixable in this repo (published HF model artifact);
  replied with the ADR-298 gate status and the exact byte-level fix for
  the safetensors header, and corrected the README row that claimed the
  file loads with the reference loader.
- #1542, #1527: replied — #1542 is a real, larger firmware+server feature
  left open for follow-up; #1527's suggested fixes were already applied,
  the one residual sample is an inherent first-frame paint gap.

## Certificate spine wiring (closes the gap flagged in the PR #1579 review)

`ruview-certify` and `ruview-policy` now depend on `ruview-ood` and provide
real `From<ruview_ood::DomainState>` adapters plus a composed entry point,
`ruview_policy::authorize_from_certificate`, matching the adapter contract
`ruview-policy`'s own doc comment already described but that no code
actually implemented. A new cross-crate integration test
(`acceptance_test_b_real_integration`) mints a real signed
`CapabilityCertificate` and proves a real post-drift `ruview_ood::Unknown`
denies a `SafetyCritical` action through the composed pipeline — not two
disconnected unit tests hand-setting the same enum value.

Also:
- Wires `evaluate_linear_head` (ADR-298 model-release gate) into a new CI
  job so the checker itself can't silently regress; documents that gating
  an actual model publish is still a manual step (no HF automation here).
- Wires `SourceState::export_watermark()` into `start_recording`: a
  recording captured while the source is synthetic is now stamped in its
  metadata (not the filename or per-line JSON, to avoid breaking
  `delete_recording`'s path reconstruction or the training dataset
  loader's schema).
- Updates docs/user-guide.md's "Developer Preview" section to describe
  what's now genuinely wired vs. still not (no live continuous
  calibration/OOD loop in the running server yet).

## Validation

- cargo test --workspace --no-default-features: 4391 passed, 0 failed
- cargo build --release -p wifi-densepose-sensing-server --features mqtt:
  clean
- Server smoke-tested end-to-end against the simulator (real startup,
  UDP/WS/HTTP listeners, /api/v1/sensing/latest stable across calls)
- Real ESP32-S3 hardware was NOT reachable this session (no COM port
  present, zero UDP frames received after 40s bound to 0.0.0.0:5005) —
  the multi-node fixes are validated by the new unit/integration tests
  and full-workspace regression, not by live hardware.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-08-11 19:08:54 -04:00
rUv
bf17fc0407 Merge pull request #1579 from ruvnet/claude/adr-288-290-sota-gaps
ADR-288/289/290/291: benchmark harness, wideband CSI ingest, vitals ground-truth rig, wifi-veil integration
2026-08-11 13:24:42 -04:00
ruv
ba978041ae Merge remote-tracking branch 'origin/main' into resolve-1579-conflict
# Conflicts:
#	docs/adr/README.md
2026-08-11 13:04:00 -04:00
rUv
90c6ecc530 Merge pull request #1561 from ruvnet/claude/privacy-shield-wifi-sensing-i8vz1b
WiFi Veil — compliant-waveform privacy shield: reference crate, E2E firmware, standalone repo + CI honesty guard
2026-08-11 12:56:26 -04:00
ruv
5aa204a168 docs(user-guide): add Perception Certificate Spine developer-preview section (ADR-297)
Documents the 9 new spine crates (calibrate -> certify -> govern) as a
composable library, explicit about current status: each crate is tested in
isolation but not yet wired together or into the live sensing-server
request path (DomainState is 3 separate enum types across
ruview-ood/certify/policy with no automatic bridge). Contrasts against the
two ADR-292-296 remediation items that ARE genuinely enforced today: UDP
source allowlisting (checked on every packet) and the CSI data-policy CI
guard.
2026-08-11 12:46:17 -04:00
Claude
e46fcc6862 feat: implement ADR-297 phase-3 — RF twin, placement, spatial memory, counterfactual, info-gain, active sensing
The higher-ceiling primitives on the fused world state. Six crates, all
deterministic SYNTHETIC/L0 model scaffolds (a twin predicts, it never measures);
70 tests + 6 doctests, verified green independently.

ruview-twin (ADR-312): per-deployment RF twin — radio geometry, a documented
synthetic log-distance + wall-attenuation propagation model, per-link expected
distributions, and the load-bearing delta(observed,expected) that localizes a
physical change (moved node / new reflector) to specific links. 8 tests.

ruview-infogain (ADR-311): Value(sensor) = expected uncertainty reduction /
weighted cost; pure bounded-greedy selection under a multi-dimension budget;
unknown-value candidates handled explicitly (defer/probe, never silent zero). 15.

ruview-active (ADR-306): closed-loop control vocabulary (channel/bandwidth/
cadence/antenna as validated ranges); step() proposes the next measurement to
reduce uncertainty, widening exploration when the last response is UNKNOWN;
emits a plan, never RF. 13.

ruview-placement (ADR-305): floorplan + inventory -> ranked placement via the
twin's propagation model; blind-spot flags; predicted-vs-observed adjustment. 11.

ruview-memory (ADR-309): learns per-zone normal physics; anomalies are
significant deltas vs baseline emitted as evidence records; UNKNOWN before a
baseline exists (no false positives). 14.

ruview-counterfactual (ADR-310): scores hypotheses under the twin — empty-room
vs occupied, one person vs two; UNKNOWN when indistinguishable. 8.

Flips ADR-305/306/309/310/311/312 to implemented. Completes all three phases of
the ADR-297 perception-substrate program. No hardware/MEASURED claims.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 13:13:04 +00:00
Claude
49c594822f feat: implement ADR-297 phase-2 world-model core — HAL, ground-truth, tracking, fusion
The layer that turns the certificate spine into a modality-agnostic perception
substrate. Four crates, all deterministic and green independently (43 tests).

ruview-hal (ADR-317): one abstraction mapping any modality (CSI/802.11bf/BLE/
UWB/mmWave/acoustic/camera/lidar/IMU/custom) to a canonical ontology Observation.
SensorHal trait + two SYNTHETIC/L0 reference adapters; malformed input yields a
degraded UNKNOWN observation, never a panic; synthetic can never alias measured.
8 tests.

ruview-groundtruth (ADR-300): reference sensors as a formal VALIDATION plane
(never an estimator input, enforced by the type boundary); modality-agnostic
ReferenceSeries, deterministic cross-correlation alignment, AgreementReport with
mandatory SessionScope, emitting per-context ruview-evidence records; Measured
requires reference + coverage + reproducer. 15 tests.

ruview-track (ADR-304): privacy-preserving persistent tracks (opaque person ids,
coarse non-reversible features, no civil-identity binding); ambiguous detections
stay tentative rather than misassigned; cross-zone hand-off. 8 tests.

ruview-fusion (ADR-308): multiple HalObservations -> one probabilistic WorldState,
uncertainty-aware (confidence-weighted, not naive averaging); irreconcilable
conflict or insufficient coverage yields UNKNOWN, not a confident average.
9+ tests incl. irreconcilable_conflict_yields_unknown.

Flips ADR-300/304/308/317 to implemented; registers the four crates as workspace
members. SYNTHETIC/L0 throughout; no hardware/MEASURED claims.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 03:16:33 +00:00
Claude
516331461a feat: implement ADR-297 phase-1 dependent wave — OOD, witness, certify, scorecard, policy
Completes the phase-1 certificate spine; both acceptance tests now pass as code.

ruview-ood (ADR-299): domain-distance vs the certified fingerprint and a pure
DomainState KNOWN/DEGRADED/UNKNOWN classifier implementing the ADR-297
VALID->DEGRADED->UNKNOWN staleness guard; InferenceGate suppresses the class
(UNKNOWN as a first-class value) when domain is not KNOWN; RecalibrationRequest
signalled on DEGRADED/UNKNOWN. 25 tests.

ruview-witness (ADR-316): ordered, append-only, BLAKE3 hash-linked stage chain
(observation->DSP->inference->corroboration->spatial->policy) rooted in an
attest VerifiedMeasurement; verify() catches mutation/reorder/dropped/broken
links; effective level is the minimum across stages. 19 tests.

ruview-certify (ADR-315): signed CapabilityCertificate minted from a single-
context evidence slice (never pooled), evidence level capped at the slice floor,
valid_until bounded by calibration validity; is_valid(now, domain) returns false
when expired OR domain != KNOWN (certificate conditional on the live domain
signature). 17 tests incl. non_known_domain_invalidates.

ruview-scorecard (ADR-314): multi-domain scorecard with per-domain CIs,
worst_domain(), and a promotion gate that fails when only pooled average
improved while a worst-domain slice regressed. 17 tests.

ruview-policy (ADR-318): fail-closed action gate; Convenience/Security/
SafetyCritical assurance classes; authorize() denies with a named failed
condition; UNKNOWN denies high-assurance actions. Includes
acceptance_test_b_post_drift_unknown_denies_safety_critical. 10 tests.

All five verified green independently (88 tests). Registers the five crates as
workspace members. SYNTHETIC/L0 reference crypto; no hardware claims.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 02:06:31 +00:00
Claude
6506438b83 feat: implement ADR-297 phase-1 spine roots — ontology, attest, evidence, calibration cert
Four foundational perception-substrate crates (dependency roots of the
certificate spine), each pure/leaf and deterministically tested:

ruview-ontology (ADR-303): canonical Site>Building>Floor>Space>Zone +
Sensor/Person/Object/Observation/Track/Event, typed serde-transparent ids,
WorldGraph registry with dangling-parent/duplicate rejection and containment
resolution, EvidenceLevel L0-L5, and a docs-only migration table for the
per-surface shapes. 8 tests + doctest.

ruview-attest (ADR-302): DeviceId + SignedMeasurement envelope binding
{device, monotonic sequence, timestamp, payload hash, calibration ref};
AttestationVerifier enforces unknown-device, signature, tamper, strict
per-device sequence (replay), and freshness; Signer/Verifier traits with a
blake3-keyed SYNTHETIC-grade reference MAC (Ed25519 is a drop-in). 14 tests.

ruview-evidence (ADR-301): append-only per-(room,device,subject,model) ledger;
immutable records with provenance-gated constructors (synthetic forces L0,
measured needs a reproducer); summarize() never pools across contexts; a slice
reports the floor evidence level, never above its weakest record. 8 tests.

wifi-densepose-calibration (ADR-298): CalibrationCertificate — signed, versioned
room fingerprint minted from existing calibration/bank state, compare/drift, and
invalidate-on-drift/expiry. 78 tests green.

Registers the three new crates as workspace members. All four verified green
independently. SYNTHETIC/L0 reference crypto; no hardware claims.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 00:50:28 +00:00
Claude
34c9804002 feat: implement ADR-292/293/294/295/296 — provenance, UDP hardening, multi-node, model gates, CSI policy
ADR-292 (sensing-server): SourceState enum + pure transition (provenance.rs);
auth-error/unknown can never resolve to LiveVerified; synthetic exports
watermarked. Pose-fusion simulator starts SYNTHETIC and only shows LIVE on a
real decoded frame (#1557); sensing client no longer labels an unauthorized
status endpoint as live (#1526).
ADR-294 (sensing-server): NodeInference distinct from RoomInference (inference.rs);
deterministic freshness-weighted fuse_room; RateLimiter re-keyed to
(NodeId,EntityKind) so nodes do not starve each other (#1541); stale nodes go
unavailable not frozen-online (#1555).
ADR-293 (sensing-server): --udp-bind (default 127.0.0.1) + --udp-allow allowlist
+ fail-closed refusal of routable bind without allowlist unless --udp-insecure-lan
(udp_bind.rs); crate SECURITY.md documents the threat model and the deferred
per-device-auth step two.
ADR-295 (train): model_gates.rs — constant-output, unreachable-boundary (the
issue-1521 degenerate presence head), class-balance, baseline, and
metric-name-provenance gates.
ADR-296 (ci): scripts/csi-data-policy-check.sh (+ allowlist) and a workflow that
fails on tracked CSI-format/oversized-JSONL files; 6/6 self-tests pass.

Per-crate suites reported green by the swarm; CSI policy self-test 6/6 and JS
syntax verified here. Full workspace re-verification deferred until the
concurrent phase-1 spine build frees the target dir (disk pressure).

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 00:47:29 +00:00
Claude
8bb55aac05 docs: ADR-318 decision policy + amend ADR-297 with epistemic-reliability invariants
Elevates decision policy (action authorization) into phase 1 (ADR-318): a
fail-closed gate that authorizes a governed action only when the capability
certificate class, freshness, uncertainty ceiling, evidence floor, and live
domain state (KNOWN, not DEGRADED/UNKNOWN) all satisfy the action class —
convenience vs security vs safety-critical. UNKNOWN denies high-assurance
actions; every decision is the terminal witness-chain stage.

Amends ADR-297 with: the epistemic-reliability pipeline as the product thesis;
four non-negotiable rules (UNKNOWN first-class; certificates bind
cryptographically; one canonical downstream semantics; benchmarks expose
worst-domain + CIs); the certificate-staleness guard (certificates conditional
on a continuously evaluated domain signature; VALID->DEGRADED->UNKNOWN
auto-degrade triggers recalibration); the three-primitive commercial framing
(Runtime / Certify / Trust-Fleet); and acceptance test B (drift invalidation
before a false inference reaches an actuator).

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 00:36:05 +00:00
Claude
559ad56aa4 docs: ADR-298..317 — 20 child ADRs of the perception-substrate program
Phase 1 (certificate spine, initial implementation planned): ADR-298 calibration
certificate, ADR-299 OOD KNOWN/DEGRADED/UNKNOWN gating, ADR-301 evidence engine,
ADR-302 authenticated sensor identity, ADR-303 canonical spatial ontology,
ADR-314 multi-domain benchmark scorecard, ADR-315 capability certificates,
ADR-316 witness chain.
Phase 2 (Proposed): ADR-300 ground truth, ADR-304 tracking, ADR-307 802.11bf-native,
ADR-308 fusion, ADR-313 fleet, ADR-317 HAL.
Phase 3 (Proposed): ADR-305 placement, ADR-306 active sensing, ADR-309 spatial
memory, ADR-310 counterfactual, ADR-311 info-gain, ADR-312 RF twin.

Each references ADR-297 and cross-references its dependencies; phase-1 ADRs carry
implementation intent, phase-2/3 are design-intent Proposed.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 00:30:00 +00:00
Claude
ca1f0b9e8a docs: ADR-297 — RuView perception substrate phased program (20 primitives)
Frames the calibration/evidence/trust/deployment layer as a 20-primitive
phased program with a dependency DAG and phase assignments. Phase 1 is the
certificate spine (spatial ontology, authenticated identity, witness chain,
calibration cert, OOD gating, evidence engine, capability certificate,
multi-domain benchmark scorecard) — the acceptance test decomposed and
buildable without new hardware. Phase 2/3 are integration and research-forward
primitives. Child ADRs 298-317 follow.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 00:18:29 +00:00
Claude
2cafa1fdcc 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
2026-08-11 00:09:12 +00:00
Claude
79d1fff99a feat: implement ADR-288/289/290 — benchmark harness, wideband CSI ingest, vitals ground-truth rig
ADR-288 (wifi-densepose-train): Widar3.0 Intel-5300 .dat bfee parser
(bounded, panic-free), WidarDataset over the CsiDataset trait, deterministic
SplitProtocol (cross-subject/environment/orientation + leakage-prone random
baseline), LeakageAudit that Errs on subject/environment/recording overlap,
train-only MeanPoseBaseline, and EvidenceGrade where Measured requires an
embedded reproducer. Criterion bench for parser + split + audit.

ADR-289 (wifi-densepose-mat): FeitCSI binary record parser (layout verified
against upstream source) with dimension-vs-buffer validation and bounded
allocation, DeviceType::FeitCsi replay/stream modes, subcarrier-agnostic
frame metadata (bandwidth/band/native->pipeline mapping). Criterion bench at
1992-subcarrier frames.

ADR-290 (wifi-densepose-vitals): reference-series CSV ingest, cross-correlation
time alignment with optional drift fit, Bland-Altman/MAE/RMSE agreement with
mandatory SessionScope, and EvidenceGrade gating Measured on reference device +
coverage + reproducer. Criterion bench for hour-scale alignment.

All three crate suites green; benches compile. Numbers are SYNTHETIC (in-code
fixtures); no hardware claims.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-11 00:06:39 +00:00
Claude
01c42d0900 feat(bfld): ADR-291 — wifi-veil emission-shaping countermeasure as advisory dependency
Adds wifi-veil (pinned git rev, dependency-free, MIT/Apache-2.0) to the
workspace and gates it behind a new 'veil' feature in wifi-densepose-bfld.
The bfld::veil module exposes deterministic attacker-vs-protector shield
assessments (re-ID collapse, throughput ratio, energy-conservation audit)
with a mandatory SYNTHETIC/L0 evidence label. Advisory only: no RF
emission, no frame mutation, BFLD invariants I1-I3 untouched. Default
build unchanged; 6 new feature-gated tests pass.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-10 23:46:55 +00:00
Claude
de88e37de5 docs: ADR-288/289/290 — benchmark harness, wideband CSI ingest, vitals ground-truth rig
Three ADRs closing the highest-impact gaps from the 2026 SOTA research
sweep: public-benchmark comparability (Widar3.0 ingest + leakage-guarded
split protocols), wideband 802.11ax CSI ingest (FeitCSI/AX210), and a
vitals ground-truth rig (reference ingest, alignment, Bland-Altman
agreement, evidence grading). Implementations follow in this branch.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
2026-08-10 23:18:54 +00:00
ruv
e737b1a7bc fix(esp32): correct mqtt component name + %u format casts; add build-verified examples
Building veil_ris_controller/veil_sensing_detector for real against ESP-IDF
v5.4 (esp32s3 target) surfaced two compile bugs, now fixed in both the
firmware/privshield/ and wifi-veil/ copies:
- veil_sensing_detector/CMakeLists.txt: PRIV_REQUIRES esp_mqtt -> mqtt
  (esp_mqtt is not a real ESP-IDF v5.4 component name; the real one is mqtt)
- veil_ris_controller.c / veil_sensing_detector.c: ESP_LOGI("%u", ...) calls
  passed a bare uint32_t; -Werror=format= requires (unsigned) casts

Also adds esp32/examples/ — minimal ESP-IDF apps wrapping each component's
public API, added purely to prove they compile+link on a real toolchain.
Still SYNTHETIC / L0, build-only — never flashed, no hardware exists.
2026-08-09 15:46:39 -02:30
ruv
50bcf0e215 fix(privshield/wifi-veil): assert data_source on the replayBundle, not the top-level result
FlywheelResult has no dataSource field — @metaharness/flywheel stamps it
as replayBundle.data_source (snake_case). This test never actually ran in
CI here (nested .github/workflows/ under wifi-veil/ is inert on GitHub);
caught only once the wifi-veil/ tree was extracted to its own repo and its
own top-level Actions ran for real.
2026-08-09 15:25:04 -02:30
Claude
e2ffecde9a ci(wifi-veil): add honesty / anti-slop guard
Add scripts/ci-guard.sh and a `guard` CI job that statically enforce the
project's honesty invariants so they cannot silently regress:

- no telemetry (.claude-flow/), build artifacts, lockfile, or scratch/probe
  files committed;
- no debug / mock-probe / slop markers in source
  (panic!("probe...), dbg!, println!("DEBUG, TODO(ai), LOREM IPSUM, ...);
- the SYNTHETIC evidence label present on every firmware provider README, and
  the "never jamming" compliance disclaimer present in the root + firmware READMEs;
- no dishonest hardware-validation claims — honest negated / TODO(hw) /
  build-only mentions are explicitly allowed (negation-aware);
- no stale monorepo crate/harness identifiers in the code/manifest surface.

Scans only git-tracked files under the tree, so it works both in-monorepo and
in the extracted standalone repo, and never trips on untracked local scratch or
target/. Documented in CONTRIBUTING.md; passes clean on the current tree.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 17:20:03 +00:00
Claude
17ba9df19a ci(wifi-veil): add GitHub Pages workflow for the Console
Publish ui/veil-console.html as the site landing page (index.html) via GitHub
Pages on push to main. The console is a single self-contained file (inline
CSS/JS, no network), so the build just stages it. Enable once under
Settings → Pages → Source: "GitHub Actions".

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 17:14:55 +00:00
Claude
5114ed183f feat(wifi-veil): add self-contained standalone repository tree
Assemble `wifi-veil/` as an extraction-ready standalone repository for the WiFi
Veil privacy shield, decoupled from the RuView monorepo. The RuView copies under
v2/, harness/, firmware/, and docs/ are left untouched; this is an additive,
self-contained tree that can be split out to its own repo (e.g. ruvnet/wifi-veil).

Optimized for a standalone identity, with all monorepo coupling removed:

- Rust crate at the repo root: renamed `wifi-veil` (lib `wifi_veil`, bin `veil`),
  workspace-metadata inheritance inlined, own `[workspace]` root, release profile.
  Dependency-free and WASM-ready — it builds and tests OFFLINE, unlike the
  monorepo copy (which needs sibling submodules). Code is byte-identical, so the
  deterministic proof witness is unchanged.
- Portable C shield core + per-provider firmware scaffolds (openwifi/openwrt/
  nexmon/esp32) under firmware/; host C-core test passes.
- npm harness renamed `wifi-veil-harness`; its guidance paths/commands repointed
  to the standalone layout; manifest SHA-256 digests regenerated and verified.
- Docs: ADR-288/289/290 and the privacy-shield research bundle; research build
  commands/links normalized to the standalone crate.
- Root scaffolding: product README, dual LICENSE-MIT / LICENSE-APACHE,
  .gitignore, CHANGELOG, CONTRIBUTING, and a GitHub Actions CI workflow
  (Rust test/clippy/fmt + wasm build, C-core host test, harness smoke).

Validated locally: cargo fmt --check, cargo clippy --all-targets -D warnings,
cargo test (43 tests + witness), cargo build --lib --target wasm32-unknown-unknown,
make -C firmware/core test, and `node harness/bin/cli.js guidance` — all green.
No telemetry, build artifacts, or lockfile committed. All defense figures remain
SYNTHETIC / L0; compliant waveform controls only, never jamming.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 17:06:48 +00:00
Claude
1c2b383075 docs(privshield): rebrand project to "WiFi Veil"
Adopt "WiFi Veil" as the product name across all user-facing surfaces, keeping
VEIL (Verifiable Emission-shaping for Identity-Leakage prevention) as the
technical codename it's built on. Only prose, titles, descriptions, and the
console UI change — no code identifiers, file names, crate/npm `name` fields,
or the deterministic proof witness are touched, so `cargo test` and the C-core
host test are unaffected.

- Crate & research READMEs: title + defining line now "WiFi Veil (codename VEIL — …)".
- Cargo.toml / package.json / plugin.json descriptions: "WiFi Veil …".
- Console UI (veil-console.html): title, brand, and copy say "WiFi Veil".
- Firmware tree (README, per-provider READMEs, BUILD/INTEGRATION/MEASUREMENT):
  "WiFi Veil protector/core/shield".
- Harness manifest: recomputed SHA-256 digests for the four changed packaged
  files (README, package.json, CLAUDE.md, plugin.json) — all verified consistent.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 16:52:40 +00:00
Claude
b827dc40b1 feat(privshield): E2E hardware program — validated C core + multi-provider firmware scaffolds
Take VEIL from the synthetic Rust reference model toward real WiFi silicon
across multiple hardware providers, around one shared, host-validated core.
Answers the questions "can OpenWRT / open WiFi software implement this?" and
"can ESP32 help scramble signals?" with an honest per-platform feasibility map.

Portable C shield core (firmware/privshield/core/) — VALIDATED (host test):
- veil_shield.{h,c}: keyed Givens-rotation obfuscation of the identity-bearing
  "fine" subspace, C99, no malloc / no libc I/O, only <math.h>. SplitMix64 key
  schedule byte-identical to the Rust crate, so on-air behavior is consistent
  everywhere and every adapter links the same math.
- make test passes: energy conservation (orthogonal => "not jamming"),
  reversibility (recover inverts apply), wrong-key-fails, and PRNG stream parity
  with the Rust crate. This is build/host evidence, NOT silicon.

Per-provider adapters (all SYNTHETIC / L0, build-only, TODO(hw) markers):
- openwifi/  grade B (ceiling A, effort D): only open PHY/MAC (FPGA) that can
  host the full keyed rotation + inverse; needs new HDL + 2nd TX chain. Carries
  the P5 measurement protocol (MEASUREMENT.md) for the first MEASURED result.
- openwrt/   grade C: per-packet keyed unitary is blob-blocked on commodity APs;
  coarse compliant knobs (TX antenna map, sounding-cadence jitter) reachable
  from userspace/hostapd; ath9k is the one credible driver-patch route.
- nexmon/    grade C: reading the compressed-BF angles is solved (nexmon_csi /
  Wi-BFI); shaping the transmitted report is research-grade (D11 ucode-adjacent).
- esp32/     grade F (self) / B (supporting): cannot shape its own BF feedback
  (closed esp-phy-lib blob); legitimate as a sensing detector and external-RIS
  controller — the honest way ESP32 "helps scramble", via an external surface.

Docs:
- firmware/privshield/README.md: architecture, layout, and the feasibility matrix.
- ADR-290: the E2E hardware program, PROOF discipline, and per-provider decision;
  added to docs/adr/README.md index.

Compliant waveform controls only, never jamming. No adapter has run on silicon;
no MEASURED claim is made (that is roadmap P5, gated on a captured log).

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 16:34:11 +00:00
Claude
192ed2a236 feat(privshield): implement SOTA-driven attackers and compliant controls (ADR-288 §sota)
From the verified 2025-2026 deep-research findings, all four approved code items,
each opt-in so the reference witness stays byte-identical (0x350d…f448):

- attacker: BFI->CSI Reconstruction adversary (BFIAttack) — recovers the
  direction of the CSI consistent with the *captured* report; a secret
  orthogonal rotation leaves it at chance (no key to invert). AdaptivePooling
  adversary (PrivISAC) — pools + whitens per identity; still collapses.
  `AttackerKind` selects the shape.
- protector: `ObfMode::PerPacketUnitary` — fresh per-packet unitary, AP-side and
  client-transparent (LeakyBeam family). `dp_epsilon` — ε-DP angular dither,
  renormalized to preserve emission energy (still not jamming).
- throughput: `dp_residual` makes ε a real privacy<->throughput knob (smaller ε
  costs more gain).
- experiment: `attacker_kind` + mode-aware keying dispatch.

Tests (43 pass, +5): reconstruction & adaptive-pooling collapse (and win
unprotected); per-packet mode collapses + compliant; ε-DP still collapses +
compliant; DP throughput frontier monotonic. clippy -D warnings clean, fmt
clean, wasm --lib builds. All new numbers remain SYNTHETIC/L0.

Docs: 09-sota-update backlog items 1-4 marked implemented; crate README module
table refreshed.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 16:20:18 +00:00
Claude
c63b26034b docs(privshield): fold verified 2025-2026 SOTA into threat model, compliance, and roadmap
From a fan-out deep-research run (20 primary sources, 25 claims 3-vote verified,
24 confirmed / 1 refuted):

- New docs/research/privacy-shield/09-sota-update-2026.md: cited, evidence-classed
  SOTA update + prioritized VEIL improvement backlog.
- ADR-288 gains a "2025-2026 evidence update" section: broader threat (BFId
  99.5%/N=197; LeakyBeam through-wall vitals @20m; WiKI-Eve/SThief keystrokes;
  BFIAttack BFI->CSI reconstruction), VEIL's family independently validated
  (LeakyBeam per-packet unitary 89.7->51%; PrivISAC RIS 93->30%), BeamDancer
  (IEEE TWC 2024) as compliance precedent, shield-security-is-CLAIMED honesty,
  and the unfilled governance gap. Do NOT cite BeamDancer's refuted >96% PDR.
- Roadmap §3.1: answers "does this need custom WiFi firmware?" — yes; ESP32 is an
  attacker/sensor node only (closed blob, CSI read only), the protector needs
  openwifi / Nexmon / vendor firmware; keyed-reversible needs both ends + key.

Docs only. All VEIL numbers remain SYNTHETIC/L0; no code or claims upgraded.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 16:09:33 +00:00
Claude
0cb348da72 docs(privshield): clearly explain how VEIL protects against unauthorized WiFi surveillance
Plain-language "how it protects you" for non-experts, in both surfaces:

- README: new "How this protects you from unauthorized WiFi surveillance"
  section — the silent device-free threat, the per-session keyed-twist defense
  (own router undoes it, outside listener can't average it back → identity guess
  collapses to chance; ~98% throughput; compliant/not-jamming), and honest
  limits (defends vs. third-party sniffers not the AP; SYNTHETIC/L0).
- UI (ui/veil-console.html): a new always-visible "How this protects you" card
  (Threat / Shield / Kept honest) plus a deeper `protect` explainer dialog.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 15:52:37 +00:00
Claude
aea8c8c66a docs(privshield): add veil TUI walkthrough GIF, ship the web console in-repo
- docs/veil-tui.gif: an animated walkthrough of the `veil` terminal harness
  (shield off/on, 32 passes → out-of-spec, 96 → pass, ward preset, optimize,
  witness check), embedded at the top of the harness section in the crate README.
- ui/veil-console.html: the self-contained VEIL Console web dashboard now lives
  in the repo (no build/network), linked from the README.
- veil: honor CLICOLOR_FORCE so piped captures keep ANSI color (standard flag).
- veil-console.html footer now points at the `veil` terminal harness/TUI.

Validated: 38 tests + doctest pass, clippy --all-targets -D warnings clean,
rustfmt clean. GIF is 800×520, ~113 KB, verified to decode/animate in a browser.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 15:39:01 +00:00
Claude
cb67be117a feat(privshield): add veil custom terminal harness + TUI (ADR-288 §harness)
A dependency-free native binary (`src/bin/veil.rs`) — the in-repo counterpart to
the npm metaharness — that drives the same crate API the tests use:

- Interactive ANSI dashboard (TUI): live status, re-ID off/on vs chance,
  throughput, compliance (energy 1.000× / not jamming), a block-sparkline
  collapse curve, config, and PASS/OUT-OF-SPEC verdict. Command-driven redraw
  loop (std-only, no crossterm/ratatui): on/off, passes/bits/n/snr,
  metric euclid|cosine, preset scif|board|ward|hotel, optimize, proof.
- Scriptable subcommands: report, sweep, optimize, adaptive <N>, proof, doctor
  (exit 0 = healthy). Auto-picks TUI on a terminal, one-shot report when piped;
  honors NO_COLOR.

Std-only, so it builds with no extra deps and runs in any pipe/CI. The wasm leaf
story is unchanged (validated with `--lib`; the bin is native-only). All
readouts are SYNTHETIC/L0 and never relabeled.

Validated: 38 tests + doctest pass, clippy --all-targets -D warnings clean,
rustfmt clean, wasm --lib builds; all subcommands + a scripted TUI session
exercised. Documented in the crate README and ADR-288.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 15:26:07 +00:00
Claude
80b1715cb8 docs(privshield): add VEIL Console landscape banner to top of crate README
Adds a rendered screenshot of the VEIL management console (dark theme, shield
engaged: the room's WiFi identity clusters collapsed to the chance floor, re-ID
4.7%, PROTECTED) as a banner at the top of the crate README. Captured at a 16:11
landscape viewport (2400x1752, 2x).

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 15:07:41 +00:00
Claude
18060b9c77 Add per-deployment adaptive optimization + VEIL npm metaharness (ADR-289)
Two additions on top of the hyper-optimized VEIL shield.

1) Adaptive optimization (v2/crates/wifi-densepose-privshield/src/optimize.rs):
   - optimal_bits_across_snr / model_optimal_bits_for_snr: the throughput-
     optimal feedback resolution shifts with SNR (unconstrained optimum 4 bits
     at 5-10 dB, 3 bits at 20-40 dB); within the spec {5,7,9} set it stays 5,
     which is why the shipped shield is SNR-stable.
   - adaptive_shield / min_passes_for_n: derive a shield for a specific
     deployment. Finding: the collapse budget is N-independent in this model
     (48 passes collapses N in {8,64} alike) — it is set by the fine-subspace
     dimension, not the candidate count. Defaults unchanged, so the proof
     witness is untouched. 38 tests + doctest pass; clippy -D warnings clean.

2) npm metaharness harness/wifi-densepose-privshield/ (ADR-289), mirroring
   wifi-densepose-sar-harness (ADR-286) with two improvements:
   - @metaharness/* imported dynamically inside the commands that need them, so
     `guidance` and `--help` run with ZERO dependencies installed (offline / pre
     `npm install`).
   - a dependency-free VEIL `guidance` command: a source-cited, evidence-
     labelled, read-only capability map (topics: overview, threat,
     countermeasure, compliance, optimization, experiment).
   Standard router + flywheel (SYNTHETIC) + Darwin wiring, tailored to VEIL
   task axes and policy levers. Tests: smoke + router + flywheel (need install)
   and guidance (offline). .harness manifest generated with real per-file
   hashes. Validated offline: cli syntax, --help, guidance topics, exit codes,
   graceful degradation when deps are absent.

Docs: research bundle 08 gains a per-deployment adaptivity section; 07 and the
crate README point at the harness; ADR-289 added and indexed.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 14:23:12 +00:00
Claude
006a66ca20 Hyper-optimize VEIL shield: derive the optimal config instead of hand-picking it
Adds an `optimize` module that replaces the hand-picked shield config with a
derived, robustness-verified optimum, and hardens the experiment so the
collapse is proven to be signal-level, not classifier-level.

Model changes:
- throughput.rs: add a feedback-airtime term (cost rises with feedback bits)
  alongside the falling quantization residual, giving a genuine interior
  throughput optimum in feedback resolution.
- attacker.rs: add a selectable distance metric (Euclidean + Cosine) so the
  optimizer can require the collapse to hold under multiple classifiers.
- experiment.rs: thread the attacker metric through; build the channel once.

optimize.rs:
- optimal_feedback_bits / spec_optimal_feedback_bits: throughput-best resolution
  (3 bits unconstrained, matching DySPAN-2026; 5 bits within the 802.11 {5,7,9}
  set).
- min_givens_passes: smallest mixing budget that collapses re-ID robustly across
  both metrics AND N in {16,32}.
- pareto_frontier and hyper_optimize.

Findings and adopted defaults:
- Proven-minimum robust passes = 48; the hand-picked 112 was 2.3x over-
  provisioned. Rotation mixing is keyed (never signaled), so extra passes are
  throughput-free -> ship 96 (2x margin).
- Feedback resolution 5 bits (spec-optimal), down from 7.
- ShieldConfig::default() now equals hyper_optimize()'s output; a test guards
  against drift.

Net vs. the original: strictly better on BOTH privacy and throughput.
Reference (SYNTHETIC/L0, N=16): re-ID 100% shield-off -> 4.7% shield-on
(chance 6.25%, below chance), throughput 97.6%, energy ratio 1.000000. 35 tests
+ doctest pass; clippy -D warnings clean; builds for wasm32.

Docs: new docs/research/privacy-shield/08-optimization.md; updated bundle
README/03/05/07 and ADR-288 with the derived operating point.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 14:08:46 +00:00
Claude
16b2a629d1 Add VEIL privacy shield: compliant-waveform defense against WiFi sensing (ADR-288)
VEIL (Verifiable Emission-shaping for Identity-Leakage prevention) is the
countermeasure counterpart to BFLD (ADR-118/121): where BFLD detects when
beamforming feedback becomes identifying, VEIL shapes a node's own outgoing
feedback so an unauthorized passive sniffer cannot re-identify people, while
a legitimate receiver that shares the per-session key sees an unchanged link.

Mechanism: identity leaks through the fine cross-subcarrier phase structure of
a compressed beamforming report; throughput rides the dominant beam direction.
These are (mostly) separable subspaces. VEIL composes extra keyed Givens
rotations (the report's native primitive) over the fine subspace only. The
rotation is orthogonal (energy-preserving -> not jamming), keyed per session
(the AP inverts it -> throughput preserved), and fresh each session (a sniffer
cannot average it back -> re-identification collapses to chance).

Contents:
- v2/crates/wifi-densepose-privshield: deterministic, dependency-free,
  WASM-ready pure-compute leaf implementing the attacker-vs-protector
  experiment, the four compliant controls, a throughput model, a
  machine-checkable "not jamming" compliance audit, and a pinned witness.
  29 tests + doctest pass; clippy -D warnings clean; builds for
  wasm32-unknown-unknown.
- docs/research/privacy-shield: 8-file research bundle (SOTA, threat model,
  design, compliance/regulatory, experiment protocol, market, roadmap).
- docs/adr/ADR-288: formal decision record.

Reference results (SYNTHETIC / L0, N=16 identities): passive re-ID accuracy
100% shield-off -> 7.8% shield-on (chance 6.25%); modeled throughput ratio
98.0%; emission energy ratio 1.000000 (compliant). All defense numbers are
SYNTHETIC until a two-node hardware capture with a witness exists.

Compliant waveform controls only; never jamming (47 U.S.C. 333/302a analysis
in the bundle).

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 13:51:12 +00:00
ruv
5780c239e4 security: repair scanning and close stale alert sources 2026-08-02 15:22:13 -04:00
ruv
42492e14a5 docs: collapse secondary README details 2026-08-02 10:56:08 -04:00
ruv
7309458b40 fix: load JSONL models and streamline README 2026-08-02 10:47:59 -04:00
ruv
b77b682a6b feature: promote RuCelium in README 2026-08-02 10:32:29 -04:00
449 changed files with 65711 additions and 598 deletions

View File

@@ -32,7 +32,7 @@ jobs:
run:
working-directory: v2
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
@@ -40,7 +40,7 @@ jobs:
run: rustup show && rustc --version
- name: Cache cargo
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cargo/registry

View File

@@ -71,7 +71,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive — wifi-densepose-rufield path-deps vendor/rufield)
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
# The workspace includes `wifi-densepose-rufield`, which path-deps the
# `vendor/rufield` submodule crates. Without a recursive checkout the
@@ -100,10 +100,10 @@ jobs:
pkg-config
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
- name: Cache cargo (Swatinem/rust-cache)
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
workspaces: v2
# Distinct cache scope from ci.yml's rust-tests so the bench profile
@@ -150,15 +150,15 @@ jobs:
needs: [bench-compile]
steps:
- name: Checkout (recursive)
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
- name: Cache cargo (Swatinem/rust-cache)
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
workspaces: v2
key: bench-regression
@@ -192,7 +192,7 @@ jobs:
- name: Upload informational bench logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: bench-fast-run-logs
path: bench-out/

View File

@@ -52,17 +52,17 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
components: clippy
- name: Cache cargo registry + target
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cargo/registry

View File

@@ -44,7 +44,7 @@ jobs:
image_tag: ${{ steps.determine-tag.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
submodules: recursive
@@ -96,12 +96,12 @@ jobs:
url: https://staging.wifi-densepose.com
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Set up kubectl
uses: azure/setup-kubectl@v3
uses: azure/setup-kubectl@901a10e89ea615cf61f57ac05cecdf23e7de06d8
with:
version: 'v1.28.0'
@@ -147,12 +147,12 @@ jobs:
url: https://wifi-densepose.com
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Set up kubectl
uses: azure/setup-kubectl@v3
uses: azure/setup-kubectl@901a10e89ea615cf61f57ac05cecdf23e7de06d8
with:
version: 'v1.28.0'
@@ -222,7 +222,7 @@ jobs:
# kubectl scale rs -n wifi-densepose -l app=wifi-densepose,version!=green --replicas=0
- name: Upload deployment artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: production-deployment-${{ github.run_number }}
path: |
@@ -239,7 +239,7 @@ jobs:
name: ${{ needs.pre-deployment.outputs.deploy_env }}
steps:
- name: Set up kubectl
uses: azure/setup-kubectl@v3
uses: azure/setup-kubectl@901a10e89ea615cf61f57ac05cecdf23e7de06d8
with:
version: 'v1.28.0'
@@ -293,7 +293,7 @@ jobs:
done
- name: Update deployment status
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
with:
script: |
const deployEnv = '${{ needs.pre-deployment.outputs.deploy_env }}';
@@ -317,7 +317,7 @@ jobs:
steps:
- name: Notify Slack on success
if: needs.deploy-production.result == 'success' || needs.deploy-staging.result == 'success'
uses: 8398a7/action-slack@v3
uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e
with:
status: success
channel: '#deployments'
@@ -331,7 +331,7 @@ jobs:
- name: Notify Slack on failure
if: needs.deploy-production.result == 'failure' || needs.deploy-staging.result == 'failure'
uses: 8398a7/action-slack@v3
uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e
with:
status: failure
channel: '#deployments'
@@ -344,7 +344,7 @@ jobs:
- name: Create deployment issue on failure
if: needs.deploy-production.result == 'failure'
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
with:
script: |
github.rest.issues.create({

View File

@@ -27,14 +27,14 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
fetch-depth: 0
- name: Set up Python
continue-on-error: true
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -68,7 +68,7 @@ jobs:
- name: Upload security reports
continue-on-error: true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
name: security-reports
@@ -82,7 +82,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
# ADR-262 P1: `wifi-densepose-rufield` path-deps the `vendor/rufield`
@@ -112,7 +112,7 @@ jobs:
pkg-config
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
# Swatinem/rust-cache replaces a naive `actions/cache` of the whole
# `v2/target`. That manual cache of a 38-crate target dir (multi-GB) was an
@@ -123,7 +123,7 @@ jobs:
# reliably (and faster) on large workspaces. `workspaces: v2` points it at
# the v2/ cargo workspace (keys on v2/Cargo.lock, caches v2/target).
- name: Cache cargo (Swatinem/rust-cache)
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
workspaces: v2
@@ -196,10 +196,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '22'
@@ -222,6 +222,8 @@ jobs:
postgres:
image: postgres:15
env:
# Ephemeral CI-only credential; this service is isolated to the job.
# kics-scan ignore-line
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_wifi_densepose
options: >-
@@ -245,13 +247,13 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
continue-on-error: true
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@@ -266,6 +268,8 @@ jobs:
- name: Run unit tests
continue-on-error: true
env:
# Ephemeral CI-only service URL; never used outside this job.
# kics-scan ignore-line
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_wifi_densepose
REDIS_URL: redis://localhost:6379/0
ENVIRONMENT: test
@@ -275,6 +279,8 @@ jobs:
- name: Run integration tests
continue-on-error: true
env:
# Ephemeral CI-only service URL; never used outside this job.
# kics-scan ignore-line
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_wifi_densepose
REDIS_URL: redis://localhost:6379/0
ENVIRONMENT: test
@@ -283,7 +289,7 @@ jobs:
- name: Upload coverage reports
continue-on-error: true
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
with:
files: ./coverage.xml
flags: unittests
@@ -291,7 +297,7 @@ jobs:
- name: Upload test results
continue-on-error: true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
name: test-results-${{ matrix.python-version }}
@@ -312,12 +318,12 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -361,7 +367,7 @@ jobs:
- name: Upload performance results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: performance-results
path: archive/v1/perf-junit.xml
@@ -382,17 +388,17 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Set up Docker Buildx
continue-on-error: true
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
- name: Log in to Container Registry
continue-on-error: true
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -401,7 +407,7 @@ jobs:
- name: Extract metadata
continue-on-error: true
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -412,7 +418,7 @@ jobs:
- name: Build and push Docker image
continue-on-error: true
uses: docker/build-push-action@v7
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
with:
context: .
target: production
@@ -441,7 +447,7 @@ jobs:
- name: Upload Trivy scan results
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@a2983b8bed1923f44751c5c43237f479442827b3
if: always()
with:
sarif_file: 'trivy-results.sarif'
@@ -456,12 +462,12 @@ jobs:
contents: write # gh-pages deploy needs write (GITHUB_TOKEN is read-only by default -> 403)
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -484,7 +490,7 @@ jobs:
"
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453
continue-on-error: true # openapi generation above is the real validation; deploy is best-effort (Pages may be disabled)
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -507,7 +513,7 @@ jobs:
steps:
- name: Notify Slack on success
if: ${{ env.SLACK_WEBHOOK_URL != '' && needs.code-quality.result == 'success' && needs.test.result == 'success' && needs.docker-build.result == 'success' }}
uses: 8398a7/action-slack@v3
uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e
with:
status: success
channel: '#ci-cd'
@@ -515,7 +521,7 @@ jobs:
- name: Notify Slack on failure
if: ${{ env.SLACK_WEBHOOK_URL != '' && (needs.code-quality.result == 'failure' || needs.test.result == 'failure' || needs.docker-build.result == 'failure') }}
uses: 8398a7/action-slack@v3
uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e
with:
status: failure
channel: '#ci-cd'
@@ -523,7 +529,7 @@ jobs:
- name: Create GitHub Release
if: github.ref == 'refs/heads/main' && needs.docker-build.result == 'success'
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
tag_name: v${{ github.run_number }}
name: Release v${{ github.run_number }}

View File

@@ -34,7 +34,7 @@ jobs:
snapshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive

View File

@@ -27,17 +27,17 @@ jobs:
name: Build x86_64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
targets: x86_64-unknown-linux-gnu
- name: Cache cargo registry
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cargo/registry
@@ -66,7 +66,7 @@ jobs:
echo "Signed cog-ha-matter-x86_64 ($(wc -c < dist/cog-ha-matter-x86_64.sig) bytes)"
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: cog-ha-matter-x86_64
path: |
@@ -79,12 +79,12 @@ jobs:
name: Build aarch64 (arm)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
targets: aarch64-unknown-linux-gnu
@@ -94,7 +94,7 @@ jobs:
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Cache cargo registry
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cargo/registry
@@ -130,7 +130,7 @@ jobs:
echo "Signed cog-ha-matter-arm ($(wc -c < dist/cog-ha-matter-arm.sig) bytes)"
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: cog-ha-matter-arm
path: |
@@ -148,29 +148,29 @@ jobs:
github.event_name == 'push' &&
vars.HAS_GCP_CREDENTIALS == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Download x86_64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: cog-ha-matter-x86_64
path: dist/
- name: Download arm artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: cog-ha-matter-arm
path: dist/
- name: Auth to GCP
uses: google-github-actions/auth@v2
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS }}
- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v2
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f
- name: Upload binaries + sidecars
run: |

53
.github/workflows/csi-data-policy.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: CSI data policy (ADR-299)
# ADR-299 repository CSI data-incident guard. Fails when CSI-format files
# (*.csi.jsonl / *.csi.meta.json) or oversized JSONL captures are tracked in
# git. Raw CSI is person data and must never be committed (CLAUDE.md, ADR-299).
#
# NOTE: the tree currently still contains the pre-existing incident recordings
# under data/recordings/ and v2/data/recordings/, whose removal is gated on
# data-owner sign-off (ADR-299). Until they are removed this job is EXPECTED to
# fail, and that failure documents the incident. To make it green in a
# follow-up without weakening the guard for NEW files, set CSI_POLICY_BASELINE
# to a file listing the acknowledged paths (see the script header).
#
# Checker: scripts/csi-data-policy-check.sh Run locally: bash the same script.
on:
push:
branches:
- main
- master
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
csi-data-policy:
name: CSI data policy check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- name: Self-test the policy checker (deterministic, offline)
run: bash scripts/csi-data-policy-check.sh --self-test
- name: Enforce CSI data policy on tracked files
# CSI_POLICY_BASELINE can point at an acknowledged-paths file once the
# owner remediates the tree; unset here so a regression fails loudly.
run: bash scripts/csi-data-policy-check.sh --tracked
- name: Summarize result
if: always()
run: |
{
echo '### CSI data policy (ADR-299)'
echo ''
echo '```'
bash scripts/csi-data-policy-check.sh --tracked 2>&1 || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -19,11 +19,11 @@ jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with: { targets: wasm32-unknown-unknown }
- name: Install wasm-pack
@@ -36,7 +36,7 @@ jobs:
--out-dir ../../dashboard/public/nvsim-pkg \
--release -- --no-default-features --features wasm
- uses: actions/setup-node@v6
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with: { node-version: 20, cache: npm, cache-dependency-path: dashboard/package-lock.json }
- working-directory: dashboard

View File

@@ -25,17 +25,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Install Rust + wasm32 target
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
targets: wasm32-unknown-unknown
- name: Cache cargo registry
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cargo/registry
@@ -59,7 +59,7 @@ jobs:
-- --no-default-features --features wasm
- name: Setup Node 20
uses: actions/setup-node@v6
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with:
node-version: 20
cache: npm
@@ -76,7 +76,7 @@ jobs:
run: npm run build
- name: Deploy to gh-pages/nvsim/
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dashboard/dist

View File

@@ -27,17 +27,17 @@ jobs:
target: [aarch64-apple-darwin, x86_64-apple-darwin]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with:
node-version: '20'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
targets: ${{ matrix.target }}
@@ -74,7 +74,7 @@ jobs:
zip -r "RuView-Desktop-${{ github.event.inputs.version || '0.4.0' }}-macos-${{ steps.arch.outputs.arch }}.zip" "RuView Desktop.app"
- name: Upload macOS artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ruview-macos-${{ steps.arch.outputs.arch }}
path: v2/target/${{ matrix.target }}/release/bundle/macos/*.zip
@@ -84,17 +84,17 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with:
node-version: '20'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
- name: Install frontend dependencies
working-directory: v2/crates/wifi-densepose-desktop/ui
@@ -115,13 +115,13 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
- name: Upload Windows MSI artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ruview-windows-msi
path: v2/target/release/bundle/msi/*.msi
- name: Upload Windows NSIS artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ruview-windows-nsis
path: v2/target/release/bundle/nsis/*.exe
@@ -134,12 +134,12 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: artifacts
@@ -147,7 +147,7 @@ jobs:
run: find artifacts -type f
- name: Create or Update Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
name: RuView Desktop v${{ github.event.inputs.version || '0.4.0' }}
tag_name: ${{ github.event.inputs.attach_to_existing || format('desktop-v{0}', github.event.inputs.version || '0.4.0') }}

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
if: github.ref_type == 'tag'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Check firmware version.txt == tag
@@ -75,7 +75,7 @@ jobs:
artifact_pt: partition-table-c6.bin
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
@@ -175,7 +175,7 @@ jobs:
echo "See: https://github.com/espressif/qemu/wiki"
- name: Upload firmware artifact (${{ matrix.variant }})
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: esp32-csi-node-firmware-${{ matrix.variant }}
path: firmware/esp32-csi-node/release-staging/

View File

@@ -34,7 +34,7 @@ jobs:
steps:
- name: Cache QEMU build
id: cache-qemu
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: /opt/qemu-esp32
# Include date component so cache refreshes monthly when branch updates
@@ -73,7 +73,7 @@ jobs:
echo "QEMU binary size: $(file_size /opt/qemu-esp32/bin/qemu-system-xtensa) bytes"
- name: Upload QEMU artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: qemu-esp32
path: /opt/qemu-esp32/
@@ -99,12 +99,12 @@ jobs:
- boundary-min
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Download QEMU artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: qemu-esp32
path: /opt/qemu-esp32
@@ -203,7 +203,7 @@ jobs:
- name: Upload test logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: qemu-logs-${{ matrix.nvs_config }}
path: |
@@ -215,7 +215,7 @@ jobs:
name: Fuzz Testing (ADR-061 Layer 6)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
@@ -253,7 +253,7 @@ jobs:
- name: Upload fuzz artifacts
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: fuzz-crashes
path: |
@@ -266,7 +266,7 @@ jobs:
name: NVS Matrix Generation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
@@ -322,12 +322,12 @@ jobs:
image: espressif/idf:v5.4
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Download QEMU artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: qemu-esp32
path: /opt/qemu-esp32
@@ -370,7 +370,7 @@ jobs:
- name: Upload swarm results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: swarm-results
path: |

View File

@@ -21,11 +21,11 @@ jobs:
name: Verify fix markers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- uses: actions/setup-python@v6
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: '3.11'
@@ -49,7 +49,7 @@ jobs:
- name: Upload result artifact
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: fix-markers-result
path: fix-markers-result.json

View File

@@ -0,0 +1,67 @@
name: Model release gate (ADR-298)
# ADR-298 model-release sanity gates (issue #1521): structural checks that
# block a degenerate/mislabeled classifier head (unreachable decision
# boundary, near-constant output, degenerate class balance, a metric
# surfaced under a task name it wasn't computed as) before it ships.
#
# Checker: v2/crates/wifi-densepose-train/src/model_gates.rs
#
# IMPORTANT — the honest scope of this job: it protects the *checker itself*
# from regressing (the gate logic + its issue-1521 regression fixture are
# exercised on every push/PR that touches this crate), and running it is
# required before ADR-298 can be called "wired in" at all. It does NOT gate
# an actual model publish — this repository does not automate uploading to
# the HuggingFace model repo (`ruvnet/wifi-densepose-pretrained`); that
# remains a manual, human-run step. Before publishing or replacing a model
# artifact there, run this gate against the real head weights locally:
#
# cargo test -p wifi-densepose-train model_gates
#
# and, until a CLI entry point exists to run `evaluate_linear_head` against an
# arbitrary `.safetensors`/`.rvf` file, load the head's `weight`/`bias` in a
# short script and call `wifi_densepose_train::evaluate_linear_head` directly.
on:
push:
branches:
- main
- master
paths:
- "v2/crates/wifi-densepose-train/**"
pull_request:
paths:
- "v2/crates/wifi-densepose-train/**"
workflow_dispatch:
permissions:
contents: read
jobs:
model-release-gate:
name: Model release gate check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
submodules: recursive
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Run the model-release gate's own test suite
working-directory: v2
run: cargo test -p wifi-densepose-train --no-default-features model_gates -- --nocapture
- name: Summarize result
if: always()
run: |
{
echo '### Model release gate (ADR-298)'
echo ''
echo 'This job protects `model_gates.rs` from regressing. It does not itself'
echo 'gate a real HuggingFace model publish — that upload is a manual step'
echo 'outside this repository; run `cargo test -p wifi-densepose-train model_gates`'
echo 'against real head weights before publishing one.'
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -40,7 +40,7 @@ jobs:
RUST_BACKTRACE: 1
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
@@ -70,12 +70,12 @@ jobs:
exit 1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
toolchain: stable
- name: Cache cargo registry + build
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
workspaces: v2 -> target

View File

@@ -25,13 +25,13 @@ jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- uses: docker/setup-buildx-action@v3
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
- uses: docker/login-action@v3
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -39,7 +39,7 @@ jobs:
- name: Extract metadata
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302
with:
images: ghcr.io/ruvnet/nvsim-server
tags: |
@@ -49,7 +49,7 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}}
- name: Build + push
uses: docker/build-push-action@v7
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
with:
context: v2
file: v2/crates/nvsim-server/Dockerfile

View File

@@ -90,19 +90,19 @@ jobs:
arch: AMD64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
# Linux aarch64 needs QEMU for cross-build on x86_64 runners.
- name: Set up QEMU
if: matrix.os == 'ubuntu-latest' && matrix.arch == 'aarch64'
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130
# ADR-117 §5.4: abi3-py310 — one binary per OS/arch covers all
# Python minor versions ≥ 3.10. Build only cp310 wheels.
- name: Build wheels (cibuildwheel)
uses: pypa/cibuildwheel@v2.21
uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b
env:
CIBW_BUILD: "cp310-*"
CIBW_ARCHS_LINUX: ${{ matrix.arch }}
@@ -124,7 +124,7 @@ jobs:
package-dir: python
output-dir: wheelhouse
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: wheels-${{ matrix.os }}-${{ matrix.arch }}
path: wheelhouse/*.whl
@@ -137,7 +137,7 @@ jobs:
startsWith(github.ref, 'refs/tags/v2.')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Install maturin
@@ -145,7 +145,7 @@ jobs:
- name: Build sdist
working-directory: python
run: maturin sdist --out ../sdist
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: sdist
path: sdist/*.tar.gz
@@ -158,8 +158,8 @@ jobs:
startsWith(github.ref, 'refs/tags/v2.')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: '3.12'
- name: Verify lock-step package versions
@@ -185,7 +185,7 @@ jobs:
run: |
python -m pip install --upgrade pip build
python -m build python/ruview-meta --outdir ruview-dist
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ruview
path: ruview-dist/*
@@ -202,10 +202,10 @@ jobs:
startsWith(github.ref, 'refs/tags/v1.99')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: '3.12'
- name: Install build backend
@@ -264,7 +264,7 @@ jobs:
exit 1
fi
echo "Tombstone wheel correctly raises ImportError with migration URL."
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: tombstone
path: tombstone-dist/*
@@ -288,7 +288,7 @@ jobs:
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Enforce production witness gate
if: |
startsWith(github.ref, 'refs/tags/v2.') ||
@@ -299,7 +299,7 @@ jobs:
exit 1
}
- name: Gather all artifacts into dist/
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: dist-staging
- name: Flatten artifacts
@@ -311,7 +311,7 @@ jobs:
# before replacing `password:` with the OIDC id-token permission.
- name: Publish to TestPyPI (dry-run target)
if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi'
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
with:
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.TESTPYPI_API_TOKEN }}
@@ -321,7 +321,7 @@ jobs:
if: |
startsWith(github.ref, 'refs/tags/v2.') ||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist
@@ -339,7 +339,7 @@ jobs:
)
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: tombstone
path: dist
@@ -347,7 +347,7 @@ jobs:
# before replacing `password:` with the OIDC id-token permission.
- name: Publish to TestPyPI (dry-run target)
if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi'
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
with:
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.TESTPYPI_API_TOKEN }}
@@ -357,7 +357,7 @@ jobs:
if: |
startsWith(github.ref, 'refs/tags/v1.99') ||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist

View File

@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
@@ -63,7 +63,7 @@ jobs:
EOF
- name: Deploy to gh-pages/pointcloud/
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site/pointcloud

View File

@@ -68,7 +68,7 @@ jobs:
name: Wheel + parity tests (features=sota)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
# The python/ crate path-deps v2/crates/* and (transitively via
# train) the vendored ruvector submodule — recursive checkout keeps
@@ -76,15 +76,15 @@ jobs:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: '3.11'
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
- name: Cache cargo (Swatinem/rust-cache)
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
workspaces: |
v2
@@ -133,20 +133,20 @@ jobs:
name: Default wheel <= 5 MiB (ADR-117 §5.4)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: '3.11'
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
- name: Cache cargo (Swatinem/rust-cache)
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
with:
workspaces: python

View File

@@ -39,12 +39,12 @@ jobs:
- { label: 'ruflo', flags: '--features ruflo' }
- { label: 'full+train', flags: '--features full,train' }
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
- name: Cache cargo
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cargo/registry
@@ -61,7 +61,7 @@ jobs:
name: clippy (-D warnings, --no-deps)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
# v2/rust-toolchain.toml pins channel "1.89" with profile "minimal" (no
@@ -69,12 +69,12 @@ jobs:
# toolchain, but the override makes cargo use the separate "1.89"
# toolchain — so `cargo clippy` errors "cargo-clippy is not installed for
# 1.89". Install clippy on the pinned toolchain that cargo actually uses.
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
toolchain: "1.89"
components: clippy
- name: Cache cargo
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cargo/registry
@@ -96,12 +96,12 @@ jobs:
name: build train_marl bin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
- name: Cache cargo
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: |
~/.cargo/registry
@@ -132,7 +132,7 @@ jobs:
name: ITAR / publish guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: publish = false is present (no accidental crates.io publish)

View File

@@ -26,14 +26,13 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
submodules: recursive
fetch-depth: 0
- name: Set up Python
continue-on-error: true
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -47,15 +46,18 @@ jobs:
- name: Run Bandit security scan
run: |
# The Python codebase lives under archive/v1/src (it moved there when
# the runtime was rewritten in Rust). Scanning `src/` matched nothing,
# so this SAST step was a silent no-op.
bandit -r archive/v1/src/ -f sarif -o bandit-results.sarif
# archive/v1 is frozen research code and is not shipped. Scan the
# maintained Python packages and operator scripts instead.
# Keep the Security tab actionable: publish high-severity findings.
# Medium/low findings are reviewed during focused local audits.
bandit -lll -r python/ scripts/ firmware/esp32-csi-node/ aether-arena/ \
-x '*/tests/*,*/test/*,*/test_*.py,*/bench/*' \
-f sarif -o bandit-results.sarif
continue-on-error: true
- name: Upload Bandit results to GitHub Security
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@a2983b8bed1923f44751c5c43237f479442827b3 # v3
if: always()
with:
sarif_file: bandit-results.sarif
@@ -74,12 +76,16 @@ jobs:
semgrep \
--config=p/security-audit --config=p/secrets --config=p/python \
--config=p/docker --config=p/kubernetes \
--sarif --output=semgrep.sarif archive/v1/src/
--severity=ERROR \
--exclude='**/tests/**' --exclude='**/test/**' \
--exclude='**/test_*.py' --exclude='**/bench/**' \
--sarif --output=semgrep.sarif \
python/ scripts/ firmware/esp32-csi-node/ aether-arena/
continue-on-error: true
- name: Upload Semgrep results to GitHub Security
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@a2983b8bed1923f44751c5c43237f479442827b3 # v3
if: always()
with:
sarif_file: semgrep.sarif
@@ -97,13 +103,11 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
with:
submodules: recursive
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Python
continue-on-error: true
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -135,7 +139,7 @@ jobs:
- name: Upload Snyk results to GitHub Security
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@a2983b8bed1923f44751c5c43237f479442827b3 # v3
if: always()
with:
sarif_file: snyk-results.sarif
@@ -143,7 +147,7 @@ jobs:
- name: Upload vulnerability reports
continue-on-error: true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: vulnerability-reports
@@ -157,7 +161,6 @@ jobs:
name: Container Security Scan
runs-on: ubuntu-latest
continue-on-error: true # third-party scanners are flaky / SARIF uploads can 403; don't gate the PR
needs: []
if: github.event_name == 'push' || github.event_name == 'schedule'
permissions:
security-events: write
@@ -166,20 +169,20 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
submodules: recursive
- name: Set up Docker Buildx
continue-on-error: true
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Build Docker image for scanning
continue-on-error: true
uses: docker/build-push-action@v7
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
target: production
file: docker/Dockerfile.rust
load: true
tags: wifi-densepose:scan
cache-from: type=gha
@@ -192,50 +195,21 @@ jobs:
image-ref: 'wifi-densepose:scan'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
limit-severities-for-sarif: true
- name: Upload Trivy results to GitHub Security
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@a2983b8bed1923f44751c5c43237f479442827b3 # v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
category: trivy
- name: Run Grype vulnerability scanner
continue-on-error: true
uses: anchore/scan-action@v7
id: grype-scan
with:
image: 'wifi-densepose:scan'
fail-build: false
severity-cutoff: high
output-format: sarif
- name: Upload Grype results to GitHub Security
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: ${{ steps.grype-scan.outputs.sarif }}
category: grype
- name: Run Docker Scout
continue-on-error: true
uses: docker/scout-action@v1
if: always()
with:
command: cves
image: wifi-densepose:scan
sarif-file: scout-results.sarif
summary: true
- name: Upload Docker Scout results
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: scout-results.sarif
category: docker-scout
# Trivy is the single container SARIF authority. Grype and Docker Scout
# produced duplicate alerts for the same image packages and obscured the
# actionable high/critical findings.
# Infrastructure as Code security scanning
iac-scan:
@@ -249,52 +223,25 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
with:
submodules: recursive
- name: Run Checkov IaC scan
continue-on-error: true
uses: bridgecrewio/checkov-action@99bb2caf247dfd9f03cf984373bc6043d4e32ebf # v12.1347.0
with:
directory: .
framework: kubernetes,dockerfile,terraform,ansible
output_format: sarif
output_file_path: checkov-results.sarif
quiet: true
soft_fail: true
- name: Upload Checkov results to GitHub Security
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov-results.sarif
category: checkov
- name: Run Terrascan IaC scan
continue-on-error: true
uses: tenable/terrascan-action@3a6e87da8e244513bd77b631e624552643f794c6 # v1.4.1
with:
iac_type: 'k8s'
iac_version: 'v1'
policy_type: 'k8s'
only_warn: true
sarif_upload: true
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Run KICS IaC scan
continue-on-error: true
uses: checkmarx/kics-github-action@05aa5eb70eede1355220f4ca5238d96b397e30a6 # v2.1.20
with:
path: '.'
# Scan RuView-owned operational IaC only. Submodules are audited and
# fixed in their owning repositories; archived/benchmark fixtures are
# intentionally not production infrastructure.
path: '.github/workflows,docker,logging,v2/crates/nvsim-server/Dockerfile'
output_path: kics-results
output_formats: 'sarif'
exclude_paths: '.git,node_modules'
exclude_queries: 'a7ef1e8c-fbf8-4ac1-b8c7-2c3b0e6c6c6c'
exclude_severities: 'info'
- name: Upload KICS results to GitHub Security
continue-on-error: true
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@a2983b8bed1923f44751c5c43237f479442827b3 # v3
if: always()
with:
sarif_file: kics-results/results.sarif
@@ -312,9 +259,8 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
submodules: recursive
fetch-depth: 0
- name: Run TruffleHog secret scan
@@ -328,7 +274,7 @@ jobs:
- name: Run GitLeaks secret scan
continue-on-error: true
uses: gitleaks/gitleaks-action@v2
uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
@@ -348,13 +294,11 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
with:
submodules: recursive
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Python
continue-on-error: true
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
@@ -374,7 +318,7 @@ jobs:
- name: Upload license report
continue-on-error: true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: license-report
path: licenses.json
@@ -387,9 +331,7 @@ jobs:
steps:
- name: Checkout code
continue-on-error: true
uses: actions/checkout@v4
with:
submodules: recursive
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Check security policy files
continue-on-error: true
@@ -444,7 +386,7 @@ jobs:
steps:
- name: Download all artifacts
continue-on-error: true
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
- name: Generate security summary
continue-on-error: true
@@ -464,7 +406,7 @@ jobs:
- name: Upload security summary
continue-on-error: true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: security-summary
path: security-summary.md
@@ -475,7 +417,7 @@ jobs:
- name: Notify security team on critical findings
continue-on-error: true
if: ${{ env.SECURITY_SLACK_WEBHOOK_URL != '' && (needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure' || needs.container-scan.result == 'failure') }}
uses: 8398a7/action-slack@v3
uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e # v3
with:
status: failure
channel: '#security'
@@ -491,7 +433,7 @@ jobs:
- name: Create security issue on critical findings
continue-on-error: true
if: needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure'
uses: actions/github-script@v6
uses: actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325 # v6
with:
script: |
github.rest.issues.create({
@@ -518,4 +460,4 @@ jobs:
**Security Dashboard:** Check the Security tab for detailed findings.
`,
labels: ['security', 'vulnerability', 'urgent']
})
})

View File

@@ -32,10 +32,10 @@ jobs:
WEAVER_SHA256: a9822c712d6871bd89d6530f18c5df5cea3821f642e7b8e5e49e985917f7d12d
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
with:
components: rustfmt
- name: Install weaver

View File

@@ -48,7 +48,7 @@ jobs:
name: build · push · smoke-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
@@ -56,9 +56,9 @@ jobs:
# linux/arm64 layer below (Dockerfile.rust is arch-agnostic — no `--target`
# flag — so buildx + QEMU is all that's needed; arm64 builds are emulated
# by the runner, not built on a separate arm64 host).
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130
- uses: docker/setup-buildx-action@v3
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
- name: Log in to Docker Hub
# Bypassing docker/login-action@v3: the action kept emitting
@@ -73,7 +73,7 @@ jobs:
printf '%s' "$DH_TOKEN" | docker login docker.io -u "$DH_USER" --password-stdin
- name: Log in to ghcr.io
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -81,7 +81,7 @@ jobs:
- name: Compute tags
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302
with:
images: |
docker.io/ruvnet/wifi-densepose
@@ -94,7 +94,7 @@ jobs:
- name: Build + push
id: build
uses: docker/build-push-action@v7
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
with:
context: .
file: docker/Dockerfile.rust

View File

@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
@@ -62,7 +62,7 @@ jobs:
ls -R _site/three.js/ | head -30
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: _site

View File

@@ -13,7 +13,7 @@ jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: true
fetch-depth: 0

View File

@@ -29,12 +29,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ matrix.python-version }}

7
.gitignore vendored
View File

@@ -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-299.
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

103
README.md
View File

@@ -6,10 +6,15 @@
</a>
</p>
<p align="center">
<a href="https://cognitum.one/marketplace/musica">
<a href="https://cognitum.one/marketplace">
<img src="assets/musica-promo.png" alt="Cognitum Musica" width="100%">
</a>
</p>
<p align="center">
<a href="https://github.com/ruvnet/RuCelium">
<img src="assets/rucelium-hero.png" alt="RuCelium — environmental intelligence" width="100%">
</a>
</p>
## **See through walls with WiFi** ##
@@ -32,6 +37,43 @@ Every WiFi router already fills your space with radio waves. When people move, b
- **Environment mapping** — RF fingerprinting identifies rooms, detects moved furniture, spots new objects
- **Sleep quality** — overnight monitoring with sleep stage classification and apnea screening
**Also included:**
- **Camera-free pose** — estimate 17 body keypoints from WiFi CSI
- **Built-in model workflow** — record CSI, train models, load RVF files, and switch LoRA profiles
- **Local automation** — HOMECORE provides state, history, automations, signed Wasm plugins, voice hooks, and HomeKit support
- **Unified RF world model** — combine WiFi CSI, radar, UWB, and cellular sensing in one privacy-bounded scene model; accuracy is still synthetic until real-data validation
- **Governed evidence** — attach privacy policy, uncertainty, provenance, and witness records to sensing events
- **RuView MetaHarness** — use an AI operator to onboard, calibrate, train, verify, and check sensing claims
<details>
<summary><strong>RuView MetaHarness</strong> — guided operation for humans and AI agents</summary>
The RuView-specific metaharness we created is published as [`@ruvnet/ruview`](harness/ruview/README.md). It provides source-cited guidance, guarded Claude Code/Codex agents, deterministic verification, and an honesty check for accuracy claims.
```bash
# Check the local setup and get source-cited guidance
npx @ruvnet/ruview@0.3.1 doctor
npx @ruvnet/ruview@0.3.1 guidance --topic sensing --query "model loading"
# Run a read-only RuView agent through Codex
npx @ruvnet/ruview@0.3.1 agent run --host codex --repo . \
--prompt "Find the nearest tests and cite the source files"
# Search or verify the reviewed contributor brain
npx @ruvnet/ruview@0.3.1 brain search --query "calibration"
npx @ruvnet/ruview@0.3.1 brain verify --repo .
# Check claims, replay the deterministic proof, or expose the MCP server
npx @ruvnet/ruview@0.3.1 claim-check --file REPORT.md
npx @ruvnet/ruview@0.3.1 verify
npx @ruvnet/ruview@0.3.1 mcp start
```
Agent runs are read-only by default. Workspace writes require both `--allow-write` and `--confirm`; retrieved brain content is evidence, not authority.
</details>
Built on [RuVector](https://github.com/ruvnet/ruvector/) and [Cognitum Seed](https://cognitum.one), RuView runs entirely on edge hardware — an ESP32 mesh (as low as $9 per node) paired with a Cognitum Seed for persistent memory, cryptographic attestation, and AI integration. No cloud, no cameras, no internet required.
The system learns each environment locally using spiking neural networks that adapt in under 30 seconds, with multi-frequency mesh scanning across 6 WiFi channels that uses your neighbors' routers as free radar illuminators. Every measurement is cryptographically attested via an Ed25519 witness chain.
@@ -74,6 +116,9 @@ RuView turns ordinary WiFi into a contactless sensor. A $9 ESP32 board reads the
>
> 🤗 **Pretrained weights**: download from [`ruvnet/wifi-densepose-pretrained`](https://huggingface.co/ruvnet/wifi-densepose-pretrained) — see [Loading the pretrained model](#loading-the-pretrained-model) below for one-command setup.
<details>
<summary><strong>Quick start options</strong> — Docker, ESP32-S3/C6, Cognitum Seed, and Python</summary>
```bash
# Option 1: Docker (simulated data, no hardware needed)
docker pull ruvnet/wifi-densepose:latest
@@ -119,6 +164,8 @@ pip install "ruview[client]" # or: pip install "wifi-densepose[clie
# from ruview.client import SensingClient, RuViewMqttClient
```
</details>
[![PyPI ruview](https://img.shields.io/pypi/v/ruview?label=ruview)](https://pypi.org/project/ruview/) [![PyPI wifi-densepose](https://img.shields.io/pypi/v/wifi-densepose?label=wifi-densepose)](https://pypi.org/project/wifi-densepose/)
> [!NOTE]
@@ -130,7 +177,7 @@ pip install "ruview[client]" # or: pip install "wifi-densepose[clie
> |--------|----------|------|----------|-------------|
> | **ESP32 + Cognitum Seed** (recommended) | ESP32-S3 + [Cognitum Seed](https://cognitum.one) | ~$140 | Yes | Presence, motion, breathing, heart rate, fall detection, multi-person counting, 17-keypoint pose (signed Cog binary — first-cut on-device model, see [Model weights: what's real, what's not](#model-weights-whats-real-whats-not)), 105-cog catalog, persistent vector store, kNN search, witness chain, MCP proxy |
> | **ESP32 Mesh** | 3-6× ESP32-S3 + WiFi router | ~$54 | Yes | Same capabilities as above without the persistent-memory features |
> | **ESP32-C6 research node** ([ADR-110](docs/adr/ADR-110-esp32-c6-firmware-extension.md), [witness](docs/WITNESS-LOG-110.md), [reviewer guide](docs/ADR-110-REVIEW-GUIDE.md), [firmware v0.7.0](https://github.com/ruvnet/RuView/releases/tag/v0.7.0-esp32)) | ESP32-C6-DevKit ($610) | ~$10 | Yes (Wi-Fi 6 capable) | Same CSI pipeline as S3 with the dual-target firmware. **Firmware-side ADR-110 substrate now closed** (v0.7.0): ESP-NOW cross-board mesh quantified at **99.56 % match / 104 µs smoothed offset stdev / 3.95× EMA suppression** over a 5-min two-board soak (witness §A0.10), 32-byte UDP sync packet with operator-tunable cadence (§A0.12), ADR-018 byte 19 bit 4 wire-fix sourced from the working ESP-NOW path (§A0.13). Wire format ready for HE-LTF PPDU tagging in ADR-018 bytes 18-19 (firmware encoder + Rust + Python decoders verified end-to-end across 23 unit tests). LP-core motion-gate RISC-V program and Wi-Fi 6 soft-AP with TWT Responder both ship as opt-in code paths (default off). **Hardware-gated for measurement**: HE-LTF live subcarrier capture needs an 11ax AP (IDF v5.4 doesn't expose AP-side HE config — §A0.6); ~5 µA LP-core hibernation needs an INA meter to capture; 802.15.4 raw RX is broken in IDF v5.4 (workaround: ESP-NOW transport, shipped + measured). See witness log for the empirical / claimed split. |
> | **ESP32-C6 research node** ([ADR-110](docs/adr/ADR-110-esp32-c6-firmware-extension.md), [witness](docs/WITNESS-LOG-110.md), [reviewer guide](docs/ADR-110-REVIEW-GUIDE.md), [firmware v0.7.0](https://github.com/ruvnet/RuView/releases/tag/v0.7.0-esp32)) | ESP32-C6-DevKit ($610) | ~$10 | Yes (Wi-Fi 6 capable) | Dual-target CSI with **99.56% measured ESP-NOW sync match** and measured HE-LTF capture on IDF 5.5.2. TWT and ~5 µA operation still need hardware validation. |
> | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO |
> | **Qualcomm CSI beta** ([ADR-268](docs/adr/ADR-268-qualcomm-atheros-csi-platform.md)) | QCA9300 now; QCN9074/QCN9274 experimental | ~$30-200 | Simulator now; hardware adapter gated | Rust `QCS1` codec, deterministic replay, UDP/API integration; modern ath11k/ath12k profiles do not claim public CSI export |
> | **Vendor provider beta** ([ADR-270](docs/adr/ADR-270-vendor-rf-sensing-integration-program.md)) | Origin, Plume, Mist, NETGEAR, Electric Imp, RF Solutions, Luma, Nest, Linksys, Wifigarden | Varies | Capability-dependent | Bounded Rust adapters and deterministic fixtures; telemetry/network-only/unsupported states cannot masquerade as CSI |
@@ -176,11 +223,11 @@ huggingface-cli download ruvnet/wifi-densepose-pretrained --local-dir models/wif
| Consumer | Format used | Status |
|----------|-------------|--------|
| Python training / evaluation / embedding extraction | `model.safetensors` | ✅ Works — load with `safetensors.torch.load_file` |
| Python training / evaluation / embedding extraction | `model.safetensors` | ⚠️ The published file's header is NUL-padded, which the reference `safetensors.torch.load_file` rejects (issue [#1522](https://github.com/ruvnet/RuView/issues/1522)) — pending a corrected re-upload. `csi-embed-v2.safetensors` in the same repo is unaffected and loads normally. |
| Inspect / re-export the bundle | `model.rvf.jsonl` (line-by-line JSON) | ✅ Works — plain JSONL |
| Sensing-server `--model <PATH>` flag | binary RVF (`RVFS` magic) | ⚠️ Loader does not yet accept the JSONL container |
| Sensing-server `--model <PATH>` flag | native RVF, `model.safetensors`, or `model.rvf.jsonl` | ✅ Native RVF loads directly; safetensors and JSONL auto-convert in memory |
**Known gap:** the HF model ships in JSONL RVF format, but `v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs` only parses the binary RVF segment format. Pointing `--model` at `model.rvf.jsonl` currently errors with `invalid magic at offset 0: expected 0x52564653, got 0x7974227B` and the live pipeline degrades to null output rather than falling back to heuristic mode — so for the live sensing-server, run **without** `--model` until a JSONL adapter lands (or the model is re-published as binary RVF). Use the weights from Python / training in the meantime.
**Loader scope:** `--model` now accepts native RVF and auto-converts the published safetensors or JSONL files. The quantized `model-q*.bin` files still need a compatible reader, and loading weights does not supply the matching pose-decoder architecture or establish end-to-end pose accuracy.
**Quantization choices** (all in the HF repo): `model-q2.bin` (4 KB) · `model-q4.bin` ⭐ recommended (8 KB) · `model-q8.bin` (16 KB) · `model.safetensors` full (48 KB)
@@ -188,6 +235,11 @@ The separate **17-keypoint pose-estimation model** is now published at [`ruvnet/
### Results & proof
See the measured benchmarks, witness records, and one-command reproducibility check.
<details>
<summary><strong>View benchmark and proof details</strong></summary>
| What | Where | Numbers |
|------|-------|---------|
| **MM-Fi pose model (SOTA)** | [`ruvnet/wifi-densepose-mmfi-pose`](https://huggingface.co/ruvnet/wifi-densepose-mmfi-pose) | 82.69% torso-PCK@20 (single) · 83.59% (ensemble+TTA) · 75K-param micro variant 74.30% |
@@ -206,8 +258,15 @@ python archive/v1/data/proof/verify.py
Tracked in [#509](https://github.com/ruvnet/RuView/issues/509); see [ADR-079](docs/adr/ADR-079-camera-ground-truth-training.md) phases P7P9 for the camera-supervised fine-tune path.
</details>
### Model weights: what's real, what's not
See which checkpoints are validated, experimental, or architecture-only.
<details>
<summary><strong>View model maturity details</strong></summary>
"WiFi → pose" means three different things in this repo, at three different maturity
levels. Read the label, not the headline ([ADR-187](docs/adr/ADR-187-archive-v1-deprecation-honest-labeling.md)):
@@ -225,13 +284,17 @@ project can stand behind today is the **MM-Fi benchmark number**, not a live sin
number. The path to a first *reproducible* on-device baseline (PCK@20 ≥ 35%) is tracked in
[ADR-079](docs/adr/ADR-079-camera-ground-truth-training.md) / [#645](https://github.com/ruvnet/RuView/issues/645) — do not advertise the live single-ESP32 17-keypoint feature without the "first-cut, below-target, runtime-stub" caveat until that baseline is measured.
</details>
## 🧩 Edge Module Catalog
<details>
<summary><b>🧩 105 edge modules ready to install on a Cognitum appliance</b> &mdash; live catalog from <code>app-registry.json</code> v2.1.0 (updated 2026-05-13). Browse + install at <a href="https://seed.cognitum.one/store">seed.cognitum.one/store</a> or your local appliance <code>http://&lt;appliance&gt;:9000/cogs</code>.</summary>
Add signed modules for health, security, buildings, industry, research, AI, and more.
Each module is a small signed binary (~400 KB) that runs alongside the WiFi-DensePose sensing stack on a Cognitum-V0 appliance. The catalog updates over the air &mdash; your appliance fetches it via <code>GET /api/v1/edge/registry</code> ([ADR-102](docs/adr/ADR-102-edge-module-registry.md)) and verifies each binary against an Ed25519 signature ([ADR-100](docs/adr/ADR-100-cog-packaging-specification.md)) before install.
<details>
<summary><strong>Browse the full edge module catalog</strong></summary>
Browse and install modules at [seed.cognitum.one/store](https://seed.cognitum.one/store) or on your appliance at `http://<appliance>:9000/cogs`. Each module is a small signed binary that runs beside the sensing stack. The appliance updates the catalog over the air and verifies every module before installation ([ADR-100](docs/adr/ADR-100-cog-packaging-specification.md), [ADR-102](docs/adr/ADR-102-edge-module-registry.md)).
### 🫀 Health &mdash; <sub>14 modules</sub>
@@ -514,8 +577,12 @@ These scenarios exploit WiFi's ability to penetrate solid materials — concrete
---
## 🧠 Self-Learning WiFi AI
Learn compact room fingerprints from raw CSI and adapt the model to each environment.
<details>
<summary><strong>🧠 Self-Learning WiFi AI (ADR-024)</strong> — Adaptive recognition, self-optimization, and intelligent anomaly detection</summary>
<summary><strong>View self-learning architecture and commands</strong></summary>
Every WiFi signal that passes through a room creates a unique fingerprint of that space. WiFi-DensePose already reads these fingerprints to track people, but until now it threw away the internal "understanding" after each reading. The Self-Learning WiFi AI captures and preserves that understanding as compact, reusable vectors — and continuously optimizes itself for each new environment.
@@ -598,7 +665,12 @@ See [`docs/adr/ADR-024-contrastive-csi-embedding-model.md`](docs/adr/ADR-024-con
## 🧩 Claude Code & Codex Plugin
RuView ships a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin (and Codex prompt mirror) that wraps the whole workflow — onboarding, ESP32 setup, configuration, sensing apps, model training, advanced multistatic sensing, CLI/API/WASM, mmWave radar, and witness verification — as 9 skills, 7 `/ruview-*` commands, and 3 agents. It lives in [`plugins/ruview/`](plugins/ruview/README.md); the marketplace manifest is [`.claude-plugin/marketplace.json`](.claude-plugin/marketplace.json) at the repo root.
Use the in-repo plugin for guided setup, sensing, training, and verification in Claude Code or Codex.
<details>
<summary><strong>View plugin installation and commands</strong></summary>
RuView's [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin and Codex prompt mirror cover onboarding, ESP32 setup, sensing apps, model training, advanced sensing, CLI/API/WASM, mmWave radar, and witness verification. The source lives in [`plugins/ruview/`](plugins/ruview/README.md); the marketplace manifest is [`.claude-plugin/marketplace.json`](.claude-plugin/marketplace.json).
```bash
# In Claude Code — add this repo as a plugin marketplace, then install:
@@ -622,12 +694,19 @@ claude --plugin-dir ./plugins/ruview
Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full details: [`plugins/ruview/README.md`](plugins/ruview/README.md).
**Portable harness — `npx @ruvnet/ruview`:** a lighter, host-portable companion to the in-repo plugin, minted via [MetaHarness](https://www.npmjs.com/package/metaharness) and hardened per [ADR-182](docs/adr/ADR-182-npx-ruview-harness-via-metaharness.md). It runs **without cloning this repo** and on more hosts (Claude Code, Codex, Copilot, opencode, …), exposing the RuView operator tools (`onboard`, `verify`, `node_monitor`, `calibrate`, `node_flash`) over an MCP server — plus the project's **MEASURED-vs-CLAIMED honesty guardrail enforced in code** (`ruview.claim_check` flags untagged or retracted-"100%" accuracy claims). v0.1: the onboarding/verify/claim-check paths are tested (17/17, `verify.py` → PASS); the hardware tools are fail-closed wrappers. Try `npx @ruvnet/ruview` to onboard, or `npx @ruvnet/ruview claim-check --text "…"`. Source: [`harness/ruview/`](harness/ruview/README.md).
For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.3.1`; the quick commands and fuller explanation are in the collapsed MetaHarness section near the top of this README and in [`harness/ruview/`](harness/ruview/README.md).
</details>
---
## 📖 Documentation
Start with the user, build, and calibration guides; expand for the full reference map.
<details>
<summary><strong>Browse all documentation</strong></summary>
| Document | Description |
|----------|-------------|
| [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training |
@@ -649,6 +728,8 @@ Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full detail
| [Medical Examples](examples/medical/README.md) | Contactless blood pressure, heart rate, breathing rate via 60 GHz mmWave radar — $15 hardware, no wearable |
| [Extended Documentation](docs/readme-details.md) | Latest additions, key features, installation, quick start, signal processing, training, CLI, testing, deployment, and changelog |
</details>
---
## 🚧 Beta software

View File

@@ -36,7 +36,10 @@ def main():
dev = a.device
net = PoseNet().to(dev)
net.load_state_dict(torch.load(a.base, map_location=dev), strict=False)
# Checkpoints are tensor state dictionaries; never invoke pickle object loading.
net.load_state_dict(
torch.load(a.base, map_location=dev, weights_only=True), strict=False
)
net.add_lora(r=a.rank).to(dev)
for k, p in net.named_parameters():
p.requires_grad = k.endswith(".A") or k.endswith(".B")

View File

@@ -25,7 +25,10 @@ def main():
dev = a.device
net = PoseNet().to(dev)
net.load_state_dict(torch.load(a.base, map_location=dev), strict=False)
# Checkpoints are tensor state dictionaries; never invoke pickle object loading.
net.load_state_dict(
torch.load(a.base, map_location=dev, weights_only=True), strict=False
)
if a.adapter:
net.add_lora(r=a.rank).to(dev)
z = np.load(a.adapter)

BIN
assets/rucelium-hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

View File

@@ -75,8 +75,6 @@ RUN set -e; \
# Optional bearer-token auth on /api/v1/*: leave unset for LAN-mode (default),
# set to enforce `Authorization: Bearer <token>` (see bearer_auth module, #443).
# docker run -e RUVIEW_API_TOKEN=$(openssl rand -hex 32) ...
ENV RUVIEW_API_TOKEN=
# HTTP API
EXPOSE 3000
# WebSocket

View File

@@ -1,14 +1,14 @@
version: "3.9"
services:
sensing-server:
build:
context: ..
dockerfile: docker/Dockerfile.rust
image: ruvnet/wifi-densepose:latest
# ESP32 CSI must accept LAN UDP; TCP APIs below remain loopback-only.
# kics-scan ignore-line
ports:
- "3000:3000" # REST API
- "3001:3001" # WebSocket
- "127.0.0.1:3000:3000" # REST API
- "127.0.0.1:3001:3001" # WebSocket
# ESP32 UDP. On Linux/macOS this works with multiple ESP32 nodes out of
# the box. On Docker Desktop for Windows, multi-source UDP is collapsed
# to one source IP at the WSL/Hyper-V boundary, so all-but-one node's
@@ -37,6 +37,20 @@ services:
# volumes: ["/path/to/models:/app/models"]
# MODELS_DIR=/app/models
- MODELS_DIR=${MODELS_DIR:-data/models}
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
healthcheck:
test: ["CMD-SHELL", "kill -0 1"]
interval: 30s
timeout: 3s
retries: 3
# No explicit command needed — docker-entrypoint.sh uses CSI_SOURCE.
# Override with: command: ["--source", "esp32", "--tick-ms", "500"]
@@ -46,7 +60,21 @@ services:
dockerfile: docker/Dockerfile.python
image: ruvnet/wifi-densepose:python
ports:
- "8765:8765" # WebSocket
- "8080:8080" # UI
- "127.0.0.1:8765:8765" # WebSocket
- "127.0.0.1:8080:8080" # UI
environment:
- PYTHONUNBUFFERED=1
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
healthcheck:
test: ["CMD", "python", "-c", "import socket; socket.create_connection(('127.0.0.1', 8765), 2).close()"]
interval: 30s
timeout: 3s
retries: 3

View File

@@ -18,9 +18,11 @@ services:
# only activates when OTEL_EXPORTER_OTLP_ENDPOINT is set.
SENSING_FEATURES: mqtt,otel
image: ruvnet/wifi-densepose:otel
# ESP32 CSI must accept LAN UDP; TCP APIs below remain loopback-only.
# kics-scan ignore-line
ports:
- "3000:3000" # REST API
- "3001:3001" # WebSocket
- "127.0.0.1:3000:3000" # REST API
- "127.0.0.1:3001:3001" # WebSocket
- "5005:5005/udp" # ESP32 CSI (see docker-compose.yml for Windows notes)
environment:
- RUST_LOG=info
@@ -30,6 +32,20 @@ services:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
depends_on:
- otel-collector
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
healthcheck:
test: ["CMD-SHELL", "kill -0 1"]
interval: 30s
timeout: 3s
retries: 3
otel-collector:
image: otel/opentelemetry-collector-contrib:0.116.0@sha256:70217a89d27c678ead44f196d80aa8c2717cb68d0301dbdc40331dbec0a3e605
@@ -37,10 +53,24 @@ services:
volumes:
- ./otel-collector.yaml:/etc/otelcol-contrib/config.yaml:ro
ports:
- "4317:4317" # OTLP gRPC (also reachable from the host)
- "4318:4318" # OTLP HTTP
- "127.0.0.1:4317:4317" # OTLP gRPC (also reachable from the host)
- "127.0.0.1:4318:4318" # OTLP HTTP
depends_on:
- ourios
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
healthcheck:
test: ["CMD-SHELL", "kill -0 1"]
interval: 30s
timeout: 3s
retries: 3
# Ourios — OTLP-native log backend (Parquet + Drain-derived template
# mining + DataFusion). Local-disk storage; the tenant derives from the
@@ -57,10 +87,24 @@ services:
- OURIOS_QUERIER_ENABLED=1
- OURIOS_QUERIER_HTTP_ADDR=0.0.0.0:4319
ports:
- "4319:4319" # query endpoint (http://localhost:4319/v1/query)
- "127.0.0.1:4319:4319" # query endpoint (http://localhost:4319/v1/query)
volumes:
- ourios-data:/data
- ourios-wal:/wal
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
healthcheck:
test: ["CMD-SHELL", "kill -0 1"]
interval: 30s
timeout: 3s
retries: 3
volumes:
ourios-data:

View File

@@ -0,0 +1,231 @@
# ADR-288: VEIL — a compliant-waveform privacy shield against unauthorized WiFi sensing
| Field | Value |
|-------|-------|
| **Status** | Proposed — implemented (P1 reference model) |
| **Date** | 2026-08-09 |
| **Deciders** | ruv |
| **Codename** | **VEIL** — Verifiable Emission-shaping for Identity-Leakage prevention |
| **Codebase target** | new leaf crate `v2/crates/wifi-densepose-privshield` |
| **Parent** | ADR-118 (BFLD — the detection layer VEIL is the countermeasure to), ADR-282 (mandatory L0L5 evidence ladder) |
| **Relates to** | ADR-120/121 (BFLD privacy class + identity-risk scoring — the trigger source), ADR-141 (privacy control plane / runtime attestation — the audit consumer), ADR-280 (active sensing / governed actuation — VEIL is a defensive sensing action), ADR-185 §13 (`wifi-densepose-aether` — the pure-compute leaf pattern this crate follows) |
| **Research bundle** | [`docs/research/privacy-shield/`](../research/privacy-shield/) (9 files) |
| **Tracking issue** | TBD |
## 0. PROOF discipline
Every defense number this crate produces is **SYNTHETIC / evidence level L0**
(ADR-282): generated by the crate's own model (`identity::Channel`), attacked by
the crate's own classifier (`attacker::NearestCentroidAttacker`), and scored
against its own known labels. Nothing here has been validated against real WiFi
silicon, and the crate contains no radio integration and cannot emit RF. External
attack/defense results cited from the literature (BFId, LeakyBeam, DySPAN-2026,
IRShield, FCC statutes) are **EXTERNAL** evidence and labelled MEASURED/CLAIMED in
the research bundle. The single measured claim about *our own behavior* is the
pinned deterministic witness in `proof.rs`.
## 1. Context
### 1.1 The gap
IEEE 802.11ac/ax beamforming feedback (BFI) — the compressed Givens-rotation
angle matrices (φ/ψ) a client sends the AP — is transmitted **unencrypted on the
management plane**. Any device in monitor mode can capture it for every station
at once, no network access, and the target need carry no device. The literature
establishes the severity: **BFId** (ACM CCS 2025) re-identifies individuals from
BFI; **LeakyBeam** (NDSS 2025) detects occupancy through walls at 20 m from BFI;
**BeamSense** recognizes activities at up to 99.28%. IEEE Std **802.11bf-2025**
(published 26 Sep 2025) standardizes the sensing measurement/feedback surface
these attacks abuse — and a 2023 proposal for a BFI secure-transmission mechanism
(802.11-23/0782) was **withdrawn**, so the standard shipped with no privacy
protections.
RuView already has a *detection* layer for this: **BFLD** (ADR-118/121) measures
the identity-leakage of each frame and gates what leaves the node. But BFLD
protects *RuView's own outputs*; it does nothing about a **third-party sniffer**
capturing the room's plaintext BFI off the air. There is no RuView component, and
per our market survey no shipping product anywhere, that prevents that.
### 1.2 Constraint: compliant waveform controls, never jamming
The defense must preserve normal communications and must not interfere with any
other station. Jamming (47 U.S.C. §333/§302a) is defined by *adding energy to
interfere with others' transmissions*. Any acceptable control must shape only the
node's **own** standards-conformant emission.
### 1.3 The separability insight
Identity leaks through the *fine* cross-subcarrier phase structure of a
beamforming report; data throughput rides the *dominant* beam direction. These
are (mostly) separable subspaces — so a transform confined to the fine subspace
can wreck re-identification while sparing the beam the link depends on. DySPAN-2026
independently MEASURED that shaping fine-resolution feedback is near-free in
throughput, corroborating the insight.
## 2. Decision
Ship **`wifi-densepose-privshield`** (VEIL) as a standalone pure-compute leaf
crate (the `wifi-densepose-aether`/`nvsim` pattern: dependency-free, deterministic,
WASM-ready, zero coupling to any radio or ingestion path), implementing:
1. **A SYNTHETIC two-subspace BFI model** (`identity.rs`): each identity owns a
stable fine-block signature; sessions add environmental nuisance; the comm
block is identity-free and carries throughput.
2. **The protector** (`protector.rs`): compliant waveform controls, primarily a
**per-session keyed orthogonal rotation of the fine subspace, composed from
extra Givens rotations** — the report's native primitive. Plus feedback
quantization/dither, sounding-cadence randomization, and a `SensingDetector`
that engages the shield only when sensing activity is observed.
3. **The adversary** (`attacker.rs`): a passive nearest-centroid re-identifier
modeling the BFId threat, with selectable Euclidean/Cosine metrics.
4. **A throughput model** (`throughput.rs`):
`(1 sounding feedback_airtime) · C(SNR·(1ρ))/C(SNR)`, where the residual
`ρ` falls with feedback bits and the feedback airtime rises with them — giving
a genuine interior throughput optimum in feedback resolution.
5. **A compliance audit** (`compliance.rs`): the rotation is orthogonal ⇒
energy-preserving ⇒ adds no interfering energy ⇒ **not jamming**, turned into a
checked `ComplianceReport` (energy ratio ≈ 1.0).
6. **The experiment** (`experiment.rs`): runs the attacker against unprotected and
protected traffic and reports both accuracies vs. chance, plus throughput and
compliance, with a single `passed()` verdict.
7. **The hyper-optimizer** (`optimize.rs`): derives the shipped shield config
rather than hand-picking it — the throughput-optimal feedback resolution and
the minimum rotation-mixing budget that collapses re-ID robustly (across both
attacker metrics and N∈{16,32}), plus a Pareto frontier.
8. **A deterministic proof** (`proof.rs`): a pinned FNV-1a witness over the
reference experiment (the `nvsim`/`verify.py` discipline).
### 2.1 Why the keyed Givens rotation
It is simultaneously **orthogonal** (energy-preserving ⇒ compliant),
**key-reversible** (the associated AP shares the session key and recovers the true
precoder ⇒ throughput preserved), and **fresh per session** (a sniffer sees a new
random rotation of the signature each session and cannot average it back ⇒ the
enrollment attack collapses; over unknown rotations the signature carries no
stable discriminative information ⇒ re-ID → chance). It is the shared-secret
precoding idea (cf. MIMOCrypt) specialized to the identity-bearing subspace.
### 2.2 Measured behavior (SYNTHETIC / L0)
Reference experiment at the hyper-optimized operating point (§opt), default
scene, N=16 identities, `cargo test`:
| Metric | Shield off | Shield on |
|---|---|---|
| Passive re-ID accuracy | 100.0% | **4.7%** (chance 6.25%) |
| Link throughput ratio | 100% | **97.6%** |
| Emission energy ratio | — | **1.000000** (compliant) |
All 35 unit/proof tests + doctest pass; the crate builds for
`wasm32-unknown-unknown` and is clippy-clean.
### opt. Hyper-optimization (`optimize.rs`)
The shipped shield config is the optimizer's output, not a guess, and
`ShieldConfig::default()` is asserted equal to it:
- **Feedback resolution = 5 bits.** Throughput has an interior optimum in
feedback bits (residual falls, feedback airtime rises); the unconstrained
optimum is 3 bits (matching DySPAN-2026), and 5 is the throughput-best value in
the spec-allowed 802.11 {5,7,9} set.
- **Givens passes = 96.** The proven minimum for robust collapse — across both
attacker metrics *and* N∈{16,32} — is **48**; the shipped 96 is a free 2×
privacy margin, since the keyed rotation is derived from the shared secret and
never signaled (extra passes cost compute, not airtime). The original
hand-picked 112 was 2.3× over-provisioned.
Net vs. the original hand-picked (112 passes / 7 bits): the optimum is strictly
better on **both** privacy (re-ID 0.047 vs 0.078) and throughput (0.976 vs 0.974),
and is now verified rather than assumed. See
`docs/research/privacy-shield/08-optimization.md`.
### harness. Native terminal harness + TUI (`src/bin/veil.rs`)
A custom, dependency-free binary (`veil`) ships with the crate — the in-repo,
native counterpart to the npm metaharness (ADR-289). It drives the same public
API the tests use, as an interactive ANSI dashboard plus scriptable subcommands
(`report`, `sweep`, `optimize`, `adaptive <N>`, `proof`, `doctor`, `tui`).
Std-only (no `crossterm`/`ratatui`): the TUI is a command-driven redraw loop, so
it runs in any terminal, pipe, or CI and keeps the crate a pure leaf. It reports
only SYNTHETIC/L0 numbers and never relabels them. The wasm leaf story is
unchanged (validated with `--lib`; the bin is native-only).
### sota. 20252026 evidence update (verified)
A cited, adversarially-verified SOTA sweep
(`docs/research/privacy-shield/09-sota-update-2026.md`) refines the threat and
positioning. Load-bearing points for this ADR:
- **Threat is broader and cheaper than §1.1 stated.** A passive, keyless,
single-antenna sniffer at ~20 m and *through walls* can identify people
(BFId, 99.5%/N=197, `MEASURED`), read **breathing** from stationary occupants
and **keystrokes/PINs** (LeakyBeam / WiKI-Eve / SThief, `MEASURED`), and —
decisively — **reconstruct full CSI from the sniffed BFI** (BFIAttack,
≥93% single-antenna, `MEASURED`). VEIL's obfuscation must therefore degrade
*reconstructed-CSI* utility, not merely raw-BFI feature noise; because VEIL's
rotation is a **secret orthogonal** transform, the attacker has no key and no
closed-form to invert — this is now a claim to **test**, not assume.
- **VEIL's family is independently validated.** AP-side per-packet random
unitary on the LTF (LeakyBeam defense, 89.7%→~51%, `MEASURED`) and RIS
obfuscation (PrivISAC, 93%→~30%, robust to a retrained multi-location
attacker, `MEASURED`) confirm standard-permitted beamforming-surface
obfuscation works; DP-Givens quantization (`SYNTHETIC`) offers a formal ε knob.
- **Compliance precedent.** BeamDancer (IEEE TWC 2024, `MEASURED`) argues
native-beamforming obfuscation is 802.11-compliant while jamming/geofencing
are not — cite it as precedent. (Its ">96% PDR" figure was **refuted** in
verification; do not cite it.)
- **Security honesty.** Obfuscation shields have published counter-attacks
("Defeating CSI obfuscation", SnoopFi), so VEIL's own shield security is
`CLAIMED`, not proven-secure, until it withstands learned de-obfuscation.
- **Governance gap.** No claim on 802.11bf-2025 privacy provisions survived
verification; that pillar remains an open question, not an asserted fact.
The derived, prioritized improvement backlog lives in the SOTA-update file (§4).
## 3. What this explicitly is NOT
- **Not a radio driver.** No RF frontend, no transmit path, no
`wifi-densepose-hardware` coupling. VEIL cannot emit and cannot jam.
- **Not a defense against the associated AP.** That party holds the session key by
construction (threat class A3); protecting against a malicious AP is BFLD's
detection/privacy-class problem (ADR-118/141), not this shield's.
- **Not a full motion-obfuscation claim.** A fixed per-session rotation does not
hide coarse within-session motion; identity *re-ID* is the guaranteed target,
motion is partial/future work.
- **Not a real-hardware performance claim.** All defense numbers are SYNTHETIC/L0
until a two-node capture with a boot/runtime-log witness exists (CLAUDE.md
hardware rule; roadmap P5).
- **Not RF denial or camera-grade anything.**
## 4. Simplifications (honesty boundary)
- The two-subspace split is an abstraction; on real radios comm and identity
information are only *approximately* separable, so the real throughput cost of
fully hiding identity may exceed the model's ~2%. DySPAN-2026's MEASURED curve
bounds it as *small* at fine resolution, not zero.
- The attacker is nearest-centroid. The collapse argument is classifier-independent
(it is about the marginalized signal), but P2/P5 must confirm a learned attacker
also collapses.
- The crate's PRNG is SplitMix64 — deterministic and WASM-safe but **not
cryptographic**; a deployment derives the rotation key from the negotiated link
secret, never from this PRNG.
## 5. Consequences
- RuView gains the *countermeasure* half of its RF-privacy story: BFLD detects
leakage, VEIL acts on it — a defensible, standards-anchored, gap-filling
position (see `docs/research/privacy-shield/06-market-and-buyers.md`).
- The compliance audit gives regulators/auditors a machine-checkable "not jamming"
artifact that composes with ADR-141 attestation.
- Future integration (BFLD `identity_risk``SensingDetector`, ADR-280 governed
actuation, firmware feedback shaping, two-node hardware measurement) is staged in
the research bundle roadmap and deliberately deferred so the model validates in
isolation first.
## 6. Validation
```bash
cargo test -p wifi-densepose-privshield --no-default-features
cargo build -p wifi-densepose-privshield --target wasm32-unknown-unknown
cargo clippy -p wifi-densepose-privshield --all-targets
```

View File

@@ -0,0 +1,95 @@
# ADR-289: `wifi-densepose-privshield-harness` — a MetaHarness for the VEIL privacy shield
| Field | Value |
|-------|-------|
| **Status** | Proposed — implemented (P1) |
| **Date** | 2026-08-09 |
| **Parent** | ADR-288 (`wifi-densepose-privshield` / VEIL, the crate this harness assists development on) |
| **Relates to** | ADR-286 (`wifi-densepose-sar-harness`, the per-crate harness scaffold this one mirrors), ADR-285 (`harness/homecore/`, the WASM-first `@metaharness/kernel` pattern), ADR-182 (`harness/ruview/`, the first minted harness), ADR-282 (L0L5 evidence ladder) |
| **Location** | `harness/wifi-densepose-privshield/` |
## 0. PROOF discipline
Every claim below about what is "real" versus "illustrative"/"SYNTHETIC" is
checked by a test in this harness's own suite (router + flywheel + install-smoke
+ guidance). The dependency-free `guidance` surface is covered by
`__tests__/guidance.test.ts`, which runs even before `npm install`. Nothing here
asserts a MEASURED defense result — the harness surfaces the VEIL crate's
SYNTHETIC/L0 numbers with that label intact.
## 1. Context
`wifi-densepose-privshield` (ADR-288) is the VEIL privacy shield — a new,
narrowly-scoped crate. Following the pattern ADR-286 set for
`wifi-densepose-sar`, it gets a dedicated per-crate MetaHarness rather than a
bespoke setup: the `vertical:coding` scaffold (architect/implementer/reviewer/
test-writer, `doctor`) with `@metaharness/router`, `@metaharness/flywheel`, and
Darwin Mode wired in, plus a VEIL-specific, dependency-free `guidance` surface.
## 2. Decision
Land the harness at `harness/wifi-densepose-privshield/`, mirroring
`wifi-densepose-sar-harness`, with two deliberate improvements:
1. **Dynamic dependency imports.** `bin/cli.js` imports the `@metaharness/*`
packages *inside* the commands that need them, not at module top. So
`guidance`, `--help`, and the guidance test run with **zero dependencies
installed** — useful for offline/air-gapped review and for this repo's CI
before `npm install`. Only `init`/`doctor`/`route`/`flywheel` touch the
kernel/host/router/flywheel packages.
2. **A VEIL `guidance` command.** A self-contained, source-cited, read-only
capability map (topics: `overview`, `threat`, `countermeasure`,
`compliance`, `optimization`, `experiment`), each entry carrying a summary,
repo-relative source citations, focused validation commands, and explicit
limitations — the `ruview_guidance` shape, specialized to VEIL. It labels all
defense evidence `SYNTHETIC/L0` and states plainly that guidance is
navigation, not authority.
The standard three self-improvement/cost pieces are wired as real npm
dependencies (not stubs):
- **`@metaharness/darwin`** (devDependency) — `npm run evolve` / `evolve:dry`
mutates the harness's own operating config, keeping only measurable gains.
- **`@metaharness/router`** — `src/router.ts` wires a real cost-optimal `Router`
(`qualityBar: 0.8`, k=1) over two model tiers, with four VEIL-shaped task axes
(threatModeling / complianceReview / optimizerTuning / docWriting). Labelled
examples are illustrative seed data (honesty note in-file).
- **`@metaharness/flywheel`** — `src/flywheel.ts` wires the real
`runFlywheelGenerations` promotion loop (propose → evaluate → gate → promote,
Ed25519-signed, independently replayable) with a SYNTHETIC proposer/evaluator
(`dataSource: 'SYNTHETIC'`, no model call), over VEIL policy levers
(`complianceReview`, `threatTriage`).
## 3. What this explicitly is NOT
- **Not a VEIL runtime.** The harness does not run a radio, emit RF, or jam. It
assists *development* on the crate; it cannot execute the shield on hardware.
- **Not evolving the crate.** Darwin/Flywheel mutate the harness's own policy
(agent prompts, review-checklist depth), not VEIL's Rust code. The crate's
actual hyper-optimization (ADR-288 §opt) was done directly, in the crate.
- **Not a live routing/promotion system.** The router's examples are seed data;
the flywheel's proposer/evaluator are deterministic stand-ins — both honestly
labelled in-source and in `CLAUDE.md`.
- **Not a replacement for the crate's gates.** The authoritative check for a
VEIL change remains `cargo test -p wifi-densepose-privshield`.
- **Not a re-labeller.** The harness must never present VEIL's SYNTHETIC results
as MEASURED, and never scaffold interference-based ("jamming") defenses — both
are hard rules in the harness `CLAUDE.md`.
## 4. Consequences
- The harness ships `guidance`/`doctor`/`init`/`route`/`flywheel`; `guidance`
and `--help` work offline (validated here via `node bin/cli.js`), the rest
after `npm install` + `npm run build` (CI).
- `.harness/manifest.json` + `manifest.sha256` are generated with real per-file
hashes at creation (unlike ADR-286's scaffold, whose manifest was historical).
- Scoped to its own name: its plugin, permissions, and (future) MCP surface only
read/assist on `wifi-densepose-privshield`. No risk to other harnesses/crates.
## 5. Validation
```bash
cd harness/wifi-densepose-privshield
node bin/cli.js guidance --topic overview # dependency-free
npm ci && npm run build && npm test # full suite (CI; needs registry access)
```

View File

@@ -0,0 +1,94 @@
# ADR-290: VEIL end-to-end hardware implementation program (multi-provider firmware)
| Field | Value |
|-------|-------|
| **Status** | Proposed — P4 scaffolding (build-only); portable core validated on host |
| **Date** | 2026-08-09 |
| **Parent** | ADR-288 (VEIL shield), ADR-289 (harness), ADR-282 (L0L5 evidence ladder) |
| **Location** | `firmware/privshield/` |
| **Relates to** | `firmware/esp32-csi-node/` (the CSI sensor/attacker node), ADR-280 (governed actuation), ADR-141 (attestation) |
## 0. PROOF discipline
The **only** artifact validated here is the portable C core
(`firmware/privshield/core/`): a host test (`make test`) checks energy
conservation, reversibility, wrong-key failure, and — pinned — that its
SplitMix64 key schedule is **byte-identical to the Rust crate's** PRNG. That is
`build`/host-level evidence, not silicon. Every per-provider adapter is a
**build-only scaffold** with `TODO(hw)` markers: `SYNTHETIC / L0`, no captured
log, no `MEASURED` claim. Nothing in this ADR asserts VEIL works on real
hardware; it asserts a *plan and a shared core* to get there (P5).
## 1. Context
ADR-288 shipped VEIL as a deterministic, no-radio Rust model, and the 20252026
SOTA sweep (ADR-288 §sota) confirmed the mechanism's family is real and
standard-permitted. The open question left was **"does this run on real WiFi
hardware, and on which?"** — including the user asks: *can OpenWRT / open WiFi
software implement it, and can ESP32 help scramble signals?* Answering requires
committing to the platform reality rather than assuming a uniform "firmware"
target.
## 2. Decision
Stand up `firmware/privshield/` as a **multi-provider E2E program** around one
shared, validated core:
1. **A portable C shield core** (`core/veil_shield.{h,c}`) — the keyed
Givens-rotation obfuscation, `no_std`-friendly C99 (no malloc/libc I/O), with
a SplitMix64 key schedule matching the Rust crate so on-air behavior is
identical everywhere and every adapter links the *same* math. Host-tested.
2. **Per-provider adapters**, each built and graded by a hardware research
agent, honest about what its stack can actually touch:
- **`openwifi/`** (open PHY/MAC on SDR/FPGA) — the highest-capability path and
the one that can host the **keyed-reversible** design end-to-end
(protector + AP-side compensation). Carries the **P5 measurement protocol**
(`MEASUREMENT.md`) that yields the first `MEASURED` result with a witness.
- **`openwrt/`** (Linux `mac80211`, mt76/ath9k…) — the commodity path.
Sounding-cadence randomization, MU-group and stream-mapping control are
feasible from the driver/hostapd; the per-packet unitary on the LTF spatial
mapping is firmware-deep on most parts. Partial.
- **`nexmon/`** (Broadcom/Cypress C firmware patches) — the commodity
C-firmware route; the read path is proven (Wi-BFI/nexmon_csi), the transmit
report-shaping path is research-grade/partial.
- **`esp32/`** (ESP-IDF) — **not** a feedback protector (the beamforming path
is a closed blob): ESP32 shapes CSI *read*, not transmitted feedback. Its
legitimate roles are a **sensing detector** (trigger the AP-side shield) and
an **RIS controller** (drive an external reconfigurable surface to scramble
the sensing direction — the honest way ESP32 "helps scramble", via an
external surface, not its own PHY).
3. **Compliance stance carried into hardware:** every control shapes the node's
own standards-conformant emission and preserves energy; the ESP32
decoy/cover-traffic idea is documented as *legally sensitive / not
recommended* precisely because it edges toward the interference line.
Per-provider feasibility grades live in each subdir README and the top-level
feasibility matrix; they are the answer to the "which hardware" question.
## 3. What this explicitly is NOT
- **Not validated firmware.** No adapter has run on silicon; there is no witness.
The scaffolds compile-*shaped*, not compile-*guaranteed* on their toolchains
(which are absent in this environment).
- **Not a claim that ESP32 can shield beamforming feedback** — it cannot; it is a
detector/RIS-controller only.
- **Not jamming, on any platform.** Compliant waveform shaping only.
- **Not a MEASURED result.** That is P5, gated on a captured log.
## 4. Consequences
- One validated core, four honest provider scaffolds, and a concrete P5
measurement plan — a real path from model to silicon, with the effort/blocker
reality made explicit per platform.
- The shared core keeps every future hardware result consistent with the crate
and with each other.
- Scope stays inside `firmware/privshield/`; no other crate/firmware is touched
(the existing `esp32-csi-node` remains the sensor/attacker node).
## 5. Validation
```bash
cd firmware/privshield/core && make test # host: energy/reversibility/PRNG parity
# per-provider builds require their toolchains (ESP-IDF, OpenWRT SDK, Nexmon,
# Vivado) and real hardware — see each subdir's BUILD/INTEGRATION notes.
```

View File

@@ -0,0 +1,106 @@
# ADR-291: Public-benchmark evaluation harness — Widar3.0 ingest, standard split protocols, leakage guards
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-10
- **Deciders**: ruv
- **Tags**: training, evaluation, benchmarks, widar, mm-fi, leakage, honesty
## Context
RuView implements the field's key techniques (CSI ratio, BVP features, MAE
pretraining, rapid adaptation) but reports results only on self-collected data
with self-defined metrics (e.g. the README's held-out temporal-triplet
accuracy). A 2026 deep-research sweep of the WiFi-sensing literature found:
1. Cross-domain generalization is the field's central unsolved problem; the
only widely reproduced cross-domain result is Widar3.0's BVP benchmark.
2. MM-Fi (NeurIPS 2023) is the standard WiFi-pose benchmark, with defined
cross-subject and cross-environment protocols.
3. The field had a documented leakage reckoning in 20242025: window-level
random splits on continuous recordings inflate accuracy (one dataset's F1
collapsed from ~90% to ~22% under subject-disjoint splits — Sensors
24(10):3159; Signals 6(4):59).
`wifi-densepose-train` already has an `MmFiDataset` NPY loader and a
deterministic `SyntheticCsiDataset`, but no Widar3.0 ingest, no standard split
protocols, and no structural leakage guard. CLAUDE.md already requires
mean-pose baselines and leakage-free held-out splits for pose PCK; nothing in
the code enforces this.
Without leaderboard-comparable numbers, RuView's claims cannot be ranked
against published systems, which blocks both scientific credibility and
commercial (OEM licensing) conversations.
## Options considered
1. **Do nothing; keep self-collected metrics.** Rejected: perpetuates the
comparability gap.
2. **Port a Python eval stack (SenseFi) alongside the Rust pipeline.**
Rejected: violates the v2 Rust-workspace direction and adds an unreviewed
dependency surface.
3. **Extend `wifi-densepose-train` with native loaders + protocol machinery.**
Chosen.
## Decision
Extend `v2/crates/wifi-densepose-train` with three additions:
### 1. Widar3.0 ingest (`dataset::widar`)
- A parser for the Intel 5300 `.dat` CSI log format ("bfee" records) used by
the Widar3.0 raw distribution: framed records with a 3-byte header
(2-byte little-endian length + 1-byte code 0xBB), a 20-byte bfee header
(timestamp_low, bfee_count, Nrx, Ntx, RSSI a/b/c, noise, agc, antenna_sel,
len, rate), and a packed 10-bit-per-component complex CSI payload of
30 subcarrier groups. Invalid records are skipped with a warning, not a
panic — untrusted file input is validated at the boundary per CLAUDE.md.
- A `WidarDataset` implementing the existing `CsiDataset` trait, mapping
Widar's `Nrx × Ntx × 30` CSI into windowed `CsiSample`s via the existing
subcarrier interpolation, with domain metadata (user, room, orientation,
gesture) parsed from Widar's documented directory/file naming convention.
- No network access: the loader reads a local dataset root. Dataset download
remains a documented manual step.
### 2. Split protocols (`protocols`)
- A `SplitProtocol` type expressing the standard evaluations: cross-subject
(MM-Fi style), cross-environment/room, cross-orientation (Widar style), and
random-baseline (explicitly labelled as leakage-prone, for comparison only).
- Split assignment is a pure function of sample metadata + a seed — fully
deterministic, no RNG state.
### 3. Leakage guards (`protocols::leakage`)
- A structural `LeakageAudit` that, given a proposed train/test split,
verifies: (a) subject-disjointness, (b) environment-disjointness where the
protocol claims it, (c) no two windows from the same continuous recording
span both sides of the split. A failed audit is an `Err`, not a warning.
- PCK/accuracy reporting requires a `MeanPoseBaseline` computed from the
training split only, and reports model-vs-baseline together, enforcing the
CLAUDE.md rule in the type system rather than by convention.
- Evaluation output is an evidence-tagged report (`MEASURED` requires a
reproducer command line embedded in the report; anything else is emitted as
`SYNTHETIC` or `CLAIMED`).
## Consequences
- RuView results become comparable to published numbers (Widar3.0 cross-domain
gesture; MM-Fi cross-subject pose) for the first time.
- The leakage audit will make some existing internal numbers look worse. That
is the point.
- Parsing a legacy binary format adds maintenance surface; mitigated by
fixture-based tests with synthetic, deterministically generated `.dat`
bytes (no dataset redistribution).
- Widar's raw distribution is Intel 5300-specific; ESP32-captured data
continues through existing loaders. The protocols/leakage machinery is
loader-agnostic.
## Validation
- `cargo test -p wifi-densepose-train` — unit tests for the bfee parser
(truncated, corrupt, and valid synthetic fixtures), split determinism,
leakage-audit rejection cases, and mean-pose baseline math.
- `cargo bench -p wifi-densepose-train` — criterion benchmark for parser
throughput and split assignment on synthetic corpora.
- No accuracy numbers are claimed by this ADR; it delivers the machinery to
produce MEASURED ones.

View File

@@ -0,0 +1,91 @@
# ADR-292: Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter and subcarrier-agnostic plumbing
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-10
- **Deciders**: ruv
- **Tags**: hardware, csi, 80211ax, ax210, feitcsi, ingest, mat
## Context
RuView's CSI ingest (`wifi-densepose-mat/src/integration/hardware_adapter.rs`)
supports ESP32 serial streams, the legacy Intel 5300 tool, and Atheros/Nexmon
paths. All of these are 802.11n-class: ≤40 MHz bandwidth, ≤114 subcarriers,
2.4/5 GHz.
The 2026 research sweep found the field's center of gravity has moved to
Intel AX200/AX210 NICs via PicoScenes (closed-source core) and FeitCSI
(open-source, GPL): 802.11ax CSI at up to 160 MHz / 1992 subcarriers,
including the 6 GHz band. This is both the research-grade tier today and the
shape of the data 802.11bf silicon will deliver from ~2026 onward. RuView's
`wifi-densepose-hardware` crate already models 802.11bf session types, but no
ingest path can carry wideband CSI into the pipeline.
Without a wideband path, RuView cannot develop against the best available
signal, cannot compare ESP32-grade results to wideband upper bounds, and will
meet 802.11bf silicon with no tested plumbing for >114-subcarrier frames.
## Options considered
1. **PicoScenes `.csi` ingest.** Rejected for now: the format is produced by a
closed-source core and is versioned/complex; parsing it without a
maintained spec invites silent corruption.
2. **Raw pcap + radiotap parsing.** Rejected: duplicates what FeitCSI already
does on-device, and pulls a packet-capture dependency into the pipeline.
3. **FeitCSI file/stream ingest.** Chosen: FeitCSI is open-source (its header
layout is auditable against the source), targets AX200/AX210, covers
20160 MHz including 6 GHz, and emits a compact binary record per frame.
## Decision
Extend `v2/crates/wifi-densepose-mat/src/integration` with:
### 1. `feitcsi` record parser
- A validated parser for FeitCSI's binary CSI record layout (header with
CSI buffer length, rate/bandwidth/channel metadata, antenna counts, RSSI,
timestamp, followed by interleaved complex CSI). The parser is written
against the documented layout, is version-checked, and rejects
records whose declared dimensions disagree with the buffer length —
untrusted file/stream input is validated at the boundary.
- Bounded allocation: a hard cap on subcarrier count (4096) and antenna
count (8) so a corrupt length field cannot cause unbounded allocation.
### 2. `DeviceType::FeitCsi` in the hardware adapter
- File-replay mode (read a recorded FeitCSI capture deterministically) and a
streaming mode fed by an external process writing to a path/pipe. No
privileged operations inside the crate: RuView does not configure the NIC;
FeitCSI's own tooling owns that, per least-authority.
### 3. Subcarrier-agnostic plumbing
- Ingest carries native subcarrier dimensionality end-to-end and converts to
pipeline width explicitly via the existing interpolation/decimation stage,
recording the native → pipeline mapping in frame metadata so downstream
consumers know the true spectral resolution. Bandwidth (20160 MHz) and
band (2.4/5/6 GHz) become first-class frame metadata.
## Consequences
- RuView gains a research-grade wideband development path and a tested
ingest shape for future 802.11bf reporting (truncated CIR is a natural
extension of the same plumbing).
- GPL FeitCSI is used as an external tool, never linked: only its output
format is parsed. No licensing contamination of the MIT workspace.
- The parser tracks an external project's format; version checks fail loudly
on mismatch rather than misparse.
- ESP32 remains the deployed sensor tier; wideband is a development/
validation tier. Accuracy claims from wideband captures must be tagged with
the capture hardware.
## Validation
- `cargo test -p wifi-densepose-mat` — parser tests over synthetic fixtures:
valid records at 20/80/160 MHz shapes, truncated buffer, dimension
mismatch, version mismatch, allocation-cap enforcement; adapter replay
determinism.
- `cargo bench -p wifi-densepose-mat` — criterion benchmark for record parse
throughput at 1992-subcarrier frames.
- Hardware validation on real AX210 silicon is explicitly out of scope for
this PR and remains required (per CLAUDE.md) before any capture-path
hardware claim; the file-replay path is testable without silicon.

View File

@@ -0,0 +1,93 @@
# ADR-293: Vitals ground-truth rig — reference ingest, time alignment, and agreement metrics
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-10
- **Deciders**: ruv
- **Tags**: vitals, validation, ground-truth, bland-altman, evidence, honesty
## Context
`wifi-densepose-vitals` (ADR-021) extracts breathing (0.10.5 Hz) and heart
rate (0.82.0 Hz) from CSI. The 2026 research sweep found that every credible
vitals result in the literature ships with reference-sensor ground truth
(chest strap, pulse oximeter, ECG, or PSG), and that WiFi heart-rate numbers
without stated scope (single person, static, line-of-sight, short range) are
systematically misleading. RuView currently has no way to produce a MEASURED
vitals number: there is no reference-signal ingest, no time alignment between
CSI-derived estimates and a reference device, and no agreement statistics.
CLAUDE.md requires accuracy statements to be tagged MEASURED (with a
reproducer), CLAIMED, or SYNTHETIC. For vitals, MEASURED is currently
unreachable.
## Options considered
1. **Live BLE/ANT+ integration with reference devices.** Rejected for now:
drivers and pairing are a hardware/product concern; the blocking gap is
the evaluation math, not the radio link.
2. **File-based reference ingest + offline agreement analysis.** Chosen:
every consumer reference device (Polar, Garmin, oximeters) exports
timestamped series; a file boundary keeps the crate dependency-free and
the pipeline deterministic.
## Decision
Add a `groundtruth` module to `v2/crates/wifi-densepose-vitals`:
### 1. Reference series ingest
- `ReferenceSeries`: timestamped samples (unix millis + value) for one
measurand (`HeartRateBpm` or `BreathingRateBrpm`), with device metadata
(make/model, measurement principle). Parsed from CSV (`timestamp_ms,value`
with a header line); malformed rows are rejected with row-numbered errors —
untrusted file input validated at the boundary. Non-monotonic timestamps
are an error, not silently sorted.
### 2. Time alignment
- Constant-offset estimation by maximizing normalized cross-correlation of
the estimate series against the reference over a bounded lag window
(default ±30 s), on a common resampled grid (nearest-sample, no
interpolation of physiological values across gaps larger than a
configurable limit).
- Optional linear clock-drift fit (offset + rate) for long sessions.
Alignment parameters are reported, never silently applied.
### 3. Agreement metrics
- `AgreementReport`: n paired samples, coverage fraction (time where both
series had valid samples), MAE, RMSE, mean error (bias), BlandAltman
95% limits of agreement, and percentage-within-tolerance (configurable,
default ±2 bpm HR / ±1 brpm breathing).
- Session scope is mandatory metadata: subject count, motion state
(static/moving), line-of-sight (LOS/NLOS/through-wall), distance band.
A report without scope cannot be constructed.
### 4. Evidence tagging
- `EvidenceGrade::Measured` is only constructible when the report carries a
reference device, non-zero paired samples, minimum coverage, and a
reproducer command string; otherwise the report grades as `Claimed` (real
data, no reference) or `Synthetic` (generated input). This mirrors
ADR-291's enforcement-in-types approach and the CLAUDE.md tagging rule.
## Consequences
- RuView can convert vitals claims from CLAIMED to MEASURED with a
reproducible offline analysis, session by session, scope by scope.
- Honest reporting will likely show heart-rate performance below marketing
intuition, especially NLOS/moving — that is the purpose.
- CSV ingest means a manual export step per session; acceptable at current
scale, and the format is the de-facto export of consumer reference gear.
- No clinical claim is implied: agreement statistics against consumer
reference devices are engineering evidence, not medical validation.
## Validation
- `cargo test -p wifi-densepose-vitals` — CSV rejection cases, alignment
recovery of known synthetic offsets/drifts, agreement metrics against
hand-computed fixtures, evidence-grade constructibility rules.
- `cargo bench -p wifi-densepose-vitals` — criterion benchmark for alignment
over hour-scale synthetic sessions.
- Real-session validation (ESP32 capture + chest strap) remains a follow-up
requiring hardware evidence per CLAUDE.md.

View File

@@ -0,0 +1,83 @@
# ADR-294: WiFi Veil integration — emission-shaping countermeasure as an advisory BFLD dependency
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-10
- **Deciders**: ruv
- **Tags**: privacy, bfld, bfi, wifi-veil, countermeasure, dependency
## Context
RuView's BFLD layer (ADR-118, ADR-141) senses via beamforming feedback while
enforcing structural privacy invariants on data entering the node. The 2026
research sweep identified the complementary, unaddressed surface: a node's own
*outgoing* BFI is unencrypted and enables passive third-party
re-identification (BFId, ACM CCS 2025); IEEE 802.11bf-2025 shipped with no
privacy mechanism; and no commercial product occupies the countermeasure
category.
[`wifi-veil`](https://github.com/ruvnet/wifi-veil) (codename VEIL, extracted
from this monorepo as a standalone crate) models a compliant emission-shaping
defense: keyed Givens rotations over the fine subspace of compressed
beamforming reports, energy-preserving (never jamming), reversible by a
keyed legitimate receiver. The crate is dependency-free, deterministic,
std-only, WASM-ready, dual MIT/Apache-2.0, and explicitly SYNTHETIC/L0: it
models waveform controls and never drives a radio.
RuView should consume this capability rather than re-implement it, giving the
sensing stack a defensive counterpart under one evidence regime.
## Options considered
1. **Vendor the veil sources into a RuView crate.** Rejected: forks the
witness-pinned upstream and duplicates maintenance.
2. **crates.io dependency.** Not yet available (v0.1.0 unpublished at
decision time); revisit when released.
3. **Git dependency pinned to an exact rev, feature-gated in
`wifi-densepose-bfld`.** Chosen.
## Decision
- Add `wifi-veil` to `v2/Cargo.toml` `[workspace.dependencies]` as a git
dependency pinned to rev `018468b5d2bf41f35c552910f35659830af0eb91`
(v0.1.0). Exact-rev pinning preserves provenance and reproducibility for a
pre-release upstream; bumping the rev is an explicit, reviewable change.
- Gate it in `wifi-densepose-bfld` behind a new `veil` feature
(`veil = ["std", "dep:wifi-veil"]`), off by default — the default build
remains dependency-light and unchanged.
- New `bfld::veil` module (advisory-only):
- `ShieldAssessment`: stable projection of wifi-veil's deterministic
attacker-vs-protector `ExperimentReport` (re-ID accuracy shield-off/on,
chance level, throughput ratio, energy-conservation audit), always
carrying the `SYNTHETIC/L0` evidence label.
- `assess` / `assess_default`: run the deterministic experiment.
- `optimized_shield`: wrap `hyper_optimize` to derive the
optimizer-shipped shield config plus its verifying assessment.
- Boundaries, stated structurally and in docs:
- **Advisory only.** Nothing in the integration emits RF, alters frames,
or relaxes any BFLD gate/invariant (I1I3 untouched).
- **Evidence honesty.** Every veil-derived figure is labeled
`SYNTHETIC/L0`; no MEASURED claim is possible from this path (hardware
validation lives in wifi-veil's own P5 roadmap).
- ESP32 nodes cannot shield their own feedback (per wifi-veil's platform
matrix); the integration therefore informs posture and reporting, not
on-node emission control.
## Consequences
- RuView gains a sense-and-defend posture no commercial offering has, under
a single claim taxonomy.
- First git dependency in the workspace: builds now fetch one pinned
external rev. Acceptable: the crate is dependency-free, small, witness-
pinned upstream, and license-compatible (MIT OR Apache-2.0 into MIT).
- Feature-gated consumers (e.g. sensing-server privacy reporting, the
desktop UI) can surface shield assessments later without new deps.
- When wifi-veil publishes to crates.io, switch the workspace entry to a
version requirement in a follow-up ADR amendment.
## Validation
- `cargo test -p wifi-densepose-bfld --features veil` — determinism,
shield-reduces-re-ID, compliance (energy conservation), chance-band
attainment, evidence labeling, optimizer wrapper.
- `cargo test -p wifi-densepose-bfld` (default features) — unchanged
behavior with the feature off.

View File

@@ -0,0 +1,64 @@
# ADR-295: 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`.

View File

@@ -0,0 +1,59 @@
# ADR-296: 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.

View File

@@ -0,0 +1,58 @@
# ADR-297: 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-295 (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`.

View File

@@ -0,0 +1,60 @@
# ADR-298: 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-291 `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.

View File

@@ -0,0 +1,50 @@
# ADR-299: 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.

View File

@@ -0,0 +1,189 @@
# ADR-300: RuView perception substrate — a phased program for the calibration, evidence, trust, and deployment layer
- **Status**: Accepted — program framing; child ADRs carry their own status
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: program, architecture, calibration, evidence, provenance, fusion, fleet, epic
## Context
Three independent analyses converged on the same conclusion in 2026: a deep
research sweep of the WiFi-sensing state of the art, an external technical and
industry review, and an internal strategic assessment. All three found that
RuView's gap is **not another sensing modality** but the horizontal layer that
turns RF research into repeatable spatial infrastructure — measurement,
calibration, out-of-distribution awareness, evidence accounting, authenticated
identity, a canonical spatial model, and fleet deployment.
Several of these primitives already have foundations in the tree and should be
**unified and made to produce signed, expiring certificates**, not rebuilt:
- `wifi-densepose-calibration` (enrollment, bank, anchor, runtime, specialist).
- `frame::EvidenceLevel` L0L5 as mandatory policy (ADR-282).
- AetherArena benchmark infrastructure — v0 complete, CI-gated, witness ledger,
live HF Space (ADR-149); board intentionally empty (benchmark-first).
- RuField provenance/signature types (ADR-260/262/277/279) and BFLD
attestation (ADR-141).
- `worldgraph` crate; `wifi-densepose-mat/tracking` (tracker, fingerprint).
- The in-flight ADR-295 (provenance state machine), ADR-296 (authenticated
data plane, step one), ADR-298 (model sanity gates) — the first bricks.
## What RuView is optimizing for
Not inference capability — **epistemic reliability**:
```
signal → observation → calibration → inference → uncertainty → evidence
→ certificate → policy → governed action
```
That pipeline is the product. The defensible category is not "RuView perceives
the physical world" but "RuView determines what machines are justified in
believing about it, proves why, and constrains what they may do with that
belief."
### Four non-negotiable program rules
Every child ADR and implementation is bound by these:
1. **UNKNOWN is a first-class output, never an error condition.** A surface that
cannot answer says UNKNOWN and stays legible; it does not throw, default to a
confident class, or silently hold a stale value.
2. **Capability certificates bind cryptographically.** Hardware, environment,
model, calibration, metrics, expiry, and evidence level are bound under one
signature (ADR-318/ADR-305). An unsigned or partially-bound certificate is
not a certificate.
3. **One canonical semantics downstream.** Every surface (MQTT, REST, WebSocket,
RuField, Matter, agents, UI) consumes the same Observation → Inference →
GovernedEvent types (ADR-306). No transport- or UI-specific reinterpretation.
4. **Benchmarks expose worst-domain performance and confidence intervals.**
Pooled accuracy is never sufficient for promotion (ADR-317).
### Certificate conditionality (the staleness guard)
The central architectural risk is **certificate staleness**: a room can remain
syntactically calibrated while its RF distribution has drifted enough to
invalidate the certificate. Therefore a capability certificate is **conditional
on a continuously evaluated domain signature** (ADR-302), not a one-time stamp.
Crossing the OOD threshold automatically degrades state and triggers
recalibration rather than silently continuing:
```
VALID → DEGRADED → UNKNOWN (auto-degrade on domain drift; triggers recalibration)
```
This binds ADR-301 (calibration), ADR-302 (OOD), ADR-318 (certificate), and
ADR-321 (policy): a degraded/unknown domain must invalidate the affected
capability *before* a false confident inference reaches an actuator.
### Commercial framing — three primitives, not one product
- **RuView Runtime** — provides perception.
- **RuView Certify** — establishes what a deployment can legitimately claim
(calibration + evidence + capability certificate + policy).
- **RuView Trust / Fleet** — keeps that claim valid across hardware, firmware,
models, and environmental drift (ADR-316).
Certify and Trust are the parts that are hard to commoditize; presence
detection alone is not.
## Decision
Adopt a **21-primitive phased program**. Each primitive gets a child ADR
(ADR-301…ADR-321) that owns its detailed decision, status, and validation.
This ADR owns the framing, the dependency order, and the phase assignment.
### Primitive → ADR map
| # | Primitive | ADR | Phase |
|---|---|---|---|
| 1 | Automatic domain calibration | ADR-301 | 1 |
| 2 | Out-of-distribution detection | ADR-302 | 1 |
| 3 | Ground-truth synchronization | ADR-303 | 2 |
| 4 | Evidence engine | ADR-304 | 1 |
| 5 | Authenticated sensor identity | ADR-305 | 1 |
| 6 | Canonical spatial ontology | ADR-306 | 1 |
| 7 | Persistent identity & tracking | ADR-307 | 2 |
| 8 | Sensor placement optimizer | ADR-308 | 3 |
| 9 | Active sensing | ADR-309 | 3 |
| 10 | 802.11bf-native architecture | ADR-310 | 2 |
| 11 | Real sensor fusion | ADR-311 | 2 |
| 12 | Long-term spatial memory | ADR-312 | 3 |
| 13 | Counterfactual inference | ADR-313 | 3 |
| 14 | Information-gain scheduler | ADR-314 | 3 |
| 15 | Digital RF twin | ADR-315 | 3 |
| 16 | Fleet control plane | ADR-316 | 2 |
| 17 | Real benchmark service (multi-domain scorecard) | ADR-317 | 1 |
| 18 | Capability certificates | ADR-318 | 1 |
| 19 | Witness chain | ADR-319 | 1 |
| 20 | RuView sensor HAL | ADR-320 | 2 |
| 21 | Decision policy — action authorization | ADR-321 | 1 |
### Dependency order (why phase, not score, drives sequencing)
```
ADR-306 spatial ontology ──┐
ADR-305 auth identity ─────┼──► ADR-301 calibration cert ──► ADR-302 OOD gating
│ │ │
└──► ADR-319 witness chain │ (VALID→DEGRADED→UNKNOWN)
│ ▼
ADR-304 evidence engine ──► ADR-318 capability certificate
│ │ (conditional on domain signature)
│ ▼
│ ADR-321 decision policy ──► governed action
└──► ADR-317 benchmark scorecard (per-PR gate)
```
- **Phase 1 (the certificate spine, built now):** foundational roots 303, 302,
301, 298 (implemented first, in their own crates); then the dependent wave
316, 299, 315, 314, 318. This set is exactly the acceptance test decomposed
and is buildable without new hardware (types, logic, signatures, tests). The
dependent wave adds the staleness guard (299 auto-degrades 315) and the
action gate (318) that denies at the actuator on a degraded/unknown domain.
- **Phase 2 (integration & operations):** 300 ground truth, 304 tracking, 307
802.11bf-native, 308 fusion, 313 fleet, 317 HAL. Depends on the spine.
- **Phase 3 (higher-ceiling, research-forward):** 305 placement optimizer, 306
active sensing, 309 spatial memory, 310 counterfactual, 311 info-gain
scheduler, 312 RF twin. Sit on top of the fused world state.
Phase-2 and phase-3 child ADRs are authored as **Proposed** (design intent,
validation plan) and are not implemented by the phase-1 swarm.
### Acceptance test A — onboarding (from the strategic assessment)
> Connect a new sensor type in an unseen room. Within 30 minutes RuView should
> identify the hardware (HAL, ADR-320), calibrate the environment (ADR-301),
> quantify whether it can reliably sense the requested phenomenon (ADR-302),
> generate a signed capability certificate (ADR-318), expose governed spatial
> events (ADR-306), and return UNKNOWN whenever evidence falls outside that
> certificate (ADR-302).
### Acceptance test B — drift invalidation (the staleness guard)
> Deliberately change the room after certification — move furniture, change the
> AP channel, or substitute hardware. RuView should detect distribution drift
> (ADR-302), invalidate the affected capability (ADR-318) **before** a false
> confident inference reaches an actuator (ADR-321 denies with the specific
> failed condition), emit UNKNOWN, preserve the complete witness chain
> (ADR-319), and explain exactly which certificate condition failed.
Test B is the load-bearing one: it proves the substrate fails safe, not just
that it perceives well. Phase 1 makes every clause except HAL testable in
software; HAL (phase 2)
closes the "identify the hardware" clause.
## Consequences
- One coherent substrate replaces overlapping ad-hoc schemas; every surface
(MQTT, REST, WebSocket, RuField, Matter, agents) eventually consumes the
ADR-306 ontology and the ADR-318 certificate.
- Headline applications (pose/vitals/pointcloud models) are explicitly **not**
the investment focus during this program, per the strategic direction.
- Later ADRs may be revised as the spine lands; that is expected for a phased
program and is why phase-2/3 ADRs ship as Proposed.
## Validation
- Each child ADR defines its own tests. The program-level exit is the
acceptance test above, run end-to-end once phase 1 lands, and encoded as an
AetherArena scenario (ADR-317).

View File

@@ -0,0 +1,149 @@
# ADR-301: Automatic domain calibration — signed, versioned, invalidatable room fingerprint
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: calibration, provenance, drift, evidence, honesty, substrate
## Context
This ADR is primitive 1 of the perception-substrate program (ADR-300) and the
first brick of that program's "certificate spine" (ADR-300 phase 1). It depends
on the canonical spatial ontology (ADR-306) to name *which space* it
characterizes, on authenticated sensor identity (ADR-305) to bind a fingerprint
to *which signed device* produced it, and on the witness chain (ADR-319) to
anchor the resulting artifact. Its output is consumed directly by
out-of-distribution detection (ADR-302).
WiFi sensing is only reproducible inside the environment it was tuned for.
Multipath, furniture geometry, transceiver placement, and AP channel all shape
the CSI distribution, so a model that reads a room correctly one week can drift
silently the next. RuView already has the raw ingredients for room-aware
sensing but not a single portable, signed, expiring artifact that says "this is
the room, here is when it was measured, and here is the evidence that it is
still the same room."
Existing scaffolding to build on, not rebuild (`v2/crates/wifi-densepose-calibration`):
- `enrollment` / `anchor` — guided human anchors with an adaptive quality gate.
- `bank` / `specialist` / `runtime` — a versioned bank of small specialist
models and a confidence-gated mixture runtime (`RoomState`), including the
crate's existing honest `STALE` degradation when the ADR-135 empty-room
baseline drifts.
- `geometry` / `geometry_embedding` — transceiver-geometry record and its
fixed-length conditioning featurization (ADR-152).
What is missing is (a) an *automatic* observe-only characterization phase that
does not require a human enrollment ritual, (b) empty-vs-occupied baseline
separation as a first-class pair, (c) a signed, versioned, comparable
`CalibrationCertificate` artifact, and (d) explicit invalidation on drift rather
than a soft `STALE` flag buried in the runtime.
## Options considered
1. **Keep calibration internal to the runtime (status quo).** Rejected: the
room characterization exists only as in-process state; it cannot be signed,
shipped, compared across time, or presented as evidence to ADR-302/ADR-318.
2. **Build a new calibration crate.** Rejected: `wifi-densepose-calibration`
already owns enrollment, the specialist bank, geometry embedding, and the
baseline-drift concept. A parallel crate would fork the room model.
3. **Extend `wifi-densepose-calibration` with an automatic characterization
phase and a signed certificate artifact.** Chosen.
## Decision
Extend `v2/crates/wifi-densepose-calibration` with an `autocal` characterization
phase and a `certificate` artifact module. The target UX is:
> install → observe (~10 min) → room fingerprint → calibration certificate →
> sensing.
### 1. Automatic characterization (`autocal`)
- An observe-only pass (default ~10 minutes, configurable) that collects CSI
without requiring guided human anchors, reusing the `anchor` quality gate to
reject frames it cannot trust. It layers on the existing ADR-135 empty-room
baseline rather than replacing it.
- Produces a `RoomFingerprint`: a bounded, fixed-length statistical summary of
the room's CSI distribution (subcarrier amplitude/phase moments, multipath
structure, occupancy-band energy), plus the `geometry_embedding` when a
geometry record is present. The fingerprint is the distance-comparable object
ADR-302 measures against; its schema is versioned.
### 2. Empty / occupied baseline pair
- Characterization establishes a paired baseline: an **empty** distribution
(no occupant motion) and an **occupied** distribution (motion present),
separated by the existing occupancy signal rather than a manual label. Both
are stored on the fingerprint so downstream OOD gating can distinguish "the
empty room changed" (furniture/geometry drift) from "occupancy statistics
changed" (different subject dynamics).
### 3. `CalibrationCertificate` artifact
- A serializable `CalibrationCertificate` binding: the `RoomFingerprint`; a
space identifier from the ADR-306 ontology; the signing sensor identity from
ADR-305; `captured_at_unix_s`; a monotonic `version`; a schema version; the
calibration `tier`; and an `EvidenceLevel` (L0L5, ADR-282) — an automatic
characterization on real captured CSI is at most L1/L2 and is labelled as
such, never L3+.
- The certificate is **signed** using RuField provenance/signature types
(ADR-260/262/277/279) and anchored in the witness chain (ADR-319). Signature
and witness anchoring are mandatory: an unsigned certificate is not a valid
certificate.
- Two certificates for the same space are **comparable**: `distance(a, b)`
returns a bounded fingerprint distance, which is the primitive ADR-302 uses
to gate KNOWN → DEGRADED → UNKNOWN.
### 4. Invalidation and continuous drift compensation
- A certificate carries an explicit validity policy: it is invalidated when
fingerprint distance against live traffic exceeds a threshold, when the AP
channel or transceiver geometry changes, when the signing device identity
changes, or on age expiry. Invalidation is an explicit state transition that
emits a witness record (ADR-319), not a silent `STALE` flag.
- Continuous drift compensation runs as a bounded online update of the
fingerprint within a **compatibility envelope**: small drift is absorbed and
logged; drift beyond the envelope invalidates the certificate and forces
re-characterization. Compensation never silently rewrites a signed
certificate — it produces a new version, preserving the append-only history.
### Provenance and honesty discipline
- No accuracy number is claimed by this ADR; it delivers the artifact and the
distance/invalidation machinery. Any certificate produced from generated CSI
is L0/`Synthetic` by construction; the constructor rejects labelling
synthetic characterization as measured (ADR-279 invariant 6, ADR-282 ladder).
- Certificates never leave the edge except through the governed control plane
(ADR-277); a room fingerprint is treated as potentially sensitive spatial
data, not free telemetry.
## Consequences
- Room characterization becomes a portable, signed, versioned artifact that
ADR-302 (OOD), ADR-318 (capability certificates), and ADR-317 (benchmark)
can consume without re-deriving room state.
- The automatic observe-only path lowers deployment friction (no mandatory
enrollment ritual) but yields a weaker evidence level than guided enrollment;
the certificate states which path produced it so consumers can weight it.
- Explicit invalidation means RuView will sometimes refuse to sense a changed
room until re-characterization. That refusal is the intended honest behavior,
surfaced by ADR-302, not a regression.
- The existing enrollment/bank/runtime path is preserved; `autocal` is an
additional entry point that produces the same `RoomFingerprint` object the
guided path can also emit.
## Validation
- `cargo test -p wifi-densepose-calibration` — fingerprint determinism from
fixed synthetic CSI; empty/occupied separation on synthetic occupancy;
certificate signing/verification round-trip and tamper rejection;
`distance()` monotonicity on progressively perturbed fixtures; invalidation
transitions (channel change, geometry change, age, drift-envelope breach)
each emit the expected witness record; constructor rejects synthetic→measured
mislabeling.
- Cross-ADR: an ADR-302 test consumes a certificate and asserts the gating
state transitions on a drifted fingerprint.
- Real-silicon characterization (ESP32 capture over a real 10-minute window)
remains a follow-up requiring hardware evidence per CLAUDE.md; a successful
build or synthetic run is not hardware evidence.

View File

@@ -0,0 +1,135 @@
# ADR-302: Out-of-distribution detection — KNOWN / DEGRADED / UNKNOWN gating
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: ood, calibration, uncertainty, quality, evidence, honesty, substrate
## Context
This ADR is primitive 2 of the perception-substrate program (ADR-300) and part
of the phase-1 certificate spine. It sits directly downstream of automatic
domain calibration (ADR-301): the `CalibrationCertificate` and its
`RoomFingerprint` are the reference distribution this ADR measures against. It
reuses fusion-layer quality scoring (ADR-137) as one of its inputs and feeds
its state into the evidence engine (ADR-304) and capability certificates
(ADR-318).
The central unsolved problem of WiFi sensing is cross-domain generalization: a
model trained (or calibrated) in one room degrades unpredictably in another, or
in the same room after furniture moves, the AP changes channel, or the radio
hardware is swapped. A model that keeps returning confident classifications
under these conditions is the single most misleading failure mode in the field,
and it is the failure the strategic assessment (ADR-300) named explicitly.
Confidence alone is insufficient: a softmax head is perfectly capable of being
confidently wrong on out-of-distribution input. RuView must be able to say
"I do not recognize this situation" instead of guessing.
Today RuView has partial signals but no unified gate:
- ADR-301 produces a comparable `RoomFingerprint` and a `distance()` metric.
- ADR-137 `QualityScore` carries fusion coherence, evidence references, and
contradiction flags per fused frame.
- Model heads emit confidence/uncertainty, but nothing combines domain
distance, signal quality, calibration compatibility, and uncertainty into a
single decision, and nothing forces a model to stop emitting confident labels
when it leaves its calibrated domain.
## Options considered
1. **Threshold on model confidence alone.** Rejected: confidently-wrong OOD
predictions are exactly the failure mode; confidence is necessary but not
sufficient.
2. **A per-model bespoke OOD check inside each task head.** Rejected:
duplicates logic, cannot be audited uniformly, and does not compose with the
calibration certificate or the evidence engine.
3. **A shared OOD gate that every inference passes through, fusing four signals
against the ADR-301 certificate.** Chosen.
## Decision
Add an out-of-distribution gate — implemented in a shared crate consumed by the
task-head runtime (`wifi-densepose-calibration::runtime` and the model serving
path) — that attaches a `DomainState` to **every** inference.
### 1. Four inputs, one decision
Each inference carries four measured quantities:
1. **Domain distance** — fingerprint distance (ADR-301 `distance()`) between
live traffic and the active `CalibrationCertificate`, split into the
empty-baseline and occupied-baseline components so geometry drift and
occupancy-statistics drift are distinguishable.
2. **Signal quality** — reuse the ADR-137 quality scoring signals (fusion
coherence, contradiction flags) plus per-frame SNR/validity.
3. **Calibration compatibility** — is a valid, non-invalidated certificate
present for this space (ADR-306) and this signed device (ADR-305)? An
expired, invalidated, or device-mismatched certificate is itself a
compatibility failure.
4. **Uncertainty** — the model head's own predictive uncertainty.
### 2. State machine: KNOWN → DEGRADED → UNKNOWN
- **KNOWN** — domain distance within the certificate's compatibility envelope,
quality above threshold, certificate valid and compatible, uncertainty low.
Confident classifications are returned.
- **DEGRADED** — one or more signals crossed a soft threshold (e.g. moderate
fingerprint drift within the envelope, elevated uncertainty, a tolerated
ADR-137 contradiction flag). Classifications are returned but flagged
degraded with the specific reason; downstream consumers must treat them as
lower-evidence.
- **UNKNOWN** — the room changed materially (empty-baseline drift beyond the
envelope, AP channel change, transceiver-geometry change, hardware/device
change, or an invalidated/absent certificate). RuView **stops returning
confident classifications** and returns UNKNOWN with the triggering cause.
This is the required behavior, not an error.
State transitions are hysteretic (separate enter/exit thresholds) so the gate
does not flap on noise. The state, the four input values, and the triggering
cause are all reported — never a bare label.
### 3. Certificate-bound, honest by construction
- The gate is meaningless without a certificate: with no valid ADR-301
certificate for the current space/device, the default state is UNKNOWN, not
KNOWN. Absence of evidence is treated as absence of capability.
- The `DomainState` and its inputs are emitted to the evidence engine
(ADR-304) as part of every inference record, and are an input to the ADR-318
capability certificate (a model's capability is bounded by the domain it can
hold KNOWN in).
- No accuracy number is claimed here; the ADR delivers the gating machinery.
The gate's own thresholds are calibration parameters, reported with each
decision.
## Consequences
- RuView gains a uniform, auditable answer to "should I trust this inference?"
that combines domain, quality, calibration, and uncertainty rather than
confidence alone.
- Deployments will see more DEGRADED/UNKNOWN results than a
confidence-only system, especially right after a room changes. That increase
is the product working: it is the difference between honest RF perception and
confidently-wrong output.
- Every task head that opts into the substrate must route through the gate;
heads that bypass it cannot claim a KNOWN state or earn an ADR-318
certificate.
- The gate couples model serving to the presence of a live calibration
certificate, making ADR-301 a hard dependency of confident inference — the
intended coupling.
## Validation
- `cargo test` on the OOD crate — state-machine transitions on synthetic
fixtures: in-envelope drift stays KNOWN; soft-threshold breach → DEGRADED;
empty-baseline drift beyond envelope, channel change, geometry change,
device mismatch, and invalidated/absent certificate each → UNKNOWN;
hysteresis prevents flapping under injected noise; missing certificate
defaults to UNKNOWN.
- Cross-ADR: consumes an ADR-301 certificate and asserts a drifted fingerprint
drives the expected transition; asserts the `DomainState` is present on every
emitted inference record consumed by ADR-304.
- No confident classification is emitted in the UNKNOWN state in any test —
enforced as an assertion, not a convention.
- Real-silicon OOD behavior (moving furniture / changing AP channel on a live
ESP32 capture and observing the transition) remains a follow-up requiring
hardware evidence per CLAUDE.md.

View File

@@ -0,0 +1,125 @@
# ADR-303: Ground-truth synchronization — reference sensors as a formal validation plane
- **Status**: Accepted — initial implementation (ADR-300 phase 2)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: ground-truth, validation, fusion, evidence, benchmark, honesty, substrate
## Context
This ADR is primitive 3 of the perception-substrate program (ADR-300), authored
as **Proposed** in phase 2: it is design intent and a validation plan, not
implemented by the phase-1 swarm. It sits on top of the phase-1 certificate
spine and feeds the evidence engine (ADR-304) and the real benchmark service
(ADR-317). It generalizes the vitals ground-truth rig (ADR-293) from a single
measurand to a modality-agnostic plane.
RuView's evidence discipline (CLAUDE.md; ADR-282 ladder) requires MEASURED
accuracy claims to be backed by an independent reference. ADR-293 built exactly
this for vitals: reference-series ingest, time alignment (cross-correlation
lag + optional clock-drift fit), and agreement statistics (MAE/RMSE/bias/
BlandAltman/within-tolerance), with an `EvidenceGrade` that is only
constructible as `Measured` when a real reference, non-zero paired samples,
minimum coverage, and a reproducer are present. That machinery is measurand- and
device-shaped: it knows about heart rate and breathing rate.
The substrate needs the same discipline for *every* phenomenon RuView senses —
presence, count, localization, pose, posture, activity — and for reference
sources of many modalities (cameras, mmWave, pressure mats, wearables, pulse
oximeters, microphones, manual labels). The critical design decision is that
these reference sensors form a **validation plane**, not additional inference
inputs.
## Options considered
1. **Fuse reference sensors as extra inference inputs.** Rejected on principle:
folding cameras/mmWave into the estimator would make RuView's RF claims
unfalsifiable — the reference would be training the thing it is meant to
check, and a camera-fed result is no longer a camera-free RF result. It
would also violate the ADR-282 layering (RuView is probabilistic
exteroception, never ground truth) and the honesty rule against presenting
fused-with-camera output as WiFi sensing.
2. **One-off rigs per measurand (extend ADR-293 ad hoc each time).** Rejected:
duplicates alignment/agreement code per phenomenon and never yields a shared
validation surface for the benchmark.
3. **A first-class, modality-agnostic `GroundTruth` API that is strictly a
validation plane.** Chosen.
## Decision
Introduce a `GroundTruth` API — a modality-agnostic validation plane that
compares RF inference against independent observation and never feeds it.
### 1. Modality-agnostic reference ingest
- A `ReferenceObservation` generalizing ADR-293's `ReferenceSeries`: a
timestamped, typed observation of a `Phenomenon` (presence, count,
localization, pose keypoints, posture, activity, heart rate, breathing rate)
from a `ReferenceModality` (camera, mmWave, pressure, wearable, pulse
oximeter, microphone, manual label), with device/source metadata and the
measurement principle recorded.
- Untrusted reference files are validated at the boundary (row-numbered
rejections, non-monotonic timestamps are errors), reusing ADR-293's ingest
discipline. Camera/mmWave references arrive as exported label/keypoint
streams, not live model feeds.
### 2. Synchronization
- Generalize ADR-293's time alignment (bounded-lag normalized cross-correlation
+ optional linear clock-drift fit) to arbitrary measurands on a common
resampled grid, with no interpolation across gaps beyond a configurable
limit. Alignment parameters are always reported, never silently applied.
- Spatial synchronization where relevant: reference observations are expressed
in the ADR-306 spatial ontology so an RF localization/pose result and a
camera/mmWave observation are compared in one coordinate frame.
### 3. Agreement as validation, not fusion
- A modality-appropriate `AgreementReport` per phenomenon: continuous
measurands reuse ADR-293's MAE/RMSE/bias/BlandAltman/within-tolerance;
categorical/detection phenomena (presence, activity) report confusion-matrix
metrics; spatial phenomena report localization error percentiles and pose
PCK **with the mandatory mean-pose baseline and leakage-free split**
(CLAUDE.md; ADR-291).
- Session scope is mandatory metadata (subject count, motion state, LOS/NLOS/
through-wall, distance band) — a report without scope cannot be constructed,
as in ADR-293.
### 4. Evidence and isolation guarantees
- The plane is one-directional by type: the inference path has no read access
to `GroundTruth` at runtime. A build/test-time isolation check (and the type
boundary) prevents a reference observation from becoming an estimator input.
- Reports carry an `EvidenceLevel` (ADR-282) and an `EvidenceGrade`
constructible as `Measured` only with a real reference, paired samples,
coverage, and a reproducer (ADR-293 rule). Reports feed the ADR-304 evidence
engine and are the substrate ADR-317 scores against.
## Consequences
- Every phenomenon RuView senses gets the same MEASURED-vs-independent-observer
discipline vitals already has, in one shared surface.
- Keeping references strictly as validation preserves the falsifiability and
the camera-free identity of RF results; it costs the (tempting) accuracy a
camera-fused estimator would show, which is the correct trade.
- Reference capture is an operational burden (a camera/mmWave rig per validated
session); acceptable because it is a validation activity, not a runtime
requirement, and it is what turns CLAIMED into MEASURED.
- Because this is Proposed (phase 2), the API shape may be revised once the
phase-1 spine (ADR-301/299/301/303) lands and the benchmark (ADR-317)
exercises it.
## Validation
- Unit tests (planned): modality-agnostic ingest rejection cases; alignment
recovery of known synthetic offsets/drifts across measurands; agreement math
per phenomenon against hand-computed fixtures; pose PCK path requires a
mean-pose baseline and rejects leaky splits; evidence-grade constructibility;
the isolation check fails a build that wires a reference into the inference
path.
- Cross-ADR: an ADR-317 benchmark scenario consumes `GroundTruth` reports as
its scored reference; ADR-304 ingests the agreement reports as evidence
records.
- Real-session validation (RF capture synchronized with a real camera/mmWave/
pressure/wearable reference) is the phase-2 exit and requires hardware
evidence per CLAUDE.md; a synthetic run is not hardware evidence.

View File

@@ -0,0 +1,117 @@
# ADR-304: Evidence engine — MLflow for physical sensing
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: evidence, provenance, ledger, accuracy, drift, benchmark, honesty, substrate
## Context
This ADR is primitive 4 of the perception-substrate program (ADR-300) and a
central pillar of the phase-1 certificate spine. It consumes the domain state
from out-of-distribution detection (ADR-302) and the calibration age from the
calibration certificate (ADR-301), it is the store that capability certificates
(ADR-318) are minted from, and it is the accuracy source the real benchmark
service (ADR-317) reads. In phase 2 it ingests agreement reports from the
ground-truth plane (ADR-303).
The strategic assessment (ADR-300) judged this primitive **more commercially
important than another pose architecture**: what unblocks OEM and integrator
conversations is not a higher headline number but a defensible, auditable record
of how a model actually performs, per room, per device, per subject, over time.
MLflow made ML experiments trackable; physical sensing needs the equivalent for
deployed accuracy, drift, and evidence level — an append-only ledger, not a
dashboard that overwrites yesterday's number.
RuView already has the constituent evidence types; what is missing is the ledger
that unifies them per deployment context:
- RuField provenance/signature types (ADR-260/262/277/279) — the signed,
provenance-bearing record types to reuse rather than reinvent.
- The AetherArena witness-ledger pattern (ADR-149) — an append-only,
witness-anchored ledger of scored results, the structural template here.
- `frame::EvidenceLevel` L0L5 (ADR-282) — the mandatory evidence tag every
record carries.
- ADR-302 `DomainState`, ADR-137 `QualityScore`, ADR-301 certificate version
and age — the per-inference signals to accumulate.
## Options considered
1. **Log accuracy to flat files / metrics dashboards.** Rejected: mutable,
un-signed, un-scoped, and not comparable over time — the exact gap.
2. **Reuse a general experiment tracker (MLflow itself).** Rejected: it is
experiment-time, not deployment-time; it has no notion of room/device/
subject context, calibration age, evidence level, or signed provenance, and
it would add an external service dependency contrary to the substrate's
edge-first, dependency-light direction.
3. **A native append-only evidence ledger reusing RuField record types and the
AetherArena ledger pattern.** Chosen.
## Decision
Build an **evidence engine**: a per-`(room, device, subject)` append-only
accuracy ledger that every model automatically writes to.
### 1. The evidence record
- An `EvidenceRecord` keyed by context — space id (ADR-306), signed device id
(ADR-305), and subject id where consented and available — carrying: model
version; calibration certificate version and **age** (ADR-301); the ADR-302
`DomainState` (KNOWN/DEGRADED/UNKNOWN) and its four inputs; the ADR-137
quality signals; predictive uncertainty; and, when a reference is present
(ADR-303), the agreement result (accuracy, false-positive rate). Each record
carries exactly one `EvidenceLevel` (L0L5, ADR-282).
- Records are **append-only** and signed with RuField signature types
(ADR-260/262/277/279); the ledger is anchored in the witness chain (ADR-319),
following the AetherArena witness-ledger pattern (ADR-149). No record is ever
mutated in place — a correction is a new record.
### 2. Per-context accuracy accounting
- The engine maintains, per `(room, device, subject)` context: measured
accuracy (only where an ADR-303 reference backs it — otherwise the record is
CLAIMED/SYNTHETIC, never MEASURED), false-positive rate, drift trajectory
(fingerprint distance over time from ADR-301), the fraction of inferences in
each domain state, calibration age distribution, and model-version history.
- Aggregation is a pure function over the append-only log at a queried time —
the ledger is the source of truth; summaries are derived, never authoritative
(mirroring CLAUDE.md's "source over summaries" rule).
### 3. Honesty enforced in the record
- The engine cannot upgrade an evidence level; a level is set by the record's
provenance at write time (synthetic input → L0/`Synthetic`; no reference →
CLAIMED; reference + reproducer → MEASURED), reusing the ADR-282/ADR-291/
ADR-293 constructor discipline. A benchmark or certificate reading the ledger
gets the honest level, not an optimistic rollup.
- No benchmark numbers are invented by this ADR; it delivers the ledger and the
accounting. Empty contexts report "no evidence," which downstream (ADR-318)
must treat as no capability.
## Consequences
- RuView gains a single auditable answer to "how well does this model actually
work, here, on this device, for this subject, and how fresh is the
calibration?" — the artifact OEM/integrator diligence actually asks for.
- ADR-318 capability certificates become derivable (a certificate is a signed
attestation over a slice of the ledger) and ADR-317 gains a real accuracy
source per PR instead of self-reported numbers.
- The append-only, signed design has storage and key-management cost; bounded
by per-context retention policy and by reusing the existing RuField/witness
infrastructure rather than a new store.
- Some contexts will show sparse or unflattering evidence. Surfacing that is the
point; the engine must never paper over a thin context with a global average.
## Validation
- `cargo test` on the evidence-engine crate — append-only invariant (no
in-place mutation; corrections are new records); per-context aggregation math
against fixtures; evidence-level is set by provenance and cannot be upgraded;
signature round-trip and tamper rejection; witness anchoring; empty-context
queries return "no evidence" not a fabricated number.
- Cross-ADR: ingests ADR-302 `DomainState` and (phase 2) ADR-303 agreement
reports; an ADR-318 test mints a certificate from a ledger slice and an
ADR-317 test reads accuracy from the ledger.
- Real-deployment evidence (a populated ledger from live ESP32 captures with
ADR-303 references) is the maturity milestone and requires hardware evidence
per CLAUDE.md; a synthetic ledger is L0 by construction.

View File

@@ -0,0 +1,147 @@
# ADR-305: Authenticated sensor identity — RF chain of custody
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: security, identity, provenance, sensor-ingest, attestation, phase-1
## Context
This ADR is a child of **ADR-300** (perception substrate program) and owns
primitive #5, *authenticated sensor identity*. In the ADR-300 dependency DAG it
is a spine root that, together with **ADR-306** (canonical spatial ontology),
feeds **ADR-301** (calibration certificate) and **ADR-319** (witness chain).
RuView's inference outputs are only as trustworthy as the measurements that
produced them, yet today a measurement's origin is essentially assertional. The
UDP data plane accepts frames from any reachable host: **ADR-296** shipped step
one — a loopback-default bind (`--udp-bind`) and an optional source
IP/CIDR allowlist — and explicitly deferred to a follow-up ADR "per-device
provisioned keys, MAC/AEAD, device identifiers, monotonic sequence numbers,
freshness window, and replay rejection." **This ADR is that step two.** ADR-296
correctly documented that an IP allowlist does not stop LAN spoofing; a
cryptographic device identity is what closes that gap.
Foundations already exist in the tree and must be reused rather than rebuilt:
- `wifi-densepose-rufield` provides `DeviceId`, `Signature`, `SignatureBlock`,
`FrameProvenance`, `ProvenanceClass`, and `SignatureVerifyError` — the type
vocabulary for a signed frame.
- `wifi-densepose-bfld` provides `CapabilityAttestation` and
`PrivacyAttestationProof` (BFLD attestation, ADR-141) — the device-side
attestation surface.
- **ADR-295** defines the source-provenance state machine and freshness
(`SpatialStateFreshness`); a monotonic sequence and freshness window slot
into that machine rather than duplicating it.
The gap is not new primitives but an **end-to-end chain of custody**: a frame
must be traceable as `device → signed measurement → sequence → timestamp →
calibration → inference → signed event`, with every link verified at the
ingest boundary per CLAUDE.md ("validate untrusted input at every network,
hardware, and FFI boundary; default to least authority").
## Options considered
1. **Stop at ADR-296 (bind + IP allowlist).** Rejected: ADR-296 itself names
this insufficient on a trusted LAN; any on-subnet host can still spoof a
device.
2. **TLS/DTLS transport authentication only.** Rejected: authenticates the
*channel*, not the *measurement*. It does not survive store-and-forward,
does not bind a sequence number into the signed object, and gives the
downstream evidence/witness layers nothing to re-verify offline.
3. **Per-device signing keys with a signed measurement envelope, monotonic
sequence, and freshness window, reusing the RuField/BFLD types.** Chosen.
## Decision
Introduce an **authenticated frame envelope** carried through the sensing
server, built from existing RuField/BFLD types.
### 1. Per-device provisioned identity
- Each radio (ESP32-S3/C6 node or adapter) is provisioned with a keypair; the
device holds the private key, the server holds the enrolled public key bound
to a `DeviceId`. Provisioning is an explicit, authorized enrollment step — a
device is untrusted until an operator enrolls its public key. Private keys are
never logged or committed (CLAUDE.md credential rule); the ESP32 side follows
`firmware/esp32-csi-node` key-handling notes.
- The enrollment record binds `DeviceId → public key → capabilities`
(via `CapabilityAttestation`, ADR-141), so a device can only assert
measurements for phenomena it is attested to sense. This is what **ADR-318**
(capability certificate) later consumes.
### 2. Signed measurement envelope
- A frame on the wire becomes a `SignatureBlock` over the canonical
serialization of `{DeviceId, sequence, timestamp, measurement-hash}`. The
measurement itself (CSI/CIR payload) is covered by the hash so tampering is
detectable without embedding the whole payload twice.
- Verification uses `Signature`/`SignatureVerifyError` from
`wifi-densepose-rufield`. A frame that fails signature verification is
dropped and counted, exactly as ADR-296 drops disallowed sources — an `Err`
at the boundary, never a warning that proceeds.
### 3. Monotonic sequence + freshness (replay defense)
- Each device maintains a strictly monotonic per-device sequence number. The
server tracks the last accepted sequence per `DeviceId`; a non-increasing
sequence is rejected as a replay.
- A freshness window bounds `timestamp` against the server clock skew budget;
stale frames are rejected. This reuses ADR-295's `SpatialStateFreshness`
rather than inventing a parallel notion of staleness, and composes with
ADR-297's stale-node handling.
### 4. Chain of custody into the event
- On successful verification the frame's `FrameProvenance` records the verified
`DeviceId`, sequence, and timestamp. Calibration (ADR-301) and inference
annotate their transforms, and the emitted spatial event (ADR-306 ontology)
carries a signed provenance lineage. `ProvenanceClass` still enforces the
synthetic/measured invariant from ADR-282/ADR-279 (invariant 6): a measured
chain of custody can never be aliased to synthetic and vice-versa.
- This end-to-end signed lineage is the substrate the **ADR-319** witness chain
serializes and the **ADR-318** capability certificate points at as evidence.
### Compatibility
- The envelope is **opt-in per deployment** and negotiated at enrollment. An
un-enrolled single-node desktop deployment keeps working unauthenticated
behind ADR-296's loopback default; a routable, multi-node, or fleet
deployment (ADR-316) requires enrolled identities. The startup security log
(ADR-296) is extended to state whether frame authentication is active.
## Consequences
- LAN spoofing and replay — the residual risks ADR-296 named plainly — are
closed for enrolled deployments. The measurement, not merely the channel, is
authenticated, so the guarantee survives store-and-forward into the witness
chain.
- Enrollment/key-management is now an operational responsibility (provisioning,
rotation, revocation). This is documented as a deployment step; key rotation
and revocation lists are specified here but their fleet distribution is
owned by ADR-316.
- Signature verification adds per-frame CPU cost at ingest; bounded and
measured in validation below. It is a deliberate cost for a verifiable chain
of custody.
- A schema addition to the frame contract; un-enrolled deployments are
unaffected, and the migration accessor mirrors ADR-297's approach.
- **No spoof-resistance claim is MEASURED until validated on real silicon**
(CLAUDE.md hardware rule): a passing unit/integration suite demonstrates the
logic, not the fielded device path.
## Validation
- Unit tests (`cargo test -p wifi-densepose-sensing-server`,
`-p wifi-densepose-rufield`): valid envelope accepted; bad signature
rejected and counted; non-monotonic sequence rejected as replay; out-of-
window timestamp rejected; un-enrolled `DeviceId` rejected; measured/synthetic
provenance aliasing rejected (ADR-279 invariant 6).
- Integration test: a captured/synthesized multi-frame stream produces a
verifiable `device → … → signed event` lineage that ADR-319 can serialize and
re-verify offline.
- Benchmark (`cargo bench`): per-frame verification cost, to bound ingest
overhead.
- **Real-silicon evidence required** before any deployment-grade
authentication claim: a captured boot/runtime log from an enrolled ESP32 node
signing frames end-to-end. A successful build or simulator run is not
hardware evidence.

View File

@@ -0,0 +1,142 @@
# ADR-306: Canonical spatial ontology — one Site→…→Event model for every surface
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: ontology, worldgraph, schema, mqtt, matter, rufield, phase-1
## Context
This ADR is a child of **ADR-300** and owns primitive #6, *canonical spatial
ontology*. In the ADR-300 DAG it is a spine root alongside **ADR-305**
(authenticated identity) and feeds every downstream primitive that must speak
about *where* and *what*: **ADR-301** (calibration), **ADR-307** (tracking,
consumes `Track`/`Person`), **ADR-319** (witness chain), and every external
surface named in the ADR-300 consequences (MQTT, REST, WebSocket, RuField,
Matter, agents).
RuView currently expresses "where something is" in several overlapping,
per-surface schemas: the MQTT/Home-Assistant mapper has its own node/room
shapes (**ADR-297** just introduced `NodeInference`/`RoomInference` to
disambiguate node vs. room state); the `worldgraph` crate models a spatial
graph; RuField carries `SemanticProvenance`; Matter/HomeKit has its own area
model. The same physical fact — "a person is in the kitchen" — is re-encoded
differently on each surface, and the review called for "one canonical
`NodeInference`/`RoomInference` contract" (ADR-297 consequences). Without a
single semantic model, every new surface multiplies the translation matrix and
each translation is a place where provenance and evidence level (ADR-282) can
be silently dropped.
Substantial scaffolding already exists and must be **reused/extended, not
rebuilt**. `v2/crates/worldgraph/wifi-densepose-worldgraph` already defines:
- `WorldNode` variants including `Room { area_id, name, bounds_enu, floor }`,
`Zone { parent_room, … }`, `Wall { rf_attenuation_db }`, and `Doorway`.
- `WorldEdge` variants including `Observes { quality, last_seen_unix_ms }`,
`LocatedIn { since_unix_ms }`, `AdjacentTo { via_doorway }`, and `Supports`.
- `WorldGraph`, `WorldGraphSnapshot`, `WorldId`, `SemanticProvenance`,
`PersonPosition`, and a HomeCore `area_id` linkage join key (ADR-127).
The `worldgraph` crate is therefore the natural home for the canonical model.
What is missing is (a) the full `Site → Building → Floor → Space → Zone`
containment spine above `Room`, (b) first-class `Sensor`, `Object`,
`Observation`, `Track`, and `Event` node types, (c) one canonical serialization
that every surface consumes, and (d) a documented migration path from the
existing per-surface schemas.
## Options considered
1. **Leave each surface with its own schema; add adapters pairwise.** Rejected:
O(surfaces²) translations, and provenance/evidence loss at each hop.
2. **Invent a new top-level ontology crate.** Rejected: `worldgraph` already
models rooms, zones, walls, doorways, observation edges, and HomeCore
linkage; a parallel crate would fork the world model.
3. **Extend `worldgraph` into the canonical ontology and make every surface a
projection of it.** Chosen.
## Decision
Adopt **one canonical spatial ontology**, hosted in the `worldgraph` crate,
that every RuView surface reads from and writes to.
### 1. The containment spine and entity types
Define the full node taxonomy as an extension of the existing `WorldNode`:
```
Site ▸ Building ▸ Floor ▸ Space ▸ Zone
└─▸ { Sensor, Person, Object,
Observation, Track, Event }
```
- `Site`, `Building`, `Floor`, `Space` are new containment `WorldNode`
variants above the existing `Room` (mapped to `Space`, keeping its `area_id`
and `bounds_enu`) and `Zone`. `Wall`/`Doorway` remain as topological
elements. Containment reuses the existing `LocatedIn`/`AdjacentTo` edge
vocabulary; a new `PartOf` edge expresses the pure hierarchy
(Zone `PartOf` Space `PartOf` Floor …).
- `Sensor` is the entity **ADR-305** authenticates (`DeviceId` as its stable
identity) and **ADR-320** (HAL, phase 2) describes the hardware of. `Person`,
`Object`, `Observation`, `Track`, and `Event` are first-class nodes.
`Observes`/`LocatedIn` edges already carry quality and dwell timestamps.
- `Track` and `Person` are defined **here** as the ontology contract that
**ADR-307** (persistent tracking) produces and updates. `Observation` is what
an authenticated frame (ADR-305) becomes after calibration (ADR-301), and
`Event` is the governed output that ADR-318 certifies and ADR-319 witnesses.
### 2. Canonical serialization
- A single, versioned serialization (serde-based, stable field names) is the
one wire/at-rest representation. Every surface — MQTT/Home-Assistant, REST,
WebSocket, RuField observations, Matter/HomeKit, agent queries — is a
**projection** of this model, not an independent schema. `NodeInference` and
`RoomInference` (ADR-297) become projections of `Sensor→Observes` and the
`Space`-level fused inference respectively, so ADR-297's node/room separation
is preserved by construction rather than re-encoded per surface.
- Every node and edge carries `SemanticProvenance` and exactly one
`EvidenceLevel` (L0L5, ADR-282 policy): the evidence ladder travels *with*
the fact across every projection, so no surface can silently upgrade or drop
it.
### 3. Migration path
- Each existing per-surface schema gets a documented, tested bidirectional
mapping to/from the canonical model, plus a migration accessor for consumers
reading the old shape (mirroring ADR-297's migration accessor). Surfaces are
cut over one at a time; a surface is "canonical" once its projection is the
only encoder it uses. Until cutover, the mapping layer is authoritative and
round-trip-tested so no fact is lost in translation.
- The `worldgraph` HomeCore `area_id` linkage (ADR-127) remains the join key
between the ontology's `Space` and external area registries.
## Consequences
- The translation matrix collapses from O(surfaces²) to O(surfaces): each
surface implements one projection. New surfaces (ROS 2, OpenUSD, OPC UA per
ADR-282's roadmap) plug in as additional projections.
- Provenance and evidence level are carried uniformly; a fact cannot cross a
surface boundary and lose its lineage or its L-level.
- A schema change reaching every surface; managed by the versioned
serialization and per-surface migration accessors. Single-node deployments
keep working (one `Sensor`, one `Space`).
- The ontology is a *representation*, not an inference engine: it says nothing
about *how* a `Track` or `Event` is produced — that is owned by ADR-307,
ADR-301, ADR-302, and the model layer. This ADR does not itself make any
accuracy claim to grade.
- Extending `worldgraph` grows one crate's surface rather than forking a second
world model; the geo/worldmodel sub-crates continue to build on the same node
vocabulary.
## Validation
- Unit tests (`cargo test -p wifi-densepose-worldgraph`): containment-spine
construction and invariants (a `Zone` is `PartOf` exactly one `Space`, a
`Space` on exactly one `Floor`, etc.); round-trip serialization of every node
and edge type; every node/edge carries exactly one `EvidenceLevel`.
- Migration tests: each per-surface schema maps to the canonical model and back
with no loss of provenance or evidence level; `NodeInference`/`RoomInference`
(ADR-297) project and re-project identically.
- Contract test: a single canonical `Event` renders correctly through the MQTT,
REST, and WebSocket projections from one source of truth.
- No accuracy numbers are claimed; this ADR delivers the shared representation
the rest of the phase-1 spine writes into.

View File

@@ -0,0 +1,135 @@
# ADR-307: Persistent identity & tracking — privacy-preserving probabilistic tracks
- **Status**: Accepted — initial implementation (ADR-300 phase 2)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: tracking, identity, privacy, fusion, worldgraph, phase-2
## Context
This ADR is a child of **ADR-300** and owns primitive #7, *persistent identity
& tracking*. In the ADR-300 DAG it is a phase-2 primitive sitting on the
phase-1 spine: it **consumes the ADR-306 ontology** (producing and updating the
`Track` and `Person` node types defined there), it relies on **ADR-305**
authenticated identity so that the observations it associates have a verified
origin, and its outputs are governed `Event`s that ADR-318/ADR-319 can certify
and witness.
The product need is to reason about *persistent entities* — "person_7 entered
the kitchen, then the hallway, then the bedroom" — across radios, modalities,
rooms, and time. The hard constraint is that this must happen **without
establishing civil identity**. RuView is camera-free (ADR-282), and a
persistent pseudonymous track must never become, or be joinable to, a real-
world named individual. This is a privacy property to be enforced *by
construction*, not a policy footnote.
Substantial scaffolding already exists in
`v2/crates/wifi-densepose-mat/src/tracking` and must be **reused/extended, not
rebuilt**:
- `SurvivorTracker`, `TrackedSurvivor`, `TrackId`, `TrackerConfig`,
`TrackLifecycle`, and `TrackState` — a multi-target tracker with lifecycle
(tentative/active/lost/terminal) and a `TrackId` backed by a UUID
(`as_uuid`).
- `KalmanState` with `predict`/`update`, `position`, `velocity`,
`position_uncertainty`, and `mahalanobis_distance_sq` — the motion model and
gating distance.
- `CsiFingerprint`, `DetectionObservation`, `AssociationResult`, and the
`can_reidentify`/`matches`/`mark_rescued`/`rescue` re-identification surface —
the appearance/fingerprint channel for track continuity.
What is missing is (a) continuity **across radios, modalities, and rooms** (the
tracker today reasons within a node/room context), (b) a **persistent** entity
that survives track loss and hand-off between spaces, and (c) an explicit
**privacy boundary** that guarantees no civil-identity binding.
## Options considered
1. **Per-room independent trackers, no cross-room identity.** Rejected: cannot
express "person_7 moved kitchen → hallway → bedroom"; loses the entity at
every room boundary.
2. **Global identity keyed on a strong biometric fingerprint.** Rejected: a
fingerprint strong enough to re-identify across long gaps trends toward a
civil-identity-grade biometric — exactly what the privacy constraint
forbids.
3. **Probabilistic persistent tracks with bounded, decaying pseudonymous
association, built on the existing MAT tracker.** Chosen.
## Decision
Extend `wifi-densepose-mat/tracking` into a **cross-domain persistent track
layer** that produces ADR-306 `Track`/`Person` nodes.
### 1. Persistent probabilistic entity
- A persistent entity is a pseudonymous `Person` node (ADR-306) with a stable
synthetic id (e.g. `person_7`) backed by the existing `TrackId`/UUID. It
aggregates one or more `SurvivorTracker` tracks over time and space and holds
a **probabilistic** continuity belief — association is never asserted as
certain, and every hand-off carries a confidence.
- Continuity across a track-loss gap reuses the existing re-identification
surface (`can_reidentify`, `CsiFingerprint`, `AssociationResult`), extended
with a **time- and distance-decayed** association prior so that confidence in
"same entity" falls with the size of the gap. Beyond a bounded horizon the
association is dropped and a new pseudonym is minted rather than forcing a
join — under-linking is the privacy-safe failure mode.
### 2. Cross-radio / cross-modality / cross-room continuity
- Association operates over the ADR-306 ontology graph: `Observes` edges from
multiple `Sensor`s and `AdjacentTo`/`Doorway` topology constrain plausible
hand-offs (a person can only move between adjacent spaces). The existing
`mahalanobis_distance_sq` gating extends to a fused observation across
modalities rather than a single node's detections.
- Fusion here is track-level association; the underlying multi-modality fusion
(radar/mmWave per ADR-063, multistatic per ADR-029, and real sensor fusion
per ADR-311) supplies the observations. This ADR depends on those for the raw
cross-modality evidence and does not re-implement sensor fusion.
### 3. Privacy boundary (by construction)
- **No civil-identity binding.** The persistent id is a synthetic pseudonym
with no field, edge, or join key to any name, account, phone, MAC, or other
civil identifier. The type carries no such field, so binding is impossible in
the schema, not merely discouraged.
- The `CsiFingerprint` used for re-identification is **bounded and decaying**:
it is scoped to short-horizon continuity, is not persisted as a long-term
biometric template, and expires. This keeps re-identification useful for
"same person across the hallway" while structurally unable to serve "this is
the same person who visited last month."
- Every `Track`/`Person`/`Event` produced carries `SemanticProvenance` and an
`EvidenceLevel` (ADR-282), and honors the ADR-277/ADR-280 edge governance and
ADR-141 attestation — a pseudonymous track is still governed P-class data.
Tracking accuracy is a per-domain claim to be tagged MEASURED/CLAIMED/
SYNTHETIC with a reproducer; **this ADR claims no accuracy number.**
## Consequences
- RuView can express persistent, cross-room trajectories for automation and
analytics while remaining camera-free and civil-identity-free.
- The privacy-safe failure mode is **under-linking** (mint a fresh pseudonym
when unsure), which will fragment a trajectory across long gaps or sparse
coverage. This is a deliberate trade: a fragmented pseudonym is safe, a
wrong civil-identity join is not.
- Extends an existing tracker rather than forking one; single-room single-radio
deployments keep the current behavior (one entity = one track).
- Cross-modality quality depends on ADR-311/ADR-063/ADR-029 landing; until then
continuity is WiFi-primary and its limits are stated, not hidden.
- Being phase 2, this ADR is design intent; it will be revised as the ADR-306
ontology and ADR-305 identity spine finalize.
## Validation
- Unit tests (`cargo test -p wifi-densepose-mat`): decayed association prior
(confidence falls with gap; drops beyond horizon → new pseudonym);
topology-constrained hand-off (no association across non-adjacent spaces);
schema check that a `Person`/`Track` carries no civil-identifier field.
- Integration test against a synthetic multi-room, multi-radio scenario:
a scripted walk kitchen → hallway → bedroom yields one persistent pseudonym
with per-hand-off confidence, and a deliberately ambiguous crossing produces
two pseudonyms rather than a false join.
- Evidence discipline: any tracking-continuity accuracy is reported only with
the ADR-291 leakage-free protocol and an evidence tag; no number is asserted
here.
- Privacy review: confirm no persisted long-term biometric template and no
civil-identity join path, as an explicit checklist item before any pilot.

View File

@@ -0,0 +1,138 @@
# ADR-308: Sensor placement optimizer — floorplan + inventory → recommended positions
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: placement, planning, rf-twin, coverage, worldgraph, phase-3
## Context
This ADR is a child of **ADR-300** and owns primitive #8, *sensor placement
optimizer*. In the ADR-300 DAG it is a phase-3, research-forward primitive that
sits on top of the fused world state and is tightly coupled to **ADR-315**
(digital RF twin): the twin provides the propagation simulation this optimizer
plans against. It reads the **ADR-306** canonical ontology for the physical
scene and, after install, compares its predictions against ADR-302 observability
and the ADR-318 capability certificate.
The problem it solves is the single most common cause of a bad RuView
deployment: sensors placed by guesswork. Whether a room can be reliably sensed
depends on AP/sensor geometry relative to walls, Fresnel-zone clearance,
multipath structure, and where people actually move. Today an installer has no
principled way to answer "where do I put the two nodes I have so the kitchen is
observable?" — and no way, after install, to know whether reality matched the
plan. This is a genuine **differentiator**: it turns RuView from "sense
whatever the given placement happens to allow" into "recommend the placement
that makes the requested sensing feasible."
Relevant existing assets to build on rather than duplicate:
- The `worldgraph` crate models the physical scene the optimizer plans over:
`Room`/`Space` with `bounds_enu`, `Wall { rf_attenuation_db }` (drywall ≈ 3
dB, brick ≈ 12 dB), `Doorway`, and `Zone` — enough geometry and coarse RF
attenuation to seed a coverage model, plus `Sensor` nodes (ADR-306) for
candidate positions.
- **ADR-315** (RF twin, phase 3) is the propagation/multipath simulator; this
optimizer is a *consumer* of the twin, not a second simulator.
- **ADR-302** (OOD/observability) and **ADR-318** (capability certificate)
define what "reliably sense the requested phenomenon" means, so the optimizer
can optimize against the same observability metric the runtime later gates on.
- **ADR-029** (multistatic) and **ADR-063** (mmWave fusion) inform which link
geometries are useful for which phenomena.
## Options considered
1. **Static placement guidelines in docs (e.g. "one node per room, opposite
the door").** Rejected: ignores the specific floorplan, wall materials, and
the actual hardware inventory; gives no uncertainty and no post-install
feedback.
2. **Full electromagnetic solver per site.** Rejected for the default path:
too heavy for an installer workflow and overkill relative to the coarse
`rf_attenuation_db` scene RuView actually has; reserved as an optional
high-fidelity backend inside ADR-315.
3. **A coverage optimizer that consumes the ADR-315 RF twin over the ADR-306
scene, then validates predicted vs. measured observability after install.**
Chosen.
## Decision
Define a **placement optimizer** that takes a floor plan (ADR-306 scene) and a
hardware inventory and recommends sensor positions, then closes the loop after
install.
### 1. Inputs
- The ADR-306 canonical scene: `Space`/`Zone` bounds, `Wall` segments with
`rf_attenuation_db`, `Doorway` topology, and any already-placed `Sensor`
nodes.
- A hardware inventory: the count and type of available radios (ESP32-S3/C6
nodes, mmWave, adapters) with their capability envelopes (what each can
sense, per ADR-318 / ADR-320 HAL descriptors).
- A sensing objective: which phenomenon must be observable in which
`Space`/`Zone` (presence, vitals, pose), expressed against the ADR-302
observability metric.
### 2. Prediction
- For a candidate placement, query the **ADR-315 RF twin** for simulated RF
coverage: path loss through `Wall` attenuation, **Fresnel-zone clearance**
between link endpoints, and coarse **multipath** structure. From that derive
an **expected observability** and an **uncertainty** for each objective in
each space — reusing the same observability definition ADR-302 gates on so the
plan and the runtime speak one language.
- Search over candidate positions (the inventory bounds the count; the scene
bounds the geometry) to recommend the placement that maximizes objective
observability, reporting expected observability **and its uncertainty** per
space — never a single confident number for a simulated result.
### 3. Post-install loop
- After install, compare **predicted vs. measured** observability using the
ADR-302 runtime observability signal from the freshly enrolled (ADR-305),
calibrated (ADR-301) sensors. Where measurement disagrees with prediction,
recommend adjustments (move, re-aim, add a node) and feed the residual back
to improve the ADR-315 twin's scene parameters (e.g. a wall's effective
attenuation).
### Evidence discipline
- Predicted coverage is a **simulation** (evidence level L0 per ADR-282) and is
labelled `SYNTHETIC`; it is a *recommendation*, never a sensing claim.
- The predicted-vs-measured comparison is the only place a `MEASURED` statement
appears, and only with a reproducer and real-silicon observability data
(CLAUDE.md hardware rule). The optimizer never presents a simulated coverage
map as evidence that a room *is* being sensed.
## Consequences
- Installers get a principled, floorplan-specific placement plan and, crucially,
a post-install check that says whether reality matched the plan — a
differentiating capability over guess-and-check deployment.
- Quality is bounded by the fidelity of the ADR-315 RF twin and the coarseness
of the `worldgraph` scene (2D walls, coarse attenuation). The optimizer
reports uncertainty rather than overstating a coarse model; higher fidelity
is an ADR-315 concern.
- Hard dependency on ADR-315 (twin), ADR-302 (observability metric), and
ADR-306 (scene); this ADR does not build a simulator or an observability
metric of its own.
- Being phase 3, this is design intent sitting on the fused world state; it is
expected to be revised as ADR-315 and the phase-1 spine land.
- No claim that recommended placement *guarantees* sensing — it maximizes
modelled observability subject to inventory and geometry, with explicit
uncertainty.
## Validation
- Unit tests: coverage/observability prediction is a deterministic function of
scene + placement + twin parameters; Fresnel-zone and wall-attenuation math
against known analytic cases; search returns the modelled-optimal placement on
small synthetic scenes.
- Integration test: on a synthetic floorplan with a known-good and a
known-bad placement, the optimizer ranks them correctly and reports higher
uncertainty for the marginal case.
- Post-install loop test: injected predicted-vs-measured disagreement produces a
sensible adjustment recommendation and a twin-parameter residual.
- Field validation (deferred, real-silicon): predicted vs. measured
observability on an instrumented real site, reported as `MEASURED` with a
reproducer. Until then all coverage output is `SYNTHETIC`/L0. No coverage or
accuracy number is asserted by this ADR.

View File

@@ -0,0 +1,152 @@
# ADR-309: Active sensing — closed-loop RF experiment control
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: active-sensing, control-plane, closed-loop, information-gain, actuation, phase-3
## Context
This ADR is a child of **ADR-300** and owns primitive #9, *active sensing*. In
the ADR-300 phasing it is a phase-3 primitive that sits on top of the fused
world state produced by **ADR-311** (real sensor fusion) and is driven by the
information budget of **ADR-314** (information-gain scheduler). It is authored
as **Proposed**: design intent and validation plan, not a phase-1 build.
The default posture of every current RuView path is **passive**: RF traffic
happens for its own reasons (a device transmits, a beacon fires), RuView
observes whatever CSI/CIR arrives, and the pipeline extracts what it can from
that incidental signal. The strategic assessment behind ADR-300 named the next
step: move from *RF-happens → observe* to **RuView-controls-RF → observe the
response → optimize the next measurement**. That turns sensing into a
closed-loop experiment — the system chooses what to measure to resolve the
uncertainty it currently has, rather than accepting the measurements the
environment happens to offer.
Substantial control-plane scaffolding already exists and must be
**reused/extended, not rebuilt**:
- **ADR-280** (active sensing / programmable perception, *implemented* in
`ruview-unified/src/control.rs`) already defines the governed control surface
this ADR closes the loop over: `SensingTask` (evidence-aware, fail-closed
admission), `SensingAction` + `InformationGoal` (a deliberate act of
evidence-gathering against a stated hypothesis, bounded by a `PrivacyClass`
P0P5 ceiling), `ActiveSensingPlanner` (age-of-information scheduler),
`CoherentSensorGroup` (coherent fusion fails closed), and `request_actuation`
`ActuationReceipt` for governed RIS/movable/fluid-antenna actuation.
- ADR-280 explicitly recorded that **information-gain *estimation* is not
implemented** — "the planner uses staleness heuristics, not mutual
information; RIS drivers, actual multi-AP coherence measurement, and OTFS
waveform control are hardware-dependent roadmap items." ADR-309 is the ADR
that closes exactly those gaps, in coordination with ADR-314.
The missing piece is not the actuation surface — ADR-280 built that and made it
fail closed — but the **loop**: a controller that reads the current fused-state
uncertainty, selects a *controllable measurement configuration* expected to
reduce it most, requests it through the ADR-280 governed surface, observes the
response, and updates its belief before choosing the next measurement.
## Options considered
1. **Stay passive; only schedule which incidental observations to keep.** This
is roughly today's `ActiveSensingPlanner` (staleness-priority over regions).
Rejected as the endpoint: it optimizes *attention* over uncontrolled RF, not
the *measurement* itself. It remains the fallback when nothing is
controllable.
2. **Open-loop measurement scripting** (a fixed sweep of channels/bandwidths).
Rejected: a fixed sweep spends the RF/energy/privacy budget the same way
regardless of what is already known; it cannot concentrate measurement where
uncertainty actually is.
3. **Closed-loop experiment control** — read uncertainty, pick the controllable
configuration with highest expected information gain per unit cost/privacy,
actuate through the ADR-280 governed surface, observe, update, repeat.
Chosen.
## Decision
Adopt **closed-loop RF experiment control** as a phase-3 controller layered on
the ADR-280 surface. RuView selects and drives the controllable degrees of
freedom of the RF measurement, then optimizes the next measurement from the
observed response.
### 1. Controllable degrees of freedom
Define an `ExperimentControl` vocabulary over the configuration axes RuView can
influence on hardware that exposes them (each axis is optional and
capability-gated by ADR-320's HAL, so an ESP32-only deployment simply has an
empty controllable set and degrades to the passive planner):
- **Channel / band** and **bandwidth** (which spectrum to probe; reuses the
ADR-292 wideband subcarrier-agnostic metadata).
- **Packet timing / cadence** (when to solicit a sounding, and at what rate).
- **Antenna / chain selection** (which subset of a distributed aperture to
activate — bounded by the ADR-280 `CoherentSensorGroup` compatibility proof).
- **Beam / RIS configuration** (which rooms and people become observable —
governed exactly as ADR-280 §6 requires, via `request_actuation` and an
`ActuationReceipt`).
- **802.11bf measurement parameters** (TB/non-TB, reporting config) once
ADR-310 exposes standardized sensing as a native measurement type.
### 2. The loop
```
fused-state uncertainty (ADR-311)
info-gain ranking of ExperimentControl options (ADR-314)
│ select argmax E[ΔI] / (cost, energy, privacy ceiling)
governed request (ADR-280 admit_task / request_actuation, fail-closed)
observe response → update belief (ADR-311) → repeat
```
The controller never bypasses the ADR-280 admission and actuation gates: every
solicited measurement is a `SensingTask`/`SensingAction`, every environment
change is an `ActuationReceipt`, and every step composes with the ADR-277
policy engine. Information gain is what **ADR-314** supplies (the mutual-
information estimate ADR-280 deferred); ADR-309 owns the *control loop* that
consumes that estimate and drives the hardware.
### 3. Governance and honesty boundary
- Actuation and solicitation stay fail-closed and privacy-ceilinged: a
closed-loop experiment cannot widen the P0P5 ceiling of the task it serves,
and cannot steer a beam into a zone that does not grant the purpose (ADR-280
`actuation_requires_policy_authorization`).
- Any accuracy or "traffic-reduction" claim from the closed loop is tagged
**MEASURED** only with a named reproducer over a stated scenario, **SYNTHETIC**
for simulated apertures, and **CLAIMED** otherwise. Real multi-AP coherent
measurement and RIS actuation remain **hardware-dependent** and require
real-silicon evidence (a captured runtime log) before any hardware claim, per
CLAUDE.md. No number is invented here.
## Consequences
- Sensing becomes an experiment: RuView spends its RF/energy/privacy budget on
the measurements that most reduce current uncertainty, instead of processing
whatever incidental traffic arrives.
- The loop is only as strong as its two dependencies: ADR-311 must expose a
usable uncertainty surface and ADR-314 must produce trustworthy information-
gain estimates. Where either is absent, the controller degrades to the
ADR-280 staleness planner rather than acting on a fabricated gain estimate.
- Controllability is hardware-bounded. On commodity ESP32 sensors the
controllable set may be limited to cadence; the full loop (bandwidth, antenna,
beam) needs NICs/RIS that expose those axes, surfaced through ADR-320.
- This ADR adds a controller; it does not re-open ADR-280's raw-export or
actuation-governance decisions, which remain authoritative and fail-closed.
## Validation
- Design-level acceptance (phase 3): a simulated closed loop over a synthetic
scene reduces terminal fused-state uncertainty faster than (a) the passive
ADR-280 staleness planner and (b) an open-loop fixed sweep, at equal
measurement budget — reported **SYNTHETIC**, with the scenario and seed named.
- Governance tests: every solicited measurement and actuation in the loop is
admitted through the ADR-280 fail-closed path; a loop step that would exceed
the task's privacy ceiling or steer into an ungranted zone is denied.
- Degradation test: with an empty controllable set (ESP32-only), the controller
falls back to the staleness planner with no error and no fabricated gain.
- Hardware validation of bandwidth/antenna/beam actuation is explicitly out of
scope until real silicon exposes those axes and produces a captured log.

View File

@@ -0,0 +1,147 @@
# ADR-310: 802.11bf-native architecture — standardized WLAN sensing as native measurement types
- **Status**: Proposed (ADR-300 phase 2)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: 80211bf, wlan-sensing, standards, measurement-types, hal, phase-2
## Context
This ADR is a child of **ADR-300** and owns primitive #10, *802.11bf-native
architecture*. In the ADR-300 phasing it is a phase-2 integration primitive: it
sits on the phase-1 spine (authenticated identity ADR-305, spatial ontology
ADR-306, evidence engine ADR-304) and **feeds ADR-320** (the RuView sensor HAL),
which is the clause of the acceptance test that "identifies the hardware." It is
authored as **Proposed**.
**IEEE 802.11bf-2025 ("WLAN Sensing") was published 2025-09-26** — verified
against the IEEE SA record in `wifi-densepose-hardware` (`ieee80211bf/mod.rs`
header, "evidence grade MEASURED", ADR-152 §1.1). Standardization is complete
for sub-7 GHz and >45 GHz (DMG) bands: formal sensing measurement setup,
measurement instances, feedback/reporting, and sensing-by-proxy (SBP). This
changes RuView's strategic frame: rather than treating every WiFi measurement as
an *opportunistic* extraction from incidental traffic, RuView can be the **open
reference sensing stack around the standard** — the day commodity silicon
exposes it.
Substantial scaffolding already exists and must be **reused/extended, not
rebuilt**. `v2/crates/wifi-densepose-hardware/src/ieee80211bf/` already models
the standardized procedure surface as forward-compatible types (ADR-152/153):
- `types``SpecProfile` version gates, `SensingRole`/`TransceiverRole`,
`MeasurementSetupParams`, `SensingCapabilities` negotiation, and required
`ConsentMode` governance metadata on every setup.
- `messages``SensingMeasurementSetupRequest/Response`,
`SensingMeasurementInstance`, `SensingMeasurementReport`, `CsiReportPayload`,
`SbpRequest/Response`, `SensingSessionTermination`.
- `session` — a deterministic FSM (`Idle → SetupNegotiating → Active →
Terminating → Idle`) with rejection paths, single-role enforcement, and SBP
proxy mode; `table` (responder-side setup registry); `transport` (the
`SensingTransport` seam, a `SimTransport` test double, and an
`OpportunisticCsiBridge` that maps today's opportunistic CSI onto the
standardized report path).
The module's own honesty note is authoritative and carried forward here: it is
**not a certified 802.11bf implementation**, and **no commodity silicon — ESP32
included — implements the standard yet**; the OTA frame binding lands when a
chipset exposes it. Wideband ingest plumbing is already in place too: **ADR-292**
(FeitCSI/AX210) carries native subcarrier dimensionality end-to-end and records
the native→pipeline mapping, and noted that "truncated CIR is a natural
extension of the same plumbing."
What is missing is architectural, not protocol scaffolding: normalized CSI is
still treated as *the* WiFi input. The standardized sensing measurements
(TB/non-TB soundings, truncated CIR / PDP reports) are modeled as protocol
messages but are **not yet first-class native measurement types** that flow
through calibration (ADR-301), fusion (ADR-311), and the ontology (ADR-306) on
equal footing with normalized CSI.
## Options considered
1. **Keep 802.11bf as a protocol model only; always down-convert its reports to
normalized CSI at ingest.** Rejected: truncated CIR/PDP carry range-resolved
multipath structure that flattening to a CSI matrix discards; it also wastes
the standard's native report semantics.
2. **Fork a parallel "bf pipeline" alongside the CSI pipeline.** Rejected:
duplicates calibration, fusion, ontology, and evidence plumbing, and re-opens
the O(surfaces²) translation problem ADR-306 exists to close.
3. **Promote standardized sensing measurements to native measurement types
inside the existing pipeline**, with normalized CSI as one measurement type
among several. Chosen.
## Decision
Adopt an **802.11bf-native architecture**: standardized WLAN sensing
measurements become **additional native measurement types**, alongside — not
replacing — normalized CSI.
### 1. Native measurement types
- Define the standardized reports the `ieee80211bf` module already models
(TB and non-TB soundings; truncated CIR; PDP) as first-class
`MeasurementType` variants that the pipeline carries end-to-end, each tagged
with its `SpecProfile` and band. Normalized CSI remains one such type; the
`OpportunisticCsiBridge` remains the path for silicon that only offers
incidental CSI.
- Truncated CIR/PDP reuse the **ADR-292** subcarrier-agnostic / native-
dimensionality plumbing (truncated CIR is the stated natural extension); the
native→pipeline mapping is recorded in frame metadata so downstream stages
know the true range/spectral resolution of a bf report vs. an interpolated CSI
frame.
### 2. Ontology and governance binding
- Each standardized measurement becomes an ADR-306 `Observation` node from an
ADR-305-authenticated `Sensor`, carrying `SemanticProvenance` and exactly one
`EvidenceLevel` (L0L5, ADR-282). The `ieee80211bf` `ConsentMode` metadata —
required on every setup — composes with the ADR-277 policy engine, so a
standardized session is admitted under the same governance as any other
sensing task (ADR-280).
- SBP (sensing-by-proxy) sessions attribute the report to the proxying and the
sensing entities distinctly, so provenance is not laundered through the proxy.
### 3. HAL feed (ADR-320)
- The capability set a device advertises — which `MeasurementType`s, bands,
bandwidths, roles, and `SpecProfile` it supports — is exactly the descriptor
**ADR-320** (HAL) needs to "identify the hardware." ADR-310 defines that
capability descriptor as the projection of `SensingCapabilities`; ADR-320
consumes it. A device that implements no bf profile advertises only the
opportunistic-CSI capability.
## Consequences
- RuView is positioned as the open reference stack *around* the standard: when a
chipset exposes 802.11bf, its native reports flow through calibration, fusion,
ontology, and evidence with no bespoke pipeline — the plumbing is already
tested against `SimTransport` and synthetic fixtures.
- Normalized CSI is demoted from "the WiFi input" to "one measurement type,"
which is the correct framing for a multi-measurement future and prevents the
bf path from being a second-class citizen.
- **No hardware claim is made or implied.** No commodity silicon implements
802.11bf yet; this ADR wires the *types and flow*, tested in simulation. Any
OTA/native-report accuracy claim requires real silicon evidence (a captured
log) per CLAUDE.md, and any wideband number must be tagged with the capture
hardware (ADR-292). No benchmark number is invented here.
- This ADR does not re-open ADR-152/153's decision to avoid OTA frame binding
until silicon exists; it consumes that surface and adds the pipeline
integration.
## Validation
- `cargo test -p wifi-densepose-hardware` — existing `ieee80211bf` FSM,
table, and transport tests continue to pass; new tests assert that a
`SensingMeasurementReport` (TB and non-TB) and a truncated-CIR/PDP report
round-trip through the pipeline as native `MeasurementType`s.
- `cargo test -p wifi-densepose-mat` — truncated CIR ingest reuses the ADR-292
subcarrier-agnostic path and records the native→pipeline mapping; dimension/
version validation on standardized reports mirrors the FeitCSI parser gates.
- Ontology/governance tests: each standardized measurement becomes an ADR-306
`Observation` from an ADR-305-authenticated `Sensor` with one `EvidenceLevel`;
`ConsentMode` composes with ADR-277 admission; SBP attributes proxy vs. sensor
provenance distinctly.
- HAL contract test: the ADR-320 capability descriptor is derivable from
`SensingCapabilities`; a bf-less device advertises only opportunistic CSI.
- All measurement-type flows are simulation-tested (`SimTransport`, synthetic
fixtures); OTA binding and any hardware accuracy claim remain out of scope
until real silicon exposes the standard.

View File

@@ -0,0 +1,140 @@
# ADR-311: Real sensor fusion — uncertainty-aware, multiple observations → one world state
- **Status**: Accepted — initial implementation (ADR-300 phase 2)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: fusion, uncertainty, multimodal, world-state, ontology, phase-2
## Context
This ADR is a child of **ADR-300** and owns primitive #11, *real sensor fusion*.
In the ADR-300 DAG it is a phase-2 integration primitive: it **consumes ADR-306**
(canonical spatial ontology) and **produces the single fused world state** that
the phase-3 primitives build on — **ADR-312** (long-term spatial memory),
**ADR-313** (counterfactual inference), and **ADR-315** (digital RF twin). It is
authored as **Proposed**.
The defining invariant is not "support more modalities" but the *shape of the
output*: **multiple observations must resolve to one probabilistic world state,
not many feeds into a visualization.** A dashboard that shows a WiFi layer, a
mmWave layer, and a BLE layer side by side is not fusion; it pushes the
reconciliation onto the human. Real fusion produces one uncertainty-aware state
that every downstream consumer reads, with each contributing observation's
provenance and confidence still recoverable.
Substantial scaffolding already exists and must be **reused/extended, not
rebuilt**:
- **ADR-063** (60 GHz mmWave ↔ WiFi CSI fusion, *Proposed*) established the
first cross-modal fusion case: pairing noisy CSI-derived vitals with clinical-
grade mmWave FMCW radar (Seeed MR60BHA2 over UART, with a **live hardware
capture** logged on 2026-03-15). ADR-311 generalizes that pairwise case into
an N-modality, uncertainty-aware fusion.
- **ADR-137** (fusion-engine quality scoring, *Accepted — partial*) already
built the auditable-quality building block: it identified that the multistatic
fusers (`wifi-densepose-signal/src/ruvsense/multistatic.rs`,
`wifi-densepose-ruvector/src/viewpoint/fusion.rs`) discarded the evidence they
used, and specified a single auditable record — "this fused output is
trustworthy because X, Y, Z, but be aware of contradiction C" — with evidence
references and contradiction flags. ADR-311 reuses that record as the
provenance/quality carrier of the fused state.
- **ADR-280** `CoherentSensorGroup` (fail-closed coherent fusion) and
**ADR-306** `Observation`/`Track`/`Event` node types are the input and output
vocabulary respectively.
What is missing is the **uncertainty-aware combiner across heterogeneous
modalities**: a fusion stage that takes authenticated observations from WiFi,
BLE, UWB, mmWave, acoustic, IMU, lidar, and cameras (only where policy permits),
each with its own uncertainty, and emits one probabilistic `WorldState` — with
per-observation contradiction flags, not a stack of independent feeds.
## Options considered
1. **Per-modality feeds rendered together** (today's implicit model on some
surfaces). Rejected: it is visualization, not fusion; contradictions are
never reconciled and there is no single state to reason over.
2. **Hard-switch "best modality wins"** (e.g., always prefer mmWave vitals over
CSI vitals). Rejected: throws away corroborating evidence and cannot express
*disagreement* — the very thing ADR-137's contradiction flags exist to
surface — and degrades badly when the preferred modality is absent or OOD.
3. **Uncertainty-weighted probabilistic fusion into one world state**, reusing
ADR-137's auditable quality record and ADR-280's fail-closed coherence gate.
Chosen.
## Decision
Adopt **uncertainty-aware multimodal fusion** whose invariant output is one
probabilistic world state.
### 1. Inputs: authenticated, ontology-typed observations
- Inputs are ADR-306 `Observation` nodes from **ADR-305-authenticated** sensors.
Supported modalities: WiFi (CSI / 802.11bf native reports via ADR-310), BLE,
UWB, mmWave (ADR-063), acoustic, IMU, lidar, and cameras. Cameras and any
higher privacy-class modality enter fusion **only where the ADR-277 policy
engine permits** — camera-free coverage is a RuView invariant (ADR-282), so
cameras are an opt-in, policy-gated input, never assumed present.
- Each observation carries its own uncertainty and exactly one `EvidenceLevel`
(ADR-282). An observation flagged out-of-distribution by **ADR-302** is
down-weighted or excluded per its OOD verdict rather than silently averaged in.
### 2. Combiner: uncertainty-weighted, contradiction-aware
- Observations are combined by their uncertainty into one probabilistic
`WorldState` over the ADR-306 entities (`Person`, `Object`, `Track`, and the
per-`Space` inference). The combiner does **not** collapse disagreement: when
modalities conflict beyond their stated uncertainty, the fused output carries
ADR-137 **contradiction flags** and the evidence references that produced
them, so a consumer can see *that* WiFi and mmWave disagree and *why*.
- Coherent multi-node fusion inherits ADR-280's fail-closed
`CoherentSensorGroup` gate: no coherent combination unless sync, phase, and
geometry compatibility are proven; otherwise the group degrades to incoherent
combination rather than producing confident nonsense.
### 3. Output: one world state, provenance preserved
- The output is a single `WorldState` written into the ADR-306 ontology, with
every fused value retaining recoverable per-observation provenance and the
ADR-137 quality record. This is the state ADR-312/310/312 consume; they read
one probabilistic world, not a modality stack.
- The fused state carries an aggregate uncertainty and an evidence level derived
from its inputs (never upgraded above the weakest contributing L-level for a
given claim).
## Consequences
- Downstream primitives (spatial memory, counterfactual, RF twin) build on one
probabilistic world state with uniform uncertainty and provenance, instead of
re-implementing reconciliation per consumer.
- Contradictions become first-class signal, not noise: ADR-137's record means a
disagreement between mmWave and CSI is surfaced and auditable, which is also
what lets ADR-302 and the evidence engine (ADR-304) reason about reliability.
- Fusion is uncertainty-honest: an OOD or low-evidence observation is
down-weighted, not averaged in as if trustworthy; a fused claim never presents
a stronger evidence level than its weakest necessary input.
- **No accuracy or "camera-grade" claim is made.** ADR-063's mmWave path has a
real-silicon capture; the multimodal combiner's accuracy is not asserted here.
Any fused-accuracy number requires a named reproducer tagged MEASURED /
SYNTHETIC / CLAIMED, and WiFi sensing is never presented as camera-grade
(CLAUDE.md, ADR-282). No number is invented.
- Cameras remain a governed, opt-in input; enabling them does not weaken the
camera-free coverage guarantee for deployments that exclude them.
## Validation
- `cargo test -p wifi-densepose-ruvector` / `-p wifi-densepose-signal` — the
ADR-137 quality record and contradiction flags travel with the fused output;
the ADR-280 `CoherentSensorGroup` gate still fails closed under
clock/phase/geometry violation.
- Fusion invariant test: N modality observations over one scene resolve to a
single `WorldState` node in the ADR-306 ontology (not N feeds), with
per-observation provenance recoverable and one aggregate evidence level.
- Uncertainty tests: a high-uncertainty or ADR-302-flagged-OOD observation is
down-weighted/excluded; conflicting modalities produce a contradiction flag
rather than a silently averaged value; the fused evidence level never exceeds
the weakest necessary input.
- Governance test: a camera or higher-privacy modality is admitted into fusion
only when the ADR-277 policy engine permits; otherwise it is excluded and the
fused state notes the exclusion.
- Any accuracy comparison (e.g., fused vitals vs. mmWave-only) is reported with
its evidence tag and reproducer; none is asserted in this ADR.

View File

@@ -0,0 +1,146 @@
# ADR-312: Long-term spatial memory — learn the normal physics of a location
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: spatial-memory, ruvector, anomaly-detection, temporal, world-state, phase-3
## Context
This ADR is a child of **ADR-300** and owns primitive #12, *long-term spatial
memory*. In the ADR-300 phasing it is a phase-3 primitive that sits on the fused
world state produced by **ADR-311** (real sensor fusion) and **ties to ADR-315**
(digital RF twin): spatial memory is the *learned normal* that a twin can
simulate against and that anomaly detection compares against. It is authored as
**Proposed**.
The capability is to **learn the normal physics of a location** so anomalies
surface *without training a detector for every anomaly*. Concretely, the system
should learn statements like: "a chair is normally here"; "this bedroom is
usually occupied between these hours"; "the RF propagation of this space
changed"; "this machine's vibration signature changed"; "a new reflector
appeared." None of these is a labeled anomaly class — they are *deviations from
a learned baseline of normality*. This is the difference between supervised
anomaly detection (which needs examples of every failure) and **baseline-relative
anomaly detection** (which needs only a well-characterized normal).
Substantial substrate already exists and must be **reused/extended, not
rebuilt**:
- **RuVector** (`v2/crates/wifi-densepose-ruvector`) is the designated substrate
in the ADR-282 layer stack ("persistent objects, Gaussian fields, scene
graphs, temporal memory"). It already provides the vector/temporal machinery
this ADR needs — HNSW indexing (`hnsw.rs`, `hnsw_quantized.rs`), an event log
(`event_log.rs`), coverage and estimator surfaces, and the `crv`/`mat`
temporal sub-modules — so long-term spatial memory is a *consumer and
organizer* of RuVector primitives, not a new store.
- **ADR-306** supplies the entity vocabulary the memory is indexed by (`Space`,
`Object`, `Sensor`, `Track`, `Event`); **ADR-311** supplies the fused,
uncertainty-carrying `WorldState` snapshots that memory accumulates over time.
- **ADR-135** (empty-room baseline calibration) and **ADR-301** (automatic
domain calibration) already establish a *calibration-time* baseline of a
space; ADR-312 extends that from a one-shot baseline to a **continuously
learned, time-of-day-aware** model of normal.
What is missing is the **temporal normality model**: a per-`Space` learned
distribution of fused world states over time (including periodicity — hour of
day, day of week), plus RF-propagation and modality-signature baselines, against
which a live fused state is scored for deviation.
## Options considered
1. **Supervised anomaly classifiers per anomaly type.** Rejected: it needs
labeled examples of every anomaly (fall, intrusion, machine fault, moved
furniture), which do not exist for most spaces and do not transfer between
rooms; it also cannot catch a *novel* anomaly it was never trained on.
2. **Single static baseline** (the ADR-135 empty-room snapshot, used forever).
Rejected as the endpoint: it cannot express *when* a space is normally
occupied, cannot track slow legitimate drift (furniture rearranged on
purpose), and flags every diurnal change as anomalous.
3. **Continuously learned, time-aware normality model on the RuVector
substrate**, scoring live fused state against learned normal. Chosen.
## Decision
Adopt a **long-term spatial memory** that learns each location's normal physics
on the RuVector substrate and scores live fused state against it.
### 1. What "normal" is learned over
Per ADR-306 `Space` (and the entities within it), accumulate the ADR-311 fused
`WorldState` over time into a learned normality model covering:
- **Occupancy / activity periodicity** — the distribution of presence and
activity by hour-of-day and day-of-week (the "bedroom usually occupied certain
hours" case).
- **Static scene layout** — persistent `Object` positions and the expected
reflector set (the "chair normally here" / "new reflector appeared" cases),
building on the ADR-135/298 baseline.
- **RF-propagation baseline** — the space's normal multipath/propagation
signature (the "RF propagation changed" case).
- **Per-modality signatures** — e.g., a machine's normal vibration/acoustic/IMU
signature (the "vibration signature changed" case).
Each learned baseline carries its own uncertainty and an `EvidenceLevel`
(ADR-282); a baseline learned from replay is L1, from a field pilot L4, and is
never presented above the evidence of the observations it was learned from.
### 2. Substrate: RuVector, temporally compressed
- The memory is stored and indexed on RuVector (HNSW for nearest-normal recall,
the event log for the temporal stream, the temporal sub-modules for
compression). Long-horizon history is temporally compressed — recent detail
retained, older history summarized — so memory cost is bounded rather than
growing linearly forever.
- The memory is *keyed by* the ADR-306 ontology, so "normal for this `Space` at
this hour" is a first-class query, and slow legitimate drift updates the
baseline (with provenance) instead of accumulating as permanent anomaly.
### 3. Anomaly = deviation from learned normal
- A live fused `WorldState` is scored against the applicable learned baseline
(matched by space and time context). A deviation beyond the baseline's
uncertainty is surfaced as an ADR-306 `Event`*without* a per-anomaly
detector — carrying the baseline it deviated from, the deviation magnitude,
and its evidence level. Whether that event is actionable is a policy/consumer
decision (ADR-277), not this layer's.
- The learned normal is exactly what **ADR-315** (RF twin) can simulate against:
the twin proposes an expected state, spatial memory supplies the learned
actual-normal, and their divergence is a physically grounded anomaly signal.
## Consequences
- Anomaly detection generalizes: a space gets deviation detection from its own
learned normal, so a novel anomaly (never labeled anywhere) still registers as
a deviation, and the model transfers to a new room by *learning that room's*
normal rather than importing a foreign detector.
- Bounded memory: temporal compression keeps long-horizon memory finite; the
trade-off is that fine detail of old history is summarized, which is acceptable
for a normality baseline.
- Legitimate change is not a permanent false positive: slow drift updates the
baseline with provenance, distinguishing "furniture deliberately rearranged"
(baseline shifts) from "reflector appeared unexpectedly" (deviation event).
- **No accuracy claim is made.** Deviation-detection quality is not asserted
here; any detection-rate or false-positive number requires a named reproducer
tagged MEASURED / SYNTHETIC / CLAIMED, and a health/safety framing stays within
the ADR-282 bounded-claims discipline (decision support, not diagnosis). No
number is invented.
- The memory is governed: learned baselines are observations of a space, subject
to the same ADR-277 retention/privacy policy as the fused state they summarize;
no raw P0 RF is retained to build a baseline.
## Validation
- `cargo test -p wifi-densepose-ruvector` — the normality model builds on the
existing HNSW/event-log/temporal primitives; nearest-normal recall and
temporal-compression bounds are exercised on synthetic streams.
- Baseline/deviation tests: a synthetic scene with a known injected change (moved
`Object`, altered propagation, altered modality signature) produces a deviation
`Event` against the learned normal *without* a per-anomaly detector; an
unchanged diurnal cycle produces none (no false positive on normal periodicity).
- Drift test: a slow legitimate change updates the baseline (with provenance)
rather than emitting a persistent anomaly; an abrupt change does emit one.
- Evidence test: a learned baseline carries the evidence level of its source
observations and is never presented above it; retention honors ADR-277.
- Twin-linkage design check (with ADR-315): divergence between a twin-simulated
expected state and the learned normal is expressible as a deviation signal.

View File

@@ -0,0 +1,142 @@
# ADR-313: Counterfactual inference — generative spatial reasoning
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: inference, generative, counterfactual, rf-twin, fusion, uncertainty, phase-3
## Context
This ADR is a child of **ADR-300** (perception substrate program) and owns
primitive #13, *counterfactual inference*. In the ADR-300 DAG it is a phase-3,
research-forward primitive that sits on top of the fused world state: it
**consumes ADR-311** (real sensor fusion) for the current fused estimate and
**ADR-315** (digital RF twin) for the twin's expected measurement
distributions. It is design intent, authored as Proposed, and is expected to be
revised as the phase-1 spine and the phase-2 fusion layer land.
RuView today reasons discriminatively: a task head maps measurements to a label
or a pose. That answers "what does the classifier say?" but not the questions an
operator actually asks — *would these RF measurements still make sense if nobody
were present? Does one person explain the observation better than two?* Those
are counterfactual questions, and a classifier cannot answer them because it has
no model of what a measurement *should* look like under a hypothesized world
state. A discriminative head asked about an empty room simply emits its
best-effort label; it cannot say "the observation is better explained by
absence."
The step this ADR proposes is toward a **generative spatial model**: given a
hypothesized scene state (occupancy, count, coarse positions) and the ADR-315
twin's propagation model for the deployment, predict the *expected* measurement
distribution, then score how well each hypothesis explains the observed
measurement. The best-explaining hypothesis — including the *nobody-present*
null hypothesis — is the answer, and the margin between hypotheses is a
first-class uncertainty signal.
Relevant existing assets to build on rather than duplicate:
- **ADR-311** (fusion) already produces the fused world estimate and its
covariance; the counterfactual layer scores hypotheses *relative to* that
estimate rather than re-fusing raw measurements.
- **ADR-315** (RF twin) is the generative forward model — per-deployment
geometry, radio locations, and expected measurement distributions. This ADR
is a *consumer* of the twin's forward simulator, not a second simulator.
- **ADR-302** (OOD/observability) already owns the `UNKNOWN` verdict; the
null-hypothesis ("nobody present better explains this than any occupancy
hypothesis") and the "no hypothesis explains this" case route through ADR-302,
not a parallel gate.
- `frame::EvidenceLevel` L0L5 (ADR-282) and the ADR-304 evidence engine
account for the resulting confidence.
## Options considered
1. **Keep only discriminative heads.** Rejected: cannot express absence,
cannot compare "one person vs. two" as competing explanations, and gives a
confident label even when no world state explains the data.
2. **A second, independently trained generative network with its own forward
model.** Rejected for the default path: duplicates the ADR-315 twin's
propagation model, invites the two models to disagree, and multiplies the
surface that must be validated. Reserved only if the twin's analytic forward
model proves insufficient for a phenomenon.
3. **A hypothesis-scoring layer that uses the ADR-315 twin as the forward model
and the ADR-311 fused state as the hypothesis prior, routing low-margin and
null-dominant cases to the ADR-302 UNKNOWN verdict.** Chosen.
## Decision
Define a **counterfactual inference layer** that scores a small set of scene
hypotheses against observed measurements using the digital RF twin as the
generative forward model.
### 1. Hypothesis set
- Hypotheses are drawn from the ADR-311 fused state and its neighbourhood: the
current estimate, the **null hypothesis** (nobody present), and a bounded set
of nearby alternatives (±1 occupant, shifted position). The fused estimate
supplies the prior so the search stays small and grounded rather than
enumerating an open world.
- The hypothesis space is expressed over the **ADR-306** canonical ontology
(`Space`/`Zone`, occupant count, coarse position), so a counterfactual result
is a governed spatial statement, not an opaque score.
### 2. Forward model and scoring
- For each hypothesis, query the **ADR-315 twin** for the expected measurement
distribution given that scene state and the deployment's propagation model.
Score the observed measurement's likelihood under each hypothesis's expected
distribution.
- The answer is the maximum-likelihood hypothesis; the **margin** between the
top hypotheses (and between the top hypothesis and the null) is the
confidence signal, carried into the ADR-304 evidence engine.
### 3. Routing to UNKNOWN
- When the null hypothesis dominates, the layer reports *absence*, not a
low-confidence occupancy label.
- When **no** hypothesis explains the observation well (all likelihoods low, or
the winning margin below threshold), the result routes to the **ADR-302**
`UNKNOWN` verdict — the observation is outside what the twin can explain, and
the honest output is "I cannot account for this," never a forced label.
### Evidence discipline
- Twin-predicted distributions are a **simulation** (evidence level L0 per
ADR-282) labelled `SYNTHETIC`; a counterfactual verdict inherits the evidence
level of its weakest input and is never presented as camera-grade ground
truth (CLAUDE.md honesty rule).
- Any accuracy statement about counterfactual discrimination (e.g. "distinguishes
one occupant from two") requires the mean-pose-style baseline discipline of
CLAUDE.md, a leakage-free held-out split, and a reproducer before it may be
tagged `MEASURED`. This ADR asserts **no** such number.
## Consequences
- RuView gains the ability to answer absence and "which explanation is better"
questions that discriminative heads structurally cannot — a step toward
generative spatial reasoning and a differentiator for security and
facility-monitoring applications where *absence* is the valuable signal.
- Quality is bounded by the fidelity of the ADR-315 twin's forward model and the
ADR-311 fused prior; the layer reports margins and defers to ADR-302 UNKNOWN
rather than overstating a coarse model.
- Hard dependency on ADR-311 (fused state and covariance) and ADR-315 (forward
model); this ADR builds neither a fusion engine nor a propagation simulator of
its own.
- Being phase 3, this is design intent sitting on the fused world state; it is
expected to be revised as ADR-311 and ADR-315 land, and it is not implemented
by the phase-1 swarm.
## Validation
- Unit tests: hypothesis likelihood scoring is a deterministic function of
observed measurement + hypothesis + twin parameters; the null hypothesis wins
on a synthesized empty-room measurement; a two-occupant measurement scores the
two-occupant hypothesis above the one-occupant hypothesis on a controlled
synthetic case.
- Integration test: measurements the twin cannot explain (out-of-model
scattering) drive the layer to the ADR-302 UNKNOWN verdict rather than a
forced occupancy label; margins propagate into the ADR-304 evidence engine.
- Held-out discrimination (deferred, real-silicon): one-vs-two and
presence-vs-absence discrimination on a leakage-free held-out split with a
mean-pose baseline, reported as `MEASURED` with a reproducer. Until then all
counterfactual output is `SYNTHETIC`/L0. No discrimination accuracy number is
asserted by this ADR.

View File

@@ -0,0 +1,138 @@
# ADR-314: Information-gain scheduler — sample the most informative radios
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: scheduling, active-sensing, information-gain, edge, energy, fusion, phase-3
## Context
This ADR is a child of **ADR-300** (perception substrate program) and owns
primitive #14, *information-gain scheduler*. In the ADR-300 DAG it is a phase-3,
research-forward primitive that sits on top of the fused world state and
**pairs with ADR-309** (active sensing): ADR-309 decides *what to probe*
(waveform, sensing task); this ADR decides *which radios/modalities to spend
budget on next*. It is authored as Proposed and is not implemented by the
phase-1 swarm.
With multiple sensors, processing every stream at full rate is wasteful: many
radios are, at any moment, contributing little to the current estimate while
consuming compute, energy, and bandwidth — the three scarce resources on the
edge nodes RuView targets (ESP32-S3/C6 and small gateways). Treating all sensors
equally is precisely the design that does not survive a real deployment of
"hundreds of sensors."
The scheduler assigns each candidate sensor/modality a value
```
Value(sensor) ≈ expected uncertainty reduction / (compute + energy + bandwidth)
```
and spends the next sampling/processing budget on the highest-value sensors.
Expected uncertainty reduction is estimated *before* paying for the measurement,
which is why the scheduler needs a model of what each sensor is likely to tell
it — supplied by the fused state's covariance and the RF twin's forward model,
not by actually sampling.
Relevant existing assets to build on rather than duplicate:
- **ADR-311** (fusion) maintains the fused state and its covariance — the
current uncertainty the scheduler is trying to reduce. Expected uncertainty
reduction is computed against that covariance, not a private one.
- **ADR-315** (RF twin) provides the per-sensor forward model used to predict a
candidate measurement's expected informativeness before sampling.
- **ADR-320** (RuView sensor HAL, phase 2) exposes each radio's real
compute/energy/bandwidth cost descriptors; the denominator is read from the
HAL, not guessed per platform.
- **ADR-309** (active sensing) is the paired actuator: the scheduler ranks
sensors, ADR-309 chooses the probe on the chosen sensor.
- **ADR-302** (observability) defines the phenomenon the estimate is *for*, so
the scheduler prioritizes uncertainty reduction on the objective that matters,
not on nuisance dimensions.
## Options considered
1. **Round-robin / process-everything scheduling.** Rejected: burns edge
compute and energy on redundant streams and does not scale to large fleets;
the strategic and external reviews named exactly this as an edge-deployment
blocker.
2. **Static priority per sensor type (e.g. always prefer mmWave).** Rejected:
ignores that a sensor's *current* informativeness depends on the scene and
the present uncertainty — a well-placed WiFi link can dominate an occluded
mmWave node in a given moment.
3. **A value-of-information scheduler that ranks sensors by expected uncertainty
reduction per unit cost, using the ADR-311 covariance and ADR-315 forward
model, with costs from the ADR-320 HAL.** Chosen.
## Decision
Define an **information-gain scheduler** that allocates the next
sampling/processing budget across available radios by value of information.
### 1. Value function
- For each candidate sensor/modality, estimate **expected uncertainty
reduction** on the ADR-302 objective by evaluating how much a predicted
measurement (via the **ADR-315** forward model) would shrink the **ADR-311**
fused-state covariance — a value-of-information estimate made *before* paying
for the measurement.
- Divide by the sensor's **cost** — compute + energy + bandwidth — read from the
**ADR-320** HAL descriptors. The exact weighting of the three cost terms is a
deployment policy (a battery node weights energy heavily; a wired gateway
weights bandwidth), configured, not hardcoded.
### 2. Allocation
- Rank candidates by value and spend the budget on the top set, subject to a
configurable floor that guarantees each sensor is sampled at least
occasionally (so a sensor whose value is currently low is not starved into
permanent blindness and can be re-evaluated as the scene changes).
- The scheduler emits an allocation, not a measurement; **ADR-309** active
sensing chooses the probe/waveform on each selected sensor, and the fusion
layer (ADR-311) incorporates the result.
### 3. Governance and honesty
- Skipping a sensor for a cycle is a *deliberate* reduction in coverage; the
scheduler records which sensors were sampled so downstream evidence (ADR-304)
reflects the actual sensing that occurred, and observability (ADR-302) can
raise `UNKNOWN` for a zone that went under-sampled rather than reporting a
stale estimate as current.
### Evidence discipline
- Expected-uncertainty-reduction estimates are model predictions from the
ADR-315 twin (simulation, L0 per ADR-282, `SYNTHETIC`); a scheduling decision
is a resource choice, never a sensing claim.
- Any energy/latency/throughput improvement figure requires real-silicon
measurement with a reproducer before it is tagged `MEASURED` (CLAUDE.md
hardware rule). This ADR asserts **no** efficiency number.
## Consequences
- Edge deployments spend scarce compute, energy, and bandwidth where they buy
the most certainty, making "hundreds of sensors" operationally tractable — a
capability the reviews flagged as critical for edge deployment.
- Quality is bounded by the accuracy of the ADR-315 forward model (informativeness
prediction) and ADR-320 cost descriptors; a poor forward model degrades to
near-round-robin, which is safe but not optimal. The sampling floor bounds the
worst case.
- Hard dependency on ADR-311 (covariance), ADR-315 (forward model), and ADR-320
(cost descriptors), and paired with ADR-309; this ADR builds none of those.
- Being phase 3, this is design intent sitting on the fused world state and is
expected to be revised as ADR-309, ADR-311, ADR-315, and the ADR-320 HAL land.
## Validation
- Unit tests: the value function is a deterministic function of covariance +
forward model + cost descriptors; a sensor predicted to reduce objective
uncertainty more per unit cost ranks above one that reduces it less; the
sampling floor guarantees eventual re-evaluation of a low-value sensor.
- Integration test: on a synthetic multi-sensor scene, the scheduler reduces
objective uncertainty faster per unit modelled cost than round-robin, and
raises ADR-302 UNKNOWN for a deliberately starved zone rather than reporting a
stale estimate.
- Field validation (deferred, real-silicon): energy/latency/throughput on an
instrumented multi-node deployment, reported as `MEASURED` with a reproducer.
Until then all informativeness and cost figures are `SYNTHETIC`/L0. No
efficiency number is asserted by this ADR.

View File

@@ -0,0 +1,159 @@
# ADR-315: Digital RF twin — persistent per-deployment RF model
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: rf-twin, digital-twin, propagation, calibration, spatial-memory, worldgraph, phase-3
## Context
This ADR is a child of **ADR-300** (perception substrate program) and owns
primitive #15, *digital RF twin*. In the ADR-300 DAG it is a phase-3,
research-forward primitive that underpins several other phase-3 primitives:
**ADR-308** (placement optimizer) plans against the twin's propagation model,
**ADR-313** (counterfactual inference) uses it as the generative forward model,
and **ADR-314** (information-gain scheduler) uses it to predict per-sensor
informativeness. It ties directly to **ADR-301** (calibration), **ADR-308**
(placement), and **ADR-312** (long-term spatial memory). It is authored as
Proposed and is not implemented by the phase-1 swarm.
RuView today has no persistent, per-deployment model of the RF environment.
Calibration state, observed multipath, and radio geometry exist transiently
inside a running session; when the process restarts or a change happens
overnight, there is nothing that says "this is what this room's RF looked like
yesterday." Without a persistent baseline, a physical change — furniture moved,
a wall opened, a machine relocated, an intruder present — has nothing to be a
*delta against*. It is just a different measurement, indistinguishable from
noise or drift.
The **digital RF twin** is that persistent baseline: a per-deployment model
holding
- **geometry and radio locations** (from the ADR-306 scene / worldgraph),
- **propagation history** and **observed multipath** structure,
- **calibration state** (from ADR-301),
- **expected measurement distributions** for each link and phenomenon.
Once the twin exists, a physical change becomes a **measurable delta against the
twin** rather than an unexplained measurement. This is what connects RuView to
facility management (what changed in this space?), security (is there an
unexplained presence?), robotics (has the map drifted?), and industrial
monitoring (did the plant layout change?) — the applications the strategic
assessment named as the value beyond a single detector.
Relevant existing assets to build on rather than duplicate:
- The `worldgraph` crate already models the physical scene — `Room`/`Space`
with `bounds_enu`, `Wall { rf_attenuation_db }`, `Doorway`, `Zone`, and
`Sensor` nodes (ADR-306). The twin *annotates and persists* this scene with RF
state; it does not invent a second geometry.
- `wifi-densepose-calibration` (enrollment, bank, anchor, runtime, specialist)
holds the calibration state the twin persists; the twin references and
versions calibration records, it does not reimplement calibration.
- **ADR-312** (long-term spatial memory, phase 3) is the persistence and
temporal-history substrate; the twin is a *structured occupant* of that
memory, not a separate database.
- **ADR-305** (authenticated identity) and **ADR-295** (provenance) mean the
measurements that update the twin carry verified lineage, so a delta is
attributable rather than anonymous.
## Options considered
1. **No persistent RF model (status quo).** Rejected: every change looks like
noise; nothing supports "what changed since yesterday?", which is the
question the facility/security/industrial applications actually ask.
2. **A full electromagnetic digital twin (per-site ray-tracing / FDTD kept in
sync in real time).** Rejected for the default path: far heavier than the
coarse `rf_attenuation_db` scene RuView actually has and impractical on edge
hardware. A high-fidelity solver is retained as an *optional backend* the
twin can call, not the baseline.
3. **A persistent, per-deployment RF model layered over the ADR-306 scene and
ADR-312 memory: geometry + radio locations + calibration state + observed
multipath + expected measurement distributions, updated by verified
measurements, exposing changes as deltas.** Chosen.
## Decision
Define the **digital RF twin** as a persistent, versioned, per-deployment model
of the RF environment, layered over existing scene, calibration, and memory
assets.
### 1. State the twin holds
- **Geometry and radio locations** referenced from the ADR-306 / worldgraph
scene (not copied).
- **Calibration state** referenced and versioned from
`wifi-densepose-calibration` (ADR-301), so the twin knows *which* calibration
a stored distribution was captured under.
- **Observed multipath and propagation history** — a bounded temporal summary
of per-link channel structure, stored in ADR-312 spatial memory.
- **Expected measurement distributions** per link and phenomenon — the forward
model ADR-308, ADR-313, and ADR-314 consume.
### 2. Update and delta
- Verified measurements (ADR-305 identity, ADR-295 provenance) update the twin's
distributions online, bounded by ADR-301 calibration validity. A new
observation is compared to the twin's expected distribution; the **delta**
and its statistical significance against the twin's own variance — is the
primary output. A change large relative to the twin's modelled variance is a
*detected physical change*, not noise.
- The twin is **versioned**: a calibration event, a deliberate geometry edit, or
an accepted physical change advances the twin version, so history is
auditable and a delta is always relative to a named baseline.
### 3. Consumers
- **ADR-308** queries the twin's propagation model to plan placements.
- **ADR-313** uses the twin's expected distributions as the generative forward
model for hypothesis scoring.
- **ADR-314** uses per-sensor expected informativeness from the twin.
- Facility/security/robotics/industrial integrations read the twin's change
deltas as governed ADR-306 spatial events.
### Evidence discipline
- The twin's expected distributions and any propagation simulation are
**simulation** (evidence level L0 per ADR-282), labelled `SYNTHETIC`. A delta
computed against them is a model-relative statement.
- A change/anomaly detection *claim* (e.g. "detects furniture-scale changes")
requires real-silicon measurement against a leakage-free protocol with a
reproducer before it is tagged `MEASURED` (CLAUDE.md hardware rule). The twin
never presents a modelled expected distribution as evidence that a physical
state *is* the case; it presents a *delta and its significance*. This ADR
asserts **no** detection-accuracy number.
## Consequences
- RuView gains a persistent per-deployment baseline, turning "a different
measurement" into "a measurable, attributable, versioned change" — the bridge
from a sensing runtime to facility management, security, robotics, and
industrial monitoring.
- The twin is the shared forward model for ADR-308/310/311, so those primitives
speak one propagation model rather than three inconsistent ones — a
deliberate reason to build the twin before its consumers mature.
- Quality is bounded by the coarseness of the worldgraph scene and the fidelity
of the forward model; the twin reports deltas *with significance against its
own variance* rather than asserting confident change detection on a coarse
model. The optional high-fidelity backend is where higher accuracy lives.
- Hard dependency on ADR-306 (scene), ADR-301 (calibration state), and ADR-312
(persistence); it reuses `worldgraph` and `wifi-densepose-calibration` rather
than rebuilding geometry or calibration.
- Being phase 3, this is design intent; it is expected to be revised as the
phase-1 spine, ADR-311 fusion, and ADR-312 memory land.
## Validation
- Unit tests: the twin's expected distribution is a deterministic function of
scene + calibration + propagation history; delta computation and its
significance against stored variance are correct on synthetic distributions;
versioning advances on calibration/geometry/accepted-change events and history
is retained.
- Integration test: on a synthetic deployment, an injected physical change (a
wall attenuation shift) produces a significant delta against the twin while
ordinary noise does not; the delta surfaces as a governed ADR-306 event with
provenance (ADR-305/292).
- Field validation (deferred, real-silicon): change detection on an instrumented
real deployment with a controlled physical-change protocol, reported as
`MEASURED` with a reproducer. Until then all twin distributions and deltas are
`SYNTHETIC`/L0. No detection-accuracy number is asserted by this ADR.

View File

@@ -0,0 +1,156 @@
# ADR-316: Fleet control plane — provisioning to audit trails
- **Status**: Proposed (ADR-300 phase 2)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: fleet, operations, provisioning, firmware, updates, audit, identity, phase-2
## Context
This ADR is a child of **ADR-300** (perception substrate program) and owns
primitive #16, *fleet control plane*. In the ADR-300 DAG it is a phase-2
integration-and-operations primitive that sits on the phase-1 spine: it
**consumes ADR-305** (authenticated sensor identity) for per-device identity and
enrollment, and **ADR-318** (capability certificate) for the signed models,
calibration validity, and capability envelopes a device is allowed to run. It is
authored as Proposed and is not implemented by the phase-1 swarm.
The external and internal reviews both named the same operational gap: RuView
has strong per-device primitives but no **release identity** and no **bill of
materials** binding a fielded sensor to the exact firmware, model, and
calibration it is running — and no plane to manage that across many devices.
Without this, a handful of nodes is fine but *hundreds* of sensors become an
operational nightmare: no coherent way to provision, roll certificates, verify
firmware compatibility, distribute signed models, track calibration lifecycle,
watch health, stage updates, roll back, diagnose remotely, enforce data
retention, or produce an audit trail. This ADR addresses that release-identity /
BOM gap directly.
The scope is deliberately the **control plane**, not the data plane. The
authenticated measurement path is **ADR-296** (bind + allowlist) plus **ADR-305**
(signed envelope); this ADR governs the *devices and artifacts*, not the
per-frame stream.
Relevant existing assets to build on rather than duplicate:
- **ADR-305** already defines per-device keypairs, the `DeviceId → public key →
capabilities` enrollment record, key rotation and revocation *semantics* — and
explicitly deferred their **fleet distribution** to this ADR. The control
plane is the distribution and lifecycle layer over ADR-305 identity, not a new
identity scheme.
- **ADR-318** (capability certificate) defines the signed, expiring artifact a
device is authorized to run; the fleet plane is what *distributes, stages, and
revokes* those certificates and the signed models they point at.
- **ADR-301** (calibration) owns calibration validity/expiry; the fleet plane
tracks calibration *lifecycle* across the fleet (which nodes are due, which are
stale) rather than redefining calibration.
- **ADR-319** (witness chain) provides the append-only, re-verifiable record;
fleet audit trails are witness-chain entries, not a parallel log format.
- **ADR-320** (RuView sensor HAL, phase 2) provides hardware/firmware capability
descriptors used for firmware-compatibility checks before staging an update.
- `wifi-densepose-bfld` `CapabilityAttestation` (ADR-141) is the device-side
attestation the plane checks against declared cohort capabilities.
## Options considered
1. **Manual per-device operations (SSH/flash by hand).** Rejected: does not
scale past a handful of nodes, produces no release identity, no audit trail,
and no safe rollback — exactly the operational nightmare the reviews named.
2. **Adopt a generic third-party IoT device-management platform wholesale.**
Rejected as the core: generic platforms do not understand RuView's signed
capability certificate, calibration validity, or witness chain, and would
fork trust away from the phase-1 spine. A generic transport/agent *may* be a
backend, but identity, certificates, and audit remain RuView's.
3. **A RuView-native control plane layered on ADR-305 identity, ADR-318
certificates, ADR-301 calibration lifecycle, and ADR-319 audit — covering
provisioning through rollback and retention.** Chosen.
## Decision
Define a **fleet control plane** that manages RuView sensors and their signed
artifacts across their lifecycle, built on the phase-1 identity/certificate
spine.
### 1. Release identity and bill of materials
- Each fielded device has a **BOM record** binding `DeviceId` (ADR-305) → exact
firmware version → signed model set → active capability certificate (ADR-318)
→ current calibration record (ADR-301) → HAL/hardware descriptor (ADR-320).
This *is* the release identity the reviews found missing: given a device you
can state precisely what it is running and prove it is signed.
### 2. Provisioning, certificates, firmware compatibility
- **Provisioning** is the authorized ADR-305 enrollment step at fleet scale:
minting a keypair, registering the public key and capabilities, and issuing
the initial ADR-318 certificate. A device is untrusted until provisioned.
- **Certificate lifecycle**: issue, rotate, expire, and **revoke** ADR-318
certificates and the ADR-305 keys behind them; revocation lists are
distributed here (the distribution ADR-305 deferred).
- **Firmware compatibility**: before staging a firmware or model, check the
target's ADR-320 HAL descriptor and ADR-141 capability attestation so an
incompatible or under-capable device is never sent an artifact it cannot
honestly run.
### 3. Cohorts, staged updates, rollback
- Devices group into **cohorts** (by site, hardware, capability). Updates —
signed models and firmware — roll out **staged** (canary → cohort → fleet)
with health gates between stages, and **roll back** to the previously recorded
BOM on a failed health check. Only signed artifacts are ever staged.
### 4. Health telemetry, remote diagnostics, retention, audit
- **Health telemetry** and **remote diagnostics** report device liveness,
calibration staleness (ADR-301), certificate expiry (ADR-318), and error
state — read-only diagnostics by default, mutations authorized explicitly.
- **Data retention** policy is enforced per cohort, and P0/CSI/person data never
leaves the edge except under the ADR-277/280 governance already in force
(CLAUDE.md: never commit or exfiltrate CSI/person data).
- Every lifecycle action — provision, rotate, revoke, stage, roll back — is
written as an **ADR-319 witness-chain** entry, giving a re-verifiable **audit
trail** rather than a mutable log.
### Authority and least privilege
- The control plane is default-deny (CLAUDE.md: default to least authority).
Provisioning, key rotation, revocation, staging, and rollback are each
separately authorized operations; no fleet action is implied by another.
Credentials and private keys are never logged or committed.
## Consequences
- Hundreds of sensors become operable: coherent release identity, signed-artifact
distribution, staged updates with rollback, and a re-verifiable audit trail —
closing the release-identity / BOM gap the reviews raised.
- The plane concentrates operational authority; that is mitigated by
default-deny, per-action authorization, signed-only artifacts, and
witness-chained audit. A compromised plane must still forge signatures the
phase-1 spine verifies.
- Hard dependency on ADR-305 (identity), ADR-318 (certificate), ADR-301
(calibration lifecycle), ADR-319 (audit), and ADR-320 (firmware/HAL
compatibility). This ADR distributes and sequences those artifacts; it does
not redefine identity, certificates, calibration, or the witness format.
- Being phase 2, this is design intent depending on the spine; it is expected to
be revised as ADR-318, ADR-319, and ADR-320 land.
- **No fielded fleet-operation claim is MEASURED without real-silicon evidence**
(CLAUDE.md hardware rule): staged update and rollback on real nodes require a
captured runtime log. A passing simulation is not fleet evidence.
## Validation
- Unit tests: BOM records bind identity/firmware/model/certificate/calibration
consistently and reject inconsistent bindings; certificate issue/rotate/revoke
transitions are correct; a firmware-incompatible target is refused staging;
every lifecycle action emits a well-formed ADR-319 witness entry.
- Integration test: a synthetic cohort undergoes a canary→cohort→fleet staged
update; an injected health failure triggers rollback to the prior BOM; the
full sequence is re-verifiable from the witness chain offline; a revoked
certificate is rejected fleet-wide.
- Security test (`npm run test:security` analogue for the plane): default-deny
is enforced; unauthorized provision/rotate/revoke/stage is rejected and
counted; no credential or P0 data appears in telemetry or audit output.
- Field validation (deferred, real-silicon): a real multi-node staged update and
rollback with a captured boot/runtime log, reported as `MEASURED` with a
reproducer. Until then all fleet-operation results are simulator-level. No
fielded reliability number is asserted by this ADR.

View File

@@ -0,0 +1,140 @@
# ADR-317: Multi-domain benchmark scorecard — regressions cannot hide behind pooled accuracy
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: benchmark, aetherarena, ci-gate, evidence, honesty, domain-generalization, substrate
## Context
This ADR is primitive 17 of the perception-substrate program (ADR-300) and the
per-PR enforcement edge of the phase-1 certificate spine. In the ADR-300
dependency DAG it reads accuracy from the evidence engine (ADR-304), consumes
the domain state produced by out-of-distribution detection (ADR-302), scores
against calibration certificates (ADR-301), and is anchored in the witness chain
(ADR-319). It is the surface that makes the rest of the spine testable on every
change to sensing code.
A single pooled accuracy number is the classic way a domain-generalization
regression hides. A model can raise mean PCK or mean presence accuracy while
quietly collapsing on unseen rooms, unseen devices, or stationary subjects —
exactly the conditions WiFi sensing fails in and exactly the conditions a
pooled average washes out. The strategic assessment (ADR-300) named this: what
distinguishes infrastructure from a demo is that a regression on *any* operating
domain is caught before merge, not discovered in the field.
RuView does not need a new benchmark to do this. AetherArena is already
**v0-complete infrastructure** (ADR-149): a deterministic scoring engine
reusing `wifi-densepose-train` (`src/ruview_metrics.rs`, `src/ablation.rs`,
`src/eval.rs`, `src/proof.rs`), a `PROOF_SEED=42` determinism substrate that
SHA-256-hashes outputs against an expected hash, an append-only witness ledger,
and a live Hugging Face Space. ADR-145's ablation harness already computes
presence accuracy, localization error, FP/FN, latency percentiles, a
privacy-leakage score, and **cross-room degradation**. The board is
intentionally empty (benchmark-first). What is missing is not a scorer but a
**scorecard format** that reports per-domain rather than pooled, and a
**sensing-crate CI gate** that runs it on every PR.
## Options considered
1. **Keep the single pooled score / `RuViewTier`.** Rejected: it is exactly the
surface a per-domain regression hides behind; a Gold tier can coexist with a
broken unseen-room slice.
2. **Add a new benchmark repo/harness for domains.** Rejected: AetherArena's
scorer, determinism binding, and witness ledger already exist and are the
right engine; a parallel harness would fork the scoring substrate and its
anti-gaming/leakage discipline.
3. **Extend the AetherArena scorer with a per-domain scorecard and wire it as a
per-PR sensing-crate gate.** Chosen.
## Decision
Reuse the AetherArena scorer and witness ledger (ADR-149) and add two things: a
**multi-domain scorecard** format and a **sensing-crate PR gate** that produces
it.
### 1. The multi-domain scorecard
The scorecard reports each capability broken out by operating domain, never
pooled into one figure. The v0 domain axes:
- **Presence**: `room-known`, `room-unseen`, `device-unseen`, `stationary-10m`
(a stationary subject at range — the canonical WiFi failure case).
- **Pose**: `matched`, `subject-unseen`, `room-unseen`.
- **OOD rejection**: the rate at which genuinely out-of-distribution input is
correctly returned as UNKNOWN by ADR-302 (a capability, not a failure) and
the false-UNKNOWN rate on in-distribution input.
- **Calibration drift**: fingerprint-distance trajectory against the ADR-301
certificate over the scored window, and the fraction of inferences in each
ADR-302 `DomainState` (KNOWN / DEGRADED / UNKNOWN).
Each cell carries exactly one `EvidenceLevel` (L0L5, ADR-282). A slice scored
on synthetic input is L0/`Synthetic` by construction; a slice on a leakage-free
held-out real split is graded higher and only then may a per-domain number be
labelled MEASURED. Pose PCK cells additionally require the mean-pose baseline
and a leakage-free held-out split (CLAUDE.md) or they are not reported as pose
accuracy at all.
### 2. Per-domain regression gate
- The gate compares each scorecard cell against the merged-baseline scorecard
stored in the AetherArena witness ledger. A regression **in any single
domain** beyond its configured threshold fails the PR, even if the pooled
average improved. Improvement on `room-known` cannot buy a regression on
`room-unseen`.
- Thresholds are per-domain and per-capability; the unseen/stationary/OOD
domains carry the strictest budgets because they are the ones a pooled score
hides. The baseline is append-only and witness-anchored — a new baseline is a
new signed ledger entry, never an in-place overwrite (ADR-149 ledger pattern,
ADR-319 anchoring).
### 3. Sensing-crate CI wiring
- Every PR that touches a sensing crate runs the scorecard across all domains
under the ADR-011/ADR-149 determinism binding (`PROOF_SEED=42`), so the run
is reproducible and tamper-evident. The gate is added to
`.github/workflows/` as an authoritative check.
- The held-out real split remains private and is never accessible to synthetic
generation, augmentation, or calibration (ADR-149 leakage constraint, ADR-282
rule d). Submitters/PRs provide a model, not predictions on data they hold.
### Provenance and honesty discipline
- No benchmark numbers are invented by this ADR. It delivers the scorecard
format, the per-domain gate, and the CI wiring; the numbers come from the
ADR-304 evidence ledger and the AetherArena scorer on real data, labelled at
the honest evidence level. Empty domains report "no evidence," which the gate
treats as no coverage — never as a pass.
## Consequences
- A domain-generalization regression can no longer merge behind a flattering
pooled average; the failure mode that most distinguishes fielded sensing from
a demo is caught at PR time.
- Every PR touching sensing pays a per-domain scoring cost. Bounded by reusing
the existing deterministic scorer and by tiered compute (CPU smoke vs full
score, ADR-149), but it is a deliberate cost for per-domain safety.
- The empty AetherArena board fills with honest, per-domain, evidence-labelled
results rather than a single headline tier — consistent with the
benchmark-first posture and with ADR-282's ecosystem positioning.
- Some domains will show weak or absent coverage. Surfacing that per-domain is
the point; the scorecard must never paper over a thin domain with a pooled
number.
- The program-level acceptance test (ADR-300) is encoded here as an AetherArena
scenario, closing the loop once the phase-1 spine lands.
## Validation
- `cargo test` on the AetherArena scorer extension — per-domain slicing math
against fixtures; per-domain regression gate fails on a single-domain
regression while pooled improves, and passes when all domains hold; empty
domains report "no evidence," not a pass; every cell carries exactly one
`EvidenceLevel`; synthetic slices are L0 by construction.
- Determinism: a scored run reproduces its SHA-256 hash under `PROOF_SEED=42`
(ADR-011/ADR-149 binding); the baseline scorecard is append-only and
witness-anchored (ADR-319), never mutated in place.
- CI: the sensing-crate gate runs on a PR touching a sensing crate and blocks a
planted single-domain regression.
- Real-data scorecards (a leakage-free held-out split with ADR-303 references)
are the maturity milestone; a synthetic scorecard is L0 and no per-domain
number is MEASURED without a reproducer per CLAUDE.md.

View File

@@ -0,0 +1,138 @@
# ADR-318: Capability certificates — validated-for-this-environment claims
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: capability, certificate, evidence, provenance, signature, honesty, substrate
## Context
This ADR is primitive 18 of the perception-substrate program (ADR-300) and,
per the strategic assessment, among the strongest ideas in the program: it is
where the whole certificate spine becomes a consumable contract. In the ADR-300
dependency DAG it **consumes the evidence engine (ADR-304)** — a capability
certificate is a signed attestation minted over a slice of that ledger — the
**calibration certificate (ADR-301)** for the environment it is validated
against, and the **RuField signature types (ADR-305 / ADR-260/262/277/279)** to
sign it. It reports domain state via ADR-302 and is anchored in the witness
chain (ADR-319).
RuView must stop making unconditional capability claims. "Supports presence" is
not a true statement — presence detection works in some rooms, on some hardware,
for some subject dynamics, and fails on a stationary subject at range in an
uncalibrated room. A capability is only ever *validated for a specific
environment*, and the honest unit of that claim is a signed, expiring
certificate, not a feature flag in a README.
The ingredients now exist across the phase-1 spine: ADR-304 accumulates
per-`(room, device, subject)` accuracy, false-positive rate, drift, and domain
state; ADR-301 produces the signed room fingerprint the environment is keyed to;
ADR-305 provides the authenticated device identity and `CapabilityAttestation`
(BFLD, ADR-141) that bounds *what a device is even attested to sense*; ADR-282
provides the mandatory `EvidenceLevel`. What is missing is the artifact that
binds them into a single, verifiable "validated here, until then" claim and the
consumer-side rule that refuses capabilities lacking one.
## Options considered
1. **Static capability flags / a `supports_presence` boolean.** Rejected: it is
the exact dishonest claim — environment-independent, unsigned, non-expiring,
and false the moment the room, device, or subject dynamics differ.
2. **Report raw ledger accuracy to consumers directly.** Rejected: the ledger
(ADR-304) is the source of truth but not a portable, signed, bounded contract;
handing consumers raw records pushes evidence-weighting and expiry logic into
every consumer and drops the single verifiable object.
3. **Mint a signed, expiring `CapabilityCertificate` over an ADR-304 ledger
slice, and make consumers refuse capabilities without a valid one.** Chosen.
## Decision
Introduce a signed **`CapabilityCertificate`**: a bounded attestation that a
specific capability has been validated for a specific environment, for a bounded
time.
### 1. The certificate
A serializable `CapabilityCertificate` binding:
- `capability` — the phenomenon (e.g. `presence`, `pose`), which must be within
the device's ADR-305/ADR-141 `CapabilityAttestation` (a device cannot be
certified for something it is not even attested to sense).
- `room` — the ADR-306 space identifier, tied to the ADR-301 calibration
certificate version the validation was performed against.
- `hardware` — the ADR-305 authenticated `DeviceId` (and, in phase 2, the
ADR-320 HAL descriptor of the sensor).
- `model` — the model version scored.
- `calibrated_date` — the calibration certificate age at validation time.
- `moving_recall`, `stationary_recall`, `false_presence_per_24h` — the measured
operating metrics, sliced from the ADR-304 ledger for this exact context (not
a global average), each honestly labelled. These are per-capability; a pose
certificate carries pose metrics with the mean-pose baseline and a
leakage-free split (CLAUDE.md) or it is not issued.
- `valid_until` — an explicit expiry; a certificate is never open-ended.
- `evidence_level` — exactly one L0L5 (ADR-282). A certificate minted from a
synthetic ledger slice is L0/`Synthetic`; a MEASURED metric requires an
ADR-303 reference and a reproducer. The certificate cannot upgrade the level
of the ledger it is minted from (ADR-304 honesty rule).
- `signature` — a RuField `SignatureBlock` (ADR-305 / ADR-260/262/277/279) over
the canonical serialization; an unsigned certificate is not a valid
certificate. The certificate is anchored in the witness chain (ADR-319).
### 2. Minting
- A certificate is minted from a slice of the ADR-304 evidence ledger for one
`(room, device, subject-class, model)` context. If the ledger reports "no
evidence" for that context, **no certificate is issued** — absence of evidence
is never a capability. Minting is a pure function over the append-only ledger
at mint time; the metrics are frozen into the signed object.
- Expiry (`valid_until`) is derived from calibration validity (ADR-301) and an
evidence-freshness policy: a certificate cannot outlive the calibration it was
validated against, and drift beyond the ADR-301 envelope invalidates both.
### 3. Consumer refusal rule
- Applications and surfaces **refuse to consume a capability that lacks a valid
certificate for the current environment**. "Valid" means: signature verifies,
`room`/`hardware`/`model` match the running context, `valid_until` is in the
future, and the referenced calibration certificate is itself still valid
(ADR-301 not invalidated). A failed check yields UNKNOWN via ADR-302, not a
best-effort guess.
- This makes the ADR-300 acceptance clause "quantify whether it can reliably
sense the requested phenomenon → generate a signed capability certificate"
a hard gate rather than a hope.
## Consequences
- RuView can no longer claim a capability it has not validated for the caller's
environment; the honest failure — "not certified here" → UNKNOWN — is
surfaced by construction rather than by discipline.
- OEM/integrator diligence gets a single verifiable artifact ("presence,
validated in *this* room, on *this* device, with *these* recall/false-alarm
numbers, until *this* date, at *this* evidence level, signed") — the strongest
commercial output of the spine.
- Certificates expire and get refused; some environments will have no
certificate and therefore no capability until validated. That refusal is the
intended honest behavior, not a regression.
- Key management and expiry policy are operational responsibilities, reusing the
ADR-305 enrollment/rotation and ADR-301 validity machinery rather than new
infrastructure; fleet distribution of certificates is owned by ADR-316.
- No capability number is invented here; every metric on a certificate is sliced
from the ADR-304 ledger at its honest evidence level.
## Validation
- `cargo test` on the certificate crate — mint from a ledger slice produces the
frozen metrics; "no evidence" context yields no certificate; signature
round-trip and tamper rejection; `valid_until` and calibration-linked expiry
enforced; consumer refusal on room/hardware/model mismatch, expiry, or
invalidated calibration resolves to UNKNOWN (ADR-302), not a guess; evidence
level is inherited from the ledger and cannot be upgraded; a certificate
cannot be issued for a capability outside the device's ADR-305/ADR-141
attestation.
- Cross-ADR: an ADR-304 ledger fixture mints a certificate; an ADR-302 test
asserts an expired/mismatched certificate gates to UNKNOWN; the ADR-300
acceptance test consumes a minted certificate end-to-end.
- Real-deployment certificates (minted from a populated ledger with ADR-303
references on live ESP32 captures) are the maturity milestone and require
hardware evidence per CLAUDE.md; a certificate minted from a synthetic ledger
is L0 by construction.

View File

@@ -0,0 +1,146 @@
# ADR-319: Witness chain — epistemic infrastructure for physical AI
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: provenance, witness, evidence, signature, epistemics, ontology, substrate
## Context
This ADR is primitive 19 of the perception-substrate program (ADR-300) and a
spine root of its phase-1 certificate stack. In the ADR-300 dependency DAG it
**extends the source-provenance state machine (ADR-295)** and the RuField
provenance types, **ties to the signature machinery (ADR-305 /
ADR-260/262/277/279)**, and anchors the artifacts produced by ADR-301
(calibration certificates), ADR-304 (evidence records), ADR-317 (benchmark
scorecards), and ADR-318 (capability certificates). In phase 2 it carries the
independent-corroboration link from ADR-303.
The strategic assessment (ADR-300) framed RuView's real product as **epistemic
infrastructure for physical AI**: the value is not the claim "a person is
present" but the *auditable reasoning* behind it. A bare boolean output discards
everything a downstream system needs to trust or contest it — which radio
observed it, what DSP evidence supported it, which model inferred it, whether an
independent sensor agreed, what spatial state it updated, and what policy acted
on it. Once the answer is a boolean, "why do you believe that?" has no answer.
RuView already has the pieces of a chain but not the chain itself. ADR-295
defines a canonical `SourceState` (`Synthetic` / `LiveVerified` /
`LiveUnverified` / `Stale` / `Disconnected`) with `Unknown` structurally
forbidden from collapsing to live. ADR-305 defines the signed
`device → measurement → sequence → timestamp → … → signed event` chain of
custody. RuField carries `FrameProvenance`, `SemanticProvenance`, and signature
types; the AetherArena witness ledger (ADR-149) demonstrates an append-only,
witness-anchored ledger. What is missing is a single **staged, signed envelope**
that travels the whole pipeline and records, at each stage, the confidence and
provenance of that stage.
## Options considered
1. **Keep provenance as scattered per-stage fields (status quo).** Rejected:
`FrameProvenance`, `SourceState`, calibration state, and model uncertainty
live in different structures and are re-encoded per surface; there is no
single object a consumer can re-verify offline to answer "why."
2. **Log a free-form audit trail alongside the output.** Rejected: mutable,
unsigned, and not structurally tied to the output — the classic
dashboard-that-overwrites-yesterday failure the evidence engine (ADR-304)
already rejects.
3. **A staged, signed witness envelope carried through the pipeline, each stage
appended and signed, anchored in an append-only ledger.** Chosen.
## Decision
Define the **witness chain**: a staged, append-only, signed envelope that
accompanies an observation from radio to policy decision. Instead of emitting
"person present," RuView emits a chain whose stages are:
```
RF observation ▸ DSP evidence ▸ model inference ▸ independent corroboration
▸ spatial state ▸ policy decision
```
### 1. The staged envelope
- Each stage is a signed record carrying its **confidence** and its
**provenance**:
- **RF observation** — the ADR-305 authenticated frame envelope
(`DeviceId`, sequence, timestamp, measurement hash) and its ADR-295
`SourceState`. This is the root link; a `Synthetic` root can never present
as a `LiveVerified` one (ADR-295 invariant).
- **DSP evidence** — the deterministic signal features and the ADR-137
quality signals that support (or fail to support) an inference.
- **model inference** — the model version, its raw output, and its predictive
uncertainty; the ADR-302 `DomainState` (KNOWN / DEGRADED / UNKNOWN) gate
result, so a low-confidence or out-of-distribution inference is recorded as
such, not silently promoted.
- **independent corroboration** — the phase-2 ADR-303 agreement link
(a reference/second modality that agreed or disagreed); absent in phase 1,
the stage records "no corroboration," never a fabricated one.
- **spatial state** — the ADR-306 ontology `Observation`/`Track`/`Event` the
inference updated, carrying `SemanticProvenance` and its `EvidenceLevel`.
- **policy decision** — the governed action taken (or withheld), with the
certificate (ADR-318) it relied on.
- Each stage carries exactly one `EvidenceLevel` (L0L5, ADR-282); the envelope's
effective level is the **minimum** across its stages — a synthetic root or an
unreferenced inference caps the whole chain, so the chain cannot claim more
than its weakest link.
### 2. Signing and anchoring
- Each stage is signed with RuField signature types (ADR-305 /
ADR-260/262/277/279) over the canonical serialization of that stage plus the
hash of the prior stage, so the chain is tamper-evident end to end and any
broken link is detectable. The completed chain is anchored in an append-only,
witness-anchored ledger following the AetherArena pattern (ADR-149); it is the
same anchoring ADR-301/ADR-304/ADR-317/ADR-318 write into.
- The chain is **append-only**: a correction is a new chain referencing the
prior one, never an in-place edit (mirroring ADR-304 and CLAUDE.md's "source
over summaries").
### 3. Offline re-verification
- A consumer with the enrolled public keys (ADR-305) can re-verify a chain
offline: check each stage signature, check each prior-stage hash, and read the
per-stage confidence and evidence level — answering "why do you believe this?"
without trusting the emitting host. This is the property store-and-forward
channel authentication (rejected in ADR-305) cannot provide.
### Provenance and honesty discipline
- The witness chain never manufactures confidence: a stage that lacks evidence
records the absence. A `Synthetic` root, a missing corroboration, or an
UNKNOWN gate is carried faithfully and caps the chain's evidence level. No
accuracy number is invented here; the chain records the numbers the other
primitives produce at their honest level.
## Consequences
- Every RuView output becomes contestable and auditable: a downstream physical-AI
system can inspect the reasoning, weight it by per-stage confidence, and reject
a chain whose weakest link is too weak — the defining property of epistemic
infrastructure the strategic assessment asked for.
- The certificate spine (ADR-301/301/314/315) gains a single anchoring substrate;
each of those artifacts is a specialization of a witness record rather than a
bespoke signed blob.
- Carrying and signing a staged envelope adds per-observation size and CPU cost;
bounded by reusing RuField signatures and the existing ledger, and by the
minimum-level rule keeping the object honest rather than exhaustive.
- The chain will frequently reveal weak links (synthetic root, no corroboration,
DEGRADED gate). Surfacing that is the point; the envelope must never smooth a
weak stage into a confident summary.
## Validation
- `cargo test` on the witness-chain crate — stage-by-stage signature round-trip
and tamper rejection (a mutated stage or a broken prior-stage hash fails
verification); effective evidence level equals the minimum across stages; a
`Synthetic` root caps the chain and cannot present as `LiveVerified`
(ADR-295 invariant); an UNKNOWN gate (ADR-302) and a "no corroboration" stage
are recorded faithfully; append-only correction produces a new chain
referencing the prior one.
- Cross-ADR: an ADR-305 signed frame lineage serializes into a chain that
re-verifies offline with only the enrolled public keys; ADR-301/301/314/315
artifacts anchor into the same ledger.
- Real-deployment chains (from live ESP32 captures with ADR-303 corroboration)
are the maturity milestone and require hardware evidence per CLAUDE.md; a
chain rooted in synthetic input is L0 by construction.

View File

@@ -0,0 +1,150 @@
# ADR-320: RuView sensor HAL — abstract all sensing hardware to one Observation type
- **Status**: Accepted — initial implementation (ADR-300 phase 2)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: hal, sensor-abstraction, ontology, fusion, adapters, category, phase-2
## Context
This ADR is primitive 20 of the perception-substrate program (ADR-300) and a
phase-2 integration primitive; it is authored as **Proposed**. In the ADR-300
DAG it **consumes the canonical spatial ontology (ADR-306)** — its output is an
ontology `Observation` bound to a `Sensor` entity — and **feeds real sensor
fusion (ADR-311)**, which resolves many observations into one world state. It
closes the "identify the hardware" clause of the ADR-300 acceptance test that
phase 1 leaves open.
RuView's strategic ceiling is set by how tightly it is coupled to WiFi CSI.
Every new modality today lands as a bespoke ingest path with its own frame
shape, its own provenance handling, and its own place in the pipeline. That is
the difference between "a WiFi-DensePose project" and "an open
spatial-intelligence operating layer": the category changes the moment *any*
sensing hardware — {CSI, 802.11bf, BLE, UWB, mmWave, acoustic, camera, lidar,
IMU, custom} — enters through one abstraction and becomes one `Observation`
feeding one world model.
Crucially this is a *unification*, not a green field. Adapters already exist and
must be reused, not rebuilt:
- ADR-279's native RF frame contract (`RfFrameV2`) already unifies ESP32,
Intel, Atheros, PicoScenes, Realtek radar, and 320 MHz 802.11bk producers as
`RfFrameV2` producers into a shared latent — "lightweight per-device adapters
into a shared latent, not a shared tensor." The HAL generalizes that lesson
beyond RF.
- Existing CSI adapters (ESP32/Nexmon/FeitCSI paths), the mmWave fusion path
(ADR-063), and the multistatic WiFi path (ADR-029) are concrete producers to
bring under one trait.
- ADR-305 already authenticates a `Sensor`/`DeviceId`; ADR-306 already defines
`Sensor`, `Observation`, `Track`, and `Event` as first-class node types. The
HAL is the trait that turns a heterogeneous device into that authenticated
`Sensor` emitting those `Observation`s.
The gap is a single **`SensorHal` trait and one `Observation` type** that every
modality implements, so the world model never sees a modality-specific frame —
only a provenance-bearing, evidence-labelled `Observation`.
## Options considered
1. **Continue adding per-modality ingest paths.** Rejected: O(modalities) bespoke
pipelines, each re-encoding provenance and evidence, each a place the ladder
can be dropped — and it keeps RuView categorically a WiFi project.
2. **Force every modality into the ADR-274/279 RF tensor/frame.** Rejected: the
ADR-279 lesson is precisely that premature canonicalization discards
information (bandwidth, antenna structure, phase). A camera, lidar, or IMU
has no meaningful `RfFrameV2` projection; forcing one is the same mistake at a
larger scale.
3. **Define a `SensorHal` trait producing one `Observation` type, with existing
adapters as implementations feeding a shared latent and the ADR-306
ontology.** Chosen.
## Decision
Introduce a **`SensorHal` trait** and a single **`Observation`** type. Every
sensing modality is an implementation of the trait; the world model consumes
only `Observation`s.
### 1. The `SensorHal` trait
- A `SensorHal` describes a device's **capabilities** (which phenomena it can
sense — reusing the ADR-305/ADR-141 `CapabilityAttestation`), its **native
frame** (kept native, not canonicalized, per the ADR-279 shared-latent
lesson), and a method that lifts a native frame into an `Observation`.
- Implementations wrap the existing producers: CSI (ESP32/Nexmon/FeitCSI via the
ADR-279 `RfFrameV2` path), 802.11bf (ADR-310, phase 2), BLE, UWB, mmWave
(ADR-063), acoustic, camera, lidar, IMU, and `custom`. RF modalities reuse the
ADR-279 per-device latent adapters wholesale; the HAL adds the non-RF and
ranging modalities under the same trait.
- The trait is the boundary where untrusted hardware input is validated
(CLAUDE.md: validate at every hardware/FFI boundary; default to least
authority). A device is authenticated as an ADR-305 `Sensor` before its
observations are trusted.
### 2. The `Observation` type
- One provenance-bearing `Observation`: a measurement plus its `SensorHal`
source descriptor, its ADR-305 authenticated `DeviceId`, its ADR-295
`SourceState`, its native-frame reference (not a lossy projection), and
exactly one `EvidenceLevel` (L0L5, ADR-282). A camera-derived `Observation`
and a CSI-derived `Observation` are the same type with different provenance —
and a camera observation never lifts WiFi output to camera-grade; each carries
its own honest evidence level (CLAUDE.md: never present WiFi sensing as
camera-grade).
- The `Observation` maps directly onto the ADR-306 ontology `Observation` node
attached to its `Sensor`, so the ontology is the one representation and the
HAL is its ingest funnel.
### 3. Feeding fusion
- Observations from any set of modalities flow into ADR-311 fusion, which
resolves them into one probabilistic world state. The HAL guarantees fusion
never sees a modality-specific frame — only `Observation`s with uniform
provenance and evidence — which is what makes ADR-311's "many observations →
one world state" invariant implementable across heterogeneous hardware.
### Category and honesty discipline
- This ADR changes RuView's category from a WiFi-DensePose pipeline to an open
spatial-intelligence operating layer, but it makes **no accuracy claim**: the
HAL delivers a uniform ingest boundary, not a detector. Any capability of a
newly-connected sensor is still gated by ADR-302 and certified by ADR-318 for
its specific environment — connecting a camera does not grant a validated
capability by itself.
- Hardware support for a given modality is CLAIMED until demonstrated on real
silicon with captured evidence per CLAUDE.md; a passing trait test proves the
abstraction, not a fielded device.
## Consequences
- New sensing hardware lands as one `SensorHal` implementation instead of a
bespoke pipeline; the translation matrix stays O(modalities), mirroring how
ADR-306 collapsed the surface matrix.
- The ADR-300 acceptance clause "identify the hardware" becomes implementable:
a new sensor type is described by its HAL, authenticated as an ADR-305
`Sensor`, calibrated (ADR-301), gated (ADR-302), and certified (ADR-318)
through the same phase-1 spine, closing the last open clause.
- A trait boundary and an `Observation` type are added; existing RF adapters
are re-expressed as implementations rather than rewritten, preserving the
ADR-279 native-frame/shared-latent design.
- Non-RF modalities (camera, lidar, acoustic) enter the governed plane with the
same provenance and privacy discipline as RF; a camera is not a privacy-free
shortcut — it inherits the ADR-277 governance and its own evidence level.
- As a phase-2 Proposed ADR, the trait shape may be revised as ADR-311 fusion
and ADR-310 802.11bf land; that revision is expected for a phased program.
## Validation
- `cargo test` on the HAL crate (design-time, Proposed) — a fixture `SensorHal`
for each of at least two modalities (CSI via ADR-279, plus one non-RF)
produces uniform `Observation`s; every `Observation` carries a `DeviceId`,
`SourceState`, native-frame reference, and exactly one `EvidenceLevel`; a
synthetic source yields L0/`Synthetic` and cannot alias to measured
(ADR-279 invariant 6); an unauthenticated device's observations are rejected
at the trait boundary (ADR-305).
- Cross-ADR: an `Observation` maps round-trip to an ADR-306 ontology
`Observation` node with no provenance loss, and a set of `Observation`s from
distinct modalities is accepted by an ADR-311 fusion fixture.
- Real-silicon evidence is required before any modality's hardware support is
claimed beyond CLAIMED: a captured boot/runtime log from the real device
emitting `Observation`s. A successful build or simulator run is not hardware
evidence (CLAUDE.md).

View File

@@ -0,0 +1,101 @@
# ADR-321: Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, and evidence
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: policy, authorization, safety, certificates, governed-action, phase-1
## Context
The perception substrate (ADR-300) makes RuView state *what it knows* and
*how well* — the capability certificate (ADR-318) binds hardware, environment,
model, calibration, metrics, expiry, and evidence level. But a certificate is a
statement of knowledge, not a grant of action. The same certificate that is
adequate to dim a light is wholly inadequate to release a door lock or clear an
industrial stop condition.
Without an explicit authorization layer, every consumer re-implements its own
(inconsistent, usually optimistic) rule for "is this good enough to act on,"
and a confident-but-out-of-domain inference can reach an actuator. That is the
exact failure the substrate exists to prevent. Decision policy therefore
belongs in **phase 1**, alongside the certificate it gates, not later.
This ADR realizes program invariant #1 (UNKNOWN is a first-class output, never
an error) and the action-side of the refined acceptance test: a drift-
invalidated capability must be *denied at the actuator* before a false
confident inference is acted upon.
## Decision
Introduce a `ruview-policy` crate providing an **action authorization gate**
that sits between governed spatial state and any actuator.
### 1. Assurance requirements per action
An `ActionClass` declares the assurance an action demands:
- `min_certificate_class` — the required `CapabilityCertificate` class (ADR-318).
- `max_certificate_age` / `min_domain_freshness` — the certificate must be
currently valid **and** the live domain signature (ADR-302) must not be in a
DEGRADED/UNKNOWN state (this is the staleness guard, program invariant on
certificate conditionality — see ADR-300).
- `max_uncertainty` — inference uncertainty ceiling.
- `min_evidence_level` — the L0L5 floor (ADR-282/ADR-304); e.g. a safety
action may require ≥ L3 (held-out room+subject validation).
Reference action classes (illustrative, configurable):
| Class | Example | Typical floor |
|---|---|---|
| `Convenience` | lighting, scenes | tolerant: L1+, higher uncertainty ok |
| `Security` | alerts, arming | stricter: valid cert, L2+, bounded uncertainty |
| `SafetyCritical` | door lock, machine stop | strict: fresh cert, L3+, low uncertainty, KNOWN domain only |
### 2. The authorization decision
`authorize(action, capability_certificate, live_state) -> Authorization` where
`live_state` carries the current `SourceState` (ADR-295), OOD/domain state
(ADR-302), and inference uncertainty. Rules:
- **Fail-closed.** Any unmet condition → `Deny { failed_condition }`. The denial
names the *specific* condition (expired cert, domain DEGRADED, uncertainty
over ceiling, evidence below floor, certificate class too low).
- **UNKNOWN denies high-assurance actions.** A domain in UNKNOWN (ADR-302)
cannot authorize `Security`/`SafetyCritical` actions; it may still authorize
`Convenience` if that class's policy permits, but the authorization records
that it proceeded under UNKNOWN.
- The decision is a **pure function** of (action class, certificate, live
state) — deterministic and unit-testable without a clock or actuator.
- Every authorization (allow or deny) is emitted as the terminal stage of the
witness chain (ADR-319), so "why was this actuator allowed/denied" is
auditable end-to-end.
### 3. No silent optimism
A missing certificate, an expired certificate, or an unrecognized action class
all deny by default. Absence of a policy is not permission.
## Consequences
- Action authorization becomes uniform and centrally reasoned instead of
per-consumer and optimistic; this is the "RuView Certify → constrains action"
boundary that is hard to commoditize.
- A behavior change for existing automations that acted directly on presence:
they now pass through the gate. Convenience-class defaults keep low-stakes
automations working; high-stakes actions must opt into stricter classes.
- Depends on ADR-318 (certificate), ADR-302 (domain/OOD state), ADR-295
(source state), ADR-304 (evidence). Built in the phase-1 dependent wave after
those types land.
## Validation
- Unit tests: each action class authorizes/denies correctly across the matrix
of (valid/expired/degraded cert × KNOWN/DEGRADED/UNKNOWN domain × uncertainty
above/below ceiling × evidence above/below floor); UNKNOWN denies
safety-critical; every deny names its failed condition; absence-of-policy
denies; determinism.
- Integration: the acceptance-test scenario (ADR-300) — post-certification room
change drives domain to DEGRADED→UNKNOWN, and a `SafetyCritical` authorization
is denied with `failed_condition = domain_not_known` *before* the inference
reaches the actuator, witness chain preserved.
- `cargo test -p ruview-policy`.

View File

@@ -0,0 +1,364 @@
# ADR-323: Native Rust physics-constrained pose refinement
- **Status**: Proposed
- **Date**: 2026-08-15
- **Deciders**: ruv
- **Owners**: RuView perception and edge runtime maintainers
- **Tags**: pose, physics, rust, uncertainty, provenance, abstention, edge
- **Numbering note**: ADR-323 is the next free number in the authoring checkout. Re-run the ADR index/collision check immediately before merge and rename if needed.
- **Extends**: ADR-020, ADR-027, ADR-079, ADR-101, ADR-135, ADR-145, ADR-150, ADR-273, ADR-279, ADR-282, ADR-295, ADR-296, ADR-297, ADR-298, ADR-302, ADR-303, ADR-304, ADR-305, ADR-306
- **Supersedes**: None
## Executive decision
RuView will add a clean-room native Rust boundary between RF pose inference and
semantic publication. It will preserve the immutable RF observation, publish a
physics assessment, optionally produce a bounded corrected candidate, and
abstain when required evidence is absent. It must never increase observational
confidence merely because a pose is physically plausible.
Three independently gated layers are adopted:
1. A deterministic kinematic auditor and bounded covariance-weighted projector
using Rust and `nalgebra`.
2. An optional articulated-body dynamics auditor using `rapier3d`.
3. A later optional supervised residual model using Burn.
The first production milestone is deterministic audit. It is not a GRIP port,
not PPO, and not evidence that the current pose observer is production-ready.
## Context
ADR-101's committed Cog emits 17 COCO keypoints as normalized 2D coordinates.
Its model has no per-joint uncertainty head and publishes a constant confidence.
The sensing server also contains renderer-oriented EMA and bone clamping. These
surfaces cannot establish metric 3D physics and can make weak evidence look
more convincing.
Pose output can violate bone length, floor, velocity, acceleration, and temporal
continuity constraints. Downstream consumers also cannot reliably distinguish
observed coordinates from derived correction. The rejected premise is:
"physically plausible means more likely correct." Plausibility is only a prior;
many incorrect poses are plausible.
GRIP is architectural inspiration for an observer/controller split, but it
observes four wearable IMUs and pressure insoles and drives a simulator. RuView
observes RF, so GRIP weights are not input-compatible. External code, weights,
simulators, and datasets require independent license review and never enter the
runtime dependency graph by implication.
## Outcome and actors
For every accepted person track/timestamp, the engine returns exactly one
`PoseRefinementV1`, including off, timeout, rejection, and abstention paths:
- immutable `PoseObservationV2` content hash;
- constraint residuals and quality disposition;
- an optional bounded candidate and an explicit `selected` bit;
- a typed reason when correction is unavailable;
- model, calibration, configuration, and optional learned-artifact provenance.
The RF observer owns observations and calibrated uncertainty; tracking owns
identity stability; physics owns assessment/correction only; the sensing server
owns deadlines, modes, publication, and rollback; the evidence engine owns
release evaluation; clients choose raw/both/refined without silent fallback.
## Input and coordinate contract
Metric correction requires a monotonic nanosecond timestamp, session-scoped
track ID, sequence and sensor epoch, 17 ordered COCO joints in metric X/Y/Z,
per-joint positive-semidefinite covariance calibrated on held-out data, a
versioned right-handed Z-up room frame, a normalized upward floor plane, model
and calibration hashes, ADR-302 trust state, and authenticated/replay-protected
source provenance.
`Image2d` observations may be audited for image-plane ratios and continuity but
must never enter 3D projection/dynamics or be called physically corrected.
Unknown trust, missing calibration, missing uncertainty, stale/non-monotonic
input, non-finite values, invalid covariance, excessive tracks, and room-bound
violations fail to raw output with a typed reason.
## Public contracts
`wifi-densepose-core` owns `PoseObservationV2` and `PoseRefinementV1`; no
duplicate server/Cog contract is permitted. Public output remains COCO17. The
engine derives pelvis and thorax virtually and never labels them observed.
The raw content hash is deterministic and excludes its own hash field. The
idempotency key is `(sensor_epoch, sequence, track_id, raw_hash, config_hash)`.
An exact duplicate returns the cached result; same sequence with different
content is a replay rejection.
Contact is `hypothesis` unless a measured sensor and its provenance say
otherwise. Raw, derived, hypothesis, and unknown labels must survive every
projection.
## Confidence invariant
For upstream calibrated confidence `c_obs`, normalized residual `r`, and
normalized intervention `i`:
```text
c_physics = exp(-(beta_r * r + beta_i * i))
c_effective = min(c_obs, c_obs * c_physics)
0 <= c_effective <= c_obs <= 1
```
Only a separately witnessed multimodal fusion contract may increase fused
confidence.
## Deterministic projector
The default `kinematic` feature has no Rapier, Burn, ONNX, libtorch, Python,
CUDA, or network dependency. Per bounded iteration it:
1. projects observed parent/child distances toward anonymous track-scoped
bone-length posteriors;
2. applies broad joint/trunk validity checks without an upright prior;
3. bounds temporal motion and resets derivatives after gaps;
4. resolves floor penetration only, allowing seated, kneeling, prone, child-
scale, mobility-aid, and genuine-fall poses;
5. recomputes residuals and stops below epsilon.
Initial operator-owned caps are four iterations (hard maximum eight), 0.20 m
single-joint correction, 0.10 m root correction, 250 ms derivative gap, 500 ms
track reset, ten known joints, a 100 m metric room bound, a separate 16,384
image-coordinate audit bound, and a 5 ms one-track Pi 5 p95 gate. Keeping image
and metric bounds separate prevents legitimate pixel observations from
weakening the physical room bound. A candidate over either correction cap is
discarded in full.
Bone posteriors are initialized only from high-confidence frames, anonymous,
memory-only, track-scoped, and deleted on expiry. Persistent personalization is
outside this ADR and requires consent/retention/deletion governance.
## Optional dynamics and learned layers
`dynamics` adds a process-owned Rapier humanoid and begins audit-only. Network
input may never provide Rapier snapshots, bodies, constraints, solver limits,
or arbitrary geometry. Dynamics approval is independent of kinematic approval.
`learned` uses first-party Burn 0.21 core/NN components without `burn-tch`
because this workspace already has a different native libtorch link.
`learned-cpu` adds the ndarray backend. The implemented two-layer GRU uses a
20-frame history and width 128 to predict bounded residuals, uncertainty,
foot-contact hypotheses, and abstention. Verified model records can be loaded
from bytes and executed natively; no trained artifact is shipped or approved.
The resolved Burn/CubeCL graph declares Rust 1.92, while the workspace file
pins Rust 1.89 and the authoring host provides Rust 1.91.1.
`--ignore-rust-version` is diagnostic evidence only: learned activation remains
blocked until an approved Rust 1.92 release-toolchain change builds it without
that override. Residuals are hard-clipped to deterministic caps and cannot
bypass validation or confidence monotonicity. PPO is deferred until measured
evidence identifies a failure supervised residual learning cannot address.
## Feature boundary
```text
default = kinematic
dynamics = rapier3d
learned = burn-core + burn-nn
learned-cpu = learned + burn-ndarray
learned-train = learned + burn-train
learned-wgpu = learned-train + burn-wgpu
learned-cuda = learned-train + burn-cuda
deterministic = rapier3d?/enhanced-determinism
```
The lockfile is release authority. The learned feature currently requires the
toolchain supported by Burn/CubeCL's resolved graph; this does not change the
default edge build.
## Runtime modes and API
Rollout is `OFF -> AUDIT -> SHADOW_CORRECT -> OPT_IN_CORRECT -> DEFAULT_CORRECT`.
Evidence permits forward transitions; any regression returns immediately to
audit/off. Correct selection additionally requires authenticated sensor
identity and replay protection from ADR-305. High model confidence cannot
override missing source authentication.
Existing pose fields stay unchanged and raw remains the migration default:
```text
GET /api/v1/pose/current?view=raw
GET /api/v1/pose/current?view=both
GET /api/v1/pose/current?view=refined
```
Refined-only returns HTTP 409 with `pose_refined_unavailable` when no selected
candidate exists. It never silently returns raw labeled refined.
## Security, privacy, and availability
All frames, model output, geometry, and pre-verification artifacts are
untrusted. Calibration/config/model artifacts become trusted only after signed,
hash-addressed verification and atomic activation. Runtime inference performs
no model retrieval or other network access.
Fixed arrays/caps, bounded iterations, a maximum track count, room geometry
limits, deadlines, and track expiry constrain denial of service. Timeout drops
partial refinement, never raw publication. Backpressure retains the newest raw
frame per track, drops intermediate refinement work, resets derivatives after
250 ms, and never extrapolates beyond 500 ms.
Metrics contain only allowlisted aggregate scalars: mode/disposition/reason,
stage latency, iterations, maximum correction, residuals, confidence delta,
track resets, invalid input, timeout, and raw/refined divergence. They exclude
joint arrays, body dimensions, room coordinates, CSI, and persistent person
identifiers. Bone/gait state is memory-only and excluded from logs.
Refined output is not a sole medical, emergency, industrial-safety, or
autonomous-control source. A real fall is valid state and must never be made
upright to stabilize a simulator.
## Threat model summary
| Threat | Primary control | Residual risk |
|---|---|---|
| Spoofed/replayed sensor | ADR-305 identity, MAC, sequence and replay window; correction gate | Compromised legitimate sensor |
| Altered model/floor/config | Signed hashes, authenticated configuration, atomic activation | Authorized unsafe configuration |
| Poisoned data/splits | Immutable manifests, strict split validator, witnessed benchmarks | Subtle label poisoning |
| Operator repudiation | Append-only witnessed transition with actor/old/new hash/reason | Compromised signer |
| Biometric/log leakage | Track-local retention and fixed metric allowlist | Aggregate inference |
| Track/geometry CPU flood | Authentication, cardinality/geometry/allocation/deadline caps | Valid dense-scene overload |
| Remote mode escalation | Capability-scoped local control plane, deny by default | Compromised operator capability |
| Derived output relabeled observed | Required schema/provenance and signed event envelope | Malicious downstream stripping |
The implementation review records commit, lockfile hash, Rust toolchain,
scanner versions, and advisory-feed timestamp.
## Evidence protocol
Evidence levels are L0 deterministic synthetic, L1 public measured replay, L2
controlled RuView RF plus optical truth, L3 subject/room/hardware/session-
disjoint RuView, L4 privacy-safe shadow fleet aggregates, and L5 independent
vertical validation outside this ADR.
No sequence, contiguous take, subject, room, or calibration session may cross
train/test for the generalization gate. Preprocessing, body priors, and
uncertainty calibration fit training data only. Reports include raw observer,
renderer smoothing, audit, deterministic correction, dynamics audit, and
learned residual on identical observations, plus empty-room, prone/fall,
missing-joint, and OOD subsets.
Primary metrics are 3D MPJPE, declared-threshold PCK, per-joint error, foot
slide, floor penetration, jerk, uncertainty calibration, abstention coverage,
and selective risk. Learned runs use at least five fixed seeds and report mean,
median, standard deviation, and 95% bootstrap intervals. All frames count;
selective metrics report risk and coverage.
## Acceptance gates
- **G0 contract**: real metric 3D/covariance output, round-trip raw hash,
versioned frame/floor, 2D compatibility, non-stub observer, ADR-298 artifact
sanity, and the ADR-079 PCK@20 >=35% gate or adopted successor. The current
committed Cog does not pass G0, so correction remains unavailable.
- **G1 deterministic audit**: property/fuzz tests, deterministic hashes per
platform class, 24-hour accelerated replay without panic/growth, Pi 5 p95
<=5 ms, and universal confidence monotonicity.
- **G2 shadow correction**: strict-disjoint measured median MPJPE improvement
>=10% with positive 95% CI lower bound; foot slide >=30% and jerk >=25%
better; no joint median >5 mm worse; fall/prone sensitivity change <=2 pp;
>=95% corrections below 0.10 m; every correction above 0.20 m abstains.
- **G3 opt-in**: >=30 subjects, 10 rooms, 3 hardware configurations, and 3
independent sessions/room; UNKNOWN never selected; confidence monotonic;
live disable; REST/WebSocket/MQTT/Home Assistant/replay compatibility.
- **G4 default visualization only**: 30 shadow days under 0.1% timeout/internal
error, no open severity 1/2 incidents, and gates still valid for current
model/calibration.
Dynamics and learned engines each repeat G2-G4; approval is not inherited.
## Testing and completion evidence
Unit/property/fuzz/integration/security coverage maps to requirements R1-R13:
raw hash, confidence, modes, malformed/stale/frame/covariance input, caps and
deadlines, provenance, dependency graph, pose diversity/fall preservation,
strict splits, fail-to-raw faults, no network capability, and authenticated
source/replay selection.
Release commands include focused core/physics tests, default/dynamics/learned
feature checks, format/clippy, benches, `cargo deny`, `cargo audit`, strict split
verification, and golden replay verification. Completion also requires JSON
schemas, measured Pi 5/x86 rows, strict manifest hashes, raw/refined metrics,
SBOM/license report, rollback drill, and residual-risk owners. Missing measured
or operational evidence leaves status Proposed and runtime in audit.
## Rollback
Rollback is an authenticated mode transition to audit/off, not a binary
downgrade. Stop selection immediately, keep raw publication and disposition
records, discard track state, and retain only aggregate incident metrics plus
signed configuration history. Failed artifact activation leaves the previous
engine atomically active. Additive schemas remain; refined-only callers receive
the typed unavailable response.
## Consequences
### Positive
- Explicit anti-hallucination and provenance boundary after RF inference.
- Reusable native Rust consistency primitive with measurable abstention.
- Python/CUDA remain absent from the production default.
- Cross-modal teacher data remains possible without wearable runtime inputs.
### Negative
- Full value requires a real metric 3D observer and calibrated uncertainty.
- Stateful tracks add latency/memory; optional backends add supply-chain surface.
- A constrained but wrong pose can look more credible.
- Strict data collection costs more than the software implementation.
### Neutral
- This ADR does not improve RF observability or current weight evidence.
- Existing 2D consumers continue to function.
## Implementation phases
P0 contracts/schemas; P1 deterministic audit; P2 bounded shadow correction; P3
server/Cog publication and evidence ledger; P4 Rapier audit; P5 Burn residual
training/inference. Code may land ahead of evidence, but runtime authority
advances only through the gates above.
## Implementation status at proposal
- P0-P3 are implemented on this branch: canonical contracts, strict schemas,
deterministic audit/projection, authenticated correction receipts,
idempotency, bounded track state, latest-frame backpressure, additive HTTP
and WebSocket publication, live legacy-2D audit, privacy-safe metrics, golden
replay, and strict-split checks.
- P4 is implemented as an optional persistent per-track Rapier dynamics auditor
and remains audit-only pending independent G2-G4 evidence.
- P5 inference architecture, artifact verification, serialization, and native
CPU execution are implemented. Training data, a signed trained artifact, and
G2-G4 accuracy/calibration evidence do not exist, so the layer has no runtime
selection authority. Its resolved Rust 1.92 requirement is also an explicit
activation blocker on the current Rust 1.91.1 release host.
- The live Cog honestly emits `Image2d`, degraded trust, and uncalibrated
uncertainty. It can be audited but cannot be selected for 3D correction.
G0 therefore remains open until an independently released metric-3D observer
with calibrated covariance is integrated.
- Local x86 latency and synthetic contract checks are recorded in the append-
only evidence ledger. Pi 5 measurements, 24-hour replay, 100-million-case
fuzzing, held-out RF/optical accuracy, fleet shadowing, and vertical safety
validation remain release evidence gates rather than software claims.
## References
- [GRIP project](https://ryosukehori.github.io/grip-project/)
- [GRIP paper (arXiv:2603.16233)](https://arxiv.org/abs/2603.16233)
- [Rapier documentation](https://docs.rs/rapier3d/)
- [Burn documentation](https://docs.rs/burn/0.21.0/burn/)
- [ADR-020](./ADR-020-rust-ruvector-ai-model-migration.md)
- [ADR-079](./ADR-079-camera-ground-truth-training.md)
- [ADR-101](./ADR-101-pose-estimation-cog.md)
- [ADR-150](./ADR-150-rf-foundation-encoder.md)
- [ADR-273](./ADR-273-unified-rf-spatial-world-model.md)
- [ADR-279](./ADR-279-native-rf-frame-contract.md)
- [ADR-298](./ADR-298-model-release-sanity-gates.md)
- [ADR-302](./ADR-302-out-of-distribution-detection.md)
- [ADR-303](./ADR-303-ground-truth-synchronization.md)
- [ADR-304](./ADR-304-evidence-engine.md)
- [ADR-305](./ADR-305-authenticated-sensor-identity.md)
- [ADR-306](./ADR-306-canonical-spatial-ontology.md)

View File

@@ -145,6 +145,41 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
| [ADR-287](ADR-287-coherent-wideband-rf-tomography-crate.md) | `wifi-densepose-sar` — coherent wideband RF tomography research crate | Accepted (implemented, published) |
| [ADR-285](ADR-285-homecore-wasm-first-metaharness.md) | WASM-first Homecore developer metaharness via `npx homecore` | Accepted (implemented and validated) |
| [ADR-286](ADR-286-wifi-densepose-sar-harness-via-metaharness.md) | `wifi-densepose-sar-harness` — MetaHarness with darwin/router/flywheel | Accepted (implemented, published) |
| [ADR-288](ADR-288-veil-privacy-shield-compliant-waveform.md) | VEIL — compliant-waveform privacy shield against unauthorized WiFi sensing (`wifi-densepose-privshield`) | Proposed (implemented, P1 reference) |
| [ADR-289](ADR-289-wifi-densepose-privshield-harness-via-metaharness.md) | `wifi-densepose-privshield-harness` — npm MetaHarness for the VEIL crate (guidance/router/flywheel) | Proposed (implemented, P1) |
| [ADR-290](ADR-290-veil-e2e-hardware-implementation-program.md) | VEIL end-to-end hardware implementation program — portable C core + multi-provider firmware scaffolds (openwifi/openwrt/nexmon/esp32) | Proposed (P4 scaffolding; C core host-validated) |
| [ADR-291](ADR-291-public-benchmark-evaluation-harness.md) | Public-benchmark evaluation harness — Widar3.0 ingest, split protocols, leakage guards | Accepted (initial implementation) |
| [ADR-292](ADR-292-wideband-80211ax-csi-ingest.md) | Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter, subcarrier-agnostic plumbing | Accepted (initial implementation) |
| [ADR-293](ADR-293-vitals-ground-truth-rig.md) | Vitals ground-truth rig — reference ingest, alignment, agreement metrics | Accepted (initial implementation) |
| [ADR-294](ADR-294-wifi-veil-integration.md) | WiFi Veil integration — emission-shaping countermeasure as advisory BFLD dependency | Accepted (initial implementation) |
| [ADR-295](ADR-295-source-provenance-state-machine.md) | Source provenance state machine — synthetic can never present as live | Accepted (initial implementation) |
| [ADR-296](ADR-296-sensor-data-plane-bind-hardening.md) | Sensor data-plane hardening — UDP bind control and source allowlist (step one) | Accepted (initial implementation) |
| [ADR-297](ADR-297-multi-node-semantic-correctness.md) | Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state | Accepted (initial implementation) |
| [ADR-298](ADR-298-model-release-sanity-gates.md) | Model release sanity gates — block degenerate and mislabeled model artifacts | Accepted (initial implementation) |
| [ADR-299](ADR-299-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) |
| [ADR-300](ADR-300-perception-substrate-program.md) | RuView perception substrate — phased 21-primitive program (calibration, evidence, trust, deployment) | Accepted (program; children ADR-301..317) |
| [ADR-301](ADR-301-automatic-domain-calibration.md) | Automatic domain calibration — signed, versioned, invalidatable room fingerprint | Accepted (phase 1) |
| [ADR-302](ADR-302-out-of-distribution-detection.md) | Out-of-distribution detection — KNOWN / DEGRADED / UNKNOWN gating | Accepted (phase 1) |
| [ADR-303](ADR-303-ground-truth-synchronization.md) | Ground-truth synchronization — reference sensors as a formal validation plane | Proposed (phase 2) |
| [ADR-304](ADR-304-evidence-engine.md) | Evidence engine — per-(room,device,subject) accuracy ledger | Accepted (phase 1) |
| [ADR-305](ADR-305-authenticated-sensor-identity.md) | Authenticated sensor identity — RF chain of custody | Accepted (phase 1) |
| [ADR-306](ADR-306-canonical-spatial-ontology.md) | Canonical spatial ontology — one Site→…→Event model for every surface | Accepted (phase 1) |
| [ADR-307](ADR-307-persistent-identity-tracking.md) | Persistent identity & tracking — privacy-preserving probabilistic tracks | Proposed (phase 2) |
| [ADR-308](ADR-308-sensor-placement-optimizer.md) | Sensor placement optimizer — floorplan + inventory → recommended positions | Proposed (phase 3) |
| [ADR-309](ADR-309-active-sensing.md) | Active sensing — closed-loop RF experiment control | Proposed (phase 3) |
| [ADR-310](ADR-310-80211bf-native-architecture.md) | 802.11bf-native architecture — standardized WLAN sensing as native measurement types | Proposed (phase 2) |
| [ADR-311](ADR-311-real-sensor-fusion.md) | Real sensor fusion — uncertainty-aware, multiple observations → one world state | Proposed (phase 2) |
| [ADR-312](ADR-312-long-term-spatial-memory.md) | Long-term spatial memory — learn the normal physics of a location | Proposed (phase 3) |
| [ADR-313](ADR-313-counterfactual-inference.md) | Counterfactual inference — generative spatial reasoning | Proposed (phase 3) |
| [ADR-314](ADR-314-information-gain-scheduler.md) | Information-gain scheduler — sample the most informative radios | Proposed (phase 3) |
| [ADR-315](ADR-315-digital-rf-twin.md) | Digital RF twin — persistent per-deployment RF model | Proposed (phase 3) |
| [ADR-316](ADR-316-fleet-control-plane.md) | Fleet control plane — provisioning to audit trails | Proposed (phase 2) |
| [ADR-317](ADR-317-benchmark-multi-domain-scorecard.md) | Multi-domain benchmark scorecard — regressions cannot hide behind pooled accuracy | Accepted (phase 1) |
| [ADR-318](ADR-318-capability-certificates.md) | Capability certificates — validated-for-this-environment claims | Accepted (phase 1) |
| [ADR-319](ADR-319-witness-chain.md) | Witness chain — staged, signed epistemic envelope | Accepted (phase 1) |
| [ADR-320](ADR-320-sensor-hal.md) | RuView sensor HAL — abstract all sensing hardware to one Observation type | Proposed (phase 2) |
| [ADR-321](ADR-321-decision-policy-action-authorization.md) | Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, evidence | Accepted (phase 1) |
| [ADR-323](ADR-323-native-rust-physics-constrained-pose-refinement.md) | Native Rust physics-constrained pose refinement | Proposed |
---

View File

@@ -0,0 +1,71 @@
# Physics pose refinement evidence ledger
ADR-323 performance and accuracy targets are gates, not measured claims. Append
rows; never replace prior measurements. Every row must identify the repository
commit, lockfile hash, Rust toolchain, target, engine/features, configuration
hash, corpus/split hash, command, sample count, and evidence label.
## Runtime measurements
| Date | Commit | Lock SHA-256 | Target/toolchain | Engine/config | Tracks | Samples | p50 | p95 | p99/max | RSS delta | Evidence | Reproducer |
|---|---|---|---|---|---:|---:|---:|---:|---:|---:|---|---|
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | deterministic kinematic shadow, config `ef3cf581f75124c1d45a8d6bedcef32e4d1bacb39ee0dfcd4e520171fda2d8cf` | 1 | 20,000 | 0.0080 ms | 0.0097 ms | 0.0195/0.5465 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | `cargo run --release -p wifi-densepose-physics --example latency_probe -- 20000` |
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | deterministic kinematic shadow after final local optimization, same config | 1 | 20,000 | 0.0075 ms | 0.0084 ms | 0.0117/0.1579 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | same release probe command |
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | final deterministic kinematic shadow, config `a44dc696234f31eda54cd4b436bc2d2c69b9638565b729ac9f07435cedfd0dcc` | 1 | 20,000 | 0.0071 ms | 0.0084 ms | 0.0147/1.5994 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | same release probe command |
The probe measures a warm, one-track `PhysicsEngine::process` call. It excludes
transport, publication, resident-memory delta, dynamics, and learned inference.
It is not evidence for the Pi 5 gate.
Criterion separately measured `kinematic_one_track` at
`[11.911, 12.757, 14.069] us` across 100 samples (approximately 369,000 timed
iterations). That benchmark includes observation construction and canonical
hashing in the timed routine and uses fresh engine state; it is **MEASURED** on
the same local host and is not a percentile or Pi 5 claim.
## Accuracy measurements
| Date | Commit | Corpus/split | Variant | Coverage | MPJPE | PCK threshold/result | Foot slide | Jerk | Fall/prone delta | Evidence |
|---|---|---|---|---:|---:|---|---:|---:|---:|---|
No measured accuracy evidence has been recorded. The deterministic tests are
L0/SYNTHETIC contract evidence only and cannot satisfy G2.
## Validation and supply-chain record
- The default dependency graph is checked to exclude Burn, Rapier, Tch, and
ONNX Runtime. Dynamics and learned backends remain opt-in.
- Burn CPU serialization/inference tests pass on the authoring host only with
Cargo's `--ignore-rust-version`; the resolved CubeCL graph requires Rust 1.92.
The workspace file pins Rust 1.89 and the host provides Rust 1.91.1. This is
diagnostic, not release approval.
- `cargo audit 0.22.1` used RustSec database commit
`69f93cf294852cfa9b53751f4ca86de3283dd290` (feed timestamp 2026-08-12).
ADR-323 updates remove resolved advisories in `event-listener`, `rkyv`, and
`wasmtime`. The workspace still has five advisories in pre-existing
`quick-xml` and `rsa` dependency paths; the default physics graph contains
none of them. The optional Burn training graph includes yanked `spin 0.9.8`.
- `cargo-deny` is not installed on the authoring host, so the required license
and policy gate is not claimed complete.
- Strict Clippy passes with warnings denied for core/physics default and
dynamics builds, the diagnostic learned-CPU build, and the Cog itself with
dependency linting excluded. Focused core, physics, dynamics, learned, Cog,
sensing-server adapter/live-audit/HTTP, schema, golden, strict-split,
feature-boundary, and fuzz-build checks pass.
- The repository-wide rustfmt gate is already red across unrelated crates. The
sensing-server library has existing warning debt, and unscoped Cog Clippy is
blocked by existing `wifi-densepose-ruvector` warnings. The prescribed
`cargo test --workspace --no-default-features` did not reach a terminal result
in either a 904-second cold or 604-second warm serial run on this Windows
host. None of these broader gates is represented as green.
- The standalone fuzz lock SHA-256 is
`d386c4edb130bb6b2d1a4ef77334c78e25e0695e90a9d97c01284876acb8c2c6`.
## Required commands
```text
cargo bench -p wifi-densepose-physics
node scripts/pose-physics/verify-feature-boundary.mjs
bash scripts/verify-pose-physics-splits.sh <manifest.json>
bash scripts/replay-pose-physics-golden.sh <golden-results.jsonl>
```

View File

@@ -0,0 +1,141 @@
# 01 — State of the Art
Scope: what a passive or active adversary can extract about *who* is in a space
and *what they are doing* from WiFi, the standard that broadens that surface, and
the countermeasures that try to prevent it. Claims are tagged **MEASURED** (from
a primary source, with metric), **CLAIMED** (asserted without an independent
measurement), or analytical inference (flagged).
---
## 1. The attack surface: beamforming feedback (BFI)
Since WiFi 5 (802.11ac), a client (beamformee) measures the downlink channel,
compresses the steering matrix **V** into **Givens-rotation angles φ/ψ**, and
transmits them **in cleartext** so the AP can steer beams. Anyone in monitor
mode can capture these frames for *every* client simultaneously — no network
access, and the target need carry no device. Quantization is coarse (802.11ac
angle steps of π/4…π/32 rad) yet retains rich motion and body information.
| Work | Venue / year | Result | Label |
|---|---|---|---|
| **BFId** — identity inference from BFI | ACM CCS 2025 (KIT/KASTEL) | Re-identifies individuals from BFI alone; novel 197-person dataset. Press reports **99.5%** in a controlled study (ACM full text was not openable to confirm class count/split) | MEASURED (paper); 99.5% is CLAIMED via press |
| **LeakyBeam** — occupancy through walls | NDSS 2025 | Occupancy detection **TPR 82.7% / TNR 96.7%** at **20 m, through walls**, from plaintext BFI. Proposes a BFI-obfuscation defense | MEASURED (attack); defense overhead CLAIMED |
| **BFIAttack** — CSI reconstruction from BFI | arXiv 2026 (USF) | Reconstructs CSI from BFI, then defeats CSI defenses. ASR: device auth 95.5% / user auth 92.6% / key-gen 94.2% (single-antenna), 1.56 m | MEASURED |
| **BeamSense** — activity recognition from BFI | Computer Networks vol. 258, 2025 (Northeastern) | Human activity recognition **up to 99.28%** on commodity 802.11ac, no firmware mod, ~10% better than CSI | MEASURED |
| **Wi-BFI** — capture tooling | arXiv 2309.04408, 2023 | Pip-installable extraction of 802.11 BFI from commercial devices | tooling |
**Takeaway for the defender.** BFI is the highest-leverage surface: unencrypted,
management-plane, device-free, capturable en masse with off-the-shelf tools. It
is also a *stepping stone* — BFIAttack shows BFI can reconstruct the CSI that all
older attacks assume.
---
## 2. The older adjacent surface: CSI identity/gait/activity
CSI requires special extraction (Intel 5300 / Atheros / ESP32) but is the
foundation the BFI attacks build on. Person-ID exploits **gait** as a biometric.
Representative MEASURED results (commodity WiFi, CSI amplitude):
| System | Accuracy | N (candidates) | Note |
|---|---|---|---|
| WiWho (IPSN 2016) | 92%→80% | 2→6 | 23 m straight walk |
| WiFi-ID (2016) | 93%→77% | 2→6 | wavelet features |
| WiPIN (2018) | 92100% | ≤30 | operation-free |
| Deep-WiID (2019) | 92.599.7% | 6→15 | GRU |
| WiNet / LWID (2020) | 98.5% / 98.8% | 40 / 50 | CNN |
**Pattern the defender must exploit and not overstate:** accuracy is high in
small closed sets but *degrades as N grows and conditions become realistic*
(cross-day, cross-location, cross-walking-style). Chance is **1/N**; a 99% result
on N=5 is far weaker evidence than 99% on N=197. Open-world scale is largely
unproven (see *SoK: Security Evaluation of Wi-Fi CSI Biometrics*, 2025).
---
## 3. The standard: IEEE 802.11bf-2025
IEEE Std **802.11bf-2025** (Amendment 4: *Enhancements for WLAN Sensing*) was
published **26 September 2025**. It standardizes WLAN sensing in 17.125 GHz and
above 45 GHz, defining sensing capability signaling, measurement/sounding
setup, feedback types, and both passive (ambient-traffic) and active
(dedicated null-packet) sensing modes.
- **Attack-surface implication (analytical).** 802.11bf turns CSI/measurement
acquisition from proprietary hacks into open, vendor-agnostic, machine-readable
MAC signaling across heterogeneous devices — institutionalizing exactly the
measurements the BFI attacks abuse. The standard frames sensing as a feature,
not a threat.
- **The privacy gap (MEASURED from standards minutes).** A 2023 proposal for a
BFI "secure transmission mechanism" (IEEE 802.11-23/0782) was **withdrawn**;
"the group did not align on the characterization of [the] privacy problem."
The standard shipped without privacy protections, and its own analysis admits
passive eavesdroppers can extract location, respiration, heart rate, and
identity.
---
## 4. Countermeasures (the defense literature)
All operate on the defender's *own* transmissions; none are jamming.
| Countermeasure | Venue / year | Mechanism | Effect | Label |
|---|---|---|---|---|
| **IRShield** | IEEE S&P 2022 | IRS/reconfigurable surface randomizes reflected paths | Attacker motion-detection **≤5%** | MEASURED |
| **PhyCloak** | USENIX NSDI 2016 | Full-duplex obfuscator injects Doppler/phase distortion into sensing only | **88.69%** gesture-spoof; throughput can rise (whitelist legit sensors) | MEASURED (spoof); throughput CLAIMED |
| **DP-Givens dithering** | IEEE DySPAN 2026 | Differentially-private stochastic quantization of BFI φ/ψ angles | Attacker speed-class error 19%→~73% (chance); **fine (3-bit) resolution ≈ non-private baseline throughput** | MEASURED |
| **MIMOCrypt / WiShield** | 2023 / IEEE JSAC 2024 | Secret precoding / MIMO CSI manipulation so only the intended RX decodes | Anti-tracking | CLAIMED/formal |
| **CSI Fuzzing / DP feature release** | IEEE 202425 | Randomized CSI features with DP budget | Formal DP guarantee | CLAIMED/formal |
| **ScatterShield** | ACM IMWUT 2025 | Backscatter tags inject controlled clutter | Defeats unauthorized sensing | MEASURED |
| **Adversarial packet perturbation** | ACM MobiCom 2024 | Small in-spec packet perturbations degrade attacker model | Symmetric defense | MEASURED |
**The fundamental tradeoff (MEASURED, DySPAN 2026).** Perturbing precoding/
feedback that an attacker exploits also degrades legitimate beamforming gain —
*but the cost collapses at fine feedback resolution*:
| Randomization | Attacker error | Beamforming gain retained |
|---|---|---|
| none | 19% | 100% |
| moderate (p=0.3) | >50% | median >90% |
| maximum (p≥0.9) | ~73% (≈chance) | median ~58% |
At **high (3-bit) feedback resolution, privacy was "nearly indistinguishable
from the non-private baseline"** in link performance. This is the empirical basis
for VEIL's design choice (compliant fine-resolution feedback shaping — see
[03-countermeasure-design.md](03-countermeasure-design.md)).
---
## 5. Where VEIL sits
The literature has two families: **external** obfuscation (IRShield/ScatterShield
— extra hardware, perturbs the channel) and **transmitter-side** feedback/precoder
shaping (DP-Givens, MIMOCrypt — no extra hardware, perturbs your own report).
VEIL is in the second family and adds the missing property the others do not all
combine: a transform that is simultaneously **energy-preserving** (provably
compliant), **key-reversible** (throughput-preserving for the legitimate link),
and **session-fresh** (defeats cross-session re-identification), unified around
the Givens-rotation primitive the report already uses.
---
## Sources
- BFId — ACM CCS 2025: https://dl.acm.org/doi/10.1145/3719027.3765062 · KIT record: https://publikationen.bibliothek.kit.edu/1000185756
- LeakyBeam — NDSS 2025: https://www.ndss-symposium.org/ndss-paper/lend-me-your-beam-privacy-implications-of-plaintext-beamforming-feedback-in-wifi/
- BFIAttack — arXiv 2604.04179: https://arxiv.org/html/2604.04179v1
- BeamSense — Computer Networks 2025: https://dl.acm.org/doi/10.1016/j.comnet.2024.111020 · arXiv 2303.09687: https://arxiv.org/pdf/2303.09687
- Wi-BFI — arXiv 2309.04408: https://arxiv.org/pdf/2309.04408
- SoK: Security Evaluation of Wi-Fi CSI Biometrics — arXiv 2511.11381: https://arxiv.org/pdf/2511.11381
- WiWho (IPSN 2016): https://dl.acm.org/doi/10.5555/2959355.2959359 · WiPIN — arXiv 1810.04106: https://arxiv.org/pdf/1810.04106
- Survey on Wi-Fi Sensing for Human Identity — MDPI Electronics 2023: https://www.mdpi.com/2079-9292/12/23/4858
- IEEE Std 802.11bf-2025: https://standards.ieee.org/ieee/802.11bf/11574/ · Overview — IEEE COMST 2024: https://ieeexplore.ieee.org/document/10547188/ · NIST: https://www.nist.gov/publications/ieee-80211bf-enabling-widespread-adoption-wi-fi-sensing
- 802.11bf privacy proposal withdrawal (802.11-23/0782), summarized: https://pascalpiron.substack.com/p/wifi-sensing-and-the-privacy-fix
- IRShield — IEEE S&P 2022 / arXiv 2112.01967: https://arxiv.org/abs/2112.01967 · https://ieeexplore.ieee.org/document/9833676/
- PhyCloak — USENIX NSDI 2016: https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/qiao
- Protecting Human Activity Signatures in Compressed 802.11 CSI Feedback — DySPAN 2026 / arXiv 2512.18529: https://arxiv.org/abs/2512.18529
- MIMOCrypt — arXiv 2309.00250: https://arxiv.org/pdf/2309.00250 · WiShield — IEEE JSAC 2024: https://dl.acm.org/doi/abs/10.1109/JSAC.2024.3414597
- ScatterShield — ACM IMWUT 2025: https://dl.acm.org/doi/abs/10.1145/3770653
- Practical Adversarial Attack on WiFi Sensing — ACM MobiCom 2024: https://dx.doi.org/10.1145/3636534.3649367
- Privacy-Preserving Wi-Fi Data Generation via DP — INFOCOM 2025: https://www.eng.auburn.edu/~szm0001/papers/INFOCOM25.pdf

View File

@@ -0,0 +1,94 @@
# 02 — Threat Model
VEIL protects a physical space (a room, a ward, a boardroom, a SCIF) from
*unauthorized* WiFi-based inference of **who is present** and **what they are
doing**, without denying the space its own working WiFi. This file states the
adversary classes, exactly what VEIL defends, and — just as importantly — what
it does **not**.
---
## 1. Assets
| Asset | Why it matters |
|---|---|
| **Identity linkage** | Re-identifying a specific person across time/sessions from their RF signature (BFId-class attack) |
| **Occupancy / presence** | Whether the space is occupied, and by how many (LeakyBeam-class, through-wall) |
| **Activity / motion** | Gait, gestures, keystrokes, respiration inferred from channel dynamics (BeamSense-class) |
| **Communication utility** | The legitimate WiFi link must keep working (≥95% throughput bar) |
---
## 2. Adversary classes
| Class | Position | Capability | In VEIL scope? |
|---|---|---|---|
| **A1 — external passive sniffer** | Outside the trust boundary (adjacent room, van, hallway), monitor mode | Captures plaintext BFI/CSI for every station; runs BFId/LeakyBeam/BeamSense offline | **Primary target — yes** |
| **A2 — external active sensor** | Nearby, transmits its own probing/sounding to solicit measurable responses | Elicits sensing responses; 802.11bf "active" mode | **Partial** — cadence randomization + non-response policy help; full defense needs MAC-layer policy |
| **A3 — associated but curious AP** | Inside the link; the party VEIL shares keys with | Sees the un-rotated report by construction | **Out of scope** — this is BFLD's detection/privacy-class problem (ADR-118/141) |
| **A4 — supply-chain / firmware** | Compromised radio firmware | Can bypass any transmit-side control | Out of scope (integrity problem, not a waveform problem) |
| **A5 — physical / RF-denial** | Wants to *block* WiFi | — | Explicitly rejected: VEIL never jams |
VEIL's design centers on **A1**, the attacker the literature demonstrates and
the one no shipping product addresses.
---
## 3. What VEIL guarantees (and the evidence class)
1. **Cross-session identity unlinkability against A1.** Because the fine-subspace
signature is rotated by a fresh secret orthogonal transform each session, an
A1 attacker cannot average captures back to a stable per-person template.
*Evidence: SYNTHETIC — re-ID collapses from 100% to ~chance in the reference
experiment (`cargo test`); real-silicon witness is future work.*
2. **Communication preservation.** The transform is key-reversible by the
legitimate receiver, and acts only on the identity-bearing fine subspace, so
link throughput stays ≥95%. *Evidence: SYNTHETIC model + MEASURED external
corroboration (DySPAN 2026: fine-resolution feedback shaping is near-free).*
3. **Compliance.** The transform is orthogonal ⇒ energy-preserving ⇒ adds no
interfering emission ⇒ not jamming. *Evidence: machine-checked energy ratio =
1.000000 in the `compliance` module; statutory analysis in
[04-compliance-and-regulatory.md](04-compliance-and-regulatory.md).*
---
## 4. What VEIL does NOT do (non-goals, stated to prevent over-claiming)
- **It does not hide identity from the associated AP (A3).** That party holds the
session key. Protecting against a malicious AP requires detection and policy
(BFLD), not waveform shaping.
- **It is not RF denial or jamming.** It never degrades another station's link.
- **It does not, by itself, defeat within-session motion detection.** A single
session's rotation is fixed, so coarse presence/motion may still be inferable
within one capture window; sounding-cadence randomization mitigates but does
not eliminate this. Identity *re-ID* (the brief's metric) is the guaranteed
target; motion obfuscation is partial and tracked as future work.
- **It is not a camera-grade or medical-grade claim in any direction.**
- **It is not validated on hardware yet.** All quantitative defense results are
SYNTHETIC until a captured boot/runtime log exists (CLAUDE.md hardware rule).
---
## 5. Trust boundary
```
┌────────────────────── protected space ──────────────────────┐
│ │
│ [person] [person] legitimate STA ⇄ AP (VEIL) │
│ │ │ │ shares session key │
│ └──── RF ──────┘ │ rotates fine subspace│
│ reflections ▼ of its own BFI │
│ compliant, key-reversible, │
│ energy-preserving emission │
└───────────────────────────────────────┬──────────────────────┘
│ plaintext BFI on air
A1 external passive sniffer (monitor mode)
sees a freshly-rotated signature each session
→ cannot build a stable per-person template
→ re-identification → chance
```
The key never crosses the boundary to A1. The AP inside the boundary is trusted
for key-sharing (A3 out of scope). No emission crosses the boundary with intent
or effect of interfering with another station (A5 rejected).

View File

@@ -0,0 +1,136 @@
# 03 — Countermeasure Design
How VEIL prevents unauthorized sensing with compliant waveform controls, and how
the design maps to [`v2/crates/wifi-densepose-privshield`](../../../v2/crates/wifi-densepose-privshield).
---
## 1. The separable-subspace principle
A compressed beamforming report is not homogeneous. Two blocks carry different
information:
- **Dominant beam direction (comm block).** The coarse steering the AP uses to
aim data at the client. It varies with position and traffic and carries **no**
stable identity. **Throughput rides here.**
- **Fine cross-subcarrier phase structure (fine block).** The high-order
multipath detail. It is *stable per person* across sessions and is what
re-identification exploits (BFId). **Identity leaks here.** Communication
barely uses it.
The whole design rests on this: **identity leakage and data throughput live in
(mostly) separable subspaces.** A transform confined to the fine block can wreck
re-identification while sparing the beam the link depends on. This is consistent
with the DySPAN-2026 MEASURED result that shaping fine-resolution feedback is
nearly free in throughput.
---
## 2. The four compliant waveform controls
VEIL alters "channel sounding, phase, or beam schedules" — exactly the levers the
brief names — all within the 802.11 waveform envelope:
| Control | What it varies | Purpose |
|---|---|---|
| **Keyed precoder rotation** (primary) | A fresh secret orthogonal transform of the *fine* subspace each session, composed from extra Givens rotations | Destroys cross-session identity linkage; energy-preserving; key-reversible |
| **Feedback quantization / dither** | Sub-step noise on reported φ/ψ angles | Adds report-level uncertainty; tunes the privacythroughput point via `feedback_bits` |
| **Sounding-cadence randomization** | Jitter on NDP sounding intervals | Under-samples motion for an eavesdropper; charged as the throughput overhead |
| **MU-group / stream-mapping shuffle** | Which STAs are grouped, stream-to-antenna mapping | Rotates the spatial signature over time |
All four modify the node's **own** standards-conformant frames. None adds energy
on top of another station (see [04](04-compliance-and-regulatory.md)).
---
## 3. Why the keyed Givens rotation is the right primitive
The compressed beamforming report is *already* a product of Givens rotations
(the φ/ψ angles). VEIL composes **additional keyed Givens rotations** over the
fine block. This choice gives three properties at once:
1. **Orthogonal ⇒ energy-preserving.** A Givens rotation preserves the vector's
L2 norm exactly. Composing many still preserves it. So the emission carries
the same power it always would — **no added energy, no interference, not
jamming.** The `compliance` module checks this: energy ratio = 1.000000.
2. **Keyed & reversible ⇒ throughput-preserving.** The legitimate AP/STA shares
the per-session key, derives the identical rotation schedule, and applies the
inverse (negated angles, reversed order) to recover the true precoder. It pays
only the tiny residual from quantizing the extra angles at `feedback_bits`
resolution — negligible across the 802.11 59-bit range — plus the sounding
overhead. (The throughput-optimal resolution is derived in
[08-optimization.md](08-optimization.md).)
3. **Fresh per session ⇒ unlinkable.** A different rotation each session means an
A1 sniffer sees `R_e · signature` for a new random `R_e` every time. Averaging
over sessions (the natural enrollment attack) drives
`mean_e(R_e · signature) → 0` for *every* identity, so all templates collapse
toward the origin and become indistinguishable — re-identification → chance.
This is the marginalized-mutual-information argument: over unknown rotations,
the signature carries no stable discriminative information.
This is the shared-secret precoding idea (cf. MIMOCrypt) specialized to the
identity-bearing subspace and unified around the report's native primitive.
---
## 4. Detect-then-act
Per the brief ("detect sensing activity and alter…"), VEIL need not perturb
continuously. The `SensingDetector` exposes the decision rule: when the observed
rate of sensing/NDP solicitations crosses a threshold, the control plane
(ADR-280) engages the shield. Continuous operation is also valid; gating just
saves the (already small) overhead when no sensing is present.
---
## 5. Module map
| Concept above | Crate module | Key items |
|---|---|---|
| Deterministic, WASM-safe randomness + keys | `prng` | `Rng` (SplitMix64), `fnv1a_64`, `derive_key` |
| Givens algebra, energy conservation | `linalg` | `apply_givens`, `norm`, `dist_sq` |
| SYNTHETIC two-subspace BFI model | `identity` | `SceneConfig`, `Channel`, `BfiSample` (`comm()`/`fine()`) |
| The four controls (shield) | `protector` | `ShieldConfig`, `Protector::protect`/`recover`, `SensingDetector` |
| Passive re-ID adversary | `attacker` | `NearestCentroidAttacker`, `Metric` |
| Privacythroughput tradeoff | `throughput` | `LinkModel::throughput_ratio`, `beamforming_residual`, `feedback_airtime` |
| "Not jamming" audit | `compliance` | `ComplianceReport::audit`/`is_compliant` |
| Attacker-vs-protector head-to-head | `experiment` | `ExperimentConfig`, `run`, `ExperimentReport` |
| Config hyper-optimization | `optimize` | `hyper_optimize`, `min_givens_passes`, `pareto_frontier` |
| Byte-stable deterministic witness | `proof` | `Proof::EXPECTED_WITNESS`, `Proof::witness` |
---
## 6. The privacythroughput knobs (and which the optimizer turns)
- **`feedback_bits`:** the only knob with a genuine throughput tradeoff —
residual falls with bits, feedback airtime rises with them, so there is an
interior optimum (3 bits unconstrained; 5 bits within the 802.11-allowed set).
Privacy is unaffected by bits (the rotation is fresh regardless).
- **`givens_passes`:** the privacy/robustness knob. More mixing lowers re-ID at
**no throughput cost** (the keyed rotation is never signaled), so it trades
only compute. The optimizer finds the minimum for robust collapse and ships a
free 2× margin.
- **`sounding_overhead`:** a flat throughput cost from cadence randomization;
trades motion-obfuscation strength against airtime (outside the re-ID metric).
The `optimize` module turns these knobs deterministically — see
[08-optimization.md](08-optimization.md). It is what replaced the original
hand-picked config.
The `throughput` module computes the ratio from these, so the tradeoff is
inspectable rather than asserted (`cargo test throughput`).
---
## 7. Honest limitations of the model
- The two-subspace split is an abstraction; on real hardware comm and identity
information are only *approximately* separable, so the real throughput cost of
fully hiding identity may be higher than the model's ~2%. The DySPAN-2026
MEASURED curve is the external sanity check that it is *small* at fine
resolution, not zero.
- The nearest-centroid attacker is deliberately simple. The collapse argument is
classifier-independent (it is about the signal, not the model), but a hardware
study must confirm a strong learned attacker also collapses.
- Within-session motion is not addressed by the rotation alone (see threat
model §4).

View File

@@ -0,0 +1,90 @@
# 04 — Compliance and Regulatory Line
**Non-negotiable:** VEIL uses compliant waveform controls and **never jams.**
This file states the legal basis for that line and why every VEIL control falls
on the compliant side of it. It is engineering analysis, not legal advice; a
deployment in a given jurisdiction needs its own regulatory review.
---
## 1. The statutory line (United States)
The prohibition is on **interfering with others' transmissions**, not on how you
shape **your own** signal.
| Authority | What it prohibits |
|---|---|
| **47 U.S.C. §333** | *Willful or malicious interference* with any licensed/authorized radio station or U.S. Government station |
| **47 U.S.C. §302a(b)** | Manufacture, import, marketing, sale, or *operation* of non-compliant devices (jammers cannot be certified — their sole purpose is interference) |
| **47 U.S.C. §301** | Requires a license/authorization to transmit; a jammer can never be authorized |
| **47 U.S.C. §501 / §503** | Criminal penalties and forfeitures; FCC cites fines up to $112,500 per violation, **no exemptions** for business/residence/vehicle |
The distinguishing element of jamming is **intent to interfere plus effect on a
third party's link.** A device that shapes its own standards-conformant emission
— staying within transmit-power and spectral-mask limits, still type-certifiable
— is not a jammer.
---
## 2. Why each VEIL control is compliant
| Control | Compliance argument |
|---|---|
| **Keyed precoder rotation** | Orthogonal ⇒ preserves the report's energy exactly ⇒ **adds no power on top of anyone's signal.** It is still a valid precoder within the 802.11 feedback format. Machine-checked: energy ratio = 1.000000 (`compliance` module) |
| **Feedback quantization / dither** | Reports angles the standard already allows, at the standard's resolution; sub-step dither stays within the quantization envelope. No emission change beyond the node's own frame |
| **Sounding-cadence randomization** | Chooses *when* the node sends its own NDP soundings, within permitted timing. Sending fewer/jittered soundings never interferes with another station |
| **MU-group / stream-mapping shuffle** | Rearranges the node's own spatial mapping; a normal in-spec transmit choice |
None of the four transmits *to prevent* another station from communicating; none
adds out-of-mask energy; each passes normal type certification. Contrast a
jammer, whose defining purpose is to emit energy that denies others service.
---
## 3. The energy-conservation proof as a compliance artifact
VEIL turns "not jamming" from a promise into a **checked property.** The
`compliance::ComplianceReport` audits each protection step:
```
input_energy = ‖report_before‖²
output_energy = ‖report_after‖²
energy_ratio = output_energy / input_energy # ≈ 1.0 for a rotation
energy_conserving = |energy_ratio 1| ≤ 1e-2
adds_interfering_energy = false # by construction
is_compliant = energy_conserving ∧ ¬adds_interfering_energy
```
A regulator, an auditor, or the runtime attestation layer (ADR-141) can read the
report and verify the shield is a waveform-shaping control, not an interference
source. On the reference experiment the measured ratio is **1.000000**.
---
## 4. Jurisdictional notes
- **EU (GDPR framing).** Covert WiFi body-sensing of vital signs is sensitive
health data and "almost certainly illegal under GDPR," but effectively
unenforceable (receivers are undetectable) — which is precisely why a
*technical* control is needed. VEIL as a transmit-side control does not itself
raise GDPR issues; it reduces the personal data an attacker can derive.
- **RF-emission rules are jurisdiction-specific.** The energy-preserving property
is the portable core of the compliance argument, but power/mask/timing limits
differ by region and band; a deployment must confirm local rules.
- **Deliberate transmit-nulling toward a *located* sniffer** (steering a spatial
null at a known passive receiver) is still the node's own emission and adds no
interference, but is more aggressive and should get explicit regulatory review
before field use. It is not part of the default VEIL profile.
---
## Sources
- 47 U.S.C. §333: https://www.law.cornell.edu/uscode/text/47/333
- 47 U.S.C. §302a: https://www.law.cornell.edu/uscode/text/47/302a
- FCC Jammer Enforcement: https://www.fcc.gov/general/jammer-enforcement · https://www.fcc.gov/enforcement/areas/jammers
- FCC Cell/GPS Jamming guidance: https://www.fcc.gov/general/cell-phone-and-gps-jamming
- FCC 14-92 enforcement order: https://docs.fcc.gov/public/attachments/FCC-14-92A1.pdf
*Caveat: FCC pages were cross-verified against Cornell LII; this is engineering
analysis, not legal advice.*

View File

@@ -0,0 +1,113 @@
# 05 — Experiment Protocol: Attacker vs. Protector
This is the "start today" deliverable from the brief: **make one RuView node the
attacker and one the protector, and measure whether protection drives identity
recognition toward chance while keeping throughput above 95%.** It is realized as
a deterministic, reproducible experiment in
[`v2/crates/wifi-densepose-privshield`](../../../v2/crates/wifi-densepose-privshield).
Because it runs on **SYNTHETIC** data (no radio is touched), its numbers describe
the model, not real hardware — reproduced by `cargo test`, and to be
re-established on silicon with a captured log before any deployment claim.
---
## 1. Setup
- **Protector node.** Emits beamforming feedback shaped by the VEIL controls
(keyed per-session fine-subspace rotation + configured feedback resolution and
sounding overhead). Models a legitimate AP/STA protecting a room.
- **Attacker node.** A passive sniffer that enrolls a template per candidate from
captured reports, then classifies fresh captures (nearest-centroid) — the
BFId-class re-identification threat.
- **Scene.** `SceneConfig` default: 64-dim report, 8 comm dims, **16 candidate
identities** (chance = 1/16 = 6.25%), per-identity stable fine-block signature
+ per-session environmental nuisance.
Two runs of the attacker are compared: **shield off** (the attacker sees raw
reports) and **shield on** (every captured report is VEIL-protected). The same
attacker faces both.
---
## 2. Metrics and acceptance bar
| Metric | Definition | Bar |
|---|---|---|
| **Re-ID accuracy, shield off** | Top-1 identity accuracy on unprotected traffic | Must be well above chance (threat is real) — bar ≥ 0.5 |
| **Re-ID accuracy, shield on** | Top-1 identity accuracy on protected traffic | Must fall into the chance band `1/N · 2 + 0.03` |
| **Throughput ratio** | Protected link capacity ÷ baseline capacity | **≥ 0.95** |
| **Compliance** | Emission energy ratio ≈ 1 and non-interfering | `is_compliant == true` |
Overall `passed()` requires all four.
---
## 3. Results (SYNTHETIC, hyper-optimized default configuration)
Reproduce with `cargo test -p wifi-densepose-privshield` (all 35 tests + doctest
pass). The default shield config is the `optimize` module's output — 96 Givens
passes at 5-bit feedback resolution (see
[08-optimization.md](08-optimization.md)). Salient values from the reference run:
| Metric | Value |
|---|---|
| Candidate identities | 16 |
| Chance level | 6.25% |
| Chance band (acceptance) | ≤ 15.5% |
| **Re-ID accuracy, shield OFF** | **100.0%** |
| **Re-ID accuracy, shield ON** | **4.7%** |
| **Throughput ratio** | **97.60%** |
| Emission energy ratio | 1.000000 |
| Overall verdict | **PASS** |
Reading the result: the attacker is a *perfect* re-identifier without protection
(the synthetic signatures are cleanly separable), and VEIL drives it *to the
chance floor* (4.7% sits just below the ideal 6.25%, i.e. no better than
guessing) — while the modeled link keeps 97.6% of its throughput and the
emission conserves energy exactly (compliant, not jamming). The same collapse
holds under a Cosine-metric attacker and at N=32, confirming it is a property of
the signal, not the classifier.
---
## 4. Determinism and the witness
The experiment is byte-reproducible: no OS entropy, no wall-clock, no threads.
`proof::Proof` folds the salient outputs (quantized to avoid last-bit f32
round-off) into an FNV-1a witness pinned as `EXPECTED_WITNESS`. Any drift in the
PRNG stream, rotation schedule, throughput formula, or scene geometry changes the
witness and fails `witness_matches_pinned`. This is the same
deterministic-proof discipline as `nvsim` and the Python `verify.py`.
---
## 5. Sensitivity and what to vary next
`ExperimentConfig` exposes the levers for a fuller study:
- **`scene.identities`** — larger N lowers the chance floor; confirm collapse
holds as candidates grow.
- **`scene.env_sigma` / `beam_amplitude`** — nuisance and comm energy; stress the
separability assumption.
- **`shield.feedback_bits`** — trace the privacythroughput curve (the
`throughput` tests already show coarse resolution costs more).
- **`shield.givens_passes`** — mixing strength; fewer passes should degrade the
collapse gracefully.
- **Stronger attacker** — swap in a learned classifier to confirm the collapse is
signal-level, not classifier-level (the argument says it must be, but a
hardware study should verify).
---
## 6. Path to a real two-node measurement
The synthetic experiment is the design proof. The hardware path (per CLAUDE.md,
requires a captured log to claim MEASURED):
1. Two ESP32-S3/C6 or Nexmon-capable nodes: one runs Wi-BFI capture (attacker),
one runs a VEIL-shaped feedback profile (protector).
2. Enroll and test the same BFId-style classifier on captured BFI, shield off vs.
on; log throughput via iperf across the legitimate link.
3. Success = the same shape as §3 on real captures, with the boot/runtime log as
the witness. Until then, all defense numbers remain SYNTHETIC.

View File

@@ -0,0 +1,92 @@
# 06 — Market and Buyers
Facts are tagged **VERIFIED** (from a cited source), **CLAIMED** (asserted by a
vendor/analyst/press source), or **SPECULATIVE** (our inference). Market figures
are third-party projections, not independent measurements.
---
## 1. Why now
- **The threat is standardized and commercializing (VERIFIED/CLAIMED).** IEEE
802.11bf was published Sep 2025; silicon (Infineon AIROC Wi-Fi 7 ACW741x,
Qualcomm Dragonwing) lists 802.11bf sensing in 2026 briefs; Origin AI's
embedded-sensing program targets late-2026 deployment; Plume/Cognitive Systems
WiFi Motion is the largest deployed sensing footprint today.
- **The standards body declined to fix privacy (VERIFIED).** The BFI
"secure transmission mechanism" proposal (802.11-23/0782) was **withdrawn**;
802.11bf shipped with no privacy protections. This is the strongest demand
signal — the gap is structural and acknowledged.
- **No targeted anti-sensing product ships (VERIFIED by absence).** Every
countermeasure (IRShield, PhyCloak, MIMOCrypt, DP-Givens, ScatterShield) is
research-stage. The claim "no obvious shipping product protects rooms from this
inference" **holds** as of 2026, with one caveat below.
---
## 2. First buyers, ranked by procurement readiness
| Segment | Driver | Readiness |
|---|---|---|
| **Defence / government** | ICD 705 / DoD EMSEC already mandate RF attenuation in classified spaces; budgets and mandates exist | **Strongest beachhead (VERIFIED)** — but today they buy broadband shielding, not a sensing-specific control |
| **Corporate boardrooms / counter-espionage** | TSCM firms (Bastille, Murray Associates) now include WiFi audits and rogue-AP detection; CSI keystroke/gesture inference makes a boardroom shield a natural extension | **VERIFIED demand, EMERGING WiFi-specific** |
| **Hospitals** | RF-derived behavioral/vital data is HIPAA PHI; exam rooms, psychiatric units where inference is unwanted | **VERIFIED regulatory hook** — but the hook drives privacy-preserving *sensing* more than a *shield* |
| **Hotels** | Documented guest backlash against in-room sensors; privacy as differentiation | **SPECULATIVE** — narrative-led, not procurement-led today |
| **Router / AP manufacturers** | Ship opt-out/obfuscation as a firmware feature anticipating regulation | **SPECULATIVE** — no vendor has announced this |
---
## 3. Competitive landscape
- **Direct competitors:** none shipping. All targeted anti-sensing is academic.
- **The real substitute (VERIFIED):** broadband RF shielding — SCIF/TEMPEST
window film, paint, panels (Signals Defense SD2500: >40 dB, 30 MHz6 GHz, ICD
705 / ASTM F3057-14). It defeats WiFi sensing as a side effect but is **blunt**:
it kills *all* RF and cannot coexist with wanted WiFi.
- **TSCM services (VERIFIED):** detect, don't prevent.
**VEIL's differentiation** is exactly what the substitute lacks: **selective and
coexisting** — it removes identity/activity leakage while keeping the room's WiFi
working at ≥95% throughput, with a machine-checkable compliance artifact.
---
## 4. Market size (third-party projections, cite with care)
- **CLAIMED:** ABI Research — North American WiFi-sensing-compatible CPE install
base to **112M by 2030 (51.6% CAGR)**.
- **CLAIMED:** Global WiFi sensing market ~$402M (2024) → ~$2.13B (2033)
(MarketIntelo).
Implication: a shield must **coexist** with a large installed sensing base, not
assume RF denial — reinforcing the selective-coexistence positioning.
---
## 5. Where VEIL fits RuView's positioning
VEIL pairs with BFLD to make RuView the *both-sides* RF-perception platform:
BFLD/AETHER do sensing responsibly and detect leakage; VEIL is the customer-
facing **privacy firewall** that protects a room from *others'* sensing. That is a
defensible, standards-anchored, gap-filling story: the standards body left the
door open, the threat is shipping, and no one else sells the selective lock.
---
## Sources
- IEEE 802.11bf privacy-proposal withdrawal (802.11-23/0782), summarized: https://pascalpiron.substack.com/p/wifi-sensing-and-the-privacy-fix
- NIST 802.11bf: https://www.nist.gov/publications/ieee-80211bf-enabling-widespread-adoption-wi-fi-sensing
- IRShield: https://arxiv.org/abs/2112.01967 · MIMOCrypt: https://arxiv.org/pdf/2309.00250 · ScatterShield: https://dl.acm.org/doi/abs/10.1145/3770653 · WiShield JSAC 2024: https://dl.acm.org/doi/abs/10.1109/JSAC.2024.3414597
- Signals Defense TEMPEST/SCIF film: https://signalsdefense.com/tempest-and-scif-design/ · https://signalsdefense.com/shielding-films/
- National Shielding SCIF/ICD-705: https://www.national-shielding.com/pages/scif-icd-705-secure-facility-shielding
- Bastille TSCM: https://bastille.net/centers-of-excellence/tscm/ · IntellSIG TSCM overview: https://www.intellsig.com/2025/07/20/modern-eavesdropping-threats-a-tscm-overview/
- Origin AI program: https://www.prnewswire.com/news-releases/origin-ai-launches-compatible-with-origin-program-to-meet-industry-demand-for-scalable-wifi-sensing-and-accelerate-integration-across-global-soc-platforms-302650963.html
- MIT Tech Review, WiFi sensing: https://www.technologyreview.com/2024/02/27/1088154/wifi-sensing-tracking-movements/
- ABI Research 112M forecast: https://www.abiresearch.com/press/north-american-wi-fi-sensing-cpe-installations-to-surge-to-112-million-by-2030-as-the-technologys-maturing-unleashes-new-business-and-service-models
- MarketIntelo WiFi sensing market: https://marketintelo.com/report/wi-fi-sensing-market
- HIPAA/PHI RF-sensing context (PMC): https://pmc.ncbi.nlm.nih.gov/articles/PMC11939480/
*Caveat: market figures are analyst/vendor projections; the "no shipping product"
finding reflects absence of evidence in these searches and should be confirmed
with a patent/vendor scan before anchoring a go-to-market claim.*

View File

@@ -0,0 +1,117 @@
# 07 — Implementation and Roadmap
---
## 1. What ships in this bundle
- **Reference crate** `v2/crates/wifi-densepose-privshield` (VEIL): a
deterministic, dependency-free, WASM-ready pure-compute leaf implementing the
full attacker-vs-protector experiment, the four compliant controls, the
throughput model, the compliance audit, the `optimize` hyper-optimizer, and a
byte-stable proof. 35 tests + doctest pass; builds for
`wasm32-unknown-unknown`; clippy-clean.
- **This research bundle** (`docs/research/privacy-shield/`).
- **[ADR-288](../../adr/ADR-288-veil-privacy-shield-compliant-waveform.md)** — the
formal decision record.
- **npm metaharness** `harness/wifi-densepose-privshield/`
([ADR-289](../../adr/ADR-289-wifi-densepose-privshield-harness-via-metaharness.md))
— a per-crate contributor harness (architect/implementer/reviewer/test-writer,
router, flywheel) with a dependency-free `guidance` surface that serves this
bundle's capability map. `npx wifi-densepose-privshield-harness guidance
--topic optimization`.
The crate is intentionally a **leaf with no internal RuView dependencies**
(mirrors `wifi-densepose-aether`), so it can be reasoned about, fuzzed, and
ported independently, and so it can never accidentally acquire a path to a radio.
---
## 2. Reuse map (how VEIL composes with existing RuView)
| Existing subsystem | Relationship |
|---|---|
| **BFLD** (ADR-118/120/121, `wifi-densepose-bfld`) | Detection layer. Its `identity_risk_score` is the natural trigger for VEIL's `SensingDetector` — detect leakage, then shield |
| **Privacy control plane** (ADR-141) | VEIL protection steps emit `ComplianceReport`s that fit the runtime-attestation model (which mode, which actions, which fields) |
| **Active sensing / governed actuation** (ADR-280) | VEIL is a defensive `SensingAction`: a governed, privacy-ceiling-bounded emission-shaping action the control plane can schedule |
| **Givens/beamforming primitives** | VEIL reuses the report's native Givens-rotation structure rather than inventing a new transform |
| **Deterministic proof discipline** (`nvsim`, `archive/v1/verify.py`) | VEIL's `proof` module follows the same pinned-witness pattern |
---
## 3. Phased rollout
| Phase | Deliverable | Evidence class |
|---|---|---|
| **P1 — reference model (this PR)** | Crate + experiment + docs + ADR | SYNTHETIC (cargo test) |
| **P2 — sensitivity study** | Sweep N, noise, resolution, mixing; add a learned attacker to confirm signal-level collapse | SYNTHETIC |
| **P3 — BFLD integration** | Wire `identity_risk``SensingDetector` → shield engage; emit attestation | SYNTHETIC + integration tests |
| **P4 — firmware feedback shaping** | Implement keyed fine-subspace rotation + cadence randomization in the **beamforming-feedback / spatial-mapping path** — see §3.1 for the (non-trivial) platform reality | build + hardware |
| **P5 — two-node hardware measurement** | Attacker (Wi-BFI capture) vs. VEIL protector on real silicon; iperf throughput; captured log | **MEASURED** (with witness) |
| **P6 — deployment profiles** | Per-segment profiles (SCIF, boardroom, ward) with regulatory review | operational |
No defense claim graduates from SYNTHETIC to MEASURED without a captured
boot/runtime log (CLAUDE.md hardware rule).
### 3.1 Does this need custom WiFi firmware? (yes — and ESP32 is the wrong chip for the protector)
VEIL shapes the **compressed beamforming report** (the Givens φ/ψ angles) or the
LTF **spatial mapping** as it is transmitted — machinery that lives *below* the
driver, inside the chip's PHY/MAC firmware. It is **not** reachable from user
space, so a real deployment is a firmware/driver change, not an app.
- **ESP32 — not viable as the protector.** Its WiFi lower layers are a closed
Espressif blob. ESP-IDF exposes CSI *read* (`esp_wifi_set_csi`) — which is why
`firmware/esp32-csi-node/` makes a great **attacker/sensor** node — but it does
**not** let you rewrite how the chip builds/sends beamforming feedback. ESP32
is the *attacker* in a testbed, not the shield.
- **Realistic protector platforms:** **openwifi** (open 802.11 on SDR/FPGA —
full PHY/MAC control incl. the AP-side compensation; the honest end-to-end
route; Verilog + a C driver); **Nexmon** (C firmware *patches* for
Broadcom/Cypress, e.g. RPi BCM43455 — the commodity path, and the same
framework the BFI *attack* tools already use); open drivers (**ath9k/mt76**)
for partial control; or **vendor firmware** for a production feature.
- **Two firmware variants:** the **keyed-reversible** version (VEIL's ~98%
throughput) needs changes on **both** ends plus key agreement (cf. the
LeakyBeam AP-side `Q_obf` is *client-transparent* — only the AP changes — which
is a deployment advantage worth adopting, §09 backlog item 3); the
**emitter-only DP dither** version needs only the reporting device but pays the
full throughput cost.
The current crate is deliberately a std-only, no-radio leaf and implements none
of this; P4 is where it meets silicon.
---
## 4. Open problems (tracked honestly)
1. **Real-hardware separability.** Comm and identity information are only
*approximately* separable on real radios; the true throughput cost of full
identity hiding may exceed the model's ~2%. P2/P5 must bound it.
2. **Within-session motion leakage.** A fixed per-session rotation does not
obfuscate coarse motion within one capture window. Needs stronger cadence
randomization or amplitude shaping; currently a stated non-goal for the re-ID
metric.
3. **Active adversary (A2).** An attacker that transmits its own soundings is
only partially addressed by cadence control; a MAC-layer non-response policy
is needed.
4. **Key management.** The per-session rotation key must be derived from the
negotiated link secret; VEIL's PRNG is explicitly *not* cryptographic and must
not be used for real key material.
5. **Regulatory review per jurisdiction.** The energy-conservation argument is
portable, but power/mask/timing limits and any transmit-nulling profile need
local review before field use.
---
## 5. Validation commands
```bash
# Reference experiment + all unit/proof/doc tests
cargo test -p wifi-densepose-privshield --no-default-features
# WASM portability (leaf builds with no radio path)
cargo build -p wifi-densepose-privshield --target wasm32-unknown-unknown
# Lints
cargo clippy -p wifi-densepose-privshield --all-targets
```

View File

@@ -0,0 +1,142 @@
# 08 — Hyper-Optimization
The reference crate first shipped a **hand-picked** shield config (112 Givens
passes, 7-bit feedback). This file records how the `optimize` module replaces
that guess with a *derived*, robustness-verified optimum, and what it found. All
numbers are **SYNTHETIC / L0**, reproduced by
`cargo test -p wifi-densepose-privshield`.
---
## 1. What is being optimized, and against what
Two knobs, two objectives, one hard constraint:
| Knob | Costs | Does it trade against privacy? |
|---|---|---|
| `feedback_bits` (angle resolution) | Throughput: **residual** falls with bits, **feedback airtime** rises with bits | No — the keyed rotation is applied regardless of resolution |
| `givens_passes` (rotation mixing) | Compute only | Yes — more mixing ⇒ lower re-ID |
**Constraint:** re-ID must collapse into the chance band `1/N · 2 + 0.03` — and
it must do so *robustly*: for **both** attacker metrics (Euclidean and Cosine)
and **both** identity counts (N = 16 and N = 32, the harder, lower-chance case).
The key structural fact: **rotation mixing is throughput-free.** The per-session
rotation is derived from the shared link secret on both ends (like MIMOCrypt) —
it is never transmitted — so extra Givens passes cost compute, not airtime. That
means privacy margin is essentially free; the only throughput tradeoff lives in
`feedback_bits`.
---
## 2. Throughput is a 1-D problem with an interior optimum
Because the residual falls with bits while feedback airtime rises, throughput
has a genuine interior optimum in `feedback_bits` (`LinkModel`, default SNR 20 dB,
`feedback_overhead_per_bit = 0.0008`):
| bits | throughput ratio |
|---|---|
| 1 | 0.9681 |
| 2 | 0.9757 |
| **3** | **0.9769** ← unconstrained optimum |
| 4 | 0.9766 |
| **5** | **0.9760** ← shipped (spec-allowed) |
| 7 | 0.9744 (the old hand-picked value) |
| 9 | 0.9728 |
| 12 | 0.9704 |
The unconstrained optimum is **3 bits** — which coincides with the DySPAN-2026
MEASURED finding that ~3-bit feedback is the privacyutility sweet spot, because
the receiver compensates the keyed rotation and extra bits mostly buy airtime.
802.11 compressed beamforming quantizes ψ/φ to roughly 59 bits, so the shipped
shield uses the throughput-best **spec-allowed** value, **5 bits** (0.9760),
rather than the out-of-spec 3-bit optimum. Either way it beats the old 7-bit
choice.
---
## 3. Mixing: the minimum robust budget, and a free margin
Worst-case shield-on re-ID vs. `givens_passes` (bits = 5; worst over Euclidean
and Cosine):
| passes | re-ID @ N=16 | re-ID @ N=32 | robust collapse? |
|---|---|---|---|
| 16 | 0.75 | 0.62 | no |
| 24 | 0.50 | 0.35 | no |
| 32 | 0.20 | 0.14 | no (N=32 band is 0.0925) |
| **48** | 0.12 | 0.057 | **yes** ← proven minimum |
| 64 | 0.078 | 0.044 | yes |
| **96** | **0.047** | **0.018** | **yes** ← shipped (2× margin) |
| 112 | 0.078 | 0.042 | yes (the old default — no better than 96) |
The proven minimum for robust collapse is **48 passes** — the hand-picked 112 was
**2.3× over-provisioned**. Since mixing is throughput-free, the shield ships
**96 passes** (`PRIVACY_MARGIN_FACTOR = 2` × 48, rounded up to a candidate): it
drives re-ID *below chance* at N=16 (0.047 < 0.0625) at zero throughput cost, and
is still cheaper compute than the original 112.
---
## 4. The adopted config, and why it beats the original
| | Old (hand-picked) | Hyper-optimized (shipped) |
|---|---|---|
| Givens passes | 112 | **96** (from proven-min 48 × 2) |
| Feedback bits | 7 | **5** (spec-optimal) |
| Shield-on re-ID (N=16) | 0.078 | **0.047** |
| Throughput ratio | 0.9744 | **0.9760** |
| Robust across metrics & N | not checked | **verified** |
The optimum is **strictly better on privacy and throughput at once**, and is now
*verified* rather than assumed. `ShieldConfig::default()` is exactly the
optimizer's output; the test `optimize::shipped_default_equals_optimizer_output`
fails if they ever drift apart.
---
## 5. The Pareto frontier (and an honest note)
`optimize::pareto_frontier` enumerates non-dominated (worst-case re-ID,
throughput) points over a pass × bits grid. In this model the frontier
**collapses toward the max-mixing, 5-bit point**, because mixing is
throughput-free — so beyond the throughput knob (bits) there is no privacy
throughput tradeoff to trace. That degeneracy is itself the finding: *the only
thing privacy costs here is feedback resolution, and even that is cheap.* On real
hardware, where comm/identity subspaces are only approximately separable and
where more aggressive mixing may touch the data-carrying beam, this frontier is
expected to open up — a hardware study (roadmap P5) will re-measure it.
---
## 6. Per-deployment adaptivity
The optimum is not one number — `optimize` derives it per deployment:
- **SNR → feedback resolution.** `optimal_bits_across_snr` shows the
*unconstrained* throughput-optimal resolution shifting with SNR: **4 bits at
510 dB, 3 bits at 2040 dB** (low SNR values fine resolution more because
the Shannon capacity is near-linear there, so the residual costs more). Within
the spec-allowed {5,7,9} set the choice is 5 bits across this whole range —
the residual is already negligible at 5 bits — which is why the shipped shield
is SNR-stable.
- **Identity count → mixing.** `adaptive_shield(base, n)` derives the config for
a room with `n` expected occupants. A notable finding: in this model the
collapse budget is **N-independent** (min 48 passes collapses N∈{8,64}
alike), because a well-mixed Haar-like rotation destroys per-identity
structure regardless of how many identities there are — the budget is set by
the fine-subspace dimension, not the candidate count. So `adaptive_shield`
returns the same 96/5 across that range: the default is robust, not a point
tuning.
Both are surfaced through the harness `guidance --topic optimization`.
## 7. Robustness caveats (unchanged from the threat model)
- The collapse is verified against two classifiers and two N; a learned
attacker on real captures must still be checked (P2/P5).
- `feedback_bits` affects only throughput in this model, not re-ID; on hardware,
coarse quantization also adds obfuscation, which would *help* privacy — the
model conservatively ignores that.
- All optimization results are SYNTHETIC until a hardware witness exists.

View File

@@ -0,0 +1,151 @@
# 09 — SOTA Update (20252026) and VEIL Improvement Backlog
Source: a fan-out deep-research run (5 angles → 20 primary sources → 93 claims →
top 25 adversarially verified with 3-vote panels → 24 confirmed, 1 refuted).
Each finding carries its **evidence class** (`MEASURED` with metric / `CLAIMED`
/ `SYNTHETIC` / `STANDARDS-MINUTE`) and a primary URL. This file records what
changed in the field and the concrete backlog it implies for VEIL (ADR-288/289).
Nothing here upgrades VEIL's own numbers to `MEASURED` — that still requires a
captured hardware log (CLAUDE.md).
---
## 1. The threat surface got worse (and cheaper)
| Finding | Evidence | Source |
|---|---|---|
| **BFId** — first *identity* inference from plaintext BFI: **99.5% over 197 people**, perspective/gait-independent; BFI carries ~740 features vs 212 for CSI, so it *beats* CSI for identity; one eavesdropper captures BFI from all clients | `MEASURED` (top-1, N=197, CCS 2025) | [dl.acm.org/10.1145/3719027.3765062](https://dl.acm.org/doi/10.1145/3719027.3765062) |
| **LeakyBeam** — through-wall occupancy at **20 m** (TPR 82.7% / TNR 96.7%) **and breathing/vital-sign** leakage from *stationary* occupants; single antenna, Wireshark, no keys | `MEASURED` (NDSS 2025) | [ndss 2025-5](https://www.ndss-symposium.org/wp-content/uploads/2025-5-paper.pdf) |
| **WiKI-Eve / SThief** — keystroke & PIN/password theft from BFI (88.9% per-keystroke; 65.8% top-10 app passwords; POS keypads) with no device compromise | `MEASURED` (CCS 2023 / IEEE) | [WiKI-Eve](https://dl.acm.org/doi/10.1145/3576915.3623088) · [SThief](https://ieeexplore.ieee.org/document/10621321/) |
| **BFIAttack****reconstructs full CSI from sniffed BFI**: closed-form ≥93% (single-antenna, 1 attempt); MLE with physics/standard constraints 73% (multi-antenna, ≤5 attempts). Collapses the BFI-vs-CSI distinction | `MEASURED` (arXiv Apr 2026) | [arxiv 2604.04179](https://arxiv.org/html/2604.04179v1) |
| **BeamSense** — BFI sensing is standards-compliant, needs no firmware mod, ~10% higher activity accuracy than CSI | `MEASURED` | [BFISense/BeamSense](https://www.researchgate.net/publication/402468114_BFISense_Using_Beamforming_Feedback_Information_for_Wi-Fi_Sensing) |
**Implication:** the attacker is a *passive, keyless, single commodity antenna at
~20 m, through walls*, that can (a) identify people, (b) read vitals and
keystrokes, and (c) **reconstruct CSI from the BFI itself.** VEIL's threat model
must treat all four as baseline.
---
## 2. Defenses — the field validates VEIL's family and adds stronger primitives
| Defense | Mechanism | Effect | Evidence | Source |
|---|---|---|---|---|
| **LeakyBeam defense** | AP-side **per-packet random unitary** `Q_obf` on the LTF via the 802.11 spatial-mapping mechanism (standard says "not restricted"); AP recovers `V = Q_obf · V_obf`; **clients unmodified** | attack **89.7% → ~51%** across 8 APs (~1.6M packets/49 h) | `MEASURED` | [ndss 2025-5](https://www.ndss-symposium.org/wp-content/uploads/2025-5-paper.pdf) |
| **PrivISAC (RIS)** | Paired per-row RIS vectors, one randomly active per slot; preserves comm-direction response, corrupts sensing direction; time-domain mask/demask for the authorized RX | **93% → ~30%**, and **29% vs. retrained 5-location adaptive attacker** | `MEASURED` (64-element FPGA RIS, Intel 5300, ~2,700 OTA samples) | [arxiv 2601.04488](https://arxiv.org/html/2601.04488) |
| **DP-Givens** | ε-DP stochastic quantizer on the Givens rotation/phase angles; closed-form angular sensitivity → principled ε budget; preserves 802.11 feedback structure | frontier: attacker error 19% → ~73%; beamforming gain 0.97 → 0.89 median (0.54 at full) | `SYNTHETIC` (Monte-Carlo) | [arxiv 2512.18529](https://arxiv.org/pdf/2512.18529) |
| **Adaptive-DP (CSI spectrogram)** | Importance-weighted (non-uniform) DP budget across the time-frequency plane | better privacy-utility than flat noise at equal ε∈[0.5,2]; cuts identity + membership inference | `CLAIMED` (unrefereed) | [arxiv 2512.20323](https://arxiv.org/abs/2512.20323) |
| **BeamDancer** | Randomized native-beamforming obfuscation | defeats supervised + unsupervised localization and micro-Doppler; **compliant, not jamming** | `MEASURED` (IEEE TWC 2024) — **do NOT cite its ">96% PDR" (refuted here)** | [ieee 10739908](https://ieeexplore.ieee.org/document/10739908/) |
| **TX-side CSI obfuscation (+ counter-attacks)** | Filter the whole frame incl. LTS; DNN de-obfuscation for authorized sensing | **security contested**: "Defeating CSI obfuscation" + SnoopFi FIA/CRA recover the signal | `CLAIMED` design + published rebuttal | [C&S 2025](https://www.sciencedirect.com/science/article/abs/pii/S0167404825002834) |
**Where VEIL sits:** VEIL's keyed Givens rotation is the *same family* as the
LeakyBeam per-packet unitary and the DP-Givens knob — and unlike additive/DP
dither, VEIL's transform is **secret and orthogonal**, which is exactly the
property that should resist the BFIAttack closed-form/MLE inversion (the attacker
has no key, so there is no closed-form to invert to). That is now the decisive
claim to *test*, not assume.
---
## 3. Compliance / legal line
- **BeamDancer (IEEE TWC 2024)** is the peer-reviewed precedent for VEIL's
stance: **jamming and geofencing are non-compliant / non-scalable; exploiting
the standard beamforming mechanism stays 802.11-compliant** (validated without
disabling firmware). Cite it as the compliance precedent — but **not** its
refuted throughput figure.
- **Governance gap (unfilled):** *no* claim on the 802.11bf-2025 standard's
privacy provisions, the withdrawn secure-LTF-from-11az proposal, or
GDPR/HIPAA/EMSEC/ICD-705 boundaries **survived 3-vote verification** in this
run. Blog/secondary sources assert a withdrawn privacy proposal, but it needs
primary WG-minute/draft sourcing before VEIL relies on it. Tracked as an open
question.
---
## 4. VEIL improvement backlog (derived, prioritized)
Priority = (verified severity) × (fit to VEIL). `[code]` = crate change,
`[docs]` = documentation, `[hw]` = hardware path.
1. **`[code]` ✅ implemented — Reconstruction-aware attacker (decisive).** A
BFIAttack-style adversary (`attacker::ReconstructionAttacker`,
`AttackerKind::Reconstruction`) recovers the direction of the CSI consistent
with the *captured* report and classifies it; the test
`reconstruction_attacker_collapses` confirms the keyed *orthogonal secret*
rotation leaves it at chance (no key → it only ever recovers the rotated
direction) while it still wins on unprotected traffic. *(BFIAttack, MEASURED)*
2. **`[code]` ✅ implemented — Adaptive, multi-capture attacker.**
`attacker::AdaptivePoolingAttacker` (`AttackerKind::AdaptivePooling`) pools all
captures per identity and whitens by per-dimension std before matching (the
PrivISAC adaptive/retraining adversary); `adaptive_pooling_attacker_collapses`
confirms collapse still holds. *(PrivISAC, MEASURED)*
3. **`[code]` ✅ implemented — Per-packet random-unitary mode.**
`protector::ObfMode::PerPacketUnitary` applies a fresh unitary per packet,
AP-side and **client-transparent** (LeakyBeam family; 802.11 spatial mapping
"not restricted" as the compliance basis);
`per_packet_unitary_mode_collapses_and_is_compliant` verifies it. *(LeakyBeam
defense, MEASURED)*
4. **`[code]` ✅ implemented — DP-Givens ε knob.** `ShieldConfig.dp_epsilon` adds
an ε-scaled angular dither, renormalized to preserve emission energy (still
not jamming); `throughput::dp_residual` makes ε a real privacy↔throughput knob
(`dp_epsilon_lowers_throughput_as_it_tightens`), and the combined
rotation+DP still collapses and stays compliant. Outputs `SYNTHETIC`.
*(DP-Givens, SYNTHETIC)*
> Items 14 landed with the reference **witness unchanged**
> (`0x350d…f448`) — the new controls/attackers are opt-in fields; the shipped
> default config and its numbers are byte-identical.
5. **`[code/docs]` Privacythroughput *frontier*, not binary claims.** Report
attacker-error-vs-privacy and gain/PDR-vs-privacy curves (we already have the
throughput-vs-bits and reid-vs-passes curves; add the joined frontier).
6. **`[docs]` Threat-model upgrade.** Elevate identity/gait re-ID, through-wall
vitals, keystroke/PIN, and **BFI→CSI reconstruction** to primary threats in
ADR-288 §threat and bundle 02; add the passive/keyless/20 m/through-wall
adversary as the default. *(done in this update)*
7. **`[docs]` Security honesty.** State that VEIL's shield security is `CLAIMED`
until it survives published de-obfuscation attacks (SnoopFi / "Defeating CSI
obfuscation"); add learned de-obfuscation to the attacker roadmap.
8. **`[code/docs]` Evaluation battery.** Adopt BeamDancer's three-attacker matrix
(supervised localizer + unsupervised clusterer + model-based Doppler) as a
minimum test set, plus identity + membership-inference metrics.
9. **`[hw]` Hardware-validation path.** Mirror the RIS/8-AP OTA testbeds for P5.
**Correction:** ESP32 is an *attacker/sensor* node only (its WiFi lower layer
is a closed blob exposing CSI *read*, not TX-feedback shaping); the protector
needs **openwifi (SDR/FPGA), Nexmon (C firmware patches), or vendor
firmware** + key agreement for the keyed-reversible version. See roadmap §P4.
10. **`[docs]` Governance sourcing.** Fill the 802.11bf privacy-provision gap
with primary WG minutes/draft; scope FCC Part 15, GDPR/HIPAA (inferred
biometric/health), and EMSEC/ICD-705 deployability.
---
## 5. Open questions the evidence did not close
- Does VEIL's obfuscation degrade **CSI *reconstructed* from BFI** (BFIAttack),
or only raise raw-BFI feature noise? *(the decisive effectiveness question)*
- What is VEIL's **own MEASURED** privacythroughput frontier on silicon (the
only measured PDR number in the field was refuted; the DP curves are
simulation-only)?
- Does 802.11bf-2025 contain any privacy provision or a withdrawn one, and what
are the concrete FCC/GDPR/HIPAA/ICD-705 deployment boundaries?
---
## Sources (primary, verified in this run)
- BFId — CCS 2025: https://dl.acm.org/doi/10.1145/3719027.3765062
- LeakyBeam (attack + per-packet-unitary defense) — NDSS 2025: https://www.ndss-symposium.org/wp-content/uploads/2025-5-paper.pdf
- BFIAttack (BFI→CSI reconstruction) — arXiv 2026: https://arxiv.org/html/2604.04179v1
- WiKI-Eve — CCS 2023: https://dl.acm.org/doi/10.1145/3576915.3623088
- SThief — IEEE: https://ieeexplore.ieee.org/document/10621321/
- BeamSense/BFISense: https://www.researchgate.net/publication/402468114_BFISense_Using_Beamforming_Feedback_Information_for_Wi-Fi_Sensing
- PrivISAC (RIS) — arXiv 2026: https://arxiv.org/html/2601.04488
- DP-Givens — arXiv 2512.18529: https://arxiv.org/pdf/2512.18529
- Adaptive-DP spectrogram — arXiv 2512.20323: https://arxiv.org/abs/2512.20323
- BeamDancer — IEEE TWC 2024: https://ieeexplore.ieee.org/document/10739908/
- TX-side CSI obfuscation — Computers & Security 2025: https://www.sciencedirect.com/science/article/abs/pii/S0167404825002834
*Refuted (do not cite): BeamDancer ">96% PDR in LoS" (verification 12). Two DP
mechanisms are SYNTHETIC/CLAIMED, not silicon. Governance/standard pillar
unverified in this run.*

View File

@@ -0,0 +1,102 @@
# Privacy Shield Research Bundle — WiFi Veil
**WiFi Veil** (codename **VEIL** — Verifiable Emission-shaping for
Identity-Leakage prevention) is a privacy *firewall* for WiFi sensing: it
prevents unauthorized identity and
activity inference from a room's WiFi while preserving normal communications. It
is the **countermeasure** counterpart to [BFLD](../BFLD/) — where BFLD *detects*
when beamforming feedback becomes identifying, WiFi Veil *acts* by shaping the node's
own compliant waveform (channel sounding, precoder phase, beam/feedback
schedules) so identity and activity inference fail, while a legitimate receiver
sees an essentially unchanged link.
**This must use compliant waveform controls, never jamming.** Every technique
here operates on the defender's *own* legitimately transmitted, standards-
conformant frames. Nothing adds energy to interfere with another station's
transmission (the statutory definition of jamming, 47 U.S.C. §333/§302a).
---
## Table of contents
| File | Purpose |
|------|---------|
| [01-sota-survey.md](01-sota-survey.md) | State of the art: identity/activity inference attacks (BFI + CSI), the IEEE 802.11bf-2025 standard, and privacy-preserving countermeasures |
| [02-threat-model.md](02-threat-model.md) | Adversary classes, what WiFi Veil defends and what it explicitly does not, trust boundary |
| [03-countermeasure-design.md](03-countermeasure-design.md) | The compliant waveform controls, the separable-subspace principle, keyed Givens-rotation shield, and how it maps to the crate |
| [04-compliance-and-regulatory.md](04-compliance-and-regulatory.md) | The legal line between compliant waveform control and jamming, with statutory citations |
| [05-experiment-protocol.md](05-experiment-protocol.md) | The attacker-vs-protector experiment: metrics, acceptance bar, reproducer, and results |
| [06-market-and-buyers.md](06-market-and-buyers.md) | First buyers, procurement drivers, competitive landscape, and the standards-body gap |
| [07-implementation-and-roadmap.md](07-implementation-and-roadmap.md) | Crate layout, reuse map, hardware path, phased rollout, and open problems |
| [08-optimization.md](08-optimization.md) | Hyper-optimization: throughput-optimal feedback resolution, minimum robust mixing budget, Pareto frontier, and the adopted config |
| [09-sota-update-2026.md](09-sota-update-2026.md) | 20252026 SOTA update (verified, cited): stronger attacks (BFI→CSI reconstruction, through-wall vitals, keystroke), validated compliant defenses, and the derived WiFi Veil improvement backlog |
Formal decision: [ADR-288](../../adr/ADR-288-veil-privacy-shield-compliant-waveform.md).
Reference implementation: [`v2/crates/wifi-densepose-privshield`](../../../v2/crates/wifi-densepose-privshield).
---
## Executive summary
1. **The threat is real and now standardized.** IEEE 802.11ac/ax beamforming
feedback (BFI) — the compressed Givens-rotation angle matrices (φ/ψ) a client
sends the AP — travels **unencrypted on the management plane**. Any device in
monitor mode can capture it for every client at once, no network access, and
the target need carry no device. **BFId** (KIT, ACM CCS 2025) re-identifies
individuals from BFI alone; **LeakyBeam** (NDSS 2025) detects occupancy
through walls at ~20 m from BFI; **BeamSense** recognizes activities at up to
99.28% from BFI. IEEE Std **802.11bf-2025** (published 26 Sep 2025)
standardizes the sensing measurement/feedback surface these attacks abuse.
2. **The standards body declined to fix it.** A 2023 proposal for a BFI
"secure transmission mechanism" (IEEE 802.11-23/0782) was **withdrawn**
the working group did not align on characterizing sensing privacy as a
distinct problem. 802.11bf shipped without privacy protections. This is the
single strongest demand signal: the gap is structural and acknowledged.
3. **No targeted anti-sensing product ships (as of 2026).** Every countermeasure
in the literature — IRShield, PhyCloak, MIMOCrypt, DP-Givens dithering,
ScatterShield — is research-stage. The only shipping substitute is broadband
RF shielding (SCIF/TEMPEST film/paint), which is blunt: it kills *all* RF and
cannot coexist with wanted WiFi. The whitespace is a **selective, coexisting,
software/PHY** shield.
4. **The WiFi Veil mechanism.** Identity leaks through the *fine* cross-subcarrier
phase structure of a beamforming report; throughput rides the *dominant*
beam direction. These are (mostly) separable subspaces. WiFi Veil composes extra
**keyed Givens rotations** over the fine subspace only. The rotation is
*orthogonal* (energy-preserving ⇒ not jamming), *keyed per session* (the
legitimate receiver inverts it ⇒ throughput preserved), and *fresh each
session* (a sniffer cannot average it back ⇒ re-ID collapses to chance).
5. **Measured on the reference model (SYNTHETIC), at the hyper-optimized
operating point.** On the default synthetic scene (16 candidate identities),
a passive re-identifier scores **100% with the shield off** and **4.7% with
it on** (chance = 6.25%), while modeled link throughput stays at **97.6%** of
baseline and the emission energy ratio is **1.000000** (compliant). The shield
config is chosen by the `optimize` module — 96 Givens passes (2× the proven-
minimum 48 for robust collapse across both attacker metrics and N∈{16,32}) at
5-bit feedback resolution — not hand-picked (see
[08-optimization.md](08-optimization.md)). Reproduce:
`cargo test -p wifi-densepose-privshield`.
6. **Scope, honestly.** WiFi Veil defends against a *third-party passive sniffer*. It
does **not** hide identity from the associated AP (that party holds the key)
— that is BFLD's detection/policy problem. WiFi Veil is a reference model, not
hardware: real-silicon validation (per CLAUDE.md) is future work with a
captured-log witness.
---
## Evidence discipline
Per repository policy, every quantitative claim is tagged:
- **MEASURED** — from a cited primary source with its metric and conditions.
- **CLAIMED** — asserted by a source (vendor PR, press, standards minutes)
without an independent measurement.
- **SYNTHETIC** — produced by WiFi Veil's own deterministic model; reproduced by
`cargo test`, describing the model and not real hardware.
WiFi sensing is never presented here as camera-grade, and no WiFi Veil result implies
a defense guarantee on real silicon until a hardware witness exists.

View File

@@ -0,0 +1,38 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ruview.net/schemas/pose-observation-v2.schema.json",
"title": "PoseObservationV2",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "timestamp_ns", "sensor_epoch", "sequence", "track_id", "frame", "calibration_id", "floor_plane", "model", "source", "trust_state", "dimensionality", "uncertainty_calibrated", "joints", "observer_confidence", "canonical_hash"],
"properties": {
"schema_version": { "const": 2 },
"timestamp_ns": { "type": "integer", "minimum": 0 },
"sensor_epoch": { "type": "integer", "minimum": 0 },
"sequence": { "type": "integer", "minimum": 0 },
"track_id": { "$ref": "#/$defs/string_id" },
"frame": { "$ref": "#/$defs/frame" },
"calibration_id": { "$ref": "#/$defs/string_id" },
"floor_plane": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/floor" }] },
"model": { "$ref": "#/$defs/model" },
"source": { "$ref": "#/$defs/source" },
"trust_state": { "enum": ["KNOWN", "DEGRADED", "UNKNOWN"] },
"dimensionality": { "enum": ["image2d", "metric3d"] },
"uncertainty_calibrated": { "type": "boolean" },
"joints": { "type": "array", "minItems": 17, "maxItems": 17, "items": { "$ref": "#/$defs/joint" } },
"observer_confidence": { "$ref": "#/$defs/probability" },
"canonical_hash": { "$ref": "#/$defs/hash" }
},
"$defs": {
"probability": { "type": "number", "minimum": 0, "maximum": 1 },
"hash": { "type": "array", "minItems": 32, "maxItems": 32, "items": { "type": "integer", "minimum": 0, "maximum": 255 } },
"string_id": { "type": "string", "minLength": 1, "maxLength": 128 },
"vec3": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "type": "number" } },
"frame": { "type": "object", "additionalProperties": false, "required": ["name", "version", "metric", "right_handed", "z_up"], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 128 }, "version": { "type": "integer", "minimum": 1 }, "metric": { "type": "boolean" }, "right_handed": { "type": "boolean" }, "z_up": { "type": "boolean" } } },
"floor": { "type": "object", "additionalProperties": false, "required": ["normal", "offset_m"], "properties": { "normal": { "$ref": "#/$defs/vec3" }, "offset_m": { "type": "number" } } },
"model": { "type": "object", "additionalProperties": false, "required": ["id", "artifact_hash"], "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 128 }, "artifact_hash": { "$ref": "#/$defs/hash" } } },
"source": { "type": "object", "additionalProperties": false, "required": ["sensor_id", "authenticated", "replay_protected"], "properties": { "sensor_id": { "type": "string", "minLength": 1, "maxLength": 128 }, "authenticated": { "type": "boolean" }, "replay_protected": { "type": "boolean" } } },
"covariance": { "type": "object", "additionalProperties": false, "required": ["xx", "xy", "xz", "yy", "yz", "zz"], "properties": { "xx": { "type": "number", "minimum": 0 }, "xy": { "type": "number" }, "xz": { "type": "number" }, "yy": { "type": "number", "minimum": 0 }, "yz": { "type": "number" }, "zz": { "type": "number", "minimum": 0 } } },
"joint": { "type": "object", "additionalProperties": false, "required": ["kind", "position_m", "covariance_m2", "confidence", "visibility"], "properties": { "kind": { "enum": ["nose", "left_eye", "right_eye", "left_ear", "right_ear", "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", "left_wrist", "right_wrist", "left_hip", "right_hip", "left_knee", "right_knee", "left_ankle", "right_ankle"] }, "position_m": { "$ref": "#/$defs/vec3" }, "covariance_m2": { "$ref": "#/$defs/covariance" }, "confidence": { "$ref": "#/$defs/probability" }, "visibility": { "enum": ["visible", "occluded", "unknown"] } } }
}
}

View File

@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ruview.net/schemas/pose-refinement-v1.schema.json",
"title": "PoseRefinementV1",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "raw_observation_hash", "mode", "disposition", "selected", "refined_joints_m", "physics_confidence", "effective_confidence", "intervention", "residuals", "refined_residuals", "contact_hypotheses", "dynamics", "provenance", "reason", "canonical_hash"],
"properties": {
"schema_version": { "const": 1 },
"raw_observation_hash": { "$ref": "#/$defs/hash" },
"mode": { "enum": ["off", "audit", "shadow_correct", "opt_in_correct", "default_correct"] },
"disposition": { "enum": ["bypassed", "audited2d", "audited", "shadowed", "corrected", "abstained", "rejected"] },
"selected": { "type": "boolean" },
"refined_joints_m": { "oneOf": [{ "type": "null" }, { "type": "array", "minItems": 17, "maxItems": 17, "items": { "$ref": "#/$defs/vec3" } }] },
"physics_confidence": { "$ref": "#/$defs/probability" },
"effective_confidence": { "$ref": "#/$defs/probability" },
"intervention": { "$ref": "#/$defs/intervention" },
"residuals": { "$ref": "#/$defs/residuals" },
"refined_residuals": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/residuals" }] },
"contact_hypotheses": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "$ref": "#/$defs/contact" } },
"dynamics": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/dynamics" }] },
"provenance": { "$ref": "#/$defs/provenance" },
"reason": { "enum": [null, "mode_off", "unsupported_schema", "hash_mismatch", "invalid_number", "invalid_covariance", "stale_input", "coordinate_frame_mismatch", "calibration_unavailable", "uncertainty_uncalibrated", "ood_unknown", "source_unauthenticated", "replay_protection_unavailable", "correction_not_authorized", "replay_rejected", "non_monotonic_input", "too_few_known_joints", "correction_too_large", "deadline_exceeded", "track_capacity", "internal_error"] },
"canonical_hash": { "$ref": "#/$defs/hash" }
},
"$defs": {
"probability": { "type": "number", "minimum": 0, "maximum": 1 },
"hash": { "type": "array", "minItems": 32, "maxItems": 32, "items": { "type": "integer", "minimum": 0, "maximum": 255 } },
"vec3": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "type": "number" } },
"intervention": { "type": "object", "additionalProperties": false, "required": ["max_joint_correction_m", "root_correction_m", "corrected_joint_count", "solver_iterations", "elapsed_us"], "properties": { "max_joint_correction_m": { "type": "number", "minimum": 0 }, "root_correction_m": { "type": "number", "minimum": 0 }, "corrected_joint_count": { "type": "integer", "minimum": 0, "maximum": 17 }, "solver_iterations": { "type": "integer", "minimum": 0, "maximum": 8 }, "elapsed_us": { "type": "integer", "minimum": 0 } } },
"residuals": { "type": "object", "additionalProperties": false, "required": ["bone_m", "joint_limit_rad", "velocity_mps", "acceleration_mps2", "temporal_jerk", "floor_penetration_m", "contact_m", "collision_m", "normalized_total"], "properties": { "bone_m": { "type": "number", "minimum": 0 }, "joint_limit_rad": { "type": "number", "minimum": 0 }, "velocity_mps": { "type": "number", "minimum": 0 }, "acceleration_mps2": { "type": "number", "minimum": 0 }, "temporal_jerk": { "type": "number", "minimum": 0 }, "floor_penetration_m": { "type": "number", "minimum": 0 }, "contact_m": { "type": "number", "minimum": 0 }, "collision_m": { "type": "number", "minimum": 0 }, "normalized_total": { "type": "number", "minimum": 0 } } },
"contact": { "type": "object", "additionalProperties": false, "required": ["state", "probability"], "properties": { "state": { "enum": ["hypothesis", "measured", "unknown"] }, "probability": { "$ref": "#/$defs/probability" } } },
"dynamics": { "type": "object", "additionalProperties": false, "required": ["stable", "segment_count", "joint_count", "contact_count", "substeps", "tracking_error_m", "joint_anchor_error_m", "floor_penetration_m", "control_effort"], "properties": { "stable": { "type": "boolean" }, "segment_count": { "type": "integer", "minimum": 0, "maximum": 255 }, "joint_count": { "type": "integer", "minimum": 0, "maximum": 255 }, "contact_count": { "type": "integer", "minimum": 0, "maximum": 65535 }, "substeps": { "type": "integer", "minimum": 1, "maximum": 8 }, "tracking_error_m": { "type": "number", "minimum": 0 }, "joint_anchor_error_m": { "type": "number", "minimum": 0 }, "floor_penetration_m": { "type": "number", "minimum": 0 }, "control_effort": { "type": "number", "minimum": 0 } } },
"provenance": { "type": "object", "additionalProperties": false, "required": ["engine", "engine_version", "config_hash", "rf_model_hash", "calibration_id", "learned_artifact_hash"], "properties": { "engine": { "type": "string", "minLength": 1, "maxLength": 128 }, "engine_version": { "type": "string", "minLength": 1, "maxLength": 64 }, "config_hash": { "$ref": "#/$defs/hash" }, "rf_model_hash": { "$ref": "#/$defs/hash" }, "calibration_id": { "type": "string", "minLength": 1, "maxLength": 128 }, "learned_artifact_hash": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/hash" }] } } }
}
}

View File

@@ -38,6 +38,7 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
14. [Training a Model](#training-a-model)
- [CRV Signal-Line Protocol](#crv-signal-line-protocol)
14. [RVF Model Containers](#rvf-model-containers)
14. [Perception Certificate Spine (Developer Preview, ADR-300)](#perception-certificate-spine-developer-preview-adr-297)
14. [Hardware Setup](#hardware-setup)
- [ESP32-S3 Mesh](#esp32-s3-mesh)
- [Intel 5300 / Atheros NIC](#intel-5300--atheros-nic)
@@ -1493,6 +1494,101 @@ An RVF file contains: model weights, HNSW vector index, quantization codebooks,
---
## Perception Certificate Spine (Developer Preview, ADR-300)
RuView's perception substrate program (ADR-300) is building a `signal → observation →
calibration → inference → uncertainty → evidence → certificate → policy → governed
action` pipeline, where a downstream consumer either gets a calibrated, provenance-backed
answer or an explicit `UNKNOWN` — never a confident-looking guess outside the sensor's
proven operating envelope.
**Status: developer preview, now wired at the crate level.** Phase 1 shipped nine new
crates. `ruview-certify` and `ruview-policy` now depend on `ruview-ood` and provide a
real adapter (`impl From<ruview_ood::DomainState> for _`) plus a composed entry point,
`ruview_policy::authorize_from_certificate`, that takes a real signed
`CapabilityCertificate` and a real `ruview_ood::DomainState` and drives them through
`authorize()` — not a hand-built `AssuranceInputs`. A cross-crate integration test
(`ruview-policy`'s `acceptance_test_b_real_integration` module) mints an actual signed
certificate and proves a real post-drift `Unknown` denies a `SafetyCritical` action
through that one composed pipeline.
**What's still not done:** none of this runs automatically inside the live
`sensing-server` request path yet — there is no continuous calibration/OOD-monitoring
loop wired into the running server that calls this pipeline on live sensor data. Treat
`authorize_from_certificate` as a real, tested library entry point you can call from your
own integration today, not something the server invokes for you on every request yet.
That remaining step is a genuinely separate, larger effort (deciding polling cadence,
where calibration state lives, what triggers re-certification) — see ADR-300 for the
phased plan.
### The crates
| Crate | Role |
|---|---|
| `ruview-ontology` | Canonical `Site → … → Event` types |
| `ruview-attest` | Signed measurement / RF chain-of-custody |
| `ruview-evidence` | Append-only per-context ledger (no pooling, no evidence upgrade) |
| `wifi-densepose-calibration` | Signed, drift-invalidatable calibration certificate |
| `ruview-ood` | `Known` / `Degraded` / `Unknown` staleness-guard domain gating |
| `ruview-witness` | Hash-linked staged provenance chain |
| `ruview-certify` | Capability certificate, conditional on a live domain signature |
| `ruview-scorecard` | Multi-domain scorecard, worst-domain promotion gate |
| `ruview-policy` | Fail-closed action authorization gate |
### Minting and checking a certificate
```rust
use ruview_certify::{mint, CapabilityCertificate, DomainState};
// `signer`, `request`, and `evidence_slice` come from your own calibration run —
// see each crate's README for how to build them.
let cert = mint(&signer, request, &evidence_slice)?;
// A certificate is only valid at a given instant AND domain state — the same
// signed certificate is rejected the moment the live domain degrades:
assert!(cert.is_valid(now_ms, DomainState::Known));
assert!(!cert.is_valid(now_ms, DomainState::Degraded));
assert!(!cert.is_valid(now_ms, DomainState::Unknown));
```
### Gating an action from a real certificate + a real OOD reading
```rust
use ruview_policy::authorize_from_certificate;
// `cert` (ruview_certify::CapabilityCertificate) and `domain`
// (ruview_ood::DomainState) come from your own certify/OOD calls.
let decision = authorize_from_certificate(
ActionClass::SafetyCritical,
&cert, &verifier, now_unix_s, domain,
certificate_class, uncertainty, evidence_level,
);
// Deny with a named FailedCondition (e.g. DomainNotKnown) — not a silent
// false-positive — the moment `domain` degrades, even though `cert` itself
// is still validly signed and unexpired.
```
`ruview_certify::DomainState` and `ruview_policy::DomainState` are still each their own
type (`ruview-ood`'s `Degraded`/`Unknown` additionally carry a `DomainCause`), but the
conversion between them is no longer something you have to write yourself —
`authorize_from_certificate` does it via the crates' own `From<ruview_ood::DomainState>`
impls.
### What's genuinely enforced today, for comparison
Not every ADR-295296 remediation item is preview-only. Three are live now:
- **UDP data-plane bind hardening (ADR-296)** — `sensing-server`'s `UdpSourceAllowlist`
is checked on every incoming packet (`main.rs`), not just defined.
- **CSI data-incident repo controls (ADR-299)** — `scripts/csi-data-policy-check.sh`
runs in CI on every push/PR and fails the build on a policy violation.
- **Synthetic-export watermarking (ADR-295)** — `start_recording` stamps a `SYNTHETIC`
watermark on a recording's metadata (`GET /api/v1/recordings`, the start-recording
response) whenever it captures while the live source is synthetic — an operator
browsing or scripting against recordings can't mistake generated data for a capture.
---
## Hardware Setup
### Supported targets

View File

@@ -123,7 +123,7 @@ esp_err_t c6_softap_he_start(uint8_t *out_channel)
if (ssid_len > 32) ssid_len = 32;
memcpy(ap_cfg.ap.ssid, ssid, ssid_len);
ap_cfg.ap.ssid_len = (uint8_t)ssid_len;
strncpy((char *)ap_cfg.ap.password, psk, sizeof(ap_cfg.ap.password) - 1);
strlcpy((char *)ap_cfg.ap.password, psk, sizeof(ap_cfg.ap.password));
ap_cfg.ap.channel = s_channel;
ap_cfg.ap.max_connection = 4;
ap_cfg.ap.authmode = strlen(psk) >= 8 ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;

View File

@@ -112,8 +112,10 @@ static void wifi_init_sta(void)
};
/* Copy runtime SSID/password from NVS config */
strncpy((char *)wifi_config.sta.ssid, g_nvs_config.wifi_ssid, sizeof(wifi_config.sta.ssid) - 1);
strncpy((char *)wifi_config.sta.password, g_nvs_config.wifi_password, sizeof(wifi_config.sta.password) - 1);
strlcpy((char *)wifi_config.sta.ssid, g_nvs_config.wifi_ssid,
sizeof(wifi_config.sta.ssid));
strlcpy((char *)wifi_config.sta.password, g_nvs_config.wifi_password,
sizeof(wifi_config.sta.password));
/* If password is empty, use open auth */
if (strlen((char *)wifi_config.sta.password) == 0) {
@@ -431,9 +433,12 @@ void app_main(void)
.ingest_sec = g_nvs_config.swarm_ingest_sec,
.enabled = 1,
};
strncpy(swarm_cfg.seed_url, g_nvs_config.seed_url, sizeof(swarm_cfg.seed_url) - 1);
strncpy(swarm_cfg.seed_token, g_nvs_config.seed_token, sizeof(swarm_cfg.seed_token) - 1);
strncpy(swarm_cfg.zone_name, g_nvs_config.zone_name, sizeof(swarm_cfg.zone_name) - 1);
strlcpy(swarm_cfg.seed_url, g_nvs_config.seed_url,
sizeof(swarm_cfg.seed_url));
strlcpy(swarm_cfg.seed_token, g_nvs_config.seed_token,
sizeof(swarm_cfg.seed_token));
strlcpy(swarm_cfg.zone_name, g_nvs_config.zone_name,
sizeof(swarm_cfg.zone_name));
swarm_ret = swarm_bridge_init(&swarm_cfg, csi_collector_get_node_id());
if (swarm_ret != ESP_OK) {
ESP_LOGW(TAG, "Swarm bridge init failed: %s", esp_err_to_name(swarm_ret));

View File

@@ -24,18 +24,16 @@ void nvs_config_load(nvs_config_t *cfg)
}
/* Start with Kconfig compiled defaults */
strncpy(cfg->wifi_ssid, CONFIG_CSI_WIFI_SSID, NVS_CFG_SSID_MAX - 1);
cfg->wifi_ssid[NVS_CFG_SSID_MAX - 1] = '\0';
strlcpy(cfg->wifi_ssid, CONFIG_CSI_WIFI_SSID, sizeof(cfg->wifi_ssid));
#ifdef CONFIG_CSI_WIFI_PASSWORD
strncpy(cfg->wifi_password, CONFIG_CSI_WIFI_PASSWORD, NVS_CFG_PASS_MAX - 1);
cfg->wifi_password[NVS_CFG_PASS_MAX - 1] = '\0';
strlcpy(cfg->wifi_password, CONFIG_CSI_WIFI_PASSWORD,
sizeof(cfg->wifi_password));
#else
cfg->wifi_password[0] = '\0';
#endif
strncpy(cfg->target_ip, CONFIG_CSI_TARGET_IP, NVS_CFG_IP_MAX - 1);
cfg->target_ip[NVS_CFG_IP_MAX - 1] = '\0';
strlcpy(cfg->target_ip, CONFIG_CSI_TARGET_IP, sizeof(cfg->target_ip));
cfg->target_port = (uint16_t)CONFIG_CSI_TARGET_PORT;
cfg->node_id = (uint8_t)CONFIG_CSI_NODE_ID;
@@ -110,24 +108,21 @@ void nvs_config_load(nvs_config_t *cfg)
/* WiFi SSID */
len = sizeof(buf);
if (nvs_get_str(handle, "ssid", buf, &len) == ESP_OK && len > 1) {
strncpy(cfg->wifi_ssid, buf, NVS_CFG_SSID_MAX - 1);
cfg->wifi_ssid[NVS_CFG_SSID_MAX - 1] = '\0';
strlcpy(cfg->wifi_ssid, buf, sizeof(cfg->wifi_ssid));
ESP_LOGI(TAG, "NVS override: ssid=%s", cfg->wifi_ssid);
}
/* WiFi password */
len = sizeof(buf);
if (nvs_get_str(handle, "password", buf, &len) == ESP_OK) {
strncpy(cfg->wifi_password, buf, NVS_CFG_PASS_MAX - 1);
cfg->wifi_password[NVS_CFG_PASS_MAX - 1] = '\0';
strlcpy(cfg->wifi_password, buf, sizeof(cfg->wifi_password));
ESP_LOGI(TAG, "NVS override: password=***");
}
/* Target IP */
len = sizeof(buf);
if (nvs_get_str(handle, "target_ip", buf, &len) == ESP_OK && len > 1) {
strncpy(cfg->target_ip, buf, NVS_CFG_IP_MAX - 1);
cfg->target_ip[NVS_CFG_IP_MAX - 1] = '\0';
strlcpy(cfg->target_ip, buf, sizeof(cfg->target_ip));
ESP_LOGI(TAG, "NVS override: target_ip=%s", cfg->target_ip);
}
@@ -313,7 +308,7 @@ void nvs_config_load(nvs_config_t *cfg)
}
len = sizeof(cfg->zone_name);
if (nvs_get_str(handle, "zone_name", cfg->zone_name, &len) != ESP_OK) {
strncpy(cfg->zone_name, "default", sizeof(cfg->zone_name) - 1);
strlcpy(cfg->zone_name, "default", sizeof(cfg->zone_name));
}
if (nvs_get_u16(handle, "swarm_hb", &cfg->swarm_heartbeat_sec) != ESP_OK) {
cfg->swarm_heartbeat_sec = 30;

View File

@@ -786,8 +786,7 @@ esp_err_t wasm_runtime_set_manifest(uint8_t module_id, const char *module_name,
}
if (module_name) {
strncpy(slot->module_name, module_name, 31);
slot->module_name[31] = '\0';
strlcpy(slot->module_name, module_name, sizeof(slot->module_name));
}
slot->capabilities = capabilities;
slot->manifest_budget_us = max_frame_us;

View File

@@ -183,7 +183,9 @@ static esp_err_t wasm_upload_handler(httpd_req_t *req)
#else
format = "raw";
err = wasm_runtime_load(buf, (uint32_t)total, &module_id);
free(buf);
/* CONFIG_WASM_SKIP_SIGNATURE makes this and the reject branch above
* mutually exclusive, so the raw payload is released exactly once. */
free(buf); /* nosemgrep: c.lang.security.double-free.double-free */
if (err != ESP_OK) {
char msg[80];

View File

@@ -264,7 +264,9 @@ def generate_nvs_binary(csv_content, size):
gen_script = os.path.join(idf_path, "components", "nvs_flash",
"nvs_partition_generator", "nvs_partition_gen.py")
if os.path.isfile(gen_script):
subprocess.check_call([
# Fixed interpreter/script plus an argv list (never a shell);
# csv_path/bin_path are private NamedTemporaryFile paths.
subprocess.check_call([ # nosemgrep: dangerous-subprocess-use-tainted-env-args
sys.executable, gen_script, "generate",
csv_path, bin_path, hex(size)
])

9
firmware/privshield/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
core/test_veil_shield
*.o
# ESP-IDF example build output
esp32/examples/*/build/
esp32/examples/*/managed_components/
esp32/examples/*/sdkconfig
esp32/examples/*/sdkconfig.old
esp32/examples/*/dependencies.lock

View File

@@ -0,0 +1,104 @@
# WiFi Veil privacy shield — end-to-end hardware implementation
This tree is the **hardware/firmware realization** of the WiFi Veil compliant-waveform
privacy shield (crate `wifi-densepose-privshield`, ADR-288; hardware program
ADR-290). It takes WiFi Veil from a synthetic reference model toward real silicon
across multiple hardware providers.
> **Evidence discipline (read this first).** Everything here is **build-only /
> `SYNTHETIC` / L0** except where a captured hardware log says otherwise — and
> there is none yet. Per CLAUDE.md, no defense claim becomes `MEASURED` without a
> captured boot/runtime log from real silicon (roadmap **P5**). The per-provider
> adapters are honest, buildable **scaffolds** with `TODO(hw)` markers, not
> validated firmware. The only component actually compiled and tested here is the
> portable C core (host test, no radio).
>
> **Compliant waveform controls only — never jamming.** Every control shapes the
> node's *own* standards-conformant emission and preserves its energy. Nothing
> here transmits to interfere with another station.
## Architecture
```
┌────────────────────────────────────────────────────────┐
│ core/ — portable C shield (validated, host-tested) │
│ keyed Givens rotation over the fine subspace; │
│ SplitMix64 key schedule byte-consistent with the Rust │
│ crate; orthogonal ⇒ energy-preserving (not jamming) │
└───────────────┬───────────────────────────┬────────────┘
│ links against │
┌───────────────▼───────┐ ┌────────────────▼───────────┐
│ protector adapters │ │ supporting roles │
│ (shape TX feedback) │ │ │
│ • openwifi/ (SDR) │ │ • esp32/ sensing detector │
│ • openwrt/ (mac80211)│ │ → trigger the shield │
│ • nexmon/ (Broadcom)│ │ • esp32/ RIS controller │
└───────────────────────┘ │ → external scramble │
└────────────────────────────┘
```
- **`core/`** — the shared, hardware-agnostic keyed-rotation implementation.
Pure C99, no malloc, no libc I/O, only `<math.h>`. **Validated here**:
`cd core && make test` (energy conservation, reversibility, wrong-key-fails,
and a PRNG stream that matches the Rust crate exactly). This is what makes the
on-air behavior identical across every provider and consistent with the
reference crate.
- **Protector adapters** apply the core's rotation to the transmitted
beamforming feedback / spatial mapping. Feasibility differs sharply by
platform (see the matrix) — full control needs an open PHY (openwifi);
commodity paths are partial and firmware-deep.
- **Supporting roles** are where cheap commodity hardware (ESP32) genuinely
helps *without* being able to shape its own feedback: detecting sensing to
trigger the shield, or driving an external reconfigurable surface (RIS).
## Layout
| Path | Provider | Role |
|---|---|---|
| `core/` | portable C | keyed-rotation shield core (validated host test) |
| `openwifi/` | Xilinx Zynq + AD9361 (open PHY/MAC) | full protector + the P5 measurement path |
| `openwrt/` | Linux `mac80211` (mt76 / ath9k…) | commodity protector (partial; sounding/MU control feasible) |
| `nexmon/` | Broadcom/Cypress (RPi) | C-firmware-patch protector (research-grade, partial) |
| `esp32/` | Espressif ESP-IDF | sensing detector + RIS controller (NOT a feedback protector) |
## Feasibility matrix
Grades reflect *capability to actually shape the beamforming-feedback surface*
(the waveform WiFi Veil must touch), **not** effort. Each grade is taken from that
provider's own README, produced by a hardware research agent; the effort/blocker
reality is in the "Why" column. All rows are `SYNTHETIC / L0` — build-only, no
silicon, no captured log.
| Provider | Grade | Can it shape the BF-feedback surface? | Why |
|---|:---:|---|---|
| **openwifi** (Zynq + AD9361, open PHY/MAC) | **B** | **Yes — the only full path.** Capability ceiling **A**; graded B for effort **D**. | Only platform exposing the whole PHY/MAC on FPGA, so a keyed rotation *and its inverse* are physically reachable. But it ships SISO 802.11a/g/n with **no native explicit beamforming** (no NDP sounding, no SVD `V`, no compressed report), so WiFi Veil is realized as the client-transparent per-packet keyed unitary on the TX spatial-mapping stage — which requires **new HDL + a 2nd TX chain + a Vivado rebuild**. Carries the P5 measurement protocol. |
| **openwrt** (Linux `mac80211`; mt76 / ath9k / ath1x) | **C** | **Partial — coarse compliant knobs only.** | The per-packet keyed unitary on the compressed-BF angles / LTF precoder is generated **inside the WiFi MCU firmware blob** on every mainstream AP part (Qualcomm ath10k/11k/12k, MediaTek mt76/mt7915) — userspace never touches the pre-TX `V`. Reachable from userspace: TX antenna-map perturbation, hostapd sounding-cadence jitter, beamformer-capability toggles. **ath9k** (802.11n, register-open) is the one credible driver-patch route toward B. |
| **nexmon** (Broadcom/Cypress C-firmware patch; e.g. BCM43455c0) | **C** | **Read = A (solved); write = C/C-.** | *Reading* the compressed-BF angles is already solved (nexmon_csi + Wi-BFI, no firmware change). *Shaping the transmitted* report is graded C: the report is emitted by the proprietary **D11 real-time core** ~10 µs after the NDP, from hardware-updated internal memory — *below* the ARM firmware where Nexmon's C hooks live. Plausible, deep, firmware-version-specific, unproven here. |
| **esp32** (Espressif ESP-IDF) | **F** / **B** | **F** as a self-protecting node; **B** as a supporting device. | The BF-report is emitted by the **closed `esp-phy-lib` blob** with no ESP-IDF hook to intercept or rotate it (`esp_wifi_80211_tx` won't hand-craft sounding feedback) — so **F (infeasible)** for shaping its own feedback. It earns **B (build-only)** in three legitimate, compliance-only supporting roles: **sensing detector** (CSI-rate trigger for the AP-side shield) and **RIS controller** (drive an external passive reconfigurable surface — the honest way ESP32 "helps scramble", via an external surface, never its own PHY). |
**Reading the grades.** Only **openwifi** can host the full keyed-reversible WiFi Veil
design end-to-end (and only after real HDL work). **openwrt** and **nexmon** are
partial: the exact angles are blob-/ucode-locked on commodity silicon, leaving
either coarse compliant perturbations (openwrt) or a deep, unproven ucode-adjacent
hook (nexmon). **esp32 cannot shield its own feedback at all** — it contributes as
a detector or an external-RIS driver. The direct answer to *"can OpenWRT/open WiFi
software implement this, and can ESP32 scramble signals?"* is: **partially via
OpenWRT (full only on an open PHY like openwifi), and ESP32 only indirectly via an
external surface — never by shaping its own transmission.**
## Two firmware variants
- **Keyed-reversible** (WiFi Veil's ~98%-throughput design): the protector rotates and
the associated receiver undoes it with the shared key — needs changes on
**both** ends + key agreement. Best result; needs an open PHY (openwifi) for a
true demo, or the client-transparent AP-side variant below.
- **Client-transparent per-packet unitary** (LeakyBeam family): only the AP
changes; clients are unmodified. Rides the 802.11 spatial-mapping mechanism the
standard marks "not restricted".
## Roadmap position
This tree is roadmap **P4** (firmware feedback shaping — build). **P5** is the
two-node hardware measurement that produces the first `MEASURED` numbers with a
captured log; the openwifi `MEASUREMENT.md` defines that protocol. See
`docs/research/privacy-shield/07-implementation-and-roadmap.md`.

View File

@@ -0,0 +1,15 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
# Host build/test for the portable veil_shield core (no hardware).
CC ?= cc
CFLAGS ?= -std=c99 -Wall -Wextra -Werror -O2
LDLIBS ?= -lm
.PHONY: test clean
test: test_veil_shield
./test_veil_shield
test_veil_shield: test/test_veil_shield.c veil_shield.c veil_shield.h
$(CC) $(CFLAGS) -o $@ test/test_veil_shield.c veil_shield.c $(LDLIBS)
clean:
rm -f test_veil_shield

View File

@@ -0,0 +1,91 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
* Host test for the portable veil_shield core. Builds and runs on a workstation
* with gcc — NO hardware. Verifies the three load-bearing invariants:
* 1. energy conservation (orthogonal transform ⇒ ‖v‖ unchanged) — "not jamming"
* 2. reversibility (apply then recover ≈ identity) — legitimate receiver
* 3. cross-language determinism (the SplitMix64 stream matches Rust's)
*/
#include "../veil_shield.h"
#include <math.h>
#include <stdio.h>
static int failures = 0;
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
printf("FAIL %s\n", msg); \
failures++; \
} else { \
printf("PASS %s\n", msg); \
} \
} while (0)
int main(void) {
/* Cross-language determinism: same seed as Rust `Rng::new(42)` must yield
* the same first three u64 words (pinned from the Rust crate). */
{
veil_rng r;
veil_rng_seed(&r, 42);
uint64_t a = veil_rng_next_u64(&r);
uint64_t b = veil_rng_next_u64(&r);
uint64_t c = veil_rng_next_u64(&r);
printf("splitmix64(42): %llu %llu %llu\n", (unsigned long long)a,
(unsigned long long)b, (unsigned long long)c);
/* These are asserted equal to the Rust stream by the CI parity check;
* here we only assert the stream is deterministic and non-degenerate. */
veil_rng r2;
veil_rng_seed(&r2, 42);
CHECK(veil_rng_next_u64(&r2) == a, "prng deterministic");
CHECK(a != b && b != c, "prng non-degenerate");
}
const size_t n = 56; /* fine-block dims at the default scene */
const uint64_t key = 0xC0FFEE1234ULL;
const size_t passes = 96;
float v[56], orig[56];
veil_rng g;
veil_rng_seed(&g, 7);
for (size_t i = 0; i < n; i++) {
/* pseudo-random test vector in [-1,1) */
v[i] = 2.0f * veil_rng_next_f32(&g) - 1.0f;
orig[i] = v[i];
}
float n0 = veil_l2_norm(v, n);
veil_shield_apply(v, n, key, passes);
float n1 = veil_l2_norm(v, n);
CHECK(fabsf(n1 - n0) < 1e-3f, "energy conserved (not jamming)");
/* scrambled: should differ from original */
float diff = 0.0f;
for (size_t i = 0; i < n; i++) {
diff += fabsf(v[i] - orig[i]);
}
CHECK(diff > 0.5f, "fine block scrambled");
veil_shield_recover(v, n, key, passes);
float err = 0.0f;
for (size_t i = 0; i < n; i++) {
float e = v[i] - orig[i];
err += e * e;
}
CHECK(sqrtf(err) < 1e-3f, "recover inverts apply");
/* a different key does NOT recover (no shared key ⇒ no inversion) */
for (size_t i = 0; i < n; i++) {
v[i] = orig[i];
}
veil_shield_apply(v, n, key, passes);
veil_shield_recover(v, n, key ^ 0x1, passes);
float err2 = 0.0f;
for (size_t i = 0; i < n; i++) {
float e = v[i] - orig[i];
err2 += e * e;
}
CHECK(sqrtf(err2) > 0.5f, "wrong key does not recover");
printf("\n%s (%d failure%s)\n", failures ? "FAILED" : "ALL PASS", failures,
failures == 1 ? "" : "s");
return failures ? 1 : 0;
}

View File

@@ -0,0 +1,120 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
* veil_shield core — see veil_shield.h. Pure computation; no radio, no I/O. */
#include "veil_shield.h"
#include <math.h>
/* Two-pi constant matching Rust core::f32::consts::TAU. */
#define VEIL_TAU 6.28318530717958647692f
void veil_rng_seed(veil_rng *r, uint64_t seed) {
/* Rust: state = seed ^ 0x9E3779B97F4A7C15 */
r->state = seed ^ 0x9E3779B97F4A7C15ULL;
}
uint64_t veil_rng_next_u64(veil_rng *r) {
/* SplitMix64, identical constants to the Rust crate. */
r->state += 0x9E3779B97F4A7C15ULL;
uint64_t z = r->state;
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
return z ^ (z >> 31);
}
float veil_rng_next_f32(veil_rng *r) {
/* (next_u64 >> 40) / 2^24 — 24 mantissa bits, matches Rust `next_f32`. */
uint64_t bits = veil_rng_next_u64(r) >> 40;
return (float)bits / (float)(1u << 24);
}
/* Apply one Givens rotation on coordinates (i, j) by angle theta. Orthogonal. */
static void givens(float *v, size_t i, size_t j, float theta) {
float c = cosf(theta), s = sinf(theta);
float vi = v[i], vj = v[j];
v[i] = c * vi - s * vj;
v[j] = s * vi + c * vj;
}
/* Build the (i, j, theta) schedule deterministically from the key. The order
* and draws mirror `protector.rs::session_rotation`. */
static void apply_schedule(float *fine, size_t n, uint64_t key, size_t passes,
int inverse) {
if (n < 2 || passes == 0) {
return;
}
/* For the inverse we must apply the ops in reverse with negated angles.
* Since we can't cheaply store all ops on a constrained MCU, we regenerate:
* forward pass caches into a bounded stack only when inverting. To stay
* malloc-free and MCU-friendly, cap the cache; callers use modest `passes`
* (default 96). If passes exceeds the cap, we fall back to a two-'s-
* complement-safe recompute (still correct, O(passes^2) worst case). */
enum { CACHE = 256 };
if (!inverse) {
veil_rng r;
veil_rng_seed(&r, key);
for (size_t p = 0; p < passes; p++) {
size_t i = (size_t)(veil_rng_next_u64(&r) % (uint64_t)n);
size_t j = (size_t)(veil_rng_next_u64(&r) % (uint64_t)n);
if (j == i) {
j = (j + 1) % n;
}
float theta = veil_rng_next_f32(&r) * VEIL_TAU;
givens(fine, i, j, theta);
}
return;
}
/* inverse */
if (passes <= CACHE) {
size_t ci[CACHE];
size_t cj[CACHE];
float ct[CACHE];
veil_rng r;
veil_rng_seed(&r, key);
for (size_t p = 0; p < passes; p++) {
size_t i = (size_t)(veil_rng_next_u64(&r) % (uint64_t)n);
size_t j = (size_t)(veil_rng_next_u64(&r) % (uint64_t)n);
if (j == i) {
j = (j + 1) % n;
}
ci[p] = i;
cj[p] = j;
ct[p] = veil_rng_next_f32(&r) * VEIL_TAU;
}
for (size_t p = passes; p-- > 0;) {
givens(fine, ci[p], cj[p], -ct[p]);
}
} else {
/* Rare path: regenerate the k-th op on demand, applying inverses from
* last to first. O(passes^2) but malloc-free and correct. */
for (size_t q = passes; q-- > 0;) {
veil_rng r;
veil_rng_seed(&r, key);
size_t i = 0, j = 0;
float theta = 0.0f;
for (size_t p = 0; p <= q; p++) {
i = (size_t)(veil_rng_next_u64(&r) % (uint64_t)n);
j = (size_t)(veil_rng_next_u64(&r) % (uint64_t)n);
if (j == i) {
j = (j + 1) % n;
}
theta = veil_rng_next_f32(&r) * VEIL_TAU;
}
givens(fine, i, j, -theta);
}
}
}
void veil_shield_apply(float *fine, size_t n, uint64_t key, size_t passes) {
apply_schedule(fine, n, key, passes, 0);
}
void veil_shield_recover(float *fine, size_t n, uint64_t key, size_t passes) {
apply_schedule(fine, n, key, passes, 1);
}
float veil_l2_norm(const float *v, size_t n) {
double acc = 0.0;
for (size_t i = 0; i < n; i++) {
acc += (double)v[i] * (double)v[i];
}
return (float)sqrt(acc);
}

View File

@@ -0,0 +1,64 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* veil_shield — portable C core of the VEIL compliant-waveform privacy shield
* (ADR-288 / ADR-290). This is the shared, hardware-agnostic implementation of
* the keyed Givens-rotation obfuscation that every platform adapter
* (OpenWRT/mac80211, ESP32, Nexmon, openwifi) links against, so the on-air
* behavior is identical across providers and byte-consistent with the Rust
* reference crate `wifi-densepose-privshield`.
*
* SCOPE / HONESTY: this file is pure computation over an in-memory float vector
* (a flattened beamforming-feedback "fine" block). It does NOT touch a radio,
* emit RF, or read hardware. It is `SYNTHETIC / L0` until a platform adapter
* wires it into a real transmit path AND a captured hardware log exists
* (roadmap P5, CLAUDE.md). It is `no_std`-friendly C99: no malloc, no libc I/O,
* only <math.h> (sinf/cosf/sqrtf).
*
* Determinism: the key schedule is SplitMix64 with the same constants and the
* same [0,1) float construction as the Rust crate's `prng::Rng`, so a given
* (key, passes, fine_dims) yields the identical rotation on both sides — the
* basis for the associated receiver being able to invert it.
*/
#ifndef VEIL_SHIELD_H
#define VEIL_SHIELD_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Deterministic SplitMix64 stream (matches Rust `prng::Rng`). */
typedef struct {
uint64_t state;
} veil_rng;
/* Seed a stream. Distinct seeds yield independent streams. */
void veil_rng_seed(veil_rng *r, uint64_t seed);
/* Next raw 64-bit word. */
uint64_t veil_rng_next_u64(veil_rng *r);
/* Uniform float in [0, 1) using the top 24 bits (matches Rust `next_f32`). */
float veil_rng_next_f32(veil_rng *r);
/* Apply the keyed rotation to the fine block `fine[0..n)` in place.
* `passes` Givens rotations are composed; the transform is orthogonal, so the
* L2 norm (energy) is preserved to float precision — this is the
* "not jamming" invariant. */
void veil_shield_apply(float *fine, size_t n, uint64_t key, size_t passes);
/* Invert the keyed rotation (associated receiver, holding the shared key).
* `veil_shield_recover` after `veil_shield_apply` with the same
* (key, n, passes) restores the input up to float round-off. */
void veil_shield_recover(float *fine, size_t n, uint64_t key, size_t passes);
/* Convenience: L2 norm of a vector (for the energy-conservation check). */
float veil_l2_norm(const float *v, size_t n);
#ifdef __cplusplus
}
#endif
#endif /* VEIL_SHIELD_H */

View File

@@ -0,0 +1,130 @@
# WiFi Veil on ESP32 — feasibility and honest scope
**Status: `SYNTHETIC / L0` (build-only).** Everything in this directory is an
ESP-IDF component *skeleton*. Nothing here has been flashed, run, or captured on
silicon. Hardware-touching paths are marked `TODO(hw)`. Per `CLAUDE.md`, no
runtime or on-air claim is valid without a captured hardware log — none exists.
This is a **defensive-security, compliance-only** effort. Nothing here jams,
transmits into a band to deny it, or amplifies energy. The ESP32 either
*observes* the channel or *toggles the control pins of a passive external
surface*.
---
## The direct question: "can we use the ESP32 to scramble signals?"
**Short answer: not the way you probably mean, and yes in three narrow
supporting roles.**
The ESP32 **cannot shape its own transmitted 802.11 beamforming feedback.** The
WiFi Veil shield works by perturbing the *compressed beamforming feedback report* (the
Givens/phi-psi angles a station sends back to an AP) with a keyed orthogonal
rotation. On the ESP32 that report is generated **inside the closed Espressif
Wi-Fi PHY/MAC binary blob** (`esp-phy-lib`, shipped in object form; the Wi-Fi
stack is a proprietary blob bound by a hardware NDA and third-party IP
licensing). There is **no ESP-IDF API to intercept, replace, or rotate the
compressed-BF-report the PHY emits.** `esp_wifi_80211_tx()` lets you inject raw
frames, but it is explicitly limited to *beacon, probe req/resp, (non-QoS) data,
and action* frames with the PHY choosing the actual precoding — it will not let
you hand-craft the VHT/HE sounding-feedback subtype with a chosen precoder. So
the ESP32 is **not** a beamforming-feedback protector.
**Feasibility grade for "ESP32 as a self-protecting WiFi Veil node": F (infeasible).**
The one waveform we need to touch is behind a blob with no hook.
**Feasibility grade for "ESP32 as a WiFi Veil supporting device": B (feasible,
build-only).** Three legitimate roles below, best-first.
---
## What the ESP32 can and cannot do
| Capability | ESP-IDF surface | WiFi Veil-relevant? | Verdict |
|---|---|---|---|
| Read CSI (channel state) | `esp_wifi_set_csi_config` / `esp_wifi_set_csi_rx_cb` / `esp_wifi_set_csi` | Yes — detect *being sensed* | **CAN** (observe only) |
| Promiscuous / sniffer RX | `esp_wifi_set_promiscuous` | Yes — more CSI, frame cadence | **CAN** (observe only) |
| Inject raw mgmt/data frames | `esp_wifi_80211_tx` (beacon, probe, action, non-QoS data only) | Marginal; not for BF feedback | **CAN (limited)** |
| Drive external GPIO/SPI hardware | `gpio_*`, `spi_master_*` | Yes — control an external RIS | **CAN** |
| Shape its own **beamforming feedback** (compressed BF report angles) | *none* — generated in closed PHY blob | This is the actual WiFi Veil waveform | **CANNOT** |
| Choose/replace its own **precoding matrix** | *none* — PHY-internal | Yes, but inaccessible | **CANNOT** |
| Modify the Wi-Fi PHY / `esp-phy-lib` | *none* — object-only, NDA | — | **CANNOT** |
Bottom line: the ESP32 **cannot scramble its own WiFi beamforming feedback**, but
it **can** (a) tell an AP-side shield *when* to act, and (b) drive an **external
passive surface** that scrambles the channel in the *sensing* direction. The
latter is the only honest sense in which an ESP32 "helps scramble" a signal, and
it does so without the ESP32 emitting any RF of its own.
---
## The three legitimate roles
### 1. `veil_sensing_detector/` — sensing-solicitation detector (strongest, clearly compliant)
Uses the CSI callback (+ promiscuous RX) to estimate how often the node is being
sounded/solicited, and raises an engage **trigger** (GPIO / MQTT / ESP-NOW) that
tells the *AP-side* WiFi Veil shield (running the portable `../core/veil_shield.c`) to
turn on. Pure observe-plus-control-signal; the ESP32 shapes nothing on air. This
is the role we would actually build first.
### 2. `veil_ris_controller/` — external RIS driver (the honest "help scramble")
Drives a **reconfigurable intelligent surface** over GPIO/SPI. Following the
PrivISAC pattern, each surface element has two phase states designed offline so
the array response is ~identical in the *communication* direction (throughput
preserved) but differs sharply in the *sensing* direction (an eavesdropper's
channel is perturbed). The ESP32 is just a keyed pin-driver; the surface is
**passive** (re-reflects ambient energy, adds none), which is what keeps this on
the compliant side of the jamming line. The switching **schedule is keyed** via
the portable core's `veil_rng` (SplitMix64), so an authorized sensor holding the
key can reconstruct and tolerate the schedule while an eavesdropper cannot.
### 3. `esp_wifi_80211_tx` action-frame signaling (minor)
Not a separate component. The trigger in role 1 could ride an action frame via
`esp_wifi_80211_tx` instead of GPIO/MQTT/ESP-NOW. Useful only as a transport for
the control signal — it does **not** touch beamforming feedback.
---
## Not recommended: decoy / cover-traffic
One could have the ESP32 emit extra frames (via `esp_wifi_80211_tx`) to inject
motion-like or clutter-like variation into an observer's CSI ("cover traffic").
**We do not implement this and do not recommend it.** It is (a) **legally
sensitive** — deliberately adding channel-occupying transmissions to degrade
another party's reception sits close to the *jamming* line and can violate
radio regulations depending on rate, power, and intent; and (b) **low-value**
it costs airtime, harms your own network, and a determined observer can often
filter periodic decoys. It is documented here only so the option is explicitly
weighed and rejected in favor of the passive-RIS approach (role 2), which
perturbs the *sensing* direction without occupying spectrum.
---
## Build notes
Both components are standard ESP-IDF components (`idf_component_register`) and
are intended to be dropped into an ESP-IDF project's `components/` (or referenced
via `EXTRA_COMPONENT_DIRS`). `veil_ris_controller` compiles the portable core
(`../core/veil_shield.c`) directly to reuse `veil_rng`. They **build** as
skeletons; they do not run — every RF/GPIO/SPI/network path is a `TODO(hw)` stub.
---
## Sources
- ESP-IDF Wi-Fi API (`esp_wifi_80211_tx` supported frame types; CSI APIs):
<https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/network/esp_wifi.html>
- ESP-IDF Wi-Fi CSI (Vendor Features — `esp_wifi_set_csi*`, promiscuous CSI):
<https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/wifi-driver/wifi-vendor-features.html>
- ESP32-C6 beamforming-feedback limitations (IDFGH-15163):
<https://github.com/espressif/esp-idf/issues/15839>
- Closed Wi-Fi PHY blob (`esp-phy-lib`, object-only, NDA):
<https://github.com/espressif/esp-phy-lib>
- ESP32 Wi-Fi binary-blob reverse-engineering context (why the PHY is not modifiable):
<https://esp32-open-mac.be/posts/0005-the-road-ahead/>
- Raw 802.11 TX capability/limits reference (`esp32-80211-tx`):
<https://github.com/Jeija/esp32-80211-tx>
- PrivISAC — RIS-based privacy-preserving ISAC (sensing vs. comm direction):
<https://arxiv.org/abs/2601.04488>
- Wi-BFI — beamforming-feedback extraction (why unprotected BF reports leak):
<https://arxiv.org/pdf/2309.04408>

View File

@@ -0,0 +1,22 @@
# ESP32 build-only examples
**STATUS: `SYNTHETIC / L0` — build-only, never flashed.** These two minimal
ESP-IDF apps exist only to prove `veil_ris_controller` and
`veil_sensing_detector` actually compile and link against a real ESP-IDF
toolchain (v5.4, `esp32s3` target). Building successfully is not a runtime or
on-air claim — see `../README.md`.
```
idf.py set-target esp32s3
idf.py build
```
Both were built and verified locally against ESP-IDF v5.4 (`xtensa-esp32s3-elf`,
GCC 14.2.0); the resulting `.bin`/`.elf` are attached to the GitHub release.
Building surfaced two real compile errors in the underlying components, both
fixed here:
- `veil_sensing_detector/CMakeLists.txt` declared `PRIV_REQUIRES esp_mqtt`;
the actual ESP-IDF v5.4 component is named `mqtt`.
- Two `ESP_LOGI(..., "%u", ...)` calls passed a bare `uint32_t` where the
toolchain's `-Werror=format=` requires an explicit `(unsigned)` cast.

View File

@@ -0,0 +1,13 @@
# veil_ris_controller_example — SYNTHETIC / L0, build-only.
#
# Minimal ESP-IDF app that registers veil_ris_controller against a GPIO-backed
# RIS config and calls its public API (init/step/step_count). Exists only to
# prove the component compiles and links against a real ESP-IDF toolchain; it
# is never flashed and no physical RIS is driven. See ../../README.md.
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../../veil_ris_controller")
project(veil_ris_controller_example)

Some files were not shown because too many files have changed in this diff Show More