mirror of
https://github.com/ruvnet/RuView.git
synced 2026-09-02 05:25:52 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d2ad6aa8e | ||
|
|
c929bbc8b3 | ||
|
|
d36f346bba | ||
|
|
2c249ec8cb | ||
|
|
7927839f4f | ||
|
|
a76adc3c2f | ||
|
|
aae2ed5345 | ||
|
|
3161af52da | ||
|
|
501f138360 | ||
|
|
4685618388 | ||
|
|
1d50518a70 | ||
|
|
0370d49e4a | ||
|
|
de27336fa1 | ||
|
|
73e82313ac | ||
|
|
bf17fc0407 | ||
|
|
ba978041ae | ||
|
|
90c6ecc530 | ||
|
|
5aa204a168 | ||
|
|
e46fcc6862 | ||
|
|
49c594822f | ||
|
|
516331461a | ||
|
|
6506438b83 | ||
|
|
34c9804002 | ||
|
|
8bb55aac05 | ||
|
|
559ad56aa4 | ||
|
|
ca1f0b9e8a | ||
|
|
2cafa1fdcc | ||
|
|
79d1fff99a | ||
|
|
01c42d0900 | ||
|
|
de88e37de5 | ||
|
|
e737b1a7bc | ||
|
|
50bcf0e215 | ||
|
|
e2ffecde9a | ||
|
|
17ba9df19a | ||
|
|
5114ed183f | ||
|
|
1c2b383075 | ||
|
|
b827dc40b1 | ||
|
|
192ed2a236 | ||
|
|
c63b26034b | ||
|
|
0cb348da72 | ||
|
|
aea8c8c66a | ||
|
|
cb67be117a | ||
|
|
80b1715cb8 | ||
|
|
18060b9c77 | ||
|
|
006a66ca20 | ||
|
|
16b2a629d1 |
@@ -1 +1 @@
|
||||
{"sessionId":"d80c93c2-51b7-42e8-a0fc-dc47cff1200f","pid":45748,"acquiredAt":1779668018388}
|
||||
{"sessionId":"905385c4-b13f-5091-96df-5752fb109cf5","pid":509,"procStart":"527","acquiredAt":1786922977672}
|
||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# The contributor harness hashes provenance inputs byte-for-byte. Keep text
|
||||
# files in this boundary on LF even when Windows enables core.autocrlf.
|
||||
harness/ruview/** text=auto eol=lf
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -204,7 +204,7 @@ jobs:
|
||||
node-version: '22'
|
||||
|
||||
- name: Run UI unit tests
|
||||
run: node --test ui/sw.test.mjs ui/services/ws-ticket.test.mjs ui/services/websocket.service.test.mjs
|
||||
run: node --test ui/sw.test.mjs ui/services/ws-ticket.test.mjs ui/services/websocket.service.test.mjs v2/crates/wifi-densepose-desktop/ui/build-config.test.mjs
|
||||
|
||||
# Unit and Integration Tests
|
||||
# Python pytest matrix — runs against the archived v1 Python tree.
|
||||
|
||||
53
.github/workflows/csi-data-policy.yml
vendored
Normal file
53
.github/workflows/csi-data-policy.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: CSI data policy (ADR-299)
|
||||
|
||||
# ADR-299 repository CSI data-incident guard. Fails when CSI-format files
|
||||
# (*.csi.jsonl / *.csi.meta.json) or oversized JSONL captures are tracked in
|
||||
# git. Raw CSI is person data and must never be committed (CLAUDE.md, ADR-299).
|
||||
#
|
||||
# NOTE: the tree currently still contains the pre-existing incident recordings
|
||||
# under data/recordings/ and v2/data/recordings/, whose removal is gated on
|
||||
# data-owner sign-off (ADR-299). Until they are removed this job is EXPECTED to
|
||||
# fail, and that failure documents the incident. To make it green in a
|
||||
# follow-up without weakening the guard for NEW files, set CSI_POLICY_BASELINE
|
||||
# to a file listing the acknowledged paths (see the script header).
|
||||
#
|
||||
# Checker: scripts/csi-data-policy-check.sh Run locally: bash the same script.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
csi-data-policy:
|
||||
name: CSI data policy check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Self-test the policy checker (deterministic, offline)
|
||||
run: bash scripts/csi-data-policy-check.sh --self-test
|
||||
|
||||
- name: Enforce CSI data policy on tracked files
|
||||
# CSI_POLICY_BASELINE can point at an acknowledged-paths file once the
|
||||
# owner remediates the tree; unset here so a regression fails loudly.
|
||||
run: bash scripts/csi-data-policy-check.sh --tracked
|
||||
|
||||
- name: Summarize result
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo '### CSI data policy (ADR-299)'
|
||||
echo ''
|
||||
echo '```'
|
||||
bash scripts/csi-data-policy-check.sh --tracked 2>&1 || true
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
67
.github/workflows/model-release-gate.yml
vendored
Normal file
67
.github/workflows/model-release-gate.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Model release gate (ADR-298)
|
||||
|
||||
# ADR-298 model-release sanity gates (issue #1521): structural checks that
|
||||
# block a degenerate/mislabeled classifier head (unreachable decision
|
||||
# boundary, near-constant output, degenerate class balance, a metric
|
||||
# surfaced under a task name it wasn't computed as) before it ships.
|
||||
#
|
||||
# Checker: v2/crates/wifi-densepose-train/src/model_gates.rs
|
||||
#
|
||||
# IMPORTANT — the honest scope of this job: it protects the *checker itself*
|
||||
# from regressing (the gate logic + its issue-1521 regression fixture are
|
||||
# exercised on every push/PR that touches this crate), and running it is
|
||||
# required before ADR-298 can be called "wired in" at all. It does NOT gate
|
||||
# an actual model publish — this repository does not automate uploading to
|
||||
# the HuggingFace model repo (`ruvnet/wifi-densepose-pretrained`); that
|
||||
# remains a manual, human-run step. Before publishing or replacing a model
|
||||
# artifact there, run this gate against the real head weights locally:
|
||||
#
|
||||
# cargo test -p wifi-densepose-train model_gates
|
||||
#
|
||||
# and, until a CLI entry point exists to run `evaluate_linear_head` against an
|
||||
# arbitrary `.safetensors`/`.rvf` file, load the head's `weight`/`bias` in a
|
||||
# short script and call `wifi_densepose_train::evaluate_linear_head` directly.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "v2/crates/wifi-densepose-train/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "v2/crates/wifi-densepose-train/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
model-release-gate:
|
||||
name: Model release gate check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: rustup toolchain install stable --profile minimal
|
||||
|
||||
- name: Run the model-release gate's own test suite
|
||||
working-directory: v2
|
||||
run: cargo test -p wifi-densepose-train --no-default-features model_gates -- --nocapture
|
||||
|
||||
- name: Summarize result
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo '### Model release gate (ADR-298)'
|
||||
echo ''
|
||||
echo 'This job protects `model_gates.rs` from regressing. It does not itself'
|
||||
echo 'gate a real HuggingFace model publish — that upload is a manual step'
|
||||
echo 'outside this repository; run `cargo test -p wifi-densepose-train model_gates`'
|
||||
echo 'against real head weights before publishing one.'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
5
.github/workflows/npm-packages.yml
vendored
5
.github/workflows/npm-packages.yml
vendored
@@ -40,8 +40,9 @@ jobs:
|
||||
- dir: harness/ruview
|
||||
build: false
|
||||
publishable: true
|
||||
# ADR-283: brain + local hosts + replay assets; still runtime-dependency-free.
|
||||
unpacked_budget: 131072
|
||||
# ADR-283/325: brain + local hosts + replay assets + guarded Spaces OAuth adapter;
|
||||
# still runtime-dependency-free. 160 KiB is the reviewed hard ceiling.
|
||||
unpacked_budget: 163840
|
||||
- dir: harness/homecore
|
||||
build: false
|
||||
publishable: true
|
||||
|
||||
4
.github/workflows/ruview-npm-release.yml
vendored
4
.github/workflows/ruview-npm-release.yml
vendored
@@ -104,8 +104,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${{ inputs.package }}" in
|
||||
# ADR-283: brain + local hosts + replay assets; no runtime deps.
|
||||
harness/ruview) export UNPACKED_BUDGET=131072 ;;
|
||||
# ADR-283/325: brain + hosts + replay + guarded Spaces OAuth; no runtime deps.
|
||||
harness/ruview) export UNPACKED_BUDGET=163840 ;;
|
||||
# ADR-285: CLI + MCP + reviewed brain + WASM-kernel adapter.
|
||||
harness/homecore) export UNPACKED_BUDGET=180000 ;;
|
||||
# ADR-264 O2: map-free tarball (was 188 kB with maps).
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -28,8 +28,13 @@ firmware/esp32-csi-node/test/*.obj
|
||||
# Claude Flow swarm runtime state
|
||||
.swarm/
|
||||
|
||||
# CSI recordings (local training data, machine-specific)
|
||||
# CSI recordings (local training/capture data — CSI is person data per
|
||||
# CLAUDE.md; never commit). Covers current and legacy layouts. See ADR-299.
|
||||
data/recordings/
|
||||
v2/data/recordings/
|
||||
rust-port/wifi-densepose-rs/data/recordings/
|
||||
**/*.csi.jsonl
|
||||
**/*.csi.meta.json
|
||||
|
||||
# NVS partition images and CSVs (contain WiFi credentials)
|
||||
nvs.bin
|
||||
@@ -298,4 +303,11 @@ ruvector.db
|
||||
# sensing-server runtime artifacts written by its test suite (trained model
|
||||
# snapshots + the generated session-secret) — never tracked
|
||||
v2/crates/wifi-densepose-sensing-server/data/
|
||||
# The server also writes this secret when launched from v2/. Keep the rule
|
||||
# file-specific so tracked datasets below v2/data remain visible.
|
||||
/v2/data/session-secret
|
||||
*.proptest-regressions
|
||||
|
||||
# ADR-324: wasm-bindgen output for ruview-offaxis is generated locally
|
||||
# (see the crate README); never commit generated artifacts.
|
||||
v2/crates/ruview-offaxis/pkg/
|
||||
|
||||
15
AGENTS.md
15
AGENTS.md
@@ -47,17 +47,18 @@ from the current tree when needed.
|
||||
|
||||
## RuView contributor harness
|
||||
|
||||
`@ruvnet/ruview@0.3.1` is the runtime-dependency-free contributor interface
|
||||
`@ruvnet/ruview@0.5.0` is the runtime-dependency-free contributor interface
|
||||
defined by ADR-283.
|
||||
|
||||
```bash
|
||||
npx @ruvnet/ruview@0.3.1 doctor
|
||||
npx @ruvnet/ruview@0.3.1 guidance --topic homecore --query "restore and plugins"
|
||||
npx @ruvnet/ruview@0.3.1 agent run \
|
||||
npx @ruvnet/ruview@0.5.0 doctor
|
||||
npx @ruvnet/ruview@0.5.0 guidance --topic homecore --query "restore and plugins"
|
||||
npx @ruvnet/ruview@0.5.0 agent run \
|
||||
--host codex --repo . --prompt "Find the nearest tests and cite files"
|
||||
npx @ruvnet/ruview@0.3.1 brain search --query "community memory"
|
||||
npx @ruvnet/ruview@0.3.1 brain verify --repo .
|
||||
npx @ruvnet/ruview@0.3.1 mcp start
|
||||
npx @ruvnet/ruview@0.5.0 brain search --query "community memory"
|
||||
npx @ruvnet/ruview@0.5.0 brain verify --repo .
|
||||
npx @ruvnet/ruview@0.5.0 spaces
|
||||
npx @ruvnet/ruview@0.5.0 mcp start
|
||||
```
|
||||
|
||||
Start unfamiliar repository work with `ruview_guidance`. It returns reviewed
|
||||
|
||||
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **`archive/v1` (the original pure-Python implementation) formally deprecated (ADR-187)** — commits `1fb5397dd`, `b1417fb6e`; refs #509, #1125. Added `archive/v1/DEPRECATED.md` (a loud tombstone) and a `> ⚠️ DEPRECATED` notice atop `archive/v1/README.md`, both pointing at the maintained `v2/` workspace and the `wifi-densepose 2.x` / `ruview` pip wheel (ADR-117). Records the honest fact behind #509: `archive/v1`'s `DensePoseHead` is **architecture-only** — random `kaiming_normal_` init with **zero committed checkpoints** under `archive/v1/` (MEASURED by Glob over `**/*.{pth,onnx,safetensors,pt,ckpt,bin}`). The ADR-028 deterministic proof `archive/v1/data/proof/verify.py` stays live and is explicitly out of scope. The same effort added a **"Model weights: what's real, what's not" three-tier table** to `README.md` + `docs/user-guide.md`, separating real-and-validated checkpoints (presence 82.3% held-out temporal-triplet, MM-Fi pose 82.69% torso-PCK@20, `count_v1`) from the real-but-weak on-device `pose_v1` (PCK@20 = 3.0%, runtime `confidence=0` stub, below the ADR-079 ≥35% target) from the architecture-only `archive/v1` head — and caveated every live single-ESP32 17-keypoint advertisement accordingly. Docs/labeling only; no code or model behavior changed.
|
||||
|
||||
### Fixed
|
||||
- **Pose-vitals, desktop, and repository-integrity issue remediation.** Breathing confidence now measures periodic autocorrelation at the estimated respiratory frequency instead of penalizing clean sinusoidal signals via crest factor (#1610). The desktop launcher resolves the Windows `.exe`, uses `where` for PATH lookup, and passes log filtering through `RUST_LOG`; its React versions, Vite type declarations, and Tauri UI hook working directories are aligned (#1516, #1517, #1518). Runtime session secrets written from `v2/` are ignored, and contributor-harness provenance inputs are pinned to LF across Windows checkouts (#1519, #1520). With explicit owner authorization, the six raw CSI/person-data capture and metadata files identified by ADR-299 were removed from the current tree; historical copies remain pending separately coordinated incident response.
|
||||
- **`docs/huggingface/MODEL_CARD.md` had drifted from the model card actually published on the Hub (issue #1481).** Every filename in its "Files in this repo" table (`pretrained-encoder.onnx`, `pretrained-heads.onnx`, `pretrained.rvf`, `room-profiles.json`) pointed at files never uploaded to `ruvnet/wifi-densepose-pretrained` — only `config.json` existed. Replaced the in-repo card with the content actually live on the Hub (`model.safetensors`, `model-q{2,4,8}.bin`, `node-{1,2}.json`, `presence-head.json`, `csi-embed-v2.*`, honest v1→v2 retraction of the single-class "100%" presence claim) and added a "Using with the Rust sensing server (RVF conversion)" section documenting the `--convert-model`/`--convert-out` and `--model` auto-convert paths that neither card previously mentioned.
|
||||
- **`--convert-model` failed on the published `model.safetensors`: NUL-padded safetensors header rejected by strict JSON parse (issue #1480, #894 follow-up).** The reference safetensors format pads its JSON header to an 8-byte boundary with trailing NUL bytes; `safetensors_to_rvf` (`wifi-densepose-sensing-server/src/model_format.rs`) fed the full declared-length header slice straight to `serde_json::from_slice`, which rejects the padding as "trailing characters." Since the only published full-precision weight file exercises this padding, `--convert-model` could not convert it at all. Fixed by trimming trailing NUL/whitespace bytes before parsing. Pinned by `safetensors_nul_padded_header_converts` (a header padded to the 8-byte boundary, matching the real HF file, converts and round-trips its weights through `ProgressiveLoader`).
|
||||
- **In-server training reconnected — "Start Training" no longer silently no-ops; `/ws/train/progress` streams real progress (ADR-186, issue #1233).** The dashboard's Start Training button POSTed a config, got `success:true`, and nothing happened: `/api/v1/train/start` was a stub that flipped a status string and logged one line, and `/ws/train/progress` 404'd. The full pure-Rust trainer in `training_api.rs` (loads recorded CSI, gradient-descent, exports a `.rvf`) already existed but was **orphaned** — never declared as a module (no `mod training_api;`), so it wasn't compiled at all. Fix (`wifi-densepose-sensing-server`): declared the module, reconciled `AppStateInner` (replaced the `training_status`/`training_config` stub fields with a shared `TrainingState` status handle + cooperative cancel flag + a `training_progress_tx` broadcast), deleted the stub handlers, and merged the real `training_api::routes()` (so `/api/v1/train/{start,stop,status,pretrain,lora}` and `/ws/train/progress` resolve under the existing `/api/v1/*` bearer gate). The training core was decoupled from the ~60-field server state so it is unit-testable. **P5 honesty guarantee:** with `RUVIEW_DISABLE_SERVER_TRAINING` set, start returns a structured `{enabled:false, cli:"wifi-densepose train-room"}` HTTP 409 — never a silent success — and the dashboard disables the Start buttons with a CLI tooltip (enablement is surfaced on `/api/v1/train/status`). Pinned by 8 new tests incl. a **live-socket** test that completes a genuine 101 WebSocket handshake and receives a real progress frame after a POST start, a full POST→poll-status→`.rvf`-exists round-trip, a path-traversal rejection, cancellation, and the disabled-409 path. `cargo test -p wifi-densepose-sensing-server -p wifi-densepose-train --no-default-features` — 0 failed.
|
||||
|
||||
17
CLAUDE.md
17
CLAUDE.md
@@ -45,7 +45,7 @@ retrieved memories, generated proposals, and old test counts are not.
|
||||
Do not hardcode crate, ADR, or test counts in instructions; derive them when a
|
||||
task needs them.
|
||||
|
||||
## Contributor metaharness (`@ruvnet/ruview@0.3.1`)
|
||||
## Contributor metaharness (`@ruvnet/ruview@0.4.0`)
|
||||
|
||||
ADR-283 defines the current community metaharness. It adds secure local
|
||||
Claude/Codex execution, a reviewed shared brain, default-deny MCP mutation
|
||||
@@ -54,21 +54,24 @@ free of runtime dependencies.
|
||||
|
||||
```bash
|
||||
# Diagnose the installed harness
|
||||
npx @ruvnet/ruview@0.3.1 doctor
|
||||
npx @ruvnet/ruview@0.4.0 doctor
|
||||
|
||||
# Get a source-cited capability map before unfamiliar work
|
||||
npx @ruvnet/ruview@0.3.1 guidance --topic homecore --query "restore and plugins"
|
||||
npx @ruvnet/ruview@0.4.0 guidance --topic homecore --query "restore and plugins"
|
||||
|
||||
# Explore this trusted checkout through Claude Code (stdin, plan/safe mode)
|
||||
npx @ruvnet/ruview@0.3.1 agent run \
|
||||
npx @ruvnet/ruview@0.4.0 agent run \
|
||||
--host claude-code --repo . --prompt "Map the relevant subsystem and cite files"
|
||||
|
||||
# Search reviewed, source-cited repository knowledge
|
||||
npx @ruvnet/ruview@0.3.1 brain search --query "community memory"
|
||||
npx @ruvnet/ruview@0.3.1 brain verify --repo .
|
||||
npx @ruvnet/ruview@0.4.0 brain search --query "community memory"
|
||||
npx @ruvnet/ruview@0.4.0 brain verify --repo .
|
||||
|
||||
# Read the OAuth-bound Cognitum Spaces projection
|
||||
npx @ruvnet/ruview@0.4.0 spaces
|
||||
|
||||
# Run the dependency-free RuView MCP server
|
||||
npx @ruvnet/ruview@0.3.1 mcp start
|
||||
npx @ruvnet/ruview@0.4.0 mcp start
|
||||
```
|
||||
|
||||
`ruview_guidance` returns reviewed capability maturity, repository citations,
|
||||
|
||||
23
README.md
23
README.md
@@ -49,25 +49,26 @@ Every WiFi router already fills your space with radio waves. When people move, b
|
||||
<details>
|
||||
<summary><strong>RuView MetaHarness</strong> — guided operation for humans and AI agents</summary>
|
||||
|
||||
The RuView-specific metaharness we created is published as [`@ruvnet/ruview`](harness/ruview/README.md). It provides source-cited guidance, guarded Claude Code/Codex agents, deterministic verification, and an honesty check for accuracy claims.
|
||||
The RuView-specific metaharness we created is published as [`@ruvnet/ruview`](harness/ruview/README.md). It provides source-cited guidance, guarded Claude Code/Codex agents, deterministic verification, an honesty check for accuracy claims, and an explicitly granted OAuth-only Cognitum Spaces read.
|
||||
|
||||
```bash
|
||||
# Check the local setup and get source-cited guidance
|
||||
npx @ruvnet/ruview@0.3.1 doctor
|
||||
npx @ruvnet/ruview@0.3.1 guidance --topic sensing --query "model loading"
|
||||
npx @ruvnet/ruview@0.4.0 doctor
|
||||
npx @ruvnet/ruview@0.4.0 guidance --topic sensing --query "model loading"
|
||||
|
||||
# Run a read-only RuView agent through Codex
|
||||
npx @ruvnet/ruview@0.3.1 agent run --host codex --repo . \
|
||||
npx @ruvnet/ruview@0.4.0 agent run --host codex --repo . \
|
||||
--prompt "Find the nearest tests and cite the source files"
|
||||
|
||||
# Search or verify the reviewed contributor brain
|
||||
npx @ruvnet/ruview@0.3.1 brain search --query "calibration"
|
||||
npx @ruvnet/ruview@0.3.1 brain verify --repo .
|
||||
npx @ruvnet/ruview@0.4.0 brain search --query "calibration"
|
||||
npx @ruvnet/ruview@0.4.0 brain verify --repo .
|
||||
|
||||
# Check claims, replay the deterministic proof, or expose the MCP server
|
||||
npx @ruvnet/ruview@0.3.1 claim-check --file REPORT.md
|
||||
npx @ruvnet/ruview@0.3.1 verify
|
||||
npx @ruvnet/ruview@0.3.1 mcp start
|
||||
npx @ruvnet/ruview@0.4.0 claim-check --file REPORT.md
|
||||
npx @ruvnet/ruview@0.4.0 verify
|
||||
npx @ruvnet/ruview@0.4.0 spaces
|
||||
npx @ruvnet/ruview@0.4.0 mcp start
|
||||
```
|
||||
|
||||
Agent runs are read-only by default. Workspace writes require both `--allow-write` and `--confirm`; retrieved brain content is evidence, not authority.
|
||||
@@ -223,7 +224,7 @@ huggingface-cli download ruvnet/wifi-densepose-pretrained --local-dir models/wif
|
||||
|
||||
| Consumer | Format used | Status |
|
||||
|----------|-------------|--------|
|
||||
| Python training / evaluation / embedding extraction | `model.safetensors` | ✅ Works — load with `safetensors.torch.load_file` |
|
||||
| Python training / evaluation / embedding extraction | `model.safetensors` | ⚠️ The published file's header is NUL-padded, which the reference `safetensors.torch.load_file` rejects (issue [#1522](https://github.com/ruvnet/RuView/issues/1522)) — pending a corrected re-upload. `csi-embed-v2.safetensors` in the same repo is unaffected and loads normally. |
|
||||
| Inspect / re-export the bundle | `model.rvf.jsonl` (line-by-line JSON) | ✅ Works — plain JSONL |
|
||||
| Sensing-server `--model <PATH>` flag | native RVF, `model.safetensors`, or `model.rvf.jsonl` | ✅ Native RVF loads directly; safetensors and JSONL auto-convert in memory |
|
||||
|
||||
@@ -694,7 +695,7 @@ claude --plugin-dir ./plugins/ruview
|
||||
|
||||
Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full details: [`plugins/ruview/README.md`](plugins/ruview/README.md).
|
||||
|
||||
For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.3.1`; the quick commands and fuller explanation are in the collapsed MetaHarness section near the top of this README and in [`harness/ruview/`](harness/ruview/README.md).
|
||||
For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.4.0`; the quick commands and fuller explanation are in the collapsed MetaHarness section near the top of this README and in [`harness/ruview/`](harness/ruview/README.md).
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "pretrain-1775182186",
|
||||
"name": "pretrain-1775182186",
|
||||
"label": "mixed-activity",
|
||||
"started_at": "2026-04-03T02:09:46Z",
|
||||
"ended_at": "2026-04-03T02:11:46Z",
|
||||
"duration_secs": 120,
|
||||
"frame_count": 5783,
|
||||
"file_size_bytes": 2580539,
|
||||
"file_path": "data/recordings\\pretrain-1775182186.csi.jsonl",
|
||||
"nodes": {
|
||||
"2": 2886,
|
||||
"1": 2897
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Accepted — **implemented** (O1–O9 in `@ruvnet/ruview@0.2.0`; security/community extension in `0.3.0`, ADR-283; source-cited guidance in `0.3.1`): fail-closed schemas and MCP policy, async dispatch, zero runtime dependencies, bounded/redacted local Claude/Codex adapters, reviewed shared brain, source-checked capability guidance, and replay-verified Darwin/Flywheel gate. CI gate: `ruview-harness-flywheel.yml` |
|
||||
| **Status** | Accepted — **implemented** (O1–O9 in `@ruvnet/ruview@0.2.0`; security/community extension in `0.3.0`, ADR-283; source-cited guidance in `0.3.1`; guarded Cognitum Spaces OAuth read in `0.4.0`, ADR-325): fail-closed schemas and MCP policy, async dispatch, zero runtime dependencies, bounded/redacted local Claude/Codex adapters, reviewed shared brain, source-checked capability guidance, credential-gated external reads, and replay-verified Darwin/Flywheel gate. CI gate: `ruview-harness-flywheel.yml` |
|
||||
| **Date** | 2026-07-02 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **RUVIEW-NPM-REVIEW-1** |
|
||||
|
||||
@@ -31,6 +31,26 @@ bounded output/time, secret redaction and realpath-based RuView checkout
|
||||
validation. Write mode requires two explicit flags and never uses permission or
|
||||
sandbox bypasses.
|
||||
|
||||
## Credentialed external reads
|
||||
|
||||
Read-only cloud access is not equivalent to an uncredentialed local read. The
|
||||
Cognitum Spaces adapter therefore delegates OAuth and response validation to
|
||||
the Rust `wifi-densepose` client, never accepts bearer tokens or API keys, and
|
||||
removes the API-key compatibility environment from the child process. Its MCP
|
||||
tool is denied unless the server operator grants `credential-use`; MCP callers
|
||||
cannot select a credential path or API origin. The adapter uses only an
|
||||
installed `wifi-densepose` binary; it never executes Cargo build scripts from
|
||||
an auto-detected checkout while holding credential authority. The tool is
|
||||
marked open-world and independently rechecks response size, structure, privacy
|
||||
class, and prohibited raw fields.
|
||||
|
||||
An expiring access token may rotate the stored refresh credential. The MCP
|
||||
annotation is therefore non-read-only and non-idempotent even though the cloud
|
||||
data operation is read-only. That bounded authentication side effect is
|
||||
disclosed in the schema and result. It does not change the cloud operation from
|
||||
read-only and confers no write or action authority. ADR-325 remains authoritative
|
||||
for the Spaces data and policy boundary.
|
||||
|
||||
## Shared brain
|
||||
|
||||
The public brain is committed JSONL, not a shared mutable database. Canonical
|
||||
@@ -67,5 +87,6 @@ autonomously promotes or publishes an evolved candidate.
|
||||
Contributors can explore RuView with either major local CLI and share durable
|
||||
findings without sharing secrets. Improvements become reproducible proposals
|
||||
with frozen evaluation evidence. The cost is a larger development-only npm
|
||||
lockfile, a 128 KiB unpacked-package budget (the current tarball is below that
|
||||
bound), and explicit maintenance of the corpus, genome and gate.
|
||||
lockfile, a 160 KiB unpacked-package budget after adding the duplicated host
|
||||
playbook and bounded OAuth adapter (the package remains runtime-dependency-free),
|
||||
and explicit maintenance of the corpus, genome and gate.
|
||||
|
||||
231
docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md
Normal file
231
docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# ADR-288: VEIL — a compliant-waveform privacy shield against unauthorized WiFi sensing
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed — implemented (P1 reference model) |
|
||||
| **Date** | 2026-08-09 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **VEIL** — Verifiable Emission-shaping for Identity-Leakage prevention |
|
||||
| **Codebase target** | new leaf crate `v2/crates/wifi-densepose-privshield` |
|
||||
| **Parent** | ADR-118 (BFLD — the detection layer VEIL is the countermeasure to), ADR-282 (mandatory L0–L5 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. 2025–2026 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 (L0–L5 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)
|
||||
```
|
||||
94
docs/adr/ADR-290-veil-e2e-hardware-implementation-program.md
Normal file
94
docs/adr/ADR-290-veil-e2e-hardware-implementation-program.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# ADR-290: VEIL end-to-end hardware implementation program (multi-provider firmware)
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed — P4 scaffolding (build-only); portable core validated on host |
|
||||
| **Date** | 2026-08-09 |
|
||||
| **Parent** | ADR-288 (VEIL shield), ADR-289 (harness), ADR-282 (L0–L5 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 2025–2026
|
||||
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.
|
||||
```
|
||||
106
docs/adr/ADR-291-public-benchmark-evaluation-harness.md
Normal file
106
docs/adr/ADR-291-public-benchmark-evaluation-harness.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# ADR-291: Public-benchmark evaluation harness — Widar3.0 ingest, standard split protocols, leakage guards
|
||||
|
||||
- **Status**: Accepted — initial implementation (this PR)
|
||||
- **Date**: 2026-08-10
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: training, evaluation, benchmarks, widar, mm-fi, leakage, honesty
|
||||
|
||||
## Context
|
||||
|
||||
RuView implements the field's key techniques (CSI ratio, BVP features, MAE
|
||||
pretraining, rapid adaptation) but reports results only on self-collected data
|
||||
with self-defined metrics (e.g. the README's held-out temporal-triplet
|
||||
accuracy). A 2026 deep-research sweep of the WiFi-sensing literature found:
|
||||
|
||||
1. Cross-domain generalization is the field's central unsolved problem; the
|
||||
only widely reproduced cross-domain result is Widar3.0's BVP benchmark.
|
||||
2. MM-Fi (NeurIPS 2023) is the standard WiFi-pose benchmark, with defined
|
||||
cross-subject and cross-environment protocols.
|
||||
3. The field had a documented leakage reckoning in 2024–2025: window-level
|
||||
random splits on continuous recordings inflate accuracy (one dataset's F1
|
||||
collapsed from ~90% to ~22% under subject-disjoint splits — Sensors
|
||||
24(10):3159; Signals 6(4):59).
|
||||
|
||||
`wifi-densepose-train` already has an `MmFiDataset` NPY loader and a
|
||||
deterministic `SyntheticCsiDataset`, but no Widar3.0 ingest, no standard split
|
||||
protocols, and no structural leakage guard. CLAUDE.md already requires
|
||||
mean-pose baselines and leakage-free held-out splits for pose PCK; nothing in
|
||||
the code enforces this.
|
||||
|
||||
Without leaderboard-comparable numbers, RuView's claims cannot be ranked
|
||||
against published systems, which blocks both scientific credibility and
|
||||
commercial (OEM licensing) conversations.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Do nothing; keep self-collected metrics.** Rejected: perpetuates the
|
||||
comparability gap.
|
||||
2. **Port a Python eval stack (SenseFi) alongside the Rust pipeline.**
|
||||
Rejected: violates the v2 Rust-workspace direction and adds an unreviewed
|
||||
dependency surface.
|
||||
3. **Extend `wifi-densepose-train` with native loaders + protocol machinery.**
|
||||
Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Extend `v2/crates/wifi-densepose-train` with three additions:
|
||||
|
||||
### 1. Widar3.0 ingest (`dataset::widar`)
|
||||
|
||||
- A parser for the Intel 5300 `.dat` CSI log format ("bfee" records) used by
|
||||
the Widar3.0 raw distribution: framed records with a 3-byte header
|
||||
(2-byte little-endian length + 1-byte code 0xBB), a 20-byte bfee header
|
||||
(timestamp_low, bfee_count, Nrx, Ntx, RSSI a/b/c, noise, agc, antenna_sel,
|
||||
len, rate), and a packed 10-bit-per-component complex CSI payload of
|
||||
30 subcarrier groups. Invalid records are skipped with a warning, not a
|
||||
panic — untrusted file input is validated at the boundary per CLAUDE.md.
|
||||
- A `WidarDataset` implementing the existing `CsiDataset` trait, mapping
|
||||
Widar's `Nrx × Ntx × 30` CSI into windowed `CsiSample`s via the existing
|
||||
subcarrier interpolation, with domain metadata (user, room, orientation,
|
||||
gesture) parsed from Widar's documented directory/file naming convention.
|
||||
- No network access: the loader reads a local dataset root. Dataset download
|
||||
remains a documented manual step.
|
||||
|
||||
### 2. Split protocols (`protocols`)
|
||||
|
||||
- A `SplitProtocol` type expressing the standard evaluations: cross-subject
|
||||
(MM-Fi style), cross-environment/room, cross-orientation (Widar style), and
|
||||
random-baseline (explicitly labelled as leakage-prone, for comparison only).
|
||||
- Split assignment is a pure function of sample metadata + a seed — fully
|
||||
deterministic, no RNG state.
|
||||
|
||||
### 3. Leakage guards (`protocols::leakage`)
|
||||
|
||||
- A structural `LeakageAudit` that, given a proposed train/test split,
|
||||
verifies: (a) subject-disjointness, (b) environment-disjointness where the
|
||||
protocol claims it, (c) no two windows from the same continuous recording
|
||||
span both sides of the split. A failed audit is an `Err`, not a warning.
|
||||
- PCK/accuracy reporting requires a `MeanPoseBaseline` computed from the
|
||||
training split only, and reports model-vs-baseline together, enforcing the
|
||||
CLAUDE.md rule in the type system rather than by convention.
|
||||
- Evaluation output is an evidence-tagged report (`MEASURED` requires a
|
||||
reproducer command line embedded in the report; anything else is emitted as
|
||||
`SYNTHETIC` or `CLAIMED`).
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView results become comparable to published numbers (Widar3.0 cross-domain
|
||||
gesture; MM-Fi cross-subject pose) for the first time.
|
||||
- The leakage audit will make some existing internal numbers look worse. That
|
||||
is the point.
|
||||
- Parsing a legacy binary format adds maintenance surface; mitigated by
|
||||
fixture-based tests with synthetic, deterministically generated `.dat`
|
||||
bytes (no dataset redistribution).
|
||||
- Widar's raw distribution is Intel 5300-specific; ESP32-captured data
|
||||
continues through existing loaders. The protocols/leakage machinery is
|
||||
loader-agnostic.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p wifi-densepose-train` — unit tests for the bfee parser
|
||||
(truncated, corrupt, and valid synthetic fixtures), split determinism,
|
||||
leakage-audit rejection cases, and mean-pose baseline math.
|
||||
- `cargo bench -p wifi-densepose-train` — criterion benchmark for parser
|
||||
throughput and split assignment on synthetic corpora.
|
||||
- No accuracy numbers are claimed by this ADR; it delivers the machinery to
|
||||
produce MEASURED ones.
|
||||
91
docs/adr/ADR-292-wideband-80211ax-csi-ingest.md
Normal file
91
docs/adr/ADR-292-wideband-80211ax-csi-ingest.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# ADR-292: Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter and subcarrier-agnostic plumbing
|
||||
|
||||
- **Status**: Accepted — initial implementation (this PR)
|
||||
- **Date**: 2026-08-10
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: hardware, csi, 80211ax, ax210, feitcsi, ingest, mat
|
||||
|
||||
## Context
|
||||
|
||||
RuView's CSI ingest (`wifi-densepose-mat/src/integration/hardware_adapter.rs`)
|
||||
supports ESP32 serial streams, the legacy Intel 5300 tool, and Atheros/Nexmon
|
||||
paths. All of these are 802.11n-class: ≤40 MHz bandwidth, ≤114 subcarriers,
|
||||
2.4/5 GHz.
|
||||
|
||||
The 2026 research sweep found the field's center of gravity has moved to
|
||||
Intel AX200/AX210 NICs via PicoScenes (closed-source core) and FeitCSI
|
||||
(open-source, GPL): 802.11ax CSI at up to 160 MHz / 1992 subcarriers,
|
||||
including the 6 GHz band. This is both the research-grade tier today and the
|
||||
shape of the data 802.11bf silicon will deliver from ~2026 onward. RuView's
|
||||
`wifi-densepose-hardware` crate already models 802.11bf session types, but no
|
||||
ingest path can carry wideband CSI into the pipeline.
|
||||
|
||||
Without a wideband path, RuView cannot develop against the best available
|
||||
signal, cannot compare ESP32-grade results to wideband upper bounds, and will
|
||||
meet 802.11bf silicon with no tested plumbing for >114-subcarrier frames.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **PicoScenes `.csi` ingest.** Rejected for now: the format is produced by a
|
||||
closed-source core and is versioned/complex; parsing it without a
|
||||
maintained spec invites silent corruption.
|
||||
2. **Raw pcap + radiotap parsing.** Rejected: duplicates what FeitCSI already
|
||||
does on-device, and pulls a packet-capture dependency into the pipeline.
|
||||
3. **FeitCSI file/stream ingest.** Chosen: FeitCSI is open-source (its header
|
||||
layout is auditable against the source), targets AX200/AX210, covers
|
||||
20–160 MHz including 6 GHz, and emits a compact binary record per frame.
|
||||
|
||||
## Decision
|
||||
|
||||
Extend `v2/crates/wifi-densepose-mat/src/integration` with:
|
||||
|
||||
### 1. `feitcsi` record parser
|
||||
|
||||
- A validated parser for FeitCSI's binary CSI record layout (header with
|
||||
CSI buffer length, rate/bandwidth/channel metadata, antenna counts, RSSI,
|
||||
timestamp, followed by interleaved complex CSI). The parser is written
|
||||
against the documented layout, is version-checked, and rejects
|
||||
records whose declared dimensions disagree with the buffer length —
|
||||
untrusted file/stream input is validated at the boundary.
|
||||
- Bounded allocation: a hard cap on subcarrier count (4096) and antenna
|
||||
count (8) so a corrupt length field cannot cause unbounded allocation.
|
||||
|
||||
### 2. `DeviceType::FeitCsi` in the hardware adapter
|
||||
|
||||
- File-replay mode (read a recorded FeitCSI capture deterministically) and a
|
||||
streaming mode fed by an external process writing to a path/pipe. No
|
||||
privileged operations inside the crate: RuView does not configure the NIC;
|
||||
FeitCSI's own tooling owns that, per least-authority.
|
||||
|
||||
### 3. Subcarrier-agnostic plumbing
|
||||
|
||||
- Ingest carries native subcarrier dimensionality end-to-end and converts to
|
||||
pipeline width explicitly via the existing interpolation/decimation stage,
|
||||
recording the native → pipeline mapping in frame metadata so downstream
|
||||
consumers know the true spectral resolution. Bandwidth (20–160 MHz) and
|
||||
band (2.4/5/6 GHz) become first-class frame metadata.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView gains a research-grade wideband development path and a tested
|
||||
ingest shape for future 802.11bf reporting (truncated CIR is a natural
|
||||
extension of the same plumbing).
|
||||
- GPL FeitCSI is used as an external tool, never linked: only its output
|
||||
format is parsed. No licensing contamination of the MIT workspace.
|
||||
- The parser tracks an external project's format; version checks fail loudly
|
||||
on mismatch rather than misparse.
|
||||
- ESP32 remains the deployed sensor tier; wideband is a development/
|
||||
validation tier. Accuracy claims from wideband captures must be tagged with
|
||||
the capture hardware.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p wifi-densepose-mat` — parser tests over synthetic fixtures:
|
||||
valid records at 20/80/160 MHz shapes, truncated buffer, dimension
|
||||
mismatch, version mismatch, allocation-cap enforcement; adapter replay
|
||||
determinism.
|
||||
- `cargo bench -p wifi-densepose-mat` — criterion benchmark for record parse
|
||||
throughput at 1992-subcarrier frames.
|
||||
- Hardware validation on real AX210 silicon is explicitly out of scope for
|
||||
this PR and remains required (per CLAUDE.md) before any capture-path
|
||||
hardware claim; the file-replay path is testable without silicon.
|
||||
93
docs/adr/ADR-293-vitals-ground-truth-rig.md
Normal file
93
docs/adr/ADR-293-vitals-ground-truth-rig.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# ADR-293: Vitals ground-truth rig — reference ingest, time alignment, and agreement metrics
|
||||
|
||||
- **Status**: Accepted — initial implementation (this PR)
|
||||
- **Date**: 2026-08-10
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: vitals, validation, ground-truth, bland-altman, evidence, honesty
|
||||
|
||||
## Context
|
||||
|
||||
`wifi-densepose-vitals` (ADR-021) extracts breathing (0.1–0.5 Hz) and heart
|
||||
rate (0.8–2.0 Hz) from CSI. The 2026 research sweep found that every credible
|
||||
vitals result in the literature ships with reference-sensor ground truth
|
||||
(chest strap, pulse oximeter, ECG, or PSG), and that WiFi heart-rate numbers
|
||||
without stated scope (single person, static, line-of-sight, short range) are
|
||||
systematically misleading. RuView currently has no way to produce a MEASURED
|
||||
vitals number: there is no reference-signal ingest, no time alignment between
|
||||
CSI-derived estimates and a reference device, and no agreement statistics.
|
||||
|
||||
CLAUDE.md requires accuracy statements to be tagged MEASURED (with a
|
||||
reproducer), CLAIMED, or SYNTHETIC. For vitals, MEASURED is currently
|
||||
unreachable.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Live BLE/ANT+ integration with reference devices.** Rejected for now:
|
||||
drivers and pairing are a hardware/product concern; the blocking gap is
|
||||
the evaluation math, not the radio link.
|
||||
2. **File-based reference ingest + offline agreement analysis.** Chosen:
|
||||
every consumer reference device (Polar, Garmin, oximeters) exports
|
||||
timestamped series; a file boundary keeps the crate dependency-free and
|
||||
the pipeline deterministic.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a `groundtruth` module to `v2/crates/wifi-densepose-vitals`:
|
||||
|
||||
### 1. Reference series ingest
|
||||
|
||||
- `ReferenceSeries`: timestamped samples (unix millis + value) for one
|
||||
measurand (`HeartRateBpm` or `BreathingRateBrpm`), with device metadata
|
||||
(make/model, measurement principle). Parsed from CSV (`timestamp_ms,value`
|
||||
with a header line); malformed rows are rejected with row-numbered errors —
|
||||
untrusted file input validated at the boundary. Non-monotonic timestamps
|
||||
are an error, not silently sorted.
|
||||
|
||||
### 2. Time alignment
|
||||
|
||||
- Constant-offset estimation by maximizing normalized cross-correlation of
|
||||
the estimate series against the reference over a bounded lag window
|
||||
(default ±30 s), on a common resampled grid (nearest-sample, no
|
||||
interpolation of physiological values across gaps larger than a
|
||||
configurable limit).
|
||||
- Optional linear clock-drift fit (offset + rate) for long sessions.
|
||||
Alignment parameters are reported, never silently applied.
|
||||
|
||||
### 3. Agreement metrics
|
||||
|
||||
- `AgreementReport`: n paired samples, coverage fraction (time where both
|
||||
series had valid samples), MAE, RMSE, mean error (bias), Bland–Altman
|
||||
95% limits of agreement, and percentage-within-tolerance (configurable,
|
||||
default ±2 bpm HR / ±1 brpm breathing).
|
||||
- Session scope is mandatory metadata: subject count, motion state
|
||||
(static/moving), line-of-sight (LOS/NLOS/through-wall), distance band.
|
||||
A report without scope cannot be constructed.
|
||||
|
||||
### 4. Evidence tagging
|
||||
|
||||
- `EvidenceGrade::Measured` is only constructible when the report carries a
|
||||
reference device, non-zero paired samples, minimum coverage, and a
|
||||
reproducer command string; otherwise the report grades as `Claimed` (real
|
||||
data, no reference) or `Synthetic` (generated input). This mirrors
|
||||
ADR-291's enforcement-in-types approach and the CLAUDE.md tagging rule.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView can convert vitals claims from CLAIMED to MEASURED with a
|
||||
reproducible offline analysis, session by session, scope by scope.
|
||||
- Honest reporting will likely show heart-rate performance below marketing
|
||||
intuition, especially NLOS/moving — that is the purpose.
|
||||
- CSV ingest means a manual export step per session; acceptable at current
|
||||
scale, and the format is the de-facto export of consumer reference gear.
|
||||
- No clinical claim is implied: agreement statistics against consumer
|
||||
reference devices are engineering evidence, not medical validation.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p wifi-densepose-vitals` — CSV rejection cases, alignment
|
||||
recovery of known synthetic offsets/drifts, agreement metrics against
|
||||
hand-computed fixtures, evidence-grade constructibility rules.
|
||||
- `cargo bench -p wifi-densepose-vitals` — criterion benchmark for alignment
|
||||
over hour-scale synthetic sessions.
|
||||
- Real-session validation (ESP32 capture + chest strap) remains a follow-up
|
||||
requiring hardware evidence per CLAUDE.md.
|
||||
83
docs/adr/ADR-294-wifi-veil-integration.md
Normal file
83
docs/adr/ADR-294-wifi-veil-integration.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# ADR-294: WiFi Veil integration — emission-shaping countermeasure as an advisory BFLD dependency
|
||||
|
||||
- **Status**: Accepted — initial implementation (this PR)
|
||||
- **Date**: 2026-08-10
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: privacy, bfld, bfi, wifi-veil, countermeasure, dependency
|
||||
|
||||
## Context
|
||||
|
||||
RuView's BFLD layer (ADR-118, ADR-141) senses via beamforming feedback while
|
||||
enforcing structural privacy invariants on data entering the node. The 2026
|
||||
research sweep identified the complementary, unaddressed surface: a node's own
|
||||
*outgoing* BFI is unencrypted and enables passive third-party
|
||||
re-identification (BFId, ACM CCS 2025); IEEE 802.11bf-2025 shipped with no
|
||||
privacy mechanism; and no commercial product occupies the countermeasure
|
||||
category.
|
||||
|
||||
[`wifi-veil`](https://github.com/ruvnet/wifi-veil) (codename VEIL, extracted
|
||||
from this monorepo as a standalone crate) models a compliant emission-shaping
|
||||
defense: keyed Givens rotations over the fine subspace of compressed
|
||||
beamforming reports, energy-preserving (never jamming), reversible by a
|
||||
keyed legitimate receiver. The crate is dependency-free, deterministic,
|
||||
std-only, WASM-ready, dual MIT/Apache-2.0, and explicitly SYNTHETIC/L0: it
|
||||
models waveform controls and never drives a radio.
|
||||
|
||||
RuView should consume this capability rather than re-implement it, giving the
|
||||
sensing stack a defensive counterpart under one evidence regime.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Vendor the veil sources into a RuView crate.** Rejected: forks the
|
||||
witness-pinned upstream and duplicates maintenance.
|
||||
2. **crates.io dependency.** Not yet available (v0.1.0 unpublished at
|
||||
decision time); revisit when released.
|
||||
3. **Git dependency pinned to an exact rev, feature-gated in
|
||||
`wifi-densepose-bfld`.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
- Add `wifi-veil` to `v2/Cargo.toml` `[workspace.dependencies]` as a git
|
||||
dependency pinned to rev `018468b5d2bf41f35c552910f35659830af0eb91`
|
||||
(v0.1.0). Exact-rev pinning preserves provenance and reproducibility for a
|
||||
pre-release upstream; bumping the rev is an explicit, reviewable change.
|
||||
- Gate it in `wifi-densepose-bfld` behind a new `veil` feature
|
||||
(`veil = ["std", "dep:wifi-veil"]`), off by default — the default build
|
||||
remains dependency-light and unchanged.
|
||||
- New `bfld::veil` module (advisory-only):
|
||||
- `ShieldAssessment`: stable projection of wifi-veil's deterministic
|
||||
attacker-vs-protector `ExperimentReport` (re-ID accuracy shield-off/on,
|
||||
chance level, throughput ratio, energy-conservation audit), always
|
||||
carrying the `SYNTHETIC/L0` evidence label.
|
||||
- `assess` / `assess_default`: run the deterministic experiment.
|
||||
- `optimized_shield`: wrap `hyper_optimize` to derive the
|
||||
optimizer-shipped shield config plus its verifying assessment.
|
||||
- Boundaries, stated structurally and in docs:
|
||||
- **Advisory only.** Nothing in the integration emits RF, alters frames,
|
||||
or relaxes any BFLD gate/invariant (I1–I3 untouched).
|
||||
- **Evidence honesty.** Every veil-derived figure is labeled
|
||||
`SYNTHETIC/L0`; no MEASURED claim is possible from this path (hardware
|
||||
validation lives in wifi-veil's own P5 roadmap).
|
||||
- ESP32 nodes cannot shield their own feedback (per wifi-veil's platform
|
||||
matrix); the integration therefore informs posture and reporting, not
|
||||
on-node emission control.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView gains a sense-and-defend posture no commercial offering has, under
|
||||
a single claim taxonomy.
|
||||
- First git dependency in the workspace: builds now fetch one pinned
|
||||
external rev. Acceptable: the crate is dependency-free, small, witness-
|
||||
pinned upstream, and license-compatible (MIT OR Apache-2.0 into MIT).
|
||||
- Feature-gated consumers (e.g. sensing-server privacy reporting, the
|
||||
desktop UI) can surface shield assessments later without new deps.
|
||||
- When wifi-veil publishes to crates.io, switch the workspace entry to a
|
||||
version requirement in a follow-up ADR amendment.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p wifi-densepose-bfld --features veil` — determinism,
|
||||
shield-reduces-re-ID, compliance (energy conservation), chance-band
|
||||
attainment, evidence labeling, optimizer wrapper.
|
||||
- `cargo test -p wifi-densepose-bfld` (default features) — unchanged
|
||||
behavior with the feature off.
|
||||
64
docs/adr/ADR-295-source-provenance-state-machine.md
Normal file
64
docs/adr/ADR-295-source-provenance-state-machine.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# ADR-295: Source provenance state machine — synthetic can never present as live
|
||||
|
||||
- **Status**: Accepted — initial implementation (this PR)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: provenance, honesty, ui, sensing-server, security
|
||||
|
||||
## Context
|
||||
|
||||
An August 2026 external review found two provenance defects on the release
|
||||
path:
|
||||
|
||||
1. The pose-fusion simulator starts in demo mode; on any page port other than
|
||||
3000 the WebSocket target falls back to `localhost:8765`, and if the
|
||||
connection fails the simulator keeps running while the status still reads
|
||||
"ready" — producing a convincing moving visualization with no live CSI
|
||||
(issue 1557).
|
||||
2. The main sensing client labels the source **live** when the authenticated
|
||||
status endpoint returns an error for lack of authorization, until a real
|
||||
frame happens to correct it (issue 1526).
|
||||
|
||||
The common root cause: source state is a boolean (live vs not), so "unknown"
|
||||
collapses to "live". CLAUDE.md requires MEASURED/CLAIMED/SYNTHETIC labeling
|
||||
and forbids presenting synthetic output as real.
|
||||
|
||||
## Decision
|
||||
|
||||
Define one canonical, mutually exclusive `SourceState` enum shared by the
|
||||
sensing server and every UI/client that renders a source:
|
||||
|
||||
- `Synthetic` — generated data (simulator/replay of synthetic fixtures).
|
||||
- `LiveVerified` — frames from an authenticated, attested source.
|
||||
- `LiveUnverified` — frames arriving but provenance not yet confirmed.
|
||||
- `Stale` — last frame older than a configured freshness window.
|
||||
- `Disconnected` — no source.
|
||||
|
||||
Rules enforced structurally:
|
||||
|
||||
- **`Unknown` is not a state.** Any ambiguous condition resolves to
|
||||
`LiveUnverified`, `Stale`, or `Disconnected` — never `LiveVerified`.
|
||||
- A status-endpoint error resolves to `Disconnected`/`LiveUnverified`, never
|
||||
live-verified.
|
||||
- The simulator constructs `Synthetic` and cannot transition to any `Live*`
|
||||
state without a verified frame.
|
||||
- `Synthetic` is watermarked in every view and every export.
|
||||
- Transitions are a pure function of (last-frame-age, auth-status,
|
||||
source-kind) so they are unit-testable without a clock or a socket.
|
||||
|
||||
Scope of this PR: the shared `SourceState` type + transition function + tests
|
||||
in the sensing server, and wiring of the two identified surfaces (pose-fusion
|
||||
simulator status, sensing client source label). Broader UI adoption follows.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Closes the "synthetic shown as live" and "unknown shown as live" classes.
|
||||
- A small breaking change to any consumer currently reading a boolean source
|
||||
flag; mitigated by exposing a compatibility accessor during migration.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests for every transition, especially: auth-error → not-live;
|
||||
simulator → never live without a verified frame; freshness expiry → `Stale`;
|
||||
watermark present on synthetic export.
|
||||
- `cargo test -p wifi-densepose-sensing-server`.
|
||||
59
docs/adr/ADR-296-sensor-data-plane-bind-hardening.md
Normal file
59
docs/adr/ADR-296-sensor-data-plane-bind-hardening.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# ADR-296: Sensor data-plane hardening — UDP bind control and source allowlist (step one)
|
||||
|
||||
- **Status**: Accepted — initial implementation (this PR)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: security, udp, sensor-ingest, sensing-server
|
||||
|
||||
## Context
|
||||
|
||||
The CSI UDP receiver binds `0.0.0.0:{udp_port}` unconditionally
|
||||
(`main.rs:5706`), with no equivalent of the HTTP `--bind-addr` flag (which
|
||||
correctly defaults to `127.0.0.1`), no source allowlist, no message
|
||||
authentication, no device identity, and no replay defense. Any host that can
|
||||
reach the UDP port can inject a valid-shaped frame, flip an auto-detecting
|
||||
server into a live source state, and influence presence/vital/automation
|
||||
outputs (issue 1394).
|
||||
|
||||
An IP allowlist does not stop LAN spoofing, but bind control plus an allowlist
|
||||
is the correct, shippable first step; per-device keys + authenticated
|
||||
encryption + monotonic sequence + freshness window + replay rejection is the
|
||||
full fix and is larger.
|
||||
|
||||
## Decision
|
||||
|
||||
**This PR (step one):**
|
||||
|
||||
- Add `--udp-bind` (env `RUVIEW_UDP_BIND`), **defaulting to `127.0.0.1`**.
|
||||
Binding to a routable address is now an explicit operator choice, mirroring
|
||||
the HTTP path. Desktop/appliance defaults stay loopback.
|
||||
- Add an optional source IP/CIDR allowlist (`--udp-allow`); when set, frames
|
||||
from other sources are dropped and counted. Loopback is always allowed.
|
||||
- Emit a startup security log line stating the bind scope and whether an
|
||||
allowlist is active; refuse a routable bind without an allowlist unless an
|
||||
explicit `--udp-insecure-lan` override is passed (parallel to the existing
|
||||
Docker HTTP refusal).
|
||||
- Publish a `SECURITY.md`/advisory note describing the threat model and safe
|
||||
deployment.
|
||||
|
||||
**Explicitly deferred to a follow-up ADR (step two):** per-device provisioned
|
||||
keys, MAC/AEAD, device identifiers, monotonic sequence numbers, freshness
|
||||
window, and replay rejection. This ADR documents that gap rather than
|
||||
implying the data plane is authenticated.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Removes the default open-to-LAN exposure with a one-line-safe default.
|
||||
- Not spoof-proof on a trusted LAN — the advisory says so plainly, and the
|
||||
override name (`--udp-insecure-lan`) makes the residual risk legible.
|
||||
- A behavior change for anyone relying on the old implicit `0.0.0.0` default;
|
||||
called out in the changelog and the startup log.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests: default bind is loopback; routable bind without allowlist is
|
||||
refused unless overridden; allowlist accept/drop with counting; loopback
|
||||
always allowed.
|
||||
- `cargo test -p wifi-densepose-sensing-server`.
|
||||
- Real-silicon validation of the LAN path remains required before any
|
||||
deployment claim.
|
||||
58
docs/adr/ADR-297-multi-node-semantic-correctness.md
Normal file
58
docs/adr/ADR-297-multi-node-semantic-correctness.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# ADR-297: Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state
|
||||
|
||||
- **Status**: Accepted — initial implementation (this PR)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: multi-node, mqtt, home-assistant, correctness, sensing-server
|
||||
|
||||
## Context
|
||||
|
||||
The external review confirmed three defects on the multi-node path — the core
|
||||
mechanism RuView uses to reduce blind spots and room dependence:
|
||||
|
||||
1. The active `NodeInfo` payload carries RSSI/position/subcarrier/sync but **no
|
||||
per-node classification**; the MQTT mapper reads `node.classification` and
|
||||
falls back to the room aggregate when absent, so every node can publish the
|
||||
same aggregate presence value (issues 1540, 1554).
|
||||
2. The MQTT `RateLimiter` is keyed by `EntityKind` only
|
||||
(`mqtt/state.rs:65`), so one node consumes the numeric publish slot and the
|
||||
others are suppressed until the interval expires, while availability still
|
||||
says online (issue 1541).
|
||||
3. In the UDP vital path, top-level classification is taken from the
|
||||
latest-arriving node while other features are fused, so with disagreeing
|
||||
nodes room presence can flip at packet frequency (issue 1555).
|
||||
|
||||
## Decision
|
||||
|
||||
- **Separate the types.** Introduce `NodeInference` (per-node classification +
|
||||
confidence + freshness) distinct from `RoomInference` (the fused room
|
||||
aggregate). `NodeInfo` carries a `NodeInference`; the room aggregate is
|
||||
computed explicitly and never overwrites node state. No silent fallback from
|
||||
node to room.
|
||||
- **Key the rate limiter by (node, entity).** `RateLimiter` becomes keyed on
|
||||
`(NodeId, EntityKind)` so nodes no longer starve each other; per-entity
|
||||
behavior per node is preserved.
|
||||
- **Deterministic fusion.** Room classification is a pure function of the set
|
||||
of current per-node inferences (e.g. freshness-weighted vote), not
|
||||
last-writer-wins; identical inputs yield identical room state.
|
||||
- **Stale entities cannot stay online.** An entity whose backing node has not
|
||||
reported within N expected publish intervals transitions to unavailable/
|
||||
stale rather than holding a frozen value while availability says online.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Multi-node HA/MQTT output becomes semantically correct; distinct nodes
|
||||
report distinct state and no longer suppress one another.
|
||||
- Schema change to `NodeInfo`/the MQTT contract; existing single-node
|
||||
deployments keep working (one node = one inference). Consumers reading the
|
||||
old aggregate-only shape need the migration accessor.
|
||||
- Aligns with ADR-295 (freshness) and the review's call for one canonical
|
||||
`NodeInference`/`RoomInference` contract.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit/integration tests: per-node classification round-trips through the MQTT
|
||||
mapper with no room fallback; two nodes with different rates both publish
|
||||
(no starvation); disagreeing nodes produce deterministic, non-flapping room
|
||||
state; a silent node's entities go stale, not frozen-online.
|
||||
- `cargo test -p wifi-densepose-sensing-server`.
|
||||
60
docs/adr/ADR-298-model-release-sanity-gates.md
Normal file
60
docs/adr/ADR-298-model-release-sanity-gates.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# ADR-298: Model release sanity gates — block degenerate and mislabeled model artifacts
|
||||
|
||||
- **Status**: Accepted — initial implementation (this PR)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: models, evaluation, release-gate, honesty, presence
|
||||
|
||||
## Context
|
||||
|
||||
The external review (corroborating issue 1521) showed the published presence
|
||||
head is mathematically degenerate: with L2-normalized embeddings, a weight
|
||||
norm ≈ 3.67 against a bias ≈ 8.19 makes the smallest possible logit positive,
|
||||
so predicted presence probability is ≥ ~0.989 for every valid input — the
|
||||
decision boundary is unreachable and the head is effectively constant. The
|
||||
README then labeled a temporal-triplet accuracy (a representation-ordering
|
||||
metric) as "presence accuracy" — a category error.
|
||||
|
||||
Nothing in the release path catches a constant classifier, an unreachable
|
||||
boundary, or a metric-name mismatch. A machine check would have.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a `model_gates` module (in `wifi-densepose-train`) plus a CI gate that,
|
||||
for any classifier artifact proposed for release, fails on:
|
||||
|
||||
- **Constant output** — output variance below a threshold across a diverse
|
||||
probe set (including the degenerate-embedding probe from issue 1521).
|
||||
- **Unreachable decision boundary** — for a normalized-embedding linear head,
|
||||
check whether `bias` sign dominates `‖weight‖` so the logit cannot change
|
||||
sign; fail if the boundary is analytically unreachable.
|
||||
- **Degenerate class balance** — predicted-positive rate at/above a ceiling
|
||||
(e.g. > 99%) on a balanced probe set.
|
||||
- **Missing/blank baseline** — a report without a paired mean-pose/majority
|
||||
baseline (ties into ADR-291 `EvaluationReport`).
|
||||
- **Metric-name provenance** — a metric may not be surfaced under a task name
|
||||
that does not match its computed kind (temporal-triplet ≠ presence);
|
||||
enforced by making the metric carry its kind and the label derive from it.
|
||||
|
||||
Each gate emits a structured, human-readable failure explaining the defect and
|
||||
the offending numbers.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The specific degenerate presence head cannot ship again, and the
|
||||
temporal-triplet-as-presence mislabel is structurally prevented.
|
||||
- Some existing artifacts will fail the gate on introduction — intended; they
|
||||
should fail.
|
||||
- The gate is heuristic, not a correctness proof; it catches the known
|
||||
failure shapes, not all bad models.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests: the issue-1521 weights fail the unreachable-boundary and
|
||||
constant-output gates; a healthy synthetic head passes; a temporal-triplet
|
||||
metric cannot be constructed with a presence label.
|
||||
- `cargo test -p wifi-densepose-train`; the CI gate runs in the model-check
|
||||
workflow.
|
||||
- This ADR does **not** withdraw the already-published artifact (an
|
||||
outward-facing action requiring maintainer sign-off) — it prevents
|
||||
recurrence and documents the model-card correction.
|
||||
52
docs/adr/ADR-299-csi-data-incident-repo-controls.md
Normal file
52
docs/adr/ADR-299-csi-data-incident-repo-controls.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# ADR-299: Repository CSI data-incident controls — ignore rules and a pre-commit/CI policy check
|
||||
|
||||
- **Status**: Accepted — controls and current-tree remediation implemented; history coordination pending
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: privacy, data-governance, ci, security, incident
|
||||
|
||||
## Context
|
||||
|
||||
The external review found ~64.6 MB of tracked raw CSI recordings under
|
||||
`data/recordings/` and `v2/data/recordings/` (largest an ~61.8 MB overnight
|
||||
capture). CLAUDE.md explicitly prohibits committing CSI or person data. The
|
||||
`.gitignore` rule pointed only at a pre-rename path
|
||||
(`rust-port/wifi-densepose-rs/data/recordings/`) and did not cover the active
|
||||
directories, which is how the captures were committed. Raw CSI is person data
|
||||
(it encodes breathing, movement, presence), so this is a data incident, not a
|
||||
formatting nit.
|
||||
|
||||
## Decision
|
||||
|
||||
**Implemented now (mechanical, no data-ownership judgment):**
|
||||
|
||||
- Fix `.gitignore` to cover `data/recordings/`, `v2/data/recordings/`, the
|
||||
legacy path, and `*.csi.jsonl` / `*.csi.meta.json` globs (done in this PR).
|
||||
- Add a policy check (pre-commit hook + CI job) that fails when CSI-format
|
||||
files (`*.csi.jsonl`, `*.csi.meta.json`) or large JSONL captures are staged
|
||||
or present as tracked files, with a message pointing here. Tests may use
|
||||
only synthetic or expressly-consented minimal fixtures.
|
||||
|
||||
**Owner-authorized current-tree remediation (2026-08-15):**
|
||||
|
||||
- The data owner authorized removal of the six known CSI capture and metadata
|
||||
files from the current tree. The removal is recoverable from Git history and
|
||||
does not claim to erase existing clones, forks, caches, or release artifacts.
|
||||
- Any history rewrite remains a separate coordinated incident-response action.
|
||||
It requires an inventory of affected refs and releases, downstream notice,
|
||||
credential and artifact review, and an explicit execution plan.
|
||||
|
||||
## Consequences
|
||||
|
||||
- No new CSI captures can be committed (ignore + policy check).
|
||||
- The six known tracked recordings are absent from the current tree. Historical
|
||||
copies remain until a separately authorized and coordinated history rewrite.
|
||||
- CI gains one fast policy job; contributors get a local pre-commit check.
|
||||
|
||||
## Validation
|
||||
|
||||
- Policy-check unit tests: a staged `*.csi.jsonl` fails; a synthetic fixture
|
||||
under an allowed test path passes; the check is deterministic and offline.
|
||||
- Manual confirmation that the new ignore globs cover both active directories.
|
||||
- `bash scripts/csi-data-policy-check.sh --tracked` passes after the authorized
|
||||
current-tree removal.
|
||||
189
docs/adr/ADR-300-perception-substrate-program.md
Normal file
189
docs/adr/ADR-300-perception-substrate-program.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# ADR-300: RuView perception substrate — a phased program for the calibration, evidence, trust, and deployment layer
|
||||
|
||||
- **Status**: Accepted — program framing; child ADRs carry their own status
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: program, architecture, calibration, evidence, provenance, fusion, fleet, epic
|
||||
|
||||
## Context
|
||||
|
||||
Three independent analyses converged on the same conclusion in 2026: a deep
|
||||
research sweep of the WiFi-sensing state of the art, an external technical and
|
||||
industry review, and an internal strategic assessment. All three found that
|
||||
RuView's gap is **not another sensing modality** but the horizontal layer that
|
||||
turns RF research into repeatable spatial infrastructure — measurement,
|
||||
calibration, out-of-distribution awareness, evidence accounting, authenticated
|
||||
identity, a canonical spatial model, and fleet deployment.
|
||||
|
||||
Several of these primitives already have foundations in the tree and should be
|
||||
**unified and made to produce signed, expiring certificates**, not rebuilt:
|
||||
|
||||
- `wifi-densepose-calibration` (enrollment, bank, anchor, runtime, specialist).
|
||||
- `frame::EvidenceLevel` L0–L5 as mandatory policy (ADR-282).
|
||||
- AetherArena benchmark infrastructure — v0 complete, CI-gated, witness ledger,
|
||||
live HF Space (ADR-149); board intentionally empty (benchmark-first).
|
||||
- RuField provenance/signature types (ADR-260/262/277/279) and BFLD
|
||||
attestation (ADR-141).
|
||||
- `worldgraph` crate; `wifi-densepose-mat/tracking` (tracker, fingerprint).
|
||||
- The in-flight ADR-295 (provenance state machine), ADR-296 (authenticated
|
||||
data plane, step one), ADR-298 (model sanity gates) — the first bricks.
|
||||
|
||||
## What RuView is optimizing for
|
||||
|
||||
Not inference capability — **epistemic reliability**:
|
||||
|
||||
```
|
||||
signal → observation → calibration → inference → uncertainty → evidence
|
||||
→ certificate → policy → governed action
|
||||
```
|
||||
|
||||
That pipeline is the product. The defensible category is not "RuView perceives
|
||||
the physical world" but "RuView determines what machines are justified in
|
||||
believing about it, proves why, and constrains what they may do with that
|
||||
belief."
|
||||
|
||||
### Four non-negotiable program rules
|
||||
|
||||
Every child ADR and implementation is bound by these:
|
||||
|
||||
1. **UNKNOWN is a first-class output, never an error condition.** A surface that
|
||||
cannot answer says UNKNOWN and stays legible; it does not throw, default to a
|
||||
confident class, or silently hold a stale value.
|
||||
2. **Capability certificates bind cryptographically.** Hardware, environment,
|
||||
model, calibration, metrics, expiry, and evidence level are bound under one
|
||||
signature (ADR-318/ADR-305). An unsigned or partially-bound certificate is
|
||||
not a certificate.
|
||||
3. **One canonical semantics downstream.** Every surface (MQTT, REST, WebSocket,
|
||||
RuField, Matter, agents, UI) consumes the same Observation → Inference →
|
||||
GovernedEvent types (ADR-306). No transport- or UI-specific reinterpretation.
|
||||
4. **Benchmarks expose worst-domain performance and confidence intervals.**
|
||||
Pooled accuracy is never sufficient for promotion (ADR-317).
|
||||
|
||||
### Certificate conditionality (the staleness guard)
|
||||
|
||||
The central architectural risk is **certificate staleness**: a room can remain
|
||||
syntactically calibrated while its RF distribution has drifted enough to
|
||||
invalidate the certificate. Therefore a capability certificate is **conditional
|
||||
on a continuously evaluated domain signature** (ADR-302), not a one-time stamp.
|
||||
Crossing the OOD threshold automatically degrades state and triggers
|
||||
recalibration rather than silently continuing:
|
||||
|
||||
```
|
||||
VALID → DEGRADED → UNKNOWN (auto-degrade on domain drift; triggers recalibration)
|
||||
```
|
||||
|
||||
This binds ADR-301 (calibration), ADR-302 (OOD), ADR-318 (certificate), and
|
||||
ADR-321 (policy): a degraded/unknown domain must invalidate the affected
|
||||
capability *before* a false confident inference reaches an actuator.
|
||||
|
||||
### Commercial framing — three primitives, not one product
|
||||
|
||||
- **RuView Runtime** — provides perception.
|
||||
- **RuView Certify** — establishes what a deployment can legitimately claim
|
||||
(calibration + evidence + capability certificate + policy).
|
||||
- **RuView Trust / Fleet** — keeps that claim valid across hardware, firmware,
|
||||
models, and environmental drift (ADR-316).
|
||||
|
||||
Certify and Trust are the parts that are hard to commoditize; presence
|
||||
detection alone is not.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt a **21-primitive phased program**. Each primitive gets a child ADR
|
||||
(ADR-301…ADR-321) that owns its detailed decision, status, and validation.
|
||||
This ADR owns the framing, the dependency order, and the phase assignment.
|
||||
|
||||
### Primitive → ADR map
|
||||
|
||||
| # | Primitive | ADR | Phase |
|
||||
|---|---|---|---|
|
||||
| 1 | Automatic domain calibration | ADR-301 | 1 |
|
||||
| 2 | Out-of-distribution detection | ADR-302 | 1 |
|
||||
| 3 | Ground-truth synchronization | ADR-303 | 2 |
|
||||
| 4 | Evidence engine | ADR-304 | 1 |
|
||||
| 5 | Authenticated sensor identity | ADR-305 | 1 |
|
||||
| 6 | Canonical spatial ontology | ADR-306 | 1 |
|
||||
| 7 | Persistent identity & tracking | ADR-307 | 2 |
|
||||
| 8 | Sensor placement optimizer | ADR-308 | 3 |
|
||||
| 9 | Active sensing | ADR-309 | 3 |
|
||||
| 10 | 802.11bf-native architecture | ADR-310 | 2 |
|
||||
| 11 | Real sensor fusion | ADR-311 | 2 |
|
||||
| 12 | Long-term spatial memory | ADR-312 | 3 |
|
||||
| 13 | Counterfactual inference | ADR-313 | 3 |
|
||||
| 14 | Information-gain scheduler | ADR-314 | 3 |
|
||||
| 15 | Digital RF twin | ADR-315 | 3 |
|
||||
| 16 | Fleet control plane | ADR-316 | 2 |
|
||||
| 17 | Real benchmark service (multi-domain scorecard) | ADR-317 | 1 |
|
||||
| 18 | Capability certificates | ADR-318 | 1 |
|
||||
| 19 | Witness chain | ADR-319 | 1 |
|
||||
| 20 | RuView sensor HAL | ADR-320 | 2 |
|
||||
| 21 | Decision policy — action authorization | ADR-321 | 1 |
|
||||
|
||||
### Dependency order (why phase, not score, drives sequencing)
|
||||
|
||||
```
|
||||
ADR-306 spatial ontology ──┐
|
||||
ADR-305 auth identity ─────┼──► ADR-301 calibration cert ──► ADR-302 OOD gating
|
||||
│ │ │
|
||||
└──► ADR-319 witness chain │ (VALID→DEGRADED→UNKNOWN)
|
||||
│ ▼
|
||||
ADR-304 evidence engine ──► ADR-318 capability certificate
|
||||
│ │ (conditional on domain signature)
|
||||
│ ▼
|
||||
│ ADR-321 decision policy ──► governed action
|
||||
└──► ADR-317 benchmark scorecard (per-PR gate)
|
||||
```
|
||||
|
||||
- **Phase 1 (the certificate spine, built now):** foundational roots 303, 302,
|
||||
301, 298 (implemented first, in their own crates); then the dependent wave
|
||||
316, 299, 315, 314, 318. This set is exactly the acceptance test decomposed
|
||||
and is buildable without new hardware (types, logic, signatures, tests). The
|
||||
dependent wave adds the staleness guard (299 auto-degrades 315) and the
|
||||
action gate (318) that denies at the actuator on a degraded/unknown domain.
|
||||
- **Phase 2 (integration & operations):** 300 ground truth, 304 tracking, 307
|
||||
802.11bf-native, 308 fusion, 313 fleet, 317 HAL. Depends on the spine.
|
||||
- **Phase 3 (higher-ceiling, research-forward):** 305 placement optimizer, 306
|
||||
active sensing, 309 spatial memory, 310 counterfactual, 311 info-gain
|
||||
scheduler, 312 RF twin. Sit on top of the fused world state.
|
||||
|
||||
Phase-2 and phase-3 child ADRs are authored as **Proposed** (design intent,
|
||||
validation plan) and are not implemented by the phase-1 swarm.
|
||||
|
||||
### Acceptance test A — onboarding (from the strategic assessment)
|
||||
|
||||
> Connect a new sensor type in an unseen room. Within 30 minutes RuView should
|
||||
> identify the hardware (HAL, ADR-320), calibrate the environment (ADR-301),
|
||||
> quantify whether it can reliably sense the requested phenomenon (ADR-302),
|
||||
> generate a signed capability certificate (ADR-318), expose governed spatial
|
||||
> events (ADR-306), and return UNKNOWN whenever evidence falls outside that
|
||||
> certificate (ADR-302).
|
||||
|
||||
### Acceptance test B — drift invalidation (the staleness guard)
|
||||
|
||||
> Deliberately change the room after certification — move furniture, change the
|
||||
> AP channel, or substitute hardware. RuView should detect distribution drift
|
||||
> (ADR-302), invalidate the affected capability (ADR-318) **before** a false
|
||||
> confident inference reaches an actuator (ADR-321 denies with the specific
|
||||
> failed condition), emit UNKNOWN, preserve the complete witness chain
|
||||
> (ADR-319), and explain exactly which certificate condition failed.
|
||||
|
||||
Test B is the load-bearing one: it proves the substrate fails safe, not just
|
||||
that it perceives well. Phase 1 makes every clause except HAL testable in
|
||||
software; HAL (phase 2)
|
||||
closes the "identify the hardware" clause.
|
||||
|
||||
## Consequences
|
||||
|
||||
- One coherent substrate replaces overlapping ad-hoc schemas; every surface
|
||||
(MQTT, REST, WebSocket, RuField, Matter, agents) eventually consumes the
|
||||
ADR-306 ontology and the ADR-318 certificate.
|
||||
- Headline applications (pose/vitals/pointcloud models) are explicitly **not**
|
||||
the investment focus during this program, per the strategic direction.
|
||||
- Later ADRs may be revised as the spine lands; that is expected for a phased
|
||||
program and is why phase-2/3 ADRs ship as Proposed.
|
||||
|
||||
## Validation
|
||||
|
||||
- Each child ADR defines its own tests. The program-level exit is the
|
||||
acceptance test above, run end-to-end once phase 1 lands, and encoded as an
|
||||
AetherArena scenario (ADR-317).
|
||||
149
docs/adr/ADR-301-automatic-domain-calibration.md
Normal file
149
docs/adr/ADR-301-automatic-domain-calibration.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# ADR-301: Automatic domain calibration — signed, versioned, invalidatable room fingerprint
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: calibration, provenance, drift, evidence, honesty, substrate
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is primitive 1 of the perception-substrate program (ADR-300) and the
|
||||
first brick of that program's "certificate spine" (ADR-300 phase 1). It depends
|
||||
on the canonical spatial ontology (ADR-306) to name *which space* it
|
||||
characterizes, on authenticated sensor identity (ADR-305) to bind a fingerprint
|
||||
to *which signed device* produced it, and on the witness chain (ADR-319) to
|
||||
anchor the resulting artifact. Its output is consumed directly by
|
||||
out-of-distribution detection (ADR-302).
|
||||
|
||||
WiFi sensing is only reproducible inside the environment it was tuned for.
|
||||
Multipath, furniture geometry, transceiver placement, and AP channel all shape
|
||||
the CSI distribution, so a model that reads a room correctly one week can drift
|
||||
silently the next. RuView already has the raw ingredients for room-aware
|
||||
sensing but not a single portable, signed, expiring artifact that says "this is
|
||||
the room, here is when it was measured, and here is the evidence that it is
|
||||
still the same room."
|
||||
|
||||
Existing scaffolding to build on, not rebuild (`v2/crates/wifi-densepose-calibration`):
|
||||
|
||||
- `enrollment` / `anchor` — guided human anchors with an adaptive quality gate.
|
||||
- `bank` / `specialist` / `runtime` — a versioned bank of small specialist
|
||||
models and a confidence-gated mixture runtime (`RoomState`), including the
|
||||
crate's existing honest `STALE` degradation when the ADR-135 empty-room
|
||||
baseline drifts.
|
||||
- `geometry` / `geometry_embedding` — transceiver-geometry record and its
|
||||
fixed-length conditioning featurization (ADR-152).
|
||||
|
||||
What is missing is (a) an *automatic* observe-only characterization phase that
|
||||
does not require a human enrollment ritual, (b) empty-vs-occupied baseline
|
||||
separation as a first-class pair, (c) a signed, versioned, comparable
|
||||
`CalibrationCertificate` artifact, and (d) explicit invalidation on drift rather
|
||||
than a soft `STALE` flag buried in the runtime.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Keep calibration internal to the runtime (status quo).** Rejected: the
|
||||
room characterization exists only as in-process state; it cannot be signed,
|
||||
shipped, compared across time, or presented as evidence to ADR-302/ADR-318.
|
||||
2. **Build a new calibration crate.** Rejected: `wifi-densepose-calibration`
|
||||
already owns enrollment, the specialist bank, geometry embedding, and the
|
||||
baseline-drift concept. A parallel crate would fork the room model.
|
||||
3. **Extend `wifi-densepose-calibration` with an automatic characterization
|
||||
phase and a signed certificate artifact.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Extend `v2/crates/wifi-densepose-calibration` with an `autocal` characterization
|
||||
phase and a `certificate` artifact module. The target UX is:
|
||||
|
||||
> install → observe (~10 min) → room fingerprint → calibration certificate →
|
||||
> sensing.
|
||||
|
||||
### 1. Automatic characterization (`autocal`)
|
||||
|
||||
- An observe-only pass (default ~10 minutes, configurable) that collects CSI
|
||||
without requiring guided human anchors, reusing the `anchor` quality gate to
|
||||
reject frames it cannot trust. It layers on the existing ADR-135 empty-room
|
||||
baseline rather than replacing it.
|
||||
- Produces a `RoomFingerprint`: a bounded, fixed-length statistical summary of
|
||||
the room's CSI distribution (subcarrier amplitude/phase moments, multipath
|
||||
structure, occupancy-band energy), plus the `geometry_embedding` when a
|
||||
geometry record is present. The fingerprint is the distance-comparable object
|
||||
ADR-302 measures against; its schema is versioned.
|
||||
|
||||
### 2. Empty / occupied baseline pair
|
||||
|
||||
- Characterization establishes a paired baseline: an **empty** distribution
|
||||
(no occupant motion) and an **occupied** distribution (motion present),
|
||||
separated by the existing occupancy signal rather than a manual label. Both
|
||||
are stored on the fingerprint so downstream OOD gating can distinguish "the
|
||||
empty room changed" (furniture/geometry drift) from "occupancy statistics
|
||||
changed" (different subject dynamics).
|
||||
|
||||
### 3. `CalibrationCertificate` artifact
|
||||
|
||||
- A serializable `CalibrationCertificate` binding: the `RoomFingerprint`; a
|
||||
space identifier from the ADR-306 ontology; the signing sensor identity from
|
||||
ADR-305; `captured_at_unix_s`; a monotonic `version`; a schema version; the
|
||||
calibration `tier`; and an `EvidenceLevel` (L0–L5, ADR-282) — an automatic
|
||||
characterization on real captured CSI is at most L1/L2 and is labelled as
|
||||
such, never L3+.
|
||||
- The certificate is **signed** using RuField provenance/signature types
|
||||
(ADR-260/262/277/279) and anchored in the witness chain (ADR-319). Signature
|
||||
and witness anchoring are mandatory: an unsigned certificate is not a valid
|
||||
certificate.
|
||||
- Two certificates for the same space are **comparable**: `distance(a, b)`
|
||||
returns a bounded fingerprint distance, which is the primitive ADR-302 uses
|
||||
to gate KNOWN → DEGRADED → UNKNOWN.
|
||||
|
||||
### 4. Invalidation and continuous drift compensation
|
||||
|
||||
- A certificate carries an explicit validity policy: it is invalidated when
|
||||
fingerprint distance against live traffic exceeds a threshold, when the AP
|
||||
channel or transceiver geometry changes, when the signing device identity
|
||||
changes, or on age expiry. Invalidation is an explicit state transition that
|
||||
emits a witness record (ADR-319), not a silent `STALE` flag.
|
||||
- Continuous drift compensation runs as a bounded online update of the
|
||||
fingerprint within a **compatibility envelope**: small drift is absorbed and
|
||||
logged; drift beyond the envelope invalidates the certificate and forces
|
||||
re-characterization. Compensation never silently rewrites a signed
|
||||
certificate — it produces a new version, preserving the append-only history.
|
||||
|
||||
### Provenance and honesty discipline
|
||||
|
||||
- No accuracy number is claimed by this ADR; it delivers the artifact and the
|
||||
distance/invalidation machinery. Any certificate produced from generated CSI
|
||||
is L0/`Synthetic` by construction; the constructor rejects labelling
|
||||
synthetic characterization as measured (ADR-279 invariant 6, ADR-282 ladder).
|
||||
- Certificates never leave the edge except through the governed control plane
|
||||
(ADR-277); a room fingerprint is treated as potentially sensitive spatial
|
||||
data, not free telemetry.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Room characterization becomes a portable, signed, versioned artifact that
|
||||
ADR-302 (OOD), ADR-318 (capability certificates), and ADR-317 (benchmark)
|
||||
can consume without re-deriving room state.
|
||||
- The automatic observe-only path lowers deployment friction (no mandatory
|
||||
enrollment ritual) but yields a weaker evidence level than guided enrollment;
|
||||
the certificate states which path produced it so consumers can weight it.
|
||||
- Explicit invalidation means RuView will sometimes refuse to sense a changed
|
||||
room until re-characterization. That refusal is the intended honest behavior,
|
||||
surfaced by ADR-302, not a regression.
|
||||
- The existing enrollment/bank/runtime path is preserved; `autocal` is an
|
||||
additional entry point that produces the same `RoomFingerprint` object the
|
||||
guided path can also emit.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p wifi-densepose-calibration` — fingerprint determinism from
|
||||
fixed synthetic CSI; empty/occupied separation on synthetic occupancy;
|
||||
certificate signing/verification round-trip and tamper rejection;
|
||||
`distance()` monotonicity on progressively perturbed fixtures; invalidation
|
||||
transitions (channel change, geometry change, age, drift-envelope breach)
|
||||
each emit the expected witness record; constructor rejects synthetic→measured
|
||||
mislabeling.
|
||||
- Cross-ADR: an ADR-302 test consumes a certificate and asserts the gating
|
||||
state transitions on a drifted fingerprint.
|
||||
- Real-silicon characterization (ESP32 capture over a real 10-minute window)
|
||||
remains a follow-up requiring hardware evidence per CLAUDE.md; a successful
|
||||
build or synthetic run is not hardware evidence.
|
||||
135
docs/adr/ADR-302-out-of-distribution-detection.md
Normal file
135
docs/adr/ADR-302-out-of-distribution-detection.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# ADR-302: Out-of-distribution detection — KNOWN / DEGRADED / UNKNOWN gating
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: ood, calibration, uncertainty, quality, evidence, honesty, substrate
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is primitive 2 of the perception-substrate program (ADR-300) and part
|
||||
of the phase-1 certificate spine. It sits directly downstream of automatic
|
||||
domain calibration (ADR-301): the `CalibrationCertificate` and its
|
||||
`RoomFingerprint` are the reference distribution this ADR measures against. It
|
||||
reuses fusion-layer quality scoring (ADR-137) as one of its inputs and feeds
|
||||
its state into the evidence engine (ADR-304) and capability certificates
|
||||
(ADR-318).
|
||||
|
||||
The central unsolved problem of WiFi sensing is cross-domain generalization: a
|
||||
model trained (or calibrated) in one room degrades unpredictably in another, or
|
||||
in the same room after furniture moves, the AP changes channel, or the radio
|
||||
hardware is swapped. A model that keeps returning confident classifications
|
||||
under these conditions is the single most misleading failure mode in the field,
|
||||
and it is the failure the strategic assessment (ADR-300) named explicitly.
|
||||
Confidence alone is insufficient: a softmax head is perfectly capable of being
|
||||
confidently wrong on out-of-distribution input. RuView must be able to say
|
||||
"I do not recognize this situation" instead of guessing.
|
||||
|
||||
Today RuView has partial signals but no unified gate:
|
||||
|
||||
- ADR-301 produces a comparable `RoomFingerprint` and a `distance()` metric.
|
||||
- ADR-137 `QualityScore` carries fusion coherence, evidence references, and
|
||||
contradiction flags per fused frame.
|
||||
- Model heads emit confidence/uncertainty, but nothing combines domain
|
||||
distance, signal quality, calibration compatibility, and uncertainty into a
|
||||
single decision, and nothing forces a model to stop emitting confident labels
|
||||
when it leaves its calibrated domain.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Threshold on model confidence alone.** Rejected: confidently-wrong OOD
|
||||
predictions are exactly the failure mode; confidence is necessary but not
|
||||
sufficient.
|
||||
2. **A per-model bespoke OOD check inside each task head.** Rejected:
|
||||
duplicates logic, cannot be audited uniformly, and does not compose with the
|
||||
calibration certificate or the evidence engine.
|
||||
3. **A shared OOD gate that every inference passes through, fusing four signals
|
||||
against the ADR-301 certificate.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Add an out-of-distribution gate — implemented in a shared crate consumed by the
|
||||
task-head runtime (`wifi-densepose-calibration::runtime` and the model serving
|
||||
path) — that attaches a `DomainState` to **every** inference.
|
||||
|
||||
### 1. Four inputs, one decision
|
||||
|
||||
Each inference carries four measured quantities:
|
||||
|
||||
1. **Domain distance** — fingerprint distance (ADR-301 `distance()`) between
|
||||
live traffic and the active `CalibrationCertificate`, split into the
|
||||
empty-baseline and occupied-baseline components so geometry drift and
|
||||
occupancy-statistics drift are distinguishable.
|
||||
2. **Signal quality** — reuse the ADR-137 quality scoring signals (fusion
|
||||
coherence, contradiction flags) plus per-frame SNR/validity.
|
||||
3. **Calibration compatibility** — is a valid, non-invalidated certificate
|
||||
present for this space (ADR-306) and this signed device (ADR-305)? An
|
||||
expired, invalidated, or device-mismatched certificate is itself a
|
||||
compatibility failure.
|
||||
4. **Uncertainty** — the model head's own predictive uncertainty.
|
||||
|
||||
### 2. State machine: KNOWN → DEGRADED → UNKNOWN
|
||||
|
||||
- **KNOWN** — domain distance within the certificate's compatibility envelope,
|
||||
quality above threshold, certificate valid and compatible, uncertainty low.
|
||||
Confident classifications are returned.
|
||||
- **DEGRADED** — one or more signals crossed a soft threshold (e.g. moderate
|
||||
fingerprint drift within the envelope, elevated uncertainty, a tolerated
|
||||
ADR-137 contradiction flag). Classifications are returned but flagged
|
||||
degraded with the specific reason; downstream consumers must treat them as
|
||||
lower-evidence.
|
||||
- **UNKNOWN** — the room changed materially (empty-baseline drift beyond the
|
||||
envelope, AP channel change, transceiver-geometry change, hardware/device
|
||||
change, or an invalidated/absent certificate). RuView **stops returning
|
||||
confident classifications** and returns UNKNOWN with the triggering cause.
|
||||
This is the required behavior, not an error.
|
||||
|
||||
State transitions are hysteretic (separate enter/exit thresholds) so the gate
|
||||
does not flap on noise. The state, the four input values, and the triggering
|
||||
cause are all reported — never a bare label.
|
||||
|
||||
### 3. Certificate-bound, honest by construction
|
||||
|
||||
- The gate is meaningless without a certificate: with no valid ADR-301
|
||||
certificate for the current space/device, the default state is UNKNOWN, not
|
||||
KNOWN. Absence of evidence is treated as absence of capability.
|
||||
- The `DomainState` and its inputs are emitted to the evidence engine
|
||||
(ADR-304) as part of every inference record, and are an input to the ADR-318
|
||||
capability certificate (a model's capability is bounded by the domain it can
|
||||
hold KNOWN in).
|
||||
- No accuracy number is claimed here; the ADR delivers the gating machinery.
|
||||
The gate's own thresholds are calibration parameters, reported with each
|
||||
decision.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView gains a uniform, auditable answer to "should I trust this inference?"
|
||||
that combines domain, quality, calibration, and uncertainty rather than
|
||||
confidence alone.
|
||||
- Deployments will see more DEGRADED/UNKNOWN results than a
|
||||
confidence-only system, especially right after a room changes. That increase
|
||||
is the product working: it is the difference between honest RF perception and
|
||||
confidently-wrong output.
|
||||
- Every task head that opts into the substrate must route through the gate;
|
||||
heads that bypass it cannot claim a KNOWN state or earn an ADR-318
|
||||
certificate.
|
||||
- The gate couples model serving to the presence of a live calibration
|
||||
certificate, making ADR-301 a hard dependency of confident inference — the
|
||||
intended coupling.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test` on the OOD crate — state-machine transitions on synthetic
|
||||
fixtures: in-envelope drift stays KNOWN; soft-threshold breach → DEGRADED;
|
||||
empty-baseline drift beyond envelope, channel change, geometry change,
|
||||
device mismatch, and invalidated/absent certificate each → UNKNOWN;
|
||||
hysteresis prevents flapping under injected noise; missing certificate
|
||||
defaults to UNKNOWN.
|
||||
- Cross-ADR: consumes an ADR-301 certificate and asserts a drifted fingerprint
|
||||
drives the expected transition; asserts the `DomainState` is present on every
|
||||
emitted inference record consumed by ADR-304.
|
||||
- No confident classification is emitted in the UNKNOWN state in any test —
|
||||
enforced as an assertion, not a convention.
|
||||
- Real-silicon OOD behavior (moving furniture / changing AP channel on a live
|
||||
ESP32 capture and observing the transition) remains a follow-up requiring
|
||||
hardware evidence per CLAUDE.md.
|
||||
125
docs/adr/ADR-303-ground-truth-synchronization.md
Normal file
125
docs/adr/ADR-303-ground-truth-synchronization.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# ADR-303: Ground-truth synchronization — reference sensors as a formal validation plane
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 2)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: ground-truth, validation, fusion, evidence, benchmark, honesty, substrate
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is primitive 3 of the perception-substrate program (ADR-300), authored
|
||||
as **Proposed** in phase 2: it is design intent and a validation plan, not
|
||||
implemented by the phase-1 swarm. It sits on top of the phase-1 certificate
|
||||
spine and feeds the evidence engine (ADR-304) and the real benchmark service
|
||||
(ADR-317). It generalizes the vitals ground-truth rig (ADR-293) from a single
|
||||
measurand to a modality-agnostic plane.
|
||||
|
||||
RuView's evidence discipline (CLAUDE.md; ADR-282 ladder) requires MEASURED
|
||||
accuracy claims to be backed by an independent reference. ADR-293 built exactly
|
||||
this for vitals: reference-series ingest, time alignment (cross-correlation
|
||||
lag + optional clock-drift fit), and agreement statistics (MAE/RMSE/bias/
|
||||
Bland–Altman/within-tolerance), with an `EvidenceGrade` that is only
|
||||
constructible as `Measured` when a real reference, non-zero paired samples,
|
||||
minimum coverage, and a reproducer are present. That machinery is measurand- and
|
||||
device-shaped: it knows about heart rate and breathing rate.
|
||||
|
||||
The substrate needs the same discipline for *every* phenomenon RuView senses —
|
||||
presence, count, localization, pose, posture, activity — and for reference
|
||||
sources of many modalities (cameras, mmWave, pressure mats, wearables, pulse
|
||||
oximeters, microphones, manual labels). The critical design decision is that
|
||||
these reference sensors form a **validation plane**, not additional inference
|
||||
inputs.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Fuse reference sensors as extra inference inputs.** Rejected on principle:
|
||||
folding cameras/mmWave into the estimator would make RuView's RF claims
|
||||
unfalsifiable — the reference would be training the thing it is meant to
|
||||
check, and a camera-fed result is no longer a camera-free RF result. It
|
||||
would also violate the ADR-282 layering (RuView is probabilistic
|
||||
exteroception, never ground truth) and the honesty rule against presenting
|
||||
fused-with-camera output as WiFi sensing.
|
||||
2. **One-off rigs per measurand (extend ADR-293 ad hoc each time).** Rejected:
|
||||
duplicates alignment/agreement code per phenomenon and never yields a shared
|
||||
validation surface for the benchmark.
|
||||
3. **A first-class, modality-agnostic `GroundTruth` API that is strictly a
|
||||
validation plane.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce a `GroundTruth` API — a modality-agnostic validation plane that
|
||||
compares RF inference against independent observation and never feeds it.
|
||||
|
||||
### 1. Modality-agnostic reference ingest
|
||||
|
||||
- A `ReferenceObservation` generalizing ADR-293's `ReferenceSeries`: a
|
||||
timestamped, typed observation of a `Phenomenon` (presence, count,
|
||||
localization, pose keypoints, posture, activity, heart rate, breathing rate)
|
||||
from a `ReferenceModality` (camera, mmWave, pressure, wearable, pulse
|
||||
oximeter, microphone, manual label), with device/source metadata and the
|
||||
measurement principle recorded.
|
||||
- Untrusted reference files are validated at the boundary (row-numbered
|
||||
rejections, non-monotonic timestamps are errors), reusing ADR-293's ingest
|
||||
discipline. Camera/mmWave references arrive as exported label/keypoint
|
||||
streams, not live model feeds.
|
||||
|
||||
### 2. Synchronization
|
||||
|
||||
- Generalize ADR-293's time alignment (bounded-lag normalized cross-correlation
|
||||
+ optional linear clock-drift fit) to arbitrary measurands on a common
|
||||
resampled grid, with no interpolation across gaps beyond a configurable
|
||||
limit. Alignment parameters are always reported, never silently applied.
|
||||
- Spatial synchronization where relevant: reference observations are expressed
|
||||
in the ADR-306 spatial ontology so an RF localization/pose result and a
|
||||
camera/mmWave observation are compared in one coordinate frame.
|
||||
|
||||
### 3. Agreement as validation, not fusion
|
||||
|
||||
- A modality-appropriate `AgreementReport` per phenomenon: continuous
|
||||
measurands reuse ADR-293's MAE/RMSE/bias/Bland–Altman/within-tolerance;
|
||||
categorical/detection phenomena (presence, activity) report confusion-matrix
|
||||
metrics; spatial phenomena report localization error percentiles and pose
|
||||
PCK **with the mandatory mean-pose baseline and leakage-free split**
|
||||
(CLAUDE.md; ADR-291).
|
||||
- Session scope is mandatory metadata (subject count, motion state, LOS/NLOS/
|
||||
through-wall, distance band) — a report without scope cannot be constructed,
|
||||
as in ADR-293.
|
||||
|
||||
### 4. Evidence and isolation guarantees
|
||||
|
||||
- The plane is one-directional by type: the inference path has no read access
|
||||
to `GroundTruth` at runtime. A build/test-time isolation check (and the type
|
||||
boundary) prevents a reference observation from becoming an estimator input.
|
||||
- Reports carry an `EvidenceLevel` (ADR-282) and an `EvidenceGrade`
|
||||
constructible as `Measured` only with a real reference, paired samples,
|
||||
coverage, and a reproducer (ADR-293 rule). Reports feed the ADR-304 evidence
|
||||
engine and are the substrate ADR-317 scores against.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every phenomenon RuView senses gets the same MEASURED-vs-independent-observer
|
||||
discipline vitals already has, in one shared surface.
|
||||
- Keeping references strictly as validation preserves the falsifiability and
|
||||
the camera-free identity of RF results; it costs the (tempting) accuracy a
|
||||
camera-fused estimator would show, which is the correct trade.
|
||||
- Reference capture is an operational burden (a camera/mmWave rig per validated
|
||||
session); acceptable because it is a validation activity, not a runtime
|
||||
requirement, and it is what turns CLAIMED into MEASURED.
|
||||
- Because this is Proposed (phase 2), the API shape may be revised once the
|
||||
phase-1 spine (ADR-301/299/301/303) lands and the benchmark (ADR-317)
|
||||
exercises it.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests (planned): modality-agnostic ingest rejection cases; alignment
|
||||
recovery of known synthetic offsets/drifts across measurands; agreement math
|
||||
per phenomenon against hand-computed fixtures; pose PCK path requires a
|
||||
mean-pose baseline and rejects leaky splits; evidence-grade constructibility;
|
||||
the isolation check fails a build that wires a reference into the inference
|
||||
path.
|
||||
- Cross-ADR: an ADR-317 benchmark scenario consumes `GroundTruth` reports as
|
||||
its scored reference; ADR-304 ingests the agreement reports as evidence
|
||||
records.
|
||||
- Real-session validation (RF capture synchronized with a real camera/mmWave/
|
||||
pressure/wearable reference) is the phase-2 exit and requires hardware
|
||||
evidence per CLAUDE.md; a synthetic run is not hardware evidence.
|
||||
117
docs/adr/ADR-304-evidence-engine.md
Normal file
117
docs/adr/ADR-304-evidence-engine.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# ADR-304: Evidence engine — MLflow for physical sensing
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: evidence, provenance, ledger, accuracy, drift, benchmark, honesty, substrate
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is primitive 4 of the perception-substrate program (ADR-300) and a
|
||||
central pillar of the phase-1 certificate spine. It consumes the domain state
|
||||
from out-of-distribution detection (ADR-302) and the calibration age from the
|
||||
calibration certificate (ADR-301), it is the store that capability certificates
|
||||
(ADR-318) are minted from, and it is the accuracy source the real benchmark
|
||||
service (ADR-317) reads. In phase 2 it ingests agreement reports from the
|
||||
ground-truth plane (ADR-303).
|
||||
|
||||
The strategic assessment (ADR-300) judged this primitive **more commercially
|
||||
important than another pose architecture**: what unblocks OEM and integrator
|
||||
conversations is not a higher headline number but a defensible, auditable record
|
||||
of how a model actually performs, per room, per device, per subject, over time.
|
||||
MLflow made ML experiments trackable; physical sensing needs the equivalent for
|
||||
deployed accuracy, drift, and evidence level — an append-only ledger, not a
|
||||
dashboard that overwrites yesterday's number.
|
||||
|
||||
RuView already has the constituent evidence types; what is missing is the ledger
|
||||
that unifies them per deployment context:
|
||||
|
||||
- RuField provenance/signature types (ADR-260/262/277/279) — the signed,
|
||||
provenance-bearing record types to reuse rather than reinvent.
|
||||
- The AetherArena witness-ledger pattern (ADR-149) — an append-only,
|
||||
witness-anchored ledger of scored results, the structural template here.
|
||||
- `frame::EvidenceLevel` L0–L5 (ADR-282) — the mandatory evidence tag every
|
||||
record carries.
|
||||
- ADR-302 `DomainState`, ADR-137 `QualityScore`, ADR-301 certificate version
|
||||
and age — the per-inference signals to accumulate.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Log accuracy to flat files / metrics dashboards.** Rejected: mutable,
|
||||
un-signed, un-scoped, and not comparable over time — the exact gap.
|
||||
2. **Reuse a general experiment tracker (MLflow itself).** Rejected: it is
|
||||
experiment-time, not deployment-time; it has no notion of room/device/
|
||||
subject context, calibration age, evidence level, or signed provenance, and
|
||||
it would add an external service dependency contrary to the substrate's
|
||||
edge-first, dependency-light direction.
|
||||
3. **A native append-only evidence ledger reusing RuField record types and the
|
||||
AetherArena ledger pattern.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Build an **evidence engine**: a per-`(room, device, subject)` append-only
|
||||
accuracy ledger that every model automatically writes to.
|
||||
|
||||
### 1. The evidence record
|
||||
|
||||
- An `EvidenceRecord` keyed by context — space id (ADR-306), signed device id
|
||||
(ADR-305), and subject id where consented and available — carrying: model
|
||||
version; calibration certificate version and **age** (ADR-301); the ADR-302
|
||||
`DomainState` (KNOWN/DEGRADED/UNKNOWN) and its four inputs; the ADR-137
|
||||
quality signals; predictive uncertainty; and, when a reference is present
|
||||
(ADR-303), the agreement result (accuracy, false-positive rate). Each record
|
||||
carries exactly one `EvidenceLevel` (L0–L5, ADR-282).
|
||||
- Records are **append-only** and signed with RuField signature types
|
||||
(ADR-260/262/277/279); the ledger is anchored in the witness chain (ADR-319),
|
||||
following the AetherArena witness-ledger pattern (ADR-149). No record is ever
|
||||
mutated in place — a correction is a new record.
|
||||
|
||||
### 2. Per-context accuracy accounting
|
||||
|
||||
- The engine maintains, per `(room, device, subject)` context: measured
|
||||
accuracy (only where an ADR-303 reference backs it — otherwise the record is
|
||||
CLAIMED/SYNTHETIC, never MEASURED), false-positive rate, drift trajectory
|
||||
(fingerprint distance over time from ADR-301), the fraction of inferences in
|
||||
each domain state, calibration age distribution, and model-version history.
|
||||
- Aggregation is a pure function over the append-only log at a queried time —
|
||||
the ledger is the source of truth; summaries are derived, never authoritative
|
||||
(mirroring CLAUDE.md's "source over summaries" rule).
|
||||
|
||||
### 3. Honesty enforced in the record
|
||||
|
||||
- The engine cannot upgrade an evidence level; a level is set by the record's
|
||||
provenance at write time (synthetic input → L0/`Synthetic`; no reference →
|
||||
CLAIMED; reference + reproducer → MEASURED), reusing the ADR-282/ADR-291/
|
||||
ADR-293 constructor discipline. A benchmark or certificate reading the ledger
|
||||
gets the honest level, not an optimistic rollup.
|
||||
- No benchmark numbers are invented by this ADR; it delivers the ledger and the
|
||||
accounting. Empty contexts report "no evidence," which downstream (ADR-318)
|
||||
must treat as no capability.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView gains a single auditable answer to "how well does this model actually
|
||||
work, here, on this device, for this subject, and how fresh is the
|
||||
calibration?" — the artifact OEM/integrator diligence actually asks for.
|
||||
- ADR-318 capability certificates become derivable (a certificate is a signed
|
||||
attestation over a slice of the ledger) and ADR-317 gains a real accuracy
|
||||
source per PR instead of self-reported numbers.
|
||||
- The append-only, signed design has storage and key-management cost; bounded
|
||||
by per-context retention policy and by reusing the existing RuField/witness
|
||||
infrastructure rather than a new store.
|
||||
- Some contexts will show sparse or unflattering evidence. Surfacing that is the
|
||||
point; the engine must never paper over a thin context with a global average.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test` on the evidence-engine crate — append-only invariant (no
|
||||
in-place mutation; corrections are new records); per-context aggregation math
|
||||
against fixtures; evidence-level is set by provenance and cannot be upgraded;
|
||||
signature round-trip and tamper rejection; witness anchoring; empty-context
|
||||
queries return "no evidence" not a fabricated number.
|
||||
- Cross-ADR: ingests ADR-302 `DomainState` and (phase 2) ADR-303 agreement
|
||||
reports; an ADR-318 test mints a certificate from a ledger slice and an
|
||||
ADR-317 test reads accuracy from the ledger.
|
||||
- Real-deployment evidence (a populated ledger from live ESP32 captures with
|
||||
ADR-303 references) is the maturity milestone and requires hardware evidence
|
||||
per CLAUDE.md; a synthetic ledger is L0 by construction.
|
||||
147
docs/adr/ADR-305-authenticated-sensor-identity.md
Normal file
147
docs/adr/ADR-305-authenticated-sensor-identity.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# ADR-305: Authenticated sensor identity — RF chain of custody
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: security, identity, provenance, sensor-ingest, attestation, phase-1
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** (perception substrate program) and owns
|
||||
primitive #5, *authenticated sensor identity*. In the ADR-300 dependency DAG it
|
||||
is a spine root that, together with **ADR-306** (canonical spatial ontology),
|
||||
feeds **ADR-301** (calibration certificate) and **ADR-319** (witness chain).
|
||||
|
||||
RuView's inference outputs are only as trustworthy as the measurements that
|
||||
produced them, yet today a measurement's origin is essentially assertional. The
|
||||
UDP data plane accepts frames from any reachable host: **ADR-296** shipped step
|
||||
one — a loopback-default bind (`--udp-bind`) and an optional source
|
||||
IP/CIDR allowlist — and explicitly deferred to a follow-up ADR "per-device
|
||||
provisioned keys, MAC/AEAD, device identifiers, monotonic sequence numbers,
|
||||
freshness window, and replay rejection." **This ADR is that step two.** ADR-296
|
||||
correctly documented that an IP allowlist does not stop LAN spoofing; a
|
||||
cryptographic device identity is what closes that gap.
|
||||
|
||||
Foundations already exist in the tree and must be reused rather than rebuilt:
|
||||
|
||||
- `wifi-densepose-rufield` provides `DeviceId`, `Signature`, `SignatureBlock`,
|
||||
`FrameProvenance`, `ProvenanceClass`, and `SignatureVerifyError` — the type
|
||||
vocabulary for a signed frame.
|
||||
- `wifi-densepose-bfld` provides `CapabilityAttestation` and
|
||||
`PrivacyAttestationProof` (BFLD attestation, ADR-141) — the device-side
|
||||
attestation surface.
|
||||
- **ADR-295** defines the source-provenance state machine and freshness
|
||||
(`SpatialStateFreshness`); a monotonic sequence and freshness window slot
|
||||
into that machine rather than duplicating it.
|
||||
|
||||
The gap is not new primitives but an **end-to-end chain of custody**: a frame
|
||||
must be traceable as `device → signed measurement → sequence → timestamp →
|
||||
calibration → inference → signed event`, with every link verified at the
|
||||
ingest boundary per CLAUDE.md ("validate untrusted input at every network,
|
||||
hardware, and FFI boundary; default to least authority").
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Stop at ADR-296 (bind + IP allowlist).** Rejected: ADR-296 itself names
|
||||
this insufficient on a trusted LAN; any on-subnet host can still spoof a
|
||||
device.
|
||||
2. **TLS/DTLS transport authentication only.** Rejected: authenticates the
|
||||
*channel*, not the *measurement*. It does not survive store-and-forward,
|
||||
does not bind a sequence number into the signed object, and gives the
|
||||
downstream evidence/witness layers nothing to re-verify offline.
|
||||
3. **Per-device signing keys with a signed measurement envelope, monotonic
|
||||
sequence, and freshness window, reusing the RuField/BFLD types.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce an **authenticated frame envelope** carried through the sensing
|
||||
server, built from existing RuField/BFLD types.
|
||||
|
||||
### 1. Per-device provisioned identity
|
||||
|
||||
- Each radio (ESP32-S3/C6 node or adapter) is provisioned with a keypair; the
|
||||
device holds the private key, the server holds the enrolled public key bound
|
||||
to a `DeviceId`. Provisioning is an explicit, authorized enrollment step — a
|
||||
device is untrusted until an operator enrolls its public key. Private keys are
|
||||
never logged or committed (CLAUDE.md credential rule); the ESP32 side follows
|
||||
`firmware/esp32-csi-node` key-handling notes.
|
||||
- The enrollment record binds `DeviceId → public key → capabilities`
|
||||
(via `CapabilityAttestation`, ADR-141), so a device can only assert
|
||||
measurements for phenomena it is attested to sense. This is what **ADR-318**
|
||||
(capability certificate) later consumes.
|
||||
|
||||
### 2. Signed measurement envelope
|
||||
|
||||
- A frame on the wire becomes a `SignatureBlock` over the canonical
|
||||
serialization of `{DeviceId, sequence, timestamp, measurement-hash}`. The
|
||||
measurement itself (CSI/CIR payload) is covered by the hash so tampering is
|
||||
detectable without embedding the whole payload twice.
|
||||
- Verification uses `Signature`/`SignatureVerifyError` from
|
||||
`wifi-densepose-rufield`. A frame that fails signature verification is
|
||||
dropped and counted, exactly as ADR-296 drops disallowed sources — an `Err`
|
||||
at the boundary, never a warning that proceeds.
|
||||
|
||||
### 3. Monotonic sequence + freshness (replay defense)
|
||||
|
||||
- Each device maintains a strictly monotonic per-device sequence number. The
|
||||
server tracks the last accepted sequence per `DeviceId`; a non-increasing
|
||||
sequence is rejected as a replay.
|
||||
- A freshness window bounds `timestamp` against the server clock skew budget;
|
||||
stale frames are rejected. This reuses ADR-295's `SpatialStateFreshness`
|
||||
rather than inventing a parallel notion of staleness, and composes with
|
||||
ADR-297's stale-node handling.
|
||||
|
||||
### 4. Chain of custody into the event
|
||||
|
||||
- On successful verification the frame's `FrameProvenance` records the verified
|
||||
`DeviceId`, sequence, and timestamp. Calibration (ADR-301) and inference
|
||||
annotate their transforms, and the emitted spatial event (ADR-306 ontology)
|
||||
carries a signed provenance lineage. `ProvenanceClass` still enforces the
|
||||
synthetic/measured invariant from ADR-282/ADR-279 (invariant 6): a measured
|
||||
chain of custody can never be aliased to synthetic and vice-versa.
|
||||
- This end-to-end signed lineage is the substrate the **ADR-319** witness chain
|
||||
serializes and the **ADR-318** capability certificate points at as evidence.
|
||||
|
||||
### Compatibility
|
||||
|
||||
- The envelope is **opt-in per deployment** and negotiated at enrollment. An
|
||||
un-enrolled single-node desktop deployment keeps working unauthenticated
|
||||
behind ADR-296's loopback default; a routable, multi-node, or fleet
|
||||
deployment (ADR-316) requires enrolled identities. The startup security log
|
||||
(ADR-296) is extended to state whether frame authentication is active.
|
||||
|
||||
## Consequences
|
||||
|
||||
- LAN spoofing and replay — the residual risks ADR-296 named plainly — are
|
||||
closed for enrolled deployments. The measurement, not merely the channel, is
|
||||
authenticated, so the guarantee survives store-and-forward into the witness
|
||||
chain.
|
||||
- Enrollment/key-management is now an operational responsibility (provisioning,
|
||||
rotation, revocation). This is documented as a deployment step; key rotation
|
||||
and revocation lists are specified here but their fleet distribution is
|
||||
owned by ADR-316.
|
||||
- Signature verification adds per-frame CPU cost at ingest; bounded and
|
||||
measured in validation below. It is a deliberate cost for a verifiable chain
|
||||
of custody.
|
||||
- A schema addition to the frame contract; un-enrolled deployments are
|
||||
unaffected, and the migration accessor mirrors ADR-297's approach.
|
||||
- **No spoof-resistance claim is MEASURED until validated on real silicon**
|
||||
(CLAUDE.md hardware rule): a passing unit/integration suite demonstrates the
|
||||
logic, not the fielded device path.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests (`cargo test -p wifi-densepose-sensing-server`,
|
||||
`-p wifi-densepose-rufield`): valid envelope accepted; bad signature
|
||||
rejected and counted; non-monotonic sequence rejected as replay; out-of-
|
||||
window timestamp rejected; un-enrolled `DeviceId` rejected; measured/synthetic
|
||||
provenance aliasing rejected (ADR-279 invariant 6).
|
||||
- Integration test: a captured/synthesized multi-frame stream produces a
|
||||
verifiable `device → … → signed event` lineage that ADR-319 can serialize and
|
||||
re-verify offline.
|
||||
- Benchmark (`cargo bench`): per-frame verification cost, to bound ingest
|
||||
overhead.
|
||||
- **Real-silicon evidence required** before any deployment-grade
|
||||
authentication claim: a captured boot/runtime log from an enrolled ESP32 node
|
||||
signing frames end-to-end. A successful build or simulator run is not
|
||||
hardware evidence.
|
||||
142
docs/adr/ADR-306-canonical-spatial-ontology.md
Normal file
142
docs/adr/ADR-306-canonical-spatial-ontology.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# ADR-306: Canonical spatial ontology — one Site→…→Event model for every surface
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: ontology, worldgraph, schema, mqtt, matter, rufield, phase-1
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** and owns primitive #6, *canonical spatial
|
||||
ontology*. In the ADR-300 DAG it is a spine root alongside **ADR-305**
|
||||
(authenticated identity) and feeds every downstream primitive that must speak
|
||||
about *where* and *what*: **ADR-301** (calibration), **ADR-307** (tracking,
|
||||
consumes `Track`/`Person`), **ADR-319** (witness chain), and every external
|
||||
surface named in the ADR-300 consequences (MQTT, REST, WebSocket, RuField,
|
||||
Matter, agents).
|
||||
|
||||
RuView currently expresses "where something is" in several overlapping,
|
||||
per-surface schemas: the MQTT/Home-Assistant mapper has its own node/room
|
||||
shapes (**ADR-297** just introduced `NodeInference`/`RoomInference` to
|
||||
disambiguate node vs. room state); the `worldgraph` crate models a spatial
|
||||
graph; RuField carries `SemanticProvenance`; Matter/HomeKit has its own area
|
||||
model. The same physical fact — "a person is in the kitchen" — is re-encoded
|
||||
differently on each surface, and the review called for "one canonical
|
||||
`NodeInference`/`RoomInference` contract" (ADR-297 consequences). Without a
|
||||
single semantic model, every new surface multiplies the translation matrix and
|
||||
each translation is a place where provenance and evidence level (ADR-282) can
|
||||
be silently dropped.
|
||||
|
||||
Substantial scaffolding already exists and must be **reused/extended, not
|
||||
rebuilt**. `v2/crates/worldgraph/wifi-densepose-worldgraph` already defines:
|
||||
|
||||
- `WorldNode` variants including `Room { area_id, name, bounds_enu, floor }`,
|
||||
`Zone { parent_room, … }`, `Wall { rf_attenuation_db }`, and `Doorway`.
|
||||
- `WorldEdge` variants including `Observes { quality, last_seen_unix_ms }`,
|
||||
`LocatedIn { since_unix_ms }`, `AdjacentTo { via_doorway }`, and `Supports`.
|
||||
- `WorldGraph`, `WorldGraphSnapshot`, `WorldId`, `SemanticProvenance`,
|
||||
`PersonPosition`, and a HomeCore `area_id` linkage join key (ADR-127).
|
||||
|
||||
The `worldgraph` crate is therefore the natural home for the canonical model.
|
||||
What is missing is (a) the full `Site → Building → Floor → Space → Zone`
|
||||
containment spine above `Room`, (b) first-class `Sensor`, `Object`,
|
||||
`Observation`, `Track`, and `Event` node types, (c) one canonical serialization
|
||||
that every surface consumes, and (d) a documented migration path from the
|
||||
existing per-surface schemas.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Leave each surface with its own schema; add adapters pairwise.** Rejected:
|
||||
O(surfaces²) translations, and provenance/evidence loss at each hop.
|
||||
2. **Invent a new top-level ontology crate.** Rejected: `worldgraph` already
|
||||
models rooms, zones, walls, doorways, observation edges, and HomeCore
|
||||
linkage; a parallel crate would fork the world model.
|
||||
3. **Extend `worldgraph` into the canonical ontology and make every surface a
|
||||
projection of it.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt **one canonical spatial ontology**, hosted in the `worldgraph` crate,
|
||||
that every RuView surface reads from and writes to.
|
||||
|
||||
### 1. The containment spine and entity types
|
||||
|
||||
Define the full node taxonomy as an extension of the existing `WorldNode`:
|
||||
|
||||
```
|
||||
Site ▸ Building ▸ Floor ▸ Space ▸ Zone
|
||||
└─▸ { Sensor, Person, Object,
|
||||
Observation, Track, Event }
|
||||
```
|
||||
|
||||
- `Site`, `Building`, `Floor`, `Space` are new containment `WorldNode`
|
||||
variants above the existing `Room` (mapped to `Space`, keeping its `area_id`
|
||||
and `bounds_enu`) and `Zone`. `Wall`/`Doorway` remain as topological
|
||||
elements. Containment reuses the existing `LocatedIn`/`AdjacentTo` edge
|
||||
vocabulary; a new `PartOf` edge expresses the pure hierarchy
|
||||
(Zone `PartOf` Space `PartOf` Floor …).
|
||||
- `Sensor` is the entity **ADR-305** authenticates (`DeviceId` as its stable
|
||||
identity) and **ADR-320** (HAL, phase 2) describes the hardware of. `Person`,
|
||||
`Object`, `Observation`, `Track`, and `Event` are first-class nodes.
|
||||
`Observes`/`LocatedIn` edges already carry quality and dwell timestamps.
|
||||
- `Track` and `Person` are defined **here** as the ontology contract that
|
||||
**ADR-307** (persistent tracking) produces and updates. `Observation` is what
|
||||
an authenticated frame (ADR-305) becomes after calibration (ADR-301), and
|
||||
`Event` is the governed output that ADR-318 certifies and ADR-319 witnesses.
|
||||
|
||||
### 2. Canonical serialization
|
||||
|
||||
- A single, versioned serialization (serde-based, stable field names) is the
|
||||
one wire/at-rest representation. Every surface — MQTT/Home-Assistant, REST,
|
||||
WebSocket, RuField observations, Matter/HomeKit, agent queries — is a
|
||||
**projection** of this model, not an independent schema. `NodeInference` and
|
||||
`RoomInference` (ADR-297) become projections of `Sensor→Observes` and the
|
||||
`Space`-level fused inference respectively, so ADR-297's node/room separation
|
||||
is preserved by construction rather than re-encoded per surface.
|
||||
- Every node and edge carries `SemanticProvenance` and exactly one
|
||||
`EvidenceLevel` (L0–L5, ADR-282 policy): the evidence ladder travels *with*
|
||||
the fact across every projection, so no surface can silently upgrade or drop
|
||||
it.
|
||||
|
||||
### 3. Migration path
|
||||
|
||||
- Each existing per-surface schema gets a documented, tested bidirectional
|
||||
mapping to/from the canonical model, plus a migration accessor for consumers
|
||||
reading the old shape (mirroring ADR-297's migration accessor). Surfaces are
|
||||
cut over one at a time; a surface is "canonical" once its projection is the
|
||||
only encoder it uses. Until cutover, the mapping layer is authoritative and
|
||||
round-trip-tested so no fact is lost in translation.
|
||||
- The `worldgraph` HomeCore `area_id` linkage (ADR-127) remains the join key
|
||||
between the ontology's `Space` and external area registries.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The translation matrix collapses from O(surfaces²) to O(surfaces): each
|
||||
surface implements one projection. New surfaces (ROS 2, OpenUSD, OPC UA per
|
||||
ADR-282's roadmap) plug in as additional projections.
|
||||
- Provenance and evidence level are carried uniformly; a fact cannot cross a
|
||||
surface boundary and lose its lineage or its L-level.
|
||||
- A schema change reaching every surface; managed by the versioned
|
||||
serialization and per-surface migration accessors. Single-node deployments
|
||||
keep working (one `Sensor`, one `Space`).
|
||||
- The ontology is a *representation*, not an inference engine: it says nothing
|
||||
about *how* a `Track` or `Event` is produced — that is owned by ADR-307,
|
||||
ADR-301, ADR-302, and the model layer. This ADR does not itself make any
|
||||
accuracy claim to grade.
|
||||
- Extending `worldgraph` grows one crate's surface rather than forking a second
|
||||
world model; the geo/worldmodel sub-crates continue to build on the same node
|
||||
vocabulary.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests (`cargo test -p wifi-densepose-worldgraph`): containment-spine
|
||||
construction and invariants (a `Zone` is `PartOf` exactly one `Space`, a
|
||||
`Space` on exactly one `Floor`, etc.); round-trip serialization of every node
|
||||
and edge type; every node/edge carries exactly one `EvidenceLevel`.
|
||||
- Migration tests: each per-surface schema maps to the canonical model and back
|
||||
with no loss of provenance or evidence level; `NodeInference`/`RoomInference`
|
||||
(ADR-297) project and re-project identically.
|
||||
- Contract test: a single canonical `Event` renders correctly through the MQTT,
|
||||
REST, and WebSocket projections from one source of truth.
|
||||
- No accuracy numbers are claimed; this ADR delivers the shared representation
|
||||
the rest of the phase-1 spine writes into.
|
||||
135
docs/adr/ADR-307-persistent-identity-tracking.md
Normal file
135
docs/adr/ADR-307-persistent-identity-tracking.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# ADR-307: Persistent identity & tracking — privacy-preserving probabilistic tracks
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 2)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: tracking, identity, privacy, fusion, worldgraph, phase-2
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** and owns primitive #7, *persistent identity
|
||||
& tracking*. In the ADR-300 DAG it is a phase-2 primitive sitting on the
|
||||
phase-1 spine: it **consumes the ADR-306 ontology** (producing and updating the
|
||||
`Track` and `Person` node types defined there), it relies on **ADR-305**
|
||||
authenticated identity so that the observations it associates have a verified
|
||||
origin, and its outputs are governed `Event`s that ADR-318/ADR-319 can certify
|
||||
and witness.
|
||||
|
||||
The product need is to reason about *persistent entities* — "person_7 entered
|
||||
the kitchen, then the hallway, then the bedroom" — across radios, modalities,
|
||||
rooms, and time. The hard constraint is that this must happen **without
|
||||
establishing civil identity**. RuView is camera-free (ADR-282), and a
|
||||
persistent pseudonymous track must never become, or be joinable to, a real-
|
||||
world named individual. This is a privacy property to be enforced *by
|
||||
construction*, not a policy footnote.
|
||||
|
||||
Substantial scaffolding already exists in
|
||||
`v2/crates/wifi-densepose-mat/src/tracking` and must be **reused/extended, not
|
||||
rebuilt**:
|
||||
|
||||
- `SurvivorTracker`, `TrackedSurvivor`, `TrackId`, `TrackerConfig`,
|
||||
`TrackLifecycle`, and `TrackState` — a multi-target tracker with lifecycle
|
||||
(tentative/active/lost/terminal) and a `TrackId` backed by a UUID
|
||||
(`as_uuid`).
|
||||
- `KalmanState` with `predict`/`update`, `position`, `velocity`,
|
||||
`position_uncertainty`, and `mahalanobis_distance_sq` — the motion model and
|
||||
gating distance.
|
||||
- `CsiFingerprint`, `DetectionObservation`, `AssociationResult`, and the
|
||||
`can_reidentify`/`matches`/`mark_rescued`/`rescue` re-identification surface —
|
||||
the appearance/fingerprint channel for track continuity.
|
||||
|
||||
What is missing is (a) continuity **across radios, modalities, and rooms** (the
|
||||
tracker today reasons within a node/room context), (b) a **persistent** entity
|
||||
that survives track loss and hand-off between spaces, and (c) an explicit
|
||||
**privacy boundary** that guarantees no civil-identity binding.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Per-room independent trackers, no cross-room identity.** Rejected: cannot
|
||||
express "person_7 moved kitchen → hallway → bedroom"; loses the entity at
|
||||
every room boundary.
|
||||
2. **Global identity keyed on a strong biometric fingerprint.** Rejected: a
|
||||
fingerprint strong enough to re-identify across long gaps trends toward a
|
||||
civil-identity-grade biometric — exactly what the privacy constraint
|
||||
forbids.
|
||||
3. **Probabilistic persistent tracks with bounded, decaying pseudonymous
|
||||
association, built on the existing MAT tracker.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Extend `wifi-densepose-mat/tracking` into a **cross-domain persistent track
|
||||
layer** that produces ADR-306 `Track`/`Person` nodes.
|
||||
|
||||
### 1. Persistent probabilistic entity
|
||||
|
||||
- A persistent entity is a pseudonymous `Person` node (ADR-306) with a stable
|
||||
synthetic id (e.g. `person_7`) backed by the existing `TrackId`/UUID. It
|
||||
aggregates one or more `SurvivorTracker` tracks over time and space and holds
|
||||
a **probabilistic** continuity belief — association is never asserted as
|
||||
certain, and every hand-off carries a confidence.
|
||||
- Continuity across a track-loss gap reuses the existing re-identification
|
||||
surface (`can_reidentify`, `CsiFingerprint`, `AssociationResult`), extended
|
||||
with a **time- and distance-decayed** association prior so that confidence in
|
||||
"same entity" falls with the size of the gap. Beyond a bounded horizon the
|
||||
association is dropped and a new pseudonym is minted rather than forcing a
|
||||
join — under-linking is the privacy-safe failure mode.
|
||||
|
||||
### 2. Cross-radio / cross-modality / cross-room continuity
|
||||
|
||||
- Association operates over the ADR-306 ontology graph: `Observes` edges from
|
||||
multiple `Sensor`s and `AdjacentTo`/`Doorway` topology constrain plausible
|
||||
hand-offs (a person can only move between adjacent spaces). The existing
|
||||
`mahalanobis_distance_sq` gating extends to a fused observation across
|
||||
modalities rather than a single node's detections.
|
||||
- Fusion here is track-level association; the underlying multi-modality fusion
|
||||
(radar/mmWave per ADR-063, multistatic per ADR-029, and real sensor fusion
|
||||
per ADR-311) supplies the observations. This ADR depends on those for the raw
|
||||
cross-modality evidence and does not re-implement sensor fusion.
|
||||
|
||||
### 3. Privacy boundary (by construction)
|
||||
|
||||
- **No civil-identity binding.** The persistent id is a synthetic pseudonym
|
||||
with no field, edge, or join key to any name, account, phone, MAC, or other
|
||||
civil identifier. The type carries no such field, so binding is impossible in
|
||||
the schema, not merely discouraged.
|
||||
- The `CsiFingerprint` used for re-identification is **bounded and decaying**:
|
||||
it is scoped to short-horizon continuity, is not persisted as a long-term
|
||||
biometric template, and expires. This keeps re-identification useful for
|
||||
"same person across the hallway" while structurally unable to serve "this is
|
||||
the same person who visited last month."
|
||||
- Every `Track`/`Person`/`Event` produced carries `SemanticProvenance` and an
|
||||
`EvidenceLevel` (ADR-282), and honors the ADR-277/ADR-280 edge governance and
|
||||
ADR-141 attestation — a pseudonymous track is still governed P-class data.
|
||||
Tracking accuracy is a per-domain claim to be tagged MEASURED/CLAIMED/
|
||||
SYNTHETIC with a reproducer; **this ADR claims no accuracy number.**
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView can express persistent, cross-room trajectories for automation and
|
||||
analytics while remaining camera-free and civil-identity-free.
|
||||
- The privacy-safe failure mode is **under-linking** (mint a fresh pseudonym
|
||||
when unsure), which will fragment a trajectory across long gaps or sparse
|
||||
coverage. This is a deliberate trade: a fragmented pseudonym is safe, a
|
||||
wrong civil-identity join is not.
|
||||
- Extends an existing tracker rather than forking one; single-room single-radio
|
||||
deployments keep the current behavior (one entity = one track).
|
||||
- Cross-modality quality depends on ADR-311/ADR-063/ADR-029 landing; until then
|
||||
continuity is WiFi-primary and its limits are stated, not hidden.
|
||||
- Being phase 2, this ADR is design intent; it will be revised as the ADR-306
|
||||
ontology and ADR-305 identity spine finalize.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests (`cargo test -p wifi-densepose-mat`): decayed association prior
|
||||
(confidence falls with gap; drops beyond horizon → new pseudonym);
|
||||
topology-constrained hand-off (no association across non-adjacent spaces);
|
||||
schema check that a `Person`/`Track` carries no civil-identifier field.
|
||||
- Integration test against a synthetic multi-room, multi-radio scenario:
|
||||
a scripted walk kitchen → hallway → bedroom yields one persistent pseudonym
|
||||
with per-hand-off confidence, and a deliberately ambiguous crossing produces
|
||||
two pseudonyms rather than a false join.
|
||||
- Evidence discipline: any tracking-continuity accuracy is reported only with
|
||||
the ADR-291 leakage-free protocol and an evidence tag; no number is asserted
|
||||
here.
|
||||
- Privacy review: confirm no persisted long-term biometric template and no
|
||||
civil-identity join path, as an explicit checklist item before any pilot.
|
||||
138
docs/adr/ADR-308-sensor-placement-optimizer.md
Normal file
138
docs/adr/ADR-308-sensor-placement-optimizer.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# ADR-308: Sensor placement optimizer — floorplan + inventory → recommended positions
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: placement, planning, rf-twin, coverage, worldgraph, phase-3
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** and owns primitive #8, *sensor placement
|
||||
optimizer*. In the ADR-300 DAG it is a phase-3, research-forward primitive that
|
||||
sits on top of the fused world state and is tightly coupled to **ADR-315**
|
||||
(digital RF twin): the twin provides the propagation simulation this optimizer
|
||||
plans against. It reads the **ADR-306** canonical ontology for the physical
|
||||
scene and, after install, compares its predictions against ADR-302 observability
|
||||
and the ADR-318 capability certificate.
|
||||
|
||||
The problem it solves is the single most common cause of a bad RuView
|
||||
deployment: sensors placed by guesswork. Whether a room can be reliably sensed
|
||||
depends on AP/sensor geometry relative to walls, Fresnel-zone clearance,
|
||||
multipath structure, and where people actually move. Today an installer has no
|
||||
principled way to answer "where do I put the two nodes I have so the kitchen is
|
||||
observable?" — and no way, after install, to know whether reality matched the
|
||||
plan. This is a genuine **differentiator**: it turns RuView from "sense
|
||||
whatever the given placement happens to allow" into "recommend the placement
|
||||
that makes the requested sensing feasible."
|
||||
|
||||
Relevant existing assets to build on rather than duplicate:
|
||||
|
||||
- The `worldgraph` crate models the physical scene the optimizer plans over:
|
||||
`Room`/`Space` with `bounds_enu`, `Wall { rf_attenuation_db }` (drywall ≈ 3
|
||||
dB, brick ≈ 12 dB), `Doorway`, and `Zone` — enough geometry and coarse RF
|
||||
attenuation to seed a coverage model, plus `Sensor` nodes (ADR-306) for
|
||||
candidate positions.
|
||||
- **ADR-315** (RF twin, phase 3) is the propagation/multipath simulator; this
|
||||
optimizer is a *consumer* of the twin, not a second simulator.
|
||||
- **ADR-302** (OOD/observability) and **ADR-318** (capability certificate)
|
||||
define what "reliably sense the requested phenomenon" means, so the optimizer
|
||||
can optimize against the same observability metric the runtime later gates on.
|
||||
- **ADR-029** (multistatic) and **ADR-063** (mmWave fusion) inform which link
|
||||
geometries are useful for which phenomena.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Static placement guidelines in docs (e.g. "one node per room, opposite
|
||||
the door").** Rejected: ignores the specific floorplan, wall materials, and
|
||||
the actual hardware inventory; gives no uncertainty and no post-install
|
||||
feedback.
|
||||
2. **Full electromagnetic solver per site.** Rejected for the default path:
|
||||
too heavy for an installer workflow and overkill relative to the coarse
|
||||
`rf_attenuation_db` scene RuView actually has; reserved as an optional
|
||||
high-fidelity backend inside ADR-315.
|
||||
3. **A coverage optimizer that consumes the ADR-315 RF twin over the ADR-306
|
||||
scene, then validates predicted vs. measured observability after install.**
|
||||
Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Define a **placement optimizer** that takes a floor plan (ADR-306 scene) and a
|
||||
hardware inventory and recommends sensor positions, then closes the loop after
|
||||
install.
|
||||
|
||||
### 1. Inputs
|
||||
|
||||
- The ADR-306 canonical scene: `Space`/`Zone` bounds, `Wall` segments with
|
||||
`rf_attenuation_db`, `Doorway` topology, and any already-placed `Sensor`
|
||||
nodes.
|
||||
- A hardware inventory: the count and type of available radios (ESP32-S3/C6
|
||||
nodes, mmWave, adapters) with their capability envelopes (what each can
|
||||
sense, per ADR-318 / ADR-320 HAL descriptors).
|
||||
- A sensing objective: which phenomenon must be observable in which
|
||||
`Space`/`Zone` (presence, vitals, pose), expressed against the ADR-302
|
||||
observability metric.
|
||||
|
||||
### 2. Prediction
|
||||
|
||||
- For a candidate placement, query the **ADR-315 RF twin** for simulated RF
|
||||
coverage: path loss through `Wall` attenuation, **Fresnel-zone clearance**
|
||||
between link endpoints, and coarse **multipath** structure. From that derive
|
||||
an **expected observability** and an **uncertainty** for each objective in
|
||||
each space — reusing the same observability definition ADR-302 gates on so the
|
||||
plan and the runtime speak one language.
|
||||
- Search over candidate positions (the inventory bounds the count; the scene
|
||||
bounds the geometry) to recommend the placement that maximizes objective
|
||||
observability, reporting expected observability **and its uncertainty** per
|
||||
space — never a single confident number for a simulated result.
|
||||
|
||||
### 3. Post-install loop
|
||||
|
||||
- After install, compare **predicted vs. measured** observability using the
|
||||
ADR-302 runtime observability signal from the freshly enrolled (ADR-305),
|
||||
calibrated (ADR-301) sensors. Where measurement disagrees with prediction,
|
||||
recommend adjustments (move, re-aim, add a node) and feed the residual back
|
||||
to improve the ADR-315 twin's scene parameters (e.g. a wall's effective
|
||||
attenuation).
|
||||
|
||||
### Evidence discipline
|
||||
|
||||
- Predicted coverage is a **simulation** (evidence level L0 per ADR-282) and is
|
||||
labelled `SYNTHETIC`; it is a *recommendation*, never a sensing claim.
|
||||
- The predicted-vs-measured comparison is the only place a `MEASURED` statement
|
||||
appears, and only with a reproducer and real-silicon observability data
|
||||
(CLAUDE.md hardware rule). The optimizer never presents a simulated coverage
|
||||
map as evidence that a room *is* being sensed.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Installers get a principled, floorplan-specific placement plan and, crucially,
|
||||
a post-install check that says whether reality matched the plan — a
|
||||
differentiating capability over guess-and-check deployment.
|
||||
- Quality is bounded by the fidelity of the ADR-315 RF twin and the coarseness
|
||||
of the `worldgraph` scene (2D walls, coarse attenuation). The optimizer
|
||||
reports uncertainty rather than overstating a coarse model; higher fidelity
|
||||
is an ADR-315 concern.
|
||||
- Hard dependency on ADR-315 (twin), ADR-302 (observability metric), and
|
||||
ADR-306 (scene); this ADR does not build a simulator or an observability
|
||||
metric of its own.
|
||||
- Being phase 3, this is design intent sitting on the fused world state; it is
|
||||
expected to be revised as ADR-315 and the phase-1 spine land.
|
||||
- No claim that recommended placement *guarantees* sensing — it maximizes
|
||||
modelled observability subject to inventory and geometry, with explicit
|
||||
uncertainty.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests: coverage/observability prediction is a deterministic function of
|
||||
scene + placement + twin parameters; Fresnel-zone and wall-attenuation math
|
||||
against known analytic cases; search returns the modelled-optimal placement on
|
||||
small synthetic scenes.
|
||||
- Integration test: on a synthetic floorplan with a known-good and a
|
||||
known-bad placement, the optimizer ranks them correctly and reports higher
|
||||
uncertainty for the marginal case.
|
||||
- Post-install loop test: injected predicted-vs-measured disagreement produces a
|
||||
sensible adjustment recommendation and a twin-parameter residual.
|
||||
- Field validation (deferred, real-silicon): predicted vs. measured
|
||||
observability on an instrumented real site, reported as `MEASURED` with a
|
||||
reproducer. Until then all coverage output is `SYNTHETIC`/L0. No coverage or
|
||||
accuracy number is asserted by this ADR.
|
||||
152
docs/adr/ADR-309-active-sensing.md
Normal file
152
docs/adr/ADR-309-active-sensing.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# ADR-309: Active sensing — closed-loop RF experiment control
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: active-sensing, control-plane, closed-loop, information-gain, actuation, phase-3
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** and owns primitive #9, *active sensing*. In
|
||||
the ADR-300 phasing it is a phase-3 primitive that sits on top of the fused
|
||||
world state produced by **ADR-311** (real sensor fusion) and is driven by the
|
||||
information budget of **ADR-314** (information-gain scheduler). It is authored
|
||||
as **Proposed**: design intent and validation plan, not a phase-1 build.
|
||||
|
||||
The default posture of every current RuView path is **passive**: RF traffic
|
||||
happens for its own reasons (a device transmits, a beacon fires), RuView
|
||||
observes whatever CSI/CIR arrives, and the pipeline extracts what it can from
|
||||
that incidental signal. The strategic assessment behind ADR-300 named the next
|
||||
step: move from *RF-happens → observe* to **RuView-controls-RF → observe the
|
||||
response → optimize the next measurement**. That turns sensing into a
|
||||
closed-loop experiment — the system chooses what to measure to resolve the
|
||||
uncertainty it currently has, rather than accepting the measurements the
|
||||
environment happens to offer.
|
||||
|
||||
Substantial control-plane scaffolding already exists and must be
|
||||
**reused/extended, not rebuilt**:
|
||||
|
||||
- **ADR-280** (active sensing / programmable perception, *implemented* in
|
||||
`ruview-unified/src/control.rs`) already defines the governed control surface
|
||||
this ADR closes the loop over: `SensingTask` (evidence-aware, fail-closed
|
||||
admission), `SensingAction` + `InformationGoal` (a deliberate act of
|
||||
evidence-gathering against a stated hypothesis, bounded by a `PrivacyClass`
|
||||
P0–P5 ceiling), `ActiveSensingPlanner` (age-of-information scheduler),
|
||||
`CoherentSensorGroup` (coherent fusion fails closed), and `request_actuation`
|
||||
→ `ActuationReceipt` for governed RIS/movable/fluid-antenna actuation.
|
||||
- ADR-280 explicitly recorded that **information-gain *estimation* is not
|
||||
implemented** — "the planner uses staleness heuristics, not mutual
|
||||
information; RIS drivers, actual multi-AP coherence measurement, and OTFS
|
||||
waveform control are hardware-dependent roadmap items." ADR-309 is the ADR
|
||||
that closes exactly those gaps, in coordination with ADR-314.
|
||||
|
||||
The missing piece is not the actuation surface — ADR-280 built that and made it
|
||||
fail closed — but the **loop**: a controller that reads the current fused-state
|
||||
uncertainty, selects a *controllable measurement configuration* expected to
|
||||
reduce it most, requests it through the ADR-280 governed surface, observes the
|
||||
response, and updates its belief before choosing the next measurement.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Stay passive; only schedule which incidental observations to keep.** This
|
||||
is roughly today's `ActiveSensingPlanner` (staleness-priority over regions).
|
||||
Rejected as the endpoint: it optimizes *attention* over uncontrolled RF, not
|
||||
the *measurement* itself. It remains the fallback when nothing is
|
||||
controllable.
|
||||
2. **Open-loop measurement scripting** (a fixed sweep of channels/bandwidths).
|
||||
Rejected: a fixed sweep spends the RF/energy/privacy budget the same way
|
||||
regardless of what is already known; it cannot concentrate measurement where
|
||||
uncertainty actually is.
|
||||
3. **Closed-loop experiment control** — read uncertainty, pick the controllable
|
||||
configuration with highest expected information gain per unit cost/privacy,
|
||||
actuate through the ADR-280 governed surface, observe, update, repeat.
|
||||
Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt **closed-loop RF experiment control** as a phase-3 controller layered on
|
||||
the ADR-280 surface. RuView selects and drives the controllable degrees of
|
||||
freedom of the RF measurement, then optimizes the next measurement from the
|
||||
observed response.
|
||||
|
||||
### 1. Controllable degrees of freedom
|
||||
|
||||
Define an `ExperimentControl` vocabulary over the configuration axes RuView can
|
||||
influence on hardware that exposes them (each axis is optional and
|
||||
capability-gated by ADR-320's HAL, so an ESP32-only deployment simply has an
|
||||
empty controllable set and degrades to the passive planner):
|
||||
|
||||
- **Channel / band** and **bandwidth** (which spectrum to probe; reuses the
|
||||
ADR-292 wideband subcarrier-agnostic metadata).
|
||||
- **Packet timing / cadence** (when to solicit a sounding, and at what rate).
|
||||
- **Antenna / chain selection** (which subset of a distributed aperture to
|
||||
activate — bounded by the ADR-280 `CoherentSensorGroup` compatibility proof).
|
||||
- **Beam / RIS configuration** (which rooms and people become observable —
|
||||
governed exactly as ADR-280 §6 requires, via `request_actuation` and an
|
||||
`ActuationReceipt`).
|
||||
- **802.11bf measurement parameters** (TB/non-TB, reporting config) once
|
||||
ADR-310 exposes standardized sensing as a native measurement type.
|
||||
|
||||
### 2. The loop
|
||||
|
||||
```
|
||||
fused-state uncertainty (ADR-311)
|
||||
│
|
||||
▼
|
||||
info-gain ranking of ExperimentControl options (ADR-314)
|
||||
│ select argmax E[ΔI] / (cost, energy, privacy ceiling)
|
||||
▼
|
||||
governed request (ADR-280 admit_task / request_actuation, fail-closed)
|
||||
│
|
||||
▼
|
||||
observe response → update belief (ADR-311) → repeat
|
||||
```
|
||||
|
||||
The controller never bypasses the ADR-280 admission and actuation gates: every
|
||||
solicited measurement is a `SensingTask`/`SensingAction`, every environment
|
||||
change is an `ActuationReceipt`, and every step composes with the ADR-277
|
||||
policy engine. Information gain is what **ADR-314** supplies (the mutual-
|
||||
information estimate ADR-280 deferred); ADR-309 owns the *control loop* that
|
||||
consumes that estimate and drives the hardware.
|
||||
|
||||
### 3. Governance and honesty boundary
|
||||
|
||||
- Actuation and solicitation stay fail-closed and privacy-ceilinged: a
|
||||
closed-loop experiment cannot widen the P0–P5 ceiling of the task it serves,
|
||||
and cannot steer a beam into a zone that does not grant the purpose (ADR-280
|
||||
`actuation_requires_policy_authorization`).
|
||||
- Any accuracy or "traffic-reduction" claim from the closed loop is tagged
|
||||
**MEASURED** only with a named reproducer over a stated scenario, **SYNTHETIC**
|
||||
for simulated apertures, and **CLAIMED** otherwise. Real multi-AP coherent
|
||||
measurement and RIS actuation remain **hardware-dependent** and require
|
||||
real-silicon evidence (a captured runtime log) before any hardware claim, per
|
||||
CLAUDE.md. No number is invented here.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Sensing becomes an experiment: RuView spends its RF/energy/privacy budget on
|
||||
the measurements that most reduce current uncertainty, instead of processing
|
||||
whatever incidental traffic arrives.
|
||||
- The loop is only as strong as its two dependencies: ADR-311 must expose a
|
||||
usable uncertainty surface and ADR-314 must produce trustworthy information-
|
||||
gain estimates. Where either is absent, the controller degrades to the
|
||||
ADR-280 staleness planner rather than acting on a fabricated gain estimate.
|
||||
- Controllability is hardware-bounded. On commodity ESP32 sensors the
|
||||
controllable set may be limited to cadence; the full loop (bandwidth, antenna,
|
||||
beam) needs NICs/RIS that expose those axes, surfaced through ADR-320.
|
||||
- This ADR adds a controller; it does not re-open ADR-280's raw-export or
|
||||
actuation-governance decisions, which remain authoritative and fail-closed.
|
||||
|
||||
## Validation
|
||||
|
||||
- Design-level acceptance (phase 3): a simulated closed loop over a synthetic
|
||||
scene reduces terminal fused-state uncertainty faster than (a) the passive
|
||||
ADR-280 staleness planner and (b) an open-loop fixed sweep, at equal
|
||||
measurement budget — reported **SYNTHETIC**, with the scenario and seed named.
|
||||
- Governance tests: every solicited measurement and actuation in the loop is
|
||||
admitted through the ADR-280 fail-closed path; a loop step that would exceed
|
||||
the task's privacy ceiling or steer into an ungranted zone is denied.
|
||||
- Degradation test: with an empty controllable set (ESP32-only), the controller
|
||||
falls back to the staleness planner with no error and no fabricated gain.
|
||||
- Hardware validation of bandwidth/antenna/beam actuation is explicitly out of
|
||||
scope until real silicon exposes those axes and produces a captured log.
|
||||
147
docs/adr/ADR-310-80211bf-native-architecture.md
Normal file
147
docs/adr/ADR-310-80211bf-native-architecture.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# ADR-310: 802.11bf-native architecture — standardized WLAN sensing as native measurement types
|
||||
|
||||
- **Status**: Proposed (ADR-300 phase 2)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: 80211bf, wlan-sensing, standards, measurement-types, hal, phase-2
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** and owns primitive #10, *802.11bf-native
|
||||
architecture*. In the ADR-300 phasing it is a phase-2 integration primitive: it
|
||||
sits on the phase-1 spine (authenticated identity ADR-305, spatial ontology
|
||||
ADR-306, evidence engine ADR-304) and **feeds ADR-320** (the RuView sensor HAL),
|
||||
which is the clause of the acceptance test that "identifies the hardware." It is
|
||||
authored as **Proposed**.
|
||||
|
||||
**IEEE 802.11bf-2025 ("WLAN Sensing") was published 2025-09-26** — verified
|
||||
against the IEEE SA record in `wifi-densepose-hardware` (`ieee80211bf/mod.rs`
|
||||
header, "evidence grade MEASURED", ADR-152 §1.1). Standardization is complete
|
||||
for sub-7 GHz and >45 GHz (DMG) bands: formal sensing measurement setup,
|
||||
measurement instances, feedback/reporting, and sensing-by-proxy (SBP). This
|
||||
changes RuView's strategic frame: rather than treating every WiFi measurement as
|
||||
an *opportunistic* extraction from incidental traffic, RuView can be the **open
|
||||
reference sensing stack around the standard** — the day commodity silicon
|
||||
exposes it.
|
||||
|
||||
Substantial scaffolding already exists and must be **reused/extended, not
|
||||
rebuilt**. `v2/crates/wifi-densepose-hardware/src/ieee80211bf/` already models
|
||||
the standardized procedure surface as forward-compatible types (ADR-152/153):
|
||||
|
||||
- `types` — `SpecProfile` version gates, `SensingRole`/`TransceiverRole`,
|
||||
`MeasurementSetupParams`, `SensingCapabilities` negotiation, and required
|
||||
`ConsentMode` governance metadata on every setup.
|
||||
- `messages` — `SensingMeasurementSetupRequest/Response`,
|
||||
`SensingMeasurementInstance`, `SensingMeasurementReport`, `CsiReportPayload`,
|
||||
`SbpRequest/Response`, `SensingSessionTermination`.
|
||||
- `session` — a deterministic FSM (`Idle → SetupNegotiating → Active →
|
||||
Terminating → Idle`) with rejection paths, single-role enforcement, and SBP
|
||||
proxy mode; `table` (responder-side setup registry); `transport` (the
|
||||
`SensingTransport` seam, a `SimTransport` test double, and an
|
||||
`OpportunisticCsiBridge` that maps today's opportunistic CSI onto the
|
||||
standardized report path).
|
||||
|
||||
The module's own honesty note is authoritative and carried forward here: it is
|
||||
**not a certified 802.11bf implementation**, and **no commodity silicon — ESP32
|
||||
included — implements the standard yet**; the OTA frame binding lands when a
|
||||
chipset exposes it. Wideband ingest plumbing is already in place too: **ADR-292**
|
||||
(FeitCSI/AX210) carries native subcarrier dimensionality end-to-end and records
|
||||
the native→pipeline mapping, and noted that "truncated CIR is a natural
|
||||
extension of the same plumbing."
|
||||
|
||||
What is missing is architectural, not protocol scaffolding: normalized CSI is
|
||||
still treated as *the* WiFi input. The standardized sensing measurements
|
||||
(TB/non-TB soundings, truncated CIR / PDP reports) are modeled as protocol
|
||||
messages but are **not yet first-class native measurement types** that flow
|
||||
through calibration (ADR-301), fusion (ADR-311), and the ontology (ADR-306) on
|
||||
equal footing with normalized CSI.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Keep 802.11bf as a protocol model only; always down-convert its reports to
|
||||
normalized CSI at ingest.** Rejected: truncated CIR/PDP carry range-resolved
|
||||
multipath structure that flattening to a CSI matrix discards; it also wastes
|
||||
the standard's native report semantics.
|
||||
2. **Fork a parallel "bf pipeline" alongside the CSI pipeline.** Rejected:
|
||||
duplicates calibration, fusion, ontology, and evidence plumbing, and re-opens
|
||||
the O(surfaces²) translation problem ADR-306 exists to close.
|
||||
3. **Promote standardized sensing measurements to native measurement types
|
||||
inside the existing pipeline**, with normalized CSI as one measurement type
|
||||
among several. Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt an **802.11bf-native architecture**: standardized WLAN sensing
|
||||
measurements become **additional native measurement types**, alongside — not
|
||||
replacing — normalized CSI.
|
||||
|
||||
### 1. Native measurement types
|
||||
|
||||
- Define the standardized reports the `ieee80211bf` module already models
|
||||
(TB and non-TB soundings; truncated CIR; PDP) as first-class
|
||||
`MeasurementType` variants that the pipeline carries end-to-end, each tagged
|
||||
with its `SpecProfile` and band. Normalized CSI remains one such type; the
|
||||
`OpportunisticCsiBridge` remains the path for silicon that only offers
|
||||
incidental CSI.
|
||||
- Truncated CIR/PDP reuse the **ADR-292** subcarrier-agnostic / native-
|
||||
dimensionality plumbing (truncated CIR is the stated natural extension); the
|
||||
native→pipeline mapping is recorded in frame metadata so downstream stages
|
||||
know the true range/spectral resolution of a bf report vs. an interpolated CSI
|
||||
frame.
|
||||
|
||||
### 2. Ontology and governance binding
|
||||
|
||||
- Each standardized measurement becomes an ADR-306 `Observation` node from an
|
||||
ADR-305-authenticated `Sensor`, carrying `SemanticProvenance` and exactly one
|
||||
`EvidenceLevel` (L0–L5, ADR-282). The `ieee80211bf` `ConsentMode` metadata —
|
||||
required on every setup — composes with the ADR-277 policy engine, so a
|
||||
standardized session is admitted under the same governance as any other
|
||||
sensing task (ADR-280).
|
||||
- SBP (sensing-by-proxy) sessions attribute the report to the proxying and the
|
||||
sensing entities distinctly, so provenance is not laundered through the proxy.
|
||||
|
||||
### 3. HAL feed (ADR-320)
|
||||
|
||||
- The capability set a device advertises — which `MeasurementType`s, bands,
|
||||
bandwidths, roles, and `SpecProfile` it supports — is exactly the descriptor
|
||||
**ADR-320** (HAL) needs to "identify the hardware." ADR-310 defines that
|
||||
capability descriptor as the projection of `SensingCapabilities`; ADR-320
|
||||
consumes it. A device that implements no bf profile advertises only the
|
||||
opportunistic-CSI capability.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView is positioned as the open reference stack *around* the standard: when a
|
||||
chipset exposes 802.11bf, its native reports flow through calibration, fusion,
|
||||
ontology, and evidence with no bespoke pipeline — the plumbing is already
|
||||
tested against `SimTransport` and synthetic fixtures.
|
||||
- Normalized CSI is demoted from "the WiFi input" to "one measurement type,"
|
||||
which is the correct framing for a multi-measurement future and prevents the
|
||||
bf path from being a second-class citizen.
|
||||
- **No hardware claim is made or implied.** No commodity silicon implements
|
||||
802.11bf yet; this ADR wires the *types and flow*, tested in simulation. Any
|
||||
OTA/native-report accuracy claim requires real silicon evidence (a captured
|
||||
log) per CLAUDE.md, and any wideband number must be tagged with the capture
|
||||
hardware (ADR-292). No benchmark number is invented here.
|
||||
- This ADR does not re-open ADR-152/153's decision to avoid OTA frame binding
|
||||
until silicon exists; it consumes that surface and adds the pipeline
|
||||
integration.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p wifi-densepose-hardware` — existing `ieee80211bf` FSM,
|
||||
table, and transport tests continue to pass; new tests assert that a
|
||||
`SensingMeasurementReport` (TB and non-TB) and a truncated-CIR/PDP report
|
||||
round-trip through the pipeline as native `MeasurementType`s.
|
||||
- `cargo test -p wifi-densepose-mat` — truncated CIR ingest reuses the ADR-292
|
||||
subcarrier-agnostic path and records the native→pipeline mapping; dimension/
|
||||
version validation on standardized reports mirrors the FeitCSI parser gates.
|
||||
- Ontology/governance tests: each standardized measurement becomes an ADR-306
|
||||
`Observation` from an ADR-305-authenticated `Sensor` with one `EvidenceLevel`;
|
||||
`ConsentMode` composes with ADR-277 admission; SBP attributes proxy vs. sensor
|
||||
provenance distinctly.
|
||||
- HAL contract test: the ADR-320 capability descriptor is derivable from
|
||||
`SensingCapabilities`; a bf-less device advertises only opportunistic CSI.
|
||||
- All measurement-type flows are simulation-tested (`SimTransport`, synthetic
|
||||
fixtures); OTA binding and any hardware accuracy claim remain out of scope
|
||||
until real silicon exposes the standard.
|
||||
140
docs/adr/ADR-311-real-sensor-fusion.md
Normal file
140
docs/adr/ADR-311-real-sensor-fusion.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# ADR-311: Real sensor fusion — uncertainty-aware, multiple observations → one world state
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 2)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: fusion, uncertainty, multimodal, world-state, ontology, phase-2
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** and owns primitive #11, *real sensor fusion*.
|
||||
In the ADR-300 DAG it is a phase-2 integration primitive: it **consumes ADR-306**
|
||||
(canonical spatial ontology) and **produces the single fused world state** that
|
||||
the phase-3 primitives build on — **ADR-312** (long-term spatial memory),
|
||||
**ADR-313** (counterfactual inference), and **ADR-315** (digital RF twin). It is
|
||||
authored as **Proposed**.
|
||||
|
||||
The defining invariant is not "support more modalities" but the *shape of the
|
||||
output*: **multiple observations must resolve to one probabilistic world state,
|
||||
not many feeds into a visualization.** A dashboard that shows a WiFi layer, a
|
||||
mmWave layer, and a BLE layer side by side is not fusion; it pushes the
|
||||
reconciliation onto the human. Real fusion produces one uncertainty-aware state
|
||||
that every downstream consumer reads, with each contributing observation's
|
||||
provenance and confidence still recoverable.
|
||||
|
||||
Substantial scaffolding already exists and must be **reused/extended, not
|
||||
rebuilt**:
|
||||
|
||||
- **ADR-063** (60 GHz mmWave ↔ WiFi CSI fusion, *Proposed*) established the
|
||||
first cross-modal fusion case: pairing noisy CSI-derived vitals with clinical-
|
||||
grade mmWave FMCW radar (Seeed MR60BHA2 over UART, with a **live hardware
|
||||
capture** logged on 2026-03-15). ADR-311 generalizes that pairwise case into
|
||||
an N-modality, uncertainty-aware fusion.
|
||||
- **ADR-137** (fusion-engine quality scoring, *Accepted — partial*) already
|
||||
built the auditable-quality building block: it identified that the multistatic
|
||||
fusers (`wifi-densepose-signal/src/ruvsense/multistatic.rs`,
|
||||
`wifi-densepose-ruvector/src/viewpoint/fusion.rs`) discarded the evidence they
|
||||
used, and specified a single auditable record — "this fused output is
|
||||
trustworthy because X, Y, Z, but be aware of contradiction C" — with evidence
|
||||
references and contradiction flags. ADR-311 reuses that record as the
|
||||
provenance/quality carrier of the fused state.
|
||||
- **ADR-280** `CoherentSensorGroup` (fail-closed coherent fusion) and
|
||||
**ADR-306** `Observation`/`Track`/`Event` node types are the input and output
|
||||
vocabulary respectively.
|
||||
|
||||
What is missing is the **uncertainty-aware combiner across heterogeneous
|
||||
modalities**: a fusion stage that takes authenticated observations from WiFi,
|
||||
BLE, UWB, mmWave, acoustic, IMU, lidar, and cameras (only where policy permits),
|
||||
each with its own uncertainty, and emits one probabilistic `WorldState` — with
|
||||
per-observation contradiction flags, not a stack of independent feeds.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Per-modality feeds rendered together** (today's implicit model on some
|
||||
surfaces). Rejected: it is visualization, not fusion; contradictions are
|
||||
never reconciled and there is no single state to reason over.
|
||||
2. **Hard-switch "best modality wins"** (e.g., always prefer mmWave vitals over
|
||||
CSI vitals). Rejected: throws away corroborating evidence and cannot express
|
||||
*disagreement* — the very thing ADR-137's contradiction flags exist to
|
||||
surface — and degrades badly when the preferred modality is absent or OOD.
|
||||
3. **Uncertainty-weighted probabilistic fusion into one world state**, reusing
|
||||
ADR-137's auditable quality record and ADR-280's fail-closed coherence gate.
|
||||
Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt **uncertainty-aware multimodal fusion** whose invariant output is one
|
||||
probabilistic world state.
|
||||
|
||||
### 1. Inputs: authenticated, ontology-typed observations
|
||||
|
||||
- Inputs are ADR-306 `Observation` nodes from **ADR-305-authenticated** sensors.
|
||||
Supported modalities: WiFi (CSI / 802.11bf native reports via ADR-310), BLE,
|
||||
UWB, mmWave (ADR-063), acoustic, IMU, lidar, and cameras. Cameras and any
|
||||
higher privacy-class modality enter fusion **only where the ADR-277 policy
|
||||
engine permits** — camera-free coverage is a RuView invariant (ADR-282), so
|
||||
cameras are an opt-in, policy-gated input, never assumed present.
|
||||
- Each observation carries its own uncertainty and exactly one `EvidenceLevel`
|
||||
(ADR-282). An observation flagged out-of-distribution by **ADR-302** is
|
||||
down-weighted or excluded per its OOD verdict rather than silently averaged in.
|
||||
|
||||
### 2. Combiner: uncertainty-weighted, contradiction-aware
|
||||
|
||||
- Observations are combined by their uncertainty into one probabilistic
|
||||
`WorldState` over the ADR-306 entities (`Person`, `Object`, `Track`, and the
|
||||
per-`Space` inference). The combiner does **not** collapse disagreement: when
|
||||
modalities conflict beyond their stated uncertainty, the fused output carries
|
||||
ADR-137 **contradiction flags** and the evidence references that produced
|
||||
them, so a consumer can see *that* WiFi and mmWave disagree and *why*.
|
||||
- Coherent multi-node fusion inherits ADR-280's fail-closed
|
||||
`CoherentSensorGroup` gate: no coherent combination unless sync, phase, and
|
||||
geometry compatibility are proven; otherwise the group degrades to incoherent
|
||||
combination rather than producing confident nonsense.
|
||||
|
||||
### 3. Output: one world state, provenance preserved
|
||||
|
||||
- The output is a single `WorldState` written into the ADR-306 ontology, with
|
||||
every fused value retaining recoverable per-observation provenance and the
|
||||
ADR-137 quality record. This is the state ADR-312/310/312 consume; they read
|
||||
one probabilistic world, not a modality stack.
|
||||
- The fused state carries an aggregate uncertainty and an evidence level derived
|
||||
from its inputs (never upgraded above the weakest contributing L-level for a
|
||||
given claim).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Downstream primitives (spatial memory, counterfactual, RF twin) build on one
|
||||
probabilistic world state with uniform uncertainty and provenance, instead of
|
||||
re-implementing reconciliation per consumer.
|
||||
- Contradictions become first-class signal, not noise: ADR-137's record means a
|
||||
disagreement between mmWave and CSI is surfaced and auditable, which is also
|
||||
what lets ADR-302 and the evidence engine (ADR-304) reason about reliability.
|
||||
- Fusion is uncertainty-honest: an OOD or low-evidence observation is
|
||||
down-weighted, not averaged in as if trustworthy; a fused claim never presents
|
||||
a stronger evidence level than its weakest necessary input.
|
||||
- **No accuracy or "camera-grade" claim is made.** ADR-063's mmWave path has a
|
||||
real-silicon capture; the multimodal combiner's accuracy is not asserted here.
|
||||
Any fused-accuracy number requires a named reproducer tagged MEASURED /
|
||||
SYNTHETIC / CLAIMED, and WiFi sensing is never presented as camera-grade
|
||||
(CLAUDE.md, ADR-282). No number is invented.
|
||||
- Cameras remain a governed, opt-in input; enabling them does not weaken the
|
||||
camera-free coverage guarantee for deployments that exclude them.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p wifi-densepose-ruvector` / `-p wifi-densepose-signal` — the
|
||||
ADR-137 quality record and contradiction flags travel with the fused output;
|
||||
the ADR-280 `CoherentSensorGroup` gate still fails closed under
|
||||
clock/phase/geometry violation.
|
||||
- Fusion invariant test: N modality observations over one scene resolve to a
|
||||
single `WorldState` node in the ADR-306 ontology (not N feeds), with
|
||||
per-observation provenance recoverable and one aggregate evidence level.
|
||||
- Uncertainty tests: a high-uncertainty or ADR-302-flagged-OOD observation is
|
||||
down-weighted/excluded; conflicting modalities produce a contradiction flag
|
||||
rather than a silently averaged value; the fused evidence level never exceeds
|
||||
the weakest necessary input.
|
||||
- Governance test: a camera or higher-privacy modality is admitted into fusion
|
||||
only when the ADR-277 policy engine permits; otherwise it is excluded and the
|
||||
fused state notes the exclusion.
|
||||
- Any accuracy comparison (e.g., fused vitals vs. mmWave-only) is reported with
|
||||
its evidence tag and reproducer; none is asserted in this ADR.
|
||||
146
docs/adr/ADR-312-long-term-spatial-memory.md
Normal file
146
docs/adr/ADR-312-long-term-spatial-memory.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# ADR-312: Long-term spatial memory — learn the normal physics of a location
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: spatial-memory, ruvector, anomaly-detection, temporal, world-state, phase-3
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** and owns primitive #12, *long-term spatial
|
||||
memory*. In the ADR-300 phasing it is a phase-3 primitive that sits on the fused
|
||||
world state produced by **ADR-311** (real sensor fusion) and **ties to ADR-315**
|
||||
(digital RF twin): spatial memory is the *learned normal* that a twin can
|
||||
simulate against and that anomaly detection compares against. It is authored as
|
||||
**Proposed**.
|
||||
|
||||
The capability is to **learn the normal physics of a location** so anomalies
|
||||
surface *without training a detector for every anomaly*. Concretely, the system
|
||||
should learn statements like: "a chair is normally here"; "this bedroom is
|
||||
usually occupied between these hours"; "the RF propagation of this space
|
||||
changed"; "this machine's vibration signature changed"; "a new reflector
|
||||
appeared." None of these is a labeled anomaly class — they are *deviations from
|
||||
a learned baseline of normality*. This is the difference between supervised
|
||||
anomaly detection (which needs examples of every failure) and **baseline-relative
|
||||
anomaly detection** (which needs only a well-characterized normal).
|
||||
|
||||
Substantial substrate already exists and must be **reused/extended, not
|
||||
rebuilt**:
|
||||
|
||||
- **RuVector** (`v2/crates/wifi-densepose-ruvector`) is the designated substrate
|
||||
in the ADR-282 layer stack ("persistent objects, Gaussian fields, scene
|
||||
graphs, temporal memory"). It already provides the vector/temporal machinery
|
||||
this ADR needs — HNSW indexing (`hnsw.rs`, `hnsw_quantized.rs`), an event log
|
||||
(`event_log.rs`), coverage and estimator surfaces, and the `crv`/`mat`
|
||||
temporal sub-modules — so long-term spatial memory is a *consumer and
|
||||
organizer* of RuVector primitives, not a new store.
|
||||
- **ADR-306** supplies the entity vocabulary the memory is indexed by (`Space`,
|
||||
`Object`, `Sensor`, `Track`, `Event`); **ADR-311** supplies the fused,
|
||||
uncertainty-carrying `WorldState` snapshots that memory accumulates over time.
|
||||
- **ADR-135** (empty-room baseline calibration) and **ADR-301** (automatic
|
||||
domain calibration) already establish a *calibration-time* baseline of a
|
||||
space; ADR-312 extends that from a one-shot baseline to a **continuously
|
||||
learned, time-of-day-aware** model of normal.
|
||||
|
||||
What is missing is the **temporal normality model**: a per-`Space` learned
|
||||
distribution of fused world states over time (including periodicity — hour of
|
||||
day, day of week), plus RF-propagation and modality-signature baselines, against
|
||||
which a live fused state is scored for deviation.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Supervised anomaly classifiers per anomaly type.** Rejected: it needs
|
||||
labeled examples of every anomaly (fall, intrusion, machine fault, moved
|
||||
furniture), which do not exist for most spaces and do not transfer between
|
||||
rooms; it also cannot catch a *novel* anomaly it was never trained on.
|
||||
2. **Single static baseline** (the ADR-135 empty-room snapshot, used forever).
|
||||
Rejected as the endpoint: it cannot express *when* a space is normally
|
||||
occupied, cannot track slow legitimate drift (furniture rearranged on
|
||||
purpose), and flags every diurnal change as anomalous.
|
||||
3. **Continuously learned, time-aware normality model on the RuVector
|
||||
substrate**, scoring live fused state against learned normal. Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt a **long-term spatial memory** that learns each location's normal physics
|
||||
on the RuVector substrate and scores live fused state against it.
|
||||
|
||||
### 1. What "normal" is learned over
|
||||
|
||||
Per ADR-306 `Space` (and the entities within it), accumulate the ADR-311 fused
|
||||
`WorldState` over time into a learned normality model covering:
|
||||
|
||||
- **Occupancy / activity periodicity** — the distribution of presence and
|
||||
activity by hour-of-day and day-of-week (the "bedroom usually occupied certain
|
||||
hours" case).
|
||||
- **Static scene layout** — persistent `Object` positions and the expected
|
||||
reflector set (the "chair normally here" / "new reflector appeared" cases),
|
||||
building on the ADR-135/298 baseline.
|
||||
- **RF-propagation baseline** — the space's normal multipath/propagation
|
||||
signature (the "RF propagation changed" case).
|
||||
- **Per-modality signatures** — e.g., a machine's normal vibration/acoustic/IMU
|
||||
signature (the "vibration signature changed" case).
|
||||
|
||||
Each learned baseline carries its own uncertainty and an `EvidenceLevel`
|
||||
(ADR-282); a baseline learned from replay is L1, from a field pilot L4, and is
|
||||
never presented above the evidence of the observations it was learned from.
|
||||
|
||||
### 2. Substrate: RuVector, temporally compressed
|
||||
|
||||
- The memory is stored and indexed on RuVector (HNSW for nearest-normal recall,
|
||||
the event log for the temporal stream, the temporal sub-modules for
|
||||
compression). Long-horizon history is temporally compressed — recent detail
|
||||
retained, older history summarized — so memory cost is bounded rather than
|
||||
growing linearly forever.
|
||||
- The memory is *keyed by* the ADR-306 ontology, so "normal for this `Space` at
|
||||
this hour" is a first-class query, and slow legitimate drift updates the
|
||||
baseline (with provenance) instead of accumulating as permanent anomaly.
|
||||
|
||||
### 3. Anomaly = deviation from learned normal
|
||||
|
||||
- A live fused `WorldState` is scored against the applicable learned baseline
|
||||
(matched by space and time context). A deviation beyond the baseline's
|
||||
uncertainty is surfaced as an ADR-306 `Event` — *without* a per-anomaly
|
||||
detector — carrying the baseline it deviated from, the deviation magnitude,
|
||||
and its evidence level. Whether that event is actionable is a policy/consumer
|
||||
decision (ADR-277), not this layer's.
|
||||
- The learned normal is exactly what **ADR-315** (RF twin) can simulate against:
|
||||
the twin proposes an expected state, spatial memory supplies the learned
|
||||
actual-normal, and their divergence is a physically grounded anomaly signal.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Anomaly detection generalizes: a space gets deviation detection from its own
|
||||
learned normal, so a novel anomaly (never labeled anywhere) still registers as
|
||||
a deviation, and the model transfers to a new room by *learning that room's*
|
||||
normal rather than importing a foreign detector.
|
||||
- Bounded memory: temporal compression keeps long-horizon memory finite; the
|
||||
trade-off is that fine detail of old history is summarized, which is acceptable
|
||||
for a normality baseline.
|
||||
- Legitimate change is not a permanent false positive: slow drift updates the
|
||||
baseline with provenance, distinguishing "furniture deliberately rearranged"
|
||||
(baseline shifts) from "reflector appeared unexpectedly" (deviation event).
|
||||
- **No accuracy claim is made.** Deviation-detection quality is not asserted
|
||||
here; any detection-rate or false-positive number requires a named reproducer
|
||||
tagged MEASURED / SYNTHETIC / CLAIMED, and a health/safety framing stays within
|
||||
the ADR-282 bounded-claims discipline (decision support, not diagnosis). No
|
||||
number is invented.
|
||||
- The memory is governed: learned baselines are observations of a space, subject
|
||||
to the same ADR-277 retention/privacy policy as the fused state they summarize;
|
||||
no raw P0 RF is retained to build a baseline.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p wifi-densepose-ruvector` — the normality model builds on the
|
||||
existing HNSW/event-log/temporal primitives; nearest-normal recall and
|
||||
temporal-compression bounds are exercised on synthetic streams.
|
||||
- Baseline/deviation tests: a synthetic scene with a known injected change (moved
|
||||
`Object`, altered propagation, altered modality signature) produces a deviation
|
||||
`Event` against the learned normal *without* a per-anomaly detector; an
|
||||
unchanged diurnal cycle produces none (no false positive on normal periodicity).
|
||||
- Drift test: a slow legitimate change updates the baseline (with provenance)
|
||||
rather than emitting a persistent anomaly; an abrupt change does emit one.
|
||||
- Evidence test: a learned baseline carries the evidence level of its source
|
||||
observations and is never presented above it; retention honors ADR-277.
|
||||
- Twin-linkage design check (with ADR-315): divergence between a twin-simulated
|
||||
expected state and the learned normal is expressible as a deviation signal.
|
||||
142
docs/adr/ADR-313-counterfactual-inference.md
Normal file
142
docs/adr/ADR-313-counterfactual-inference.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# ADR-313: Counterfactual inference — generative spatial reasoning
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: inference, generative, counterfactual, rf-twin, fusion, uncertainty, phase-3
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** (perception substrate program) and owns
|
||||
primitive #13, *counterfactual inference*. In the ADR-300 DAG it is a phase-3,
|
||||
research-forward primitive that sits on top of the fused world state: it
|
||||
**consumes ADR-311** (real sensor fusion) for the current fused estimate and
|
||||
**ADR-315** (digital RF twin) for the twin's expected measurement
|
||||
distributions. It is design intent, authored as Proposed, and is expected to be
|
||||
revised as the phase-1 spine and the phase-2 fusion layer land.
|
||||
|
||||
RuView today reasons discriminatively: a task head maps measurements to a label
|
||||
or a pose. That answers "what does the classifier say?" but not the questions an
|
||||
operator actually asks — *would these RF measurements still make sense if nobody
|
||||
were present? Does one person explain the observation better than two?* Those
|
||||
are counterfactual questions, and a classifier cannot answer them because it has
|
||||
no model of what a measurement *should* look like under a hypothesized world
|
||||
state. A discriminative head asked about an empty room simply emits its
|
||||
best-effort label; it cannot say "the observation is better explained by
|
||||
absence."
|
||||
|
||||
The step this ADR proposes is toward a **generative spatial model**: given a
|
||||
hypothesized scene state (occupancy, count, coarse positions) and the ADR-315
|
||||
twin's propagation model for the deployment, predict the *expected* measurement
|
||||
distribution, then score how well each hypothesis explains the observed
|
||||
measurement. The best-explaining hypothesis — including the *nobody-present*
|
||||
null hypothesis — is the answer, and the margin between hypotheses is a
|
||||
first-class uncertainty signal.
|
||||
|
||||
Relevant existing assets to build on rather than duplicate:
|
||||
|
||||
- **ADR-311** (fusion) already produces the fused world estimate and its
|
||||
covariance; the counterfactual layer scores hypotheses *relative to* that
|
||||
estimate rather than re-fusing raw measurements.
|
||||
- **ADR-315** (RF twin) is the generative forward model — per-deployment
|
||||
geometry, radio locations, and expected measurement distributions. This ADR
|
||||
is a *consumer* of the twin's forward simulator, not a second simulator.
|
||||
- **ADR-302** (OOD/observability) already owns the `UNKNOWN` verdict; the
|
||||
null-hypothesis ("nobody present better explains this than any occupancy
|
||||
hypothesis") and the "no hypothesis explains this" case route through ADR-302,
|
||||
not a parallel gate.
|
||||
- `frame::EvidenceLevel` L0–L5 (ADR-282) and the ADR-304 evidence engine
|
||||
account for the resulting confidence.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Keep only discriminative heads.** Rejected: cannot express absence,
|
||||
cannot compare "one person vs. two" as competing explanations, and gives a
|
||||
confident label even when no world state explains the data.
|
||||
2. **A second, independently trained generative network with its own forward
|
||||
model.** Rejected for the default path: duplicates the ADR-315 twin's
|
||||
propagation model, invites the two models to disagree, and multiplies the
|
||||
surface that must be validated. Reserved only if the twin's analytic forward
|
||||
model proves insufficient for a phenomenon.
|
||||
3. **A hypothesis-scoring layer that uses the ADR-315 twin as the forward model
|
||||
and the ADR-311 fused state as the hypothesis prior, routing low-margin and
|
||||
null-dominant cases to the ADR-302 UNKNOWN verdict.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Define a **counterfactual inference layer** that scores a small set of scene
|
||||
hypotheses against observed measurements using the digital RF twin as the
|
||||
generative forward model.
|
||||
|
||||
### 1. Hypothesis set
|
||||
|
||||
- Hypotheses are drawn from the ADR-311 fused state and its neighbourhood: the
|
||||
current estimate, the **null hypothesis** (nobody present), and a bounded set
|
||||
of nearby alternatives (±1 occupant, shifted position). The fused estimate
|
||||
supplies the prior so the search stays small and grounded rather than
|
||||
enumerating an open world.
|
||||
- The hypothesis space is expressed over the **ADR-306** canonical ontology
|
||||
(`Space`/`Zone`, occupant count, coarse position), so a counterfactual result
|
||||
is a governed spatial statement, not an opaque score.
|
||||
|
||||
### 2. Forward model and scoring
|
||||
|
||||
- For each hypothesis, query the **ADR-315 twin** for the expected measurement
|
||||
distribution given that scene state and the deployment's propagation model.
|
||||
Score the observed measurement's likelihood under each hypothesis's expected
|
||||
distribution.
|
||||
- The answer is the maximum-likelihood hypothesis; the **margin** between the
|
||||
top hypotheses (and between the top hypothesis and the null) is the
|
||||
confidence signal, carried into the ADR-304 evidence engine.
|
||||
|
||||
### 3. Routing to UNKNOWN
|
||||
|
||||
- When the null hypothesis dominates, the layer reports *absence*, not a
|
||||
low-confidence occupancy label.
|
||||
- When **no** hypothesis explains the observation well (all likelihoods low, or
|
||||
the winning margin below threshold), the result routes to the **ADR-302**
|
||||
`UNKNOWN` verdict — the observation is outside what the twin can explain, and
|
||||
the honest output is "I cannot account for this," never a forced label.
|
||||
|
||||
### Evidence discipline
|
||||
|
||||
- Twin-predicted distributions are a **simulation** (evidence level L0 per
|
||||
ADR-282) labelled `SYNTHETIC`; a counterfactual verdict inherits the evidence
|
||||
level of its weakest input and is never presented as camera-grade ground
|
||||
truth (CLAUDE.md honesty rule).
|
||||
- Any accuracy statement about counterfactual discrimination (e.g. "distinguishes
|
||||
one occupant from two") requires the mean-pose-style baseline discipline of
|
||||
CLAUDE.md, a leakage-free held-out split, and a reproducer before it may be
|
||||
tagged `MEASURED`. This ADR asserts **no** such number.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView gains the ability to answer absence and "which explanation is better"
|
||||
questions that discriminative heads structurally cannot — a step toward
|
||||
generative spatial reasoning and a differentiator for security and
|
||||
facility-monitoring applications where *absence* is the valuable signal.
|
||||
- Quality is bounded by the fidelity of the ADR-315 twin's forward model and the
|
||||
ADR-311 fused prior; the layer reports margins and defers to ADR-302 UNKNOWN
|
||||
rather than overstating a coarse model.
|
||||
- Hard dependency on ADR-311 (fused state and covariance) and ADR-315 (forward
|
||||
model); this ADR builds neither a fusion engine nor a propagation simulator of
|
||||
its own.
|
||||
- Being phase 3, this is design intent sitting on the fused world state; it is
|
||||
expected to be revised as ADR-311 and ADR-315 land, and it is not implemented
|
||||
by the phase-1 swarm.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests: hypothesis likelihood scoring is a deterministic function of
|
||||
observed measurement + hypothesis + twin parameters; the null hypothesis wins
|
||||
on a synthesized empty-room measurement; a two-occupant measurement scores the
|
||||
two-occupant hypothesis above the one-occupant hypothesis on a controlled
|
||||
synthetic case.
|
||||
- Integration test: measurements the twin cannot explain (out-of-model
|
||||
scattering) drive the layer to the ADR-302 UNKNOWN verdict rather than a
|
||||
forced occupancy label; margins propagate into the ADR-304 evidence engine.
|
||||
- Held-out discrimination (deferred, real-silicon): one-vs-two and
|
||||
presence-vs-absence discrimination on a leakage-free held-out split with a
|
||||
mean-pose baseline, reported as `MEASURED` with a reproducer. Until then all
|
||||
counterfactual output is `SYNTHETIC`/L0. No discrimination accuracy number is
|
||||
asserted by this ADR.
|
||||
138
docs/adr/ADR-314-information-gain-scheduler.md
Normal file
138
docs/adr/ADR-314-information-gain-scheduler.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# ADR-314: Information-gain scheduler — sample the most informative radios
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: scheduling, active-sensing, information-gain, edge, energy, fusion, phase-3
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** (perception substrate program) and owns
|
||||
primitive #14, *information-gain scheduler*. In the ADR-300 DAG it is a phase-3,
|
||||
research-forward primitive that sits on top of the fused world state and
|
||||
**pairs with ADR-309** (active sensing): ADR-309 decides *what to probe*
|
||||
(waveform, sensing task); this ADR decides *which radios/modalities to spend
|
||||
budget on next*. It is authored as Proposed and is not implemented by the
|
||||
phase-1 swarm.
|
||||
|
||||
With multiple sensors, processing every stream at full rate is wasteful: many
|
||||
radios are, at any moment, contributing little to the current estimate while
|
||||
consuming compute, energy, and bandwidth — the three scarce resources on the
|
||||
edge nodes RuView targets (ESP32-S3/C6 and small gateways). Treating all sensors
|
||||
equally is precisely the design that does not survive a real deployment of
|
||||
"hundreds of sensors."
|
||||
|
||||
The scheduler assigns each candidate sensor/modality a value
|
||||
|
||||
```
|
||||
Value(sensor) ≈ expected uncertainty reduction / (compute + energy + bandwidth)
|
||||
```
|
||||
|
||||
and spends the next sampling/processing budget on the highest-value sensors.
|
||||
Expected uncertainty reduction is estimated *before* paying for the measurement,
|
||||
which is why the scheduler needs a model of what each sensor is likely to tell
|
||||
it — supplied by the fused state's covariance and the RF twin's forward model,
|
||||
not by actually sampling.
|
||||
|
||||
Relevant existing assets to build on rather than duplicate:
|
||||
|
||||
- **ADR-311** (fusion) maintains the fused state and its covariance — the
|
||||
current uncertainty the scheduler is trying to reduce. Expected uncertainty
|
||||
reduction is computed against that covariance, not a private one.
|
||||
- **ADR-315** (RF twin) provides the per-sensor forward model used to predict a
|
||||
candidate measurement's expected informativeness before sampling.
|
||||
- **ADR-320** (RuView sensor HAL, phase 2) exposes each radio's real
|
||||
compute/energy/bandwidth cost descriptors; the denominator is read from the
|
||||
HAL, not guessed per platform.
|
||||
- **ADR-309** (active sensing) is the paired actuator: the scheduler ranks
|
||||
sensors, ADR-309 chooses the probe on the chosen sensor.
|
||||
- **ADR-302** (observability) defines the phenomenon the estimate is *for*, so
|
||||
the scheduler prioritizes uncertainty reduction on the objective that matters,
|
||||
not on nuisance dimensions.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Round-robin / process-everything scheduling.** Rejected: burns edge
|
||||
compute and energy on redundant streams and does not scale to large fleets;
|
||||
the strategic and external reviews named exactly this as an edge-deployment
|
||||
blocker.
|
||||
2. **Static priority per sensor type (e.g. always prefer mmWave).** Rejected:
|
||||
ignores that a sensor's *current* informativeness depends on the scene and
|
||||
the present uncertainty — a well-placed WiFi link can dominate an occluded
|
||||
mmWave node in a given moment.
|
||||
3. **A value-of-information scheduler that ranks sensors by expected uncertainty
|
||||
reduction per unit cost, using the ADR-311 covariance and ADR-315 forward
|
||||
model, with costs from the ADR-320 HAL.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Define an **information-gain scheduler** that allocates the next
|
||||
sampling/processing budget across available radios by value of information.
|
||||
|
||||
### 1. Value function
|
||||
|
||||
- For each candidate sensor/modality, estimate **expected uncertainty
|
||||
reduction** on the ADR-302 objective by evaluating how much a predicted
|
||||
measurement (via the **ADR-315** forward model) would shrink the **ADR-311**
|
||||
fused-state covariance — a value-of-information estimate made *before* paying
|
||||
for the measurement.
|
||||
- Divide by the sensor's **cost** — compute + energy + bandwidth — read from the
|
||||
**ADR-320** HAL descriptors. The exact weighting of the three cost terms is a
|
||||
deployment policy (a battery node weights energy heavily; a wired gateway
|
||||
weights bandwidth), configured, not hardcoded.
|
||||
|
||||
### 2. Allocation
|
||||
|
||||
- Rank candidates by value and spend the budget on the top set, subject to a
|
||||
configurable floor that guarantees each sensor is sampled at least
|
||||
occasionally (so a sensor whose value is currently low is not starved into
|
||||
permanent blindness and can be re-evaluated as the scene changes).
|
||||
- The scheduler emits an allocation, not a measurement; **ADR-309** active
|
||||
sensing chooses the probe/waveform on each selected sensor, and the fusion
|
||||
layer (ADR-311) incorporates the result.
|
||||
|
||||
### 3. Governance and honesty
|
||||
|
||||
- Skipping a sensor for a cycle is a *deliberate* reduction in coverage; the
|
||||
scheduler records which sensors were sampled so downstream evidence (ADR-304)
|
||||
reflects the actual sensing that occurred, and observability (ADR-302) can
|
||||
raise `UNKNOWN` for a zone that went under-sampled rather than reporting a
|
||||
stale estimate as current.
|
||||
|
||||
### Evidence discipline
|
||||
|
||||
- Expected-uncertainty-reduction estimates are model predictions from the
|
||||
ADR-315 twin (simulation, L0 per ADR-282, `SYNTHETIC`); a scheduling decision
|
||||
is a resource choice, never a sensing claim.
|
||||
- Any energy/latency/throughput improvement figure requires real-silicon
|
||||
measurement with a reproducer before it is tagged `MEASURED` (CLAUDE.md
|
||||
hardware rule). This ADR asserts **no** efficiency number.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Edge deployments spend scarce compute, energy, and bandwidth where they buy
|
||||
the most certainty, making "hundreds of sensors" operationally tractable — a
|
||||
capability the reviews flagged as critical for edge deployment.
|
||||
- Quality is bounded by the accuracy of the ADR-315 forward model (informativeness
|
||||
prediction) and ADR-320 cost descriptors; a poor forward model degrades to
|
||||
near-round-robin, which is safe but not optimal. The sampling floor bounds the
|
||||
worst case.
|
||||
- Hard dependency on ADR-311 (covariance), ADR-315 (forward model), and ADR-320
|
||||
(cost descriptors), and paired with ADR-309; this ADR builds none of those.
|
||||
- Being phase 3, this is design intent sitting on the fused world state and is
|
||||
expected to be revised as ADR-309, ADR-311, ADR-315, and the ADR-320 HAL land.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests: the value function is a deterministic function of covariance +
|
||||
forward model + cost descriptors; a sensor predicted to reduce objective
|
||||
uncertainty more per unit cost ranks above one that reduces it less; the
|
||||
sampling floor guarantees eventual re-evaluation of a low-value sensor.
|
||||
- Integration test: on a synthetic multi-sensor scene, the scheduler reduces
|
||||
objective uncertainty faster per unit modelled cost than round-robin, and
|
||||
raises ADR-302 UNKNOWN for a deliberately starved zone rather than reporting a
|
||||
stale estimate.
|
||||
- Field validation (deferred, real-silicon): energy/latency/throughput on an
|
||||
instrumented multi-node deployment, reported as `MEASURED` with a reproducer.
|
||||
Until then all informativeness and cost figures are `SYNTHETIC`/L0. No
|
||||
efficiency number is asserted by this ADR.
|
||||
159
docs/adr/ADR-315-digital-rf-twin.md
Normal file
159
docs/adr/ADR-315-digital-rf-twin.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# ADR-315: Digital RF twin — persistent per-deployment RF model
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 3)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: rf-twin, digital-twin, propagation, calibration, spatial-memory, worldgraph, phase-3
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** (perception substrate program) and owns
|
||||
primitive #15, *digital RF twin*. In the ADR-300 DAG it is a phase-3,
|
||||
research-forward primitive that underpins several other phase-3 primitives:
|
||||
**ADR-308** (placement optimizer) plans against the twin's propagation model,
|
||||
**ADR-313** (counterfactual inference) uses it as the generative forward model,
|
||||
and **ADR-314** (information-gain scheduler) uses it to predict per-sensor
|
||||
informativeness. It ties directly to **ADR-301** (calibration), **ADR-308**
|
||||
(placement), and **ADR-312** (long-term spatial memory). It is authored as
|
||||
Proposed and is not implemented by the phase-1 swarm.
|
||||
|
||||
RuView today has no persistent, per-deployment model of the RF environment.
|
||||
Calibration state, observed multipath, and radio geometry exist transiently
|
||||
inside a running session; when the process restarts or a change happens
|
||||
overnight, there is nothing that says "this is what this room's RF looked like
|
||||
yesterday." Without a persistent baseline, a physical change — furniture moved,
|
||||
a wall opened, a machine relocated, an intruder present — has nothing to be a
|
||||
*delta against*. It is just a different measurement, indistinguishable from
|
||||
noise or drift.
|
||||
|
||||
The **digital RF twin** is that persistent baseline: a per-deployment model
|
||||
holding
|
||||
|
||||
- **geometry and radio locations** (from the ADR-306 scene / worldgraph),
|
||||
- **propagation history** and **observed multipath** structure,
|
||||
- **calibration state** (from ADR-301),
|
||||
- **expected measurement distributions** for each link and phenomenon.
|
||||
|
||||
Once the twin exists, a physical change becomes a **measurable delta against the
|
||||
twin** rather than an unexplained measurement. This is what connects RuView to
|
||||
facility management (what changed in this space?), security (is there an
|
||||
unexplained presence?), robotics (has the map drifted?), and industrial
|
||||
monitoring (did the plant layout change?) — the applications the strategic
|
||||
assessment named as the value beyond a single detector.
|
||||
|
||||
Relevant existing assets to build on rather than duplicate:
|
||||
|
||||
- The `worldgraph` crate already models the physical scene — `Room`/`Space`
|
||||
with `bounds_enu`, `Wall { rf_attenuation_db }`, `Doorway`, `Zone`, and
|
||||
`Sensor` nodes (ADR-306). The twin *annotates and persists* this scene with RF
|
||||
state; it does not invent a second geometry.
|
||||
- `wifi-densepose-calibration` (enrollment, bank, anchor, runtime, specialist)
|
||||
holds the calibration state the twin persists; the twin references and
|
||||
versions calibration records, it does not reimplement calibration.
|
||||
- **ADR-312** (long-term spatial memory, phase 3) is the persistence and
|
||||
temporal-history substrate; the twin is a *structured occupant* of that
|
||||
memory, not a separate database.
|
||||
- **ADR-305** (authenticated identity) and **ADR-295** (provenance) mean the
|
||||
measurements that update the twin carry verified lineage, so a delta is
|
||||
attributable rather than anonymous.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **No persistent RF model (status quo).** Rejected: every change looks like
|
||||
noise; nothing supports "what changed since yesterday?", which is the
|
||||
question the facility/security/industrial applications actually ask.
|
||||
2. **A full electromagnetic digital twin (per-site ray-tracing / FDTD kept in
|
||||
sync in real time).** Rejected for the default path: far heavier than the
|
||||
coarse `rf_attenuation_db` scene RuView actually has and impractical on edge
|
||||
hardware. A high-fidelity solver is retained as an *optional backend* the
|
||||
twin can call, not the baseline.
|
||||
3. **A persistent, per-deployment RF model layered over the ADR-306 scene and
|
||||
ADR-312 memory: geometry + radio locations + calibration state + observed
|
||||
multipath + expected measurement distributions, updated by verified
|
||||
measurements, exposing changes as deltas.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Define the **digital RF twin** as a persistent, versioned, per-deployment model
|
||||
of the RF environment, layered over existing scene, calibration, and memory
|
||||
assets.
|
||||
|
||||
### 1. State the twin holds
|
||||
|
||||
- **Geometry and radio locations** referenced from the ADR-306 / worldgraph
|
||||
scene (not copied).
|
||||
- **Calibration state** referenced and versioned from
|
||||
`wifi-densepose-calibration` (ADR-301), so the twin knows *which* calibration
|
||||
a stored distribution was captured under.
|
||||
- **Observed multipath and propagation history** — a bounded temporal summary
|
||||
of per-link channel structure, stored in ADR-312 spatial memory.
|
||||
- **Expected measurement distributions** per link and phenomenon — the forward
|
||||
model ADR-308, ADR-313, and ADR-314 consume.
|
||||
|
||||
### 2. Update and delta
|
||||
|
||||
- Verified measurements (ADR-305 identity, ADR-295 provenance) update the twin's
|
||||
distributions online, bounded by ADR-301 calibration validity. A new
|
||||
observation is compared to the twin's expected distribution; the **delta** —
|
||||
and its statistical significance against the twin's own variance — is the
|
||||
primary output. A change large relative to the twin's modelled variance is a
|
||||
*detected physical change*, not noise.
|
||||
- The twin is **versioned**: a calibration event, a deliberate geometry edit, or
|
||||
an accepted physical change advances the twin version, so history is
|
||||
auditable and a delta is always relative to a named baseline.
|
||||
|
||||
### 3. Consumers
|
||||
|
||||
- **ADR-308** queries the twin's propagation model to plan placements.
|
||||
- **ADR-313** uses the twin's expected distributions as the generative forward
|
||||
model for hypothesis scoring.
|
||||
- **ADR-314** uses per-sensor expected informativeness from the twin.
|
||||
- Facility/security/robotics/industrial integrations read the twin's change
|
||||
deltas as governed ADR-306 spatial events.
|
||||
|
||||
### Evidence discipline
|
||||
|
||||
- The twin's expected distributions and any propagation simulation are
|
||||
**simulation** (evidence level L0 per ADR-282), labelled `SYNTHETIC`. A delta
|
||||
computed against them is a model-relative statement.
|
||||
- A change/anomaly detection *claim* (e.g. "detects furniture-scale changes")
|
||||
requires real-silicon measurement against a leakage-free protocol with a
|
||||
reproducer before it is tagged `MEASURED` (CLAUDE.md hardware rule). The twin
|
||||
never presents a modelled expected distribution as evidence that a physical
|
||||
state *is* the case; it presents a *delta and its significance*. This ADR
|
||||
asserts **no** detection-accuracy number.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView gains a persistent per-deployment baseline, turning "a different
|
||||
measurement" into "a measurable, attributable, versioned change" — the bridge
|
||||
from a sensing runtime to facility management, security, robotics, and
|
||||
industrial monitoring.
|
||||
- The twin is the shared forward model for ADR-308/310/311, so those primitives
|
||||
speak one propagation model rather than three inconsistent ones — a
|
||||
deliberate reason to build the twin before its consumers mature.
|
||||
- Quality is bounded by the coarseness of the worldgraph scene and the fidelity
|
||||
of the forward model; the twin reports deltas *with significance against its
|
||||
own variance* rather than asserting confident change detection on a coarse
|
||||
model. The optional high-fidelity backend is where higher accuracy lives.
|
||||
- Hard dependency on ADR-306 (scene), ADR-301 (calibration state), and ADR-312
|
||||
(persistence); it reuses `worldgraph` and `wifi-densepose-calibration` rather
|
||||
than rebuilding geometry or calibration.
|
||||
- Being phase 3, this is design intent; it is expected to be revised as the
|
||||
phase-1 spine, ADR-311 fusion, and ADR-312 memory land.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests: the twin's expected distribution is a deterministic function of
|
||||
scene + calibration + propagation history; delta computation and its
|
||||
significance against stored variance are correct on synthetic distributions;
|
||||
versioning advances on calibration/geometry/accepted-change events and history
|
||||
is retained.
|
||||
- Integration test: on a synthetic deployment, an injected physical change (a
|
||||
wall attenuation shift) produces a significant delta against the twin while
|
||||
ordinary noise does not; the delta surfaces as a governed ADR-306 event with
|
||||
provenance (ADR-305/292).
|
||||
- Field validation (deferred, real-silicon): change detection on an instrumented
|
||||
real deployment with a controlled physical-change protocol, reported as
|
||||
`MEASURED` with a reproducer. Until then all twin distributions and deltas are
|
||||
`SYNTHETIC`/L0. No detection-accuracy number is asserted by this ADR.
|
||||
156
docs/adr/ADR-316-fleet-control-plane.md
Normal file
156
docs/adr/ADR-316-fleet-control-plane.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# ADR-316: Fleet control plane — provisioning to audit trails
|
||||
|
||||
- **Status**: Proposed (ADR-300 phase 2)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: fleet, operations, provisioning, firmware, updates, audit, identity, phase-2
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is a child of **ADR-300** (perception substrate program) and owns
|
||||
primitive #16, *fleet control plane*. In the ADR-300 DAG it is a phase-2
|
||||
integration-and-operations primitive that sits on the phase-1 spine: it
|
||||
**consumes ADR-305** (authenticated sensor identity) for per-device identity and
|
||||
enrollment, and **ADR-318** (capability certificate) for the signed models,
|
||||
calibration validity, and capability envelopes a device is allowed to run. It is
|
||||
authored as Proposed and is not implemented by the phase-1 swarm.
|
||||
|
||||
The external and internal reviews both named the same operational gap: RuView
|
||||
has strong per-device primitives but no **release identity** and no **bill of
|
||||
materials** binding a fielded sensor to the exact firmware, model, and
|
||||
calibration it is running — and no plane to manage that across many devices.
|
||||
Without this, a handful of nodes is fine but *hundreds* of sensors become an
|
||||
operational nightmare: no coherent way to provision, roll certificates, verify
|
||||
firmware compatibility, distribute signed models, track calibration lifecycle,
|
||||
watch health, stage updates, roll back, diagnose remotely, enforce data
|
||||
retention, or produce an audit trail. This ADR addresses that release-identity /
|
||||
BOM gap directly.
|
||||
|
||||
The scope is deliberately the **control plane**, not the data plane. The
|
||||
authenticated measurement path is **ADR-296** (bind + allowlist) plus **ADR-305**
|
||||
(signed envelope); this ADR governs the *devices and artifacts*, not the
|
||||
per-frame stream.
|
||||
|
||||
Relevant existing assets to build on rather than duplicate:
|
||||
|
||||
- **ADR-305** already defines per-device keypairs, the `DeviceId → public key →
|
||||
capabilities` enrollment record, key rotation and revocation *semantics* — and
|
||||
explicitly deferred their **fleet distribution** to this ADR. The control
|
||||
plane is the distribution and lifecycle layer over ADR-305 identity, not a new
|
||||
identity scheme.
|
||||
- **ADR-318** (capability certificate) defines the signed, expiring artifact a
|
||||
device is authorized to run; the fleet plane is what *distributes, stages, and
|
||||
revokes* those certificates and the signed models they point at.
|
||||
- **ADR-301** (calibration) owns calibration validity/expiry; the fleet plane
|
||||
tracks calibration *lifecycle* across the fleet (which nodes are due, which are
|
||||
stale) rather than redefining calibration.
|
||||
- **ADR-319** (witness chain) provides the append-only, re-verifiable record;
|
||||
fleet audit trails are witness-chain entries, not a parallel log format.
|
||||
- **ADR-320** (RuView sensor HAL, phase 2) provides hardware/firmware capability
|
||||
descriptors used for firmware-compatibility checks before staging an update.
|
||||
- `wifi-densepose-bfld` `CapabilityAttestation` (ADR-141) is the device-side
|
||||
attestation the plane checks against declared cohort capabilities.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Manual per-device operations (SSH/flash by hand).** Rejected: does not
|
||||
scale past a handful of nodes, produces no release identity, no audit trail,
|
||||
and no safe rollback — exactly the operational nightmare the reviews named.
|
||||
2. **Adopt a generic third-party IoT device-management platform wholesale.**
|
||||
Rejected as the core: generic platforms do not understand RuView's signed
|
||||
capability certificate, calibration validity, or witness chain, and would
|
||||
fork trust away from the phase-1 spine. A generic transport/agent *may* be a
|
||||
backend, but identity, certificates, and audit remain RuView's.
|
||||
3. **A RuView-native control plane layered on ADR-305 identity, ADR-318
|
||||
certificates, ADR-301 calibration lifecycle, and ADR-319 audit — covering
|
||||
provisioning through rollback and retention.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Define a **fleet control plane** that manages RuView sensors and their signed
|
||||
artifacts across their lifecycle, built on the phase-1 identity/certificate
|
||||
spine.
|
||||
|
||||
### 1. Release identity and bill of materials
|
||||
|
||||
- Each fielded device has a **BOM record** binding `DeviceId` (ADR-305) → exact
|
||||
firmware version → signed model set → active capability certificate (ADR-318)
|
||||
→ current calibration record (ADR-301) → HAL/hardware descriptor (ADR-320).
|
||||
This *is* the release identity the reviews found missing: given a device you
|
||||
can state precisely what it is running and prove it is signed.
|
||||
|
||||
### 2. Provisioning, certificates, firmware compatibility
|
||||
|
||||
- **Provisioning** is the authorized ADR-305 enrollment step at fleet scale:
|
||||
minting a keypair, registering the public key and capabilities, and issuing
|
||||
the initial ADR-318 certificate. A device is untrusted until provisioned.
|
||||
- **Certificate lifecycle**: issue, rotate, expire, and **revoke** ADR-318
|
||||
certificates and the ADR-305 keys behind them; revocation lists are
|
||||
distributed here (the distribution ADR-305 deferred).
|
||||
- **Firmware compatibility**: before staging a firmware or model, check the
|
||||
target's ADR-320 HAL descriptor and ADR-141 capability attestation so an
|
||||
incompatible or under-capable device is never sent an artifact it cannot
|
||||
honestly run.
|
||||
|
||||
### 3. Cohorts, staged updates, rollback
|
||||
|
||||
- Devices group into **cohorts** (by site, hardware, capability). Updates —
|
||||
signed models and firmware — roll out **staged** (canary → cohort → fleet)
|
||||
with health gates between stages, and **roll back** to the previously recorded
|
||||
BOM on a failed health check. Only signed artifacts are ever staged.
|
||||
|
||||
### 4. Health telemetry, remote diagnostics, retention, audit
|
||||
|
||||
- **Health telemetry** and **remote diagnostics** report device liveness,
|
||||
calibration staleness (ADR-301), certificate expiry (ADR-318), and error
|
||||
state — read-only diagnostics by default, mutations authorized explicitly.
|
||||
- **Data retention** policy is enforced per cohort, and P0/CSI/person data never
|
||||
leaves the edge except under the ADR-277/280 governance already in force
|
||||
(CLAUDE.md: never commit or exfiltrate CSI/person data).
|
||||
- Every lifecycle action — provision, rotate, revoke, stage, roll back — is
|
||||
written as an **ADR-319 witness-chain** entry, giving a re-verifiable **audit
|
||||
trail** rather than a mutable log.
|
||||
|
||||
### Authority and least privilege
|
||||
|
||||
- The control plane is default-deny (CLAUDE.md: default to least authority).
|
||||
Provisioning, key rotation, revocation, staging, and rollback are each
|
||||
separately authorized operations; no fleet action is implied by another.
|
||||
Credentials and private keys are never logged or committed.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Hundreds of sensors become operable: coherent release identity, signed-artifact
|
||||
distribution, staged updates with rollback, and a re-verifiable audit trail —
|
||||
closing the release-identity / BOM gap the reviews raised.
|
||||
- The plane concentrates operational authority; that is mitigated by
|
||||
default-deny, per-action authorization, signed-only artifacts, and
|
||||
witness-chained audit. A compromised plane must still forge signatures the
|
||||
phase-1 spine verifies.
|
||||
- Hard dependency on ADR-305 (identity), ADR-318 (certificate), ADR-301
|
||||
(calibration lifecycle), ADR-319 (audit), and ADR-320 (firmware/HAL
|
||||
compatibility). This ADR distributes and sequences those artifacts; it does
|
||||
not redefine identity, certificates, calibration, or the witness format.
|
||||
- Being phase 2, this is design intent depending on the spine; it is expected to
|
||||
be revised as ADR-318, ADR-319, and ADR-320 land.
|
||||
- **No fielded fleet-operation claim is MEASURED without real-silicon evidence**
|
||||
(CLAUDE.md hardware rule): staged update and rollback on real nodes require a
|
||||
captured runtime log. A passing simulation is not fleet evidence.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests: BOM records bind identity/firmware/model/certificate/calibration
|
||||
consistently and reject inconsistent bindings; certificate issue/rotate/revoke
|
||||
transitions are correct; a firmware-incompatible target is refused staging;
|
||||
every lifecycle action emits a well-formed ADR-319 witness entry.
|
||||
- Integration test: a synthetic cohort undergoes a canary→cohort→fleet staged
|
||||
update; an injected health failure triggers rollback to the prior BOM; the
|
||||
full sequence is re-verifiable from the witness chain offline; a revoked
|
||||
certificate is rejected fleet-wide.
|
||||
- Security test (`npm run test:security` analogue for the plane): default-deny
|
||||
is enforced; unauthorized provision/rotate/revoke/stage is rejected and
|
||||
counted; no credential or P0 data appears in telemetry or audit output.
|
||||
- Field validation (deferred, real-silicon): a real multi-node staged update and
|
||||
rollback with a captured boot/runtime log, reported as `MEASURED` with a
|
||||
reproducer. Until then all fleet-operation results are simulator-level. No
|
||||
fielded reliability number is asserted by this ADR.
|
||||
140
docs/adr/ADR-317-benchmark-multi-domain-scorecard.md
Normal file
140
docs/adr/ADR-317-benchmark-multi-domain-scorecard.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# ADR-317: Multi-domain benchmark scorecard — regressions cannot hide behind pooled accuracy
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: benchmark, aetherarena, ci-gate, evidence, honesty, domain-generalization, substrate
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is primitive 17 of the perception-substrate program (ADR-300) and the
|
||||
per-PR enforcement edge of the phase-1 certificate spine. In the ADR-300
|
||||
dependency DAG it reads accuracy from the evidence engine (ADR-304), consumes
|
||||
the domain state produced by out-of-distribution detection (ADR-302), scores
|
||||
against calibration certificates (ADR-301), and is anchored in the witness chain
|
||||
(ADR-319). It is the surface that makes the rest of the spine testable on every
|
||||
change to sensing code.
|
||||
|
||||
A single pooled accuracy number is the classic way a domain-generalization
|
||||
regression hides. A model can raise mean PCK or mean presence accuracy while
|
||||
quietly collapsing on unseen rooms, unseen devices, or stationary subjects —
|
||||
exactly the conditions WiFi sensing fails in and exactly the conditions a
|
||||
pooled average washes out. The strategic assessment (ADR-300) named this: what
|
||||
distinguishes infrastructure from a demo is that a regression on *any* operating
|
||||
domain is caught before merge, not discovered in the field.
|
||||
|
||||
RuView does not need a new benchmark to do this. AetherArena is already
|
||||
**v0-complete infrastructure** (ADR-149): a deterministic scoring engine
|
||||
reusing `wifi-densepose-train` (`src/ruview_metrics.rs`, `src/ablation.rs`,
|
||||
`src/eval.rs`, `src/proof.rs`), a `PROOF_SEED=42` determinism substrate that
|
||||
SHA-256-hashes outputs against an expected hash, an append-only witness ledger,
|
||||
and a live Hugging Face Space. ADR-145's ablation harness already computes
|
||||
presence accuracy, localization error, FP/FN, latency percentiles, a
|
||||
privacy-leakage score, and **cross-room degradation**. The board is
|
||||
intentionally empty (benchmark-first). What is missing is not a scorer but a
|
||||
**scorecard format** that reports per-domain rather than pooled, and a
|
||||
**sensing-crate CI gate** that runs it on every PR.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Keep the single pooled score / `RuViewTier`.** Rejected: it is exactly the
|
||||
surface a per-domain regression hides behind; a Gold tier can coexist with a
|
||||
broken unseen-room slice.
|
||||
2. **Add a new benchmark repo/harness for domains.** Rejected: AetherArena's
|
||||
scorer, determinism binding, and witness ledger already exist and are the
|
||||
right engine; a parallel harness would fork the scoring substrate and its
|
||||
anti-gaming/leakage discipline.
|
||||
3. **Extend the AetherArena scorer with a per-domain scorecard and wire it as a
|
||||
per-PR sensing-crate gate.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Reuse the AetherArena scorer and witness ledger (ADR-149) and add two things: a
|
||||
**multi-domain scorecard** format and a **sensing-crate PR gate** that produces
|
||||
it.
|
||||
|
||||
### 1. The multi-domain scorecard
|
||||
|
||||
The scorecard reports each capability broken out by operating domain, never
|
||||
pooled into one figure. The v0 domain axes:
|
||||
|
||||
- **Presence**: `room-known`, `room-unseen`, `device-unseen`, `stationary-10m`
|
||||
(a stationary subject at range — the canonical WiFi failure case).
|
||||
- **Pose**: `matched`, `subject-unseen`, `room-unseen`.
|
||||
- **OOD rejection**: the rate at which genuinely out-of-distribution input is
|
||||
correctly returned as UNKNOWN by ADR-302 (a capability, not a failure) and
|
||||
the false-UNKNOWN rate on in-distribution input.
|
||||
- **Calibration drift**: fingerprint-distance trajectory against the ADR-301
|
||||
certificate over the scored window, and the fraction of inferences in each
|
||||
ADR-302 `DomainState` (KNOWN / DEGRADED / UNKNOWN).
|
||||
|
||||
Each cell carries exactly one `EvidenceLevel` (L0–L5, ADR-282). A slice scored
|
||||
on synthetic input is L0/`Synthetic` by construction; a slice on a leakage-free
|
||||
held-out real split is graded higher and only then may a per-domain number be
|
||||
labelled MEASURED. Pose PCK cells additionally require the mean-pose baseline
|
||||
and a leakage-free held-out split (CLAUDE.md) or they are not reported as pose
|
||||
accuracy at all.
|
||||
|
||||
### 2. Per-domain regression gate
|
||||
|
||||
- The gate compares each scorecard cell against the merged-baseline scorecard
|
||||
stored in the AetherArena witness ledger. A regression **in any single
|
||||
domain** beyond its configured threshold fails the PR, even if the pooled
|
||||
average improved. Improvement on `room-known` cannot buy a regression on
|
||||
`room-unseen`.
|
||||
- Thresholds are per-domain and per-capability; the unseen/stationary/OOD
|
||||
domains carry the strictest budgets because they are the ones a pooled score
|
||||
hides. The baseline is append-only and witness-anchored — a new baseline is a
|
||||
new signed ledger entry, never an in-place overwrite (ADR-149 ledger pattern,
|
||||
ADR-319 anchoring).
|
||||
|
||||
### 3. Sensing-crate CI wiring
|
||||
|
||||
- Every PR that touches a sensing crate runs the scorecard across all domains
|
||||
under the ADR-011/ADR-149 determinism binding (`PROOF_SEED=42`), so the run
|
||||
is reproducible and tamper-evident. The gate is added to
|
||||
`.github/workflows/` as an authoritative check.
|
||||
- The held-out real split remains private and is never accessible to synthetic
|
||||
generation, augmentation, or calibration (ADR-149 leakage constraint, ADR-282
|
||||
rule d). Submitters/PRs provide a model, not predictions on data they hold.
|
||||
|
||||
### Provenance and honesty discipline
|
||||
|
||||
- No benchmark numbers are invented by this ADR. It delivers the scorecard
|
||||
format, the per-domain gate, and the CI wiring; the numbers come from the
|
||||
ADR-304 evidence ledger and the AetherArena scorer on real data, labelled at
|
||||
the honest evidence level. Empty domains report "no evidence," which the gate
|
||||
treats as no coverage — never as a pass.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A domain-generalization regression can no longer merge behind a flattering
|
||||
pooled average; the failure mode that most distinguishes fielded sensing from
|
||||
a demo is caught at PR time.
|
||||
- Every PR touching sensing pays a per-domain scoring cost. Bounded by reusing
|
||||
the existing deterministic scorer and by tiered compute (CPU smoke vs full
|
||||
score, ADR-149), but it is a deliberate cost for per-domain safety.
|
||||
- The empty AetherArena board fills with honest, per-domain, evidence-labelled
|
||||
results rather than a single headline tier — consistent with the
|
||||
benchmark-first posture and with ADR-282's ecosystem positioning.
|
||||
- Some domains will show weak or absent coverage. Surfacing that per-domain is
|
||||
the point; the scorecard must never paper over a thin domain with a pooled
|
||||
number.
|
||||
- The program-level acceptance test (ADR-300) is encoded here as an AetherArena
|
||||
scenario, closing the loop once the phase-1 spine lands.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test` on the AetherArena scorer extension — per-domain slicing math
|
||||
against fixtures; per-domain regression gate fails on a single-domain
|
||||
regression while pooled improves, and passes when all domains hold; empty
|
||||
domains report "no evidence," not a pass; every cell carries exactly one
|
||||
`EvidenceLevel`; synthetic slices are L0 by construction.
|
||||
- Determinism: a scored run reproduces its SHA-256 hash under `PROOF_SEED=42`
|
||||
(ADR-011/ADR-149 binding); the baseline scorecard is append-only and
|
||||
witness-anchored (ADR-319), never mutated in place.
|
||||
- CI: the sensing-crate gate runs on a PR touching a sensing crate and blocks a
|
||||
planted single-domain regression.
|
||||
- Real-data scorecards (a leakage-free held-out split with ADR-303 references)
|
||||
are the maturity milestone; a synthetic scorecard is L0 and no per-domain
|
||||
number is MEASURED without a reproducer per CLAUDE.md.
|
||||
138
docs/adr/ADR-318-capability-certificates.md
Normal file
138
docs/adr/ADR-318-capability-certificates.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# ADR-318: Capability certificates — validated-for-this-environment claims
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: capability, certificate, evidence, provenance, signature, honesty, substrate
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is primitive 18 of the perception-substrate program (ADR-300) and,
|
||||
per the strategic assessment, among the strongest ideas in the program: it is
|
||||
where the whole certificate spine becomes a consumable contract. In the ADR-300
|
||||
dependency DAG it **consumes the evidence engine (ADR-304)** — a capability
|
||||
certificate is a signed attestation minted over a slice of that ledger — the
|
||||
**calibration certificate (ADR-301)** for the environment it is validated
|
||||
against, and the **RuField signature types (ADR-305 / ADR-260/262/277/279)** to
|
||||
sign it. It reports domain state via ADR-302 and is anchored in the witness
|
||||
chain (ADR-319).
|
||||
|
||||
RuView must stop making unconditional capability claims. "Supports presence" is
|
||||
not a true statement — presence detection works in some rooms, on some hardware,
|
||||
for some subject dynamics, and fails on a stationary subject at range in an
|
||||
uncalibrated room. A capability is only ever *validated for a specific
|
||||
environment*, and the honest unit of that claim is a signed, expiring
|
||||
certificate, not a feature flag in a README.
|
||||
|
||||
The ingredients now exist across the phase-1 spine: ADR-304 accumulates
|
||||
per-`(room, device, subject)` accuracy, false-positive rate, drift, and domain
|
||||
state; ADR-301 produces the signed room fingerprint the environment is keyed to;
|
||||
ADR-305 provides the authenticated device identity and `CapabilityAttestation`
|
||||
(BFLD, ADR-141) that bounds *what a device is even attested to sense*; ADR-282
|
||||
provides the mandatory `EvidenceLevel`. What is missing is the artifact that
|
||||
binds them into a single, verifiable "validated here, until then" claim and the
|
||||
consumer-side rule that refuses capabilities lacking one.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Static capability flags / a `supports_presence` boolean.** Rejected: it is
|
||||
the exact dishonest claim — environment-independent, unsigned, non-expiring,
|
||||
and false the moment the room, device, or subject dynamics differ.
|
||||
2. **Report raw ledger accuracy to consumers directly.** Rejected: the ledger
|
||||
(ADR-304) is the source of truth but not a portable, signed, bounded contract;
|
||||
handing consumers raw records pushes evidence-weighting and expiry logic into
|
||||
every consumer and drops the single verifiable object.
|
||||
3. **Mint a signed, expiring `CapabilityCertificate` over an ADR-304 ledger
|
||||
slice, and make consumers refuse capabilities without a valid one.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce a signed **`CapabilityCertificate`**: a bounded attestation that a
|
||||
specific capability has been validated for a specific environment, for a bounded
|
||||
time.
|
||||
|
||||
### 1. The certificate
|
||||
|
||||
A serializable `CapabilityCertificate` binding:
|
||||
|
||||
- `capability` — the phenomenon (e.g. `presence`, `pose`), which must be within
|
||||
the device's ADR-305/ADR-141 `CapabilityAttestation` (a device cannot be
|
||||
certified for something it is not even attested to sense).
|
||||
- `room` — the ADR-306 space identifier, tied to the ADR-301 calibration
|
||||
certificate version the validation was performed against.
|
||||
- `hardware` — the ADR-305 authenticated `DeviceId` (and, in phase 2, the
|
||||
ADR-320 HAL descriptor of the sensor).
|
||||
- `model` — the model version scored.
|
||||
- `calibrated_date` — the calibration certificate age at validation time.
|
||||
- `moving_recall`, `stationary_recall`, `false_presence_per_24h` — the measured
|
||||
operating metrics, sliced from the ADR-304 ledger for this exact context (not
|
||||
a global average), each honestly labelled. These are per-capability; a pose
|
||||
certificate carries pose metrics with the mean-pose baseline and a
|
||||
leakage-free split (CLAUDE.md) or it is not issued.
|
||||
- `valid_until` — an explicit expiry; a certificate is never open-ended.
|
||||
- `evidence_level` — exactly one L0–L5 (ADR-282). A certificate minted from a
|
||||
synthetic ledger slice is L0/`Synthetic`; a MEASURED metric requires an
|
||||
ADR-303 reference and a reproducer. The certificate cannot upgrade the level
|
||||
of the ledger it is minted from (ADR-304 honesty rule).
|
||||
- `signature` — a RuField `SignatureBlock` (ADR-305 / ADR-260/262/277/279) over
|
||||
the canonical serialization; an unsigned certificate is not a valid
|
||||
certificate. The certificate is anchored in the witness chain (ADR-319).
|
||||
|
||||
### 2. Minting
|
||||
|
||||
- A certificate is minted from a slice of the ADR-304 evidence ledger for one
|
||||
`(room, device, subject-class, model)` context. If the ledger reports "no
|
||||
evidence" for that context, **no certificate is issued** — absence of evidence
|
||||
is never a capability. Minting is a pure function over the append-only ledger
|
||||
at mint time; the metrics are frozen into the signed object.
|
||||
- Expiry (`valid_until`) is derived from calibration validity (ADR-301) and an
|
||||
evidence-freshness policy: a certificate cannot outlive the calibration it was
|
||||
validated against, and drift beyond the ADR-301 envelope invalidates both.
|
||||
|
||||
### 3. Consumer refusal rule
|
||||
|
||||
- Applications and surfaces **refuse to consume a capability that lacks a valid
|
||||
certificate for the current environment**. "Valid" means: signature verifies,
|
||||
`room`/`hardware`/`model` match the running context, `valid_until` is in the
|
||||
future, and the referenced calibration certificate is itself still valid
|
||||
(ADR-301 not invalidated). A failed check yields UNKNOWN via ADR-302, not a
|
||||
best-effort guess.
|
||||
- This makes the ADR-300 acceptance clause "quantify whether it can reliably
|
||||
sense the requested phenomenon → generate a signed capability certificate"
|
||||
a hard gate rather than a hope.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView can no longer claim a capability it has not validated for the caller's
|
||||
environment; the honest failure — "not certified here" → UNKNOWN — is
|
||||
surfaced by construction rather than by discipline.
|
||||
- OEM/integrator diligence gets a single verifiable artifact ("presence,
|
||||
validated in *this* room, on *this* device, with *these* recall/false-alarm
|
||||
numbers, until *this* date, at *this* evidence level, signed") — the strongest
|
||||
commercial output of the spine.
|
||||
- Certificates expire and get refused; some environments will have no
|
||||
certificate and therefore no capability until validated. That refusal is the
|
||||
intended honest behavior, not a regression.
|
||||
- Key management and expiry policy are operational responsibilities, reusing the
|
||||
ADR-305 enrollment/rotation and ADR-301 validity machinery rather than new
|
||||
infrastructure; fleet distribution of certificates is owned by ADR-316.
|
||||
- No capability number is invented here; every metric on a certificate is sliced
|
||||
from the ADR-304 ledger at its honest evidence level.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test` on the certificate crate — mint from a ledger slice produces the
|
||||
frozen metrics; "no evidence" context yields no certificate; signature
|
||||
round-trip and tamper rejection; `valid_until` and calibration-linked expiry
|
||||
enforced; consumer refusal on room/hardware/model mismatch, expiry, or
|
||||
invalidated calibration resolves to UNKNOWN (ADR-302), not a guess; evidence
|
||||
level is inherited from the ledger and cannot be upgraded; a certificate
|
||||
cannot be issued for a capability outside the device's ADR-305/ADR-141
|
||||
attestation.
|
||||
- Cross-ADR: an ADR-304 ledger fixture mints a certificate; an ADR-302 test
|
||||
asserts an expired/mismatched certificate gates to UNKNOWN; the ADR-300
|
||||
acceptance test consumes a minted certificate end-to-end.
|
||||
- Real-deployment certificates (minted from a populated ledger with ADR-303
|
||||
references on live ESP32 captures) are the maturity milestone and require
|
||||
hardware evidence per CLAUDE.md; a certificate minted from a synthetic ledger
|
||||
is L0 by construction.
|
||||
146
docs/adr/ADR-319-witness-chain.md
Normal file
146
docs/adr/ADR-319-witness-chain.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# ADR-319: Witness chain — epistemic infrastructure for physical AI
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: provenance, witness, evidence, signature, epistemics, ontology, substrate
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is primitive 19 of the perception-substrate program (ADR-300) and a
|
||||
spine root of its phase-1 certificate stack. In the ADR-300 dependency DAG it
|
||||
**extends the source-provenance state machine (ADR-295)** and the RuField
|
||||
provenance types, **ties to the signature machinery (ADR-305 /
|
||||
ADR-260/262/277/279)**, and anchors the artifacts produced by ADR-301
|
||||
(calibration certificates), ADR-304 (evidence records), ADR-317 (benchmark
|
||||
scorecards), and ADR-318 (capability certificates). In phase 2 it carries the
|
||||
independent-corroboration link from ADR-303.
|
||||
|
||||
The strategic assessment (ADR-300) framed RuView's real product as **epistemic
|
||||
infrastructure for physical AI**: the value is not the claim "a person is
|
||||
present" but the *auditable reasoning* behind it. A bare boolean output discards
|
||||
everything a downstream system needs to trust or contest it — which radio
|
||||
observed it, what DSP evidence supported it, which model inferred it, whether an
|
||||
independent sensor agreed, what spatial state it updated, and what policy acted
|
||||
on it. Once the answer is a boolean, "why do you believe that?" has no answer.
|
||||
|
||||
RuView already has the pieces of a chain but not the chain itself. ADR-295
|
||||
defines a canonical `SourceState` (`Synthetic` / `LiveVerified` /
|
||||
`LiveUnverified` / `Stale` / `Disconnected`) with `Unknown` structurally
|
||||
forbidden from collapsing to live. ADR-305 defines the signed
|
||||
`device → measurement → sequence → timestamp → … → signed event` chain of
|
||||
custody. RuField carries `FrameProvenance`, `SemanticProvenance`, and signature
|
||||
types; the AetherArena witness ledger (ADR-149) demonstrates an append-only,
|
||||
witness-anchored ledger. What is missing is a single **staged, signed envelope**
|
||||
that travels the whole pipeline and records, at each stage, the confidence and
|
||||
provenance of that stage.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Keep provenance as scattered per-stage fields (status quo).** Rejected:
|
||||
`FrameProvenance`, `SourceState`, calibration state, and model uncertainty
|
||||
live in different structures and are re-encoded per surface; there is no
|
||||
single object a consumer can re-verify offline to answer "why."
|
||||
2. **Log a free-form audit trail alongside the output.** Rejected: mutable,
|
||||
unsigned, and not structurally tied to the output — the classic
|
||||
dashboard-that-overwrites-yesterday failure the evidence engine (ADR-304)
|
||||
already rejects.
|
||||
3. **A staged, signed witness envelope carried through the pipeline, each stage
|
||||
appended and signed, anchored in an append-only ledger.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Define the **witness chain**: a staged, append-only, signed envelope that
|
||||
accompanies an observation from radio to policy decision. Instead of emitting
|
||||
"person present," RuView emits a chain whose stages are:
|
||||
|
||||
```
|
||||
RF observation ▸ DSP evidence ▸ model inference ▸ independent corroboration
|
||||
▸ spatial state ▸ policy decision
|
||||
```
|
||||
|
||||
### 1. The staged envelope
|
||||
|
||||
- Each stage is a signed record carrying its **confidence** and its
|
||||
**provenance**:
|
||||
- **RF observation** — the ADR-305 authenticated frame envelope
|
||||
(`DeviceId`, sequence, timestamp, measurement hash) and its ADR-295
|
||||
`SourceState`. This is the root link; a `Synthetic` root can never present
|
||||
as a `LiveVerified` one (ADR-295 invariant).
|
||||
- **DSP evidence** — the deterministic signal features and the ADR-137
|
||||
quality signals that support (or fail to support) an inference.
|
||||
- **model inference** — the model version, its raw output, and its predictive
|
||||
uncertainty; the ADR-302 `DomainState` (KNOWN / DEGRADED / UNKNOWN) gate
|
||||
result, so a low-confidence or out-of-distribution inference is recorded as
|
||||
such, not silently promoted.
|
||||
- **independent corroboration** — the phase-2 ADR-303 agreement link
|
||||
(a reference/second modality that agreed or disagreed); absent in phase 1,
|
||||
the stage records "no corroboration," never a fabricated one.
|
||||
- **spatial state** — the ADR-306 ontology `Observation`/`Track`/`Event` the
|
||||
inference updated, carrying `SemanticProvenance` and its `EvidenceLevel`.
|
||||
- **policy decision** — the governed action taken (or withheld), with the
|
||||
certificate (ADR-318) it relied on.
|
||||
- Each stage carries exactly one `EvidenceLevel` (L0–L5, ADR-282); the envelope's
|
||||
effective level is the **minimum** across its stages — a synthetic root or an
|
||||
unreferenced inference caps the whole chain, so the chain cannot claim more
|
||||
than its weakest link.
|
||||
|
||||
### 2. Signing and anchoring
|
||||
|
||||
- Each stage is signed with RuField signature types (ADR-305 /
|
||||
ADR-260/262/277/279) over the canonical serialization of that stage plus the
|
||||
hash of the prior stage, so the chain is tamper-evident end to end and any
|
||||
broken link is detectable. The completed chain is anchored in an append-only,
|
||||
witness-anchored ledger following the AetherArena pattern (ADR-149); it is the
|
||||
same anchoring ADR-301/ADR-304/ADR-317/ADR-318 write into.
|
||||
- The chain is **append-only**: a correction is a new chain referencing the
|
||||
prior one, never an in-place edit (mirroring ADR-304 and CLAUDE.md's "source
|
||||
over summaries").
|
||||
|
||||
### 3. Offline re-verification
|
||||
|
||||
- A consumer with the enrolled public keys (ADR-305) can re-verify a chain
|
||||
offline: check each stage signature, check each prior-stage hash, and read the
|
||||
per-stage confidence and evidence level — answering "why do you believe this?"
|
||||
without trusting the emitting host. This is the property store-and-forward
|
||||
channel authentication (rejected in ADR-305) cannot provide.
|
||||
|
||||
### Provenance and honesty discipline
|
||||
|
||||
- The witness chain never manufactures confidence: a stage that lacks evidence
|
||||
records the absence. A `Synthetic` root, a missing corroboration, or an
|
||||
UNKNOWN gate is carried faithfully and caps the chain's evidence level. No
|
||||
accuracy number is invented here; the chain records the numbers the other
|
||||
primitives produce at their honest level.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every RuView output becomes contestable and auditable: a downstream physical-AI
|
||||
system can inspect the reasoning, weight it by per-stage confidence, and reject
|
||||
a chain whose weakest link is too weak — the defining property of epistemic
|
||||
infrastructure the strategic assessment asked for.
|
||||
- The certificate spine (ADR-301/301/314/315) gains a single anchoring substrate;
|
||||
each of those artifacts is a specialization of a witness record rather than a
|
||||
bespoke signed blob.
|
||||
- Carrying and signing a staged envelope adds per-observation size and CPU cost;
|
||||
bounded by reusing RuField signatures and the existing ledger, and by the
|
||||
minimum-level rule keeping the object honest rather than exhaustive.
|
||||
- The chain will frequently reveal weak links (synthetic root, no corroboration,
|
||||
DEGRADED gate). Surfacing that is the point; the envelope must never smooth a
|
||||
weak stage into a confident summary.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test` on the witness-chain crate — stage-by-stage signature round-trip
|
||||
and tamper rejection (a mutated stage or a broken prior-stage hash fails
|
||||
verification); effective evidence level equals the minimum across stages; a
|
||||
`Synthetic` root caps the chain and cannot present as `LiveVerified`
|
||||
(ADR-295 invariant); an UNKNOWN gate (ADR-302) and a "no corroboration" stage
|
||||
are recorded faithfully; append-only correction produces a new chain
|
||||
referencing the prior one.
|
||||
- Cross-ADR: an ADR-305 signed frame lineage serializes into a chain that
|
||||
re-verifies offline with only the enrolled public keys; ADR-301/301/314/315
|
||||
artifacts anchor into the same ledger.
|
||||
- Real-deployment chains (from live ESP32 captures with ADR-303 corroboration)
|
||||
are the maturity milestone and require hardware evidence per CLAUDE.md; a
|
||||
chain rooted in synthetic input is L0 by construction.
|
||||
150
docs/adr/ADR-320-sensor-hal.md
Normal file
150
docs/adr/ADR-320-sensor-hal.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# ADR-320: RuView sensor HAL — abstract all sensing hardware to one Observation type
|
||||
|
||||
- **Status**: Accepted — initial implementation (ADR-300 phase 2)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: hal, sensor-abstraction, ontology, fusion, adapters, category, phase-2
|
||||
|
||||
## Context
|
||||
|
||||
This ADR is primitive 20 of the perception-substrate program (ADR-300) and a
|
||||
phase-2 integration primitive; it is authored as **Proposed**. In the ADR-300
|
||||
DAG it **consumes the canonical spatial ontology (ADR-306)** — its output is an
|
||||
ontology `Observation` bound to a `Sensor` entity — and **feeds real sensor
|
||||
fusion (ADR-311)**, which resolves many observations into one world state. It
|
||||
closes the "identify the hardware" clause of the ADR-300 acceptance test that
|
||||
phase 1 leaves open.
|
||||
|
||||
RuView's strategic ceiling is set by how tightly it is coupled to WiFi CSI.
|
||||
Every new modality today lands as a bespoke ingest path with its own frame
|
||||
shape, its own provenance handling, and its own place in the pipeline. That is
|
||||
the difference between "a WiFi-DensePose project" and "an open
|
||||
spatial-intelligence operating layer": the category changes the moment *any*
|
||||
sensing hardware — {CSI, 802.11bf, BLE, UWB, mmWave, acoustic, camera, lidar,
|
||||
IMU, custom} — enters through one abstraction and becomes one `Observation`
|
||||
feeding one world model.
|
||||
|
||||
Crucially this is a *unification*, not a green field. Adapters already exist and
|
||||
must be reused, not rebuilt:
|
||||
|
||||
- ADR-279's native RF frame contract (`RfFrameV2`) already unifies ESP32,
|
||||
Intel, Atheros, PicoScenes, Realtek radar, and 320 MHz 802.11bk producers as
|
||||
`RfFrameV2` producers into a shared latent — "lightweight per-device adapters
|
||||
into a shared latent, not a shared tensor." The HAL generalizes that lesson
|
||||
beyond RF.
|
||||
- Existing CSI adapters (ESP32/Nexmon/FeitCSI paths), the mmWave fusion path
|
||||
(ADR-063), and the multistatic WiFi path (ADR-029) are concrete producers to
|
||||
bring under one trait.
|
||||
- ADR-305 already authenticates a `Sensor`/`DeviceId`; ADR-306 already defines
|
||||
`Sensor`, `Observation`, `Track`, and `Event` as first-class node types. The
|
||||
HAL is the trait that turns a heterogeneous device into that authenticated
|
||||
`Sensor` emitting those `Observation`s.
|
||||
|
||||
The gap is a single **`SensorHal` trait and one `Observation` type** that every
|
||||
modality implements, so the world model never sees a modality-specific frame —
|
||||
only a provenance-bearing, evidence-labelled `Observation`.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Continue adding per-modality ingest paths.** Rejected: O(modalities) bespoke
|
||||
pipelines, each re-encoding provenance and evidence, each a place the ladder
|
||||
can be dropped — and it keeps RuView categorically a WiFi project.
|
||||
2. **Force every modality into the ADR-274/279 RF tensor/frame.** Rejected: the
|
||||
ADR-279 lesson is precisely that premature canonicalization discards
|
||||
information (bandwidth, antenna structure, phase). A camera, lidar, or IMU
|
||||
has no meaningful `RfFrameV2` projection; forcing one is the same mistake at a
|
||||
larger scale.
|
||||
3. **Define a `SensorHal` trait producing one `Observation` type, with existing
|
||||
adapters as implementations feeding a shared latent and the ADR-306
|
||||
ontology.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce a **`SensorHal` trait** and a single **`Observation`** type. Every
|
||||
sensing modality is an implementation of the trait; the world model consumes
|
||||
only `Observation`s.
|
||||
|
||||
### 1. The `SensorHal` trait
|
||||
|
||||
- A `SensorHal` describes a device's **capabilities** (which phenomena it can
|
||||
sense — reusing the ADR-305/ADR-141 `CapabilityAttestation`), its **native
|
||||
frame** (kept native, not canonicalized, per the ADR-279 shared-latent
|
||||
lesson), and a method that lifts a native frame into an `Observation`.
|
||||
- Implementations wrap the existing producers: CSI (ESP32/Nexmon/FeitCSI via the
|
||||
ADR-279 `RfFrameV2` path), 802.11bf (ADR-310, phase 2), BLE, UWB, mmWave
|
||||
(ADR-063), acoustic, camera, lidar, IMU, and `custom`. RF modalities reuse the
|
||||
ADR-279 per-device latent adapters wholesale; the HAL adds the non-RF and
|
||||
ranging modalities under the same trait.
|
||||
- The trait is the boundary where untrusted hardware input is validated
|
||||
(CLAUDE.md: validate at every hardware/FFI boundary; default to least
|
||||
authority). A device is authenticated as an ADR-305 `Sensor` before its
|
||||
observations are trusted.
|
||||
|
||||
### 2. The `Observation` type
|
||||
|
||||
- One provenance-bearing `Observation`: a measurement plus its `SensorHal`
|
||||
source descriptor, its ADR-305 authenticated `DeviceId`, its ADR-295
|
||||
`SourceState`, its native-frame reference (not a lossy projection), and
|
||||
exactly one `EvidenceLevel` (L0–L5, ADR-282). A camera-derived `Observation`
|
||||
and a CSI-derived `Observation` are the same type with different provenance —
|
||||
and a camera observation never lifts WiFi output to camera-grade; each carries
|
||||
its own honest evidence level (CLAUDE.md: never present WiFi sensing as
|
||||
camera-grade).
|
||||
- The `Observation` maps directly onto the ADR-306 ontology `Observation` node
|
||||
attached to its `Sensor`, so the ontology is the one representation and the
|
||||
HAL is its ingest funnel.
|
||||
|
||||
### 3. Feeding fusion
|
||||
|
||||
- Observations from any set of modalities flow into ADR-311 fusion, which
|
||||
resolves them into one probabilistic world state. The HAL guarantees fusion
|
||||
never sees a modality-specific frame — only `Observation`s with uniform
|
||||
provenance and evidence — which is what makes ADR-311's "many observations →
|
||||
one world state" invariant implementable across heterogeneous hardware.
|
||||
|
||||
### Category and honesty discipline
|
||||
|
||||
- This ADR changes RuView's category from a WiFi-DensePose pipeline to an open
|
||||
spatial-intelligence operating layer, but it makes **no accuracy claim**: the
|
||||
HAL delivers a uniform ingest boundary, not a detector. Any capability of a
|
||||
newly-connected sensor is still gated by ADR-302 and certified by ADR-318 for
|
||||
its specific environment — connecting a camera does not grant a validated
|
||||
capability by itself.
|
||||
- Hardware support for a given modality is CLAIMED until demonstrated on real
|
||||
silicon with captured evidence per CLAUDE.md; a passing trait test proves the
|
||||
abstraction, not a fielded device.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New sensing hardware lands as one `SensorHal` implementation instead of a
|
||||
bespoke pipeline; the translation matrix stays O(modalities), mirroring how
|
||||
ADR-306 collapsed the surface matrix.
|
||||
- The ADR-300 acceptance clause "identify the hardware" becomes implementable:
|
||||
a new sensor type is described by its HAL, authenticated as an ADR-305
|
||||
`Sensor`, calibrated (ADR-301), gated (ADR-302), and certified (ADR-318)
|
||||
through the same phase-1 spine, closing the last open clause.
|
||||
- A trait boundary and an `Observation` type are added; existing RF adapters
|
||||
are re-expressed as implementations rather than rewritten, preserving the
|
||||
ADR-279 native-frame/shared-latent design.
|
||||
- Non-RF modalities (camera, lidar, acoustic) enter the governed plane with the
|
||||
same provenance and privacy discipline as RF; a camera is not a privacy-free
|
||||
shortcut — it inherits the ADR-277 governance and its own evidence level.
|
||||
- As a phase-2 Proposed ADR, the trait shape may be revised as ADR-311 fusion
|
||||
and ADR-310 802.11bf land; that revision is expected for a phased program.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test` on the HAL crate (design-time, Proposed) — a fixture `SensorHal`
|
||||
for each of at least two modalities (CSI via ADR-279, plus one non-RF)
|
||||
produces uniform `Observation`s; every `Observation` carries a `DeviceId`,
|
||||
`SourceState`, native-frame reference, and exactly one `EvidenceLevel`; a
|
||||
synthetic source yields L0/`Synthetic` and cannot alias to measured
|
||||
(ADR-279 invariant 6); an unauthenticated device's observations are rejected
|
||||
at the trait boundary (ADR-305).
|
||||
- Cross-ADR: an `Observation` maps round-trip to an ADR-306 ontology
|
||||
`Observation` node with no provenance loss, and a set of `Observation`s from
|
||||
distinct modalities is accepted by an ADR-311 fusion fixture.
|
||||
- Real-silicon evidence is required before any modality's hardware support is
|
||||
claimed beyond CLAIMED: a captured boot/runtime log from the real device
|
||||
emitting `Observation`s. A successful build or simulator run is not hardware
|
||||
evidence (CLAUDE.md).
|
||||
101
docs/adr/ADR-321-decision-policy-action-authorization.md
Normal file
101
docs/adr/ADR-321-decision-policy-action-authorization.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# ADR-321: Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, and evidence
|
||||
|
||||
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: policy, authorization, safety, certificates, governed-action, phase-1
|
||||
|
||||
## Context
|
||||
|
||||
The perception substrate (ADR-300) makes RuView state *what it knows* and
|
||||
*how well* — the capability certificate (ADR-318) binds hardware, environment,
|
||||
model, calibration, metrics, expiry, and evidence level. But a certificate is a
|
||||
statement of knowledge, not a grant of action. The same certificate that is
|
||||
adequate to dim a light is wholly inadequate to release a door lock or clear an
|
||||
industrial stop condition.
|
||||
|
||||
Without an explicit authorization layer, every consumer re-implements its own
|
||||
(inconsistent, usually optimistic) rule for "is this good enough to act on,"
|
||||
and a confident-but-out-of-domain inference can reach an actuator. That is the
|
||||
exact failure the substrate exists to prevent. Decision policy therefore
|
||||
belongs in **phase 1**, alongside the certificate it gates, not later.
|
||||
|
||||
This ADR realizes program invariant #1 (UNKNOWN is a first-class output, never
|
||||
an error) and the action-side of the refined acceptance test: a drift-
|
||||
invalidated capability must be *denied at the actuator* before a false
|
||||
confident inference is acted upon.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce a `ruview-policy` crate providing an **action authorization gate**
|
||||
that sits between governed spatial state and any actuator.
|
||||
|
||||
### 1. Assurance requirements per action
|
||||
|
||||
An `ActionClass` declares the assurance an action demands:
|
||||
|
||||
- `min_certificate_class` — the required `CapabilityCertificate` class (ADR-318).
|
||||
- `max_certificate_age` / `min_domain_freshness` — the certificate must be
|
||||
currently valid **and** the live domain signature (ADR-302) must not be in a
|
||||
DEGRADED/UNKNOWN state (this is the staleness guard, program invariant on
|
||||
certificate conditionality — see ADR-300).
|
||||
- `max_uncertainty` — inference uncertainty ceiling.
|
||||
- `min_evidence_level` — the L0–L5 floor (ADR-282/ADR-304); e.g. a safety
|
||||
action may require ≥ L3 (held-out room+subject validation).
|
||||
|
||||
Reference action classes (illustrative, configurable):
|
||||
|
||||
| Class | Example | Typical floor |
|
||||
|---|---|---|
|
||||
| `Convenience` | lighting, scenes | tolerant: L1+, higher uncertainty ok |
|
||||
| `Security` | alerts, arming | stricter: valid cert, L2+, bounded uncertainty |
|
||||
| `SafetyCritical` | door lock, machine stop | strict: fresh cert, L3+, low uncertainty, KNOWN domain only |
|
||||
|
||||
### 2. The authorization decision
|
||||
|
||||
`authorize(action, capability_certificate, live_state) -> Authorization` where
|
||||
`live_state` carries the current `SourceState` (ADR-295), OOD/domain state
|
||||
(ADR-302), and inference uncertainty. Rules:
|
||||
|
||||
- **Fail-closed.** Any unmet condition → `Deny { failed_condition }`. The denial
|
||||
names the *specific* condition (expired cert, domain DEGRADED, uncertainty
|
||||
over ceiling, evidence below floor, certificate class too low).
|
||||
- **UNKNOWN denies high-assurance actions.** A domain in UNKNOWN (ADR-302)
|
||||
cannot authorize `Security`/`SafetyCritical` actions; it may still authorize
|
||||
`Convenience` if that class's policy permits, but the authorization records
|
||||
that it proceeded under UNKNOWN.
|
||||
- The decision is a **pure function** of (action class, certificate, live
|
||||
state) — deterministic and unit-testable without a clock or actuator.
|
||||
- Every authorization (allow or deny) is emitted as the terminal stage of the
|
||||
witness chain (ADR-319), so "why was this actuator allowed/denied" is
|
||||
auditable end-to-end.
|
||||
|
||||
### 3. No silent optimism
|
||||
|
||||
A missing certificate, an expired certificate, or an unrecognized action class
|
||||
all deny by default. Absence of a policy is not permission.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Action authorization becomes uniform and centrally reasoned instead of
|
||||
per-consumer and optimistic; this is the "RuView Certify → constrains action"
|
||||
boundary that is hard to commoditize.
|
||||
- A behavior change for existing automations that acted directly on presence:
|
||||
they now pass through the gate. Convenience-class defaults keep low-stakes
|
||||
automations working; high-stakes actions must opt into stricter classes.
|
||||
- Depends on ADR-318 (certificate), ADR-302 (domain/OOD state), ADR-295
|
||||
(source state), ADR-304 (evidence). Built in the phase-1 dependent wave after
|
||||
those types land.
|
||||
|
||||
## Validation
|
||||
|
||||
- Unit tests: each action class authorizes/denies correctly across the matrix
|
||||
of (valid/expired/degraded cert × KNOWN/DEGRADED/UNKNOWN domain × uncertainty
|
||||
above/below ceiling × evidence above/below floor); UNKNOWN denies
|
||||
safety-critical; every deny names its failed condition; absence-of-policy
|
||||
denies; determinism.
|
||||
- Integration: the acceptance-test scenario (ADR-300) — post-certification room
|
||||
change drives domain to DEGRADED→UNKNOWN, and a `SafetyCritical` authorization
|
||||
is denied with `failed_condition = domain_not_known` *before* the inference
|
||||
reaches the actuator, witness chain preserved.
|
||||
- `cargo test -p ruview-policy`.
|
||||
@@ -0,0 +1,364 @@
|
||||
# ADR-323: Native Rust physics-constrained pose refinement
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-08-15
|
||||
- **Deciders**: ruv
|
||||
- **Owners**: RuView perception and edge runtime maintainers
|
||||
- **Tags**: pose, physics, rust, uncertainty, provenance, abstention, edge
|
||||
- **Numbering note**: ADR-323 is the next free number in the authoring checkout. Re-run the ADR index/collision check immediately before merge and rename if needed.
|
||||
- **Extends**: ADR-020, ADR-027, ADR-079, ADR-101, ADR-135, ADR-145, ADR-150, ADR-273, ADR-279, ADR-282, ADR-295, ADR-296, ADR-297, ADR-298, ADR-302, ADR-303, ADR-304, ADR-305, ADR-306
|
||||
- **Supersedes**: None
|
||||
|
||||
## Executive decision
|
||||
|
||||
RuView will add a clean-room native Rust boundary between RF pose inference and
|
||||
semantic publication. It will preserve the immutable RF observation, publish a
|
||||
physics assessment, optionally produce a bounded corrected candidate, and
|
||||
abstain when required evidence is absent. It must never increase observational
|
||||
confidence merely because a pose is physically plausible.
|
||||
|
||||
Three independently gated layers are adopted:
|
||||
|
||||
1. A deterministic kinematic auditor and bounded covariance-weighted projector
|
||||
using Rust and `nalgebra`.
|
||||
2. An optional articulated-body dynamics auditor using `rapier3d`.
|
||||
3. A later optional supervised residual model using Burn.
|
||||
|
||||
The first production milestone is deterministic audit. It is not a GRIP port,
|
||||
not PPO, and not evidence that the current pose observer is production-ready.
|
||||
|
||||
## Context
|
||||
|
||||
ADR-101's committed Cog emits 17 COCO keypoints as normalized 2D coordinates.
|
||||
Its model has no per-joint uncertainty head and publishes a constant confidence.
|
||||
The sensing server also contains renderer-oriented EMA and bone clamping. These
|
||||
surfaces cannot establish metric 3D physics and can make weak evidence look
|
||||
more convincing.
|
||||
|
||||
Pose output can violate bone length, floor, velocity, acceleration, and temporal
|
||||
continuity constraints. Downstream consumers also cannot reliably distinguish
|
||||
observed coordinates from derived correction. The rejected premise is:
|
||||
"physically plausible means more likely correct." Plausibility is only a prior;
|
||||
many incorrect poses are plausible.
|
||||
|
||||
GRIP is architectural inspiration for an observer/controller split, but it
|
||||
observes four wearable IMUs and pressure insoles and drives a simulator. RuView
|
||||
observes RF, so GRIP weights are not input-compatible. External code, weights,
|
||||
simulators, and datasets require independent license review and never enter the
|
||||
runtime dependency graph by implication.
|
||||
|
||||
## Outcome and actors
|
||||
|
||||
For every accepted person track/timestamp, the engine returns exactly one
|
||||
`PoseRefinementV1`, including off, timeout, rejection, and abstention paths:
|
||||
|
||||
- immutable `PoseObservationV2` content hash;
|
||||
- constraint residuals and quality disposition;
|
||||
- an optional bounded candidate and an explicit `selected` bit;
|
||||
- a typed reason when correction is unavailable;
|
||||
- model, calibration, configuration, and optional learned-artifact provenance.
|
||||
|
||||
The RF observer owns observations and calibrated uncertainty; tracking owns
|
||||
identity stability; physics owns assessment/correction only; the sensing server
|
||||
owns deadlines, modes, publication, and rollback; the evidence engine owns
|
||||
release evaluation; clients choose raw/both/refined without silent fallback.
|
||||
|
||||
## Input and coordinate contract
|
||||
|
||||
Metric correction requires a monotonic nanosecond timestamp, session-scoped
|
||||
track ID, sequence and sensor epoch, 17 ordered COCO joints in metric X/Y/Z,
|
||||
per-joint positive-semidefinite covariance calibrated on held-out data, a
|
||||
versioned right-handed Z-up room frame, a normalized upward floor plane, model
|
||||
and calibration hashes, ADR-302 trust state, and authenticated/replay-protected
|
||||
source provenance.
|
||||
|
||||
`Image2d` observations may be audited for image-plane ratios and continuity but
|
||||
must never enter 3D projection/dynamics or be called physically corrected.
|
||||
Unknown trust, missing calibration, missing uncertainty, stale/non-monotonic
|
||||
input, non-finite values, invalid covariance, excessive tracks, and room-bound
|
||||
violations fail to raw output with a typed reason.
|
||||
|
||||
## Public contracts
|
||||
|
||||
`wifi-densepose-core` owns `PoseObservationV2` and `PoseRefinementV1`; no
|
||||
duplicate server/Cog contract is permitted. Public output remains COCO17. The
|
||||
engine derives pelvis and thorax virtually and never labels them observed.
|
||||
|
||||
The raw content hash is deterministic and excludes its own hash field. The
|
||||
idempotency key is `(sensor_epoch, sequence, track_id, raw_hash, config_hash)`.
|
||||
An exact duplicate returns the cached result; same sequence with different
|
||||
content is a replay rejection.
|
||||
|
||||
Contact is `hypothesis` unless a measured sensor and its provenance say
|
||||
otherwise. Raw, derived, hypothesis, and unknown labels must survive every
|
||||
projection.
|
||||
|
||||
## Confidence invariant
|
||||
|
||||
For upstream calibrated confidence `c_obs`, normalized residual `r`, and
|
||||
normalized intervention `i`:
|
||||
|
||||
```text
|
||||
c_physics = exp(-(beta_r * r + beta_i * i))
|
||||
c_effective = min(c_obs, c_obs * c_physics)
|
||||
0 <= c_effective <= c_obs <= 1
|
||||
```
|
||||
|
||||
Only a separately witnessed multimodal fusion contract may increase fused
|
||||
confidence.
|
||||
|
||||
## Deterministic projector
|
||||
|
||||
The default `kinematic` feature has no Rapier, Burn, ONNX, libtorch, Python,
|
||||
CUDA, or network dependency. Per bounded iteration it:
|
||||
|
||||
1. projects observed parent/child distances toward anonymous track-scoped
|
||||
bone-length posteriors;
|
||||
2. applies broad joint/trunk validity checks without an upright prior;
|
||||
3. bounds temporal motion and resets derivatives after gaps;
|
||||
4. resolves floor penetration only, allowing seated, kneeling, prone, child-
|
||||
scale, mobility-aid, and genuine-fall poses;
|
||||
5. recomputes residuals and stops below epsilon.
|
||||
|
||||
Initial operator-owned caps are four iterations (hard maximum eight), 0.20 m
|
||||
single-joint correction, 0.10 m root correction, 250 ms derivative gap, 500 ms
|
||||
track reset, ten known joints, a 100 m metric room bound, a separate 16,384
|
||||
image-coordinate audit bound, and a 5 ms one-track Pi 5 p95 gate. Keeping image
|
||||
and metric bounds separate prevents legitimate pixel observations from
|
||||
weakening the physical room bound. A candidate over either correction cap is
|
||||
discarded in full.
|
||||
|
||||
Bone posteriors are initialized only from high-confidence frames, anonymous,
|
||||
memory-only, track-scoped, and deleted on expiry. Persistent personalization is
|
||||
outside this ADR and requires consent/retention/deletion governance.
|
||||
|
||||
## Optional dynamics and learned layers
|
||||
|
||||
`dynamics` adds a process-owned Rapier humanoid and begins audit-only. Network
|
||||
input may never provide Rapier snapshots, bodies, constraints, solver limits,
|
||||
or arbitrary geometry. Dynamics approval is independent of kinematic approval.
|
||||
|
||||
`learned` uses first-party Burn 0.21 core/NN components without `burn-tch`
|
||||
because this workspace already has a different native libtorch link.
|
||||
`learned-cpu` adds the ndarray backend. The implemented two-layer GRU uses a
|
||||
20-frame history and width 128 to predict bounded residuals, uncertainty,
|
||||
foot-contact hypotheses, and abstention. Verified model records can be loaded
|
||||
from bytes and executed natively; no trained artifact is shipped or approved.
|
||||
The resolved Burn/CubeCL graph declares Rust 1.92, while the workspace file
|
||||
pins Rust 1.89 and the authoring host provides Rust 1.91.1.
|
||||
`--ignore-rust-version` is diagnostic evidence only: learned activation remains
|
||||
blocked until an approved Rust 1.92 release-toolchain change builds it without
|
||||
that override. Residuals are hard-clipped to deterministic caps and cannot
|
||||
bypass validation or confidence monotonicity. PPO is deferred until measured
|
||||
evidence identifies a failure supervised residual learning cannot address.
|
||||
|
||||
## Feature boundary
|
||||
|
||||
```text
|
||||
default = kinematic
|
||||
dynamics = rapier3d
|
||||
learned = burn-core + burn-nn
|
||||
learned-cpu = learned + burn-ndarray
|
||||
learned-train = learned + burn-train
|
||||
learned-wgpu = learned-train + burn-wgpu
|
||||
learned-cuda = learned-train + burn-cuda
|
||||
deterministic = rapier3d?/enhanced-determinism
|
||||
```
|
||||
|
||||
The lockfile is release authority. The learned feature currently requires the
|
||||
toolchain supported by Burn/CubeCL's resolved graph; this does not change the
|
||||
default edge build.
|
||||
|
||||
## Runtime modes and API
|
||||
|
||||
Rollout is `OFF -> AUDIT -> SHADOW_CORRECT -> OPT_IN_CORRECT -> DEFAULT_CORRECT`.
|
||||
Evidence permits forward transitions; any regression returns immediately to
|
||||
audit/off. Correct selection additionally requires authenticated sensor
|
||||
identity and replay protection from ADR-305. High model confidence cannot
|
||||
override missing source authentication.
|
||||
|
||||
Existing pose fields stay unchanged and raw remains the migration default:
|
||||
|
||||
```text
|
||||
GET /api/v1/pose/current?view=raw
|
||||
GET /api/v1/pose/current?view=both
|
||||
GET /api/v1/pose/current?view=refined
|
||||
```
|
||||
|
||||
Refined-only returns HTTP 409 with `pose_refined_unavailable` when no selected
|
||||
candidate exists. It never silently returns raw labeled refined.
|
||||
|
||||
## Security, privacy, and availability
|
||||
|
||||
All frames, model output, geometry, and pre-verification artifacts are
|
||||
untrusted. Calibration/config/model artifacts become trusted only after signed,
|
||||
hash-addressed verification and atomic activation. Runtime inference performs
|
||||
no model retrieval or other network access.
|
||||
|
||||
Fixed arrays/caps, bounded iterations, a maximum track count, room geometry
|
||||
limits, deadlines, and track expiry constrain denial of service. Timeout drops
|
||||
partial refinement, never raw publication. Backpressure retains the newest raw
|
||||
frame per track, drops intermediate refinement work, resets derivatives after
|
||||
250 ms, and never extrapolates beyond 500 ms.
|
||||
|
||||
Metrics contain only allowlisted aggregate scalars: mode/disposition/reason,
|
||||
stage latency, iterations, maximum correction, residuals, confidence delta,
|
||||
track resets, invalid input, timeout, and raw/refined divergence. They exclude
|
||||
joint arrays, body dimensions, room coordinates, CSI, and persistent person
|
||||
identifiers. Bone/gait state is memory-only and excluded from logs.
|
||||
|
||||
Refined output is not a sole medical, emergency, industrial-safety, or
|
||||
autonomous-control source. A real fall is valid state and must never be made
|
||||
upright to stabilize a simulator.
|
||||
|
||||
## Threat model summary
|
||||
|
||||
| Threat | Primary control | Residual risk |
|
||||
|---|---|---|
|
||||
| Spoofed/replayed sensor | ADR-305 identity, MAC, sequence and replay window; correction gate | Compromised legitimate sensor |
|
||||
| Altered model/floor/config | Signed hashes, authenticated configuration, atomic activation | Authorized unsafe configuration |
|
||||
| Poisoned data/splits | Immutable manifests, strict split validator, witnessed benchmarks | Subtle label poisoning |
|
||||
| Operator repudiation | Append-only witnessed transition with actor/old/new hash/reason | Compromised signer |
|
||||
| Biometric/log leakage | Track-local retention and fixed metric allowlist | Aggregate inference |
|
||||
| Track/geometry CPU flood | Authentication, cardinality/geometry/allocation/deadline caps | Valid dense-scene overload |
|
||||
| Remote mode escalation | Capability-scoped local control plane, deny by default | Compromised operator capability |
|
||||
| Derived output relabeled observed | Required schema/provenance and signed event envelope | Malicious downstream stripping |
|
||||
|
||||
The implementation review records commit, lockfile hash, Rust toolchain,
|
||||
scanner versions, and advisory-feed timestamp.
|
||||
|
||||
## Evidence protocol
|
||||
|
||||
Evidence levels are L0 deterministic synthetic, L1 public measured replay, L2
|
||||
controlled RuView RF plus optical truth, L3 subject/room/hardware/session-
|
||||
disjoint RuView, L4 privacy-safe shadow fleet aggregates, and L5 independent
|
||||
vertical validation outside this ADR.
|
||||
|
||||
No sequence, contiguous take, subject, room, or calibration session may cross
|
||||
train/test for the generalization gate. Preprocessing, body priors, and
|
||||
uncertainty calibration fit training data only. Reports include raw observer,
|
||||
renderer smoothing, audit, deterministic correction, dynamics audit, and
|
||||
learned residual on identical observations, plus empty-room, prone/fall,
|
||||
missing-joint, and OOD subsets.
|
||||
|
||||
Primary metrics are 3D MPJPE, declared-threshold PCK, per-joint error, foot
|
||||
slide, floor penetration, jerk, uncertainty calibration, abstention coverage,
|
||||
and selective risk. Learned runs use at least five fixed seeds and report mean,
|
||||
median, standard deviation, and 95% bootstrap intervals. All frames count;
|
||||
selective metrics report risk and coverage.
|
||||
|
||||
## Acceptance gates
|
||||
|
||||
- **G0 contract**: real metric 3D/covariance output, round-trip raw hash,
|
||||
versioned frame/floor, 2D compatibility, non-stub observer, ADR-298 artifact
|
||||
sanity, and the ADR-079 PCK@20 >=35% gate or adopted successor. The current
|
||||
committed Cog does not pass G0, so correction remains unavailable.
|
||||
- **G1 deterministic audit**: property/fuzz tests, deterministic hashes per
|
||||
platform class, 24-hour accelerated replay without panic/growth, Pi 5 p95
|
||||
<=5 ms, and universal confidence monotonicity.
|
||||
- **G2 shadow correction**: strict-disjoint measured median MPJPE improvement
|
||||
>=10% with positive 95% CI lower bound; foot slide >=30% and jerk >=25%
|
||||
better; no joint median >5 mm worse; fall/prone sensitivity change <=2 pp;
|
||||
>=95% corrections below 0.10 m; every correction above 0.20 m abstains.
|
||||
- **G3 opt-in**: >=30 subjects, 10 rooms, 3 hardware configurations, and 3
|
||||
independent sessions/room; UNKNOWN never selected; confidence monotonic;
|
||||
live disable; REST/WebSocket/MQTT/Home Assistant/replay compatibility.
|
||||
- **G4 default visualization only**: 30 shadow days under 0.1% timeout/internal
|
||||
error, no open severity 1/2 incidents, and gates still valid for current
|
||||
model/calibration.
|
||||
|
||||
Dynamics and learned engines each repeat G2-G4; approval is not inherited.
|
||||
|
||||
## Testing and completion evidence
|
||||
|
||||
Unit/property/fuzz/integration/security coverage maps to requirements R1-R13:
|
||||
raw hash, confidence, modes, malformed/stale/frame/covariance input, caps and
|
||||
deadlines, provenance, dependency graph, pose diversity/fall preservation,
|
||||
strict splits, fail-to-raw faults, no network capability, and authenticated
|
||||
source/replay selection.
|
||||
|
||||
Release commands include focused core/physics tests, default/dynamics/learned
|
||||
feature checks, format/clippy, benches, `cargo deny`, `cargo audit`, strict split
|
||||
verification, and golden replay verification. Completion also requires JSON
|
||||
schemas, measured Pi 5/x86 rows, strict manifest hashes, raw/refined metrics,
|
||||
SBOM/license report, rollback drill, and residual-risk owners. Missing measured
|
||||
or operational evidence leaves status Proposed and runtime in audit.
|
||||
|
||||
## Rollback
|
||||
|
||||
Rollback is an authenticated mode transition to audit/off, not a binary
|
||||
downgrade. Stop selection immediately, keep raw publication and disposition
|
||||
records, discard track state, and retain only aggregate incident metrics plus
|
||||
signed configuration history. Failed artifact activation leaves the previous
|
||||
engine atomically active. Additive schemas remain; refined-only callers receive
|
||||
the typed unavailable response.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Explicit anti-hallucination and provenance boundary after RF inference.
|
||||
- Reusable native Rust consistency primitive with measurable abstention.
|
||||
- Python/CUDA remain absent from the production default.
|
||||
- Cross-modal teacher data remains possible without wearable runtime inputs.
|
||||
|
||||
### Negative
|
||||
|
||||
- Full value requires a real metric 3D observer and calibrated uncertainty.
|
||||
- Stateful tracks add latency/memory; optional backends add supply-chain surface.
|
||||
- A constrained but wrong pose can look more credible.
|
||||
- Strict data collection costs more than the software implementation.
|
||||
|
||||
### Neutral
|
||||
|
||||
- This ADR does not improve RF observability or current weight evidence.
|
||||
- Existing 2D consumers continue to function.
|
||||
|
||||
## Implementation phases
|
||||
|
||||
P0 contracts/schemas; P1 deterministic audit; P2 bounded shadow correction; P3
|
||||
server/Cog publication and evidence ledger; P4 Rapier audit; P5 Burn residual
|
||||
training/inference. Code may land ahead of evidence, but runtime authority
|
||||
advances only through the gates above.
|
||||
|
||||
## Implementation status at proposal
|
||||
|
||||
- P0-P3 are implemented on this branch: canonical contracts, strict schemas,
|
||||
deterministic audit/projection, authenticated correction receipts,
|
||||
idempotency, bounded track state, latest-frame backpressure, additive HTTP
|
||||
and WebSocket publication, live legacy-2D audit, privacy-safe metrics, golden
|
||||
replay, and strict-split checks.
|
||||
- P4 is implemented as an optional persistent per-track Rapier dynamics auditor
|
||||
and remains audit-only pending independent G2-G4 evidence.
|
||||
- P5 inference architecture, artifact verification, serialization, and native
|
||||
CPU execution are implemented. Training data, a signed trained artifact, and
|
||||
G2-G4 accuracy/calibration evidence do not exist, so the layer has no runtime
|
||||
selection authority. Its resolved Rust 1.92 requirement is also an explicit
|
||||
activation blocker on the current Rust 1.91.1 release host.
|
||||
- The live Cog honestly emits `Image2d`, degraded trust, and uncalibrated
|
||||
uncertainty. It can be audited but cannot be selected for 3D correction.
|
||||
G0 therefore remains open until an independently released metric-3D observer
|
||||
with calibrated covariance is integrated.
|
||||
- Local x86 latency and synthetic contract checks are recorded in the append-
|
||||
only evidence ledger. Pi 5 measurements, 24-hour replay, 100-million-case
|
||||
fuzzing, held-out RF/optical accuracy, fleet shadowing, and vertical safety
|
||||
validation remain release evidence gates rather than software claims.
|
||||
|
||||
## References
|
||||
|
||||
- [GRIP project](https://ryosukehori.github.io/grip-project/)
|
||||
- [GRIP paper (arXiv:2603.16233)](https://arxiv.org/abs/2603.16233)
|
||||
- [Rapier documentation](https://docs.rs/rapier3d/)
|
||||
- [Burn documentation](https://docs.rs/burn/0.21.0/burn/)
|
||||
- [ADR-020](./ADR-020-rust-ruvector-ai-model-migration.md)
|
||||
- [ADR-079](./ADR-079-camera-ground-truth-training.md)
|
||||
- [ADR-101](./ADR-101-pose-estimation-cog.md)
|
||||
- [ADR-150](./ADR-150-rf-foundation-encoder.md)
|
||||
- [ADR-273](./ADR-273-unified-rf-spatial-world-model.md)
|
||||
- [ADR-279](./ADR-279-native-rf-frame-contract.md)
|
||||
- [ADR-298](./ADR-298-model-release-sanity-gates.md)
|
||||
- [ADR-302](./ADR-302-out-of-distribution-detection.md)
|
||||
- [ADR-303](./ADR-303-ground-truth-synchronization.md)
|
||||
- [ADR-304](./ADR-304-evidence-engine.md)
|
||||
- [ADR-305](./ADR-305-authenticated-sensor-identity.md)
|
||||
- [ADR-306](./ADR-306-canonical-spatial-ontology.md)
|
||||
276
docs/adr/ADR-324-off-axis-head-coupled-perspective-demo.md
Normal file
276
docs/adr/ADR-324-off-axis-head-coupled-perspective-demo.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# ADR-324: off-axis-mode — RF-assisted head-coupled perspective for the three.js realtime demo
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed (core implemented — see §2.5) |
|
||||
| **Date** | 2026-08-16 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **off-axis-mode** |
|
||||
| **Scope** | New `examples/three.js/demos/07-off-axis-window.html` (client-side only); no server changes |
|
||||
| **Relates to** | ADR-019 (sensing-only UI), ADR-035 (live sensing UI accuracy), ADR-169 (adam-mode), ADR-170 (yoga-mode), ADR-282 (L0–L5 evidence ladder), ADR-295 (source provenance), ADR-306 (spatial ontology), ADR-307 (persistent tracking), ADR-323 (pose refinement) |
|
||||
| **Prior art** | [`icurtis1/off-axis-sneaker`](https://github.com/icurtis1/off-axis-sneaker) (reference only — see §2.1 licensing) |
|
||||
| **Numbering note** | ADR-324 is the next free number in the authoring checkout (322 is unused, 323 is the latest on disk). Re-run the ADR index/collision check immediately before merge and rename if needed. |
|
||||
| **Tracking issue** | none yet |
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
### 1.1 The question this ADR answers
|
||||
|
||||
"Can we use [`icurtis1/off-axis-sneaker`](https://github.com/icurtis1/off-axis-sneaker)
|
||||
with RuView?" The answer is: **yes for the technique, no for the code, and
|
||||
only honestly for the RF part.** This ADR records the research behind each of
|
||||
those three clauses and defines the integration that is actually defensible.
|
||||
|
||||
### 1.2 What off-axis-sneaker is
|
||||
|
||||
`off-axis-sneaker` is a React + TypeScript + Vite web app that renders a GLB
|
||||
model (a sneaker) in three.js and creates a *head-coupled perspective*
|
||||
("fish-tank VR" / "window into the screen") illusion:
|
||||
|
||||
- **Tracking input**: MediaPipe Face Mesh (468 facial landmarks) from a
|
||||
webcam. Head (x, y) comes from the eye midpoint; depth (z) is proxied by
|
||||
inter-ocular distance. An exponential moving average (default factor 0.3)
|
||||
smooths jitter; sensitivity multipliers are `strengthX: 4`, `strengthY: 3`,
|
||||
`strengthZ: 2`.
|
||||
- **Projection**: `src/utils/offAxisCamera.ts` builds a **true asymmetric
|
||||
(off-axis) frustum** — `makePerspective(left, right, top, bottom, near, far)`
|
||||
with `left/right/top/bottom = (screenBound − eyePosition) · (near /
|
||||
viewerToScreenDistance)` — i.e. Kooima's generalized perspective projection,
|
||||
plus a matching camera translation. Constants: `nearPlane 0.05`,
|
||||
`farPlane 1000`, `worldScale 0.01` (cm → world units), `movementScale 1.5`.
|
||||
- **Calibration**: a wizard captures physical screen width/height (cm),
|
||||
typical viewing distance, and pixel density, stored locally, so eye position
|
||||
is computed relative to the *physical* display.
|
||||
|
||||
The technique descends from Johnny Chung Lee's 2007 Wii-remote desktop VR
|
||||
demo and the fish-tank VR literature (Ware, Arthur & Booth, CHI '93). The
|
||||
projection math is Robert Kooima's "Generalized Perspective Projection"
|
||||
(2008). Both are public, well-documented techniques independent of any one
|
||||
implementation.
|
||||
|
||||
### 1.3 What the illusion physically requires
|
||||
|
||||
The head-coupled illusion is only convincing when the tracked eye position is
|
||||
**accurate to roughly centimeters** and **low-latency**. The VR literature
|
||||
puts comfortable motion-to-photon latency below ~20 ms for head-mounted
|
||||
displays; desktop fish-tank VR tolerates more, but visible lag between head
|
||||
motion and parallax response is exactly what breaks the "window" illusion.
|
||||
`CLAIMED` (literature values; no RuView measurement exists for this demo yet).
|
||||
|
||||
### 1.4 What RuView RF sensing can actually supply today
|
||||
|
||||
This is where honesty is mandatory (repo rule: never present WiFi sensing as
|
||||
camera-grade).
|
||||
|
||||
- **Field-peak position, not metric localization.**
|
||||
`wifi-densepose-sensing-server/src/field_localize.rs` derives a position
|
||||
from the strongest peak of the 20×20 `signal_field` carried on
|
||||
`/ws/sensing` `sensing_update` frames. Its own module doc states the
|
||||
caveat: the subcarrier→angle mapping is a *representation*; "a single ESP32
|
||||
link cannot resolve a true (x, z) room position." The emitted position is
|
||||
"strongest field peak in the room model," mapped with `X_SCALE 0.6`,
|
||||
`Z_SCALE 0.5`, gated by `PEAK_THRESHOLD 0.35` — real, live, motion-tracking,
|
||||
but **not a calibrated person fix** and nowhere near eye-position precision.
|
||||
- **RF pose is 2-D, normalized, constant-confidence.** The committed Cog
|
||||
(ADR-101, restated by ADR-323) emits 17 COCO keypoints as normalized 2-D
|
||||
coordinates with a constant confidence and no per-joint uncertainty. A
|
||||
"nose" keypoint exists (COCO index 0), but it is not a metric 3-D head fix.
|
||||
- **Tracks are coarse and pseudonymous by design.** `ruview-track` (ADR-307)
|
||||
maintains `person_N` tracks with container-level ("kitchen → hallway")
|
||||
continuity, coarse non-reversible features, and asserts **no accuracy
|
||||
number** — outputs default to evidence level `L1`.
|
||||
- **Update cadence and latency are unmeasured for this purpose.** The demo
|
||||
pipeline runs at ~30 Hz on the MediaPipe side (ADR-170), but no end-to-end
|
||||
RF motion-to-photon latency has been measured. Any figure quoted for the RF
|
||||
path must be tagged `MEASURED` with a reproducer before it appears in docs
|
||||
or UI.
|
||||
|
||||
Conclusion of the capability match: **RF cannot drive a convincing fish-tank
|
||||
illusion by itself today**, and this ADR does not claim it can. RF *can*
|
||||
supply things a webcam cannot: camera-free presence, zone-level position,
|
||||
person count, approach direction, and pseudonymous continuity — including
|
||||
when the camera is off.
|
||||
|
||||
### 1.5 What this ADR is *not*
|
||||
|
||||
- Not a vendoring of `off-axis-sneaker` (see §2.1 — the repo has no license).
|
||||
- Not a claim of camera-grade RF head tracking, at any tier.
|
||||
- Not a backend change: no new server endpoints, no new auth surface, no
|
||||
schema changes. Purely additive client-side HTML/JS, like ADR-169/170.
|
||||
- Not a React/Vite/Tailwind adoption. The `examples/three.js/demos/*` are
|
||||
dependency-light single-file HTML demos and stay that way.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
### 2.1 Licensing: adopt the technique, not the code
|
||||
|
||||
`off-axis-sneaker` publishes **no license**. Under default copyright, its
|
||||
source cannot be copied, vendored, or translated into this repository.
|
||||
Decision:
|
||||
|
||||
1. **No code, assets, or models from `off-axis-sneaker` enter this repo.**
|
||||
The GLB sneaker model is likewise unlicensed for reuse; demos use assets
|
||||
already present in `examples/`.
|
||||
2. The off-axis projection is implemented **clean-room from the public
|
||||
sources**: Kooima's "Generalized Perspective Projection" (2008) — the
|
||||
`pa/pb/pc` screen-corner formulation — and three.js's documented
|
||||
`PerspectiveCamera.projectionMatrix` override path. The repository is cited
|
||||
as prior art in this ADR only.
|
||||
3. If upstream later adds a permissive license, revisiting reuse requires a
|
||||
new ADR note, not silent copying.
|
||||
|
||||
### 2.2 Tiered integration — each tier labeled by what it really is
|
||||
|
||||
**Tier A (ships first): webcam-fine + RF-context hybrid.**
|
||||
`07-off-axis-window.html` uses MediaPipe Face Landmarker (already the pattern
|
||||
in demo 05) for fine head tracking and the Kooima frustum for rendering —
|
||||
functionally what off-axis-sneaker does, reimplemented. RuView RF adds the
|
||||
camera-free layer around it:
|
||||
|
||||
- **Presence-gated camera**: the webcam pipeline starts only when the RF
|
||||
presence signal (`/ws/sensing` `sensing_update`) says someone is in the
|
||||
zone, and stops after a configurable RF-vacancy timeout. The privacy
|
||||
posture improves: the camera is *off* until physics says there is someone
|
||||
to track.
|
||||
- **Multi-person arbitration**: when RF reports more than one person, the HUD
|
||||
says so and the demo holds the last stable perspective instead of jumping
|
||||
between faces.
|
||||
- **Pre-warm**: RF approach direction (field-peak trajectory) warms up
|
||||
MediaPipe and the scene before the person sits down.
|
||||
|
||||
**Tier B (demo mode, prominently labeled): RF-only coarse parallax.**
|
||||
A toggle drives the off-axis eye position from RF alone — field peak (x, z)
|
||||
plus the pose nose keypoint when present — through a one-euro filter, a
|
||||
deadband, and a hard gain clamp. The HUD labels it **"RF coarse body
|
||||
parallax — not head tracking"** and shows the live evidence level (`L1`
|
||||
heuristic unless a certificate says otherwise, per ADR-282/ADR-318). The
|
||||
expected experience is a slow, body-scale parallax sway — a demonstrative
|
||||
"the room model moves because *you* moved, with no camera" — not a stable
|
||||
fish-tank illusion. The demo must never present Tier B as equivalent to
|
||||
Tier A.
|
||||
|
||||
**Tier C (future, explicitly gated, not promised): metric RF head position.**
|
||||
Only a calibrated multistatic deployment (ADR-297 multi-node semantics,
|
||||
ADR-311 fusion, ADR-303 ground-truth sync) with an evidence-engine ledger
|
||||
entry (ADR-304) and a capability certificate (ADR-318) could justify feeding
|
||||
RF positions into the fine path. No current data supports this; Tier C exists
|
||||
in this ADR solely so nobody ships it informally without those gates.
|
||||
|
||||
### 2.3 Implementation surface
|
||||
|
||||
- New file `examples/three.js/demos/07-off-axis-window.html` (07, not 06 —
|
||||
ADR-170 reserves `06-yoga-mode.html`). Single-file demo following the 01–05
|
||||
conventions: same CSS custom properties, same HUD/helper-panel pattern,
|
||||
served from the existing static demo server
|
||||
(`http://127.0.0.1:8765/examples/three.js/demos/…`).
|
||||
- A small clean-room module (inline `<script type="module">` or
|
||||
`examples/three.js/lib/off-axis-camera.js` if shared later) that, given
|
||||
screen corners `pa, pb, pc` (from calibration) and eye point `pe`, sets
|
||||
`camera.projectionMatrix` via the Kooima formulation each frame.
|
||||
- Data inputs are the **existing** streams only: `/ws/sensing`
|
||||
(`sensing_update` → `signal_field` → field peak, using the same
|
||||
`X_SCALE`/`Z_SCALE`/`PEAK_THRESHOLD` mapping as `field_localize.rs`) and,
|
||||
when available, `/api/v1/stream/pose` for the nose keypoint. WebSocket
|
||||
access uses the existing ticket flow (`ws_ticket.rs` / `bearer_auth.rs`);
|
||||
no endpoint is exempted or added.
|
||||
- Calibration mirrors the sneaker app's concept without its code: screen
|
||||
width/height in cm, viewing distance, persisted in `localStorage` under a
|
||||
demo-scoped key. No calibration data leaves the browser.
|
||||
- Provenance discipline: if the demo is pointed at a synthetic or replayed
|
||||
source, the ADR-295 provenance state must surface in the HUD exactly as the
|
||||
Observatory does — synthetic can never present as live.
|
||||
|
||||
### 2.4 Honesty and evidence rules binding this feature
|
||||
|
||||
1. Every user-visible latency, accuracy, or precision statement in the demo,
|
||||
README, or docs carries a `MEASURED` (with reproducer), `CLAIMED`, or
|
||||
`SYNTHETIC` tag. This ADR itself contains no `MEASURED` claims.
|
||||
2. Tier B is labeled coarse body parallax in the HUD at all times; there is
|
||||
no configuration that hides the label while RF drives the camera.
|
||||
3. No PCK or pose-accuracy number may be quoted for the RF path without the
|
||||
mean-pose baseline and a leakage-free held-out split (repo rule).
|
||||
4. The webcam feed never leaves the browser; no frames, landmarks, or
|
||||
embeddings are sent to the server. RF data continues to obey ADR-307's
|
||||
privacy invariants (pseudonymous, coarse, rotatable).
|
||||
|
||||
### 2.5 Implementation status (2026-08-16 amendment)
|
||||
|
||||
The projection core shipped as a **Rust crate compiled to WASM** rather than
|
||||
the inline JS module §2.3 anticipated — a strict upgrade with the same
|
||||
surface: `v2/crates/ruview-offaxis` (dependency-free native core; wasm-bindgen
|
||||
only on wasm32) implements the Kooima projection, the one-euro filter, the
|
||||
field-peak mapping (constants mirroring `field_localize.rs`), and the Tier B
|
||||
coarse-parallax stage with its deadband/gain/clamp bounds enforced in Rust.
|
||||
`examples/three.js/demos/07-off-axis-window.html` consumes the wasm-bindgen
|
||||
output (built locally per the crate README; generated artifacts are not
|
||||
committed). Validation and `MEASURED` benchmarks live in the crate README.
|
||||
The demo ships with a `SYNTHETIC`-labeled mouse simulator and the labeled
|
||||
Tier B RF mode; a Tier A fine tracker connects through
|
||||
`OffAxisCamera.update_normalized` and remains host-provided.
|
||||
|
||||
## 3. Options considered
|
||||
|
||||
| Option | Verdict | Why |
|
||||
|---|---|---|
|
||||
| Vendor `off-axis-sneaker` (or fork + point at RuView) | **Rejected** | No license ⇒ no redistribution rights. Also React/Vite stack conflicts with the repo's single-file demo convention. |
|
||||
| Clean-room Kooima off-axis demo, webcam-fine + RF-context (Tier A/B) | **Chosen** | Legally clean, matches demo conventions, uses RF for what it is actually good at, and demonstrates camera-free presence value honestly. |
|
||||
| RF-only head-coupled perspective as the headline | **Rejected** | Over-claim. Single-link field peaks are a representation, not metric localization (`field_localize.rs` caveat); shipping this as "head tracking" violates the camera-grade rule. Survives only as the labeled Tier B toggle. |
|
||||
| Wait for multistatic metric localization (Tier C) before any demo | **Rejected** | Blocks a useful, honest demo on a phase-2/3 program (ADR-303/311/318) with no delivery date. The gates are recorded instead. |
|
||||
| Add a dedicated server endpoint for head position | **Rejected** | Unnecessary — existing `/ws/sensing` + `/api/v1/stream/pose` suffice; a new endpoint would expand the auth surface for no capability gain. |
|
||||
|
||||
## 4. Consequences
|
||||
|
||||
**Improves**
|
||||
|
||||
- A publicly legible demo of RF sensing's actual differentiator: the scene
|
||||
knows you are there, where you roughly are, and how many of you there are —
|
||||
before and without any camera.
|
||||
- Privacy posture of the head-tracking demo class: camera duty-cycle is
|
||||
bounded by RF presence instead of always-on.
|
||||
- Canonical, licensed off-axis projection code the Observatory or future UI
|
||||
can reuse.
|
||||
|
||||
**Costs / risks**
|
||||
|
||||
- Tier B can underwhelm viewers primed by webcam demos; the mitigation is the
|
||||
labeling and the side-by-side toggle, not inflated gain.
|
||||
- MediaPipe CDN dependency (same as demo 05) remains a network-availability
|
||||
risk for Tier A; the demo must degrade to Tier B with a visible notice.
|
||||
- Screen-calibration friction (cm measurements) may deter casual users; a
|
||||
"skip calibration (approximate)" path with degraded-accuracy labeling is
|
||||
acceptable.
|
||||
- Upstream `off-axis-sneaker` may change or add a license; tracking that is
|
||||
manual.
|
||||
|
||||
**Follow-ups (not in this ADR's scope)**
|
||||
|
||||
- Measure end-to-end RF motion-to-parallax latency with a reproducer and
|
||||
publish it `MEASURED`.
|
||||
- If/when ADR-303/311 land, evaluate Tier C against the ADR-318 certificate
|
||||
gate.
|
||||
- Consider promoting the off-axis camera module into the Observatory 3D view.
|
||||
|
||||
## 5. Validation
|
||||
|
||||
- Demo checklist (manual, per ADR-169/170 practice): loads from the static
|
||||
server; Tier A activates only on RF presence; Tier B label visible whenever
|
||||
RF drives the camera; provenance badge correct against a synthetic source;
|
||||
no network requests carrying webcam-derived data (verified in devtools).
|
||||
- `rg` gate before merge: no file under `examples/` contains code originating
|
||||
from `icurtis1/off-axis-sneaker`.
|
||||
- No workspace, harness, or firmware validation rows are triggered — the
|
||||
change is a static HTML demo plus this document.
|
||||
|
||||
## 6. References
|
||||
|
||||
- [`icurtis1/off-axis-sneaker`](https://github.com/icurtis1/off-axis-sneaker) — prior-art reference (unlicensed; technique only)
|
||||
- Robert Kooima, *Generalized Perspective Projection*, 2008 — off-axis frustum math
|
||||
- Johnny Chung Lee, *Head Tracking for Desktop VR Displays using the Wii Remote*, 2007
|
||||
- Ware, Arthur & Booth, *Fish Tank Virtual Reality*, CHI '93 — head coupling vs. stereo
|
||||
- `v2/crates/wifi-densepose-sensing-server/src/field_localize.rs` — field-peak honesty caveat and coordinate mapping
|
||||
- `v2/crates/wifi-densepose-sensing-server/src/ws_ticket.rs`, `bearer_auth.rs` — WebSocket auth pattern
|
||||
- `v2/crates/ruview-track/src/lib.rs` — ADR-307 privacy invariants and evidence discipline
|
||||
- ADR-169, ADR-170 — demo-scoped ADR pattern for `examples/three.js/demos/`
|
||||
- ADR-282 — L0–L5 evidence ladder; ADR-295 — provenance state machine
|
||||
@@ -0,0 +1,527 @@
|
||||
# ADR-325: Cognitum Spaces activation and governed spatial exchange
|
||||
|
||||
- **Status**: Accepted — legacy read live; versioned hierarchy, local memory, and governed-action implementation validated on feature branches; deployment pending
|
||||
- **Date**: 2026-08-17
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: cognitum-spaces, oauth, spatial-state, privacy, ruvector, policy, autogenous
|
||||
- **Relates to**: ADR-271, ADR-277, ADR-304, ADR-306, ADR-312, ADR-318, ADR-319, ADR-321; Cognitum API ADR-094; Autogenous ADR-402
|
||||
|
||||
## Context
|
||||
|
||||
RuView produces camera-free RF perception locally. Cognitum Spaces provides a
|
||||
tenant-scoped cloud projection of physical places. Autogenous ADR-402 proposes
|
||||
using that projection as a spatial-intelligence input for agent coordination.
|
||||
The useful product is not another sensor dashboard: it is a governed chain from
|
||||
local perception to spatial state, persistent memory, explanation, and action.
|
||||
|
||||
Four product pillars define the requested integration:
|
||||
|
||||
1. **Spatial state** — sites, buildings, floors, rooms/spaces, zones, entities,
|
||||
semantic events, and alerts.
|
||||
2. **RuView perception** — camera-free sensing is normalized locally before any
|
||||
permitted P2/P3 semantic event synchronizes.
|
||||
3. **Persistent memory** — RuVector grounds anomaly explanations in
|
||||
tenant-scoped spatial history.
|
||||
4. **Governed action** — agents observe or recommend by default; consequential
|
||||
execution requires explicit policy authorization.
|
||||
|
||||
The live API audit on 2026-08-17 established the current production boundary:
|
||||
|
||||
- `GET https://api.cognitum.one/v1/spaces` exists and returns a bounded list;
|
||||
- an unauthenticated request is rejected;
|
||||
- the current account has no paired sites, so the authenticated result is an
|
||||
empty list rather than fabricated sample state;
|
||||
- the projection declares HomeCore Edge authoritative and excludes raw CSI,
|
||||
CIR, RF tensors, recordings, pose frames, vital waveforms, and identity
|
||||
observations;
|
||||
- the first deployed Function revision accepted only legacy `cog_` API keys;
|
||||
- the gateway was configured to authenticate private Function hops, but the
|
||||
direct Function endpoint was still publicly invokable; that bypass has now
|
||||
been closed and the exact gateway runtime service account is the only
|
||||
invoker;
|
||||
- OAuth protected-resource metadata and a RuView-scoped OAuth accept path were
|
||||
absent.
|
||||
|
||||
The Autogenous review at commit
|
||||
`f7fa308b261bac89a8909edae8a3fdbbfb8ce66c` found additional integration risks:
|
||||
|
||||
- its Spaces client only listed spaces; no governed ingest contract existed;
|
||||
- it trusted a loose TypeScript cast, with no response-size, timeout, redirect,
|
||||
or strict semantic-boundary validation;
|
||||
- its observation conversion dropped tenant/message/sequence identity;
|
||||
- missing confidence became zero but could still enter fusion;
|
||||
- provenance could be substituted for calibration identity;
|
||||
- a Spaces-derived belief could be converted back into an observation and
|
||||
counted as independent corroboration, laundering one source into two;
|
||||
- its API-key exchange returns a `cognitum-cli` OAuth token, but the live Spaces
|
||||
endpoint accepted only a `cog_` key. Calling this “OAuth Spaces access” was a
|
||||
contract mismatch.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt a one-way-by-default, typed spatial exchange with separate activation,
|
||||
data, memory, and action authorities.
|
||||
|
||||
```text
|
||||
RuView RF capture (P0/P1, local)
|
||||
-> calibrated/OOD-gated semantic observation
|
||||
-> ontology + evidence + witness envelope (P2/P3)
|
||||
-> HomeCore authoritative edge state
|
||||
-> Cognitum Spaces tenant/workspace projection
|
||||
-> RuView bounded read client / Autogenous spatial context
|
||||
-> RuVector tenant-scoped memory and explanation
|
||||
-> recommendation
|
||||
-> ruvview-policy authorization + approval + receipt
|
||||
-> optional consequential action
|
||||
```
|
||||
|
||||
Cloud state is a projection of edge state, not a second sensor and not an
|
||||
independent corroborating modality.
|
||||
|
||||
### 1. Activation and data-plane credentials are distinct
|
||||
|
||||
RuView uses Cognitum's existing Authorization Code + PKCE flow with the public
|
||||
`ruview` client. A user explicitly requests `spaces:read` with
|
||||
`wifi-densepose login --spaces`. The authorization-server registration is a
|
||||
ceiling; ordinary sensing login does not silently gain cloud access.
|
||||
|
||||
The Spaces resource server accepts either:
|
||||
|
||||
- a legacy API key carrying `spaces:read` (or the migration-compatible
|
||||
predecessor `devices:manage`); or
|
||||
- a Cognitum OAuth access token that passes every condition below.
|
||||
|
||||
OAuth acceptance is conjunctive:
|
||||
|
||||
| Check | Required value |
|
||||
|---|---|
|
||||
| Signature | ES256 against `https://auth.cognitum.one/.well-known/jwks.json` |
|
||||
| Issuer | exact `https://auth.cognitum.one` |
|
||||
| Audience | exact `ruview` |
|
||||
| Client claim | exact `ruview` |
|
||||
| Token type | ordinary `access`; setup/workload tokens denied |
|
||||
| Lifetime | current `exp`/`nbf`, five-second clock tolerance only |
|
||||
| Scope | exact token `spaces:read` member |
|
||||
| Tenant binding | valid non-empty UUID `org_id` and `workspace_id` |
|
||||
|
||||
An API key is not called OAuth. An OAuth token is not stored in
|
||||
`COGNITUM_SPACES_API`. The compatibility environment variable contains an API
|
||||
key only and is never printed, logged, or committed.
|
||||
|
||||
OAuth consent grants identity-bound read access. It does **not** grant device
|
||||
pairing, data publication, deployment, billing, spending, leases, learning
|
||||
promotion, automation installation, commands, or actuator authority.
|
||||
|
||||
The contributor metaharness exposes this as CLI verb `spaces` and MCP tool
|
||||
`ruview_spaces_list`. It delegates to the same Rust client rather than parsing
|
||||
or refreshing OAuth independently. The tool never accepts a bearer token or API
|
||||
key. MCP use requires an operator-provided `credential-use` grant, and MCP calls
|
||||
cannot select the credential path or API origin. The adapter requires an
|
||||
installed `wifi-densepose` binary rather than executing Cargo build scripts
|
||||
from an auto-detected checkout while holding credential authority. Because
|
||||
refresh tokens rotate, a read may atomically update the local OAuth credential
|
||||
before contacting Spaces; this authentication side effect is disclosed and
|
||||
does not add cloud write authority.
|
||||
|
||||
### 2. The gateway owns the private credential relay
|
||||
|
||||
The public gateway strips inbound `X-Cognitum-User-Authorization` and
|
||||
`X-Serverless-Authorization`. For a locked Function upstream it then:
|
||||
|
||||
1. retains a legacy `cog_` credential in `X-API-Key`, or, for the exact Spaces
|
||||
route only, retains a non-key bearer in a gateway-owned internal header;
|
||||
2. replaces `Authorization` with the gateway's Google invoker ID token;
|
||||
3. fails closed with `503` if it cannot mint that hop identity;
|
||||
4. forwards only to the configured Function origin.
|
||||
|
||||
The Function's Cloud Run invoker check is enabled. `allUsers` has no invoker
|
||||
binding; only the exact `apigateway-sa` service account may invoke it. This is
|
||||
required because otherwise a caller could bypass Cloud Armor and spoof an
|
||||
internal relay header.
|
||||
|
||||
The API publishes RFC 9728 protected-resource metadata naming the authorization
|
||||
server and `spaces:read` scope. Discovery describes capability; it does not
|
||||
grant it.
|
||||
|
||||
### 3. Tenant isolation is part of authentication
|
||||
|
||||
Legacy API-key documents are queried by their existing owner-bound `tenantId`.
|
||||
OAuth requests are conjunctively queried by both signed `org_id` and
|
||||
`workspace_id` using stored `tenantId` and `workspaceId` fields. The public
|
||||
tenant identifier is projected from signed `org_id`. A request cannot supply
|
||||
either selector in a query string.
|
||||
|
||||
No cross-tenant aggregation exists on this path. Pagination, search, memory,
|
||||
and event endpoints added later must carry the same authoritative principal;
|
||||
client-provided tenant filters may only narrow within it, never replace it.
|
||||
|
||||
### 4. Spatial model and ownership
|
||||
|
||||
The canonical RuView vocabulary remains ADR-306:
|
||||
|
||||
```text
|
||||
Site -> Building -> Floor -> Space -> Zone
|
||||
-> Sensor / Person / Object / Track
|
||||
-> Observation -> Event -> Alert
|
||||
```
|
||||
|
||||
Cognitum may call a bounded room a “space”; RuView does not create a second
|
||||
room type. Stable external IDs are namespaced and validated before entering the
|
||||
ontology. HomeCore remains authoritative for local registry state and local
|
||||
automation. Cognitum owns tenant/workspace projection and activation. RuVector
|
||||
owns indexed spatial history, not tenancy or authorization.
|
||||
|
||||
The current live endpoint exposes the first `Space` slice only. Sites, floors,
|
||||
zones, entities, events, and alerts are contract milestones, not inferred from
|
||||
missing fields. A client must represent absence as unknown/unavailable and must
|
||||
not fabricate parents, coordinates, people, alerts, or provenance.
|
||||
|
||||
### 5. Privacy boundary and synchronization eligibility
|
||||
|
||||
Only allow-listed P2/P3 semantic projections may cross the cloud boundary.
|
||||
|
||||
| Class | Examples | Cloud default |
|
||||
|---|---|---|
|
||||
| P0 | raw CSI, CIR, RF tensors, packet captures | prohibited |
|
||||
| P1 | pose frames, vital waveforms, identity observations, recordings | prohibited |
|
||||
| P2 | occupancy count, bounded activity/fall possibility, anomaly score | permitted when policy allows |
|
||||
| P3 | versions, connection health, signed capability metadata | permitted |
|
||||
|
||||
The client independently rejects forbidden raw-field names anywhere in the
|
||||
response. This is defense in depth, not a substitute for server-side
|
||||
projection. It also enforces HTTPS except for loopback tests, refuses redirects,
|
||||
uses bounded connect/total timeouts, caps responses at 1 MiB, caps the list at
|
||||
100 spaces, bounds nesting/arrays/strings, validates confidence, and rejects
|
||||
non-P2/P3 space records.
|
||||
|
||||
Cloud-bound envelopes must preserve, when available:
|
||||
|
||||
- tenant/workspace/site/space/device identity;
|
||||
- `messageId` and monotonic `eventSequence`;
|
||||
- `observedAt`, `expiresAt`, freshness, and connection state;
|
||||
- privacy class and semantic schema version;
|
||||
- calibrated confidence and explicit uncertainty/abstention;
|
||||
- model, HomeCore, hardware-manifest, calibration, evidence, and witness
|
||||
provenance.
|
||||
|
||||
Provenance is never used as a calibration identifier. Missing confidence,
|
||||
calibration, timestamp, or tenant identity stays missing and cannot satisfy an
|
||||
admission rule.
|
||||
|
||||
### 6. No feedback laundering or false corroboration
|
||||
|
||||
A Spaces record derived from RuView evidence carries derivation lineage. If it
|
||||
returns to RuView or Autogenous, it is a **projection/recollection** of that
|
||||
lineage, not a new observation. It cannot:
|
||||
|
||||
- increment corroborating-sensor count;
|
||||
- raise evidence level;
|
||||
- be fused as an independent modality;
|
||||
- reset freshness to retrieval time;
|
||||
- erase abstention, contradiction, or uncertainty;
|
||||
- generate a second belief that cites the first as support.
|
||||
|
||||
Deduplication keys include tenant, source/witness identity, message ID, and
|
||||
sequence. Cycles are detected and rejected. Independent corroboration requires
|
||||
a distinct authenticated source and evidence chain.
|
||||
|
||||
### 7. Persistent memory is tenant-scoped and explanation-oriented
|
||||
|
||||
RuVector indexes accepted semantic state under at least:
|
||||
|
||||
```text
|
||||
(tenant_id, workspace_id, site_id, space_id, schema_version, time_bucket)
|
||||
```
|
||||
|
||||
It stores bounded semantic features, uncertainty, evidence references, and
|
||||
witness digests. It does not store OAuth/API credentials or prohibited raw
|
||||
payloads. Retrieval always applies the authenticated tenant/workspace filter
|
||||
before similarity ranking.
|
||||
|
||||
An anomaly explanation names:
|
||||
|
||||
- the current semantic state and its uncertainty;
|
||||
- the relevant learned baseline/window from ADR-312;
|
||||
- comparable tenant-local history;
|
||||
- the measured deviation and contradictory evidence;
|
||||
- the provenance/witness chain;
|
||||
- the evidence label (`MEASURED`, `SYNTHETIC`, or `CLAIMED`).
|
||||
|
||||
Memory supplies context, not permission. A historically common action is not
|
||||
automatically authorized.
|
||||
|
||||
### 8. Agents observe and recommend; policy authorizes action
|
||||
|
||||
Autogenous and other agents receive read-only spatial context by default. Their
|
||||
normal outputs are observations, explanations, proposals, and recommendations.
|
||||
|
||||
Any consequential action must cross the ADR-321 `ruview-policy` gate with:
|
||||
|
||||
- an exact action class and target;
|
||||
- a fresh capability certificate;
|
||||
- KNOWN/DEGRADED/UNKNOWN domain state;
|
||||
- bounded uncertainty and sufficient evidence;
|
||||
- tenant/workspace authorization;
|
||||
- expiry, nonce, idempotency key, and replay protection;
|
||||
- required human/policy approval;
|
||||
- a terminal witness receipt for allow or deny.
|
||||
|
||||
Missing policy, unknown action class, stale state, incomplete provenance, or an
|
||||
unavailable approval service denies. OAuth `spaces:read` can never authorize an
|
||||
action. This ADR adds no actuator method to the Spaces client.
|
||||
|
||||
## Implementation
|
||||
|
||||
### RuView
|
||||
|
||||
- `ruview-cognitum-spaces` is a reusable, read-only client with typed/redacted
|
||||
credentials and a bounded response decoder.
|
||||
- `wifi-densepose login --spaces` explicitly requests `spaces:read` through the
|
||||
existing PKCE flow and credential store.
|
||||
- `wifi-densepose spaces` refreshes OAuth through the existing single-flight,
|
||||
persist-before-return mechanism, verifies that the stored grant contains
|
||||
`spaces:read`, and lists validated state. `COGNITUM_SPACES_API` remains an
|
||||
explicit compatibility path.
|
||||
- the dependency-free contributor metaharness adds `spaces` /
|
||||
`ruview_spaces_list`, invokes only the OAuth branch, bounds and revalidates
|
||||
child output, fixes the production API origin, strips the API-key compatibility
|
||||
environment, requires an installed binary, and default-denies MCP access
|
||||
without `credential-use`.
|
||||
|
||||
### Cognitum Identity
|
||||
|
||||
- the `ruview` public client allow-list includes `spaces:read`;
|
||||
- RFC 8414 metadata advertises it;
|
||||
- refresh preserves the originally granted scope;
|
||||
- no new client secret or password grant is introduced.
|
||||
|
||||
### Cognitum API
|
||||
|
||||
- the gateway preserves caller OAuth through an internal, spoof-resistant
|
||||
relay while authenticating the private Function hop;
|
||||
- Spaces verifies the signed OAuth principal and queries by tenant + workspace;
|
||||
- legacy API-key behavior remains available;
|
||||
- bounded semantic-state `PUT` is available only to an explicitly scoped API-key
|
||||
publisher and is not exposed by the RuView OAuth client;
|
||||
- OpenAPI documents both alternatives and RFC 9728 metadata supports discovery;
|
||||
- the Function remains gateway-only at Cloud Run IAM.
|
||||
|
||||
### Autogenous
|
||||
|
||||
Autogenous must consume an explicitly typed credential. It must not imply that
|
||||
`/v1/cli/session/exchange` produces a RuView-audience token: that exchange
|
||||
currently produces `client_id=cognitum-cli` and cannot pass the Spaces policy.
|
||||
An external RuView PKCE token may be supplied after activation, or a scoped API
|
||||
key may be used as the compatibility path. Response validation and lineage
|
||||
rules in this ADR apply before agent belief formation.
|
||||
|
||||
## Threat model
|
||||
|
||||
| Threat | Required control |
|
||||
|---|---|
|
||||
| Direct Function bypass | invoker IAM check; gateway SA only; no `allUsers` |
|
||||
| Forged internal OAuth header | strip inbound relay headers; gateway writes after route classification |
|
||||
| Token substitution | ES256/JWKS plus exact issuer, audience, client, type, scope, and tenant claims |
|
||||
| Cross-tenant enumeration | principal-derived Firestore selector; bounded non-enumerating errors |
|
||||
| Redirect/token exfiltration | redirects disabled; HTTPS required; fixed path |
|
||||
| Oversized/malformed response | byte/depth/count/string bounds before use |
|
||||
| Raw-data regression | server allow-list plus client forbidden-field rejection |
|
||||
| Secret disclosure | redacting types; no token logs/URLs; `.env` untracked |
|
||||
| Feedback amplification | lineage preservation, dedupe, cycle rejection, no independent corroboration |
|
||||
| Memory leakage | tenant filter before vector search; no global nearest-neighbor pass |
|
||||
| Agent overreach | observe/recommend default; ADR-321 fail-closed action gate |
|
||||
| Stale/replayed state | expiry, sequence, message ID, freshness, witness receipt |
|
||||
| JWKS outage/rotation | bounded cache; fail closed; refresh after unknown `kid`; no algorithm fallback |
|
||||
|
||||
## Deployment and rollback
|
||||
|
||||
Rollout order is dependency-safe:
|
||||
|
||||
1. merge and deploy Identity scope/metadata;
|
||||
2. deploy the Spaces Function with OAuth verification while API-key behavior
|
||||
remains unchanged;
|
||||
3. deploy the gateway relay and protected-resource metadata;
|
||||
4. verify gateway API-key access, OAuth denial matrices, direct-origin platform
|
||||
denial (`401` or `403` before application code), and tenant isolation;
|
||||
5. merge/release the RuView client and CLI activation;
|
||||
6. enable Autogenous consumption only after its strict validation/lineage gates
|
||||
pass.
|
||||
|
||||
Rollback disables OAuth advertisement/relay and returns clients to scoped API
|
||||
keys. It must not restore public Function invocation. Revoking an OAuth session
|
||||
or API key must not alter paired-site state.
|
||||
|
||||
## Validation and acceptance
|
||||
|
||||
Required automated gates:
|
||||
|
||||
- Identity: metadata test, migration application, PKCE authorize/token/refresh
|
||||
scope preservation, cross-client scope denial;
|
||||
- API Function: valid claim matrix and rejection for wrong issuer/audience/
|
||||
client/type/scope/tenant, API-key regression, tenant query assertion, bounded
|
||||
projection tests, build and dependency audit;
|
||||
- gateway: spoofed relay stripped, caller OAuth preserved, Google hop identity
|
||||
substituted, OpenAPI security alternatives, RFC 9728 metadata, build and
|
||||
dependency audit;
|
||||
- RuView: semantic decoder bounds/privacy tests, redaction tests, login scope
|
||||
tests, CLI compile, and live empty/non-empty response tests without fixtures
|
||||
masquerading as production;
|
||||
- policy: no Spaces read can invoke an actuator; denial receipts are witnessed.
|
||||
|
||||
Production readback must prove:
|
||||
|
||||
- unauthenticated gateway request returns `401`;
|
||||
- legacy scoped API key returns the authenticated tenant list;
|
||||
- valid RuView OAuth returns only its workspace;
|
||||
- wrong client, missing `spaces:read`, setup/workload token, and second-tenant
|
||||
token are denied;
|
||||
- the direct Function origin is rejected by the Google platform with `401` or
|
||||
`403` before application code, even with a valid application credential;
|
||||
- response remains `no-store` and excludes P0/P1;
|
||||
- no secret appears in logs, diffs, artifacts, or issue/PR text.
|
||||
|
||||
Performance, detection quality, and action-safety numbers are not claimed by
|
||||
this decision. Any such number requires a named reproducer and the repository's
|
||||
evidence labels. An empty production tenant is a successful isolation/read-path
|
||||
test, not sensing-quality evidence.
|
||||
|
||||
## Production evidence (2026-08-18)
|
||||
|
||||
The bounded Spaces read slice and RuView activation path are deployed. The exact
|
||||
production release chain is:
|
||||
|
||||
- Spaces run `32148530629`, revision `spacesapi-00003-xij`, source
|
||||
`fc333e634cd918b9d6fdde4eecbe7beac1043ab8`, Node 22, runtime service account
|
||||
`spacesapi-runtime@cognitum-20260110.iam.gserviceaccount.com`, with
|
||||
`apigateway-sa@cognitum-20260110.iam.gserviceaccount.com` as sole invoker;
|
||||
- gateway run `32151485401`, revision `apigateway-00180-peh`, source
|
||||
`c4e99ebb4ce0d4e1407f435f905621476c1f0166`, image digest
|
||||
`sha256:bacb81281a54256ff6fdaac253175e76ce6fc225f399163ca0a807a2839bd6a3`;
|
||||
- Identity run `32163542502`, revision `identity-00052-fid`, source
|
||||
`fb6320827b879e481cad6caf184d3cbccd8279c4`, image digest
|
||||
`sha256:0cd5896518bd8ecf042d2f3e9aea58a32e65a68dbddaab1e54f8ae6da2bfab06`,
|
||||
and runtime service account
|
||||
`identity-runtime-prod@cognitum-20260110.iam.gserviceaccount.com`.
|
||||
|
||||
The live API-key matrix returned `200` with an empty bounded list,
|
||||
`Cache-Control: private, no-store`, and no prohibited P0/P1 projection fields.
|
||||
No credential returned `401`. A direct-origin request received a Google
|
||||
Frontend Bearer challenge (`401`) before application code.
|
||||
|
||||
Two independent RuView Authorization Code + PKCE principals also passed the
|
||||
live matrix. Each token used ES256, exact issuer/audience/client checks,
|
||||
`sensing:read spaces:read`, signed UUID organization/workspace claims, refresh
|
||||
rotation, and revocation. Each gateway read returned `200`, an empty bounded
|
||||
list, and `private, no-store`; a corrupted signature returned `401`; and the
|
||||
principals had distinct pseudonymous tenant/workspace fingerprints. This proves
|
||||
the production empty-tenant behavior and independent claim binding. Non-empty
|
||||
cross-tenant isolation remains emulator/staging evidence because production was
|
||||
not mutated to manufacture a fixture.
|
||||
|
||||
Identity metadata deliberately advertises `spaces:read` for RuView but not
|
||||
`spaces:write`. The deployed semantic-state `PUT` remains an API-key-only
|
||||
publisher surface. RuView therefore has no OAuth write, command, policy-approval,
|
||||
or actuator capability.
|
||||
|
||||
This production evidence does not claim deployment of sites/buildings/floors/
|
||||
zones, entities, semantic event or alert resources, tenant-scoped RuVector
|
||||
spatial history, MQTT reconciliation, governed actions, commands, or actuators.
|
||||
The first three are implemented and locally validated in the 2026-08-19 feature
|
||||
branches described below, but remain non-production until their workflow and
|
||||
readback gates pass. Commands and actuators remain out of scope.
|
||||
|
||||
## Feature-branch implementation evidence (2026-08-19)
|
||||
|
||||
- Cognitum API ADR-101 implements all eight `/v1/spatial` collections, coherent
|
||||
transactional hierarchy checks, stable pagination, event/alert retention,
|
||||
strict P2/P3 admission, API-key-only writes, and OAuth/API-key reads. The
|
||||
Firestore emulator passed both the legacy and versioned verification scripts,
|
||||
including cross-resource message replay denial.
|
||||
- `ruview-cognitum-spaces` adds a strictly decoded, paged, read-only client for
|
||||
every versioned collection. `wifi-densepose spaces --resource ...` exposes it
|
||||
while preserving the legacy flat command. The focused client suite and the
|
||||
CLI no-default-features test gate pass.
|
||||
- The contributor metaharness now accepts `resource`, `limit`, and opaque
|
||||
`cursor` on `ruview_spaces_list`, fixes the API origin, strips API-key
|
||||
compatibility authority, and revalidates hierarchy/event/alert contracts.
|
||||
Full metaharness, security, brain, flywheel, manifest, audit, and pack gates
|
||||
pass for the `@ruvnet/ruview` 0.5.0 release candidate.
|
||||
- ADR-326 adds `ruview-spatial-memory`: one RuVector HNSW index per authenticated
|
||||
tenant/workspace, replay/derivation/retention gates, cascading erasure,
|
||||
bounded explanations, and XChaCha20-Poly1305 snapshots with reload-verified
|
||||
key rotation. Its focused crate suite passes with `SYNTHETIC` evidence.
|
||||
- ADR-327 extends `ruview-policy` with typed observe/recommend/execute intents,
|
||||
exact host grants, signed approvals, nonce/idempotency defense, the existing
|
||||
ADR-321 assurance matrix, and signed hash-chained receipts. `spaces:read` is
|
||||
explicitly denied as execution authority. Its focused crate suite passes.
|
||||
- The required whole-workspace Rust command was attempted twice on Windows:
|
||||
parallel compilation ended in a compiler-process stack-buffer-overrun and a
|
||||
single-job retry reached the configured timeout without a source/test
|
||||
diagnostic. This is not recorded as a green gate; Linux CI remains required.
|
||||
- No OAuth write/action scope, actuator callback, MQTT deployment claim, sensing
|
||||
accuracy claim, or real-hardware claim is introduced.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- One Cognitum identity can explicitly activate RuView's cloud spatial read
|
||||
capability without sharing a long-lived static bearer.
|
||||
- Tenant and workspace become cryptographically bound inputs to the data query.
|
||||
- RuView and Autogenous gain useful spatial context without importing raw RF or
|
||||
inventing independent evidence.
|
||||
- The design keeps a path for RuVector-grounded explanations and separately
|
||||
governed action without treating either as part of the deployed read slice.
|
||||
- The direct-origin bypass is closed permanently, independent of OAuth rollout.
|
||||
|
||||
### Costs and limitations
|
||||
|
||||
- Two credential types coexist during migration and must stay visibly distinct.
|
||||
- OAuth depends on Identity JWKS availability and correct key rotation.
|
||||
- Production currently exposes the legacy spaces slice; the implemented full
|
||||
hierarchy/events/alerts model remains staged until deployment/readback.
|
||||
- OAuth workspace IDs will return only documents populated with `workspaceId`;
|
||||
legacy owner-only documents require an explicit migration, never a broad query.
|
||||
- The RuView client exposes no write, command, or agent execution surface. The
|
||||
separate API-key semantic-state ingress is neither OAuth activation nor
|
||||
actuator authority.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep API keys only.** Rejected as the target: keys are useful for service
|
||||
compatibility but do not provide user activation, consent, short lifetime, or
|
||||
refresh/revocation semantics.
|
||||
|
||||
**Treat the CLI API-key exchange token as a Spaces OAuth token.** Rejected: it
|
||||
is minted for `cognitum-cli`, not `ruview`, and accepting it would remove the
|
||||
audience/client boundary.
|
||||
|
||||
**Trust the gateway without verifying OAuth in Spaces.** Rejected: hop identity
|
||||
and user authorization are distinct, and authorization must remain valid if the
|
||||
route topology changes.
|
||||
|
||||
**Make Spaces state independent corroboration.** Rejected: it is derived from
|
||||
the same RuView/HomeCore lineage and would double-count evidence.
|
||||
|
||||
**Allow agents to execute from `spaces:read`.** Rejected: read consent is not
|
||||
action authority, and perception confidence alone cannot authorize consequence.
|
||||
|
||||
**Synchronize raw RF for better cloud models.** Rejected by default: it violates
|
||||
the edge privacy boundary and is unnecessary for the semantic product.
|
||||
|
||||
## References
|
||||
|
||||
- Autogenous ADR-402, `docs/adr/ADR-402-ruview-cognitum-spaces-spatial-intelligence.md`
|
||||
- Cognitum API ADR-094, `docs/adr/ADR-094-cognitum-spaces-homecore-edge-boundary.md`
|
||||
- Cognitum API hierarchy/events/alerts follow-up,
|
||||
`https://github.com/cognitum-one/api/issues/206`
|
||||
- RuView metaharness OAuth surface,
|
||||
`https://github.com/ruvnet/RuView/issues/1643`
|
||||
- RuVector spatial-history follow-up,
|
||||
`https://github.com/ruvnet/RuView/issues/1640`
|
||||
- governed-action and witness-receipt follow-up,
|
||||
`https://github.com/ruvnet/RuView/issues/1641`
|
||||
- RFC 7636, Proof Key for Code Exchange
|
||||
- RFC 8414, OAuth 2.0 Authorization Server Metadata
|
||||
- RFC 9700, OAuth 2.0 Security Best Current Practice
|
||||
- RFC 9728, OAuth 2.0 Protected Resource Metadata
|
||||
137
docs/adr/ADR-326-tenant-scoped-ruvector-spatial-memory.md
Normal file
137
docs/adr/ADR-326-tenant-scoped-ruvector-spatial-memory.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# ADR-326: Tenant-scoped RuVector spatial memory and anomaly explanations
|
||||
|
||||
- **Status**: Accepted — implementation complete; repository-wide and deployment gates pending
|
||||
- **Date**: 2026-08-19
|
||||
- **Decision owners**: RuView maintainers
|
||||
- **Extends**: ADR-312, ADR-319, ADR-325
|
||||
- **Implements**: ruvnet/RuView#1640
|
||||
- **Tags**: cognitum-spaces, ruvector, memory, tenant-isolation, explanation, privacy
|
||||
|
||||
## Context
|
||||
|
||||
ADR-325 requires anomaly explanations grounded in tenant-local spatial history,
|
||||
but the deployed client only returns a current list. A global vector index would
|
||||
be unsafe: filtering nearest-neighbor results after the search can reveal that a
|
||||
different tenant has a close match, even when identifiers are removed. A memory
|
||||
record can also launder returned RuView-derived state into a second independent
|
||||
observation, reset freshness, or form circular evidence.
|
||||
|
||||
Spatial memory must be useful without storing OAuth/API credentials, raw CSI/CIR,
|
||||
RF tensors, pose frames, vital waveforms, recordings, identity observations, or
|
||||
unbounded agent transcripts. Persistence also needs explicit retention,
|
||||
deletion, provenance, and key-rotation behavior.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Partition before similarity
|
||||
|
||||
`ruview-spatial-memory` owns a `SpatialMemory` map keyed by the exact authenticated
|
||||
`(tenant_id, workspace_id)` pair. Each partition owns its own RuVector HNSW index.
|
||||
Ingest and search resolve the partition first; no global ANN query exists. Site,
|
||||
space, schema version, and time-window constraints narrow within the selected
|
||||
partition before results are returned.
|
||||
|
||||
### 2. Bounded semantic records
|
||||
|
||||
An accepted record contains:
|
||||
|
||||
- tenant/workspace/site/space and stable record identity;
|
||||
- source ID, message ID, record ID, monotonic event sequence, schema version;
|
||||
- original `observed_at`/`expires_at` and a retention deadline;
|
||||
- a bounded finite semantic feature vector, uncertainty, and evidence label;
|
||||
- provenance and witness digests, plus bounded derivation references;
|
||||
- explicit observation/inference classification.
|
||||
|
||||
Credentials and P0/P1 fields have no representation in the type. Strings,
|
||||
features, references, record counts, and query `k` are bounded. Non-finite
|
||||
features and uncertainty fail closed.
|
||||
|
||||
### 3. Lineage and replay
|
||||
|
||||
The partition rejects:
|
||||
|
||||
- changed reuse of `(source_id, message_id)`;
|
||||
- a non-increasing sequence for the same source;
|
||||
- duplicate derivation references;
|
||||
- self-reference, missing/forward parents, and therefore every cycle;
|
||||
- expired input or a provenance/witness substitution.
|
||||
|
||||
A recollection keeps its original lineage, timestamp, uncertainty, and evidence
|
||||
label. It cannot increment corroborating-source count or become independent
|
||||
support for its own ancestor.
|
||||
|
||||
### 4. Persistent encrypted storage
|
||||
|
||||
Snapshots are encrypted with XChaCha20-Poly1305 under a caller-supplied 256-bit
|
||||
key and a non-secret key ID. The authenticated associated data binds the storage
|
||||
format and key ID. The envelope is bounded and versioned; plaintext spatial
|
||||
records are never written to disk. Loading requires a keyring containing the
|
||||
named key. Rotation decrypts with the old key, atomically creates a new
|
||||
generation under the new key ID, reload-verifies that generation, and leaves
|
||||
the source intact. Snapshots never overwrite an existing path implicitly.
|
||||
|
||||
Deletion supports a tenant/workspace partition, a record, and retention cutoff.
|
||||
Every deletion rebuilds that partition's HNSW index so removed records cannot be
|
||||
returned from stale graph nodes.
|
||||
|
||||
### 5. Explanations
|
||||
|
||||
`explain` compares a bounded query vector with nearest tenant-local history and
|
||||
returns the exact authenticated partition, generation time, ordered record IDs,
|
||||
RuVector distances, original uncertainty/evidence labels, and provenance/witness
|
||||
digests. Its basis explicitly says that similarity is not causation. The API
|
||||
does not expose the vectors or invent a causal explanation.
|
||||
|
||||
History provides context, not authority. An explanation cannot authorize an
|
||||
action, increase certificate class, or replace a policy decision.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Cross-tenant ANN leakage is structurally unavailable.
|
||||
- Explanations cite the exact tenant-local records used.
|
||||
- Replay/cycle/provenance substitution are rejected before indexing.
|
||||
- Encrypted persistence has explicit key IDs and rotation behavior.
|
||||
|
||||
### Costs and limitations
|
||||
|
||||
- Partition-local HNSW uses more indexes than a global graph.
|
||||
- Deletes and key rotation rebuild indexes.
|
||||
- No detection-quality or latency claim is made; tests are `SYNTHETIC` unless a
|
||||
reproducer explicitly marks a measurement.
|
||||
- Cloud Cognitum does not receive the local encrypted memory file.
|
||||
|
||||
## Validation
|
||||
|
||||
- cross-tenant and cross-workspace nearest-neighbor denial;
|
||||
- duplicate record/message, stale-sequence, self/duplicate/missing-parent, and
|
||||
provenance-substitution tests;
|
||||
- expiry, retention deletion, whole-partition deletion, sealed round-trip,
|
||||
tamper rejection, wrong-key rejection, and key-rotation tests;
|
||||
- explanation citations and retained evidence/provenance labels;
|
||||
- no forbidden raw-field or credential representation;
|
||||
- the focused `ruview-spatial-memory` crate suite passes with `SYNTHETIC`
|
||||
evidence on 2026-08-19;
|
||||
- the whole-workspace Windows gate was non-terminal (compiler crash in parallel,
|
||||
timeout when serialized), so Linux CI, a RustSec advisory scan, and package
|
||||
review remain release gates.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**One global HNSW followed by filtering.** Rejected: ranking itself crosses the
|
||||
tenant boundary.
|
||||
|
||||
**Cloud vector memory.** Rejected as the default: it expands the privacy and
|
||||
credential boundary without being needed for local explanations.
|
||||
|
||||
**Plain JSONL persistence.** Rejected because tenant spatial history is sensitive
|
||||
even when raw sensing is excluded.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-312: Long-term spatial memory
|
||||
- ADR-319: Witness chain
|
||||
- ADR-325: Cognitum Spaces activation and governed exchange
|
||||
- Cognitum API ADR-101
|
||||
- ruvnet/RuView#1640
|
||||
133
docs/adr/ADR-327-governed-action-intents-and-witness-receipts.md
Normal file
133
docs/adr/ADR-327-governed-action-intents-and-witness-receipts.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# ADR-327: Governed action intents, approvals, replay protection, and witness receipts
|
||||
|
||||
- **Status**: Accepted — implementation complete; repository-wide and deployment gates pending
|
||||
- **Date**: 2026-08-19
|
||||
- **Decision owners**: RuView maintainers
|
||||
- **Extends**: ADR-318, ADR-319, ADR-321, ADR-325
|
||||
- **Implements**: ruvnet/RuView#1641
|
||||
- **Tags**: policy, governed-action, approval, idempotency, witness, cognitum-spaces
|
||||
|
||||
## Context
|
||||
|
||||
The current `ruview-policy` crate evaluates assurance for an action class, but it
|
||||
does not define a complete action intent, tenant/workspace binding, policy
|
||||
version, approval, nonce/idempotency replay behavior, or signed terminal receipt.
|
||||
An agent recommendation can therefore be mistaken for execution authority, and
|
||||
`spaces:read` could be accidentally treated as a general capability.
|
||||
|
||||
The system needs a framework that can prove why an action was allowed or denied
|
||||
without adding any actuator. Real actuation remains a separate integration and
|
||||
requires its own threat model and device evidence.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Typed intent and registered policy
|
||||
|
||||
A governed `ActionIntent` binds:
|
||||
|
||||
- intent ID, tenant, workspace, action name/class, and exact target;
|
||||
- requested policy version and parameter/evidence digests;
|
||||
- creation/expiry, replay nonce, and requesting principal;
|
||||
- the recommendation/explanation that motivated review, never a hidden command.
|
||||
|
||||
The gate accepts only a registered action policy. Unknown action, action-class
|
||||
mismatch, policy-version mismatch, target mismatch, invalid timestamps, and
|
||||
missing exact host authority deny before assurance is evaluated. Tenant and
|
||||
workspace are part of the signed intent/receipt and nonce key. `spaces:read` is
|
||||
explicitly tested as insufficient for an `alerts:execute` rule.
|
||||
|
||||
### 2. Assurance and approval
|
||||
|
||||
The existing ADR-321 certificate/domain/uncertainty/evidence gate remains the
|
||||
assurance authority. The registered policy declares a bounded minimum of
|
||||
distinct enrolled approvers. An absent, rejected, duplicated, expired,
|
||||
wrong-intent, wrong-policy-version, or unverifiable approval denies. Approval
|
||||
resolution fails closed.
|
||||
|
||||
Agents observe, explain, or recommend by default. `evaluate` returns a decision
|
||||
receipt; it does not call an actuator. An executor may consume an `allow` receipt
|
||||
only if a separate adapter verifies the receipt, target, expiry, and its own
|
||||
device-specific authority.
|
||||
|
||||
### 3. Replay and idempotency
|
||||
|
||||
The bounded in-memory gate stores terminal receipts by intent ID and tracks
|
||||
nonces by `(tenant, workspace, nonce)`.
|
||||
|
||||
- exact intent replay returns the original terminal receipt;
|
||||
- changed reuse of an intent ID returns a fail-closed idempotency error;
|
||||
- reuse of a nonce by another intent returns a fail-closed replay error;
|
||||
- expired intents and approvals deny;
|
||||
- failed or denied attempts are terminal and auditable.
|
||||
|
||||
The current state store is bounded and in-memory, intended for local/runtime use
|
||||
rather than cross-process replay protection. A production executor must place
|
||||
the same intent/nonce/receipt invariants behind a transactional durable store;
|
||||
this ADR does not claim that adapter exists.
|
||||
|
||||
### 4. Witnessed terminal receipt
|
||||
|
||||
Every evaluated observe/recommend/execute request produces a canonical receipt
|
||||
containing the intent digest, decision/reason, policy version, tenant/workspace,
|
||||
decision/expiry time, intent ID and nonce, approval count, and previous receipt
|
||||
digest. The receipt is signed through the `ruview-attest` signer interface and
|
||||
can be independently verified. Hash chaining makes removal/reordering visible.
|
||||
Malformed input, ID conflict, nonce replay, capacity exhaustion, and sequence
|
||||
exhaustion are errors before receipt creation and must be audited by the host.
|
||||
|
||||
The reference keyed-BLAKE3 signer remains `SYNTHETIC` evidence only, as documented
|
||||
by ADR-319. Production asymmetric signing and key custody must be supplied by the
|
||||
deployment adapter; no symmetric test MAC is represented as hardware identity.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Recommendation, authorization, and execution are distinct typed stages.
|
||||
- Default-deny covers missing policy, stale evidence, unavailable approval, and replay.
|
||||
- Every decision has a terminal, verifiable explanation.
|
||||
- `spaces:read` cannot silently expand into consequence.
|
||||
|
||||
### Costs and limitations
|
||||
|
||||
- Executors must implement a separate receipt-verifying adapter.
|
||||
- Distributed replay protection needs a transactional durable store.
|
||||
- This ADR implements no actuator, command transport, pairing mutation, or device control.
|
||||
- Simulator tests are not hardware validation.
|
||||
|
||||
## Validation
|
||||
|
||||
- unknown/missing policy, stale intent, policy-version/target mismatch,
|
||||
insufficient authority, and `spaces:read`-only denial;
|
||||
- certificate/domain/uncertainty/evidence denial matrix from ADR-321;
|
||||
- missing/rejected/expired/duplicate/wrong-intent approval tests;
|
||||
- exact idempotent replay, changed reuse, nonce replay, and bounded-store tests;
|
||||
- receipt signature, canonical digest, chain linkage, and tamper rejection;
|
||||
- tests proving evaluation exposes no actuator callback or network/file side effect.
|
||||
|
||||
The focused `ruview-policy` suite passes on 2026-08-19. The reference signer
|
||||
tests are `SYNTHETIC`; they are not hardware-identity evidence. The non-terminal
|
||||
whole-workspace Windows gate still requires authoritative Linux CI evidence.
|
||||
|
||||
Any future actuator adds a separate ADR, credential boundary, failure/rollback
|
||||
plan, allow/deny integration tests, and captured target-device evidence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Let agents call actuators after a recommendation.** Rejected: recommendation
|
||||
quality is not authorization.
|
||||
|
||||
**Treat OAuth scopes as action policy.** Rejected: `spaces:read` expresses read
|
||||
consent only and carries no target-specific assurance or approval.
|
||||
|
||||
**Emit receipts only for successful actions.** Rejected: denial and unavailable
|
||||
approval are security-relevant terminal facts.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-318: Capability certificates
|
||||
- ADR-319: Witness chain
|
||||
- ADR-321: Decision policy action authorization
|
||||
- ADR-325: Cognitum Spaces activation and governed exchange
|
||||
- ADR-326: Tenant-scoped RuVector spatial memory
|
||||
- ruvnet/RuView#1641
|
||||
@@ -108,6 +108,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
||||
| [ADR-115](ADR-115-home-assistant-integration.md) | Home Assistant integration via MQTT auto-discovery + Matter bridge (HA-DISCO + HA-FABRIC + HA-MIND) | Accepted (MQTT track) / Proposed (Matter SDK P8b) |
|
||||
| [ADR-169](ADR-169-adam-mode-light-theme.md) | adam-mode — light theme toggle for the three.js realtime demo | Proposed |
|
||||
| [ADR-170](ADR-170-yoga-mode-pose-system.md) | yoga-mode — yoga pose detection, classification, and scoring for the three.js realtime demo | Proposed |
|
||||
| [ADR-324](ADR-324-off-axis-head-coupled-perspective-demo.md) | off-axis-mode — RF-assisted head-coupled perspective demo (clean-room Kooima projection; RF presence gating) | Proposed |
|
||||
|
||||
### Architecture and infrastructure
|
||||
|
||||
@@ -145,6 +146,41 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
||||
| [ADR-287](ADR-287-coherent-wideband-rf-tomography-crate.md) | `wifi-densepose-sar` — coherent wideband RF tomography research crate | Accepted (implemented, published) |
|
||||
| [ADR-285](ADR-285-homecore-wasm-first-metaharness.md) | WASM-first Homecore developer metaharness via `npx homecore` | Accepted (implemented and validated) |
|
||||
| [ADR-286](ADR-286-wifi-densepose-sar-harness-via-metaharness.md) | `wifi-densepose-sar-harness` — MetaHarness with darwin/router/flywheel | Accepted (implemented, published) |
|
||||
| [ADR-288](ADR-288-veil-privacy-shield-compliant-waveform.md) | VEIL — compliant-waveform privacy shield against unauthorized WiFi sensing (`wifi-densepose-privshield`) | Proposed (implemented, P1 reference) |
|
||||
| [ADR-289](ADR-289-wifi-densepose-privshield-harness-via-metaharness.md) | `wifi-densepose-privshield-harness` — npm MetaHarness for the VEIL crate (guidance/router/flywheel) | Proposed (implemented, P1) |
|
||||
| [ADR-290](ADR-290-veil-e2e-hardware-implementation-program.md) | VEIL end-to-end hardware implementation program — portable C core + multi-provider firmware scaffolds (openwifi/openwrt/nexmon/esp32) | Proposed (P4 scaffolding; C core host-validated) |
|
||||
| [ADR-291](ADR-291-public-benchmark-evaluation-harness.md) | Public-benchmark evaluation harness — Widar3.0 ingest, split protocols, leakage guards | Accepted (initial implementation) |
|
||||
| [ADR-292](ADR-292-wideband-80211ax-csi-ingest.md) | Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter, subcarrier-agnostic plumbing | Accepted (initial implementation) |
|
||||
| [ADR-293](ADR-293-vitals-ground-truth-rig.md) | Vitals ground-truth rig — reference ingest, alignment, agreement metrics | Accepted (initial implementation) |
|
||||
| [ADR-294](ADR-294-wifi-veil-integration.md) | WiFi Veil integration — emission-shaping countermeasure as advisory BFLD dependency | Accepted (initial implementation) |
|
||||
| [ADR-295](ADR-295-source-provenance-state-machine.md) | Source provenance state machine — synthetic can never present as live | Accepted (initial implementation) |
|
||||
| [ADR-296](ADR-296-sensor-data-plane-bind-hardening.md) | Sensor data-plane hardening — UDP bind control and source allowlist (step one) | Accepted (initial implementation) |
|
||||
| [ADR-297](ADR-297-multi-node-semantic-correctness.md) | Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state | Accepted (initial implementation) |
|
||||
| [ADR-298](ADR-298-model-release-sanity-gates.md) | Model release sanity gates — block degenerate and mislabeled model artifacts | Accepted (initial implementation) |
|
||||
| [ADR-299](ADR-299-csi-data-incident-repo-controls.md) | Repository CSI data-incident controls — ignore rules and pre-commit/CI policy check | Accepted (controls implemented; tree remediation gated) |
|
||||
| [ADR-300](ADR-300-perception-substrate-program.md) | RuView perception substrate — phased 21-primitive program (calibration, evidence, trust, deployment) | Accepted (program; children ADR-301..317) |
|
||||
| [ADR-301](ADR-301-automatic-domain-calibration.md) | Automatic domain calibration — signed, versioned, invalidatable room fingerprint | Accepted (phase 1) |
|
||||
| [ADR-302](ADR-302-out-of-distribution-detection.md) | Out-of-distribution detection — KNOWN / DEGRADED / UNKNOWN gating | Accepted (phase 1) |
|
||||
| [ADR-303](ADR-303-ground-truth-synchronization.md) | Ground-truth synchronization — reference sensors as a formal validation plane | Proposed (phase 2) |
|
||||
| [ADR-304](ADR-304-evidence-engine.md) | Evidence engine — per-(room,device,subject) accuracy ledger | Accepted (phase 1) |
|
||||
| [ADR-305](ADR-305-authenticated-sensor-identity.md) | Authenticated sensor identity — RF chain of custody | Accepted (phase 1) |
|
||||
| [ADR-306](ADR-306-canonical-spatial-ontology.md) | Canonical spatial ontology — one Site→…→Event model for every surface | Accepted (phase 1) |
|
||||
| [ADR-307](ADR-307-persistent-identity-tracking.md) | Persistent identity & tracking — privacy-preserving probabilistic tracks | Proposed (phase 2) |
|
||||
| [ADR-308](ADR-308-sensor-placement-optimizer.md) | Sensor placement optimizer — floorplan + inventory → recommended positions | Proposed (phase 3) |
|
||||
| [ADR-309](ADR-309-active-sensing.md) | Active sensing — closed-loop RF experiment control | Proposed (phase 3) |
|
||||
| [ADR-310](ADR-310-80211bf-native-architecture.md) | 802.11bf-native architecture — standardized WLAN sensing as native measurement types | Proposed (phase 2) |
|
||||
| [ADR-311](ADR-311-real-sensor-fusion.md) | Real sensor fusion — uncertainty-aware, multiple observations → one world state | Proposed (phase 2) |
|
||||
| [ADR-312](ADR-312-long-term-spatial-memory.md) | Long-term spatial memory — learn the normal physics of a location | Proposed (phase 3) |
|
||||
| [ADR-313](ADR-313-counterfactual-inference.md) | Counterfactual inference — generative spatial reasoning | Proposed (phase 3) |
|
||||
| [ADR-314](ADR-314-information-gain-scheduler.md) | Information-gain scheduler — sample the most informative radios | Proposed (phase 3) |
|
||||
| [ADR-315](ADR-315-digital-rf-twin.md) | Digital RF twin — persistent per-deployment RF model | Proposed (phase 3) |
|
||||
| [ADR-316](ADR-316-fleet-control-plane.md) | Fleet control plane — provisioning to audit trails | Proposed (phase 2) |
|
||||
| [ADR-317](ADR-317-benchmark-multi-domain-scorecard.md) | Multi-domain benchmark scorecard — regressions cannot hide behind pooled accuracy | Accepted (phase 1) |
|
||||
| [ADR-318](ADR-318-capability-certificates.md) | Capability certificates — validated-for-this-environment claims | Accepted (phase 1) |
|
||||
| [ADR-319](ADR-319-witness-chain.md) | Witness chain — staged, signed epistemic envelope | Accepted (phase 1) |
|
||||
| [ADR-320](ADR-320-sensor-hal.md) | RuView sensor HAL — abstract all sensing hardware to one Observation type | Proposed (phase 2) |
|
||||
| [ADR-321](ADR-321-decision-policy-action-authorization.md) | Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, evidence | Accepted (phase 1) |
|
||||
| [ADR-323](ADR-323-native-rust-physics-constrained-pose-refinement.md) | Native Rust physics-constrained pose refinement | Proposed |
|
||||
|
||||
---
|
||||
|
||||
|
||||
71
docs/benchmarks/physics-pose-refinement.md
Normal file
71
docs/benchmarks/physics-pose-refinement.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Physics pose refinement evidence ledger
|
||||
|
||||
ADR-323 performance and accuracy targets are gates, not measured claims. Append
|
||||
rows; never replace prior measurements. Every row must identify the repository
|
||||
commit, lockfile hash, Rust toolchain, target, engine/features, configuration
|
||||
hash, corpus/split hash, command, sample count, and evidence label.
|
||||
|
||||
## Runtime measurements
|
||||
|
||||
| Date | Commit | Lock SHA-256 | Target/toolchain | Engine/config | Tracks | Samples | p50 | p95 | p99/max | RSS delta | Evidence | Reproducer |
|
||||
|---|---|---|---|---|---:|---:|---:|---:|---:|---:|---|---|
|
||||
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | deterministic kinematic shadow, config `ef3cf581f75124c1d45a8d6bedcef32e4d1bacb39ee0dfcd4e520171fda2d8cf` | 1 | 20,000 | 0.0080 ms | 0.0097 ms | 0.0195/0.5465 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | `cargo run --release -p wifi-densepose-physics --example latency_probe -- 20000` |
|
||||
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | deterministic kinematic shadow after final local optimization, same config | 1 | 20,000 | 0.0075 ms | 0.0084 ms | 0.0117/0.1579 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | same release probe command |
|
||||
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | final deterministic kinematic shadow, config `a44dc696234f31eda54cd4b436bc2d2c69b9638565b729ac9f07435cedfd0dcc` | 1 | 20,000 | 0.0071 ms | 0.0084 ms | 0.0147/1.5994 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | same release probe command |
|
||||
|
||||
The probe measures a warm, one-track `PhysicsEngine::process` call. It excludes
|
||||
transport, publication, resident-memory delta, dynamics, and learned inference.
|
||||
It is not evidence for the Pi 5 gate.
|
||||
|
||||
Criterion separately measured `kinematic_one_track` at
|
||||
`[11.911, 12.757, 14.069] us` across 100 samples (approximately 369,000 timed
|
||||
iterations). That benchmark includes observation construction and canonical
|
||||
hashing in the timed routine and uses fresh engine state; it is **MEASURED** on
|
||||
the same local host and is not a percentile or Pi 5 claim.
|
||||
|
||||
## Accuracy measurements
|
||||
|
||||
| Date | Commit | Corpus/split | Variant | Coverage | MPJPE | PCK threshold/result | Foot slide | Jerk | Fall/prone delta | Evidence |
|
||||
|---|---|---|---|---:|---:|---|---:|---:|---:|---|
|
||||
|
||||
No measured accuracy evidence has been recorded. The deterministic tests are
|
||||
L0/SYNTHETIC contract evidence only and cannot satisfy G2.
|
||||
|
||||
## Validation and supply-chain record
|
||||
|
||||
- The default dependency graph is checked to exclude Burn, Rapier, Tch, and
|
||||
ONNX Runtime. Dynamics and learned backends remain opt-in.
|
||||
- Burn CPU serialization/inference tests pass on the authoring host only with
|
||||
Cargo's `--ignore-rust-version`; the resolved CubeCL graph requires Rust 1.92.
|
||||
The workspace file pins Rust 1.89 and the host provides Rust 1.91.1. This is
|
||||
diagnostic, not release approval.
|
||||
- `cargo audit 0.22.1` used RustSec database commit
|
||||
`69f93cf294852cfa9b53751f4ca86de3283dd290` (feed timestamp 2026-08-12).
|
||||
ADR-323 updates remove resolved advisories in `event-listener`, `rkyv`, and
|
||||
`wasmtime`. The workspace still has five advisories in pre-existing
|
||||
`quick-xml` and `rsa` dependency paths; the default physics graph contains
|
||||
none of them. The optional Burn training graph includes yanked `spin 0.9.8`.
|
||||
- `cargo-deny` is not installed on the authoring host, so the required license
|
||||
and policy gate is not claimed complete.
|
||||
- Strict Clippy passes with warnings denied for core/physics default and
|
||||
dynamics builds, the diagnostic learned-CPU build, and the Cog itself with
|
||||
dependency linting excluded. Focused core, physics, dynamics, learned, Cog,
|
||||
sensing-server adapter/live-audit/HTTP, schema, golden, strict-split,
|
||||
feature-boundary, and fuzz-build checks pass.
|
||||
- The repository-wide rustfmt gate is already red across unrelated crates. The
|
||||
sensing-server library has existing warning debt, and unscoped Cog Clippy is
|
||||
blocked by existing `wifi-densepose-ruvector` warnings. The prescribed
|
||||
`cargo test --workspace --no-default-features` did not reach a terminal result
|
||||
in either a 904-second cold or 604-second warm serial run on this Windows
|
||||
host. None of these broader gates is represented as green.
|
||||
- The standalone fuzz lock SHA-256 is
|
||||
`d386c4edb130bb6b2d1a4ef77334c78e25e0695e90a9d97c01284876acb8c2c6`.
|
||||
|
||||
## Required commands
|
||||
|
||||
```text
|
||||
cargo bench -p wifi-densepose-physics
|
||||
node scripts/pose-physics/verify-feature-boundary.mjs
|
||||
bash scripts/verify-pose-physics-splits.sh <manifest.json>
|
||||
bash scripts/replay-pose-physics-golden.sh <golden-results.jsonl>
|
||||
```
|
||||
141
docs/research/privacy-shield/01-sota-survey.md
Normal file
141
docs/research/privacy-shield/01-sota-survey.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 01 — State of the Art
|
||||
|
||||
Scope: what a passive or active adversary can extract about *who* is in a space
|
||||
and *what they are doing* from WiFi, the standard that broadens that surface, and
|
||||
the countermeasures that try to prevent it. Claims are tagged **MEASURED** (from
|
||||
a primary source, with metric), **CLAIMED** (asserted without an independent
|
||||
measurement), or analytical inference (flagged).
|
||||
|
||||
---
|
||||
|
||||
## 1. The attack surface: beamforming feedback (BFI)
|
||||
|
||||
Since WiFi 5 (802.11ac), a client (beamformee) measures the downlink channel,
|
||||
compresses the steering matrix **V** into **Givens-rotation angles φ/ψ**, and
|
||||
transmits them **in cleartext** so the AP can steer beams. Anyone in monitor
|
||||
mode can capture these frames for *every* client simultaneously — no network
|
||||
access, and the target need carry no device. Quantization is coarse (802.11ac
|
||||
angle steps of π/4…π/32 rad) yet retains rich motion and body information.
|
||||
|
||||
| Work | Venue / year | Result | Label |
|
||||
|---|---|---|---|
|
||||
| **BFId** — identity inference from BFI | ACM CCS 2025 (KIT/KASTEL) | Re-identifies individuals from BFI alone; novel 197-person dataset. Press reports **99.5%** in a controlled study (ACM full text was not openable to confirm class count/split) | MEASURED (paper); 99.5% is CLAIMED via press |
|
||||
| **LeakyBeam** — occupancy through walls | NDSS 2025 | Occupancy detection **TPR 82.7% / TNR 96.7%** at **20 m, through walls**, from plaintext BFI. Proposes a BFI-obfuscation defense | MEASURED (attack); defense overhead CLAIMED |
|
||||
| **BFIAttack** — CSI reconstruction from BFI | arXiv 2026 (USF) | Reconstructs CSI from BFI, then defeats CSI defenses. ASR: device auth 95.5% / user auth 92.6% / key-gen 94.2% (single-antenna), 1.5–6 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 | 2–3 m straight walk |
|
||||
| WiFi-ID (2016) | 93%→77% | 2→6 | wavelet features |
|
||||
| WiPIN (2018) | 92–100% | ≤30 | operation-free |
|
||||
| Deep-WiID (2019) | 92.5–99.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 1–7.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 2024–25 | 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
|
||||
94
docs/research/privacy-shield/02-threat-model.md
Normal file
94
docs/research/privacy-shield/02-threat-model.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 02 — Threat Model
|
||||
|
||||
VEIL protects a physical space (a room, a ward, a boardroom, a SCIF) from
|
||||
*unauthorized* WiFi-based inference of **who is present** and **what they are
|
||||
doing**, without denying the space its own working WiFi. This file states the
|
||||
adversary classes, exactly what VEIL defends, and — just as importantly — what
|
||||
it does **not**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Assets
|
||||
|
||||
| Asset | Why it matters |
|
||||
|---|---|
|
||||
| **Identity linkage** | Re-identifying a specific person across time/sessions from their RF signature (BFId-class attack) |
|
||||
| **Occupancy / presence** | Whether the space is occupied, and by how many (LeakyBeam-class, through-wall) |
|
||||
| **Activity / motion** | Gait, gestures, keystrokes, respiration inferred from channel dynamics (BeamSense-class) |
|
||||
| **Communication utility** | The legitimate WiFi link must keep working (≥95% throughput bar) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Adversary classes
|
||||
|
||||
| Class | Position | Capability | In VEIL scope? |
|
||||
|---|---|---|---|
|
||||
| **A1 — external passive sniffer** | Outside the trust boundary (adjacent room, van, hallway), monitor mode | Captures plaintext BFI/CSI for every station; runs BFId/LeakyBeam/BeamSense offline | **Primary target — yes** |
|
||||
| **A2 — external active sensor** | Nearby, transmits its own probing/sounding to solicit measurable responses | Elicits sensing responses; 802.11bf "active" mode | **Partial** — cadence randomization + non-response policy help; full defense needs MAC-layer policy |
|
||||
| **A3 — associated but curious AP** | Inside the link; the party VEIL shares keys with | Sees the un-rotated report by construction | **Out of scope** — this is BFLD's detection/privacy-class problem (ADR-118/141) |
|
||||
| **A4 — supply-chain / firmware** | Compromised radio firmware | Can bypass any transmit-side control | Out of scope (integrity problem, not a waveform problem) |
|
||||
| **A5 — physical / RF-denial** | Wants to *block* WiFi | — | Explicitly rejected: VEIL never jams |
|
||||
|
||||
VEIL's design centers on **A1**, the attacker the literature demonstrates and
|
||||
the one no shipping product addresses.
|
||||
|
||||
---
|
||||
|
||||
## 3. What VEIL guarantees (and the evidence class)
|
||||
|
||||
1. **Cross-session identity unlinkability against A1.** Because the fine-subspace
|
||||
signature is rotated by a fresh secret orthogonal transform each session, an
|
||||
A1 attacker cannot average captures back to a stable per-person template.
|
||||
*Evidence: SYNTHETIC — re-ID collapses from 100% to ~chance in the reference
|
||||
experiment (`cargo test`); real-silicon witness is future work.*
|
||||
2. **Communication preservation.** The transform is key-reversible by the
|
||||
legitimate receiver, and acts only on the identity-bearing fine subspace, so
|
||||
link throughput stays ≥95%. *Evidence: SYNTHETIC model + MEASURED external
|
||||
corroboration (DySPAN 2026: fine-resolution feedback shaping is near-free).*
|
||||
3. **Compliance.** The transform is orthogonal ⇒ energy-preserving ⇒ adds no
|
||||
interfering emission ⇒ not jamming. *Evidence: machine-checked energy ratio =
|
||||
1.000000 in the `compliance` module; statutory analysis in
|
||||
[04-compliance-and-regulatory.md](04-compliance-and-regulatory.md).*
|
||||
|
||||
---
|
||||
|
||||
## 4. What VEIL does NOT do (non-goals, stated to prevent over-claiming)
|
||||
|
||||
- **It does not hide identity from the associated AP (A3).** That party holds the
|
||||
session key. Protecting against a malicious AP requires detection and policy
|
||||
(BFLD), not waveform shaping.
|
||||
- **It is not RF denial or jamming.** It never degrades another station's link.
|
||||
- **It does not, by itself, defeat within-session motion detection.** A single
|
||||
session's rotation is fixed, so coarse presence/motion may still be inferable
|
||||
within one capture window; sounding-cadence randomization mitigates but does
|
||||
not eliminate this. Identity *re-ID* (the brief's metric) is the guaranteed
|
||||
target; motion obfuscation is partial and tracked as future work.
|
||||
- **It is not a camera-grade or medical-grade claim in any direction.**
|
||||
- **It is not validated on hardware yet.** All quantitative defense results are
|
||||
SYNTHETIC until a captured boot/runtime log exists (CLAUDE.md hardware rule).
|
||||
|
||||
---
|
||||
|
||||
## 5. Trust boundary
|
||||
|
||||
```
|
||||
┌────────────────────── protected space ──────────────────────┐
|
||||
│ │
|
||||
│ [person] [person] legitimate STA ⇄ AP (VEIL) │
|
||||
│ │ │ │ shares session key │
|
||||
│ └──── RF ──────┘ │ rotates fine subspace│
|
||||
│ reflections ▼ of its own BFI │
|
||||
│ compliant, key-reversible, │
|
||||
│ energy-preserving emission │
|
||||
└───────────────────────────────────────┬──────────────────────┘
|
||||
│ plaintext BFI on air
|
||||
▼
|
||||
A1 external passive sniffer (monitor mode)
|
||||
sees a freshly-rotated signature each session
|
||||
→ cannot build a stable per-person template
|
||||
→ re-identification → chance
|
||||
```
|
||||
|
||||
The key never crosses the boundary to A1. The AP inside the boundary is trusted
|
||||
for key-sharing (A3 out of scope). No emission crosses the boundary with intent
|
||||
or effect of interfering with another station (A5 rejected).
|
||||
136
docs/research/privacy-shield/03-countermeasure-design.md
Normal file
136
docs/research/privacy-shield/03-countermeasure-design.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# 03 — Countermeasure Design
|
||||
|
||||
How VEIL prevents unauthorized sensing with compliant waveform controls, and how
|
||||
the design maps to [`v2/crates/wifi-densepose-privshield`](../../../v2/crates/wifi-densepose-privshield).
|
||||
|
||||
---
|
||||
|
||||
## 1. The separable-subspace principle
|
||||
|
||||
A compressed beamforming report is not homogeneous. Two blocks carry different
|
||||
information:
|
||||
|
||||
- **Dominant beam direction (comm block).** The coarse steering the AP uses to
|
||||
aim data at the client. It varies with position and traffic and carries **no**
|
||||
stable identity. **Throughput rides here.**
|
||||
- **Fine cross-subcarrier phase structure (fine block).** The high-order
|
||||
multipath detail. It is *stable per person* across sessions and is what
|
||||
re-identification exploits (BFId). **Identity leaks here.** Communication
|
||||
barely uses it.
|
||||
|
||||
The whole design rests on this: **identity leakage and data throughput live in
|
||||
(mostly) separable subspaces.** A transform confined to the fine block can wreck
|
||||
re-identification while sparing the beam the link depends on. This is consistent
|
||||
with the DySPAN-2026 MEASURED result that shaping fine-resolution feedback is
|
||||
nearly free in throughput.
|
||||
|
||||
---
|
||||
|
||||
## 2. The four compliant waveform controls
|
||||
|
||||
VEIL alters "channel sounding, phase, or beam schedules" — exactly the levers the
|
||||
brief names — all within the 802.11 waveform envelope:
|
||||
|
||||
| Control | What it varies | Purpose |
|
||||
|---|---|---|
|
||||
| **Keyed precoder rotation** (primary) | A fresh secret orthogonal transform of the *fine* subspace each session, composed from extra Givens rotations | Destroys cross-session identity linkage; energy-preserving; key-reversible |
|
||||
| **Feedback quantization / dither** | Sub-step noise on reported φ/ψ angles | Adds report-level uncertainty; tunes the privacy–throughput 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 5–9-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` |
|
||||
| Privacy–throughput 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 privacy–throughput 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).
|
||||
90
docs/research/privacy-shield/04-compliance-and-regulatory.md
Normal file
90
docs/research/privacy-shield/04-compliance-and-regulatory.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 04 — Compliance and Regulatory Line
|
||||
|
||||
**Non-negotiable:** VEIL uses compliant waveform controls and **never jams.**
|
||||
This file states the legal basis for that line and why every VEIL control falls
|
||||
on the compliant side of it. It is engineering analysis, not legal advice; a
|
||||
deployment in a given jurisdiction needs its own regulatory review.
|
||||
|
||||
---
|
||||
|
||||
## 1. The statutory line (United States)
|
||||
|
||||
The prohibition is on **interfering with others' transmissions**, not on how you
|
||||
shape **your own** signal.
|
||||
|
||||
| Authority | What it prohibits |
|
||||
|---|---|
|
||||
| **47 U.S.C. §333** | *Willful or malicious interference* with any licensed/authorized radio station or U.S. Government station |
|
||||
| **47 U.S.C. §302a(b)** | Manufacture, import, marketing, sale, or *operation* of non-compliant devices (jammers cannot be certified — their sole purpose is interference) |
|
||||
| **47 U.S.C. §301** | Requires a license/authorization to transmit; a jammer can never be authorized |
|
||||
| **47 U.S.C. §501 / §503** | Criminal penalties and forfeitures; FCC cites fines up to $112,500 per violation, **no exemptions** for business/residence/vehicle |
|
||||
|
||||
The distinguishing element of jamming is **intent to interfere plus effect on a
|
||||
third party's link.** A device that shapes its own standards-conformant emission
|
||||
— staying within transmit-power and spectral-mask limits, still type-certifiable
|
||||
— is not a jammer.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why each VEIL control is compliant
|
||||
|
||||
| Control | Compliance argument |
|
||||
|---|---|
|
||||
| **Keyed precoder rotation** | Orthogonal ⇒ preserves the report's energy exactly ⇒ **adds no power on top of anyone's signal.** It is still a valid precoder within the 802.11 feedback format. Machine-checked: energy ratio = 1.000000 (`compliance` module) |
|
||||
| **Feedback quantization / dither** | Reports angles the standard already allows, at the standard's resolution; sub-step dither stays within the quantization envelope. No emission change beyond the node's own frame |
|
||||
| **Sounding-cadence randomization** | Chooses *when* the node sends its own NDP soundings, within permitted timing. Sending fewer/jittered soundings never interferes with another station |
|
||||
| **MU-group / stream-mapping shuffle** | Rearranges the node's own spatial mapping; a normal in-spec transmit choice |
|
||||
|
||||
None of the four transmits *to prevent* another station from communicating; none
|
||||
adds out-of-mask energy; each passes normal type certification. Contrast a
|
||||
jammer, whose defining purpose is to emit energy that denies others service.
|
||||
|
||||
---
|
||||
|
||||
## 3. The energy-conservation proof as a compliance artifact
|
||||
|
||||
VEIL turns "not jamming" from a promise into a **checked property.** The
|
||||
`compliance::ComplianceReport` audits each protection step:
|
||||
|
||||
```
|
||||
input_energy = ‖report_before‖²
|
||||
output_energy = ‖report_after‖²
|
||||
energy_ratio = output_energy / input_energy # ≈ 1.0 for a rotation
|
||||
energy_conserving = |energy_ratio − 1| ≤ 1e-2
|
||||
adds_interfering_energy = false # by construction
|
||||
is_compliant = energy_conserving ∧ ¬adds_interfering_energy
|
||||
```
|
||||
|
||||
A regulator, an auditor, or the runtime attestation layer (ADR-141) can read the
|
||||
report and verify the shield is a waveform-shaping control, not an interference
|
||||
source. On the reference experiment the measured ratio is **1.000000**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Jurisdictional notes
|
||||
|
||||
- **EU (GDPR framing).** Covert WiFi body-sensing of vital signs is sensitive
|
||||
health data and "almost certainly illegal under GDPR," but effectively
|
||||
unenforceable (receivers are undetectable) — which is precisely why a
|
||||
*technical* control is needed. VEIL as a transmit-side control does not itself
|
||||
raise GDPR issues; it reduces the personal data an attacker can derive.
|
||||
- **RF-emission rules are jurisdiction-specific.** The energy-preserving property
|
||||
is the portable core of the compliance argument, but power/mask/timing limits
|
||||
differ by region and band; a deployment must confirm local rules.
|
||||
- **Deliberate transmit-nulling toward a *located* sniffer** (steering a spatial
|
||||
null at a known passive receiver) is still the node's own emission and adds no
|
||||
interference, but is more aggressive and should get explicit regulatory review
|
||||
before field use. It is not part of the default VEIL profile.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- 47 U.S.C. §333: https://www.law.cornell.edu/uscode/text/47/333
|
||||
- 47 U.S.C. §302a: https://www.law.cornell.edu/uscode/text/47/302a
|
||||
- FCC Jammer Enforcement: https://www.fcc.gov/general/jammer-enforcement · https://www.fcc.gov/enforcement/areas/jammers
|
||||
- FCC Cell/GPS Jamming guidance: https://www.fcc.gov/general/cell-phone-and-gps-jamming
|
||||
- FCC 14-92 enforcement order: https://docs.fcc.gov/public/attachments/FCC-14-92A1.pdf
|
||||
|
||||
*Caveat: FCC pages were cross-verified against Cornell LII; this is engineering
|
||||
analysis, not legal advice.*
|
||||
113
docs/research/privacy-shield/05-experiment-protocol.md
Normal file
113
docs/research/privacy-shield/05-experiment-protocol.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# 05 — Experiment Protocol: Attacker vs. Protector
|
||||
|
||||
This is the "start today" deliverable from the brief: **make one RuView node the
|
||||
attacker and one the protector, and measure whether protection drives identity
|
||||
recognition toward chance while keeping throughput above 95%.** It is realized as
|
||||
a deterministic, reproducible experiment in
|
||||
[`v2/crates/wifi-densepose-privshield`](../../../v2/crates/wifi-densepose-privshield).
|
||||
|
||||
Because it runs on **SYNTHETIC** data (no radio is touched), its numbers describe
|
||||
the model, not real hardware — reproduced by `cargo test`, and to be
|
||||
re-established on silicon with a captured log before any deployment claim.
|
||||
|
||||
---
|
||||
|
||||
## 1. Setup
|
||||
|
||||
- **Protector node.** Emits beamforming feedback shaped by the VEIL controls
|
||||
(keyed per-session fine-subspace rotation + configured feedback resolution and
|
||||
sounding overhead). Models a legitimate AP/STA protecting a room.
|
||||
- **Attacker node.** A passive sniffer that enrolls a template per candidate from
|
||||
captured reports, then classifies fresh captures (nearest-centroid) — the
|
||||
BFId-class re-identification threat.
|
||||
- **Scene.** `SceneConfig` default: 64-dim report, 8 comm dims, **16 candidate
|
||||
identities** (chance = 1/16 = 6.25%), per-identity stable fine-block signature
|
||||
+ per-session environmental nuisance.
|
||||
|
||||
Two runs of the attacker are compared: **shield off** (the attacker sees raw
|
||||
reports) and **shield on** (every captured report is VEIL-protected). The same
|
||||
attacker faces both.
|
||||
|
||||
---
|
||||
|
||||
## 2. Metrics and acceptance bar
|
||||
|
||||
| Metric | Definition | Bar |
|
||||
|---|---|---|
|
||||
| **Re-ID accuracy, shield off** | Top-1 identity accuracy on unprotected traffic | Must be well above chance (threat is real) — bar ≥ 0.5 |
|
||||
| **Re-ID accuracy, shield on** | Top-1 identity accuracy on protected traffic | Must fall into the chance band `1/N · 2 + 0.03` |
|
||||
| **Throughput ratio** | Protected link capacity ÷ baseline capacity | **≥ 0.95** |
|
||||
| **Compliance** | Emission energy ratio ≈ 1 and non-interfering | `is_compliant == true` |
|
||||
|
||||
Overall `passed()` requires all four.
|
||||
|
||||
---
|
||||
|
||||
## 3. Results (SYNTHETIC, hyper-optimized default configuration)
|
||||
|
||||
Reproduce with `cargo test -p wifi-densepose-privshield` (all 35 tests + doctest
|
||||
pass). The default shield config is the `optimize` module's output — 96 Givens
|
||||
passes at 5-bit feedback resolution (see
|
||||
[08-optimization.md](08-optimization.md)). Salient values from the reference run:
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Candidate identities | 16 |
|
||||
| Chance level | 6.25% |
|
||||
| Chance band (acceptance) | ≤ 15.5% |
|
||||
| **Re-ID accuracy, shield OFF** | **100.0%** |
|
||||
| **Re-ID accuracy, shield ON** | **4.7%** |
|
||||
| **Throughput ratio** | **97.60%** |
|
||||
| Emission energy ratio | 1.000000 |
|
||||
| Overall verdict | **PASS** |
|
||||
|
||||
Reading the result: the attacker is a *perfect* re-identifier without protection
|
||||
(the synthetic signatures are cleanly separable), and VEIL drives it *to the
|
||||
chance floor* (4.7% sits just below the ideal 6.25%, i.e. no better than
|
||||
guessing) — while the modeled link keeps 97.6% of its throughput and the
|
||||
emission conserves energy exactly (compliant, not jamming). The same collapse
|
||||
holds under a Cosine-metric attacker and at N=32, confirming it is a property of
|
||||
the signal, not the classifier.
|
||||
|
||||
---
|
||||
|
||||
## 4. Determinism and the witness
|
||||
|
||||
The experiment is byte-reproducible: no OS entropy, no wall-clock, no threads.
|
||||
`proof::Proof` folds the salient outputs (quantized to avoid last-bit f32
|
||||
round-off) into an FNV-1a witness pinned as `EXPECTED_WITNESS`. Any drift in the
|
||||
PRNG stream, rotation schedule, throughput formula, or scene geometry changes the
|
||||
witness and fails `witness_matches_pinned`. This is the same
|
||||
deterministic-proof discipline as `nvsim` and the Python `verify.py`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sensitivity and what to vary next
|
||||
|
||||
`ExperimentConfig` exposes the levers for a fuller study:
|
||||
|
||||
- **`scene.identities`** — larger N lowers the chance floor; confirm collapse
|
||||
holds as candidates grow.
|
||||
- **`scene.env_sigma` / `beam_amplitude`** — nuisance and comm energy; stress the
|
||||
separability assumption.
|
||||
- **`shield.feedback_bits`** — trace the privacy–throughput 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.
|
||||
92
docs/research/privacy-shield/06-market-and-buyers.md
Normal file
92
docs/research/privacy-shield/06-market-and-buyers.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 06 — Market and Buyers
|
||||
|
||||
Facts are tagged **VERIFIED** (from a cited source), **CLAIMED** (asserted by a
|
||||
vendor/analyst/press source), or **SPECULATIVE** (our inference). Market figures
|
||||
are third-party projections, not independent measurements.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why now
|
||||
|
||||
- **The threat is standardized and commercializing (VERIFIED/CLAIMED).** IEEE
|
||||
802.11bf was published Sep 2025; silicon (Infineon AIROC Wi-Fi 7 ACW741x,
|
||||
Qualcomm Dragonwing) lists 802.11bf sensing in 2026 briefs; Origin AI's
|
||||
embedded-sensing program targets late-2026 deployment; Plume/Cognitive Systems
|
||||
WiFi Motion is the largest deployed sensing footprint today.
|
||||
- **The standards body declined to fix privacy (VERIFIED).** The BFI
|
||||
"secure transmission mechanism" proposal (802.11-23/0782) was **withdrawn**;
|
||||
802.11bf shipped with no privacy protections. This is the strongest demand
|
||||
signal — the gap is structural and acknowledged.
|
||||
- **No targeted anti-sensing product ships (VERIFIED by absence).** Every
|
||||
countermeasure (IRShield, PhyCloak, MIMOCrypt, DP-Givens, ScatterShield) is
|
||||
research-stage. The claim "no obvious shipping product protects rooms from this
|
||||
inference" **holds** as of 2026, with one caveat below.
|
||||
|
||||
---
|
||||
|
||||
## 2. First buyers, ranked by procurement readiness
|
||||
|
||||
| Segment | Driver | Readiness |
|
||||
|---|---|---|
|
||||
| **Defence / government** | ICD 705 / DoD EMSEC already mandate RF attenuation in classified spaces; budgets and mandates exist | **Strongest beachhead (VERIFIED)** — but today they buy broadband shielding, not a sensing-specific control |
|
||||
| **Corporate boardrooms / counter-espionage** | TSCM firms (Bastille, Murray Associates) now include WiFi audits and rogue-AP detection; CSI keystroke/gesture inference makes a boardroom shield a natural extension | **VERIFIED demand, EMERGING WiFi-specific** |
|
||||
| **Hospitals** | RF-derived behavioral/vital data is HIPAA PHI; exam rooms, psychiatric units where inference is unwanted | **VERIFIED regulatory hook** — but the hook drives privacy-preserving *sensing* more than a *shield* |
|
||||
| **Hotels** | Documented guest backlash against in-room sensors; privacy as differentiation | **SPECULATIVE** — narrative-led, not procurement-led today |
|
||||
| **Router / AP manufacturers** | Ship opt-out/obfuscation as a firmware feature anticipating regulation | **SPECULATIVE** — no vendor has announced this |
|
||||
|
||||
---
|
||||
|
||||
## 3. Competitive landscape
|
||||
|
||||
- **Direct competitors:** none shipping. All targeted anti-sensing is academic.
|
||||
- **The real substitute (VERIFIED):** broadband RF shielding — SCIF/TEMPEST
|
||||
window film, paint, panels (Signals Defense SD2500: >40 dB, 30 MHz–6 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.*
|
||||
117
docs/research/privacy-shield/07-implementation-and-roadmap.md
Normal file
117
docs/research/privacy-shield/07-implementation-and-roadmap.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 07 — Implementation and Roadmap
|
||||
|
||||
---
|
||||
|
||||
## 1. What ships in this bundle
|
||||
|
||||
- **Reference crate** `v2/crates/wifi-densepose-privshield` (VEIL): a
|
||||
deterministic, dependency-free, WASM-ready pure-compute leaf implementing the
|
||||
full attacker-vs-protector experiment, the four compliant controls, the
|
||||
throughput model, the compliance audit, the `optimize` hyper-optimizer, and a
|
||||
byte-stable proof. 35 tests + doctest pass; builds for
|
||||
`wasm32-unknown-unknown`; clippy-clean.
|
||||
- **This research bundle** (`docs/research/privacy-shield/`).
|
||||
- **[ADR-288](../../adr/ADR-288-veil-privacy-shield-compliant-waveform.md)** — the
|
||||
formal decision record.
|
||||
- **npm metaharness** `harness/wifi-densepose-privshield/`
|
||||
([ADR-289](../../adr/ADR-289-wifi-densepose-privshield-harness-via-metaharness.md))
|
||||
— a per-crate contributor harness (architect/implementer/reviewer/test-writer,
|
||||
router, flywheel) with a dependency-free `guidance` surface that serves this
|
||||
bundle's capability map. `npx wifi-densepose-privshield-harness guidance
|
||||
--topic optimization`.
|
||||
|
||||
The crate is intentionally a **leaf with no internal RuView dependencies**
|
||||
(mirrors `wifi-densepose-aether`), so it can be reasoned about, fuzzed, and
|
||||
ported independently, and so it can never accidentally acquire a path to a radio.
|
||||
|
||||
---
|
||||
|
||||
## 2. Reuse map (how VEIL composes with existing RuView)
|
||||
|
||||
| Existing subsystem | Relationship |
|
||||
|---|---|
|
||||
| **BFLD** (ADR-118/120/121, `wifi-densepose-bfld`) | Detection layer. Its `identity_risk_score` is the natural trigger for VEIL's `SensingDetector` — detect leakage, then shield |
|
||||
| **Privacy control plane** (ADR-141) | VEIL protection steps emit `ComplianceReport`s that fit the runtime-attestation model (which mode, which actions, which fields) |
|
||||
| **Active sensing / governed actuation** (ADR-280) | VEIL is a defensive `SensingAction`: a governed, privacy-ceiling-bounded emission-shaping action the control plane can schedule |
|
||||
| **Givens/beamforming primitives** | VEIL reuses the report's native Givens-rotation structure rather than inventing a new transform |
|
||||
| **Deterministic proof discipline** (`nvsim`, `archive/v1/verify.py`) | VEIL's `proof` module follows the same pinned-witness pattern |
|
||||
|
||||
---
|
||||
|
||||
## 3. Phased rollout
|
||||
|
||||
| Phase | Deliverable | Evidence class |
|
||||
|---|---|---|
|
||||
| **P1 — reference model (this PR)** | Crate + experiment + docs + ADR | SYNTHETIC (cargo test) |
|
||||
| **P2 — sensitivity study** | Sweep N, noise, resolution, mixing; add a learned attacker to confirm signal-level collapse | SYNTHETIC |
|
||||
| **P3 — BFLD integration** | Wire `identity_risk` → `SensingDetector` → shield engage; emit attestation | SYNTHETIC + integration tests |
|
||||
| **P4 — firmware feedback shaping** | Implement keyed fine-subspace rotation + cadence randomization in the **beamforming-feedback / spatial-mapping path** — see §3.1 for the (non-trivial) platform reality | build + hardware |
|
||||
| **P5 — two-node hardware measurement** | Attacker (Wi-BFI capture) vs. VEIL protector on real silicon; iperf throughput; captured log | **MEASURED** (with witness) |
|
||||
| **P6 — deployment profiles** | Per-segment profiles (SCIF, boardroom, ward) with regulatory review | operational |
|
||||
|
||||
No defense claim graduates from SYNTHETIC to MEASURED without a captured
|
||||
boot/runtime log (CLAUDE.md hardware rule).
|
||||
|
||||
### 3.1 Does this need custom WiFi firmware? (yes — and ESP32 is the wrong chip for the protector)
|
||||
|
||||
VEIL shapes the **compressed beamforming report** (the Givens φ/ψ angles) or the
|
||||
LTF **spatial mapping** as it is transmitted — machinery that lives *below* the
|
||||
driver, inside the chip's PHY/MAC firmware. It is **not** reachable from user
|
||||
space, so a real deployment is a firmware/driver change, not an app.
|
||||
|
||||
- **ESP32 — not viable as the protector.** Its WiFi lower layers are a closed
|
||||
Espressif blob. ESP-IDF exposes CSI *read* (`esp_wifi_set_csi`) — which is why
|
||||
`firmware/esp32-csi-node/` makes a great **attacker/sensor** node — but it does
|
||||
**not** let you rewrite how the chip builds/sends beamforming feedback. ESP32
|
||||
is the *attacker* in a testbed, not the shield.
|
||||
- **Realistic protector platforms:** **openwifi** (open 802.11 on SDR/FPGA —
|
||||
full PHY/MAC control incl. the AP-side compensation; the honest end-to-end
|
||||
route; Verilog + a C driver); **Nexmon** (C firmware *patches* for
|
||||
Broadcom/Cypress, e.g. RPi BCM43455 — the commodity path, and the same
|
||||
framework the BFI *attack* tools already use); open drivers (**ath9k/mt76**)
|
||||
for partial control; or **vendor firmware** for a production feature.
|
||||
- **Two firmware variants:** the **keyed-reversible** version (VEIL's ~98%
|
||||
throughput) needs changes on **both** ends plus key agreement (cf. the
|
||||
LeakyBeam AP-side `Q_obf` is *client-transparent* — only the AP changes — which
|
||||
is a deployment advantage worth adopting, §09 backlog item 3); the
|
||||
**emitter-only DP dither** version needs only the reporting device but pays the
|
||||
full throughput cost.
|
||||
|
||||
The current crate is deliberately a std-only, no-radio leaf and implements none
|
||||
of this; P4 is where it meets silicon.
|
||||
|
||||
---
|
||||
|
||||
## 4. Open problems (tracked honestly)
|
||||
|
||||
1. **Real-hardware separability.** Comm and identity information are only
|
||||
*approximately* separable on real radios; the true throughput cost of full
|
||||
identity hiding may exceed the model's ~2%. P2/P5 must bound it.
|
||||
2. **Within-session motion leakage.** A fixed per-session rotation does not
|
||||
obfuscate coarse motion within one capture window. Needs stronger cadence
|
||||
randomization or amplitude shaping; currently a stated non-goal for the re-ID
|
||||
metric.
|
||||
3. **Active adversary (A2).** An attacker that transmits its own soundings is
|
||||
only partially addressed by cadence control; a MAC-layer non-response policy
|
||||
is needed.
|
||||
4. **Key management.** The per-session rotation key must be derived from the
|
||||
negotiated link secret; VEIL's PRNG is explicitly *not* cryptographic and must
|
||||
not be used for real key material.
|
||||
5. **Regulatory review per jurisdiction.** The energy-conservation argument is
|
||||
portable, but power/mask/timing limits and any transmit-nulling profile need
|
||||
local review before field use.
|
||||
|
||||
---
|
||||
|
||||
## 5. Validation commands
|
||||
|
||||
```bash
|
||||
# Reference experiment + all unit/proof/doc tests
|
||||
cargo test -p wifi-densepose-privshield --no-default-features
|
||||
|
||||
# WASM portability (leaf builds with no radio path)
|
||||
cargo build -p wifi-densepose-privshield --target wasm32-unknown-unknown
|
||||
|
||||
# Lints
|
||||
cargo clippy -p wifi-densepose-privshield --all-targets
|
||||
```
|
||||
142
docs/research/privacy-shield/08-optimization.md
Normal file
142
docs/research/privacy-shield/08-optimization.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 08 — Hyper-Optimization
|
||||
|
||||
The reference crate first shipped a **hand-picked** shield config (112 Givens
|
||||
passes, 7-bit feedback). This file records how the `optimize` module replaces
|
||||
that guess with a *derived*, robustness-verified optimum, and what it found. All
|
||||
numbers are **SYNTHETIC / L0**, reproduced by
|
||||
`cargo test -p wifi-densepose-privshield`.
|
||||
|
||||
---
|
||||
|
||||
## 1. What is being optimized, and against what
|
||||
|
||||
Two knobs, two objectives, one hard constraint:
|
||||
|
||||
| Knob | Costs | Does it trade against privacy? |
|
||||
|---|---|---|
|
||||
| `feedback_bits` (angle resolution) | Throughput: **residual** falls with bits, **feedback airtime** rises with bits | No — the keyed rotation is applied regardless of resolution |
|
||||
| `givens_passes` (rotation mixing) | Compute only | Yes — more mixing ⇒ lower re-ID |
|
||||
|
||||
**Constraint:** re-ID must collapse into the chance band `1/N · 2 + 0.03` — and
|
||||
it must do so *robustly*: for **both** attacker metrics (Euclidean and Cosine)
|
||||
and **both** identity counts (N = 16 and N = 32, the harder, lower-chance case).
|
||||
|
||||
The key structural fact: **rotation mixing is throughput-free.** The per-session
|
||||
rotation is derived from the shared link secret on both ends (like MIMOCrypt) —
|
||||
it is never transmitted — so extra Givens passes cost compute, not airtime. That
|
||||
means privacy margin is essentially free; the only throughput tradeoff lives in
|
||||
`feedback_bits`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Throughput is a 1-D problem with an interior optimum
|
||||
|
||||
Because the residual falls with bits while feedback airtime rises, throughput
|
||||
has a genuine interior optimum in `feedback_bits` (`LinkModel`, default SNR 20 dB,
|
||||
`feedback_overhead_per_bit = 0.0008`):
|
||||
|
||||
| bits | throughput ratio |
|
||||
|---|---|
|
||||
| 1 | 0.9681 |
|
||||
| 2 | 0.9757 |
|
||||
| **3** | **0.9769** ← unconstrained optimum |
|
||||
| 4 | 0.9766 |
|
||||
| **5** | **0.9760** ← shipped (spec-allowed) |
|
||||
| 7 | 0.9744 (the old hand-picked value) |
|
||||
| 9 | 0.9728 |
|
||||
| 12 | 0.9704 |
|
||||
|
||||
The unconstrained optimum is **3 bits** — which coincides with the DySPAN-2026
|
||||
MEASURED finding that ~3-bit feedback is the privacy–utility sweet spot, because
|
||||
the receiver compensates the keyed rotation and extra bits mostly buy airtime.
|
||||
802.11 compressed beamforming quantizes ψ/φ to roughly 5–9 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
|
||||
5–10 dB, 3 bits at 20–40 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.
|
||||
151
docs/research/privacy-shield/09-sota-update-2026.md
Normal file
151
docs/research/privacy-shield/09-sota-update-2026.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 09 — SOTA Update (2025–2026) 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 1–4 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]` Privacy–throughput *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** privacy–throughput 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 1–2). Two DP
|
||||
mechanisms are SYNTHETIC/CLAIMED, not silicon. Governance/standard pillar
|
||||
unverified in this run.*
|
||||
102
docs/research/privacy-shield/README.md
Normal file
102
docs/research/privacy-shield/README.md
Normal 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) | 2025–2026 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.
|
||||
38
docs/schemas/pose-observation-v2.schema.json
Normal file
38
docs/schemas/pose-observation-v2.schema.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://ruview.net/schemas/pose-observation-v2.schema.json",
|
||||
"title": "PoseObservationV2",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "timestamp_ns", "sensor_epoch", "sequence", "track_id", "frame", "calibration_id", "floor_plane", "model", "source", "trust_state", "dimensionality", "uncertainty_calibrated", "joints", "observer_confidence", "canonical_hash"],
|
||||
"properties": {
|
||||
"schema_version": { "const": 2 },
|
||||
"timestamp_ns": { "type": "integer", "minimum": 0 },
|
||||
"sensor_epoch": { "type": "integer", "minimum": 0 },
|
||||
"sequence": { "type": "integer", "minimum": 0 },
|
||||
"track_id": { "$ref": "#/$defs/string_id" },
|
||||
"frame": { "$ref": "#/$defs/frame" },
|
||||
"calibration_id": { "$ref": "#/$defs/string_id" },
|
||||
"floor_plane": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/floor" }] },
|
||||
"model": { "$ref": "#/$defs/model" },
|
||||
"source": { "$ref": "#/$defs/source" },
|
||||
"trust_state": { "enum": ["KNOWN", "DEGRADED", "UNKNOWN"] },
|
||||
"dimensionality": { "enum": ["image2d", "metric3d"] },
|
||||
"uncertainty_calibrated": { "type": "boolean" },
|
||||
"joints": { "type": "array", "minItems": 17, "maxItems": 17, "items": { "$ref": "#/$defs/joint" } },
|
||||
"observer_confidence": { "$ref": "#/$defs/probability" },
|
||||
"canonical_hash": { "$ref": "#/$defs/hash" }
|
||||
},
|
||||
"$defs": {
|
||||
"probability": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||
"hash": { "type": "array", "minItems": 32, "maxItems": 32, "items": { "type": "integer", "minimum": 0, "maximum": 255 } },
|
||||
"string_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||
"vec3": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "type": "number" } },
|
||||
"frame": { "type": "object", "additionalProperties": false, "required": ["name", "version", "metric", "right_handed", "z_up"], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 128 }, "version": { "type": "integer", "minimum": 1 }, "metric": { "type": "boolean" }, "right_handed": { "type": "boolean" }, "z_up": { "type": "boolean" } } },
|
||||
"floor": { "type": "object", "additionalProperties": false, "required": ["normal", "offset_m"], "properties": { "normal": { "$ref": "#/$defs/vec3" }, "offset_m": { "type": "number" } } },
|
||||
"model": { "type": "object", "additionalProperties": false, "required": ["id", "artifact_hash"], "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 128 }, "artifact_hash": { "$ref": "#/$defs/hash" } } },
|
||||
"source": { "type": "object", "additionalProperties": false, "required": ["sensor_id", "authenticated", "replay_protected"], "properties": { "sensor_id": { "type": "string", "minLength": 1, "maxLength": 128 }, "authenticated": { "type": "boolean" }, "replay_protected": { "type": "boolean" } } },
|
||||
"covariance": { "type": "object", "additionalProperties": false, "required": ["xx", "xy", "xz", "yy", "yz", "zz"], "properties": { "xx": { "type": "number", "minimum": 0 }, "xy": { "type": "number" }, "xz": { "type": "number" }, "yy": { "type": "number", "minimum": 0 }, "yz": { "type": "number" }, "zz": { "type": "number", "minimum": 0 } } },
|
||||
"joint": { "type": "object", "additionalProperties": false, "required": ["kind", "position_m", "covariance_m2", "confidence", "visibility"], "properties": { "kind": { "enum": ["nose", "left_eye", "right_eye", "left_ear", "right_ear", "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", "left_wrist", "right_wrist", "left_hip", "right_hip", "left_knee", "right_knee", "left_ankle", "right_ankle"] }, "position_m": { "$ref": "#/$defs/vec3" }, "covariance_m2": { "$ref": "#/$defs/covariance" }, "confidence": { "$ref": "#/$defs/probability" }, "visibility": { "enum": ["visible", "occluded", "unknown"] } } }
|
||||
}
|
||||
}
|
||||
36
docs/schemas/pose-refinement-v1.schema.json
Normal file
36
docs/schemas/pose-refinement-v1.schema.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://ruview.net/schemas/pose-refinement-v1.schema.json",
|
||||
"title": "PoseRefinementV1",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "raw_observation_hash", "mode", "disposition", "selected", "refined_joints_m", "physics_confidence", "effective_confidence", "intervention", "residuals", "refined_residuals", "contact_hypotheses", "dynamics", "provenance", "reason", "canonical_hash"],
|
||||
"properties": {
|
||||
"schema_version": { "const": 1 },
|
||||
"raw_observation_hash": { "$ref": "#/$defs/hash" },
|
||||
"mode": { "enum": ["off", "audit", "shadow_correct", "opt_in_correct", "default_correct"] },
|
||||
"disposition": { "enum": ["bypassed", "audited2d", "audited", "shadowed", "corrected", "abstained", "rejected"] },
|
||||
"selected": { "type": "boolean" },
|
||||
"refined_joints_m": { "oneOf": [{ "type": "null" }, { "type": "array", "minItems": 17, "maxItems": 17, "items": { "$ref": "#/$defs/vec3" } }] },
|
||||
"physics_confidence": { "$ref": "#/$defs/probability" },
|
||||
"effective_confidence": { "$ref": "#/$defs/probability" },
|
||||
"intervention": { "$ref": "#/$defs/intervention" },
|
||||
"residuals": { "$ref": "#/$defs/residuals" },
|
||||
"refined_residuals": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/residuals" }] },
|
||||
"contact_hypotheses": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "$ref": "#/$defs/contact" } },
|
||||
"dynamics": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/dynamics" }] },
|
||||
"provenance": { "$ref": "#/$defs/provenance" },
|
||||
"reason": { "enum": [null, "mode_off", "unsupported_schema", "hash_mismatch", "invalid_number", "invalid_covariance", "stale_input", "coordinate_frame_mismatch", "calibration_unavailable", "uncertainty_uncalibrated", "ood_unknown", "source_unauthenticated", "replay_protection_unavailable", "correction_not_authorized", "replay_rejected", "non_monotonic_input", "too_few_known_joints", "correction_too_large", "deadline_exceeded", "track_capacity", "internal_error"] },
|
||||
"canonical_hash": { "$ref": "#/$defs/hash" }
|
||||
},
|
||||
"$defs": {
|
||||
"probability": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||
"hash": { "type": "array", "minItems": 32, "maxItems": 32, "items": { "type": "integer", "minimum": 0, "maximum": 255 } },
|
||||
"vec3": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "type": "number" } },
|
||||
"intervention": { "type": "object", "additionalProperties": false, "required": ["max_joint_correction_m", "root_correction_m", "corrected_joint_count", "solver_iterations", "elapsed_us"], "properties": { "max_joint_correction_m": { "type": "number", "minimum": 0 }, "root_correction_m": { "type": "number", "minimum": 0 }, "corrected_joint_count": { "type": "integer", "minimum": 0, "maximum": 17 }, "solver_iterations": { "type": "integer", "minimum": 0, "maximum": 8 }, "elapsed_us": { "type": "integer", "minimum": 0 } } },
|
||||
"residuals": { "type": "object", "additionalProperties": false, "required": ["bone_m", "joint_limit_rad", "velocity_mps", "acceleration_mps2", "temporal_jerk", "floor_penetration_m", "contact_m", "collision_m", "normalized_total"], "properties": { "bone_m": { "type": "number", "minimum": 0 }, "joint_limit_rad": { "type": "number", "minimum": 0 }, "velocity_mps": { "type": "number", "minimum": 0 }, "acceleration_mps2": { "type": "number", "minimum": 0 }, "temporal_jerk": { "type": "number", "minimum": 0 }, "floor_penetration_m": { "type": "number", "minimum": 0 }, "contact_m": { "type": "number", "minimum": 0 }, "collision_m": { "type": "number", "minimum": 0 }, "normalized_total": { "type": "number", "minimum": 0 } } },
|
||||
"contact": { "type": "object", "additionalProperties": false, "required": ["state", "probability"], "properties": { "state": { "enum": ["hypothesis", "measured", "unknown"] }, "probability": { "$ref": "#/$defs/probability" } } },
|
||||
"dynamics": { "type": "object", "additionalProperties": false, "required": ["stable", "segment_count", "joint_count", "contact_count", "substeps", "tracking_error_m", "joint_anchor_error_m", "floor_penetration_m", "control_effort"], "properties": { "stable": { "type": "boolean" }, "segment_count": { "type": "integer", "minimum": 0, "maximum": 255 }, "joint_count": { "type": "integer", "minimum": 0, "maximum": 255 }, "contact_count": { "type": "integer", "minimum": 0, "maximum": 65535 }, "substeps": { "type": "integer", "minimum": 1, "maximum": 8 }, "tracking_error_m": { "type": "number", "minimum": 0 }, "joint_anchor_error_m": { "type": "number", "minimum": 0 }, "floor_penetration_m": { "type": "number", "minimum": 0 }, "control_effort": { "type": "number", "minimum": 0 } } },
|
||||
"provenance": { "type": "object", "additionalProperties": false, "required": ["engine", "engine_version", "config_hash", "rf_model_hash", "calibration_id", "learned_artifact_hash"], "properties": { "engine": { "type": "string", "minLength": 1, "maxLength": 128 }, "engine_version": { "type": "string", "minLength": 1, "maxLength": 64 }, "config_hash": { "$ref": "#/$defs/hash" }, "rf_model_hash": { "$ref": "#/$defs/hash" }, "calibration_id": { "type": "string", "minLength": 1, "maxLength": 128 }, "learned_artifact_hash": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/hash" }] } } }
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
|
||||
- [ESP32-S3 (Full CSI)](#esp32-s3-full-csi)
|
||||
- [ESP32 Multistatic Mesh (Advanced)](#esp32-multistatic-mesh-advanced)
|
||||
- [Connect Mesh Data to the Dashboard and Observatory](#connect-mesh-data-to-the-dashboard-and-observatory)
|
||||
- [Cognitum Spaces activation](#cognitum-spaces-activation)
|
||||
- [Cognitum Seed Integration (ADR-069)](#cognitum-seed-integration-adr-069)
|
||||
5. [REST API Reference](#rest-api-reference)
|
||||
6. [WebSocket Streaming](#websocket-streaming)
|
||||
@@ -38,6 +39,7 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
|
||||
14. [Training a Model](#training-a-model)
|
||||
- [CRV Signal-Line Protocol](#crv-signal-line-protocol)
|
||||
14. [RVF Model Containers](#rvf-model-containers)
|
||||
14. [Perception Certificate Spine (Developer Preview, ADR-300)](#perception-certificate-spine-developer-preview-adr-297)
|
||||
14. [Hardware Setup](#hardware-setup)
|
||||
- [ESP32-S3 Mesh](#esp32-s3-mesh)
|
||||
- [Intel 5300 / Atheros NIC](#intel-5300--atheros-nic)
|
||||
@@ -424,6 +426,57 @@ curl http://localhost:3000/api/v1/sensing/latest
|
||||
|
||||
If the ESP32 nodes are provisioned with `--target-ip <AGGREGATOR_HOST>`, that IP must be the machine running `sensing-server`. Only one process can receive UDP `:5005` at a time, so leave the standalone hardware `aggregator` off while the dashboard or Observatory is live.
|
||||
|
||||
### Cognitum Spaces activation
|
||||
|
||||
Cognitum Spaces gives RuView a tenant/workspace-scoped semantic world model
|
||||
without uploading raw RF/CSI, recordings, pose frames, vital waveforms, or
|
||||
identity observations. It represents sites, buildings, floors, bounded
|
||||
rooms/spaces, zones, anonymous entities, semantic events, and alerts.
|
||||
|
||||
Activate the public RuView OAuth client with Authorization Code + PKCE:
|
||||
|
||||
```bash
|
||||
wifi-densepose login --spaces
|
||||
wifi-densepose whoami
|
||||
wifi-densepose spaces --resource sites --limit 50
|
||||
wifi-densepose spaces --resource events --limit 25
|
||||
```
|
||||
|
||||
The login requests `sensing:read spaces:read`. That consent is read-only: it
|
||||
does not grant publication, pairing, policy approval, command, or actuator
|
||||
authority. Versioned collections are `sites`, `buildings`, `floors`,
|
||||
`spaces`, `zones`, `entities`, `events`, and `alerts`. A returned
|
||||
`nextCursor` is opaque and valid only for the same collection.
|
||||
|
||||
The dependency-free contributor harness exposes the same read path:
|
||||
|
||||
```bash
|
||||
npx @ruvnet/ruview@0.5.0 spaces --resource alerts --limit 25
|
||||
npx @ruvnet/ruview@0.5.0 mcp start
|
||||
```
|
||||
|
||||
Its MCP tool is `ruview_spaces_list`. MCP reads are OAuth-only, use the fixed
|
||||
Cognitum API origin, and require the explicit guarded-tool opt-in. The harness
|
||||
does not accept an arbitrary credential path or API origin.
|
||||
|
||||
For service compatibility, `wifi-densepose spaces` can read
|
||||
`COGNITUM_SPACES_API` at request time. API-key access to a versioned collection
|
||||
also requires `--workspace <uuid>`; OAuth derives the workspace from the
|
||||
signed token. Never print or commit either credential.
|
||||
|
||||
Every response is bounded and revalidated. Raw-sensing aliases, malformed
|
||||
hierarchy, non-anonymous person/track entities, invalid timestamps, stale
|
||||
confidence, and oversized structures fail closed. Empty data means no
|
||||
authorized state is present; it does not prove that a physical site is empty.
|
||||
|
||||
RuVector spatial memory remains physically separated by tenant and workspace.
|
||||
Agents observe or recommend by default. Any consequential execution requires a
|
||||
separate policy/grant/approval decision and produces a signed, hash-chained
|
||||
receipt; the Spaces read token can never satisfy that gate.
|
||||
|
||||
See ADR-325, ADR-326, and ADR-327 for the activation, memory, and governed-action
|
||||
decisions.
|
||||
|
||||
### Cognitum Seed Integration (ADR-069)
|
||||
|
||||
Connect an ESP32-S3 to a [Cognitum Seed](https://cognitum.one) (Pi Zero 2 W, ~$15) for persistent vector storage, kNN similarity search, cryptographic witness chain, and AI-accessible sensing via MCP proxy.
|
||||
@@ -1493,6 +1546,101 @@ An RVF file contains: model weights, HNSW vector index, quantization codebooks,
|
||||
|
||||
---
|
||||
|
||||
## Perception Certificate Spine (Developer Preview, ADR-300)
|
||||
|
||||
RuView's perception substrate program (ADR-300) is building a `signal → observation →
|
||||
calibration → inference → uncertainty → evidence → certificate → policy → governed
|
||||
action` pipeline, where a downstream consumer either gets a calibrated, provenance-backed
|
||||
answer or an explicit `UNKNOWN` — never a confident-looking guess outside the sensor's
|
||||
proven operating envelope.
|
||||
|
||||
**Status: developer preview, now wired at the crate level.** Phase 1 shipped nine new
|
||||
crates. `ruview-certify` and `ruview-policy` now depend on `ruview-ood` and provide a
|
||||
real adapter (`impl From<ruview_ood::DomainState> for _`) plus a composed entry point,
|
||||
`ruview_policy::authorize_from_certificate`, that takes a real signed
|
||||
`CapabilityCertificate` and a real `ruview_ood::DomainState` and drives them through
|
||||
`authorize()` — not a hand-built `AssuranceInputs`. A cross-crate integration test
|
||||
(`ruview-policy`'s `acceptance_test_b_real_integration` module) mints an actual signed
|
||||
certificate and proves a real post-drift `Unknown` denies a `SafetyCritical` action
|
||||
through that one composed pipeline.
|
||||
|
||||
**What's still not done:** none of this runs automatically inside the live
|
||||
`sensing-server` request path yet — there is no continuous calibration/OOD-monitoring
|
||||
loop wired into the running server that calls this pipeline on live sensor data. Treat
|
||||
`authorize_from_certificate` as a real, tested library entry point you can call from your
|
||||
own integration today, not something the server invokes for you on every request yet.
|
||||
That remaining step is a genuinely separate, larger effort (deciding polling cadence,
|
||||
where calibration state lives, what triggers re-certification) — see ADR-300 for the
|
||||
phased plan.
|
||||
|
||||
### The crates
|
||||
|
||||
| Crate | Role |
|
||||
|---|---|
|
||||
| `ruview-ontology` | Canonical `Site → … → Event` types |
|
||||
| `ruview-attest` | Signed measurement / RF chain-of-custody |
|
||||
| `ruview-evidence` | Append-only per-context ledger (no pooling, no evidence upgrade) |
|
||||
| `wifi-densepose-calibration` | Signed, drift-invalidatable calibration certificate |
|
||||
| `ruview-ood` | `Known` / `Degraded` / `Unknown` staleness-guard domain gating |
|
||||
| `ruview-witness` | Hash-linked staged provenance chain |
|
||||
| `ruview-certify` | Capability certificate, conditional on a live domain signature |
|
||||
| `ruview-scorecard` | Multi-domain scorecard, worst-domain promotion gate |
|
||||
| `ruview-policy` | Fail-closed action authorization gate |
|
||||
|
||||
### Minting and checking a certificate
|
||||
|
||||
```rust
|
||||
use ruview_certify::{mint, CapabilityCertificate, DomainState};
|
||||
|
||||
// `signer`, `request`, and `evidence_slice` come from your own calibration run —
|
||||
// see each crate's README for how to build them.
|
||||
let cert = mint(&signer, request, &evidence_slice)?;
|
||||
|
||||
// A certificate is only valid at a given instant AND domain state — the same
|
||||
// signed certificate is rejected the moment the live domain degrades:
|
||||
assert!(cert.is_valid(now_ms, DomainState::Known));
|
||||
assert!(!cert.is_valid(now_ms, DomainState::Degraded));
|
||||
assert!(!cert.is_valid(now_ms, DomainState::Unknown));
|
||||
```
|
||||
|
||||
### Gating an action from a real certificate + a real OOD reading
|
||||
|
||||
```rust
|
||||
use ruview_policy::authorize_from_certificate;
|
||||
|
||||
// `cert` (ruview_certify::CapabilityCertificate) and `domain`
|
||||
// (ruview_ood::DomainState) come from your own certify/OOD calls.
|
||||
let decision = authorize_from_certificate(
|
||||
ActionClass::SafetyCritical,
|
||||
&cert, &verifier, now_unix_s, domain,
|
||||
certificate_class, uncertainty, evidence_level,
|
||||
);
|
||||
// Deny with a named FailedCondition (e.g. DomainNotKnown) — not a silent
|
||||
// false-positive — the moment `domain` degrades, even though `cert` itself
|
||||
// is still validly signed and unexpired.
|
||||
```
|
||||
|
||||
`ruview_certify::DomainState` and `ruview_policy::DomainState` are still each their own
|
||||
type (`ruview-ood`'s `Degraded`/`Unknown` additionally carry a `DomainCause`), but the
|
||||
conversion between them is no longer something you have to write yourself —
|
||||
`authorize_from_certificate` does it via the crates' own `From<ruview_ood::DomainState>`
|
||||
impls.
|
||||
|
||||
### What's genuinely enforced today, for comparison
|
||||
|
||||
Not every ADR-295–296 remediation item is preview-only. Three are live now:
|
||||
|
||||
- **UDP data-plane bind hardening (ADR-296)** — `sensing-server`'s `UdpSourceAllowlist`
|
||||
is checked on every incoming packet (`main.rs`), not just defined.
|
||||
- **CSI data-incident repo controls (ADR-299)** — `scripts/csi-data-policy-check.sh`
|
||||
runs in CI on every push/PR and fails the build on a policy violation.
|
||||
- **Synthetic-export watermarking (ADR-295)** — `start_recording` stamps a `SYNTHETIC`
|
||||
watermark on a recording's metadata (`GET /api/v1/recordings`, the start-recording
|
||||
response) whenever it captures while the live source is synthetic — an operator
|
||||
browsing or scripting against recordings can't mistake generated data for a capture.
|
||||
|
||||
---
|
||||
|
||||
## Hardware Setup
|
||||
|
||||
### Supported targets
|
||||
|
||||
378
examples/three.js/demos/07-off-axis-window.html
Normal file
378
examples/three.js/demos/07-off-axis-window.html
Normal file
@@ -0,0 +1,378 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RuView · ADR-324 · off-axis window (ruview-offaxis WASM)</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect x='7' y='9' width='18' height='14' rx='2' fill='none' stroke='%23e8a634' stroke-width='2'/><circle cx='16' cy='16' r='3' fill='%23e8a634'/></svg>">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0a0a;
|
||||
--bg-panel: rgba(0, 0, 0, 0.88);
|
||||
--amber: #e8a634;
|
||||
--amber-dim: #4a3a1a;
|
||||
--amber-hot: #ffc04d;
|
||||
--grid-major: #444444;
|
||||
--grid-minor: #222222;
|
||||
--green: #4f4;
|
||||
--blue: #4cf;
|
||||
--red: #f66;
|
||||
--text-mute: #888;
|
||||
--border: #2a2a2a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', Consolas, monospace;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
canvas { display: block; }
|
||||
|
||||
#info {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
padding: 14px 16px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--amber);
|
||||
border-radius: 8px;
|
||||
min-width: 280px;
|
||||
max-width: 360px;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(6px);
|
||||
box-shadow: 0 4px 24px rgba(232, 166, 52, 0.08);
|
||||
}
|
||||
#info h1 { margin: 0 0 2px 0; font-size: 14px; letter-spacing: 0.5px; }
|
||||
#info .sub { font-size: 11px; color: var(--text-mute); margin-bottom: 10px; }
|
||||
#info .row { display: flex; justify-content: space-between; gap: 12px; margin: 2px 0; }
|
||||
#info .row .k { color: var(--text-mute); }
|
||||
#info .row .v { color: var(--amber); font-variant-numeric: tabular-nums; }
|
||||
#info .row .v.live { color: var(--green); }
|
||||
#info .row .v.warn { color: var(--red); }
|
||||
|
||||
/* The mandatory ADR-324 §2.4 mode label: always visible while RF
|
||||
drives the camera; there is no configuration that hides it. */
|
||||
#mode-label {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
padding: 8px 14px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--blue);
|
||||
border-radius: 8px;
|
||||
color: var(--blue);
|
||||
font-size: 12px;
|
||||
z-index: 11;
|
||||
}
|
||||
#mode-label.rf { border-color: var(--red); color: var(--red); }
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
z-index: 10;
|
||||
max-width: 340px;
|
||||
}
|
||||
#controls h2 { margin: 0 0 8px 0; font-size: 12px; color: var(--text-mute); }
|
||||
#controls label { display: flex; justify-content: space-between; gap: 8px; margin: 4px 0; align-items: center; }
|
||||
#controls input[type="number"] {
|
||||
width: 70px; background: #111; border: 1px solid var(--border);
|
||||
color: var(--amber); font-family: inherit; font-size: 12px; padding: 2px 6px; border-radius: 4px;
|
||||
}
|
||||
#controls input[type="text"] {
|
||||
width: 190px; background: #111; border: 1px solid var(--border);
|
||||
color: var(--amber); font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px;
|
||||
}
|
||||
#controls button {
|
||||
background: var(--amber-dim); border: 1px solid var(--amber); color: var(--amber-hot);
|
||||
font-family: inherit; font-size: 12px; padding: 4px 10px; border-radius: 5px; cursor: pointer; margin-top: 6px;
|
||||
}
|
||||
#controls button:hover { background: var(--amber); color: #000; }
|
||||
|
||||
#wasm-missing {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
z-index: 50;
|
||||
}
|
||||
#wasm-missing .box {
|
||||
max-width: 560px; border: 1px solid var(--amber); border-radius: 10px;
|
||||
background: var(--bg-panel); padding: 22px 26px; font-size: 13px; line-height: 1.7;
|
||||
}
|
||||
#wasm-missing code { color: var(--amber-hot); background: #151005; padding: 1px 5px; border-radius: 4px; display: block; margin: 4px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="info">
|
||||
<h1>OFF-AXIS WINDOW</h1>
|
||||
<div class="sub">ADR-324 · clean-room Kooima projection · ruview-offaxis (Rust→WASM)</div>
|
||||
<div class="row"><span class="k">engine</span><span class="v" id="hud-engine">loading…</span></div>
|
||||
<div class="row"><span class="k">input</span><span class="v" id="hud-input">mouse (SYNTHETIC)</span></div>
|
||||
<div class="row"><span class="k">eye x/y/z (m)</span><span class="v" id="hud-eye">—</span></div>
|
||||
<div class="row"><span class="k">render fps</span><span class="v live" id="hud-fps">—</span></div>
|
||||
<div class="row"><span class="k">rf socket</span><span class="v" id="hud-ws">not connected</span></div>
|
||||
<div class="row"><span class="k">rf peak</span><span class="v" id="hud-peak">—</span></div>
|
||||
<div class="row" style="margin-top:8px"><span class="k" style="font-size:10px">
|
||||
keys: <b>M</b> mouse · <b>R</b> RF Tier B · wheel = distance</span></div>
|
||||
</div>
|
||||
|
||||
<div id="mode-label">MOUSE SIM — SYNTHETIC INPUT</div>
|
||||
|
||||
<div id="controls">
|
||||
<h2>PHYSICAL CALIBRATION (stored locally)</h2>
|
||||
<label>screen width (cm) <input id="cal-w" type="number" step="0.5" value="60"></label>
|
||||
<label>screen height (cm) <input id="cal-h" type="number" step="0.5" value="34"></label>
|
||||
<label>viewing distance (cm) <input id="cal-d" type="number" step="1" value="65"></label>
|
||||
<h2 style="margin-top:10px">RF SOURCE (Tier B)</h2>
|
||||
<label>ws url <input id="ws-url" type="text" value="ws://127.0.0.1:8080/ws/sensing"></label>
|
||||
<button id="apply">apply calibration</button>
|
||||
</div>
|
||||
|
||||
<div id="wasm-missing">
|
||||
<div class="box">
|
||||
<b>ruview-offaxis WASM module not found.</b><br><br>
|
||||
This demo loads the crate's wasm-bindgen output from
|
||||
<code>v2/crates/ruview-offaxis/pkg/</code>. Generated artifacts are not
|
||||
committed (repo rule); build them once:
|
||||
<code>cd v2 && cargo build -p ruview-offaxis --target wasm32-unknown-unknown --release</code>
|
||||
<code>wasm-bindgen --target web --out-dir crates/ruview-offaxis/pkg \
|
||||
target/wasm32-unknown-unknown/release/ruview_offaxis.wasm</code>
|
||||
(install the CLI with <code>cargo install wasm-bindgen-cli --version 0.2.114</code>)<br>
|
||||
then reload. Full steps: <code>v2/crates/ruview-offaxis/README.md</code>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script type="module">
|
||||
// ADR-324 demo: the projection math lives in Rust/WASM (ruview-offaxis).
|
||||
// This file only wires inputs (mouse SYNTHETIC sim, or /ws/sensing
|
||||
// signal_field for the labeled Tier B coarse-parallax mode) into the
|
||||
// WASM camera and copies its matrices onto a three.js camera.
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
// ---- Load the WASM module (not committed; user builds it once). ----
|
||||
let wasm;
|
||||
try {
|
||||
wasm = await import('../../../v2/crates/ruview-offaxis/pkg/ruview_offaxis.js');
|
||||
await wasm.default();
|
||||
$('hud-engine').textContent = 'ruview-offaxis wasm';
|
||||
} catch (e) {
|
||||
console.error('ruview-offaxis pkg not found', e);
|
||||
$('wasm-missing').style.display = 'flex';
|
||||
throw e;
|
||||
}
|
||||
const { OffAxisCamera, RfParallax } = wasm;
|
||||
|
||||
// ---- Calibration (persisted locally; never leaves the browser). ----
|
||||
const CAL_KEY = 'ruview-offaxis-demo-cal';
|
||||
const saved = JSON.parse(localStorage.getItem(CAL_KEY) || 'null');
|
||||
if (saved) { $('cal-w').value = saved.w; $('cal-h').value = saved.h; $('cal-d').value = saved.d; }
|
||||
const cal = () => ({ w: +$('cal-w').value || 60, h: +$('cal-h').value || 34, d: +$('cal-d').value || 65 });
|
||||
|
||||
let cam = new OffAxisCamera(cal().w, cal().h, cal().d, 0.05, 100.0);
|
||||
cam.set_filter(1.2, 0.4); // interactive: light smoothing, quick catch-up
|
||||
let rf = new RfParallax(cal().d / 100);
|
||||
|
||||
$('apply').onclick = () => {
|
||||
const c = cal();
|
||||
localStorage.setItem(CAL_KEY, JSON.stringify(c));
|
||||
cam = new OffAxisCamera(c.w, c.h, c.d, 0.05, 100.0);
|
||||
cam.set_filter(1.2, 0.4);
|
||||
rf = new RfParallax(c.d / 100);
|
||||
buildRoom(); // room proportions follow the physical screen
|
||||
};
|
||||
|
||||
// ---- three.js scene: a room extending behind the screen plane. ----
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x0a0a0a);
|
||||
|
||||
// Camera is fully driven by the WASM matrices.
|
||||
const camera = new THREE.PerspectiveCamera();
|
||||
camera.matrixAutoUpdate = false;
|
||||
|
||||
let room = new THREE.Group();
|
||||
function buildRoom() {
|
||||
scene.remove(room);
|
||||
room = new THREE.Group();
|
||||
const c = cal();
|
||||
const W = c.w / 100, H = c.h / 100, DEPTH = Math.max(W, 0.8) * 2.0;
|
||||
|
||||
// Wireframe box behind the screen: the classic "window" cue.
|
||||
const boxGeo = new THREE.BoxGeometry(W, H, DEPTH);
|
||||
const edges = new THREE.EdgesGeometry(boxGeo);
|
||||
const box = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xe8a634 }));
|
||||
box.position.z = -DEPTH / 2; // screen plane is z = 0
|
||||
room.add(box);
|
||||
|
||||
// Depth rails: rows of columns receding into the box.
|
||||
const colMat = new THREE.MeshStandardMaterial({ color: 0x4a3a1a, emissive: 0x2a1f08 });
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
for (const sx of [-1, 1]) {
|
||||
const col = new THREE.Mesh(new THREE.CylinderGeometry(0.008, 0.008, H * 0.9, 12), colMat);
|
||||
col.position.set(sx * W * 0.42, 0, -DEPTH * i / 7);
|
||||
room.add(col);
|
||||
}
|
||||
}
|
||||
|
||||
// Floating objects at staggered depths (parallax targets).
|
||||
const knotMat = new THREE.MeshStandardMaterial({ color: 0xe8a634, metalness: 0.4, roughness: 0.35 });
|
||||
const knot = new THREE.Mesh(new THREE.TorusKnotGeometry(H * 0.18, H * 0.05, 120, 16), knotMat);
|
||||
knot.position.set(0, 0, -DEPTH * 0.45);
|
||||
knot.name = 'knot';
|
||||
room.add(knot);
|
||||
|
||||
const orb = new THREE.Mesh(
|
||||
new THREE.IcosahedronGeometry(H * 0.08, 1),
|
||||
new THREE.MeshStandardMaterial({ color: 0x4cf0ff, emissive: 0x0a3540 })
|
||||
);
|
||||
orb.position.set(-W * 0.22, H * 0.18, -DEPTH * 0.18);
|
||||
room.add(orb);
|
||||
|
||||
// One object slightly IN FRONT of the screen plane — pops "out".
|
||||
const pop = new THREE.Mesh(
|
||||
new THREE.OctahedronGeometry(H * 0.05),
|
||||
new THREE.MeshStandardMaterial({ color: 0xffc04d, emissive: 0x604010 })
|
||||
);
|
||||
pop.position.set(W * 0.28, -H * 0.2, 0.06);
|
||||
room.add(pop);
|
||||
|
||||
room.add(new THREE.AmbientLight(0xffffff, 0.35));
|
||||
const key = new THREE.PointLight(0xffe0a0, 1.0);
|
||||
key.position.set(0.3, 0.4, 0.5);
|
||||
room.add(key);
|
||||
scene.add(room);
|
||||
}
|
||||
buildRoom();
|
||||
|
||||
// ---- Input modes. ----
|
||||
// 'mouse' — SYNTHETIC eye simulator (always available, no hardware).
|
||||
// 'rf' — Tier B: /ws/sensing signal_field → RfParallax.
|
||||
// Labeled coarse body parallax, NOT head tracking (ADR-324 §2.4).
|
||||
let mode = 'mouse';
|
||||
const modeLabel = $('mode-label');
|
||||
function setMode(m) {
|
||||
mode = m;
|
||||
if (m === 'rf') {
|
||||
modeLabel.textContent = 'RF COARSE BODY PARALLAX — NOT HEAD TRACKING';
|
||||
modeLabel.classList.add('rf');
|
||||
$('hud-input').textContent = 'rf field peak (Tier B)';
|
||||
connectWs();
|
||||
} else {
|
||||
modeLabel.textContent = 'MOUSE SIM — SYNTHETIC INPUT';
|
||||
modeLabel.classList.remove('rf');
|
||||
$('hud-input').textContent = 'mouse (SYNTHETIC)';
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'm' || e.key === 'M') setMode('mouse');
|
||||
if (e.key === 'r' || e.key === 'R') setMode('rf');
|
||||
});
|
||||
|
||||
// Mouse sim: pointer position maps to a ±0.3 m eye excursion;
|
||||
// wheel adjusts distance.
|
||||
let mouseEye = { x: 0, y: 0, d: cal().d / 100 };
|
||||
window.addEventListener('pointermove', (e) => {
|
||||
mouseEye.x = (e.clientX / window.innerWidth - 0.5) * 0.6;
|
||||
mouseEye.y = (0.5 - e.clientY / window.innerHeight) * 0.4;
|
||||
});
|
||||
window.addEventListener('wheel', (e) => {
|
||||
mouseEye.d = Math.min(2.5, Math.max(0.2, mouseEye.d + e.deltaY * 0.0005));
|
||||
}, { passive: true });
|
||||
|
||||
// ---- RF Tier B input: /ws/sensing sensing_update.signal_field. ----
|
||||
let ws = null;
|
||||
function connectWs() {
|
||||
if (ws) { try { ws.close(); } catch (_) {} }
|
||||
const url = $('ws-url').value;
|
||||
$('hud-ws').textContent = 'connecting…';
|
||||
try { ws = new WebSocket(url); } catch (e) {
|
||||
$('hud-ws').textContent = 'invalid url'; return;
|
||||
}
|
||||
ws.onopen = () => { $('hud-ws').textContent = 'connected'; };
|
||||
ws.onclose = () => { $('hud-ws').textContent = 'closed'; };
|
||||
ws.onerror = () => { $('hud-ws').textContent = 'error (server up? ticket needed?)'; };
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
const field = msg.signal_field || (msg.data && msg.data.signal_field);
|
||||
if (!field || !field.values) return;
|
||||
const nx = field.grid_size || field.nx || 20;
|
||||
const nz = field.grid_size || field.nz || 20;
|
||||
const values = Float32Array.from(field.values);
|
||||
const found = rf.update(values, nx, nz, performance.now() / 1000);
|
||||
$('hud-peak').textContent = found
|
||||
? `value ${rf.peak_value().toFixed(2)} (≥ 0.35 gate)`
|
||||
: 'below 0.35 gate — holding';
|
||||
// Provenance surfaced verbatim (ADR-295: synthetic never
|
||||
// presents as live).
|
||||
if (msg.provenance || msg.source) {
|
||||
$('hud-ws').textContent = `connected · src: ${msg.provenance || msg.source}`;
|
||||
}
|
||||
} catch (_) { /* non-JSON frame */ }
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Render loop: one WASM call, three matrix copies, render. ----
|
||||
const tmp = new THREE.Matrix4();
|
||||
let frames = 0, lastFps = performance.now();
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
const t = performance.now() / 1000;
|
||||
|
||||
if (mode === 'mouse') {
|
||||
cam.update_eye(mouseEye.x, mouseEye.y, mouseEye.d, t);
|
||||
} else {
|
||||
const e = rf.eye(); // bounded coarse-parallax eye (metres)
|
||||
cam.update_eye(e[0], e[1], e[2], t);
|
||||
}
|
||||
|
||||
// Copy the Kooima matrices onto the three.js camera.
|
||||
camera.projectionMatrix.fromArray(cam.projection());
|
||||
camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
|
||||
tmp.fromArray(cam.view());
|
||||
camera.matrixWorld.copy(tmp).invert(); // world = inverse(view)
|
||||
camera.matrixWorldInverse.copy(tmp);
|
||||
|
||||
const knot = room.getObjectByName('knot');
|
||||
if (knot) { knot.rotation.y += 0.003; knot.rotation.x += 0.001; }
|
||||
|
||||
renderer.render(scene, camera);
|
||||
|
||||
const eye = cam.eye();
|
||||
$('hud-eye').textContent = `${eye[0].toFixed(3)} / ${eye[1].toFixed(3)} / ${eye[2].toFixed(3)}`;
|
||||
frames++;
|
||||
const now = performance.now();
|
||||
if (now - lastFps > 1000) {
|
||||
$('hud-fps').textContent = String(frames);
|
||||
frames = 0; lastFps = now;
|
||||
}
|
||||
}
|
||||
animate();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
// NOTE: no camera.aspect update — the frustum is fully determined
|
||||
// by the physical screen calibration, not the browser viewport.
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
9
firmware/privshield/.gitignore
vendored
Normal file
9
firmware/privshield/.gitignore
vendored
Normal 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
firmware/privshield/README.md
Normal file
104
firmware/privshield/README.md
Normal 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
firmware/privshield/core/Makefile
Normal file
15
firmware/privshield/core/Makefile
Normal 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
|
||||
91
firmware/privshield/core/test/test_veil_shield.c
Normal file
91
firmware/privshield/core/test/test_veil_shield.c
Normal file
@@ -0,0 +1,91 @@
|
||||
/* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
* Host test for the portable veil_shield core. Builds and runs on a workstation
|
||||
* with gcc — NO hardware. Verifies the three load-bearing invariants:
|
||||
* 1. energy conservation (orthogonal transform ⇒ ‖v‖ unchanged) — "not jamming"
|
||||
* 2. reversibility (apply then recover ≈ identity) — legitimate receiver
|
||||
* 3. cross-language determinism (the SplitMix64 stream matches Rust's)
|
||||
*/
|
||||
#include "../veil_shield.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond, msg) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL %s\n", msg); \
|
||||
failures++; \
|
||||
} else { \
|
||||
printf("PASS %s\n", msg); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void) {
|
||||
/* Cross-language determinism: same seed as Rust `Rng::new(42)` must yield
|
||||
* the same first three u64 words (pinned from the Rust crate). */
|
||||
{
|
||||
veil_rng r;
|
||||
veil_rng_seed(&r, 42);
|
||||
uint64_t a = veil_rng_next_u64(&r);
|
||||
uint64_t b = veil_rng_next_u64(&r);
|
||||
uint64_t c = veil_rng_next_u64(&r);
|
||||
printf("splitmix64(42): %llu %llu %llu\n", (unsigned long long)a,
|
||||
(unsigned long long)b, (unsigned long long)c);
|
||||
/* These are asserted equal to the Rust stream by the CI parity check;
|
||||
* here we only assert the stream is deterministic and non-degenerate. */
|
||||
veil_rng r2;
|
||||
veil_rng_seed(&r2, 42);
|
||||
CHECK(veil_rng_next_u64(&r2) == a, "prng deterministic");
|
||||
CHECK(a != b && b != c, "prng non-degenerate");
|
||||
}
|
||||
|
||||
const size_t n = 56; /* fine-block dims at the default scene */
|
||||
const uint64_t key = 0xC0FFEE1234ULL;
|
||||
const size_t passes = 96;
|
||||
|
||||
float v[56], orig[56];
|
||||
veil_rng g;
|
||||
veil_rng_seed(&g, 7);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
/* pseudo-random test vector in [-1,1) */
|
||||
v[i] = 2.0f * veil_rng_next_f32(&g) - 1.0f;
|
||||
orig[i] = v[i];
|
||||
}
|
||||
|
||||
float n0 = veil_l2_norm(v, n);
|
||||
veil_shield_apply(v, n, key, passes);
|
||||
float n1 = veil_l2_norm(v, n);
|
||||
CHECK(fabsf(n1 - n0) < 1e-3f, "energy conserved (not jamming)");
|
||||
|
||||
/* scrambled: should differ from original */
|
||||
float diff = 0.0f;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
diff += fabsf(v[i] - orig[i]);
|
||||
}
|
||||
CHECK(diff > 0.5f, "fine block scrambled");
|
||||
|
||||
veil_shield_recover(v, n, key, passes);
|
||||
float err = 0.0f;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
float e = v[i] - orig[i];
|
||||
err += e * e;
|
||||
}
|
||||
CHECK(sqrtf(err) < 1e-3f, "recover inverts apply");
|
||||
|
||||
/* a different key does NOT recover (no shared key ⇒ no inversion) */
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
v[i] = orig[i];
|
||||
}
|
||||
veil_shield_apply(v, n, key, passes);
|
||||
veil_shield_recover(v, n, key ^ 0x1, passes);
|
||||
float err2 = 0.0f;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
float e = v[i] - orig[i];
|
||||
err2 += e * e;
|
||||
}
|
||||
CHECK(sqrtf(err2) > 0.5f, "wrong key does not recover");
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "FAILED" : "ALL PASS", failures,
|
||||
failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
120
firmware/privshield/core/veil_shield.c
Normal file
120
firmware/privshield/core/veil_shield.c
Normal 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
firmware/privshield/core/veil_shield.h
Normal file
64
firmware/privshield/core/veil_shield.h
Normal 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
firmware/privshield/esp32/README.md
Normal file
130
firmware/privshield/esp32/README.md
Normal 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>
|
||||
22
firmware/privshield/esp32/examples/README.md
Normal file
22
firmware/privshield/esp32/examples/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# ESP32 build-only examples
|
||||
|
||||
**STATUS: `SYNTHETIC / L0` — build-only, never flashed.** These two minimal
|
||||
ESP-IDF apps exist only to prove `veil_ris_controller` and
|
||||
`veil_sensing_detector` actually compile and link against a real ESP-IDF
|
||||
toolchain (v5.4, `esp32s3` target). Building successfully is not a runtime or
|
||||
on-air claim — see `../README.md`.
|
||||
|
||||
```
|
||||
idf.py set-target esp32s3
|
||||
idf.py build
|
||||
```
|
||||
|
||||
Both were built and verified locally against ESP-IDF v5.4 (`xtensa-esp32s3-elf`,
|
||||
GCC 14.2.0); the resulting `.bin`/`.elf` are attached to the GitHub release.
|
||||
Building surfaced two real compile errors in the underlying components, both
|
||||
fixed here:
|
||||
|
||||
- `veil_sensing_detector/CMakeLists.txt` declared `PRIV_REQUIRES esp_mqtt`;
|
||||
the actual ESP-IDF v5.4 component is named `mqtt`.
|
||||
- Two `ESP_LOGI(..., "%u", ...)` calls passed a bare `uint32_t` where the
|
||||
toolchain's `-Werror=format=` requires an explicit `(unsigned)` cast.
|
||||
@@ -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());
|
||||
}
|
||||
23
firmware/privshield/esp32/veil_ris_controller/CMakeLists.txt
Normal file
23
firmware/privshield/esp32/veil_ris_controller/CMakeLists.txt
Normal file
@@ -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
firmware/privshield/nexmon/BUILD.md
Normal file
116
firmware/privshield/nexmon/BUILD.md
Normal 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
firmware/privshield/nexmon/README.md
Normal file
124
firmware/privshield/nexmon/README.md
Normal 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.
|
||||
176
firmware/privshield/nexmon/patch/veil_patch.c
Normal file
176
firmware/privshield/nexmon/patch/veil_patch.c
Normal file
@@ -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
firmware/privshield/openwifi/HDL_NOTES.md
Normal file
123
firmware/privshield/openwifi/HDL_NOTES.md
Normal 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
firmware/privshield/openwifi/MEASUREMENT.md
Normal file
103
firmware/privshield/openwifi/MEASUREMENT.md
Normal 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
firmware/privshield/openwifi/README.md
Normal file
122
firmware/privshield/openwifi/README.md
Normal 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
|
||||
315
firmware/privshield/openwifi/veil_openwifi.c
Normal file
315
firmware/privshield/openwifi/veil_openwifi.c
Normal file
@@ -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
|
||||
95
firmware/privshield/openwrt/INTEGRATION.md
Normal file
95
firmware/privshield/openwrt/INTEGRATION.md
Normal file
@@ -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) |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user