Merge remote-tracking branch 'origin/main' into resolve-1579-conflict

# Conflicts:
#	docs/adr/README.md
This commit is contained in:
ruv
2026-08-11 13:04:00 -04:00
309 changed files with 20764 additions and 1222 deletions
+5 -5
View File
@@ -1,12 +1,12 @@
name: CSI data policy (ADR-296)
name: CSI data policy (ADR-299)
# ADR-296 repository CSI data-incident guard. Fails when CSI-format files
# 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-296).
# 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-296). Until they are removed this job is EXPECTED to
# 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).
@@ -45,7 +45,7 @@ jobs:
if: always()
run: |
{
echo '### CSI data policy (ADR-296)'
echo '### CSI data policy (ADR-299)'
echo ''
echo '```'
bash scripts/csi-data-policy-check.sh --tracked 2>&1 || true
+1 -1
View File
@@ -29,7 +29,7 @@ firmware/esp32-csi-node/test/*.obj
.swarm/
# CSI recordings (local training/capture data — CSI is person data per
# CLAUDE.md; never commit). Covers current and legacy layouts. See ADR-296.
# CLAUDE.md; never commit). Covers current and legacy layouts. See ADR-299.
data/recordings/
v2/data/recordings/
rust-port/wifi-densepose-rs/data/recordings/
@@ -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
```
@@ -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)
```
@@ -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.
```
@@ -1,4 +1,4 @@
# ADR-288: Public-benchmark evaluation harness — Widar3.0 ingest, standard split protocols, leakage guards
# ADR-291: Public-benchmark evaluation harness — Widar3.0 ingest, standard split protocols, leakage guards
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-10
@@ -1,4 +1,4 @@
# ADR-289: Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter and subcarrier-agnostic plumbing
# ADR-292: Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter and subcarrier-agnostic plumbing
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-10
@@ -1,4 +1,4 @@
# ADR-290: Vitals ground-truth rig — reference ingest, time alignment, and agreement metrics
# ADR-293: Vitals ground-truth rig — reference ingest, time alignment, and agreement metrics
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-10
@@ -69,7 +69,7 @@ Add a `groundtruth` module to `v2/crates/wifi-densepose-vitals`:
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-288's enforcement-in-types approach and the CLAUDE.md tagging rule.
ADR-291's enforcement-in-types approach and the CLAUDE.md tagging rule.
## Consequences
@@ -1,4 +1,4 @@
# ADR-291: WiFi Veil integration — emission-shaping countermeasure as an advisory BFLD dependency
# ADR-294: WiFi Veil integration — emission-shaping countermeasure as an advisory BFLD dependency
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-10
@@ -1,4 +1,4 @@
# ADR-292: Source provenance state machine — synthetic can never present as live
# ADR-295: Source provenance state machine — synthetic can never present as live
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-11
@@ -1,4 +1,4 @@
# ADR-293: Sensor data-plane hardening — UDP bind control and source allowlist (step one)
# ADR-296: Sensor data-plane hardening — UDP bind control and source allowlist (step one)
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-11
@@ -1,4 +1,4 @@
# ADR-294: Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state
# 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
@@ -46,7 +46,7 @@ mechanism RuView uses to reduce blind spots and room dependence:
- Schema change to `NodeInfo`/the MQTT contract; existing single-node
deployments keep working (one node = one inference). Consumers reading the
old aggregate-only shape need the migration accessor.
- Aligns with ADR-292 (freshness) and the review's call for one canonical
- Aligns with ADR-295 (freshness) and the review's call for one canonical
`NodeInference`/`RoomInference` contract.
## Validation
@@ -1,4 +1,4 @@
# ADR-295: Model release sanity gates — block degenerate and mislabeled model artifacts
# ADR-298: Model release sanity gates — block degenerate and mislabeled model artifacts
- **Status**: Accepted — initial implementation (this PR)
- **Date**: 2026-08-11
@@ -31,7 +31,7 @@ for any classifier artifact proposed for release, fails on:
- **Degenerate class balance** — predicted-positive rate at/above a ceiling
(e.g. > 99%) on a balanced probe set.
- **Missing/blank baseline** — a report without a paired mean-pose/majority
baseline (ties into ADR-288 `EvaluationReport`).
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.
@@ -1,4 +1,4 @@
# ADR-296: Repository CSI data-incident controls — ignore rules and a pre-commit/CI policy check
# 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
@@ -1,4 +1,4 @@
# ADR-297: RuView perception substrate — a phased program for the calibration, evidence, trust, and deployment layer
# 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
@@ -25,8 +25,8 @@ Several of these primitives already have foundations in the tree and should be
- 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-292 (provenance state machine), ADR-293 (authenticated
data plane, step one), ADR-295 (model sanity gates) — the first bricks.
- 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
@@ -51,20 +51,20 @@ Every child ADR and implementation is bound by these:
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-315/ADR-302). An unsigned or partially-bound certificate is
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-303). No transport- or UI-specific reinterpretation.
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-314).
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-299), not a one-time stamp.
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:
@@ -72,8 +72,8 @@ recalibration rather than silently continuing:
VALID → DEGRADED → UNKNOWN (auto-degrade on domain drift; triggers recalibration)
```
This binds ADR-298 (calibration), ADR-299 (OOD), ADR-315 (certificate), and
ADR-318 (policy): a degraded/unknown domain must invalidate the affected
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
@@ -82,7 +82,7 @@ capability *before* a false confident inference reaches an actuator.
- **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-313).
models, and environmental drift (ADR-316).
Certify and Trust are the parts that are hard to commoditize; presence
detection alone is not.
@@ -90,48 +90,48 @@ detection alone is not.
## Decision
Adopt a **21-primitive phased program**. Each primitive gets a child ADR
(ADR-298…ADR-318) that owns its detailed decision, status, and validation.
(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-298 | 1 |
| 2 | Out-of-distribution detection | ADR-299 | 1 |
| 3 | Ground-truth synchronization | ADR-300 | 2 |
| 4 | Evidence engine | ADR-301 | 1 |
| 5 | Authenticated sensor identity | ADR-302 | 1 |
| 6 | Canonical spatial ontology | ADR-303 | 1 |
| 7 | Persistent identity & tracking | ADR-304 | 2 |
| 8 | Sensor placement optimizer | ADR-305 | 3 |
| 9 | Active sensing | ADR-306 | 3 |
| 10 | 802.11bf-native architecture | ADR-307 | 2 |
| 11 | Real sensor fusion | ADR-308 | 2 |
| 12 | Long-term spatial memory | ADR-309 | 3 |
| 13 | Counterfactual inference | ADR-310 | 3 |
| 14 | Information-gain scheduler | ADR-311 | 3 |
| 15 | Digital RF twin | ADR-312 | 3 |
| 16 | Fleet control plane | ADR-313 | 2 |
| 17 | Real benchmark service (multi-domain scorecard) | ADR-314 | 1 |
| 18 | Capability certificates | ADR-315 | 1 |
| 19 | Witness chain | ADR-316 | 1 |
| 20 | RuView sensor HAL | ADR-317 | 2 |
| 21 | Decision policy — action authorization | ADR-318 | 1 |
| 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-303 spatial ontology ──┐
ADR-302 auth identity ─────┼──► ADR-298 calibration cert ──► ADR-299 OOD gating
ADR-306 spatial ontology ──┐
ADR-305 auth identity ─────┼──► ADR-301 calibration cert ──► ADR-302 OOD gating
│ │ │
└──► ADR-316 witness chain │ (VALID→DEGRADED→UNKNOWN)
└──► ADR-319 witness chain │ (VALID→DEGRADED→UNKNOWN)
│ ▼
ADR-301 evidence engine ──► ADR-315 capability certificate
ADR-304 evidence engine ──► ADR-318 capability certificate
│ │ (conditional on domain signature)
│ ▼
│ ADR-318 decision policy ──► governed action
└──► ADR-314 benchmark scorecard (per-PR gate)
│ ADR-321 decision policy ──► governed action
└──► ADR-317 benchmark scorecard (per-PR gate)
```
- **Phase 1 (the certificate spine, built now):** foundational roots 303, 302,
@@ -152,20 +152,20 @@ 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-317), calibrate the environment (ADR-298),
> quantify whether it can reliably sense the requested phenomenon (ADR-299),
> generate a signed capability certificate (ADR-315), expose governed spatial
> events (ADR-303), and return UNKNOWN whenever evidence falls outside that
> certificate (ADR-299).
> 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-299), invalidate the affected capability (ADR-315) **before** a false
> confident inference reaches an actuator (ADR-318 denies with the specific
> (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-316), and explain exactly which certificate condition failed.
> (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
@@ -176,7 +176,7 @@ closes the "identify the hardware" clause.
- One coherent substrate replaces overlapping ad-hoc schemas; every surface
(MQTT, REST, WebSocket, RuField, Matter, agents) eventually consumes the
ADR-303 ontology and the ADR-315 certificate.
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
@@ -186,4 +186,4 @@ closes the "identify the hardware" clause.
- 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-314).
AetherArena scenario (ADR-317).
@@ -1,19 +1,19 @@
# ADR-298: Automatic domain calibration — signed, versioned, invalidatable room fingerprint
# ADR-301: Automatic domain calibration — signed, versioned, invalidatable room fingerprint
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297) and the
first brick of that program's "certificate spine" (ADR-297 phase 1). It depends
on the canonical spatial ontology (ADR-303) to name *which space* it
characterizes, on authenticated sensor identity (ADR-302) to bind a fingerprint
to *which signed device* produced it, and on the witness chain (ADR-316) to
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-299).
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
@@ -43,7 +43,7 @@ than a soft `STALE` flag buried in the runtime.
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-299/ADR-315.
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.
@@ -68,7 +68,7 @@ phase and a `certificate` artifact module. The target UX is:
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-299 measures against; its schema is versioned.
ADR-302 measures against; its schema is versioned.
### 2. Empty / occupied baseline pair
@@ -82,17 +82,17 @@ phase and a `certificate` artifact module. The target UX is:
### 3. `CalibrationCertificate` artifact
- A serializable `CalibrationCertificate` binding: the `RoomFingerprint`; a
space identifier from the ADR-303 ontology; the signing sensor identity from
ADR-302; `captured_at_unix_s`; a monotonic `version`; a schema version; the
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-316). Signature
(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-299 uses
returns a bounded fingerprint distance, which is the primitive ADR-302 uses
to gate KNOWN → DEGRADED → UNKNOWN.
### 4. Invalidation and continuous drift compensation
@@ -101,7 +101,7 @@ phase and a `certificate` artifact module. The target UX is:
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-316), not a silent `STALE` flag.
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
@@ -121,14 +121,14 @@ phase and a `certificate` artifact module. The target UX is:
## Consequences
- Room characterization becomes a portable, signed, versioned artifact that
ADR-299 (OOD), ADR-315 (capability certificates), and ADR-314 (benchmark)
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-299, not a regression.
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.
@@ -142,7 +142,7 @@ phase and a `certificate` artifact module. The target UX is:
transitions (channel change, geometry change, age, drift-envelope breach)
each emit the expected witness record; constructor rejects synthetic→measured
mislabeling.
- Cross-ADR: an ADR-299 test consumes a certificate and asserts the gating
- 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
@@ -1,33 +1,33 @@
# ADR-299: Out-of-distribution detection — KNOWN / DEGRADED / UNKNOWN gating
# ADR-302: Out-of-distribution detection — KNOWN / DEGRADED / UNKNOWN gating
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297) and part
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-298): the `CalibrationCertificate` and its
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-301) and capability certificates
(ADR-315).
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-297) named explicitly.
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-298 produces a comparable `RoomFingerprint` and a `distance()` metric.
- 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
@@ -44,7 +44,7 @@ Today RuView has partial signals but no unified gate:
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-298 certificate.** Chosen.
against the ADR-301 certificate.** Chosen.
## Decision
@@ -56,14 +56,14 @@ path) — that attaches a `DomainState` to **every** inference.
Each inference carries four measured quantities:
1. **Domain distance** — fingerprint distance (ADR-298 `distance()`) between
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-303) and this signed device (ADR-302)? An
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.
@@ -90,11 +90,11 @@ 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-298
- 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-301) as part of every inference record, and are an input to the ADR-315
(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.
@@ -111,10 +111,10 @@ cause are all reported — never a bare label.
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-315
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-298 a hard dependency of confident inference — the
certificate, making ADR-301 a hard dependency of confident inference — the
intended coupling.
## Validation
@@ -125,9 +125,9 @@ cause are all reported — never a bare label.
device mismatch, and invalidated/absent certificate each → UNKNOWN;
hysteresis prevents flapping under injected noise; missing certificate
defaults to UNKNOWN.
- Cross-ADR: consumes an ADR-298 certificate and asserts a drifted fingerprint
- 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-301.
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
@@ -1,21 +1,21 @@
# ADR-300: Ground-truth synchronization — reference sensors as a formal validation plane
# ADR-303: Ground-truth synchronization — reference sensors as a formal validation plane
- **Status**: Accepted — initial implementation (ADR-297 phase 2)
- **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-297), authored
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-301) and the real benchmark service
(ADR-314). It generalizes the vitals ground-truth rig (ADR-290) from a single
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-290 built exactly
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
@@ -39,7 +39,7 @@ inputs.
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-290 ad hoc each time).** Rejected:
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
@@ -52,38 +52,38 @@ compares RF inference against independent observation and never feeds it.
### 1. Modality-agnostic reference ingest
- A `ReferenceObservation` generalizing ADR-290's `ReferenceSeries`: a
- 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-290's ingest
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-290's time alignment (bounded-lag normalized cross-correlation
- 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-303 spatial ontology so an RF localization/pose result and a
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-290's MAE/RMSE/bias/BlandAltman/within-tolerance;
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-288).
(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-290.
as in ADR-293.
### 4. Evidence and isolation guarantees
@@ -92,8 +92,8 @@ compares RF inference against independent observation and never feeds it.
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-290 rule). Reports feed the ADR-301 evidence
engine and are the substrate ADR-314 scores against.
coverage, and a reproducer (ADR-293 rule). Reports feed the ADR-304 evidence
engine and are the substrate ADR-317 scores against.
## Consequences
@@ -106,7 +106,7 @@ compares RF inference against independent observation and never feeds it.
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-298/299/301/303) lands and the benchmark (ADR-314)
phase-1 spine (ADR-301/299/301/303) lands and the benchmark (ADR-317)
exercises it.
## Validation
@@ -117,8 +117,8 @@ compares RF inference against independent observation and never feeds it.
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-314 benchmark scenario consumes `GroundTruth` reports as
its scored reference; ADR-301 ingests the agreement reports as evidence
- 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
@@ -1,21 +1,21 @@
# ADR-301: Evidence engine — MLflow for physical sensing
# ADR-304: Evidence engine — MLflow for physical sensing
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297) and a
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-299) and the calibration age from the
calibration certificate (ADR-298), it is the store that capability certificates
(ADR-315) are minted from, and it is the accuracy source the real benchmark
service (ADR-314) reads. In phase 2 it ingests agreement reports from the
ground-truth plane (ADR-300).
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-297) judged this primitive **more commercially
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.
@@ -32,7 +32,7 @@ that unifies them per deployment context:
witness-anchored ledger of scored results, the structural template here.
- `frame::EvidenceLevel` L0L5 (ADR-282) — the mandatory evidence tag every
record carries.
- ADR-299 `DomainState`, ADR-137 `QualityScore`, ADR-298 certificate version
- ADR-302 `DomainState`, ADR-137 `QualityScore`, ADR-301 certificate version
and age — the per-inference signals to accumulate.
## Options considered
@@ -54,24 +54,24 @@ accuracy ledger that every model automatically writes to.
### 1. The evidence record
- An `EvidenceRecord` keyed by context — space id (ADR-303), signed device id
(ADR-302), and subject id where consented and available — carrying: model
version; calibration certificate version and **age** (ADR-298); the ADR-299
- 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-300), the agreement result (accuracy, false-positive rate). Each record
(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-316),
(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-300 reference backs it — otherwise the record is
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-298), the fraction of inferences in
(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
@@ -81,11 +81,11 @@ accuracy ledger that every model automatically writes to.
- 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-288/
ADR-290 constructor discipline. A benchmark or certificate reading the ledger
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-315)
accounting. Empty contexts report "no evidence," which downstream (ADR-318)
must treat as no capability.
## Consequences
@@ -93,8 +93,8 @@ accuracy ledger that every model automatically writes to.
- 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-315 capability certificates become derivable (a certificate is a signed
attestation over a slice of the ledger) and ADR-314 gains a real accuracy
- 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
@@ -109,9 +109,9 @@ accuracy ledger that every model automatically writes to.
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-299 `DomainState` and (phase 2) ADR-300 agreement
reports; an ADR-315 test mints a certificate from a ledger slice and an
ADR-314 test reads accuracy from the ledger.
- 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-300 references) is the maturity milestone and requires hardware evidence
ADR-303 references) is the maturity milestone and requires hardware evidence
per CLAUDE.md; a synthetic ledger is L0 by construction.
@@ -1,24 +1,24 @@
# ADR-302: Authenticated sensor identity — RF chain of custody
# ADR-305: Authenticated sensor identity — RF chain of custody
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297** (perception substrate program) and owns
primitive #5, *authenticated sensor identity*. In the ADR-297 dependency DAG it
is a spine root that, together with **ADR-303** (canonical spatial ontology),
feeds **ADR-298** (calibration certificate) and **ADR-316** (witness chain).
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-293** shipped step
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-293
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.
@@ -30,7 +30,7 @@ Foundations already exist in the tree and must be reused rather than rebuilt:
- `wifi-densepose-bfld` provides `CapabilityAttestation` and
`PrivacyAttestationProof` (BFLD attestation, ADR-141) — the device-side
attestation surface.
- **ADR-292** defines the source-provenance state machine and freshness
- **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.
@@ -42,7 +42,7 @@ hardware, and FFI boundary; default to least authority").
## Options considered
1. **Stop at ADR-293 (bind + IP allowlist).** Rejected: ADR-293 itself names
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
@@ -67,7 +67,7 @@ server, built from existing RuField/BFLD types.
`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-315**
measurements for phenomena it is attested to sense. This is what **ADR-318**
(capability certificate) later consumes.
### 2. Signed measurement envelope
@@ -78,7 +78,7 @@ server, built from existing RuField/BFLD types.
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-293 drops disallowed sources — an `Err`
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)
@@ -87,44 +87,44 @@ server, built from existing RuField/BFLD types.
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-292's `SpatialStateFreshness`
stale frames are rejected. This reuses ADR-295's `SpatialStateFreshness`
rather than inventing a parallel notion of staleness, and composes with
ADR-294's stale-node handling.
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-298) and inference
annotate their transforms, and the emitted spatial event (ADR-303 ontology)
`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-316** witness chain
serializes and the **ADR-315** capability certificate points at as evidence.
- 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-293's loopback default; a routable, multi-node, or fleet
deployment (ADR-313) requires enrolled identities. The startup security log
(ADR-293) is extended to state whether frame authentication is active.
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-293 named plainly — are
- 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-313.
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-294's approach.
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.
@@ -137,7 +137,7 @@ server, built from existing RuField/BFLD types.
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-316 can serialize and
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.
@@ -1,28 +1,28 @@
# ADR-303: Canonical spatial ontology — one Site→…→Event model for every surface
# ADR-306: Canonical spatial ontology — one Site→…→Event model for every surface
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297** and owns primitive #6, *canonical spatial
ontology*. In the ADR-297 DAG it is a spine root alongside **ADR-302**
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-298** (calibration), **ADR-304** (tracking,
consumes `Track`/`Person`), **ADR-316** (witness chain), and every external
surface named in the ADR-297 consequences (MQTT, REST, WebSocket, RuField,
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-294** just introduced `NodeInference`/`RoomInference` to
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-294 consequences). Without a
`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.
@@ -75,14 +75,14 @@ Site ▸ Building ▸ Floor ▸ Space ▸ Zone
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-302** authenticates (`DeviceId` as its stable
identity) and **ADR-317** (HAL, phase 2) describes the hardware of. `Person`,
- `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-304** (persistent tracking) produces and updates. `Observation` is what
an authenticated frame (ADR-302) becomes after calibration (ADR-298), and
`Event` is the governed output that ADR-315 certifies and ADR-316 witnesses.
**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
@@ -90,8 +90,8 @@ Site ▸ Building ▸ Floor ▸ Space ▸ Zone
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-294) become projections of `Sensor→Observes` and the
`Space`-level fused inference respectively, so ADR-294's node/room separation
`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*
@@ -102,7 +102,7 @@ Site ▸ Building ▸ Floor ▸ Space ▸ Zone
- 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-294's migration accessor). Surfaces are
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.
@@ -120,8 +120,8 @@ Site ▸ Building ▸ Floor ▸ Space ▸ Zone
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-304,
ADR-298, ADR-299, and the model layer. This ADR does not itself make any
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
@@ -135,7 +135,7 @@ Site ▸ Building ▸ Floor ▸ Space ▸ Zone
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-294) project and re-project identically.
(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
@@ -1,18 +1,18 @@
# ADR-304: Persistent identity & tracking — privacy-preserving probabilistic tracks
# ADR-307: Persistent identity & tracking — privacy-preserving probabilistic tracks
- **Status**: Accepted — initial implementation (ADR-297 phase 2)
- **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-297** and owns primitive #7, *persistent identity
& tracking*. In the ADR-297 DAG it is a phase-2 primitive sitting on the
phase-1 spine: it **consumes the ADR-303 ontology** (producing and updating the
`Track` and `Person` node types defined there), it relies on **ADR-302**
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-315/ADR-316 can certify
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
@@ -58,11 +58,11 @@ that survives track loss and hand-off between spaces, and (c) an explicit
## Decision
Extend `wifi-densepose-mat/tracking` into a **cross-domain persistent track
layer** that produces ADR-303 `Track`/`Person` nodes.
layer** that produces ADR-306 `Track`/`Person` nodes.
### 1. Persistent probabilistic entity
- A persistent entity is a pseudonymous `Person` node (ADR-303) with a stable
- 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
@@ -76,14 +76,14 @@ layer** that produces ADR-303 `Track`/`Person` nodes.
### 2. Cross-radio / cross-modality / cross-room continuity
- Association operates over the ADR-303 ontology graph: `Observes` edges from
- 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-308) supplies the observations. This ADR depends on those for the raw
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)
@@ -113,10 +113,10 @@ layer** that produces ADR-303 `Track`/`Person` nodes.
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-308/ADR-063/ADR-029 landing; until then
- 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-303
ontology and ADR-302 identity spine finalize.
- Being phase 2, this ADR is design intent; it will be revised as the ADR-306
ontology and ADR-305 identity spine finalize.
## Validation
@@ -129,7 +129,7 @@ layer** that produces ADR-303 `Track`/`Person` nodes.
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-288 leakage-free protocol and an evidence tag; no number is asserted
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.
@@ -1,19 +1,19 @@
# ADR-305: Sensor placement optimizer — floorplan + inventory → recommended positions
# ADR-308: Sensor placement optimizer — floorplan + inventory → recommended positions
- **Status**: Accepted — initial implementation (ADR-297 phase 3)
- **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-297** and owns primitive #8, *sensor placement
optimizer*. In the ADR-297 DAG it is a phase-3, research-forward primitive that
sits on top of the fused world state and is tightly coupled to **ADR-312**
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-303** canonical ontology for the physical
scene and, after install, compares its predictions against ADR-299 observability
and the ADR-315 capability certificate.
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
@@ -30,11 +30,11 @@ 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-303) for
attenuation to seed a coverage model, plus `Sensor` nodes (ADR-306) for
candidate positions.
- **ADR-312** (RF twin, phase 3) is the propagation/multipath simulator; this
- **ADR-315** (RF twin, phase 3) is the propagation/multipath simulator; this
optimizer is a *consumer* of the twin, not a second simulator.
- **ADR-299** (OOD/observability) and **ADR-315** (capability certificate)
- **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
@@ -49,36 +49,36 @@ Relevant existing assets to build on rather than duplicate:
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-312.
3. **A coverage optimizer that consumes the ADR-312 RF twin over the ADR-303
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-303 scene) and a
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-303 canonical scene: `Space`/`Zone` bounds, `Wall` segments with
- 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-315 / ADR-317 HAL descriptors).
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-299
`Space`/`Zone` (presence, vitals, pose), expressed against the ADR-302
observability metric.
### 2. Prediction
- For a candidate placement, query the **ADR-312 RF twin** for simulated RF
- 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-299 gates on so the
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
@@ -88,10 +88,10 @@ install.
### 3. Post-install loop
- After install, compare **predicted vs. measured** observability using the
ADR-299 runtime observability signal from the freshly enrolled (ADR-302),
calibrated (ADR-298) sensors. Where measurement disagrees with prediction,
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-312 twin's scene parameters (e.g. a wall's effective
to improve the ADR-315 twin's scene parameters (e.g. a wall's effective
attenuation).
### Evidence discipline
@@ -108,15 +108,15 @@ install.
- 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-312 RF twin and the coarseness
- 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-312 concern.
- Hard dependency on ADR-312 (twin), ADR-299 (observability metric), and
ADR-303 (scene); this ADR does not build a simulator or an observability
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-312 and the phase-1 spine land.
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.
@@ -1,22 +1,22 @@
# ADR-306: Active sensing — closed-loop RF experiment control
# ADR-309: Active sensing — closed-loop RF experiment control
- **Status**: Accepted — initial implementation (ADR-297 phase 3)
- **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-297** and owns primitive #9, *active sensing*. In
the ADR-297 phasing it is a phase-3 primitive that sits on top of the fused
world state produced by **ADR-308** (real sensor fusion) and is driven by the
information budget of **ADR-311** (information-gain scheduler). It is authored
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-297 named the next
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
@@ -37,8 +37,8 @@ Substantial control-plane scaffolding already exists and must be
- 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-306 is the ADR
that closes exactly those gaps, in coordination with ADR-311.
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
@@ -73,11 +73,11 @@ observed response.
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-317's HAL, so an ESP32-only deployment simply has an
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-289 wideband subcarrier-agnostic metadata).
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).
@@ -85,28 +85,28 @@ empty controllable set and degrades to the passive planner):
governed exactly as ADR-280 §6 requires, via `request_actuation` and an
`ActuationReceipt`).
- **802.11bf measurement parameters** (TB/non-TB, reporting config) once
ADR-307 exposes standardized sensing as a native measurement type.
ADR-310 exposes standardized sensing as a native measurement type.
### 2. The loop
```
fused-state uncertainty (ADR-308)
fused-state uncertainty (ADR-311)
info-gain ranking of ExperimentControl options (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-308) → repeat
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-311** supplies (the mutual-
information estimate ADR-280 deferred); ADR-306 owns the *control loop* that
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
@@ -127,13 +127,13 @@ consumes that estimate and drives the hardware.
- 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-308 must expose a
usable uncertainty surface and ADR-311 must produce trustworthy information-
- 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-317.
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.
@@ -1,16 +1,16 @@
# ADR-307: 802.11bf-native architecture — standardized WLAN sensing as native measurement types
# ADR-310: 802.11bf-native architecture — standardized WLAN sensing as native measurement types
- **Status**: Proposed (ADR-297 phase 2)
- **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-297** and owns primitive #10, *802.11bf-native
architecture*. In the ADR-297 phasing it is a phase-2 integration primitive: it
sits on the phase-1 spine (authenticated identity ADR-302, spatial ontology
ADR-303, evidence engine ADR-301) and **feeds ADR-317** (the RuView sensor HAL),
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**.
@@ -44,7 +44,7 @@ the standardized procedure surface as forward-compatible types (ADR-152/153):
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-289**
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."
@@ -53,7 +53,7 @@ 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-298), fusion (ADR-308), and the ontology (ADR-303) on
through calibration (ADR-301), fusion (ADR-311), and the ontology (ADR-306) on
equal footing with normalized CSI.
## Options considered
@@ -64,7 +64,7 @@ equal footing with normalized CSI.
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-303 exists to close.
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.
@@ -83,7 +83,7 @@ replacing — normalized CSI.
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-289** subcarrier-agnostic / native-
- 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
@@ -91,8 +91,8 @@ replacing — normalized CSI.
### 2. Ontology and governance binding
- Each standardized measurement becomes an ADR-303 `Observation` node from an
ADR-302-authenticated `Sensor`, carrying `SemanticProvenance` and exactly one
- 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
@@ -100,12 +100,12 @@ replacing — normalized CSI.
- 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-317)
### 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-317** (HAL) needs to "identify the hardware." ADR-307 defines that
capability descriptor as the projection of `SensingCapabilities`; ADR-317
**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.
@@ -122,7 +122,7 @@ replacing — normalized CSI.
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-289). No benchmark number is invented here.
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.
@@ -133,14 +133,14 @@ replacing — normalized CSI.
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-289
- `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-303
`Observation` from an ADR-302-authenticated `Sensor` with one `EvidenceLevel`;
- 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-317 capability descriptor is derivable from
- 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
@@ -1,17 +1,17 @@
# ADR-308: Real sensor fusion — uncertainty-aware, multiple observations → one world state
# ADR-311: Real sensor fusion — uncertainty-aware, multiple observations → one world state
- **Status**: Accepted — initial implementation (ADR-297 phase 2)
- **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-297** and owns primitive #11, *real sensor fusion*.
In the ADR-297 DAG it is a phase-2 integration primitive: it **consumes ADR-303**
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-309** (long-term spatial memory),
**ADR-310** (counterfactual inference), and **ADR-312** (digital RF twin). It is
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
@@ -28,7 +28,7 @@ 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-308 generalizes that pairwise case into
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
@@ -36,10 +36,10 @@ rebuilt**:
`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-308 reuses that record as the
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-303** `Observation`/`Track`/`Event` node types are the input and output
**ADR-306** `Observation`/`Track`/`Event` node types are the input and output
vocabulary respectively.
What is missing is the **uncertainty-aware combiner across heterogeneous
@@ -68,20 +68,20 @@ probabilistic world state.
### 1. Inputs: authenticated, ontology-typed observations
- Inputs are ADR-303 `Observation` nodes from **ADR-302-authenticated** sensors.
Supported modalities: WiFi (CSI / 802.11bf native reports via ADR-307), BLE,
- 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-299** is
(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-303 entities (`Person`, `Object`, `Track`, and the
`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
@@ -93,9 +93,9 @@ probabilistic world state.
### 3. Output: one world state, provenance preserved
- The output is a single `WorldState` written into the ADR-303 ontology, with
- 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-309/310/312 consume; they read
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
@@ -108,7 +108,7 @@ probabilistic world state.
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-299 and the evidence engine (ADR-301) reason about reliability.
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.
@@ -127,9 +127,9 @@ probabilistic world state.
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-303 ontology (not N feeds), with
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-299-flagged-OOD observation is
- 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.
@@ -1,15 +1,15 @@
# ADR-309: Long-term spatial memory — learn the normal physics of a location
# ADR-312: Long-term spatial memory — learn the normal physics of a location
- **Status**: Accepted — initial implementation (ADR-297 phase 3)
- **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-297** and owns primitive #12, *long-term spatial
memory*. In the ADR-297 phasing it is a phase-3 primitive that sits on the fused
world state produced by **ADR-308** (real sensor fusion) and **ties to ADR-312**
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**.
@@ -34,12 +34,12 @@ rebuilt**:
(`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-303** supplies the entity vocabulary the memory is indexed by (`Space`,
`Object`, `Sensor`, `Track`, `Event`); **ADR-308** supplies the fused,
- **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-298** (automatic
- **ADR-135** (empty-room baseline calibration) and **ADR-301** (automatic
domain calibration) already establish a *calibration-time* baseline of a
space; ADR-309 extends that from a one-shot baseline to a **continuously
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
@@ -67,7 +67,7 @@ on the RuVector substrate and scores live fused state against it.
### 1. What "normal" is learned over
Per ADR-303 `Space` (and the entities within it), accumulate the ADR-308 fused
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
@@ -92,7 +92,7 @@ never presented above the evidence of the observations it was learned from.
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-303 ontology, so "normal for this `Space` at
- 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.
@@ -100,11 +100,11 @@ never presented above the evidence of the observations it was learned from.
- 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-303 `Event`*without* a per-anomaly
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-312** (RF twin) can simulate against:
- 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.
@@ -142,5 +142,5 @@ never presented above the evidence of the observations it was learned from.
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-312): divergence between a twin-simulated
- Twin-linkage design check (with ADR-315): divergence between a twin-simulated
expected state and the learned normal is expressible as a deviation signal.
@@ -1,17 +1,17 @@
# ADR-310: Counterfactual inference — generative spatial reasoning
# ADR-313: Counterfactual inference — generative spatial reasoning
- **Status**: Accepted — initial implementation (ADR-297 phase 3)
- **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-297** (perception substrate program) and owns
primitive #13, *counterfactual inference*. In the ADR-297 DAG it is a phase-3,
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-308** (real sensor fusion) for the current fused estimate and
**ADR-312** (digital RF twin) for the twin's expected measurement
**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.
@@ -26,7 +26,7 @@ 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-312
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*
@@ -35,17 +35,17 @@ first-class uncertainty signal.
Relevant existing assets to build on rather than duplicate:
- **ADR-308** (fusion) already produces the fused world estimate and its
- **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-312** (RF twin) is the generative forward model — per-deployment
- **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-299** (OOD/observability) already owns the `UNKNOWN` verdict; the
- **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-299,
hypothesis") and the "no hypothesis explains this" case route through ADR-302,
not a parallel gate.
- `frame::EvidenceLevel` L0L5 (ADR-282) and the ADR-301 evidence engine
- `frame::EvidenceLevel` L0L5 (ADR-282) and the ADR-304 evidence engine
account for the resulting confidence.
## Options considered
@@ -54,13 +54,13 @@ Relevant existing assets to build on rather than duplicate:
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-312 twin's
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-312 twin as the forward model
and the ADR-308 fused state as the hypothesis prior, routing low-margin and
null-dominant cases to the ADR-299 UNKNOWN verdict.** Chosen.
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
@@ -70,31 +70,31 @@ generative forward model.
### 1. Hypothesis set
- Hypotheses are drawn from the ADR-308 fused state and its neighbourhood: the
- 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-303** canonical ontology
- 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-312 twin** for the expected measurement
- 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-301 evidence engine.
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-299**
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.
@@ -115,14 +115,14 @@ generative forward model.
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-312 twin's forward model and the
ADR-308 fused prior; the layer reports margins and defers to ADR-299 UNKNOWN
- 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-308 (fused state and covariance) and ADR-312 (forward
- 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-308 and ADR-312 land, and it is not implemented
expected to be revised as ADR-311 and ADR-315 land, and it is not implemented
by the phase-1 swarm.
## Validation
@@ -133,8 +133,8 @@ generative forward model.
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-299 UNKNOWN verdict rather than a
forced occupancy label; margins propagate into the ADR-301 evidence engine.
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
@@ -1,16 +1,16 @@
# ADR-311: Information-gain scheduler — sample the most informative radios
# ADR-314: Information-gain scheduler — sample the most informative radios
- **Status**: Accepted — initial implementation (ADR-297 phase 3)
- **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-297** (perception substrate program) and owns
primitive #14, *information-gain scheduler*. In the ADR-297 DAG it is a phase-3,
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-306** (active sensing): ADR-306 decides *what to probe*
**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.
@@ -36,17 +36,17 @@ not by actually sampling.
Relevant existing assets to build on rather than duplicate:
- **ADR-308** (fusion) maintains the fused state and its covariance — the
- **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-312** (RF twin) provides the per-sensor forward model used to predict a
- **ADR-315** (RF twin) provides the per-sensor forward model used to predict a
candidate measurement's expected informativeness before sampling.
- **ADR-317** (RuView sensor HAL, phase 2) exposes each radio's real
- **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-306** (active sensing) is the paired actuator: the scheduler ranks
sensors, ADR-306 chooses the probe on the chosen sensor.
- **ADR-299** (observability) defines the phenomenon the estimate is *for*, so
- **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.
@@ -61,8 +61,8 @@ Relevant existing assets to build on rather than duplicate:
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-308 covariance and ADR-312 forward
model, with costs from the ADR-317 HAL.** Chosen.
reduction per unit cost, using the ADR-311 covariance and ADR-315 forward
model, with costs from the ADR-320 HAL.** Chosen.
## Decision
@@ -72,12 +72,12 @@ 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-299 objective by evaluating how much a predicted
measurement (via the **ADR-312** forward model) would shrink the **ADR-308**
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-317** HAL descriptors. The exact weighting of the three cost terms is a
**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.
@@ -87,22 +87,22 @@ sampling/processing budget across available radios by value of information.
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-306** active
- 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-308) incorporates the result.
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-301)
reflects the actual sensing that occurred, and observability (ADR-299) can
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-312 twin (simulation, L0 per ADR-282, `SYNTHETIC`); a scheduling decision
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
@@ -113,14 +113,14 @@ sampling/processing budget across available radios by value of information.
- 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-312 forward model (informativeness
prediction) and ADR-317 cost descriptors; a poor forward model degrades to
- 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-308 (covariance), ADR-312 (forward model), and ADR-317
(cost descriptors), and paired with ADR-306; this ADR builds none of those.
- 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-306, ADR-308, ADR-312, and the ADR-317 HAL land.
expected to be revised as ADR-309, ADR-311, ADR-315, and the ADR-320 HAL land.
## Validation
@@ -130,7 +130,7 @@ sampling/processing budget across available radios by value of information.
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-299 UNKNOWN for a deliberately starved zone rather than reporting a
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.
@@ -1,20 +1,20 @@
# ADR-312: Digital RF twin — persistent per-deployment RF model
# ADR-315: Digital RF twin — persistent per-deployment RF model
- **Status**: Accepted — initial implementation (ADR-297 phase 3)
- **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-297** (perception substrate program) and owns
primitive #15, *digital RF twin*. In the ADR-297 DAG it is a phase-3,
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-305** (placement optimizer) plans against the twin's propagation model,
**ADR-310** (counterfactual inference) uses it as the generative forward model,
and **ADR-311** (information-gain scheduler) uses it to predict per-sensor
informativeness. It ties directly to **ADR-298** (calibration), **ADR-305**
(placement), and **ADR-309** (long-term spatial memory). It is authored as
**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.
@@ -29,9 +29,9 @@ noise or drift.
The **digital RF twin** is that persistent baseline: a per-deployment model
holding
- **geometry and radio locations** (from the ADR-303 scene / worldgraph),
- **geometry and radio locations** (from the ADR-306 scene / worldgraph),
- **propagation history** and **observed multipath** structure,
- **calibration state** (from ADR-298),
- **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
@@ -45,15 +45,15 @@ 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-303). The twin *annotates and persists* this scene with RF
`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-309** (long-term spatial memory, phase 3) is the persistence and
- **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-302** (authenticated identity) and **ADR-292** (provenance) mean the
- **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.
@@ -67,8 +67,8 @@ Relevant existing assets to build on rather than duplicate:
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-303 scene and
ADR-309 memory: geometry + radio locations + calibration state + observed
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.
@@ -80,20 +80,20 @@ assets.
### 1. State the twin holds
- **Geometry and radio locations** referenced from the ADR-303 / worldgraph
- **Geometry and radio locations** referenced from the ADR-306 / worldgraph
scene (not copied).
- **Calibration state** referenced and versioned from
`wifi-densepose-calibration` (ADR-298), so the twin knows *which* calibration
`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-309 spatial memory.
of per-link channel structure, stored in ADR-312 spatial memory.
- **Expected measurement distributions** per link and phenomenon — the forward
model ADR-305, ADR-310, and ADR-311 consume.
model ADR-308, ADR-313, and ADR-314 consume.
### 2. Update and delta
- Verified measurements (ADR-302 identity, ADR-292 provenance) update the twin's
distributions online, bounded by ADR-298 calibration validity. A new
- 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
@@ -104,12 +104,12 @@ assets.
### 3. Consumers
- **ADR-305** queries the twin's propagation model to plan placements.
- **ADR-310** uses the twin's expected distributions as the generative forward
- **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-311** uses per-sensor expected informativeness from the twin.
- **ADR-314** uses per-sensor expected informativeness from the twin.
- Facility/security/robotics/industrial integrations read the twin's change
deltas as governed ADR-303 spatial events.
deltas as governed ADR-306 spatial events.
### Evidence discipline
@@ -129,18 +129,18 @@ assets.
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-305/310/311, so those primitives
- 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-303 (scene), ADR-298 (calibration state), and ADR-309
- 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-308 fusion, and ADR-309 memory land.
phase-1 spine, ADR-311 fusion, and ADR-312 memory land.
## Validation
@@ -151,8 +151,8 @@ assets.
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-303 event with
provenance (ADR-302/292).
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
@@ -1,17 +1,17 @@
# ADR-313: Fleet control plane — provisioning to audit trails
# ADR-316: Fleet control plane — provisioning to audit trails
- **Status**: Proposed (ADR-297 phase 2)
- **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-297** (perception substrate program) and owns
primitive #16, *fleet control plane*. In the ADR-297 DAG it is a phase-2
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-302** (authenticated sensor identity) for per-device identity and
enrollment, and **ADR-315** (capability certificate) for the signed models,
**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.
@@ -27,26 +27,26 @@ 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-293** (bind + allowlist) plus **ADR-302**
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-302** already defines per-device keypairs, the `DeviceId → public key →
- **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-302 identity, not a new
plane is the distribution and lifecycle layer over ADR-305 identity, not a new
identity scheme.
- **ADR-315** (capability certificate) defines the signed, expiring artifact a
- **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-298** (calibration) owns calibration validity/expiry; the fleet plane
- **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-316** (witness chain) provides the append-only, re-verifiable record;
- **ADR-319** (witness chain) provides the append-only, re-verifiable record;
fleet audit trails are witness-chain entries, not a parallel log format.
- **ADR-317** (RuView sensor HAL, phase 2) provides hardware/firmware capability
- **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.
@@ -61,8 +61,8 @@ Relevant existing assets to build on rather than duplicate:
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-302 identity, ADR-315
certificates, ADR-298 calibration lifecycle, and ADR-316 audit — covering
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
@@ -73,22 +73,22 @@ spine.
### 1. Release identity and bill of materials
- Each fielded device has a **BOM record** binding `DeviceId` (ADR-302) → exact
firmware version → signed model set → active capability certificate (ADR-315)
→ current calibration record (ADR-298) → HAL/hardware descriptor (ADR-317).
- 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-302 enrollment step at fleet scale:
- **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-315 certificate. A device is untrusted until provisioned.
- **Certificate lifecycle**: issue, rotate, expire, and **revoke** ADR-315
certificates and the ADR-302 keys behind them; revocation lists are
distributed here (the distribution ADR-302 deferred).
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-317 HAL descriptor and ADR-141 capability attestation so an
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.
@@ -102,13 +102,13 @@ spine.
### 4. Health telemetry, remote diagnostics, retention, audit
- **Health telemetry** and **remote diagnostics** report device liveness,
calibration staleness (ADR-298), certificate expiry (ADR-315), and error
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-316 witness-chain** entry, giving a re-verifiable **audit
written as an **ADR-319 witness-chain** entry, giving a re-verifiable **audit
trail** rather than a mutable log.
### Authority and least privilege
@@ -127,12 +127,12 @@ spine.
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-302 (identity), ADR-315 (certificate), ADR-298
(calibration lifecycle), ADR-316 (audit), and ADR-317 (firmware/HAL
- 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-315, ADR-316, and ADR-317 land.
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.
@@ -142,7 +142,7 @@ spine.
- 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-316 witness entry.
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
@@ -1,25 +1,25 @@
# ADR-314: Multi-domain benchmark scorecard — regressions cannot hide behind pooled accuracy
# ADR-317: Multi-domain benchmark scorecard — regressions cannot hide behind pooled accuracy
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297) and the
per-PR enforcement edge of the phase-1 certificate spine. In the ADR-297
dependency DAG it reads accuracy from the evidence engine (ADR-301), consumes
the domain state produced by out-of-distribution detection (ADR-299), scores
against calibration certificates (ADR-298), and is anchored in the witness chain
(ADR-316). It is the surface that makes the rest of the spine testable on every
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-297) named this: what
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.
@@ -62,11 +62,11 @@ pooled into one figure. The v0 domain axes:
(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-299 (a capability, not a failure) and
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-298
- **Calibration drift**: fingerprint-distance trajectory against the ADR-301
certificate over the scored window, and the fraction of inferences in each
ADR-299 `DomainState` (KNOWN / DEGRADED / UNKNOWN).
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
@@ -86,7 +86,7 @@ accuracy at all.
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-316 anchoring).
ADR-319 anchoring).
### 3. Sensing-crate CI wiring
@@ -102,7 +102,7 @@ accuracy at all.
- 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-301 evidence ledger and the AetherArena scorer on real data, labelled at
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.
@@ -120,7 +120,7 @@ accuracy at all.
- 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-297) is encoded here as an AetherArena
- The program-level acceptance test (ADR-300) is encoded here as an AetherArena
scenario, closing the loop once the phase-1 spine lands.
## Validation
@@ -132,9 +132,9 @@ accuracy at all.
`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-316), never mutated in place.
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-300 references)
- 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.
@@ -1,21 +1,21 @@
# ADR-315: Capability certificates — validated-for-this-environment claims
# ADR-318: Capability certificates — validated-for-this-environment claims
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297) and,
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-297
dependency DAG it **consumes the evidence engine (ADR-301)** — a capability
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-298)** for the environment it is validated
against, and the **RuField signature types (ADR-302 / ADR-260/262/277/279)** to
sign it. It reports domain state via ADR-299 and is anchored in the witness
chain (ADR-316).
**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,
@@ -24,10 +24,10 @@ 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-301 accumulates
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-298 produces the signed room fingerprint the environment is keyed to;
ADR-302 provides the authenticated device identity and `CapabilityAttestation`
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
@@ -39,10 +39,10 @@ consumer-side rule that refuses capabilities lacking one.
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-301) is the source of truth but not a portable, signed, bounded contract;
(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-301 ledger
3. **Mint a signed, expiring `CapabilityCertificate` over an ADR-304 ledger
slice, and make consumers refuse capabilities without a valid one.** Chosen.
## Decision
@@ -56,38 +56,38 @@ time.
A serializable `CapabilityCertificate` binding:
- `capability` — the phenomenon (e.g. `presence`, `pose`), which must be within
the device's ADR-302/ADR-141 `CapabilityAttestation` (a device cannot be
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-303 space identifier, tied to the ADR-298 calibration
- `room` — the ADR-306 space identifier, tied to the ADR-301 calibration
certificate version the validation was performed against.
- `hardware` — the ADR-302 authenticated `DeviceId` (and, in phase 2, the
ADR-317 HAL descriptor of the sensor).
- `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-301 ledger for this exact context (not
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-300 reference and a reproducer. The certificate cannot upgrade the level
of the ledger it is minted from (ADR-301 honesty rule).
- `signature` — a RuField `SignatureBlock` (ADR-302 / ADR-260/262/277/279) over
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-316).
certificate. The certificate is anchored in the witness chain (ADR-319).
### 2. Minting
- A certificate is minted from a slice of the ADR-301 evidence ledger for one
- 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-298) and an
- 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-298 envelope invalidates both.
validated against, and drift beyond the ADR-301 envelope invalidates both.
### 3. Consumer refusal rule
@@ -95,9 +95,9 @@ A serializable `CapabilityCertificate` binding:
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-298 not invalidated). A failed check yields UNKNOWN via ADR-299, not a
(ADR-301 not invalidated). A failed check yields UNKNOWN via ADR-302, not a
best-effort guess.
- This makes the ADR-297 acceptance clause "quantify whether it can reliably
- 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.
@@ -114,10 +114,10 @@ A serializable `CapabilityCertificate` binding:
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-302 enrollment/rotation and ADR-298 validity machinery rather than new
infrastructure; fleet distribution of certificates is owned by ADR-313.
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-301 ledger at its honest evidence level.
from the ADR-304 ledger at its honest evidence level.
## Validation
@@ -125,14 +125,14 @@ A serializable `CapabilityCertificate` binding:
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-299), not a guess; evidence
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-302/ADR-141
cannot be issued for a capability outside the device's ADR-305/ADR-141
attestation.
- Cross-ADR: an ADR-301 ledger fixture mints a certificate; an ADR-299 test
asserts an expired/mismatched certificate gates to UNKNOWN; the ADR-297
- 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-300
- 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.
@@ -1,22 +1,22 @@
# ADR-316: Witness chain — epistemic infrastructure for physical AI
# ADR-319: Witness chain — epistemic infrastructure for physical AI
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297) and a
spine root of its phase-1 certificate stack. In the ADR-297 dependency DAG it
**extends the source-provenance state machine (ADR-292)** and the RuField
provenance types, **ties to the signature machinery (ADR-302 /
ADR-260/262/277/279)**, and anchors the artifacts produced by ADR-298
(calibration certificates), ADR-301 (evidence records), ADR-314 (benchmark
scorecards), and ADR-315 (capability certificates). In phase 2 it carries the
independent-corroboration link from ADR-300.
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-297) framed RuView's real product as **epistemic
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
@@ -24,10 +24,10 @@ 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-292
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-302 defines the signed
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,
@@ -43,7 +43,7 @@ provenance of that stage.
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-301)
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.
@@ -63,23 +63,23 @@ RF observation ▸ DSP evidence ▸ model inference ▸ independent corroboratio
- Each stage is a signed record carrying its **confidence** and its
**provenance**:
- **RF observation** — the ADR-302 authenticated frame envelope
(`DeviceId`, sequence, timestamp, measurement hash) and its ADR-292
- **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-292 invariant).
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-299 `DomainState` (KNOWN / DEGRADED / UNKNOWN) gate
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-300 agreement link
- **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-303 ontology `Observation`/`Track`/`Event` the
- **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-315) it relied on.
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
@@ -87,23 +87,23 @@ RF observation ▸ DSP evidence ▸ model inference ▸ independent corroboratio
### 2. Signing and anchoring
- Each stage is signed with RuField signature types (ADR-302 /
- 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-298/ADR-301/ADR-314/ADR-315 write into.
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-301 and CLAUDE.md's "source
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-302) can re-verify a chain
- 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-302) cannot provide.
channel authentication (rejected in ADR-305) cannot provide.
### Provenance and honesty discipline
@@ -119,7 +119,7 @@ RF observation ▸ DSP evidence ▸ model inference ▸ independent corroboratio
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-298/301/314/315) gains a single anchoring substrate;
- 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;
@@ -135,12 +135,12 @@ RF observation ▸ DSP evidence ▸ model inference ▸ independent corroboratio
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-292 invariant); an UNKNOWN gate (ADR-299) and a "no corroboration" stage
(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-302 signed frame lineage serializes into a chain that
re-verifies offline with only the enrolled public keys; ADR-298/301/314/315
- 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-300 corroboration)
- 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.
@@ -1,18 +1,18 @@
# ADR-317: RuView sensor HAL — abstract all sensing hardware to one Observation type
# ADR-320: RuView sensor HAL — abstract all sensing hardware to one Observation type
- **Status**: Accepted — initial implementation (ADR-297 phase 2)
- **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-297) and a
phase-2 integration primitive; it is authored as **Proposed**. In the ADR-297
DAG it **consumes the canonical spatial ontology (ADR-303)** — its output is an
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-308)**, which resolves many observations into one world state. It
closes the "identify the hardware" clause of the ADR-297 acceptance test that
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.
@@ -35,7 +35,7 @@ must be reused, not rebuilt:
- 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-302 already authenticates a `Sensor`/`DeviceId`; ADR-303 already defines
- 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.
@@ -55,7 +55,7 @@ only a provenance-bearing, evidence-labelled `Observation`.
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-303
adapters as implementations feeding a shared latent and the ADR-306
ontology.** Chosen.
## Decision
@@ -67,39 +67,39 @@ only `Observation`s.
### 1. The `SensorHal` trait
- A `SensorHal` describes a device's **capabilities** (which phenomena it can
sense — reusing the ADR-302/ADR-141 `CapabilityAttestation`), its **native
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-307, phase 2), BLE, UWB, mmWave
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-302 `Sensor` before its
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-302 authenticated `DeviceId`, its ADR-292
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-303 ontology `Observation` node
- 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-308 fusion, which
- 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-308's "many observations →
provenance and evidence — which is what makes ADR-311's "many observations →
one world state" invariant implementable across heterogeneous hardware.
### Category and honesty discipline
@@ -107,7 +107,7 @@ only `Observation`s.
- 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-299 and certified by ADR-315 for
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
@@ -118,10 +118,10 @@ only `Observation`s.
- New sensing hardware lands as one `SensorHal` implementation instead of a
bespoke pipeline; the translation matrix stays O(modalities), mirroring how
ADR-303 collapsed the surface matrix.
- The ADR-297 acceptance clause "identify the hardware" becomes implementable:
a new sensor type is described by its HAL, authenticated as an ADR-302
`Sensor`, calibrated (ADR-298), gated (ADR-299), and certified (ADR-315)
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
@@ -129,8 +129,8 @@ only `Observation`s.
- 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-308 fusion
and ADR-307 802.11bf land; that revision is expected for a phased program.
- 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
@@ -140,10 +140,10 @@ only `Observation`s.
`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-302).
- Cross-ADR: an `Observation` maps round-trip to an ADR-303 ontology
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-308 fusion fixture.
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
@@ -1,14 +1,14 @@
# ADR-318: Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, and evidence
# ADR-321: Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, and evidence
- **Status**: Accepted — initial implementation planned (ADR-297 phase 1)
- **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-297) makes RuView state *what it knows* and
*how well* — the capability certificate (ADR-315) binds hardware, environment,
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
@@ -34,13 +34,13 @@ that sits between governed spatial state and any actuator.
An `ActionClass` declares the assurance an action demands:
- `min_certificate_class` — the required `CapabilityCertificate` class (ADR-315).
- `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-299) must not be in a
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-297).
certificate conditionality — see ADR-300).
- `max_uncertainty` — inference uncertainty ceiling.
- `min_evidence_level` — the L0L5 floor (ADR-282/ADR-301); e.g. a safety
- `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):
@@ -54,20 +54,20 @@ Reference action classes (illustrative, configurable):
### 2. The authorization decision
`authorize(action, capability_certificate, live_state) -> Authorization` where
`live_state` carries the current `SourceState` (ADR-292), OOD/domain state
(ADR-299), and inference uncertainty. Rules:
`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-299)
- **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-316), so "why was this actuator allowed/denied" is
witness chain (ADR-319), so "why was this actuator allowed/denied" is
auditable end-to-end.
### 3. No silent optimism
@@ -83,8 +83,8 @@ all deny by default. Absence of a policy is not permission.
- 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-315 (certificate), ADR-299 (domain/OOD state), ADR-292
(source state), ADR-301 (evidence). Built in the phase-1 dependent wave after
- 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
@@ -94,7 +94,7 @@ all deny by default. Absence of a policy is not permission.
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-297) — post-certification room
- 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.
+34 -31
View File
@@ -145,37 +145,40 @@ 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-public-benchmark-evaluation-harness.md) | Public-benchmark evaluation harness — Widar3.0 ingest, split protocols, leakage guards | Accepted (initial implementation) |
| [ADR-289](ADR-289-wideband-80211ax-csi-ingest.md) | Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter, subcarrier-agnostic plumbing | Accepted (initial implementation) |
| [ADR-290](ADR-290-vitals-ground-truth-rig.md) | Vitals ground-truth rig — reference ingest, alignment, agreement metrics | Accepted (initial implementation) |
| [ADR-291](ADR-291-wifi-veil-integration.md) | WiFi Veil integration — emission-shaping countermeasure as advisory BFLD dependency | Accepted (initial implementation) |
| [ADR-292](ADR-292-source-provenance-state-machine.md) | Source provenance state machine — synthetic can never present as live | Accepted (initial implementation) |
| [ADR-293](ADR-293-sensor-data-plane-bind-hardening.md) | Sensor data-plane hardening — UDP bind control and source allowlist (step one) | Accepted (initial implementation) |
| [ADR-294](ADR-294-multi-node-semantic-correctness.md) | Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state | Accepted (initial implementation) |
| [ADR-295](ADR-295-model-release-sanity-gates.md) | Model release sanity gates — block degenerate and mislabeled model artifacts | Accepted (initial implementation) |
| [ADR-296](ADR-296-csi-data-incident-repo-controls.md) | Repository CSI data-incident controls — ignore rules and pre-commit/CI policy check | Accepted (controls implemented; tree remediation gated) |
| [ADR-297](ADR-297-perception-substrate-program.md) | RuView perception substrate — phased 21-primitive program (calibration, evidence, trust, deployment) | Accepted (program; children ADR-298..317) |
| [ADR-298](ADR-298-automatic-domain-calibration.md) | Automatic domain calibration — signed, versioned, invalidatable room fingerprint | Accepted (phase 1) |
| [ADR-299](ADR-299-out-of-distribution-detection.md) | Out-of-distribution detection — KNOWN / DEGRADED / UNKNOWN gating | Accepted (phase 1) |
| [ADR-300](ADR-300-ground-truth-synchronization.md) | Ground-truth synchronization — reference sensors as a formal validation plane | Proposed (phase 2) |
| [ADR-301](ADR-301-evidence-engine.md) | Evidence engine — per-(room,device,subject) accuracy ledger | Accepted (phase 1) |
| [ADR-302](ADR-302-authenticated-sensor-identity.md) | Authenticated sensor identity — RF chain of custody | Accepted (phase 1) |
| [ADR-303](ADR-303-canonical-spatial-ontology.md) | Canonical spatial ontology — one Site→…→Event model for every surface | Accepted (phase 1) |
| [ADR-304](ADR-304-persistent-identity-tracking.md) | Persistent identity & tracking — privacy-preserving probabilistic tracks | Proposed (phase 2) |
| [ADR-305](ADR-305-sensor-placement-optimizer.md) | Sensor placement optimizer — floorplan + inventory → recommended positions | Proposed (phase 3) |
| [ADR-306](ADR-306-active-sensing.md) | Active sensing — closed-loop RF experiment control | Proposed (phase 3) |
| [ADR-307](ADR-307-80211bf-native-architecture.md) | 802.11bf-native architecture — standardized WLAN sensing as native measurement types | Proposed (phase 2) |
| [ADR-308](ADR-308-real-sensor-fusion.md) | Real sensor fusion — uncertainty-aware, multiple observations → one world state | Proposed (phase 2) |
| [ADR-309](ADR-309-long-term-spatial-memory.md) | Long-term spatial memory — learn the normal physics of a location | Proposed (phase 3) |
| [ADR-310](ADR-310-counterfactual-inference.md) | Counterfactual inference — generative spatial reasoning | Proposed (phase 3) |
| [ADR-311](ADR-311-information-gain-scheduler.md) | Information-gain scheduler — sample the most informative radios | Proposed (phase 3) |
| [ADR-312](ADR-312-digital-rf-twin.md) | Digital RF twin — persistent per-deployment RF model | Proposed (phase 3) |
| [ADR-313](ADR-313-fleet-control-plane.md) | Fleet control plane — provisioning to audit trails | Proposed (phase 2) |
| [ADR-314](ADR-314-benchmark-multi-domain-scorecard.md) | Multi-domain benchmark scorecard — regressions cannot hide behind pooled accuracy | Accepted (phase 1) |
| [ADR-315](ADR-315-capability-certificates.md) | Capability certificates — validated-for-this-environment claims | Accepted (phase 1) |
| [ADR-316](ADR-316-witness-chain.md) | Witness chain — staged, signed epistemic envelope | Accepted (phase 1) |
| [ADR-317](ADR-317-sensor-hal.md) | RuView sensor HAL — abstract all sensing hardware to one Observation type | Proposed (phase 2) |
| [ADR-318](ADR-318-decision-policy-action-authorization.md) | Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, evidence | Accepted (phase 1) |
| [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) |
---
@@ -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
@@ -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).
@@ -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).
@@ -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.*
@@ -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.
@@ -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.*
@@ -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
```
@@ -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.
@@ -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.*
+102
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.
+6 -6
View File
@@ -38,7 +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-297)](#perception-certificate-spine-developer-preview-adr-297)
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)
@@ -1494,9 +1494,9 @@ An RVF file contains: model weights, HNSW vector index, quantization codebooks,
---
## Perception Certificate Spine (Developer Preview, ADR-297)
## Perception Certificate Spine (Developer Preview, ADR-300)
RuView's perception substrate program (ADR-297) is building a `signal → observation →
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
@@ -1557,11 +1557,11 @@ one crate's domain read automatically reaches another's gate.
### What's genuinely enforced today, for comparison
Not every ADR-292296 remediation item is preview-only. Two are live now:
Not every ADR-295296 remediation item is preview-only. Two are live now:
- **UDP data-plane bind hardening (ADR-293)** — `sensing-server`'s `UdpSourceAllowlist`
- **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-296)** — `scripts/csi-data-policy-check.sh`
- **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.
---
+9
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
+104
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`.
+15
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
@@ -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;
}
+120
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);
}
+64
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 */
+130
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>
@@ -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.
@@ -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)
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "app_main.c"
INCLUDE_DIRS "."
REQUIRES veil_ris_controller
)
@@ -0,0 +1,31 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* SYNTHETIC / L0 build-only. Exercises veil_ris_controller's public API
* against a GPIO-backed config so the component compiles and links on a real
* ESP-IDF toolchain. Never flashed; no physical RIS exists. Per the component
* README, do not treat a successful build as a runtime or on-air claim.
*/
#include "esp_log.h"
#include "veil_ris_controller.h"
static const char *TAG = "veil_ris_controller_example";
static const int kRisPins[4] = {4, 5, 6, 7};
void app_main(void)
{
veil_ris_controller_cfg_t cfg = {
.iface = VEIL_RIS_IFACE_GPIO,
.n_elements = 4,
.key = 0x5EED5EED5EED5EEDULL,
.dwell_us = 500,
.gpio_pins = kRisPins,
.spi_host = -1,
.spi_cs_gpio = -1,
.spi_clock_hz = 0,
};
ESP_ERROR_CHECK(veil_ris_controller_init(&cfg));
ESP_ERROR_CHECK(veil_ris_controller_step(NULL, 0));
ESP_LOGI(TAG, "step_count=%llu (build-only, never flashed)",
(unsigned long long)veil_ris_controller_step_count());
}
@@ -0,0 +1,13 @@
# veil_sensing_detector_example SYNTHETIC / L0, build-only.
#
# Minimal ESP-IDF app that registers veil_sensing_detector with the GPIO
# trigger backend and calls its public API. Exists only to prove the
# component compiles and links against a real ESP-IDF toolchain; it is never
# flashed and no CSI is ever captured. 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_sensing_detector")
project(veil_sensing_detector_example)
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "app_main.c"
INCLUDE_DIRS "."
REQUIRES veil_sensing_detector
)
@@ -0,0 +1,23 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* SYNTHETIC / L0 build-only. Exercises veil_sensing_detector's public API
* against the GPIO trigger backend so the component compiles and links on a
* real ESP-IDF toolchain. Never flashed; no CSI is ever captured. Per the
* component README, do not treat a successful build as a runtime or on-air
* claim.
*/
#include "esp_log.h"
#include "veil_sensing_detector.h"
static const char *TAG = "veil_sensing_detector_example";
void app_main(void)
{
veil_sensing_detector_cfg_t cfg = VEIL_SENSING_DETECTOR_DEFAULT_CFG();
cfg.backend = VEIL_TRIGGER_GPIO;
cfg.gpio_num = 8;
ESP_ERROR_CHECK(veil_sensing_detector_start(&cfg));
ESP_LOGI(TAG, "rate_hz=%.2f engaged=%d (build-only, never flashed)",
veil_sensing_detector_rate_hz(), veil_sensing_detector_engaged());
}
@@ -0,0 +1,23 @@
# veil_ris_controller ESP-IDF component (SYNTHETIC / L0, build-only)
#
# Drives an EXTERNAL reconfigurable intelligent surface (RIS) over GPIO/SPI to
# scramble the *sensing-direction* channel while preserving the *comm-direction*
# channel (the PrivISAC pattern, arXiv:2601.04488). This is the honest way an
# ESP32 "helps scramble": through an external passive surface, NOT its own
# closed Wi-Fi PHY. See the subdir README.md.
#
# The keyed configuration schedule reuses the portable VEIL core's SplitMix64
# `veil_rng` (../../core/veil_shield.{h,c}) so the schedule is deterministic and
# byte-consistent with the Rust reference the same key can be shared with an
# associated receiver.
#
# NOTE: build-only skeleton, never run on silicon. Hardware paths -> TODO(hw).
set(VEIL_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../core")
idf_component_register(
SRCS "veil_ris_controller.c"
"${VEIL_CORE_DIR}/veil_shield.c" # reuse veil_rng from the portable core
INCLUDE_DIRS "include" "${VEIL_CORE_DIR}"
REQUIRES esp_timer esp_driver_gpio esp_driver_spi
)
@@ -0,0 +1,91 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* veil_ris_controller drive an EXTERNAL reconfigurable intelligent surface
* (RIS) to obfuscate the sensing-direction channel.
*
* STATUS: SYNTHETIC / L0. Build-only ESP-IDF component skeleton. Never flashed,
* never captured on silicon. No RIS hardware exists in this repo. Do NOT claim
* runtime or on-air behavior without a captured hardware log.
*
* WHY THIS EXISTS (honest framing): the ESP32 cannot shape its own transmitted
* beamforming feedback the precoding / compressed-BF-report path lives in the
* closed Espressif Wi-Fi PHY blob (esp-phy-lib) and is not modifiable (see
* README.md). The legitimate, compliant way an ESP32 can "help scramble" a
* sensing signal is to act as the *controller for a separate passive surface*:
* a RIS whose per-element phase states are switched over time. Following the
* PrivISAC pattern (arXiv:2601.04488), each element is toggled between two
* states chosen so the surface's 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 a GPIO/SPI
* pin-driver here; it emits no RF of its own.
*
* The state schedule is *keyed* and deterministic: it is drawn from the
* portable core's `veil_rng` (SplitMix64), so an associated / authorized
* sensor holding the same key can reconstruct and thus tolerate the
* schedule, while an unauthorized observer cannot.
*/
#ifndef VEIL_RIS_CONTROLLER_H
#define VEIL_RIS_CONTROLLER_H
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* How the surface's element bits are clocked out. */
typedef enum {
VEIL_RIS_IFACE_GPIO = 0, /* small surfaces: one GPIO per element / bank */
VEIL_RIS_IFACE_SPI, /* larger surfaces: shift-register / driver IC */
} veil_ris_iface_t;
typedef struct {
veil_ris_iface_t iface;
/* Number of independently switchable RIS elements (or 1-bit banks). */
size_t n_elements;
/* Keyed, deterministic schedule (shared with the associated receiver). */
uint64_t key;
/* Dwell time per configuration, microseconds. Must be short vs. the
* channel coherence time to spread perturbation across the sensing burst,
* yet long enough for the surface's switching diodes to settle. */
uint32_t dwell_us;
/* GPIO backend: one pin per element (n_elements <= number of pins). */
const int *gpio_pins; /* borrowed; length == n_elements */
/* SPI backend: bits are packed MSB-first into ceil(n_elements/8) bytes and
* shifted out per configuration. */
int spi_host; /* e.g. SPI2_HOST */
int spi_cs_gpio; /* latch / chip-select */
int spi_clock_hz; /* driver-IC clock */
} veil_ris_controller_cfg_t;
/* Initialize the chosen interface. Registration only — says nothing about a
* physical surface actually switching. */
esp_err_t veil_ris_controller_init(const veil_ris_controller_cfg_t *cfg);
/* Compute the next keyed configuration bitmap and clock it to the surface.
* `out_bits` (optional, may be NULL) receives the packed bitmap for tests.
* `out_len` is the byte length of `out_bits` on input. The bit pattern is
* derived purely from `veil_rng` + the PrivISAC two-state assignment, so it is
* reproducible from (key, step_index). */
esp_err_t veil_ris_controller_step(uint8_t *out_bits, size_t out_len);
/* Start/stop a periodic timer that calls _step() every dwell_us. */
esp_err_t veil_ris_controller_start(void);
esp_err_t veil_ris_controller_stop(void);
/* Monotonic count of configurations applied since init (telemetry/tests). */
uint64_t veil_ris_controller_step_count(void);
#ifdef __cplusplus
}
#endif
#endif /* VEIL_RIS_CONTROLLER_H */
@@ -0,0 +1,196 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* veil_ris_controller see veil_ris_controller.h.
*
* STATUS: SYNTHETIC / L0. Build-only skeleton. Never run on silicon; no RIS
* hardware exists here. Hardware-touching paths are marked TODO(hw). The keyed
* bitmap generator (pure math over veil_rng) is fully implemented and testable
* off target; the GPIO/SPI clock-out is stubbed.
*
* Compliance: the ESP32 only toggles control pins of a *passive* external
* surface. It emits no RF and does not transmit into any band. The surface
* re-reflects ambient energy; it does not add energy or occupy spectrum, which
* is what keeps this on the compliant side of the jamming line. (A powered,
* amplifying, or spectrum-occupying surface would NOT be compliant and is out
* of scope.)
*/
#include "veil_ris_controller.h"
#include <string.h>
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "veil_shield.h" /* portable core: veil_rng, veil_rng_next_u64/_f32 */
static const char *TAG = "veil_ris";
static veil_ris_controller_cfg_t s_cfg;
static bool s_inited;
static uint64_t s_step; /* configurations applied so far */
static esp_timer_handle_t s_timer;
/* ---- keyed configuration generator (pure, testable off-target) ----------- */
/* PrivISAC two-state assignment: every element has two candidate phase states
* (A/B) designed offline so the *comm-direction* array response is ~invariant
* under A<->B while the *sensing-direction* response changes. At runtime we
* only pick, per element, which of the two states is active this step. That
* choice is the single bit we clock out. Drawing the bits from the keyed
* veil_rng makes the whole schedule reproducible from (key, step_index) and
* shareable with an authorized receiver.
*
* `step_index` seeds a per-step substream so any step can be regenerated
* without replaying history (matches the core's deterministic style).
* Fills `bits` (packed MSB-first) with n_elements selection bits. */
void veil_ris_gen_bits(uint64_t key, uint64_t step_index,
size_t n_elements, uint8_t *bits, size_t bits_len)
{
if (!bits || bits_len == 0) {
return;
}
memset(bits, 0, bits_len);
veil_rng r;
/* Mix the step index into the key so each dwell gets an independent draw
* while staying a pure function of (key, step_index). */
veil_rng_seed(&r, key ^ (step_index * 0x9E3779B97F4A7C15ULL));
for (size_t e = 0; e < n_elements; e++) {
size_t byte = e >> 3;
if (byte >= bits_len) {
break;
}
/* Top bit of the draw selects state B (1) vs state A (0). */
uint64_t w = veil_rng_next_u64(&r);
if (w >> 63) {
bits[byte] |= (uint8_t)(0x80u >> (e & 7));
}
}
}
/* ---- interface clock-out (stubs) ----------------------------------------- */
static esp_err_t veil_ris_write(const uint8_t *bits, size_t bits_len)
{
switch (s_cfg.iface) {
case VEIL_RIS_IFACE_GPIO:
/* TODO(hw): for each element e, set its pin to the selected state.
* for (size_t e = 0; e < s_cfg.n_elements; e++) {
* int level = (bits[e >> 3] >> (7 - (e & 7))) & 1;
* gpio_set_level(s_cfg.gpio_pins[e], level);
* }
* Requires each pin configured as output in _init(). Unverified. */
ESP_LOGD(TAG, "TODO(hw) GPIO write %u bits (stub)",
(unsigned)s_cfg.n_elements);
return ESP_ERR_NOT_SUPPORTED;
case VEIL_RIS_IFACE_SPI:
/* TODO(hw): shift the packed bitmap to the surface driver IC.
* spi_transaction_t t = {
* .length = bits_len * 8,
* .tx_buffer = bits,
* };
* spi_device_transmit(s_spi_dev, &t); // then latch via CS
* s_spi_dev created in _init() via spi_bus_add_device(). Unverified. */
ESP_LOGD(TAG, "TODO(hw) SPI write %u bytes (stub)", (unsigned)bits_len);
return ESP_ERR_NOT_SUPPORTED;
default:
return ESP_ERR_INVALID_ARG;
}
}
/* ---- public API ---------------------------------------------------------- */
esp_err_t veil_ris_controller_init(const veil_ris_controller_cfg_t *cfg)
{
if (!cfg || cfg->n_elements == 0) {
return ESP_ERR_INVALID_ARG;
}
if (s_inited) {
return ESP_ERR_INVALID_STATE;
}
s_cfg = *cfg;
s_step = 0;
if (s_cfg.iface == VEIL_RIS_IFACE_GPIO) {
/* TODO(hw): configure each s_cfg.gpio_pins[e] as GPIO_MODE_OUTPUT via
* gpio_config() (build a pin_bit_mask over all elements). */
ESP_LOGW(TAG, "TODO(hw) configure %u GPIO element pins (stub)",
(unsigned)s_cfg.n_elements);
} else {
/* TODO(hw): spi_bus_initialize(s_cfg.spi_host, &buscfg, ...) +
* spi_bus_add_device(s_cfg.spi_host, &devcfg, &s_spi_dev). */
ESP_LOGW(TAG, "TODO(hw) init SPI host %d @ %d Hz (stub)",
s_cfg.spi_host, s_cfg.spi_clock_hz);
}
s_inited = true;
ESP_LOGI(TAG, "init (SYNTHETIC/L0): %u elements, dwell=%uus, keyed schedule",
(unsigned)s_cfg.n_elements, (unsigned)s_cfg.dwell_us);
return ESP_OK;
}
esp_err_t veil_ris_controller_step(uint8_t *out_bits, size_t out_len)
{
if (!s_inited) {
return ESP_ERR_INVALID_STATE;
}
/* Bounded, malloc-free scratch: cap at 256 elements (32 bytes) for the
* skeleton. Larger surfaces would stream in chunks. */
enum { VEIL_RIS_MAX_BYTES = 32 };
uint8_t bits[VEIL_RIS_MAX_BYTES];
size_t need = (s_cfg.n_elements + 7) / 8;
if (need > sizeof bits) {
need = sizeof bits;
}
veil_ris_gen_bits(s_cfg.key, s_step, s_cfg.n_elements, bits, need);
esp_err_t err = veil_ris_write(bits, need); /* stub on host/no-hw */
s_step++;
if (out_bits && out_len) {
size_t n = out_len < need ? out_len : need;
memcpy(out_bits, bits, n);
}
/* NOT_SUPPORTED from the stubbed writer is expected off-silicon; surface
* the generator result as OK so tests can validate the keyed bitmap. */
return (err == ESP_ERR_NOT_SUPPORTED) ? ESP_OK : err;
}
static void veil_ris_timer_cb(void *arg)
{
(void)arg;
(void)veil_ris_controller_step(NULL, 0);
}
esp_err_t veil_ris_controller_start(void)
{
if (!s_inited) {
return ESP_ERR_INVALID_STATE;
}
/* TODO(hw): a real deployment would gate this on the sensing detector's
* engage trigger so the surface only churns during a sensing burst. */
const esp_timer_create_args_t args = {
.callback = veil_ris_timer_cb,
.name = "veil_ris",
};
esp_err_t err = esp_timer_create(&args, &s_timer);
if (err != ESP_OK) {
return err;
}
return esp_timer_start_periodic(s_timer, s_cfg.dwell_us);
}
esp_err_t veil_ris_controller_stop(void)
{
if (s_timer) {
esp_timer_stop(s_timer);
esp_timer_delete(s_timer);
s_timer = NULL;
}
return ESP_OK;
}
uint64_t veil_ris_controller_step_count(void) { return s_step; }
@@ -0,0 +1,19 @@
# veil_sensing_detector ESP-IDF component (SYNTHETIC / L0, build-only)
#
# Estimates the 802.11 sensing-solicitation rate from the ESP32 CSI callback
# and raises a trigger (GPIO / MQTT / ESP-NOW) that engages the AP-side VEIL
# shield. This component only READS the channel; it never shapes RF. See the
# subdir README.md for the honest capability boundary.
#
# NOTE: This is a build-only skeleton. It has never run on silicon. All
# hardware-touching paths are marked TODO(hw).
idf_component_register(
SRCS "veil_sensing_detector.c"
INCLUDE_DIRS "include"
# esp_wifi: esp_wifi_set_csi_rx_cb / esp_wifi_set_csi / promiscuous.
# The MQTT and ESP-NOW trigger backends are optional; they are only
# referenced under CONFIG_ guards so the core build stays minimal.
REQUIRES esp_wifi esp_event esp_timer esp_driver_gpio
PRIV_REQUIRES mqtt
)
@@ -0,0 +1,88 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* veil_sensing_detector detect 802.11 sensing solicitation and raise a
* trigger that engages the AP-side VEIL shield.
*
* STATUS: SYNTHETIC / L0. Build-only ESP-IDF component skeleton. Never flashed,
* never captured on silicon. Do NOT claim runtime behavior without a captured
* hardware log (CLAUDE.md hardware-evidence rule).
*
* ROLE (honest): the ESP32 is a passive CSI *observer* here. It watches how
* often it is being sounded / probed (NDP announcements, action frames, and the
* cadence of incoming CSI-bearing frames) and, when that rate crosses a
* threshold, tells a *separate* protector (the AP running the veil_shield core)
* that a sensing burst is in progress. The ESP32 does NOT modify any waveform
* and does NOT protect its own beamforming feedback (see README.md). This is
* the strongest, clearly-compliant supporting role for the ESP32.
*/
#ifndef VEIL_SENSING_DETECTOR_H
#define VEIL_SENSING_DETECTOR_H
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* How the detector announces "sensing burst detected" to the protector. */
typedef enum {
VEIL_TRIGGER_GPIO = 0, /* drive a GPIO line to a co-located AP / relay */
VEIL_TRIGGER_MQTT, /* publish to a broker the AP subscribes to */
VEIL_TRIGGER_ESPNOW, /* connectionless ESP-NOW unicast to the AP node */
} veil_trigger_backend_t;
typedef struct {
/* Sliding-window length for the solicitation-rate estimate, milliseconds. */
uint32_t window_ms;
/* Solicitations/second above which the shield should be engaged. */
float trigger_rate_hz;
/* Hysteresis: rate must fall below this to clear the trigger. */
float release_rate_hz;
veil_trigger_backend_t backend;
/* GPIO backend. */
int gpio_num; /* output line; active-high engage */
/* MQTT backend. broker_uri/topic are borrowed, must outlive the detector. */
const char *mqtt_broker_uri; /* e.g. "mqtts://ap.local:8883" */
const char *mqtt_topic; /* e.g. "veil/engage" */
/* ESP-NOW backend. */
uint8_t espnow_peer[6]; /* AP node MAC */
} veil_sensing_detector_cfg_t;
/* Sensible SYNTHETIC defaults (not silicon-validated). */
#define VEIL_SENSING_DETECTOR_DEFAULT_CFG() \
(veil_sensing_detector_cfg_t){ \
.window_ms = 1000, \
.trigger_rate_hz = 20.0f, \
.release_rate_hz = 5.0f, \
.backend = VEIL_TRIGGER_GPIO, \
.gpio_num = -1, \
.mqtt_broker_uri = NULL, \
.mqtt_topic = "veil/engage", \
.espnow_peer = {0}, \
}
/* Install the CSI callback + configured trigger backend. Enables promiscuous
* CSI capture. Returns ESP_OK on successful *registration* only this says
* nothing about on-air behavior. */
esp_err_t veil_sensing_detector_start(const veil_sensing_detector_cfg_t *cfg);
/* Tear down callback + backend. */
esp_err_t veil_sensing_detector_stop(void);
/* Last estimated solicitation rate (Hz), for telemetry/tests. */
float veil_sensing_detector_rate_hz(void);
/* True while the engage trigger is asserted. */
bool veil_sensing_detector_engaged(void);
#ifdef __cplusplus
}
#endif
#endif /* VEIL_SENSING_DETECTOR_H */
@@ -0,0 +1,188 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* veil_sensing_detector see veil_sensing_detector.h.
*
* STATUS: SYNTHETIC / L0. Build-only skeleton. Never run on silicon. Every
* hardware-touching path is marked TODO(hw). The rate estimator (pure math over
* timestamps) is the only fully-implemented piece and is unit-testable off
* target; the RF/observe path and the trigger backends are stubs.
*
* Compliance: this component only READS the channel (CSI + frame cadence). It
* emits no RF and shapes no waveform. It cannot and does not touch the closed
* ESP32 Wi-Fi PHY blob. The "action" it takes is a low-rate control signal to a
* separate protector.
*/
#include "veil_sensing_detector.h"
#include <string.h>
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_wifi.h" /* esp_wifi_set_csi_rx_cb, esp_wifi_set_csi, ... */
#include "esp_wifi_types.h" /* wifi_csi_info_t, wifi_csi_config_t */
#include "driver/gpio.h" /* gpio_config, gpio_set_level */
static const char *TAG = "veil_sense";
/* ---- module state -------------------------------------------------------- */
static veil_sensing_detector_cfg_t s_cfg;
static bool s_running;
static bool s_engaged;
static float s_rate_hz;
/* Bounded ring of recent solicitation timestamps (µs), malloc-free. */
enum { VEIL_TS_RING = 256 };
static int64_t s_ts[VEIL_TS_RING];
static size_t s_ts_head; /* next write slot */
static size_t s_ts_count; /* live entries, capped at VEIL_TS_RING */
/* ---- rate estimator (pure, testable off-target) -------------------------- */
/* Record one solicitation at time `now_us` and recompute the sliding-window
* rate. Returns the current rate in Hz. This function is deliberately free of
* any ESP-IDF dependency so it can be exercised in host unit tests. */
float veil_sd_note_solicitation(int64_t now_us)
{
s_ts[s_ts_head] = now_us;
s_ts_head = (s_ts_head + 1) % VEIL_TS_RING;
if (s_ts_count < VEIL_TS_RING) {
s_ts_count++;
}
const int64_t window_us = (int64_t)s_cfg.window_ms * 1000;
const int64_t cutoff = now_us - window_us;
size_t in_window = 0;
for (size_t k = 0; k < s_ts_count; k++) {
if (s_ts[k] >= cutoff) {
in_window++;
}
}
/* rate = events within the trailing window / window length. */
s_rate_hz = (float)in_window * 1000.0f / (float)s_cfg.window_ms;
/* Hysteresis around engage/release. */
if (!s_engaged && s_rate_hz >= s_cfg.trigger_rate_hz) {
s_engaged = true;
ESP_LOGI(TAG, "sensing burst: %.1f Hz >= %.1f -> ENGAGE",
s_rate_hz, s_cfg.trigger_rate_hz);
/* fire-and-forget; backend errors are logged, not fatal */
(void)0; /* veil_sd_emit_trigger(true) — see below */
} else if (s_engaged && s_rate_hz <= s_cfg.release_rate_hz) {
s_engaged = false;
ESP_LOGI(TAG, "sensing quiet: %.1f Hz <= %.1f -> RELEASE",
s_rate_hz, s_cfg.release_rate_hz);
}
return s_rate_hz;
}
/* ---- trigger backends (all stubs) ---------------------------------------- */
static esp_err_t veil_sd_emit_trigger(bool engage)
{
switch (s_cfg.backend) {
case VEIL_TRIGGER_GPIO:
/* TODO(hw): drive the engage line to the co-located AP/relay.
* gpio_set_level(s_cfg.gpio_num, engage ? 1 : 0);
* Requires a wired GPIO to the protector; unverified on silicon. */
ESP_LOGW(TAG, "TODO(hw) GPIO trigger -> %d (stub)", engage);
return ESP_ERR_NOT_SUPPORTED;
case VEIL_TRIGGER_MQTT:
/* TODO(hw): esp_mqtt_client_publish(client, s_cfg.mqtt_topic,
* engage ? "1" : "0", 0, 1 /qos/, 0 /retain/);
* Client lifecycle (esp_mqtt_client_init/_start) omitted from skeleton. */
ESP_LOGW(TAG, "TODO(hw) MQTT trigger -> %d (stub)", engage);
return ESP_ERR_NOT_SUPPORTED;
case VEIL_TRIGGER_ESPNOW:
/* TODO(hw): esp_now_send(s_cfg.espnow_peer, &payload, sizeof payload);
* Requires esp_now_init() + esp_now_add_peer() during start(). */
ESP_LOGW(TAG, "TODO(hw) ESP-NOW trigger -> %d (stub)", engage);
return ESP_ERR_NOT_SUPPORTED;
default:
return ESP_ERR_INVALID_ARG;
}
}
/* ---- CSI callback (observe path) ----------------------------------------- */
/* Runs in the Wi-Fi task. Keep it short: post to a queue in real firmware.
* Here we only classify whether this frame indicates a sounding/solicitation
* and, if so, feed the estimator. */
static void veil_sd_csi_cb(void *ctx, wifi_csi_info_t *info)
{
(void)ctx;
if (!info) {
return;
}
/* TODO(hw): a real classifier would inspect info->rx_ctrl (rate, sig_mode,
* channel, secondary channel) and, alongside a promiscuous frame-type
* filter, distinguish NDP / NDP-announcement / CSI-solicit action frames
* from ordinary data. On silicon the ESP32 does NOT surface the raw
* VHT/HE sounding subtype through the CSI struct, so this classifier is
* necessarily heuristic (cadence + rate + frame length). Treated here as
* "every CSI-bearing frame is a candidate solicitation" for the skeleton. */
(void)veil_sd_note_solicitation(esp_timer_get_time());
}
/* ---- lifecycle ----------------------------------------------------------- */
esp_err_t veil_sensing_detector_start(const veil_sensing_detector_cfg_t *cfg)
{
if (!cfg) {
return ESP_ERR_INVALID_ARG;
}
if (s_running) {
return ESP_ERR_INVALID_STATE;
}
s_cfg = *cfg;
s_engaged = false;
s_rate_hz = 0.0f;
s_ts_head = 0;
s_ts_count = 0;
if (s_cfg.backend == VEIL_TRIGGER_GPIO && s_cfg.gpio_num >= 0) {
/* TODO(hw): configure the engage line.
* gpio_config_t io = {
* .pin_bit_mask = 1ULL << s_cfg.gpio_num,
* .mode = GPIO_MODE_OUTPUT,
* };
* gpio_config(&io);
* gpio_set_level(s_cfg.gpio_num, 0);
*/
ESP_LOGW(TAG, "TODO(hw) configure GPIO %d (stub)", s_cfg.gpio_num);
}
/* Observe path. On real hardware:
* wifi_csi_config_t csi = { ... };
* ESP_ERROR_CHECK(esp_wifi_set_csi_config(&csi));
* ESP_ERROR_CHECK(esp_wifi_set_csi_rx_cb(veil_sd_csi_cb, NULL));
* ESP_ERROR_CHECK(esp_wifi_set_csi(true));
* ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); // more CSI when idle
* The Wi-Fi driver must already be started by the app. */
ESP_LOGW(TAG, "TODO(hw) esp_wifi_set_csi_rx_cb/_set_csi/_set_promiscuous "
"(stub; not wired on silicon)");
(void)veil_sd_csi_cb; /* referenced once wired */
s_running = true;
ESP_LOGI(TAG, "started (SYNTHETIC/L0): window=%ums engage>=%.1fHz",
(unsigned)s_cfg.window_ms, s_cfg.trigger_rate_hz);
return ESP_OK;
}
esp_err_t veil_sensing_detector_stop(void)
{
if (!s_running) {
return ESP_ERR_INVALID_STATE;
}
/* TODO(hw): esp_wifi_set_csi(false); esp_wifi_set_csi_rx_cb(NULL, NULL);
* esp_wifi_set_promiscuous(false); release GPIO/MQTT/ESP-NOW. */
if (s_engaged) {
(void)veil_sd_emit_trigger(false);
}
s_running = false;
return ESP_OK;
}
float veil_sensing_detector_rate_hz(void) { return s_rate_hz; }
bool veil_sensing_detector_engaged(void) { return s_engaged; }
+116
View File
@@ -0,0 +1,116 @@
# Building the WiFi Veil Nexmon patch — **UNTESTED**
> **This procedure has never been run.** It has not been built with the Nexmon
> toolchain, not flashed, and not captured on air. Addresses/symbols in
> `patch/veil_patch.c` are placeholders (one is intentionally invalid,
> `0xDEAD0000`) so it will **not** produce a flashable image as-is. This file
> documents *how it would build* so a hardware operator with real silicon can
> take it forward. `SYNTHETIC / L0`, per CLAUDE.md.
## Prerequisites (host, not in this repo)
- A Linux host (Nexmon expects an x86_64 Ubuntu-like build host) with the
Broadcom-flavored ARM toolchain Nexmon downloads/uses, plus `git`, `make`,
`gcc-arm-none-eabi`, `flex`, `bison`, `libisl`, `automake`.
- Nexmon checked out **outside** this repo (do not vendor it here):
```bash
git clone https://github.com/seemoo-lab/nexmon.git
cd nexmon
source setup_env.sh # sets NEXMON_ROOT, toolchain paths
make # builds libISL / firmwares tooling
```
- The target firmware blob present on the device: BCM43455c0
(`brcmfmac43455-sdio.bin`), version **7_45_189** (Cypress) or 7_45_154
(Raspbian). Do **not** commit the blob or any extracted symbols/ROM to RuView.
## Where this patch would live in the Nexmon tree
Nexmon builds per chip/firmware under `patches/<chip>/<fwver>/<name>/`. This
adapter would be a Nexmon project, e.g.:
```
$NEXMON_ROOT/patches/bcm43455c0/7_45_189/veil/
├── Makefile # copy of an existing nexmon patch Makefile (e.g. nexmon_csi's)
├── src/
│ ├── veil_patch.c # <- symlink/copy of firmware/privshield/nexmon/patch/veil_patch.c
│ ├── veil_shield.c # <- from firmware/privshield/core/ (compiled into the patch)
│ └── veil_shield.h # <- from firmware/privshield/core/
└── ...
```
Keep the RuView copies canonical; the Nexmon tree gets copies/symlinks so the
core stays byte-identical to `../core/`.
## Linking the portable core (MCU-friendly)
The core is `no_std`-style C99: no malloc, no libc I/O, only `<math.h>`
(`sinf`/`cosf`/`sqrtf`/`sqrt`). To build it into the patch:
1. Add `veil_shield.c` to the patch `Makefile`'s object list (alongside
`patch.o`/`wrapper.o`), so it compiles with the same ARM flags.
2. Ensure the firmware provides `sinf`/`cosf`/`sqrtf`. **TODO(hw):** Broadcom
firmware may not export libm. Options, in order of preference:
- link a small `libm`/`compiler-rt` for `arm-none-eabi`;
- or replace the trig with a fixed-point / CORDIC Givens rotation
(`TODO(reverse-engineer)`), which also avoids float on parts without an FPU.
3. All WiFi Veil working storage is stack-bounded (`VEIL_MAX_FINE`, `CACHE` in the
core) — no heap is introduced on-chip.
## Build
```bash
cd $NEXMON_ROOT/patches/bcm43455c0/7_45_189/veil
make # produces the patched brcmfmac43455-sdio.bin
```
Before `make` can succeed you must first resolve every `TODO(reverse-engineer)`
in `veil_patch.c`:
- replace `0xDEAD0000` and the `wlc_sendmgmt_veil_target` symbol with the real,
disassembled target address/symbol for 7_45_189;
- implement `veil_bfr_unpack_fine` / `veil_bfr_pack_fine` (the angle bit-field
codec) and the report-body offset/length;
- confirm the compressed-beamforming report is assembled in ARM on this chip
(else move to hook candidate #2/#3 — see README).
## Flash (Raspberry Pi, on-device)
**TODO(hw) — untested.** Typical Nexmon flow on the Pi:
```bash
# back up stock firmware first!
sudo cp /lib/firmware/brcm/brcmfmac43455-sdio.bin ~/brcmfmac43455-sdio.bin.orig
sudo cp brcmfmac43455-sdio.bin /lib/firmware/brcm/brcmfmac43455-sdio.bin
# (some setups also need the matching *.clm_blob / nexmon's own copy path)
sudo rmmod brcmfmac && sudo modprobe brcmfmac # reload driver with new firmware
dmesg | tail # confirm firmware loaded
```
Push the session key at runtime (matches the IOCTL stub in `veil_patch.c`):
```bash
# TODO(hw): nexutil vendor-IOCTL id and payload format are placeholders
nexutil -s<VEIL_IOCTL_SET_KEY> -b -l8 -v<base64-8-byte-key>
```
**Recovery:** if WiFi breaks, restore the backup blob and reload the driver.
A bad flashpatch offset can knock out WiFi until you reflash stock firmware.
## Validation you can honestly do (still not `MEASURED` firmware)
1. **Host unit test of the math** (already green in this repo):
`cd ../../core && make test`.
2. **Read-back on hardware** with `nexmon_csi`/Wi-BFI: capture the report with
and without the patch and check the fine subspace changed while SNR/norm is
preserved. This validates the transform end-to-end but is a *receiver*
observation, not proof the TX hook is robust.
3. Only a captured device runtime log showing the shaped report leaving *this*
node, plus receiver-side recovery with the shared key, would move any claim
from `SYNTHETIC`/`CLAIMED` toward `MEASURED` (roadmap P5).
## References
See `README.md` for sources (Nexmon, nexmon_csi, Wi-BFI, D11 reverse
engineering).
+124
View File
@@ -0,0 +1,124 @@
# WiFi Veil protector — Nexmon (Broadcom/Cypress) path
C-firmware-patch adapter that would call the portable WiFi Veil core
(`../core/veil_shield.{h,c}`) on the compressed-beamforming-feedback **angles
before transmission**, using the [Nexmon](https://github.com/seemoo-lab/nexmon)
patching framework on a Broadcom/Cypress WiFi chip.
> **Evidence discipline.** Everything here is **`SYNTHETIC` / L0 / build-only**.
> Nothing in this directory has been built with the Nexmon toolchain, flashed to
> a chip, or captured on air. There are **no** `MEASURED` claims and **no**
> hardware logs. The patch is an honest **skeleton** with `TODO(hw)` and
> `TODO(reverse-engineer)` markers, not working firmware. Per CLAUDE.md, no
> defense claim becomes `MEASURED` without a captured runtime log from real
> silicon (roadmap P5).
>
> **Compliant waveform only — never jamming.** The core applies an *orthogonal*
> (energy-preserving) keyed rotation to the node's *own* standards-conformant
> feedback report. It does not add power, transmit out of turn, or interfere
> with any other station.
## Feasibility grade: **C** (research-grade, partial, unproven)
| Sub-path | Grade | Why |
|---|---|---|
| **Read** the compressed BF feedback | **A** (proven by others) | `nexmon_csi` extracts CSI, and Wi-BFI parses the compressed-beamforming *angles* straight from captured action frames — no firmware change at all. The report content is observable today. |
| **Write / shape** the transmitted report | **C / C-** | The report is generated by the proprietary **D11** real-time core, not the ARM firmware Nexmon comfortably patches. The hook point is deep, chip- and firmware-version-specific, and unverified here. Plausible, not demonstrated. |
Grade **C** reflects *this* deliverable's goal — shaping the **TX** report. The
read side is a solved problem and is graded only to contrast honestly.
### Why the write path is hard (the core honesty point)
Broadcom/Cypress chips put all time-critical 802.11 MAC/PHY work on the **D11
core**, a proprietary microcontroller running a programmable state machine
("ucode"). Published reverse-engineering of these chips reports that the D11
generates the **VHT/HE compressed beamforming report ~10 µs after the NDP**, with
its contents fetched from an **internal memory updated directly by the hardware**
on NDP reception. In other words, the angles WiFi Veil wants to touch are staged and
emitted inside the ucode/PHY path on a microsecond deadline — *below* the ARM
"wl" driver firmware where Nexmon's C hooks (`__attribute__((at(addr, ...)))`
flashpatches / branch hooks) live most reliably. Reaching them means either a
D11-ucode patch (needs the D11 assembler and SHM/template-RAM layout) or catching
the report while the ARM path still assembles the action-frame body — if it does
so on this chip at all. Both are `TODO(reverse-engineer)`.
## Target chip(s)
Primary: **BCM43455c0** (Raspberry Pi 3B+/4B; also RPi Zero 2 W), firmware
**7_45_154** (Raspbian) or **7_45_189** (Cypress) — the best-documented,
most-reproducible Nexmon target, and one of the four chips `nexmon_csi` already
supports. Secondary candidates that `nexmon_csi` also supports: **BCM4339**
(Nexus 5), **BCM4358** (Nexus 6P), **BCM4366c0** (Asus RT-AC86U). We scope the
skeleton to BCM43455c0 / 7_45_189 and leave the others as build-matrix `TODO`s.
Caveat: the RPi BCM43455c0 is an **802.11ac (VHT)** single-stream part; its own
*transmit* beamforming/sounding activity as a beamformee is limited. The
skeleton targets the **VHT compressed beamforming report** action-frame path;
whether this chip emits enough to shape in practice is itself a `TODO(hw)`
question.
## Hook-point candidates (all `TODO(reverse-engineer)`)
Ordered most-tractable → deepest. Addresses are **placeholders** — real offsets
come from disassembling the specific firmware blob and cross-checking the Nexmon
symbol tables (`wl_ram.elf` / IDA); none are known-good here.
1. **ARM action-frame TX assembly (best first target).** If the "wl" driver
assembles the VHT Compressed Beamforming Report action-frame *body* in ARM
firmware before handing it to the D11 (function family around
`wlc_txbf_*` / a `wlc_send*mgmt`/action path), a branch hook there could
locate the report's fine-angle block and call `veil_shield_apply` in place.
Cheapest if it exists on this chip.
2. **ARM → D11 TX descriptor / template handoff.** Hook where the driver stages
a frame into the D11 TX FIFO / template RAM (`wlc_d11hdrs` / `wlc_txfifo`
region) and rewrite the angle bytes there. Requires knowing the exact
template-RAM offset of the report body.
3. **D11 ucode patch (deepest).** Patch the ucode routine that copies angles
from the hardware-updated internal memory into the outgoing report, applying
the rotation in D11 SHM. Needs the D11 assembler and PHY/SHM map; highest
fidelity, highest effort, most fragile across firmware versions.
The skeleton wires candidate **#1** and leaves #2/#3 documented but unimplemented.
## What is realistic
- **Realistic now:** verify WiFi Veil's *effect* by reading — capture the shaped vs.
unshaped report with `nexmon_csi`/Wi-BFI and confirm the fine subspace changed
while energy (SNR/norm) is preserved. This validates the math, not the TX hook.
- **Realistic with serious RE effort:** candidate #1, on one pinned firmware, as
a demo — partial, brittle, chip-specific.
- **Not realistic as a portable product:** a clean, firmware-version-stable TX
report-shaping patch across Broadcom parts. Treat as research.
## Risk / honesty
- Wrong flashpatch offsets can **brick the WiFi blob** (recoverable by
reflashing stock firmware, but real).
- Regulatory: the transform is energy-preserving and rides standards-marked
spatial-mapping freedom, but any TX-path firmware patch on a certified radio is
**outside the device's certification** — bench/anechoic use only.
- Firmware blobs are proprietary; do **not** commit extracted firmware, symbols,
or ROM dumps to this repo.
## Sources
- Nexmon framework — <https://github.com/seemoo-lab/nexmon>
- `nexmon_csi` (chips: bcm4339, bcm43455c0, bcm4358, bcm4366c0) —
<https://github.com/seemoo-lab/nexmon_csi>
- Wi-BFI (reads BFAs/BFI from captured compressed-beamforming action frames) —
<https://github.com/kfoysalhaque/Wi-BFI>, paper arXiv:2309.04408
<https://arxiv.org/abs/2309.04408>
- BCM43455c0 patches / D11 headers (`d11.h`) —
<https://github.com/seemoo-lab/nexmon/tree/master/patches/bcm43455c0>
- D11 real-time core / ucode reverse engineering (SEEMOO, Quarkslab) —
<https://www.seemoo.tu-darmstadt.de/> ,
<https://blog.quarkslab.com/reverse-engineering-broadcom-wireless-chipsets.html>
- 802.11ac VHT NDP sounding & compressed beamforming report structure (context) —
<https://community.cisco.com/t5/wireless-mobility-knowledge-base/802-11ac-transmit-beamforming-and-vht-ndp-sounding-procedure/ta-p/3155879>
> The "~10 µs / hardware-updated internal memory" characterization above is drawn
> from published Broadcom D11 reverse-engineering (reported for BCM4365-class
> parts) and is used here as design guidance; it is **not** independently
> verified on BCM43455c0 in this repo. `TODO(reverse-engineer)`: confirm on the
> target blob.
@@ -0,0 +1,176 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* veil_patch.c VEIL protector, Nexmon (Broadcom/Cypress) path.
*
* ============================ HONESTY BANNER ============================
* SYNTHETIC / L0 / BUILD-ONLY. This file is an HONEST SKELETON in Nexmon
* style. It has NOT been built with the Nexmon toolchain, NOT flashed to a
* chip, and NOT captured on air. Every __attribute__((at(...))) address and
* every firmware symbol below is a PLACEHOLDER. Do not treat this as working
* firmware. See ../README.md for the feasibility grade (C, research-grade).
*
* Goal: call the portable VEIL core (../../core/veil_shield.c)
* `veil_shield_apply()` on the compressed-beamforming-feedback FINE ANGLES in
* the transmitted VHT/HE compressed beamforming report, so the identity-bearing
* fine subspace is obfuscated by a keyed, ENERGY-PRESERVING (orthogonal)
* Givens rotation before the frame leaves the radio. Compliant only, never
* jamming: the transform preserves the report's L2 norm.
*
* Target: BCM43455c0 (Raspberry Pi 3B+/4B), firmware 7_45_189. Others TODO.
* =======================================================================
*/
#pragma NEXMON targetregion "patch"
#include <firmware_version.h> /* FW_VER_7_45_189, CHIP_VER_BCM43455c0 (Nexmon) */
#include <patcher.h> /* BPatch / GPatch / __attribute__((at(...))) */
#include <structs.h> /* struct sk_buff, struct wlc_info, etc. */
#include <wrapper.h> /* Nexmon wrappers for ROM/firmware functions */
/* --- Portable VEIL core, linked/inlined for the MCU -------------------------
* The core is pure C99: no malloc, no libc I/O, only <math.h> (sinf/cosf/sqrtf).
* On the Nexmon ARM target we compile ../../core/veil_shield.c into this patch
* object (see ../BUILD.md) and pull in only the declarations here. Everything
* operates on a caller-provided fixed buffer no dynamic allocation on-chip. */
#include "veil_shield.h"
/* ------------------------------------------------------------------------- */
/* Configuration (compile-time; no on-chip allocation) */
/* ------------------------------------------------------------------------- */
/* Max fine-angle count we will touch in one report. Sized for a VHT SU report
* fine block; bound it so all working storage is on the stack, malloc-free. */
#define VEIL_MAX_FINE 64u
/* Rotation passes — MUST match the associated receiver and the Rust reference
* crate default so recover() inverts exactly. TODO(hw): confirm against the
* receiver config actually deployed. */
#define VEIL_PASSES 96u
/* Session key. TODO(hw): DO NOT hardcode a real key in flashed firmware. Inject
* via nexutil IOCTL (see veil_ioctl_set_key stub) or a provisioning step; this
* placeholder exists only so the skeleton type-checks. */
static uint64_t g_veil_key = 0x0000000000000000ULL;
/* ------------------------------------------------------------------------- */
/* Bridge: decode angles -> rotate -> re-encode, in place */
/* ------------------------------------------------------------------------- */
/*
* TODO(reverse-engineer): The compressed beamforming report packs the phi/psi
* angles as bit-fields whose widths depend on the codebook (VHT: (7,5) or (9,7);
* HE differs) and on Nc/Nr. The bytes handed to us are NOT plain floats. This
* bridge must:
* (1) parse the fine-angle bit-fields from `report` into `fine[]` as floats
* in the same units/order the receiver + Rust reference expect,
* (2) call veil_shield_apply() on that flat vector,
* (3) re-quantize and repack the rotated angles back into `report`,
* preserving all coarse/header fields and the frame length.
* Steps (1)/(3) are the real work and are UNIMPLEMENTED here.
*/
static void veil_shape_report_inplace(uint8_t *report, uint32_t report_len)
{
if (report == 0 || report_len == 0)
return;
float fine[VEIL_MAX_FINE];
uint32_t n = 0;
/* TODO(reverse-engineer): unpack fine-angle bit-fields -> fine[0..n) */
/* n = veil_bfr_unpack_fine(report, report_len, fine, VEIL_MAX_FINE); */
if (n < 2 || n > VEIL_MAX_FINE)
return; /* nothing safely shapeable; leave frame untouched (fail-open) */
/* Orthogonal, energy-preserving, keyed. This is the ONLY validated step. */
veil_shield_apply(fine, (size_t)n, g_veil_key, VEIL_PASSES);
/* TODO(reverse-engineer): repack fine[0..n) back into `report` bit-fields,
* keeping report_len and all non-fine fields byte-identical. */
/* veil_bfr_pack_fine(report, report_len, fine, n); */
(void)report_len;
}
/* ------------------------------------------------------------------------- */
/* Hook candidate #1 (see README): ARM action-frame TX assembly */
/* ------------------------------------------------------------------------- */
/*
* We hook the point where the "wl" driver has assembled the VHT Compressed
* Beamforming Report action frame in an sk_buff, just before it is queued to
* the D11 for transmission, locate the report body, and shape it.
*
* TODO(reverse-engineer): the symbol/address below is a PLACEHOLDER. The real
* target must be found by disassembling 7_45_189 (IDA + Nexmon's wl_ram.elf
* symbol map) and confirming: (a) the report body is assembled in ARM (not
* only in D11 ucode), (b) `p` really carries a compressed-beamforming action
* frame, and (c) the offset of the report body within the frame.
*
* If (a) is false on this chip, candidate #1 is dead and we fall to #2/#3
* (TX template-RAM rewrite / D11 ucode patch) both documented in README,
* neither implemented here.
*/
/* Original firmware function prototype (PLACEHOLDER signature). */
extern int wlc_sendmgmt_veil_target(struct wlc_info *wlc, void *p, void *scb);
/* Our replacement. GPatch/BPatch below redirects the target to this. */
int wlc_sendmgmt_veil_hook(struct wlc_info *wlc, void *p, void *scb)
{
/* TODO(reverse-engineer): confirm `p` is a struct sk_buff* and that this
* frame is a VHT/HE compressed beamforming action frame (category 21
* VHT / 30 HE, action = Compressed Beamforming). Guard hard so we never
* mangle unrelated management frames. */
struct sk_buff *skb = (struct sk_buff *)p;
if (skb != 0 /* && veil_is_bf_report_action(skb) */) {
/* TODO(reverse-engineer): compute report body pointer + length from the
* action-frame layout. PLACEHOLDER offsets: */
uint8_t *report = 0; /* skb->data + VEIL_BFR_BODY_OFFSET; */
uint32_t report_len = 0; /* skb->len - VEIL_BFR_BODY_OFFSET; */
veil_shape_report_inplace(report, report_len);
}
/* Always fall through to the real firmware routine so normal TX proceeds. */
return wlc_sendmgmt_veil_target(wlc, p, scb);
}
/*
* Redirect the firmware's mgmt/action TX routine to our hook.
* PLACEHOLDER ADDRESS 0xDEAD0000 is intentionally invalid so nobody mistakes
* this for a real, flashable patch. TODO(reverse-engineer): replace with the
* verified address for CHIP_VER_BCM43455c0 / FW_VER_7_45_189.
*
* Nexmon idiom: a branch patch that overwrites the target's prologue with a
* branch to our replacement (which tail-calls the saved original).
*/
__attribute__((at(0xDEAD0000, "flashpatch", CHIP_VER_BCM43455c0, FW_VER_7_45_189)))
BPatch(veil_sendmgmt_hook, wlc_sendmgmt_veil_hook);
/* ------------------------------------------------------------------------- */
/* Key provisioning via nexutil IOCTL (stub) */
/* ------------------------------------------------------------------------- */
/*
* TODO(hw): register a custom IOCTL so `nexutil` can push the 64-bit session
* key at runtime instead of baking it into flash. Hook the driver's ioctl
* dispatch (wlc_ioctl) the same way nexmon_csi installs its config IOCTLs.
* Left as a stub: the dispatch address and the nexmon_ioctl plumbing are
* PLACEHOLDERS.
*/
#define VEIL_IOCTL_SET_KEY 0x7EIL /* TODO(hw): pick a free vendor IOCTL id */
int veil_ioctl_set_key(struct wlc_info *wlc, const uint8_t *buf, uint32_t len)
{
(void)wlc;
if (buf == 0 || len < sizeof(uint64_t))
return -1;
uint64_t k = 0;
for (uint32_t i = 0; i < sizeof(uint64_t); i++)
k |= ((uint64_t)buf[i]) << (8u * i);
g_veil_key = k;
return 0;
}
/*
* ---------------------------------------------------------------------------
* Candidate #2 (TX template-RAM rewrite) and #3 (D11 ucode patch) are NOT
* implemented. See ../README.md "Hook-point candidates". #3 would require the
* D11 assembler and the PHY/SHM angle-staging map deepest and most fragile.
* ---------------------------------------------------------------------------
*/
+123
View File
@@ -0,0 +1,123 @@
# HDL notes — `veil_rot` (TX) / `veil_unrot` (RX)
> **STATUS: SYNTHETIC / L0 — design notes only. No RTL is shipped here, none has
> been synthesized, placed, routed, or run on an FPGA.** This describes the
> Verilog blocks that *would* apply the keyed unitary in the openwifi datapath.
> Every concrete number (offsets, latency, resource use) is `TODO(hdl)` until a
> real build exists. **Orthogonal transform ⇒ transmit energy preserved:
> compliant, never jamming.**
## Where the blocks sit
openwifi's baseband IQ moves as **AXI-Stream** between blocks and its control is
**AXI-Lite** ([FPGA module design][fmd]). The two new blocks are AXI-Stream
pass-through filters with an AXI-Lite slave for the key schedule.
```
TX (protector):
openofdm_tx ──AXI-S(IQ)──► [ veil_rot ] ──AXI-S(IQ)──► tx_intf ──► AD9361 DAC
▲ AXI-Lite (key, coeff RAM)
└── veil_openwifi.c
RX (legitimate STA, shares key):
AD9361 ADC ──► rx_intf ──AXI-S──► [ veil_unrot ] ──AXI-S──► openofdm_rx (FFT → chan est)
▲ AXI-Lite
└── veil_openwifi.c
```
`veil_unrot` may equivalently sit **in the frequency domain**, right after the
FFT and **before channel estimation**, if a per-subcarrier `Q^H` is cheaper to
apply there. Same AXI-Lite contract either way.
## Why a *new* block is required (honesty)
openwifi is **SISO 802.11a/g/n** and has **no explicit-beamforming / spatial-
mapping stage** and **no compressed-BF-report generation** — the two-antenna app
note is RX-only capture, not a TX spatial mapper ([iq_2ant][2ant]). So there is
no existing `Q` matrix to modify; `veil_rot`/`veil_unrot` **introduce** the
spatial-mapping stage. Two realizable RTL scopes:
- **Scope A — 1×1 per-subcarrier phase/rotation (lower effort).** Treat the
rotation as operating over a **synthetic vector** formed from the fine
subspace of the per-packet subcarrier response (a stream of `N` IQ elements
the block buffers), applying the core's Givens schedule across those elements.
Single TX chain; no board change. This is enough to *scramble the CSI a
sniffer estimates* and to demonstrate keyed invert at RX. It is **not** true
spatial MIMO.
- **Scope B — 2×2 true spatial mapping (higher effort, the A-capability demo).**
Enable the **second TX chain** (AD9361 has 2 DACs on fmcomms2/3) and apply a
keyed 2×2 unitary across the two streams — a genuine transmit spatial mapping
the standard marks "not restricted." Needs a Vivado top-level rebuild wiring
the 2nd DAC and the extra AXI-S lane. `TODO(hdl)`.
## `veil_rot` datapath
The core applies `passes` **Givens rotations** `G(i,j,θ)` composed into `Q`
(`../core/veil_shield.c`). In hardware we apply the *same schedule* to the on-air
sample vector, so both ends derive identical coefficients from the shared key —
no matrix is transmitted.
Per Givens op on elements `(i, j)` with programmed `(cos, sin)` in Q1.15:
```
v_i' = cos*v_i - sin*v_j
v_j' = sin*v_i + cos*v_j // complex IQ: apply to I and Q lanes
```
- Coefficients arrive from `veil_openwifi.c` as the packed `(i, j, cos, sin)`
schedule (2 AXI-Lite words per pass; packing defined in `veil_openwifi.c`).
- `veil_unrot` applies the schedule **in reverse with negated sin** (`sin → -sin`,
i.e. `Gᵀ`), matching `veil_shield_recover`. A `CTRL.inverse` bit selects it.
- Fixed point: openwifi baseband IQ is 16-bit I / 16-bit Q; coeffs are signed
Q1.15. `TODO(hdl)`: guard-bit / rounding so the composed rotation stays
norm-preserving to spec and never clips (clipping would break the
energy-preservation invariant — must be verified, not assumed).
## AXI-Lite register map (must match `veil_openwifi.c`)
| Offset | Name | Meaning |
|---|---|---|
| `0x00` | `CTRL` | bit0 enable, bit1 inverse (`veil_unrot`), bit2 load |
| `0x04` | `KEY_LO` | session key [31:0] |
| `0x08` | `KEY_HI` | session key [63:32] |
| `0x0C` | `NDIM` | on-air fine-block dimension `N` (≤ 64) |
| `0x10` | `PASSES` | number of Givens passes (default 96) |
| `0x14` | `COEFF_ADDR` | write index into coeff RAM |
| `0x18` | `COEFF_DATA` | packed `{j,i}` then `{sin,cos}` (2 words/pass) |
| `0x1C` | `STATUS` | bit0 ready, bit1 applied, bit2 err |
`TODO(hdl)`: regenerate this from the block's `*_s_axi.v` once written (cf.
`openofdm_tx`'s 6 AXI-Lite registers at `ip/openofdm_tx/src/openofdm_tx_s_axi.v`)
and reconcile any offset changes back into `veil_openwifi.c`.
## Timing / integration risks (call them out, don't hide them)
- **802.11 SIFS budget.** The block adds pipeline latency between IFFT and DAC;
it must not violate the tight TX timing openwifi maintains in `tx_intf`.
`TODO(hdl)`: measure added cycles; keep within budget or absorb in existing
FIFO slack.
- **On-FPGA schedule vs. per-packet coeff load.** For per-*packet* keying, either
compute the SplitMix64 schedule on-FPGA from `(key, packet_counter)` or
double-buffer the coeff RAM. `TODO(hdl)`.
- **Bit-exactness with the core.** The on-FPGA (or shim-fed) `(cos,sin)` must
reproduce the core's schedule so `veil_unrot` inverts exactly. First gate is a
**self-loopback** IQ test (`veil_rot → veil_unrot`, assert recovered == input
within Q1.15 round-off) using openwifi's existing packet/IQ self-loopback
facility ([self-loopback app note][loop]). Passing loopback is a correctness
gate, **not** a defense `MEASURED` claim.
## Build
`TODO(hdl)`: add `veil_rot`/`veil_unrot` as `openwifi-hw` IP, instantiate in the
board block design, and rebuild the bitstream with Vivado per the openwifi-hw
build flow ([openwifi-hw][hw]). No bitstream is produced from this directory.
## Sources
- FPGA module design (AXI-S / AXI-Lite, block roles) — [deepwiki][fmd]
- openwifi-hw (FPGA IP + build flow) — [github.com/open-sdr/openwifi-hw][hw]
- Two-antenna IQ (RX-only; confirms no TX spatial mapper ships) — [iq_2ant][2ant]
- Packet/IQ self-loopback test — [self-loopback app note][loop]
[fmd]: https://deepwiki.com/open-sdr/openwifi/2.2-fpga-module-design
[hw]: https://github.com/open-sdr/openwifi-hw
[2ant]: https://github.com/open-sdr/openwifi/blob/master/doc/app_notes/iq_2ant.md
[loop]: https://github.com/open-sdr/openwifi/blob/master/doc/app_notes/packet-iq-self-loopback-test.md
+103
View File
@@ -0,0 +1,103 @@
# P5 measurement protocol — openwifi WiFi Veil end-to-end
> **STATUS: SYNTHETIC / L0 — this is a PLAN, not a result. No hardware has been
> run; no capture, log, or number in this repo is real.** This document defines
> exactly what must be executed and captured to earn the first `MEASURED` claim
> under CLAUDE.md's hardware-evidence rule. Until the witness artifact below
> exists, every accuracy/throughput/energy statement about openwifi WiFi Veil is
> `SYNTHETIC` and must be labelled so. **Compliant waveform controls only —
> orthogonal, energy-preserving; never jamming.**
## Roadmap position
This is roadmap **P5**: the two-node hardware measurement that turns the P4
build scaffolds into a `MEASURED` defense result. Prerequisite gates (all on real
silicon, all currently unmet): a bitstream with `veil_rot`/`veil_unrot`
(`HDL_NOTES.md`), a driver loading `veil_openwifi.c`, and a passing on-FPGA
**self-loopback** correctness test.
## Topology
```
[ Protector AP ] over the air [ Legitimate STA ]
openwifi node A ───────────────────────────────────► openwifi node B
veil_rot: Q(key) engaged │ veil_unrot: Q^H(key)
│ (shares key with A)
[ Attacker sniffer ]
commodity NIC, monitor mode
Wi-BFI CSI/BF-feedback extraction
+ re-ID model
```
The attacker is **passive** (monitor capture only). Nothing in this test
transmits to interfere with any station.
## Hardware list
| Role | Hardware | Software |
|---|---|---|
| Protector AP (A) | Zynq-7000 + AD9361 FMC (ZC706+fmcomms2/3, or ADRV9361-Z7035) | openwifi image + `veil_rot` bitstream + `veil_openwifi.c` |
| Legitimate STA (B) | second identical openwifi node | openwifi image + `veil_unrot` bitstream + `veil_openwifi.c`, same key as A |
| Attacker | host + Wi-BFI-supported Wi-Fi NIC in monitor mode | Wi-BFI ([arxiv 2309.04408][wibfi]) + re-ID model |
| Bench | shielded room or wired attenuator path preferred | `iperf3`, power meter / board rail sense |
Key agreement A↔B is out-of-band for the demo (pre-shared session key);
per-packet keying uses `(key, packet_counter)` as in `HDL_NOTES.md`.
## Procedure
Run every condition **twice**: WiFi Veil **OFF** (baseline) and **ON**. Same
positions, same MCS, same duration, same seed for the attacker model.
1. **Correctness precondition (not a defense claim).** Confirm on-FPGA
self-loopback recovers IQ within Q1.15 round-off, and A→B link works with
`veil_unrot` engaged. Capture the console log.
2. **Attacker capture.** Sniffer records CSI / beamforming-feedback for a fixed
traffic pattern A→B, OFF then ON. Save raw captures (pcap + Wi-BFI output).
3. **Re-ID metric.** Run the same re-identification / fingerprinting model on the
OFF and ON captures. Report accuracy and confusion vs. the **chance / mean
baseline** (per CLAUDE.md, a defense claim needs the baseline and a
leakage-free held-out split — never report bare accuracy).
4. **Throughput (near-free check).** `iperf3` A↔B, OFF vs. ON, both directions.
Expected: ON ≈ OFF (the receiver inverts the rotation). Save `iperf3 --json`.
5. **Energy / compliance.** Record per-frame TX energy OFF vs. ON (rail sense or
power meter) to substantiate the "energy-preserving / not jamming" claim, and
spectrum/mask conformance if a spectrum analyzer is available.
## Metrics reported
| Metric | OFF | ON | Requirement for a pass |
|---|---|---|---|
| Attacker re-ID accuracy vs. chance | baseline | — | collapses toward chance ON |
| iperf3 throughput A↔B | baseline | — | ON within a few % of OFF |
| Per-frame TX energy | baseline | — | ON ≈ OFF (orthogonality holds on-air) |
| Spectral mask conformance | pass | — | still conformant ON |
## Required witness artifact (CLAUDE.md gate)
Before **any** `MEASURED` claim, this directory (or the P5 evidence path) must
contain a **captured real-silicon log**, not a build or simulator output:
- Boot/runtime console log of both openwifi nodes showing the `veil_rot` /
`veil_unrot` bitstream loaded and `veil_openwifi.c` programming the session
(register writes / STATUS ready), with timestamps and board identifiers.
- The self-loopback correctness log (step 1).
- Raw attacker captures (pcap + Wi-BFI output) for OFF and ON, plus the exact
re-ID reproducer command and its output.
- `iperf3 --json` for OFF and ON; energy trace for OFF and ON.
- A manifest tying each artifact to the git commit of the RTL, driver, and shim
used, so the result is reproducible.
Label the result `MEASURED` **only** with all of the above captured from real
hardware. A successful Vivado build, a Verilator/QEMU run, or the host
`veil_openwifi.c` self-test is **not** hardware evidence and must stay
`SYNTHETIC`. No log in this repo today — do not fabricate one.
## Sources
- Wi-BFI (attacker BF-feedback extraction) — [arxiv.org/pdf/2309.04408][wibfi]
- Packet/IQ self-loopback test — [openwifi self-loopback app note][loop]
[wibfi]: https://arxiv.org/pdf/2309.04408
[loop]: https://github.com/open-sdr/openwifi/blob/master/doc/app_notes/packet-iq-self-loopback-test.md
+122
View File
@@ -0,0 +1,122 @@
# WiFi Veil protector — openwifi (Xilinx Zynq + AD9361, open PHY/MAC)
> **STATUS: SYNTHETIC / L0 — build-only scaffold. No hardware, no flash, no
> capture. Nothing here has run on silicon.** Per CLAUDE.md, none of this is a
> `MEASURED` result and none may be claimed as working. Files are honest
> skeletons with real openwifi idioms plus `TODO(hw)` / `TODO(hdl)` markers, not
> validated firmware or complete HDL. **Compliant waveform controls only — the
> keyed rotation is orthogonal (energy-preserving) and shapes only this node's
> own standards-conformant emission. Never jamming.**
## Feasibility grade: **B (capability ceiling A; effort D)**
openwifi is the **only** platform in this tree where a true end-to-end keyed
rotation *and its inverse* are physically reachable, because it is the only one
that exposes the full open PHY/MAC on FPGA: `openofdm_tx`/`openofdm_rx`,
`tx_intf`/`rx_intf`, and `side_ch`, all AXI-Lite-programmable from a Linux
driver ([FPGA module design][fmd], [openwifi overview][ov]). That is the **A**
capability ceiling.
It is graded **B**, not A, for two honest reasons that make it the
highest-*effort* path:
1. **openwifi has no native explicit transmit beamforming.** It ships as an
802.11a/g/n **single-spatial-stream (SISO)** design. It does not run NDP
sounding, does not compute an SVD `V` matrix, and does not emit a compressed
beamforming report. The two-antenna app note is **RX-only** coherent capture
(`side_ch_ctl wh3h11`), not a MIMO transmit spatial mapper ([iq_2ant][2ant]).
So there is no shipped compressed-BF-report to obfuscate and no shipped
spatial-mapping matrix `Q` to left-multiply — both must be **added in HDL**.
2. Reaching a true two-stream demo needs a **second TX chain** (the AD9361 on
fmcomms2/3 has two DACs) plus a new spatial-mapping RTL stage and a Vivado
rebuild — days-to-weeks of FPGA work, not a driver patch.
Because of (1), on openwifi WiFi Veil is realized as the **client-transparent
per-packet keyed unitary** (LeakyBeam family) applied at the TX spatial-mapping
stage, with the legitimate STA (a second openwifi node sharing the key)
inverting it — **not** as obfuscation of a compressed-BF report the hardware
never produces. This keeps the claim honest: we rotate the *transmitted spatial
mapping* so a sniffer's per-subcarrier channel estimate `H·Q(key)` is scrambled,
and the keyed receiver applies `Q(key)^H` before channel estimation.
## Exact insertion points
The rotation is a keyed orthogonal (unitary) matrix `Q(key, session)` computed
by the portable core (`../core/veil_shield.{h,c}`), the same SplitMix64 schedule
used everywhere, so both ends derive the identical `Q` from the shared key.
**TX (protector) — FPGA, new block `veil_rot`:**
Insert on the baseband IQ AXI-Stream path **between `openofdm_tx` (post-IFFT,
post-CP) and `tx_intf`** (which feeds the AD9361 DAC). `veil_rot` left-multiplies
the per-subcarrier / per-stream sample vector by `Q(key)`. Its coefficients (or a
key seed + on-FPGA schedule) are written over **AXI-Lite** from the driver shim
using the standard openwifi `iowrite32(value, base_addr + reg)` idiom
([tx_intf driver][txintf]). See `HDL_NOTES.md`.
**RX (legitimate STA) — FPGA, new block `veil_unrot`:**
Insert **between `rx_intf` (AD9361 ADC) and `openofdm_rx`**, or in the frequency
domain immediately after the FFT and **before channel estimation**, applying
`Q(key)^H`. Same AXI-Lite programming path.
**Driver / control plane:** the C shim `veil_openwifi.c` computes the session
key schedule via the core and programs the blocks. Real openwifi control idioms:
AXI-Lite MMIO from the kernel driver, and the `sdrctl` nl80211-testmode tool /
`side_ch_ctl` register pokes for bring-up ([sdrctl/side_ch][ov], [frequent
tricks][ft]). Where the exact offsets/bitfields are not yet fixed, the shim
marks `TODO(hw)`; RTL specifics are `TODO(hdl)`.
Doing the rotation in HDL (not the DMA'd payload) is deliberate: it keeps the
frame **standards-conformant on the wire** and preserves transmit energy — the
"not jamming" invariant the core guarantees by construction (orthogonal `Q`).
## Two-node measurement plan (the P5 path)
Three roles produce the first `MEASURED` / P5 result (full protocol +
required witness log in `MEASUREMENT.md`):
- **Protector AP** — openwifi node A, `veil_rot` engaged, TX spatial mapping
keyed with the session key.
- **Legitimate STA** — openwifi node B, shares the key, `veil_unrot` engaged;
should see **near-baseline throughput** (rotation cancels).
- **Attacker sniffer** — a commodity Wi-Fi NIC running **Wi-BFI** / monitor
capture, extracting the per-subcarrier CSI / beamforming feedback and running
the re-ID model ([Wi-BFI][wibfi]).
Headline metric: **re-identification accuracy off vs. on** at the attacker
(target: collapse toward chance) **while** iperf throughput A↔B stays near
baseline and per-frame energy is unchanged. No number here is real until a
captured on-silicon log exists.
## Bill of materials (target, not procured)
- 2× Xilinx Zynq-7000 board with AD9361 FMC (e.g. ZC706 + fmcomms2/3, or
ADRV9361-Z7035 / Antenna-SDR), openwifi image per the openwifi build docs.
- 1× attacker host + Wi-BFI-capable NIC (per Wi-BFI's supported list).
- Vivado for the FPGA rebuild that adds `veil_rot` / `veil_unrot`.
## Files here
| File | What it is |
|---|---|
| `README.md` | this — feasibility, insertion points, measurement plan |
| `veil_openwifi.c` | driver-side C shim: core → session `Q` → AXI-Lite program (scaffold, `TODO(hw)`) |
| `HDL_NOTES.md` | the `veil_rot` / `veil_unrot` Verilog blocks (design notes, `TODO(hdl)`) |
| `MEASUREMENT.md` | exact P5 protocol, metrics, and the required witness artifact |
## Sources
- FPGA module design — [deepwiki.com/open-sdr/openwifi/2.2-fpga-module-design][fmd]
- openwifi overview (sdrctl, side_ch, nl80211 testmode) — [deepwiki.com/open-sdr/openwifi/1-openwifi-overview][ov]
- Two-antenna IQ (RX-only) app note — [github.com/open-sdr/openwifi .../iq_2ant.md][2ant]
- tx_intf driver register idioms (`iowrite32`/`ioread32`) — [github.com/open-sdr/openwifi .../tx_intf.c][txintf]
- Frequent tricks / register pokes — [github.com/open-sdr/openwifi .../frequent_trick.md][ft]
- openwifi paper (SDR 802.11 on SoC) — [researchgate .../342582824][paper]
- Wi-BFI (attacker BF-feedback extraction) — [arxiv.org/pdf/2309.04408][wibfi]
[fmd]: https://deepwiki.com/open-sdr/openwifi/2.2-fpga-module-design
[ov]: https://deepwiki.com/open-sdr/openwifi/1-openwifi-overview
[2ant]: https://github.com/open-sdr/openwifi/blob/master/doc/app_notes/iq_2ant.md
[txintf]: https://github.com/open-sdr/openwifi/blob/master/driver/tx_intf/tx_intf.c
[ft]: https://github.com/open-sdr/openwifi/blob/master/doc/app_notes/frequent_trick.md
[paper]: https://www.researchgate.net/publication/342582824_openwifi_a_free_and_open-source_IEEE80211_SDR_implementation_on_SoC
[wibfi]: https://arxiv.org/pdf/2309.04408
@@ -0,0 +1,315 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* veil_openwifi driver-side shim that binds the portable VEIL core
* (../core/veil_shield.{h,c}) to the openwifi FPGA TX/RX datapath.
*
* STATUS: SYNTHETIC / L0. Build-only scaffold. Never compiled into the openwifi
* kernel module on real silicon, never flashed, never captured. Do NOT claim
* runtime behavior without a captured hardware log (CLAUDE.md hardware-evidence
* rule). Register offsets, bitfields, and the FPGA blocks it programs
* (veil_rot / veil_unrot) do NOT exist in upstream openwifi yet every place
* that depends on real hardware is marked TODO(hw); RTL specifics live in
* HDL_NOTES.md and are marked TODO(hdl) there.
*
* ROLE (honest): this shim runs on the protector AP and on the legitimate STA.
* - Protector: derive the per-session keyed unitary Q(key) from the core and
* program the veil_rot block that left-multiplies the transmit spatial
* mapping (inserted between openofdm_tx and tx_intf; see HDL_NOTES.md).
* - Legitimate STA: derive the same Q(key) and program veil_unrot to apply
* Q^H before channel estimation, cancelling the rotation (near-free tput).
* The transform is orthogonal, so transmit energy is preserved: compliant,
* NOT jamming. openwifi ships SISO with no explicit beamforming, so this is the
* client-transparent per-packet unitary route, not obfuscation of a compressed
* beamforming report (openwifi never generates one) see README.md.
*
* openwifi idioms used where known:
* - AXI-Lite MMIO from the driver: iowrite32(value, base + reg) /
* ioread32(base + reg), matching driver/tx_intf/tx_intf.c reg_write/reg_read.
* - Coefficients are quantized to the fixed-point width the datapath uses
* (openwifi baseband IQ is 16-bit I / 16-bit Q); see VEIL_ROT_FRAC below.
*
* This file is written to compile in two modes:
* - Host/CI (default): __KERNEL__ undefined -> MMIO is stubbed to a local
* shadow buffer so the key-schedule + quantization logic is unit-testable
* with no hardware. This is the ONLY path exercised today.
* - In-tree kernel build: define VEIL_OPENWIFI_KERNEL to pull the real
* linux/io.h accessors. Untested. TODO(hw).
*/
#include "../core/veil_shield.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
/* -------------------------------------------------------------------------
* MMIO layer. Real openwifi drivers keep a per-block __iomem base and use
* iowrite32/ioread32. We isolate that here so host/CI builds need no kernel.
* ------------------------------------------------------------------------- */
#if defined(VEIL_OPENWIFI_KERNEL)
#include <linux/io.h>
typedef void __iomem *veil_mmio_base;
static inline void veil_reg_write(veil_mmio_base b, uint32_t reg, uint32_t v) {
iowrite32(v, (uint8_t __iomem *)b + reg);
}
static inline uint32_t veil_reg_read(veil_mmio_base b, uint32_t reg) {
return ioread32((uint8_t __iomem *)b + reg);
}
#else
/* Host/CI shadow: a small register file so logic is testable with no radio. */
#define VEIL_SHADOW_REGS 256
typedef struct {
uint32_t regs[VEIL_SHADOW_REGS];
} veil_mmio_shadow;
typedef veil_mmio_shadow *veil_mmio_base;
static inline void veil_reg_write(veil_mmio_base b, uint32_t reg, uint32_t v) {
if (b && (reg >> 2) < VEIL_SHADOW_REGS) {
b->regs[reg >> 2] = v;
}
}
static inline uint32_t veil_reg_read(veil_mmio_base b, uint32_t reg) {
if (b && (reg >> 2) < VEIL_SHADOW_REGS) {
return b->regs[reg >> 2];
}
return 0;
}
#endif
/* -------------------------------------------------------------------------
* Register map for the (not-yet-existing) veil_rot / veil_unrot AXI-Lite
* slaves. Offsets are PLACEHOLDERS chosen to be word-aligned; the real map is
* fixed when the RTL lands. TODO(hw): confirm against the generated
* *_s_axi.v once veil_rot exists (cf. openofdm_tx's 6 AXI-Lite regs).
* ------------------------------------------------------------------------- */
#define VEIL_ROT_REG_CTRL 0x00u /* bit0 enable, bit1 inverse, bit2 load */
#define VEIL_ROT_REG_KEY_LO 0x04u /* session key [31:0] */
#define VEIL_ROT_REG_KEY_HI 0x08u /* session key [63:32] */
#define VEIL_ROT_REG_NDIM 0x0Cu /* fine-block dimension N applied on-air */
#define VEIL_ROT_REG_PASSES 0x10u /* number of Givens passes */
#define VEIL_ROT_REG_COEFF_ADDR 0x14u /* write index into the coeff RAM */
#define VEIL_ROT_REG_COEFF_DATA 0x18u /* {Q16.15 sin, Q16.15 cos} packed */
#define VEIL_ROT_REG_STATUS 0x1Cu /* bit0 ready, bit1 applied, bit2 err */
#define VEIL_ROT_CTRL_ENABLE (1u << 0)
#define VEIL_ROT_CTRL_INVERSE (1u << 1)
#define VEIL_ROT_CTRL_LOAD (1u << 2)
#define VEIL_ROT_STATUS_READY (1u << 0)
/* Fixed-point: openwifi baseband IQ is 16-bit. We program rotation coeffs as
* signed Q1.15 (fractional bits = 15). cos/sin in [-1,1] map cleanly. */
#define VEIL_ROT_FRAC 15
/* Default schedule parameters — kept byte-consistent with the core/Rust crate
* defaults. N is the on-air fine-block dimension the datapath vectorizes over;
* for the SISO-plus-synthetic-stream demo this is small (see HDL_NOTES.md). */
#define VEIL_OW_DEFAULT_PASSES 96u
#define VEIL_OW_MAX_NDIM 64u /* bounded coeff RAM; keeps it malloc-free */
typedef enum {
VEIL_OW_ROLE_PROTECTOR = 0, /* TX veil_rot, forward rotation Q */
VEIL_OW_ROLE_LEGIT_RX = 1, /* RX veil_unrot, inverse rotation Q^H */
} veil_ow_role;
typedef struct {
veil_mmio_base base; /* AXI-Lite base of veil_rot / veil_unrot slave */
uint64_t key; /* shared session key (both ends must match) */
uint32_t ndim; /* fine-block dimension, <= VEIL_OW_MAX_NDIM */
uint32_t passes; /* Givens passes */
veil_ow_role role;
} veil_ow_ctx;
/* Saturating float -> signed Q1.15. */
static int16_t veil_q15(float x) {
float scaled = x * (float)(1 << VEIL_ROT_FRAC);
if (scaled > 32767.0f) return 32767;
if (scaled < -32768.0f) return -32768;
return (int16_t)lrintf(scaled);
}
/* -------------------------------------------------------------------------
* Coefficient generation. The core's schedule is (i, j, theta) Givens ops
* derived from SplitMix64(key). The FPGA applies the SAME schedule to on-air
* samples, so we hand it the per-pass (i, j, cos, sin). We regenerate the
* schedule here with the identical draw order as veil_shield.c so the shim and
* the (future) RTL agree bit-for-bit with the reference crate.
*
* NOTE: this mirrors veil_shield.c's private schedule. It is duplicated (not
* exported) on purpose the core stays a pure in-memory transform with a
* stable ABI; the adapter owns the hardware-facing serialization. If the core
* later exports its schedule, collapse this. TODO(hw): validate equality with a
* captured on-FPGA coeff dump before any MEASURED claim.
* ------------------------------------------------------------------------- */
typedef struct {
uint16_t i;
uint16_t j;
int16_t cos_q15;
int16_t sin_q15;
} veil_ow_givens;
/* TAU matches VEIL_TAU in veil_shield.c / Rust core::f32::consts::TAU. */
#define VEIL_OW_TAU 6.28318530717958647692f
static void veil_ow_build_schedule(uint64_t key, uint32_t n, uint32_t passes,
veil_ow_givens *out /* [passes] */) {
veil_rng r;
uint32_t p;
if (n < 2) {
for (p = 0; p < passes; p++) {
out[p].i = 0; out[p].j = 0;
out[p].cos_q15 = veil_q15(1.0f); out[p].sin_q15 = 0;
}
return;
}
veil_rng_seed(&r, key);
for (p = 0; p < passes; p++) {
uint32_t i = (uint32_t)(veil_rng_next_u64(&r) % (uint64_t)n);
uint32_t j = (uint32_t)(veil_rng_next_u64(&r) % (uint64_t)n);
float theta;
if (j == i) {
j = (j + 1) % n;
}
theta = veil_rng_next_f32(&r) * VEIL_OW_TAU;
out[p].i = (uint16_t)i;
out[p].j = (uint16_t)j;
out[p].cos_q15 = veil_q15(cosf(theta));
out[p].sin_q15 = veil_q15(sinf(theta));
}
}
/* -------------------------------------------------------------------------
* Public API.
* ------------------------------------------------------------------------- */
/* Program a session key into the veil_rot/veil_unrot block. Returns 0 on the
* host shadow path; on real hardware it must poll STATUS_READY. */
int veil_ow_program_session(veil_ow_ctx *ctx) {
veil_ow_givens sched[VEIL_OW_DEFAULT_PASSES];
uint32_t ctrl = VEIL_ROT_CTRL_LOAD;
uint32_t p, passes, n;
if (!ctx || ctx->ndim < 2 || ctx->ndim > VEIL_OW_MAX_NDIM) {
return -1; /* bounds check the on-air dimension (least authority) */
}
passes = ctx->passes ? ctx->passes : VEIL_OW_DEFAULT_PASSES;
if (passes > VEIL_OW_DEFAULT_PASSES) {
passes = VEIL_OW_DEFAULT_PASSES; /* bounded, stack-only schedule */
}
n = ctx->ndim;
veil_ow_build_schedule(ctx->key, n, passes, sched);
/* Program header registers. */
veil_reg_write(ctx->base, VEIL_ROT_REG_KEY_LO, (uint32_t)(ctx->key));
veil_reg_write(ctx->base, VEIL_ROT_REG_KEY_HI, (uint32_t)(ctx->key >> 32));
veil_reg_write(ctx->base, VEIL_ROT_REG_NDIM, n);
veil_reg_write(ctx->base, VEIL_ROT_REG_PASSES, passes);
/* Stream the (i, j, cos, sin) schedule into the coeff RAM. Packing:
* COEFF_DATA = {i[15:0]... } is too wide for one 32-bit word, so we use a
* 2-word-per-pass convention: word A = {j[15:0], i[15:0]}, word B =
* {sin_q15[15:0], cos_q15[15:0]}. TODO(hdl): the veil_rot coeff-RAM write
* FSM must match this exact packing. TODO(hw): confirm endianness of the
* AXI-Lite slave. */
for (p = 0; p < passes; p++) {
uint32_t wa = ((uint32_t)(uint16_t)sched[p].j << 16) |
(uint32_t)(uint16_t)sched[p].i;
uint32_t wb = ((uint32_t)(uint16_t)sched[p].sin_q15 << 16) |
(uint32_t)(uint16_t)sched[p].cos_q15;
veil_reg_write(ctx->base, VEIL_ROT_REG_COEFF_ADDR, p * 2u);
veil_reg_write(ctx->base, VEIL_ROT_REG_COEFF_DATA, wa);
veil_reg_write(ctx->base, VEIL_ROT_REG_COEFF_ADDR, p * 2u + 1u);
veil_reg_write(ctx->base, VEIL_ROT_REG_COEFF_DATA, wb);
}
if (ctx->role == VEIL_OW_ROLE_LEGIT_RX) {
ctrl |= VEIL_ROT_CTRL_INVERSE; /* veil_unrot applies Q^H */
}
veil_reg_write(ctx->base, VEIL_ROT_REG_CTRL, ctrl);
/* TODO(hw): on real silicon, poll VEIL_ROT_REG_STATUS for READY here and
* time out. The host shadow has no FSM, so we return success directly and
* DO NOT claim the hardware accepted it. */
#if defined(VEIL_OPENWIFI_KERNEL)
{
int spins = 100000; /* TODO(hw): calibrate against real ready latency */
while (spins-- > 0) {
if (veil_reg_read(ctx->base, VEIL_ROT_REG_STATUS) &
VEIL_ROT_STATUS_READY) {
break;
}
}
if (spins <= 0) {
return -2; /* not ready — never treat as success */
}
}
#endif
return 0;
}
/* Engage / disengage the block (bit0 of CTRL), preserving the inverse bit. */
int veil_ow_set_enabled(veil_ow_ctx *ctx, int enable) {
uint32_t ctrl;
if (!ctx) {
return -1;
}
ctrl = veil_reg_read(ctx->base, VEIL_ROT_REG_CTRL);
if (enable) {
ctrl |= VEIL_ROT_CTRL_ENABLE;
} else {
ctrl &= ~VEIL_ROT_CTRL_ENABLE;
}
veil_reg_write(ctx->base, VEIL_ROT_REG_CTRL, ctrl);
return 0;
}
/*
* Control-plane bring-up alternatives (documented idioms, not wired here):
* - sdrctl (nl80211 testmode) for driver-level toggles once a testmode verb
* is added, e.g. a "veil" subcommand mirroring existing sdrctl reg pokes.
* - side_ch_ctl-style hex register pokes during bench bring-up, e.g. the
* side_ch app note's `./side_ch_ctl whXXdY` write convention, retargeted at
* the veil_rot slave. TODO(hw): pick and document the actual verb.
*
* Self-loopback validation (before over-the-air): openwifi supports a
* packet/IQ self-loopback test. Route veil_rot -> veil_unrot in loopback and
* assert recovered IQ == original within Q1.15 round-off. That is the first
* on-FPGA correctness gate (still not a defense MEASURED claim). TODO(hw).
*/
#if defined(VEIL_OPENWIFI_SELFTEST)
/* Host-only smoke test of the schedule/quantization path — NO hardware.
* Verifies the shadow register file receives a plausible, bounded program.
* Build: cc -DVEIL_OPENWIFI_SELFTEST veil_openwifi.c ../core/veil_shield.c -lm */
#include <stdio.h>
int main(void) {
veil_mmio_shadow shadow;
veil_ow_ctx ctx;
memset(&shadow, 0, sizeof(shadow));
ctx.base = &shadow;
ctx.key = 0x0123456789ABCDEFull;
ctx.ndim = 16;
ctx.passes = VEIL_OW_DEFAULT_PASSES;
ctx.role = VEIL_OW_ROLE_PROTECTOR;
if (veil_ow_program_session(&ctx) != 0) {
printf("FAIL: program_session\n");
return 1;
}
if (veil_ow_set_enabled(&ctx, 1) != 0) {
printf("FAIL: set_enabled\n");
return 1;
}
if (veil_reg_read(&shadow, VEIL_ROT_REG_NDIM) != 16u) {
printf("FAIL: ndim not programmed\n");
return 1;
}
if (!(veil_reg_read(&shadow, VEIL_ROT_REG_CTRL) & VEIL_ROT_CTRL_ENABLE)) {
printf("FAIL: enable bit\n");
return 1;
}
printf("OK (SYNTHETIC/L0 host shadow only — NOT hardware-validated)\n");
return 0;
}
#endif
@@ -0,0 +1,95 @@
# WiFi Veil ↔ `mac80211` / driver integration map
> **`SYNTHETIC / L0` — BUILD-ONLY, UNTESTED ON HARDWARE.** These are hook-point
> designs derived from public API/source, not validated on silicon. Function and
> attribute names are real (verified against in-tree `linux/nl80211.h` and public
> hostapd/driver docs); where a hook does **not** exist upstream it is marked
> `TODO(hw)` with what a patch would have to add. Compliant controls only.
Legend: **US** = userspace-reachable today · **DP** = needs driver patch ·
**FW** = needs firmware patch (blob-blocked).
---
## 1. TX antenna-map perturbation — **US** (feasible)
- **Daemon:** `veil_set_tx_antenna_mask()` in `veil_shieldd.c`.
- **Kernel path:** `nl80211``cfg80211_ops.set_antenna()` → driver
`.set_antenna` (e.g. `mt7915_set_antenna`, `ath9k` `set_antenna`).
- **Attributes:** `NL80211_CMD_SET_WIPHY`, `NL80211_ATTR_WIPHY_ANTENNA_TX`,
`NL80211_ATTR_WIPHY_ANTENNA_RX`.
- **Constraints:** many drivers require the phy DOWN and accept only symmetric
masks; validate per driver. Coarse static spatial-mapping change, not the keyed
rotation. Fully standards-compliant.
## 2. NDP sounding-cadence jitter — **US (indirect)**
- **Daemon:** `veil_randomize_sounding_cadence()` / `veil_next_cadence_ms()`.
The schedule is derived from the session key via the core SplitMix64 so the
paired receiver can anticipate it (not random spraying).
- **Real lever:** hostapd `ctrl_iface` (UNIX socket `/var/run/hostapd/<iface>`):
`SET he_su_beamformer …` / rewrite `vht_capab` `[SOUNDING-DIMENSION-n]` /
toggle `[SU-BEAMFORMER]`, then `RECONFIGURE`. Config keys documented in
`hostapd.conf`.
- **`TODO(hw)`:** there is **no** `nl80211` "set sounding interval" command; the
per-NDP timer is in driver/firmware. We can only jitter the *offered* cadence.
The `ctrl_iface` write itself is not yet wired (function currently only
computes `ms`).
## 3. MU-MIMO group shuffling — **FW** (blob-blocked)
- **Daemon:** `veil_shuffle_mumimo_groups()` — explicit `-ENOTSUP` no-op.
- **Where it lives:** MU group formation + per-group steering matrices are
computed in the WiFi MCU firmware on mt76 (mt7915) and all ath1x parts.
- **`TODO(hw)`:** would require `NL80211_CMD_VENDOR` with a driver-specific
`NL80211_ATTR_VENDOR_ID` / `NL80211_ATTR_VENDOR_SUBCMD` /
`NL80211_ATTR_VENDOR_DATA` that upstream mt76/ath do **not** define, plus a
firmware change to honor an externally supplied grouping. Not reachable without
both a driver and firmware patch.
## 4. Per-packet keyed unitary (the core WiFi Veil transform) — **FW** (blob-blocked)
- **Daemon:** `veil_apply_keyed_rotation()` → `veil_shield_apply(fine, n, key,
passes)` from the portable core. Orthogonal / energy-preserving (the
"not jamming" invariant, checked via `veil_l2_norm` before/after).
- **What a full path must touch:**
- **mt76 (mt7915):** the MCU firmware stage that builds the compressed
beamforming report (φ/ψ angles) or applies the steering/precoder Q to the
LTF spatial mapping. A firmware patch would call the rotation on the fine
subspace *before* the report is emitted / precoder applied. The driver
(`mt7915/mcu.c`) would ferry the key/passes down via a new MCU command.
- **ath9k (DP, best open case):** the static spatial-mapping matrix is set via
`AR_PHY_*` registers in the open PHY init; a driver patch could apply a keyed
*static* Q there. This is coarser than a true per-packet report edit but is
the most credible OpenWRT-adjacent route (older 802.11n hardware only).
- **ath10k/ath11k/ath12k:** report generation + precoder are entirely
firmware-side with no open firmware (ath11k/ath12k) — not patchable.
- **`TODO(hw)`:** on OpenWRT there is **no** userspace/`mac80211` hook that hands
the pre-precoder V/steering buffer to the daemon before TX. Reaching it needs
the driver+firmware patch above, or use the **openwifi (FPGA)** / **Nexmon
(Broadcom)** adapters, which expose the datapath. The daemon only proves the
math is invariant; nothing goes on air.
## 5. Sensing-solicitation (NDPA) detection — **US/DP** (partial)
- **Daemon:** `veil_event_cb()` on `NL80211_CMD_FRAME`.
- **Real path:** `NL80211_CMD_REGISTER_FRAME` to subscribe to specific
management action categories, delivered as `NL80211_CMD_FRAME` with
`NL80211_ATTR_FRAME`. Classify VHT/HE compressed beamforming action
(categories 21 / 30) and NDP Announcement to measure cadence.
- **`TODO(hw)`:** commodity drivers do **not** forward raw NDPA to userspace by
default; honest external-solicitation detection needs monitor-mode capture or a
driver notification that is not guaranteed upstream. Frame parsing is stubbed.
---
## Summary of the effort boundary
| Control | Effort to reach full WiFi Veil fidelity |
|---|---|
| TX antenna map | Ready now (US), coarse only |
| Sounding cadence jitter | Wire hostapd `ctrl_iface` (US), coarse only |
| Static spatial Q | ath9k driver patch (DP) |
| MU grouping | driver vendor subcmd + firmware (FW) |
| Per-packet keyed rotation | mt76/ath **firmware** patch, or openwifi/Nexmon adapter (FW) |
| NDPA detection | frame registration + likely driver patch (US/DP) |
+44
View File
@@ -0,0 +1,44 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
#
# Host build-CHECK for the OpenWRT/mac80211 VEIL adapter.
# STATUS: SYNTHETIC / L0 — build-only, UNTESTED ON HARDWARE.
#
# Two targets:
# make core - compile+link the portable core only (always works,
# no libnl needed) — proves the rotation math builds.
# make daemon - build veil_shieldd against libnl-genl-3 (needs the
# dev headers: `pkg-config libnl-genl-3.0`). On OpenWRT
# the package build uses libnl-tiny instead (see openwrt.mk).
#
# This Makefile does NOT flash, run on, or validate any radio.
CC ?= cc
COREDIR := ../core
CFLAGS ?= -std=c99 -Wall -Wextra -O2 -I$(COREDIR)
LDLIBS ?= -lm
NL_CFLAGS := $(shell pkg-config --cflags libnl-genl-3.0 2>/dev/null)
NL_LIBS := $(shell pkg-config --libs libnl-genl-3.0 2>/dev/null)
.PHONY: all core daemon clean
all: core
# Always-buildable: the core object, no netlink dependency.
core: $(COREDIR)/veil_shield.c $(COREDIR)/veil_shield.h
$(CC) $(CFLAGS) -c $(COREDIR)/veil_shield.c -o veil_shield.o
@echo "core built (rotation math OK). Nothing was run on hardware."
# Full daemon: requires libnl-genl-3 dev headers on the host.
daemon: veil_shieldd.c core
ifeq ($(strip $(NL_LIBS)),)
@echo "SKIP daemon: libnl-genl-3.0 not found (pkg-config)."
@echo " Install libnl-3-dev + libnl-genl-3-dev, or build via openwrt.mk."
@exit 0
else
$(CC) $(CFLAGS) $(NL_CFLAGS) -o veil_shieldd \
veil_shieldd.c veil_shield.o $(NL_LIBS) $(LDLIBS)
@echo "veil_shieldd linked (BUILD-ONLY; untested on silicon)."
endif
clean:
rm -f veil_shield.o veil_shieldd
+112
View File
@@ -0,0 +1,112 @@
# WiFi Veil — OpenWRT / Linux `mac80211` adapter
> **STATUS: `SYNTHETIC / L0` — BUILD-ONLY, UNTESTED ON HARDWARE.**
> No radio was driven, no CSI captured, no log produced on silicon. Every
> claim below is a design/feasibility statement, not a `MEASURED` result. This
> adapter uses **compliant waveform controls only** — it never jams and emits
> no denial energy.
This directory is the OpenWRT/`mac80211` platform adapter for the WiFi Veil privacy
shield. It links the validated portable core
(`../core/veil_shield.{h,c}` — the keyed Givens rotation over the identity-bearing
"fine" subspace of 802.11 compressed beamforming feedback) and drives the subset
of controls that Linux userspace/`mac80211` can actually reach on commodity APs.
---
## Feasibility grade: **C** (partial — coarse compliant controls only)
**Why C, not higher.** WiFi Veil's defining action is a *per-packet keyed unitary* on
the compressed beamforming-feedback angles (equivalently, a keyed Q on the LTF
spatial mapping / precoder). On every mainstream OpenWRT AP chipset
(Qualcomm ath10k/ath11k/ath12k, MediaTek mt76 / mt7915), that report is generated
and the precoder applied **inside the WiFi MCU firmware blob** — userspace and the
open driver never touch the pre-transmit V matrix. So the full keyed-rotation path
is **blob-blocked** from OpenWRT. What remains reachable is a set of *coarse*
compliant knobs that perturb, but do not cryptographically obfuscate, the CSI a
sensor observes. That is a real, honest defense-in-depth layer — hence C, not D —
but it is not the full WiFi Veil transform.
**Why not D.** Some controls genuinely work from userspace (TX antenna map;
hostapd-mediated sounding/beamformer capability), and one chipset family
(**ath9k**) is open enough at the register level that a *driver patch* could reach
the static spatial-mapping matrix — a credible route to B on that specific,
older hardware. openwifi (FPGA) and Nexmon (Broadcom) are the routes to the full
A-grade keyed rotation, but those are **separate adapters**, not OpenWRT.
---
## What is FEASIBLE vs. BLOB-BLOCKED from OpenWRT
| WiFi Veil control | Reachable from OpenWRT? | Mechanism (real API / knob) | Notes |
|---|---|---|---|
| **TX antenna-map perturbation** | ✅ Feasible | `NL80211_CMD_SET_WIPHY` + `NL80211_ATTR_WIPHY_ANTENNA_TX` / `_RX` | Coarse static spatial-mapping change. Many drivers require phy DOWN and symmetric masks. Compliant. |
| **NDP sounding-cadence jitter** | 🟡 Indirect | hostapd `ctrl_iface` (rewrite `SOUNDING-DIMENSION`, toggle `[SU-BEAMFORMER]`, `RECONFIGURE`) | No `nl80211` "set sounding interval" exists; the per-NDP timer lives in driver/firmware. We can only jitter the *offered* capability. |
| **Beamformer/beamformee capability toggle** | ✅ Feasible | hostapd `vht_capab` / `he_su_beamformer` etc. | Standards-compliant advertisement. Coarse on/off, not per-packet. |
| **Spatial-stream → antenna mapping (static Q)** | 🟡 Driver-patch (ath9k only) | ath9k PHY spatial-mapping registers (`AR_PHY_*`) | Open enough to patch on ath9k; opaque/firmware on ath10k+/mt76. Not a stock userspace knob. |
| **MU-MIMO group shuffling** | ❌ Blob-blocked | would need `NL80211_CMD_VENDOR` subcmd that upstream mt76/ath do **not** expose | Group formation + steering matrices computed in MCU firmware. |
| **Per-packet keyed unitary on LTF / precoder** | ❌ Blob-blocked | — | The core WiFi Veil transform. Lives in firmware on all commodity AP parts. Requires firmware patch, or use openwifi / Nexmon adapters. |
| **Compressed-BF-report angle edit (φ/ψ)** | ❌ Blob-blocked | — | Report is generated in firmware/PHY; not exposed pre-TX on OpenWRT. |
| **External sensing-solicitation detection (NDPA cadence)** | 🟡 Partial | `NL80211_CMD_FRAME` + `NL80211_CMD_REGISTER_FRAME`, or monitor-mode capture | Commodity drivers do not forward raw NDPA to userspace by default. |
---
## Best candidate chipsets / drivers
- **ath9k (Atheros 802.11n)***best open target for a driver-side patch.* The
most transparent open driver (no per-packet firmware for the datapath), with a
long history of PHY register access and the Atheros CSI Tool ecosystem. A
static spatial-mapping perturbation and CSI observation are realistic here;
full HT beamforming-feedback editing still is not in open code. 802.11n-only.
- **mt76 (MediaTek mt7915 / mt7622-mt7615)** — *best-maintained modern open
driver* and the most likely place upstream would eventually accept a vendor
hook, but beamforming/sounding/MU grouping run in the MCU firmware today, so
the keyed path needs a firmware patch (blob-blocked out of the box).
- **ath10k / ath11k / ath12k (Qualcomm)** — most capable radios but the most
closed: regulatory + beamforming + sounding all firmware-side. ath11k/ath12k
have **no open firmware** at all. Worst target for the keyed path.
- **openwifi (FPGA SDR) / Nexmon (Broadcom)** — the only routes to the full
A-grade keyed rotation; handled by the sibling `../openwifi/` and `../nexmon/`
adapters, **not** this OpenWRT one.
**Recommendation:** for OpenWRT specifically, target **ath9k** for a
driver-patch proof-of-concept (spatial-mapping + CSI), and **mt76/mt7915** as the
strategic modern platform pending a firmware/vendor-subcmd hook.
---
## Build (host, build-only)
```bash
make core # always works: compiles+links the portable core, no libnl needed
make daemon # builds veil_shieldd IF libnl-genl-3.0 dev headers are present
make clean
```
`make daemon` cleanly **skips** (does not fail) when `libnl-genl-3.0` is absent,
printing the required dev packages. On an OpenWRT buildroot use `openwrt.mk`
(rename to `Makefile` under `package/utils/veil-shieldd/`), which builds against
`libnl-tiny`. See `INTEGRATION.md` for the per-control hook points and exactly
what a driver/firmware patch would need to touch.
---
## Sources
- Linux `nl80211.h` (in-tree, this host): `NL80211_CMD_SET_WIPHY`,
`NL80211_ATTR_WIPHY_ANTENNA_TX` / `_RX`, `NL80211_CMD_VENDOR`,
`NL80211_CMD_FRAME` / `NL80211_CMD_REGISTER_FRAME`.
- ath10k configuration (beamforming only via hostapd `vht_capab`, no debugfs
sounding control): <https://wireless.docs.kernel.org/en/latest/en/users/drivers/ath10k/configuration.html>
- hostapd beamforming/sounding knobs (`[SU-BEAMFORMER]`, `[MU-BEAMFORMER]`,
`[SOUNDING-DIMENSION-4]`, `he_su_beamformer`):
<https://w1.fi/cgit/hostap/tree/hostapd/hostapd.conf> and
<https://github.com/morrownr/USB-WiFi/blob/main/home/AP_Mode/hostapd-WiFi6.conf>
- mt76 beamforming lives in firmware (mt7622/mt7615 performance/beamforming
discussion): <https://github.com/openwrt/mt76/issues/863>
- Qualcomm firmware closedness (ath11k/ath12k no open firmware; regulatory +
features firmware-enforced): ath10k mailing-list thread
<https://ath10k.infradead.narkive.com/6bdEJZih/qca99xx-with-mu-mimo-and-beamforming>
and CodeLinaro ath firmware <https://git.codelinaro.org/clo/ath-firmware/ath11k-firmware>
- ath11k reports VHT beamformee spatial streams *from firmware*:
<https://lkml.iu.edu/2210.2/09619.html>
+60
View File
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
#
# OpenWRT package Makefile STUB for veil_shieldd.
# STATUS: SYNTHETIC / L0 — package skeleton, UNTESTED ON HARDWARE / not in any feed.
#
# Drop this (renamed to `Makefile`) into a package dir such as
# `package/utils/veil-shieldd/` in an OpenWRT buildroot, alongside the copied
# core (veil_shield.{c,h}) and veil_shieldd.c under ./src/. It builds against
# libnl-tiny (the OpenWRT netlink lib) — the same nl80211 API surface, smaller.
#
# This stub does NOT prove the daemon works on a device; it only wires the
# build. No hardware validation is implied.
include $(TOPDIR)/rules.mk
PKG_NAME:=veil-shieldd
PKG_VERSION:=0.0.0-l0
PKG_RELEASE:=1
PKG_LICENSE:=MIT OR Apache-2.0
include $(INCLUDE_DIR)/package.mk
define Package/veil-shieldd
SECTION:=utils
CATEGORY:=Utilities
TITLE:=VEIL compliant-waveform privacy shield (mac80211 adapter, L0)
# libnl-tiny provides nl80211/genl; hostapd for the ctrl_iface cadence path.
DEPENDS:=+libnl-tiny +hostapd-common
URL:=https://github.com/ruvnet/RuView
endef
define Package/veil-shieldd/description
BUILD-ONLY / UNTESTED-ON-HARDWARE userspace adapter that drives the
standards-compliant subset of VEIL controls reachable from OpenWRT
(TX antenna map, hostapd-mediated sounding cadence) and links the portable
keyed-rotation core. The full per-packet keyed rotation is blob-blocked on
commodity Qualcomm/MediaTek parts and requires a driver/firmware patch.
This is NOT a jammer and emits no denial energy.
endef
# Build flags: point at libnl-tiny headers and the copied core.
TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include/libnl-tiny -I$(PKG_BUILD_DIR)/src
TARGET_LDFLAGS += -lnl-tiny -lm
define Build/Compile
$(TARGET_CC) $(TARGET_CFLAGS) -std=c99 -Wall -Wextra \
-o $(PKG_BUILD_DIR)/veil_shieldd \
$(PKG_BUILD_DIR)/src/veil_shieldd.c \
$(PKG_BUILD_DIR)/src/veil_shield.c \
$(TARGET_LDFLAGS)
endef
define Package/veil-shieldd/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/veil_shieldd $(1)/usr/sbin/veil_shieldd
# TODO(hw): ship a procd init script that reads the session key from a
# secure store (never a world-readable config) and passes -i <ifindex>.
endef
$(eval $(call BuildPackage,veil-shieldd))
+294
View File
@@ -0,0 +1,294 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* veil_shieldd OpenWRT / Linux mac80211 userspace adapter for the VEIL
* compliant-waveform privacy shield (ADR-288 / ADR-290).
*
* ============================= HONESTY BANNER ==============================
* STATUS: SYNTHETIC / L0 BUILD-ONLY SCAFFOLD, UNTESTED ON HARDWARE.
*
* This daemon compiles and links the portable veil_shield core, and it issues
* REAL nl80211/libnl calls for the small set of controls that Linux actually
* exposes to userspace (antenna TX mask, station/BSS observation). Everything
* that would edit the per-packet spatial mapping / precoder or the compressed
* beamforming-feedback angles is BLOB-BLOCKED on commodity Qualcomm/MediaTek
* parts and is marked `TODO(hw)` at the exact call site see README.md and
* INTEGRATION.md. Nothing here has been run against a radio. Do not read any
* comment in this file as evidence that VEIL obfuscation reaches the air.
*
* COMPLIANCE: every control below is a standards-compliant configuration or
* observation action. This daemon never transmits energy to deny a channel;
* it only shapes/observes our own compliant frames. It is NOT a jammer.
* ==========================================================================
*
* Build deps (OpenWRT: libnl-tiny; desktop: libnl-3 + libnl-genl-3):
* pkg-config --cflags --libs libnl-genl-3.0
* See Makefile (host build-check) and openwrt.mk (package stub).
*/
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Real libnl / nl80211 headers. On OpenWRT these resolve to libnl-tiny; on a
* desktop to libnl-3. If the toolchain lacks them the host Makefile still
* builds the core object so the rotation math is validated in isolation. */
#include <netlink/netlink.h>
#include <netlink/genl/genl.h>
#include <netlink/genl/ctrl.h>
#include <linux/nl80211.h>
#include "veil_shield.h"
/* ---- Tunables (compliant, conservative defaults) ---------------------- */
#define VEIL_DEFAULT_PASSES 96u /* matches core default (ADR-290) */
#define VEIL_CADENCE_JITTER_MIN_MS 20 /* NDP sounding cadence jitter floor */
#define VEIL_CADENCE_JITTER_MAX_MS 400 /* ... and ceiling (stays in-spec) */
/* ---- Daemon context --------------------------------------------------- */
struct veil_ctx {
struct nl_sock *sock; /* generic-netlink socket to nl80211 */
int family; /* resolved "nl80211" genl family id */
int ifindex;/* target AP interface (e.g. phy0-ap0) */
uint64_t key; /* shared session key for the keyed rotation */
size_t passes; /* Givens passes */
volatile sig_atomic_t running;
};
static struct veil_ctx g_ctx;
static void on_signal(int sig) { (void)sig; g_ctx.running = 0; }
/* ---------------------------------------------------------------------- */
/* nl80211 bring-up — all REAL libnl-genl-3 API names. */
/* ---------------------------------------------------------------------- */
static int veil_nl_connect(struct veil_ctx *c) {
c->sock = nl_socket_alloc();
if (!c->sock) {
fprintf(stderr, "veil: nl_socket_alloc failed\n");
return -ENOMEM;
}
if (genl_connect(c->sock)) {
fprintf(stderr, "veil: genl_connect failed\n");
return -EIO;
}
c->family = genl_ctrl_resolve(c->sock, "nl80211");
if (c->family < 0) {
fprintf(stderr, "veil: genl_ctrl_resolve(nl80211) failed: %d\n",
c->family);
return c->family;
}
/* Observe MLME events (auth/assoc, and — where the driver forwards them —
* action-frame notifications). Real multicast group name is "mlme". */
int grp = genl_ctrl_resolve_grp(c->sock, "nl80211", "mlme");
if (grp >= 0) {
(void)nl_socket_add_membership(c->sock, grp);
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* CONTROL 1 (FEASIBLE): TX antenna-map perturbation. */
/* Rotating the allowed TX antenna bitmap changes the static spatial */
/* mapping the PHY uses, coarsely perturbing the CSI a sensor observes. */
/* This is a genuinely userspace-reachable, compliant knob. */
/* NL80211_CMD_SET_WIPHY + NL80211_ATTR_WIPHY_ANTENNA_TX / _RX */
/* NOTE: many drivers only accept this while the phy is DOWN, and only on */
/* symmetric masks — validate per driver. Coarse, not the keyed rotation. */
/* ---------------------------------------------------------------------- */
static int veil_set_tx_antenna_mask(struct veil_ctx *c,
uint32_t tx_mask, uint32_t rx_mask) {
struct nl_msg *msg = nlmsg_alloc();
if (!msg) return -ENOMEM;
genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, c->family, 0, 0,
NL80211_CMD_SET_WIPHY, 0);
/* wiphy is addressed via the interface index on most drivers. */
NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, (uint32_t)c->ifindex);
NLA_PUT_U32(msg, NL80211_ATTR_WIPHY_ANTENNA_TX, tx_mask);
NLA_PUT_U32(msg, NL80211_ATTR_WIPHY_ANTENNA_RX, rx_mask);
int ret = nl_send_auto(c->sock, msg);
nlmsg_free(msg);
if (ret < 0) return ret;
return nl_recvmsgs_default(c->sock); /* consume ACK/ERR */
nla_put_failure:
nlmsg_free(msg);
return -EMSGSIZE;
}
/* ---------------------------------------------------------------------- */
/* CONTROL 2 (FEASIBLE, indirect): NDP sounding-cadence randomization. */
/* mac80211/driver decides when to send NDP Announcement + NDP. There is */
/* NO stable nl80211 attribute to set the sounding period directly, so the */
/* compliant lever from userspace is hostapd's advertised sounding */
/* capability and dimensions, toggled/rewritten over the hostapd ctrl */
/* interface (RECONFIGURE / SET). We jitter the *offered* cadence. */
/* */
/* TODO(hw): there is no nl80211 "set sounding interval" command. Confirm */
/* against hostapd ctrl_iface docs; the direct per-NDP timer lives in */
/* driver/firmware. See INTEGRATION.md §2. Cite: */
/* https://w1.fi/cgit/hostap/tree/hostapd/hostapd.conf */
/* ---------------------------------------------------------------------- */
static unsigned veil_next_cadence_ms(struct veil_ctx *c) {
/* Derive jitter deterministically from the session key stream so the
* paired receiver can anticipate the schedule (compliant, not random
* spraying). Reuses the core SplitMix64 for byte-identical behavior. */
static veil_rng r;
static int seeded = 0;
if (!seeded) { veil_rng_seed(&r, c->key ^ 0xCADE11CEULL); seeded = 1; }
unsigned span = VEIL_CADENCE_JITTER_MAX_MS - VEIL_CADENCE_JITTER_MIN_MS;
return VEIL_CADENCE_JITTER_MIN_MS +
(unsigned)(veil_rng_next_f32(&r) * (float)span);
}
static int veil_randomize_sounding_cadence(struct veil_ctx *c) {
unsigned ms = veil_next_cadence_ms(c);
/* TODO(hw): push `ms` into the offered sounding cadence. On OpenWRT the
* realistic path is the hostapd ctrl_iface (UNIX socket at
* /var/run/hostapd/<iface>): rewrite he/vht sounding-dimension or toggle
* beamformer capability and RECONFIGURE. mac80211 has no direct knob.
* This function currently only computes the schedule. */
fprintf(stderr, "veil: [feasible/indirect] next sounding jitter = %u ms "
"(TODO(hw): apply via hostapd ctrl_iface)\n", ms);
return 0;
}
/* ---------------------------------------------------------------------- */
/* CONTROL 3 (MOSTLY BLOB-BLOCKED): MU-MIMO group shuffling. */
/* The MU group definition + steering matrices are computed and applied in */
/* the WiFi MCU firmware on mt76 (mt7915) and all ath1x parts. There is no */
/* generic nl80211 command to reshuffle MU groups. Only a vendor subcmd */
/* (NL80211_CMD_VENDOR) on a driver that chose to expose one could do it. */
/* ---------------------------------------------------------------------- */
static int veil_shuffle_mumimo_groups(struct veil_ctx *c) {
(void)c;
/* TODO(hw): requires NL80211_CMD_VENDOR + a driver-specific
* NL80211_ATTR_VENDOR_ID / _SUBCMD / _DATA that does not exist upstream
* for mt76/ath. Without a driver+firmware patch this is unreachable.
* See INTEGRATION.md §3. Left as an explicit no-op, not a fake success. */
fprintf(stderr, "veil: [blob-blocked] MU-MIMO group shuffle needs a "
"vendor subcmd / firmware patch (TODO(hw))\n");
return -ENOTSUP;
}
/* ---------------------------------------------------------------------- */
/* CONTROL 4 (BLOB-BLOCKED on commodity AP silicon): the keyed rotation. */
/* This is the actual VEIL transform — a keyed Givens rotation on the fine */
/* subspace of the compressed beamforming feedback (the phi/psi angles), */
/* or equivalently a unitary Q on the LTF spatial mapping. On mt76/ath the */
/* feedback report is generated and the precoder applied inside firmware, */
/* so userspace cannot edit it. This function shows WHERE the core plugs */
/* in for the platforms that CAN reach the buffer (openwifi FPGA datapath, */
/* Nexmon Broadcom patch) — it operates on a caller-supplied fine block. */
/* ---------------------------------------------------------------------- */
static int veil_apply_keyed_rotation(struct veil_ctx *c,
float *fine, size_t n) {
if (!fine || n < 2) return -EINVAL;
/* Pure, orthogonal, energy-preserving (the "not jamming" invariant). */
float before = veil_l2_norm(fine, n);
veil_shield_apply(fine, n, c->key, c->passes);
float after = veil_l2_norm(fine, n);
/* TODO(hw): on OpenWRT there is NO userspace/mac80211 hook that hands us
* this buffer before TX. Reaching it requires a driver+firmware patch
* (mt76 MCU / ath) to expose the pre-precoder V/steering matrix, OR use
* the openwifi (FPGA) or Nexmon adapters. See INTEGRATION.md §4.
* We only prove the math is invariant here; nothing goes on air. */
fprintf(stderr, "veil: [blob-blocked path] rotated %zu coeffs, "
"L2 %.6f -> %.6f (delta %.2e; must be ~0)\n",
n, before, after, (double)(after - before));
return 0;
}
/* ---------------------------------------------------------------------- */
/* Event loop: watch for sensing-solicitation cadence. */
/* We register interest in MLME/frame events. On commodity drivers the raw */
/* NDP Announcement is NOT forwarded to userspace, so honest detection of */
/* an *external* sensing solicitation needs monitor-mode capture or a */
/* driver notification that does not exist upstream — marked TODO(hw). */
/* ---------------------------------------------------------------------- */
static int veil_event_cb(struct nl_msg *msg, void *arg) {
struct veil_ctx *c = (struct veil_ctx *)arg;
struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
switch (gnlh->cmd) {
case NL80211_CMD_FRAME:
/* TODO(hw): parse NL80211_ATTR_FRAME; classify VHT/HE compressed
* beamforming action (category 21/30) or NDPA to measure solicitation
* cadence. Requires the driver to forward these frames (registered via
* NL80211_CMD_REGISTER_FRAME / monitor). Not guaranteed upstream. */
(void)veil_randomize_sounding_cadence(c);
break;
case NL80211_CMD_NEW_STATION:
case NL80211_CMD_DEL_STATION:
/* Membership churn changes MU grouping surface. */
(void)veil_shuffle_mumimo_groups(c);
break;
default:
break;
}
return NL_SKIP;
}
static void usage(const char *p) {
fprintf(stderr,
"Usage: %s -i <ifindex> [-k <key_hex>] [-p <passes>]\n"
" BUILD-ONLY / UNTESTED-ON-HARDWARE. See README.md.\n", p);
}
int main(int argc, char **argv) {
memset(&g_ctx, 0, sizeof(g_ctx));
g_ctx.key = 0xA5A5A5A5A5A5A5A5ULL; /* placeholder; real key from keystore */
g_ctx.passes = VEIL_DEFAULT_PASSES;
g_ctx.ifindex = -1;
g_ctx.running = 1;
int opt;
while ((opt = getopt(argc, argv, "i:k:p:h")) != -1) {
switch (opt) {
case 'i': g_ctx.ifindex = atoi(optarg); break;
case 'k': g_ctx.key = strtoull(optarg, NULL, 16); break;
case 'p': g_ctx.passes = (size_t)strtoul(optarg, NULL, 10); break;
case 'h': default: usage(argv[0]); return (opt == 'h') ? 0 : 2;
}
}
if (g_ctx.ifindex < 0) { usage(argv[0]); return 2; }
fprintf(stderr, "veil_shieldd: SYNTHETIC/L0 build-only scaffold — "
"no RF is emitted, nothing is validated on silicon.\n");
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
if (veil_nl_connect(&g_ctx)) return 1;
/* Install the event callback (valid-message path). */
nl_socket_modify_cb(g_ctx.sock, NL_CB_VALID, NL_CB_CUSTOM,
veil_event_cb, &g_ctx);
nl_socket_disable_seq_check(g_ctx.sock); /* required for multicast events */
/* Self-check the one genuinely feasible active control at startup. Comment
* this out on a live AP; it may bounce the radio depending on the driver.
* (void)veil_set_tx_antenna_mask(&g_ctx, 0x3, 0x3); */
(void)veil_set_tx_antenna_mask;
/* Prove the linked core is byte-consistent (no radio involved). */
{
float demo[8] = {1,0,0,0,0,0,0,0};
(void)veil_apply_keyed_rotation(&g_ctx, demo, 8);
veil_shield_recover(demo, 8, g_ctx.key, g_ctx.passes);
fprintf(stderr, "veil: recover round-trip demo[0]=%.6f (expect ~1.0)\n",
(double)demo[0]);
}
while (g_ctx.running) {
int r = nl_recvmsgs_default(g_ctx.sock);
if (r < 0 && r != -NLE_AGAIN) {
fprintf(stderr, "veil: nl_recvmsgs_default: %d\n", r);
break;
}
}
nl_socket_free(g_ctx.sock);
return 0;
}
@@ -0,0 +1,25 @@
{
"name": "wifi-densepose-privshield-harness",
"version": "0.1.0",
"description": "Harness for wifi-densepose-privshield (WiFi Veil privacy shield)",
"author": {
"displayName": "Generated by metaharness",
"url": "https://www.npmjs.com/package/metaharness"
},
"license": "MIT",
"categories": [
"agent-harness",
"metaharness-scaffold",
"Engineering",
"software-engineering"
],
"tags": [
"metaharness",
"agent-harness",
"vertical:coding",
"software-engineering",
"wifi-sensing",
"privacy"
],
"homepage": "https://github.com/ruvnet/agent-harness-generator"
}
@@ -0,0 +1,21 @@
{
"permissions": {
"allow": [
"Bash(npx wifi-densepose-privshield-harness*)",
"mcp__wifi-densepose-privshield-harness__*",
"Bash(npm test*)",
"Bash(npm run*)",
"Bash(cargo test -p wifi-densepose-privshield*)",
"Bash(cargo clippy -p wifi-densepose-privshield*)",
"Bash(git diff*)",
"Bash(git status*)",
"Bash(git log*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Bash(git push*)",
"Bash(rm -rf*)"
]
}
}
@@ -0,0 +1,3 @@
node_modules/
dist/
*.tsbuildinfo
@@ -0,0 +1,32 @@
{
"schema": 1,
"generator": "0.1.0",
"template": "vertical:coding",
"template_version": "0.0.0",
"vars": {
"name": "wifi-densepose-privshield-harness",
"description": "Harness for wifi-densepose-privshield (WiFi Veil privacy shield)",
"host": "claude-code"
},
"hosts": ["claude-code"],
"files": {
".claude/settings.json": "fedb60921a0e3c78848f43edddd75f448819594c680d48ff2033ef8f1588da3f",
".claude-plugin/plugin.json": "8b155a3130d212c88dd8b631d9bd6dd1b4eacb52e5eb282fddbe08576ae23be2",
"bin/cli.js": "1133e7a47accada1c9b2184873776d8ca0d028f9b76dd55f467dfe38bb9ce609",
"CLAUDE.md": "f9ccf20c341ff0296b2e64ce692103572d61e856ae0df8a8bc4c35a7ac8b2ff5",
"package.json": "1ccedf0e62b0ed884431a2a9192a3865b525a2dad72fa6491569b9001e7dd24f",
"README.md": "688e207f95e4f58eeade84f149c38fa8ec048796556a7b1bd75f08ee1945edba",
"src/init.ts": "f05d6905d8681f45f610ff5b6e9d425dfa66183acdfe7857248e50e3583e13b8",
"src/router.ts": "4545b42d1423db21bcfe6ab6bf132b805ba383937d142997cb7256e835c245e4",
"src/flywheel.ts": "aab56d82c4f018ddc83923c877a66acdf9c624214307d0a9c4bf930ddb00599a",
"tsconfig.json": "8b4e730a1aa39162ac574455d7a98e1881f5313ca80ffe503b9652dcf0c76b9d",
"vitest.config.ts": "021b33ec623593effc3d163020479a91a1179329ee4ed1cb25f2dd9388e19820",
"__tests__/smoke.test.ts": "8c5a2acc3a956ea48e60c996c9034224684f6f4110cb3c5885741ff8d18bb82d",
"__tests__/router.test.ts": "97c29fc0ff718692ec97a9cd81f92e65ebde996fe1a3d8d2182e66598a583fad",
"__tests__/flywheel.test.ts": "87b149f7d68b4cf72fe3dcf6c76e4307b280e9f4ab6e6b1f7ee6689340faf5fb",
"__tests__/guidance.test.ts": "66b68615d27671d91b9efcf1eee5f2c7a53b0db7f475cc4ddb17ba8b6ddbff7f",
"LICENSE": "07b1a7c2aa25991872e3594de2ecb64ff6b4c5d3dc2376dd5b9e9f77c4b258e8"
},
"generated_at": "2026-08-09T00:00:00.000Z",
"meta": { "surface": "cli" }
}
@@ -0,0 +1 @@
da48afb45d776c10f1841331facf65aa7ba4802f990a2480b91227fc100d4a47
@@ -0,0 +1,67 @@
# wifi-densepose-privshield-harness
Harness for [`wifi-densepose-privshield`](../../v2/crates/wifi-densepose-privshield)
(WiFi Veil — the compliant-waveform WiFi-sensing privacy shield, ADR-288). This
package is defined by ADR-289.
> Advanced Coding harness · domain: `software-engineering`. Modeled on the
> `wifi-densepose-sar-harness` scaffold (ADR-286), generated with
> [create-agent-harness](https://github.com/ruvnet/agent-harness-generator).
## Behavioral rules
- Use the harness's tools for orchestration; memory and routing are handled by
the kernel.
- Defer destructive operations to the user.
- **Never present WiFi sensing as camera-grade, and never relabel WiFi Veil's
SYNTHETIC/L0 results as MEASURED** — a hardware witness is required first
(CLAUDE.md hardware rule; ADR-282 ladder). The harness is a development aid;
it does not run a radio and cannot emit RF.
- WiFi Veil uses compliant waveform controls only — **never jamming.** Do not add,
suggest, or scaffold interference-based "defenses."
## Commands
- `init` — boot the kernel + host adapter.
- `doctor` — verify the install end-to-end (kernel, host, guidance map).
- `guidance --topic <t> [--query <q>]` — read-only WiFi Veil capability map
(dependency-free; topics: `overview`, `threat`, `countermeasure`,
`compliance`, `optimization`, `experiment`). Source-cited and
evidence-labelled; navigation only, not authority.
- `route <e0> <e1> <e2> <e3>` — cost-optimal model routing via
`@metaharness/router` (needs `npm run build`).
- `flywheel [generations]` — SYNTHETIC self-improvement demo via
`@metaharness/flywheel` (needs `npm run build`).
## Architecture
Uses [@metaharness/kernel](https://www.npmjs.com/package/@metaharness/kernel)
(Rust-compiled WASM with a NAPI-RS native fallback) so the same code runs on
every platform. The `@metaharness/*` packages are imported *dynamically* inside
the commands that need them, so `guidance`/`--help` work with no dependencies
installed.
### Darwin, router, flywheel
- **Darwin Mode** (`@metaharness/darwin`, devDependency) — `npm run evolve` /
`evolve:dry` mutates the harness's own config and keeps only measurable
improvements.
- **Router** (`@metaharness/router`) — `src/router.ts` wires a real cost-optimal
`Router` (`qualityBar: 0.8`) over two model tiers. Its labelled examples are
illustrative seed data (see the file's honesty note), not measured eval-log
observations.
- **Flywheel** (`@metaharness/flywheel`) — `src/flywheel.ts` wires the real
promotion loop (propose → evaluate → gate → promote, Ed25519-signed,
independently replayable) with a SYNTHETIC proposer/evaluator
(`dataSource: 'SYNTHETIC'`, no model call). A LIVE run needs a real Proposer
and Evaluator supplied by the operator — see the file's comments.
## Relationship to the crate
This harness assists development *on* the WiFi Veil crate; it does not replace the
crate's own gates. The authoritative validation for a WiFi Veil change is still:
```bash
cargo test -p wifi-densepose-privshield --no-default-features
cargo clippy -p wifi-densepose-privshield --all-targets -- -D warnings
```
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 wifi-densepose-privshield-harness authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,68 @@
# wifi-densepose-privshield-harness
A metaharness (contributor harness) for
[`wifi-densepose-privshield`](../../v2/crates/wifi-densepose-privshield) — **WiFi Veil**,
the compliant-waveform WiFi-sensing privacy shield (ADR-288). Defined by ADR-289.
> **Advanced Coding** — architect → implement → review → test, plus a
> dependency-free WiFi Veil guidance surface. Modeled on `wifi-densepose-sar-harness`
> (ADR-286). Multi-host scaffold with a kernel that resolves native → wasm → js.
## Install
```bash
npm install -g wifi-densepose-privshield-harness
wifi-densepose-privshield-harness doctor
```
Or run without installing:
```bash
npx wifi-densepose-privshield-harness guidance --topic overview
```
## Commands
| Command | Deps needed | Purpose |
|---|---|---|
| `init` | kernel + host | Boot the kernel + host adapter |
| `doctor` | kernel + host | Verify the install end-to-end |
| `guidance --topic <t>` | **none** | Read-only WiFi Veil capability map (source-cited, evidence-labelled) |
| `route <e0..e3>` | router + `npm run build` | Cost-optimal model routing |
| `flywheel [gens]` | flywheel + `npm run build` | SYNTHETIC self-improvement demo |
`guidance` topics: `overview`, `threat`, `countermeasure`, `compliance`,
`optimization`, `experiment`. It needs no dependencies or build step, so it
works offline and in CI before `npm install`.
## What WiFi Veil is
WiFi Veil shapes a node's **own** beamforming feedback with keyed Givens rotations so
a third-party passive sniffer cannot re-identify people, while a keyed receiver
sees an essentially unchanged link. **Compliant waveform controls only — never
jamming.** Reference results are **SYNTHETIC / evidence level L0** (reproduced by
`cargo test`), never MEASURED until a hardware witness exists. See the crate's
[ADR-288](../../docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md) and
[research bundle](../../docs/research/privacy-shield/).
## Darwin, router, flywheel
- `npm run evolve` / `evolve:dry` — Darwin Mode self-mutation of the harness
config (`@metaharness/darwin`).
- `npm run route -- <e0> <e1> <e2> <e3>` (after `npm run build`) — cost-optimal
model routing (`@metaharness/router`).
- `npm run flywheel:dry` — the SYNTHETIC `@metaharness/flywheel` demo
(propose → evaluate → gate → promote, signed + independently replayable).
See `CLAUDE.md` and the honesty notes atop `src/router.ts` / `src/flywheel.ts`
for what is real wiring vs. illustrative/synthetic data.
## Scope
The harness is a **development aid**. It does not run a WiFi Veil radio, does not
emit RF, and cannot jam. It does not replace the crate's own gates — the
authoritative check for a WiFi Veil change is `cargo test -p wifi-densepose-privshield`.
## License
MIT
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
// Verifies the SYNTHETIC flywheel demo wires end-to-end: a non-empty lift curve
// and a replay bundle that verifies independently. Does NOT assert any real
// self-improvement — the proposer/evaluator are deterministic stand-ins.
import { describe, it, expect } from 'vitest';
import { runVeilFlywheelDemo, verifyVeilFlywheelDemo } from '../src/flywheel.js';
describe('wifi-densepose-privshield-harness — flywheel (SYNTHETIC)', () => {
it('produces a non-empty lift curve', async () => {
const result = await runVeilFlywheelDemo(3);
expect(result.liftCurve.length).toBeGreaterThan(0);
expect(result.generationsRun).toBeGreaterThan(0);
});
it('produces an independently verifiable replay bundle', async () => {
const result = await runVeilFlywheelDemo(3);
const verdict = verifyVeilFlywheelDemo(result);
expect(verdict.pass).toBe(true);
});
it('stamps the run as SYNTHETIC provenance', async () => {
const result = await runVeilFlywheelDemo(2);
expect(result.replayBundle.data_source).toBe('SYNTHETIC');
});
});
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
// The VEIL guidance map is dependency-free (no @metaharness/* import), so this
// test runs even before `npm install` resolves the kernel. It guards the
// read-only capability map the MCP/CLI `guidance` surface exposes.
import { describe, it, expect } from 'vitest';
import { run, guidanceReport } from '../bin/cli.js';
describe('wifi-densepose-privshield-harness — guidance', () => {
it('returns a source-cited report for a known topic', () => {
const r = guidanceReport('optimization');
expect(r.ok).toBe(true);
expect(r.summary.length).toBeGreaterThan(0);
expect(r.sources.some((s: string) => s.includes('optimize.rs'))).toBe(true);
expect(r.authority).toContain('read-only');
});
it('labels evidence as SYNTHETIC/L0', () => {
const r = guidanceReport('experiment');
expect(r.evidence).toContain('SYNTHETIC');
});
it('rejects an unknown topic and lists the valid ones', () => {
const r = guidanceReport('not-a-topic');
expect(r.ok).toBe(false);
expect(r.topics).toContain('overview');
expect(r.topics).toContain('compliance');
});
it('CLI `guidance --topic overview` exits 0; unknown topic exits non-zero', async () => {
expect(await run(['guidance', '--topic', 'overview'])).toBe(0);
expect(await run(['guidance', '--topic', 'nope'])).not.toBe(0);
});
});
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
// Verifies the cost-optimal router mechanism (not its illustrative data): cheap
// query shapes route to the cheap tier; hard shapes escalate to the frontier.
import { describe, it, expect } from 'vitest';
import { routeVeilQuery } from '../src/router.js';
describe('wifi-densepose-privshield-harness — router', () => {
it('routes a threat-model query (cheap-tier-capable) to the cheap tier', () => {
const pick = routeVeilQuery([1, 0, 0, 0]);
expect(pick.id).toBe('cheap-tier');
expect(pick.metBar).toBe(true);
});
it('escalates a compliance-review query to the frontier tier', () => {
const pick = routeVeilQuery([0, 1, 0, 0]);
expect(pick.id).toBe('frontier-tier');
});
it('escalates an optimizer-tuning query to the frontier tier', () => {
const pick = routeVeilQuery([0, 0, 1, 0]);
expect(pick.id).toBe('frontier-tier');
});
});
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
// A real smoke test for wifi-densepose-privshield-harness: it boots the actual
// kernel + host adapter the harness depends on, so `npm test` fails loudly if
// @metaharness/kernel or @metaharness/host-claude-code is missing, broken, or
// version-skewed. Fastest signal that `npm install` produced a runnable harness.
import { describe, it, expect } from 'vitest';
import { loadKernel } from '@metaharness/kernel';
import adapter from '@metaharness/host-claude-code';
import { run } from '../bin/cli.js';
describe('wifi-densepose-privshield-harness — install smoke test', () => {
it('loads the kernel and reports a version + a known backend', async () => {
const kernel = await loadKernel();
const info = kernel.kernelInfo();
expect(typeof info.version).toBe('string');
expect(info.version.length).toBeGreaterThan(0);
expect(['native', 'wasm', 'js']).toContain(kernel.backend);
});
it('resolves the host adapter with a name', () => {
expect(typeof adapter.name).toBe('string');
expect(adapter.name.length).toBeGreaterThan(0);
});
it('the CLI doctor command succeeds (exit 0)', async () => {
const code = await run(['doctor']);
expect(code).toBe(0);
});
it('an unknown CLI command exits non-zero', async () => {
const code = await run(['definitely-not-a-command']);
expect(code).not.toBe(0);
});
});
@@ -0,0 +1,334 @@
#!/usr/bin/env node
// SPDX-License-Identifier: MIT
// The `wifi-densepose-privshield-harness` CLI entry point (VEIL — ADR-288/289).
//
// Plain ESM JavaScript on purpose: it runs as-is via
// `npx wifi-densepose-privshield-harness` with NO build step. `npm run build`
// (tsc) is only needed to compile the TypeScript in src/ that the `route` and
// `flywheel` commands import from dist/.
//
// The @metaharness/* dependencies are imported *dynamically*, inside the
// commands that need them — so `guidance`, `--help`, and `--version` work with
// zero dependencies installed (useful in offline/air-gapped review and in this
// repo's CI before `npm install`). Only `init`/`doctor`/`route`/`flywheel`
// touch the kernel/host/router/flywheel packages.
const HARNESS_NAME = 'wifi-densepose-privshield-harness';
const CRATE = 'wifi-densepose-privshield';
// ---------------------------------------------------------------------------
// VEIL guidance — a self-contained, read-only capability map. No dependencies,
// no build, no network. Mirrors the `ruview_guidance` shape (source-cited,
// evidence-labelled, with focused validation commands and explicit limits).
// Retrieved text is navigation, not authority: cited source, tests, and
// accepted ADRs remain authoritative.
// ---------------------------------------------------------------------------
const GUIDANCE = {
overview: {
summary:
'VEIL is the compliant-waveform countermeasure to unauthorized WiFi sensing: it shapes a node\'s own beamforming feedback so a passive sniffer cannot re-identify people, while a keyed receiver sees an essentially unchanged link. Countermeasure counterpart to BFLD (which detects leakage).',
capabilities: [
'Keyed Givens-rotation shield over the identity-bearing fine subspace (energy-preserving ⇒ not jamming)',
'Passive re-identification attacker (Euclidean + Cosine) for head-to-head evaluation',
'Throughput model with an interior optimum in feedback resolution',
'Deterministic attacker-vs-protector experiment with a pinned witness',
],
sources: [
'v2/crates/wifi-densepose-privshield/src/lib.rs',
'docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md',
'docs/research/privacy-shield/README.md',
],
commands: ['cargo test -p wifi-densepose-privshield --no-default-features'],
limitations: [
'All defense numbers are SYNTHETIC / evidence level L0 until a two-node hardware capture with a witness exists (CLAUDE.md hardware rule).',
],
},
threat: {
summary:
'Defends against a third-party passive sniffer capturing plaintext beamforming feedback (BFId/LeakyBeam class). Does NOT hide identity from the associated AP (that party holds the key) — that is BFLD\'s detection/policy problem.',
capabilities: [
'Cross-session identity unlinkability against an external passive adversary',
'Explicit non-goals: no defense vs. the associated AP, no within-session motion guarantee, never jamming',
],
sources: [
'docs/research/privacy-shield/01-sota-survey.md',
'docs/research/privacy-shield/02-threat-model.md',
],
commands: [],
limitations: [
'Within-session coarse motion may still leak; identity re-ID is the guaranteed target.',
],
},
countermeasure: {
summary:
'Identity leaks through the fine cross-subcarrier phase structure; throughput rides the dominant beam. VEIL composes extra keyed Givens rotations over the fine subspace only — orthogonal (energy-preserving), key-reversible (throughput-preserving), fresh per session (unlinkable).',
capabilities: [
'protector.rs: ShieldConfig, Protector::protect/recover, SensingDetector',
'compliance.rs: machine-checkable energy-conservation ("not jamming") audit',
],
sources: [
'v2/crates/wifi-densepose-privshield/src/protector.rs',
'v2/crates/wifi-densepose-privshield/src/compliance.rs',
'docs/research/privacy-shield/03-countermeasure-design.md',
],
commands: ['cargo test -p wifi-densepose-privshield protector'],
limitations: [
'The two-subspace separability is a model abstraction; real hardware is only approximately separable.',
],
},
compliance: {
summary:
'Compliant waveform controls only, never jamming. The keyed rotation is orthogonal, so it preserves the report energy exactly (ratio ≈ 1.0) — it adds no interfering emission. Jamming (47 U.S.C. §333/§302a) is defined by interfering with OTHERS\' transmissions, not shaping your own.',
capabilities: [
'ComplianceReport::audit / is_compliant — energy ratio + non-interference verdict',
],
sources: [
'v2/crates/wifi-densepose-privshield/src/compliance.rs',
'docs/research/privacy-shield/04-compliance-and-regulatory.md',
],
commands: ['cargo test -p wifi-densepose-privshield compliance'],
limitations: [
'Engineering analysis, not legal advice; RF power/mask/timing limits are jurisdiction-specific.',
],
},
optimization: {
summary:
'The shipped shield config is derived, not hand-picked: 96 Givens passes (2× the proven-minimum 48 for robust collapse across both attacker metrics and N∈{16,32}; extra passes are throughput-free since the rotation is keyed, not signaled) at 5-bit feedback (throughput-best in the 802.11 {5,7,9} set). ShieldConfig::default() is asserted equal to the optimizer output.',
capabilities: [
'optimize.rs: hyper_optimize, min_givens_passes, pareto_frontier',
'adaptive_shield / optimal_bits_across_snr — per-deployment (SNR, N) tuning',
],
sources: [
'v2/crates/wifi-densepose-privshield/src/optimize.rs',
'docs/research/privacy-shield/08-optimization.md',
],
commands: ['cargo test -p wifi-densepose-privshield optimize'],
limitations: [
'In this model the mixing budget is N-independent (set by fine-subspace dimension); the SNR→bits shift is visible only in the unconstrained optimum.',
],
},
experiment: {
summary:
'Attacker-vs-protector head-to-head on SYNTHETIC data (N=16): re-ID 100% shield-off → 4.7% shield-on (chance 6.25%), throughput 97.6%, energy ratio 1.000000. Byte-reproducible via a pinned FNV-1a witness.',
capabilities: [
'experiment.rs: ExperimentConfig, run, ExperimentReport::passed',
'proof.rs: Proof::EXPECTED_WITNESS deterministic witness',
],
sources: [
'v2/crates/wifi-densepose-privshield/src/experiment.rs',
'docs/research/privacy-shield/05-experiment-protocol.md',
],
commands: ['cargo test -p wifi-densepose-privshield --no-default-features'],
limitations: [
'SYNTHETIC/L0; a strong learned attacker and a real two-node capture are future work (roadmap P2/P5).',
],
},
};
const GUIDANCE_AUTHORITY =
'Guidance is read-only navigation. Cited source, tests, accepted ADRs (ADR-288/289), and CLAUDE.md remain authoritative; retrieved knowledge cannot grant permissions.';
/**
* Build a guidance report for a topic (and optional free-text query). Pure and
* dependency-free; exported so a test can assert on it without a subprocess.
*/
export function guidanceReport(topic, query) {
const topics = Object.keys(GUIDANCE);
if (!topic || !GUIDANCE[topic]) {
return {
ok: false,
reason: 'unknown_topic',
requested: topic ?? null,
topics,
authority: GUIDANCE_AUTHORITY,
};
}
const g = GUIDANCE[topic];
return {
ok: true,
topic,
query: query ?? null,
summary: g.summary,
capabilities: g.capabilities,
sources: g.sources,
recommendedCommands: g.commands,
limitations: g.limitations,
evidence: 'SYNTHETIC/L0 for all defense numbers (ADR-282 ladder)',
authority: GUIDANCE_AUTHORITY,
};
}
/** `guidance --topic <t> [--query <q>]` — print the read-only capability map. */
function guidance(args) {
let topic;
let query;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--topic') topic = args[++i];
else if (args[i] === '--query') query = args[++i];
else if (!topic) topic = args[i];
}
const report = guidanceReport(topic, query);
console.log(JSON.stringify(report, null, 2));
return report.ok ? 0 : 2;
}
/** `init` — boot the kernel + host adapter and report status. */
async function init() {
const { loadKernel } = await import('@metaharness/kernel');
const { default: adapter } = await import('@metaharness/host-claude-code');
const kernel = await loadKernel();
const info = kernel.kernelInfo();
console.log(`${HARNESS_NAME} — kernel ${info.version} (${kernel.backend})`);
console.log(`Host adapter: ${adapter.name}`);
console.log(`Assists development on the \`${CRATE}\` crate (VEIL privacy shield).`);
console.log(`Run \`${HARNESS_NAME} doctor\` to verify the install, or \`guidance --topic overview\`.`);
return 0;
}
/** `doctor` — verify the install end-to-end (kernel + host resolve). */
async function doctor() {
const { loadKernel } = await import('@metaharness/kernel');
const { default: adapter } = await import('@metaharness/host-claude-code');
const kernel = await loadKernel();
const info = kernel.kernelInfo();
const checks = [
['kernel loads', !!kernel],
['kernel reports a version', typeof info.version === 'string' && info.version.length > 0],
['kernel backend is native|wasm|js', ['native', 'wasm', 'js'].includes(kernel.backend)],
['host adapter has a name', typeof adapter?.name === 'string' && adapter.name.length > 0],
['guidance map resolves', guidanceReport('overview').ok === true],
];
let ok = true;
for (const [label, pass] of checks) {
console.log(`${pass ? 'PASS' : 'FAIL'} ${label}`);
if (!pass) ok = false;
}
console.log(
ok
? `\n${HARNESS_NAME}: all checks passed (kernel ${info.version}, ${kernel.backend} backend, host ${adapter.name})`
: `\n${HARNESS_NAME}: doctor found problems`,
);
return ok ? 0 : 1;
}
/**
* `route <e0> <e1> <e2> <e3>` route a 4-axis task embedding to the
* cost-optimal model tier via @metaharness/router. Needs `npm run build`.
*/
async function route(args) {
const embedding = args.map(Number);
if (embedding.length !== 4 || embedding.some((n) => Number.isNaN(n))) {
console.error(
`Usage: ${HARNESS_NAME} route <threatModeling> <complianceReview> <optimizerTuning> <docWriting> (four 0..1 numbers)`,
);
return 2;
}
let routeVeilQuery;
try {
({ routeVeilQuery } = await import('../dist/router.js'));
} catch (err) {
console.error(`route: dist/router.js not found — run \`npm run build\` first. (${err.message})`);
return 1;
}
const pick = routeVeilQuery(embedding);
console.log(
`route -> ${pick.id} (predicted quality ${pick.predictedQuality.toFixed(3)}, $${pick.costPerMTok}/MTok, met bar: ${pick.metBar})`,
);
return 0;
}
/**
* `flywheel [generations]` run the SYNTHETIC @metaharness/flywheel demo and
* print the lift curve + an independent replay-bundle verification. Needs
* `npm run build`.
*/
async function flywheel(args) {
const generations = args[0] ? Number(args[0]) : 3;
if (Number.isNaN(generations) || generations < 1) {
console.error(`Usage: ${HARNESS_NAME} flywheel [generations>=1]`);
return 2;
}
let runVeilFlywheelDemo, verifyVeilFlywheelDemo;
try {
({ runVeilFlywheelDemo, verifyVeilFlywheelDemo } = await import('../dist/flywheel.js'));
} catch (err) {
console.error(`flywheel: dist/flywheel.js not found — run \`npm run build\` first. (${err.message})`);
return 1;
}
console.log(`Running ${generations}-generation flywheel demo (dataSource: SYNTHETIC — see src/flywheel.ts)...`);
const result = await runVeilFlywheelDemo(generations);
for (const point of result.liftCurve) {
console.log(` gen ${point.generation}: primary=${point.primary.toFixed(3)} delta=${point.delta.toFixed(3)} anchor=${point.anchor ?? 'n/a'}`);
}
const verdict = verifyVeilFlywheelDemo(result);
console.log(`generations run: ${result.generationsRun} · promotions: ${result.promotions.length} · replay verified: ${verdict.pass}`);
return verdict.pass ? 0 : 1;
}
/**
* Dispatch one CLI invocation. Exported (not just run on import) so a test can
* drive it without spawning a subprocess. Returns the intended exit code.
*/
export async function run(argv) {
const cmd = argv[0] ?? 'init';
switch (cmd) {
case 'init':
return init();
case 'doctor':
return doctor();
case 'guidance':
return guidance(argv.slice(1));
case 'route':
return route(argv.slice(1));
case 'flywheel':
return flywheel(argv.slice(1));
case '--version':
case '-v': {
const { loadKernel } = await import('@metaharness/kernel');
const kernel = await loadKernel();
console.log(kernel.version());
return 0;
}
case '--help':
case '-h':
console.log(
`Usage: ${HARNESS_NAME} <command>\n\n` +
` init boot the kernel + host adapter (default)\n` +
` doctor verify the install end-to-end\n` +
` guidance --topic <t> read-only VEIL capability map (no deps/build)\n` +
` topics: overview threat countermeasure compliance optimization experiment\n` +
` route <e0..e3> cost-optimal model routing (needs \`npm run build\`)\n` +
` flywheel [generations] SYNTHETIC self-improvement demo (needs \`npm run build\`)\n` +
` --version print the kernel version`,
);
return 0;
default:
console.error(`Unknown command: ${cmd}. Try \`${HARNESS_NAME} --help\`.`);
return 2;
}
}
// CLI guard: execute only when invoked directly (not when imported by a test).
// npm's bin shims pass a NON-normalized argv[1], so realpath BOTH sides before
// comparing — a naive string === misses the npx/shim path and the CLI no-ops.
import { fileURLToPath } from 'node:url';
import { realpathSync } from 'node:fs';
import { argv } from 'node:process';
const invokedDirectly = (() => {
if (!argv[1]) return false;
try {
const a = realpathSync(argv[1]);
const b = realpathSync(fileURLToPath(import.meta.url));
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
} catch {
return false;
}
})();
if (invokedDirectly) {
run(argv.slice(2))
.then((code) => process.exit(code))
.catch((err) => {
console.error(err);
process.exit(1);
});
}
@@ -0,0 +1,50 @@
{
"name": "wifi-densepose-privshield-harness",
"version": "0.1.0",
"description": "Harness for wifi-densepose-privshield (WiFi Veil — compliant-waveform WiFi-sensing privacy shield, ADR-288/289)",
"license": "MIT",
"type": "module",
"bin": {
"wifi-densepose-privshield-harness": "bin/cli.js"
},
"files": [
"bin/**",
"dist/**",
"src/**",
"tsconfig.json",
".claude/**",
".claude-plugin/**",
"CLAUDE.md",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc",
"test": "vitest run",
"init": "node ./bin/cli.js init",
"doctor": "node ./bin/cli.js doctor",
"guidance": "node ./bin/cli.js guidance",
"evolve": "metaharness-darwin evolve . --sandbox real --generations 3 --children 4",
"evolve:dry": "metaharness-darwin evolve . --sandbox mock --generations 2 --children 3",
"route": "npm run build && node ./bin/cli.js route",
"flywheel:dry": "npm run build && node ./bin/cli.js flywheel 3"
},
"dependencies": {
"@metaharness/kernel": "^0.1.0",
"@metaharness/host-claude-code": "^0.1.0",
"@metaharness/router": "^0.3.2",
"@metaharness/flywheel": "^0.1.7"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.4.0",
"vitest": "^3.0.0",
"@metaharness/darwin": "^0.2.2"
},
"engines": {
"node": ">=20.0.0"
},
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT
//
// The wifi-densepose-privshield (VEIL) harness's self-improvement loop, via
// @metaharness/flywheel: run -> measure -> mutate -> verify -> promote, with a
// frozen, conjunctive promotion gate and a signed, replayable lineage.
//
// HONESTY NOTE (load-bearing): `runVeilFlywheelDemo()` wires the real
// @metaharness/flywheel API end-to-end, but its Proposer and Evaluator are
// SYNTHETIC stand-ins — a deterministic string mutation and a deterministic
// scoring function over that string, with NO model call and NO real benchmark.
// It proves the wiring works (see __tests__/flywheel.test.ts: a non-empty lift
// curve, a verifiable replay bundle) and gives a `dataSource: 'SYNTHETIC'`-
// stamped demo. A LIVE run needs the operator to supply:
// - a real Proposer: a model call that improves one policy lever (e.g. the
// compliance-review checklist, the threat-model triage prompt);
// - a real Evaluator: scores that policy against real tasks (e.g. "did the
// compliance reviewer catch a non-energy-preserving perturbation").
// Neither exists in this repo — wiring them is a live-API-key decision for the
// harness operator, not something to fake here.
import {
runFlywheelGenerations,
meetsPromotionRule,
makeSigner,
verifyReplayBundle,
type Policy,
type PolicyGenome,
type Proposer,
type Evaluator,
type Suite,
type FlywheelResult,
} from '@metaharness/flywheel';
/** The gen-0 operating policy for the VEIL harness's review agents. Opaque
* string levers the flywheel never interprets their meaning, only the
* Evaluator does. */
export const VEIL_ROOT_POLICY: Policy = {
complianceReview: 'energy-ratio-checklist',
threatTriage: 'single-pass',
};
/** SYNTHETIC proposer: deterministically varies the target lever's value
* rather than calling a model. */
const syntheticProposer: Proposer = async (base: PolicyGenome, target: string) => {
const current = base.policy[target] ?? '';
return `${current}+g${base.generation + 1}`;
};
/** SYNTHETIC evaluator: scores a policy purely as a function of its own string
* content a deterministic stand-in for running the harness's agents against a
* real task suite. `noopRate` must move for anything to promote (the default
* gate requires it to strictly improve generation over generation). */
const syntheticEvaluator: Evaluator = async (policy: Policy, _suite: Suite) => {
const totalLength = Object.values(policy).reduce((s, v) => s + v.length, 0);
const primary = Math.min(0.5 + totalLength / 200, 0.98);
const noopRate = Math.max(0.3 - totalLength / 300, 0.02);
return {
primary,
noopRate,
costPerWin: 1 / primary,
regressed: false,
};
};
const VEIL_HOLDOUT: Suite = {
id: 'veil-harness-holdout-synthetic',
items: ['seeded-compliance-task-1', 'seeded-threat-task-2', 'seeded-optimizer-task-3'],
};
const VEIL_ANCHOR: Suite = {
id: 'veil-harness-anchor-synthetic',
items: ['frozen-not-jamming-regression-1'],
};
/**
* Run a small, fully SYNTHETIC flywheel demo end-to-end and return the real
* @metaharness/flywheel result a genuine lift curve and a signed,
* independently replayable bundle, built from synthetic (not live) evidence.
*/
export async function runVeilFlywheelDemo(maxGenerations = 3): Promise<FlywheelResult> {
return runFlywheelGenerations({
rootPolicy: VEIL_ROOT_POLICY,
proposer: syntheticProposer,
evaluator: syntheticEvaluator,
promotionRule: meetsPromotionRule,
holdout: VEIL_HOLDOUT,
anchor: VEIL_ANCHOR,
maxGenerations,
signer: makeSigner(),
dataSource: 'SYNTHETIC',
});
}
/** Independently verify a flywheel demo's replay bundle (no trust in the producer). */
export function verifyVeilFlywheelDemo(result: FlywheelResult) {
return verifyReplayBundle(result.replayBundle);
}
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
// The harness's `wifi-densepose-privshield-harness init` entry (typed mirror of
// the JS command in bin/cli.js; the published CLI uses the JS version so no
// build is required for `init`).
import { loadKernel } from '@metaharness/kernel';
import adapter from '@metaharness/host-claude-code';
const HARNESS_NAME = 'wifi-densepose-privshield-harness';
async function main(): Promise<number> {
const kernel = await loadKernel();
const info = kernel.kernelInfo();
console.log(`${HARNESS_NAME} — kernel ${info.version} (${kernel.backend})`);
console.log(`Host adapter: ${adapter.name}`);
console.log(`Run \`${HARNESS_NAME} doctor\` to verify the install.`);
return 0;
}
main()
.then((c) => process.exit(c))
.catch((err) => {
console.error(err);
process.exit(1);
});
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
//
// Cost-optimal task routing for the wifi-densepose-privshield (VEIL) harness,
// via @metaharness/router: route each agent query to the cheapest model
// predicted to clear a quality bar, instead of defaulting every query to the
// frontier tier.
//
// HONESTY NOTE: the candidate `examples` below are SEED/ILLUSTRATIVE data —
// four hand-picked (embedding, quality) points per candidate, not measured
// eval-log observations. They exist so `veilTaskRouter` is a real, runnable
// k-NN router out of the box (see __tests__/router.test.ts), not so its routing
// decisions should be trusted for production cost savings. Replace
// `VEIL_ROUTER_CANDIDATES[*].examples` with real (query embedding → quality
// achieved) rows from your own eval logs before relying on this.
import { Router, type RouterCandidate } from '@metaharness/router';
/**
* A 4-axis feature embedding for a harness query (each axis 0..1):
* [0] threatModeling "is this attack in scope / what does VEIL defend"-shaped
* [1] complianceReview "does this stay compliant / not jamming"-shaped
* [2] optimizerTuning "tune passes/bits / re-run the optimizer"-shaped
* [3] docWriting "write/update the research bundle or ADR"-shaped
* A caller with a real embedding model should project onto that model's
* dimensionality instead the router only needs consistent vectors.
*/
export type VeilTaskEmbedding = readonly [number, number, number, number];
export const VEIL_ROUTER_CANDIDATES: RouterCandidate[] = [
{
id: 'cheap-tier',
costPerMTok: 1,
examples: [
{ embedding: [1, 0, 0, 0], quality: 0.88 }, // threat-model Q&A: cheap tier is fine
{ embedding: [0, 0, 0, 1], quality: 0.85 }, // doc writing: cheap tier is fine
{ embedding: [0, 1, 0, 0], quality: 0.55 }, // compliance review: cheap tier is weak
{ embedding: [0, 0, 1, 0], quality: 0.5 }, // optimizer tuning: cheap tier is weak
],
},
{
id: 'frontier-tier',
costPerMTok: 15,
examples: [
{ embedding: [1, 0, 0, 0], quality: 0.95 },
{ embedding: [0, 0, 0, 1], quality: 0.93 },
{ embedding: [0, 1, 0, 0], quality: 0.93 }, // compliance review: frontier tier needed
{ embedding: [0, 0, 1, 0], quality: 0.92 }, // optimizer tuning: frontier tier needed
],
},
];
/**
* Cost-optimal router for the harness's four query shapes above. `qualityBar`
* of 0.8: return the cheapest candidate predicted to clear 80% quality, or the
* best-predicted candidate if none do. k=1 because each candidate has only 4
* orthogonal one-hot examples (see the SAR harness note on why the default k=5
* would collapse every query to the same prediction here).
*/
export const veilTaskRouter = new Router({
qualityBar: 0.8,
candidates: VEIL_ROUTER_CANDIDATES,
k: 1,
});
/** Route one query embedding to the cost-optimal model tier. */
export function routeVeilQuery(queryEmbedding: VeilTaskEmbedding) {
return veilTaskRouter.route([...queryEmbedding]);
}
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "__tests__"]
}
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
// Strips the `#!/usr/bin/env node` shebang from importable entrypoints (e.g.
// bin/cli.js) before Vite parses them — Vite/esbuild (used internally by
// Vitest) does NOT strip shebangs, so importing a shebanged module throws
// `SyntaxError: Invalid or unexpected token`. No effect on direct CLI
// execution.
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
{
name: 'strip-shebang',
enforce: 'pre',
transform(code: string) {
if (code.startsWith('#!')) {
return { code: code.replace(/^#![^\n]*/, ''), map: null };
}
return null;
},
},
],
});
+9 -9
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
#
# csi-data-policy-check.sh — ADR-296 CSI data-incident repository guard.
# csi-data-policy-check.sh — ADR-299 CSI data-incident repository guard.
#
# WHY (ADR-296): raw CSI recordings are person data (they encode breathing,
# WHY (ADR-299): raw CSI recordings are person data (they encode breathing,
# movement, and presence) and CLAUDE.md prohibits committing CSI or person
# data. A stale `.gitignore` rule let ~64.6 MB of raw captures reach the tree
# under `data/recordings/` and `v2/data/recordings/`. This check is the
@@ -30,7 +30,7 @@
# ------------------------------------------------------------------------------
# ALLOWLIST (synthetic test fixtures)
# ------------------------------------------------------------------------------
# Tests may use only synthetic or expressly-consented minimal fixtures (ADR-296).
# Tests may use only synthetic or expressly-consented minimal fixtures (ADR-299).
# A file whose path matches an allow pattern is exempt. Patterns come from:
# * the file `scripts/csi-data-policy.allow` (one glob per line, `#` comments), and
# * the env var `CSI_POLICY_ALLOW` (colon-separated globs).
@@ -38,11 +38,11 @@
# scripts/tests/fixtures/csi-policy/*.csi.jsonl
#
# ------------------------------------------------------------------------------
# BASELINE (acknowledged pre-existing incident, ADR-296)
# BASELINE (acknowledged pre-existing incident, ADR-299)
# ------------------------------------------------------------------------------
# The tree today ALREADY contains the incident recordings under
# `data/recordings/` and `v2/data/recordings/`. Removing them is destructive and
# gated on data-owner sign-off (ADR-296 "Decision"), so this guard is EXPECTED to
# gated on data-owner sign-off (ADR-299 "Decision"), so this guard is EXPECTED to
# fail on the current tree — that failure documents the incident.
#
# Once the owner removes those files, or to acknowledge them in the interim
@@ -160,7 +160,7 @@ scan_stdin() {
continue # synthetic fixture, expressly allowed
fi
if matches_any "$f" BASELINE_PATTERNS; then
echo "ack: $f$reason (acknowledged baseline, ADR-296)" >&2
echo "ack: $f$reason (acknowledged baseline, ADR-299)" >&2
acknowledged=$((acknowledged + 1))
continue
fi
@@ -169,14 +169,14 @@ scan_stdin() {
done
if [[ "$acknowledged" -gt 0 ]]; then
echo "note: $acknowledged file(s) acknowledged via CSI_POLICY_BASELINE (ADR-296)." >&2
echo "note: $acknowledged file(s) acknowledged via CSI_POLICY_BASELINE (ADR-299)." >&2
fi
if [[ "$violations" -gt 0 ]]; then
echo "" >&2
echo "FAIL: $violations CSI/person-data policy violation(s) (ADR-296)." >&2
echo "FAIL: $violations CSI/person-data policy violation(s) (ADR-299)." >&2
echo " Raw CSI is person data and must not be tracked in git. See" >&2
echo " docs/adr/ADR-296-csi-data-incident-repo-controls.md." >&2
echo " docs/adr/ADR-299-csi-data-incident-repo-controls.md." >&2
echo " Synthetic test fixtures can be allowlisted in $ALLOW_FILE." >&2
return 1
fi

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