mirror of
https://github.com/ruvnet/RuView
synced 2026-08-07 20:01:43 +00:00
2bfa60a4624c9be9ee3ece009fb73da4d40dbe19
22 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9fb5af7cf2 |
feat(sensing-server): add secure opt-in OTLP log export
Import and harden the OpenTelemetry logging work from #1382. Preserve default stderr behavior, register and validate RuView semantic conventions, attach a published schema, enable TLS roots, pin demo images, and fix first-CSI node lifecycle reporting. Supersedes #1382 Closes #1460 Co-authored-by: Jens Holdgaard Pedersen <jens@holdgaard.org> |
||
|
|
2cc378c12f |
chore: version-bump and republish 10 of 12 documented crates to crates.io (#1439)
Published: wifi-densepose-core 0.3.2, -vitals 0.3.2, -wifiscan 0.3.2, -hardware 0.3.2, -signal 0.3.6, -nn 0.3.2, -ruvector 0.3.3, -train 0.3.3, -mat 0.3.2, -wasm 0.3.1. Not published (blocked, needs a decision): wifi-densepose-sensing-server and wifi-densepose-cli both path-depend on ruview-auth, which is publish = false. Version bumps reserved (0.3.5, 0.3.2) but not published. |
||
|
|
89babb00a9 |
Merge remote-tracking branch 'origin/main' into feat/ruview-auth-cognitum-oauth-verifier
# Conflicts: # v2/crates/wifi-densepose-sensing-server/src/main.rs |
||
|
|
347698b67c |
feat(auth): browser sign-in — /oauth/start, /oauth/callback, session cookie
Closes the gap adversarial review found: `wifi-densepose login` writes ~/.ruview/credentials.json, which a BROWSER CANNOT READ. The UI therefore had no way to obtain a Cognitum token at all, and the WebSocket ticket mechanism ADR-272 built "for browsers" was only exercisable with the legacy static shared secret OAuth was meant to replace. The ADRs described a browser story that did not exist. Ported from cognitum-one/freetokens (src/auth/oauth.ts, live at freetokens.cognitum.one), whose shape is not the obvious one and is the whole point: THE BROWSER NEVER HOLDS AN OAUTH TOKEN. The server generates the PKCE verifier and state, keeps them in an HMAC-signed cookie, performs the code exchange itself, verifies the token, and issues its OWN session cookie carrying an assertion — subject, account, scope, expiry — not a credential. So the access token cannot be read by an XSS, cannot sit in localStorage, and cannot leak through a URL. A stolen session cookie is useless against Cognitum or any sibling service. GET /oauth/start -> 302 to auth.cognitum.one + signed transaction cookie GET /oauth/callback -> constant-time state check, exchange, verify, session GET /oauth/logout -> clears the local session (not the Cognitum session) Verified against the running binary: /oauth/start returns the same 302 + HttpOnly/SameSite=Lax/Max-Age=600 shape freetokens does live; a forged `state` is refused 400; a forged session cookie is refused 401. DELIBERATE DEVIATION from freetokens: no `__Host-` cookie prefix. That prefix REQUIRES `Secure`, and RuView is routinely reached at http://localhost or over plain HTTP on a LAN, where such a cookie is never sent and sign-in would fail silently. `Secure` is set only when the request actually arrived over TLS (direct or via x-forwarded-proto). Every other attribute matches; the HMAC is what protects the value. Other decisions worth stating: - The callback verifies through the SAME `verify_access_token` every other request uses — signature, audience (client_id), typ, expiry, scope. A sign-in path must not be a softer path. - The session cookie is checked LAST in the middleware, after bearer and ticket: it is the weakest-bound credential, so a presented bearer should win. - A browser session requests `sensing:read` only. Admin work goes through the CLI's explicit `--admin`. - The token exchange runs in `spawn_blocking` — `ureq` is blocking, and parking an async worker is the mistake this codebase just had to fix in jwks.rs. - `/oauth/*` sits outside `/api/v1/*` on purpose: gating the routes you use to obtain a credential would deadlock. PKCE moved out from behind the `login` feature into its own light `pkce` feature (rand + sha2 + base64, no HTTP stack), so the server can build an authorize URL without pulling in the client-side login machinery. `login` now implies `pkce`. Tests: 13 new browser_session unit tests — signature round-trip, tampered payload, wrong secret, malformed cookie values, HttpOnly/SameSite/Secure attributes, exact scope matching with no implied escalation, a cookie name that merely ends with the target not matching, multi-scope URL encoding, and the core property that a session cookie never contains the access token. Totals: 547 sensing-server lib + 5 wiring integration, 87 ruview-auth. Co-Authored-By: Ruflo & AQE |
||
|
|
7d6d66941a |
feat(ws): single-use WebSocket ticket store (ADR-272 groundwork)
Audit finding this exists to close — verified empirically, not inferred. With RUVIEW_API_TOKEN set (operator believes auth is ON), a real WebSocket handshake carrying NO credential: /ws/sensing -> 101 Switching Protocols /ws/introspection -> 101 Switching Protocols /api/v1/stream/pose -> 101 Switching Protocols /api/v1/models -> 401 (control: REST is correctly gated) The REST control plane is locked while the DATA plane — live presence, pose and vitals — is open to anyone who can reach the port. Two of those paths sit outside PROTECTED_PREFIX entirely; the third is the documented EXEMPT_PATHS entry. The exemption was reasonable when added (a browser cannot set Authorization on an upgrade) but its blast radius is larger than it looks, and phase 3 sharpened the contrast by making REST genuinely strong. (To be precise about what was proven: the handshake is accepted. A payload frame was not captured in that window, so this is "the connection is established without a credential", not "data was read".) This commit adds only the store; wiring it into the middleware is a breaking change for browser clients and lands with the UI update. Design notes: - Single use. `consume` REMOVES the entry, so a replay of the same URL fails even inside the TTL. This is what makes a credential-in-a-query tolerable: by the time it reaches an access log or a Referer header, it is spent. - 30-second TTL. Long enough for a page to open a socket; too short to harvest. - It is not the credential. It authorizes one WebSocket. It cannot be replayed against /api/v1/*, cannot be refreshed, and carries no reusable identity. - The grant captures the ISSUING principal's scopes, so a WebSocket inherits exactly the authority of the credential that asked for it — a sensing:read session cannot mint a ticket that outranks itself. - Capped at 512 outstanding, self-healing as tickets expire, so an authenticated but misbehaving caller cannot grow the map without bound. - In-memory because that is correct, not merely convenient: a ticket surviving a restart would outlive the server that vouched for it. Native clients (Python, Rust CLI, TS MCP) are NOT browsers and will send a normal Authorization header on the upgrade instead — tickets would add a round-trip and a second credential path for no benefit. 12 tests: single-use enforced, replay refused, expiry refused AND pruned, unknown ticket refused, 256-bit unpredictability, grant carries issuer scopes, cap enforced and self-healing, and query parsing including `?myticket=x` not being read as `?ticket=x`. Co-Authored-By: Ruflo & AQE |
||
|
|
c2bd33e649 |
feat(sensing-server): accept Cognitum OAuth on /api/v1/*, scope-gated (ADR-271 phase 3)
Wires `ruview-auth` into `bearer_auth.rs`. `RUVIEW_OAUTH_ISSUER` enables it;
unset, nothing changes.
Layering, in order:
1. `RUVIEW_API_TOKEN` set and the bearer matches exactly -> allow. Byte-for-
byte today's behaviour.
2. Otherwise, if OAuth is configured, verify the bearer as a Cognitum access
token and require the scope the route needs.
3. Otherwise 401.
The static compare goes first for compatibility, not security: a matching
static token is not a JWT and a JWT never matches the static token. It means an
existing deployment behaves identically even with OAuth switched on.
Scope gate (`required_scope_for`), split by blast radius per ADR-060 —
"can this destroy something", not how many routes it covers:
sensing:admin /api/v1/train/* (hours of Pi CPU, writes models)
DELETE /api/v1/models/{id} (irreversible)
DELETE /api/v1/recording/{id} (irreversible)
sensing:read everything else
Deliberately NOT admin: model load/unload and recording start. They mutate
server state but destroy nothing, and gating them would push routine dashboard
use into requesting delete capability — the opposite of least privilege.
The legacy static token stays un-scope-gated. It predates scopes and carries no
claims, so narrowing it would be a silent breaking change to deployments using
it; migrating to OAuth is how an operator opts into the finer split.
FAIL CLOSED at boot. If OAuth is requested but cannot work — empty issuer, or a
JWKS we cannot fetch — the server logs why and exits rather than serving.
Starting anyway would silently downgrade an operator who asked for OAuth to
either an open API or a shared-secret one, with no signal it happened. The JWKS
is warmed eagerly for the same reason: a bad `jwks_uri` should die at boot with
a legible message, not surface as a puzzling 401 an hour later.
The verified `Principal` is attached to request extensions, so handlers and
audit logs can attribute a request (`sub`, `account_id`, `org_id`,
`workspace_id`, `jti`) instead of knowing only "someone had the secret". That
is the point of moving off a shared bearer.
Verification failures are logged with the reason and returned as a flat 401 —
the reason is useful to an operator and equally useful to an attacker probing
for which claim to forge next.
Also aligns `ruview_auth::extract_bearer` to match the scheme
case-insensitively (RFC 7235 §2.1). The sensing server has always done this
deliberately, with a comment saying why; the two layers disagreeing about what
a valid header looks like would be a latent bug.
Tests: 16 new in `bearer_auth::oauth_tests`, driving a real Router end to end
(request -> middleware -> verifier -> handler) with ES256 tokens signed by a
runtime-generated key. Covers the scope policy as a pure function, read-scoped
tokens refused on delete and train, admin-scoped tokens allowed, an
`inference`-only token from another Cognitum product refused on every route,
garbage and absent bearers, both legacy-token layering directions, the
principal reaching a handler, and the unset case remaining a no-op.
`cargo test -p wifi-densepose-sensing-server --lib --no-default-features`:
501 passed, 0 failed. `ruview-auth`: 43 passed across both feature configs.
Co-Authored-By: Ruflo & AQE
|
||
|
|
a47bb71b22 |
fix(adr-185): hoist AETHER compute surface into a std-only leaf crate
ADR-185 §13 flagged the `[aether]` wheel as linking the whole
`wifi-densepose-sensing-server` Axum/tokio/worldgraph/ruvector tree via the
non-optional deps of that crate. Hoist the pure-compute AETHER stack out so the
binding depends only on pure Rust.
New crate `wifi-densepose-aether` (std-only, ZERO external deps) holds the four
self-contained modules the AETHER surface needs — `embedding`, `graph_transformer`,
`sona`, `sparse_inference` (verified: no serde/tokio/axum/crate:: refs outside the
set; the four form a closed dependency graph). `wifi-densepose-sensing-server`
now `pub use`s them from the leaf crate, so its own code (`crate::embedding`, …)
and public API (`wifi_densepose_sensing_server::embedding`, …) are unchanged —
`main.rs`/`trainer.rs` untouched. The Python binding + parity test + `[aether]`
feature repoint at the leaf crate; the sensing-server dep is dropped from python.
MEASURED wheel size (maturin build --release --features aether --strip; each
wheel install-verified to actually contain `AetherConfig`):
BEFORE (aether -> sensing-server): 369,782 B (~361 KB)
AFTER (aether -> leaf crate): 319,719 B (~312 KB)
HONEST CORRECTION to the §9/§13 premise: the pre-hoist wheel was NOT over the
ADR-117 §5.4 5 MB budget — it was ~361 KB, ~14x under. Linker dead-code
elimination (--gc-sections on the pyo3 cdylib) already strips the unreached
server tree because the binding only reaches pure-compute symbols. So this is
"blows the budget" CLAIMED, not MEASURED. The hoist is still worthwhile and
lands the real, measured wins:
- `[aether]` build time 71s -> 12s (no server tree to compile),
- python/Cargo.lock -1238 lines (server tree no longer resolved),
- ~50 KB smaller wheel,
- honest, minimal declared dependency surface + removes the latent risk that a
future change makes some server code reachable and *then* bloats the wheel.
Verified this session (real counts):
- cargo test --features aether --test aether_parity: 2 passed / 0 failed
- pytest tests/test_aether.py (maturin develop --features aether): 9 passed
- cargo test -p wifi-densepose-sensing-server --no-default-features: 217 bin +
388 lib, 0 failed (no regression)
- cargo test -p wifi-densepose-aether: 96 passed (the relocated unit tests)
The §9/§13 wheel-size narrative in ADR-185 (owned by the ADR author) should be
corrected to reflect the measured reality — flagged to the team lead.
|
||
|
|
65648fe4c2 |
feat(adr-186): P5/P6 + acceptance verification — dashboard honesty, HTTP tests, Accepted
Completes ADR-186 phases P5 (dashboard honesty / fallback guarantee) and P6
(tests + witness) on top of the P1–P4 reconnection, and flips the ADR to Accepted.
P5 — dashboard honesty:
- Backend (training_api): a runtime enablement gate (`RUVIEW_DISABLE_SERVER_TRAINING`)
makes the three start endpoints return a structured `{enabled:false, reason,
cli:"wifi-densepose train-room"}` HTTP 409 (never a silent success) when server
training is disabled; `/api/v1/train/status` now carries an `enabled` flag. A runtime
flag (not a Cargo feature) so `--no-default-features` builds keep training ON (resolves
§9.4). Handlers return `Response` to carry the 409.
- Frontend (TrainingPanel.js): reads `enabled` off the status payload and disables the
Start/Pretrain/LoRA buttons with a CLI tooltip when server training is off; a rejected
start tears down the optimistically-opened WS and refreshes. The enabled path was
already fully wired (opens the WS before POST, renders live epoch/loss/ETA + terminal
state) — verified.
P6 — tests & witness:
- New HTTP-level tests in a `#[cfg(test)] mod adr186_http_tests` built on a minimal
`AppStateInner::minimal()` test-state helper: a live-socket test that completes a
genuine 101 WebSocket handshake (tokio-tungstenite) and receives a real progress frame
after a POST start; a 426-not-404 route-wired check; a full POST→poll-status→`.rvf`-exists
round-trip; and the disabled-409 structured-response test. Added `tokio-tungstenite`
dev-dep (version already in the workspace lock).
- CHANGELOG updated; ADR §6 ledger + §7 acceptance criteria checked off with evidence;
Status → Accepted. README/CLAUDE have no training route table (no edit needed).
Fixed a test-only parallelism race found under `cargo test --workspace`: two
model-writing tests deleted `.rvf`s by directory-diff and could remove a file a third
test asserted existed. Each test now cleans only its own artifact (data/models is
gitignored).
Verified this session: `cargo test -p wifi-densepose-sensing-server -p wifi-densepose-train
--no-default-features` — sensing-server bin 217 passed / 0 failed, all train suites 0
failed. Full `--workspace` re-run in progress to reconfirm untouched crates.
|
||
|
|
8c1d3d772a |
chore: bump wifi-densepose-engine 0.3.1, wifi-densepose-sensing-server 0.3.4
engine 0.3.1: additive StreamingEngine::set_multistatic_config (#1312) sensing-server 0.3.4: bearer-auth pose-WS exemption + EngineBridge guard config threading (#1312, #1313); no public lib API change (engine_bridge is binary-internal) Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
aeac5f5543 |
chore(worldgraph): extract geo+worldgraph+worldmodel to ruvnet/worldgraph submodule
- published as github.com/ruvnet/worldgraph (3-crate workspace, history via git-filter-repo) - replace the 3 in-tree crates with one submodule at v2/crates/worldgraph - parent workspace: drop the 3 members, exclude the submodule (it is its own workspace), repoint workspace.dependencies(worldmodel) + engine/sensing-server path-deps into it - cargo metadata resolves clean (geo/worldgraph/worldmodel consumed from the submodule) Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
df617145d6 |
feat(ADR-262 P3): live /api/field + /ws/field — RuView sensing speaks RuField (fail-closed egress) (#1071)
* feat(ADR-262 P3): live RuField surface — RuView sensing speaks RuField on /api/field + /ws/field Wire the P1 `wifi-densepose-rufield` bridge into the live `wifi-densepose-sensing-server` so the governed sensing cycle emits real signed RuField `FieldEvent`s on two additive endpoints. - Cargo: add the `wifi-densepose-rufield` path dep (the single coupling point, ADR-262 §5.4 — no new RuView-internal coupling). - New `src/rufield_surface.rs` (kept out of the 8k-line main.rs): `FieldSurface` holds a dedicated ed25519 `Signer` + a bounded ring of recent events + the `/ws/field` broadcast topic; `GET /api/field` and `GET /ws/field` handlers; a standalone `router()` for isolated testing. - Signer (defers the P2 key decision, ADR-262 §8 Q1): a STANDALONE dev/sensing key from `WDP_RUFIELD_SIGNING_SEED`, else a deterministic dev default with a logged WARN. Reusing the `cog-ha-matter` Ed25519 key is the deferred P2 call — P3 does not pre-empt it. - Tap: at the ESP32 governed-trust cycle (`main.rs` ~5886 observe_cycle / ~5938 SensingUpdate build), `emit_rufield_event` joins the cycle's features/classification/signal_field with the engine's effective_class/demoted trust state into a `SensingSnapshot` and surfaces it via the bridge. Existing endpoints (`/ws/sensing` etc.) are unchanged — purely additive. - Privacy egress: `network_egress_allowed` is fail-closed for an unattended live surface — only P1/P2 leave the box; P0 raw and P3/P4/P5 (identity/biometric/aggregate) are held edge-local. A `Derived` cycle maps to P4/P5 and never surfaces. - No-phantom: `emit` drops no-presence cycles (no fabricated events). Gates (tests/rufield_surface_test.rs, tower::oneshot, 4/0): well-formed signed event (WifiCsi, P2 not P1, is_fusable, real timestamp); empty cycle → no phantom; Derived trust never surfaces; mixed stream surfaces only egress-safe events. Honesty (ADR-262 §0/§6): real plumbing on a live endpoint, NOT accuracy. Single-link CSI with its existing caveats (no validated room-coordinate accuracy); dedicated dev signing key pending the P2 ownership decision; no accuracy claim. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(ADR-262 P3): mark P1+P3 implemented; document /api/field + /ws/field; CHANGELOG - ADR-262 Status → "P1 + P3 implemented"; add a P3 implementation-status block (tap site, endpoints, dedicated dev signer deferring the §8 Q1 key decision, fail-closed egress, gates). Keep the honesty framing: real plumbing on a live endpoint, not accuracy. - CHANGELOG [Unreleased]: add the ADR-262 P3 entry. - user-guide: add `/api/field` to the REST table + a "RuField surface (ADR-262 P3)" section covering `/api/field` + `/ws/field`, the fail-closed P1/P2-only egress, the WDP_RUFIELD_SIGNING_SEED dev key, and the no-accuracy honesty note. Co-Authored-By: claude-flow <ruv@ruv.net> * ci: checkout submodules everywhere + Dockerfile copies vendor/rufield Making wifi-densepose-rufield (ADR-262 bridge) a v2 workspace member means EVERY cargo-on-workspace context must have the vendor/rufield submodule present (cargo loads all member manifests). P1 only fixed the rust-tests job; this adds `submodules: recursive` to all workflow checkouts that run cargo (mqtt-integration was failing on the missing submodule manifest), and makes Dockerfile.rust COPY vendor/rufield/ to /vendor/rufield (matches the bridge's ../../../vendor/rufield path-dep under the collapsed Docker layout). update-submodules.yml left alone (it manages submodules itself). Co-Authored-By: claude-flow <ruv@ruv.net> --------- Co-authored-by: ruv <ruvnet@gmail.com> |
||
|
|
53b327e649 |
release: bump signal 0.3.4 / sensing-server 0.3.3 / cli 0.3.1 (fixes #1009, #1004)
HE20 calibration baseline fix (signal), sensing-server --source auto simulate-latch fix (sensing-server), HE20 calibrate parser/asserts (cli). See PR #1038. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
2e4461d64d |
release: bump 9 crates changed in the beyond-SOTA sweep for crates.io
vitals/wifiscan/hardware/nn 0.3.0->0.3.1, ruvector 0.3.1->0.3.2, signal 0.3.2->0.3.3, train 0.3.1->0.3.2, mat 0.3.0->0.3.1, sensing-server 0.3.1->0.3.2. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
29de574e63 |
Beyond-SOTA engine/signal/train improvements: mesh partition guard, FFT CIR solver, canonical frame decoder, falsifiable occupancy benchmark, governed streaming, adapter provenance (#1018)
* docs(research): add RuView beyond-SOTA system review (00) First document of the beyond-SOTA research series: capability audit of the current RuView engine with role-to-crate maturity matrix, ruvsense module inventory, gap analysis, and risk register. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add beyond-SOTA architecture design (02, in progress) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): finalize beyond-SOTA architecture (02) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add benchmark/validation methodology snapshot (03) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add beyond-SOTA series index with validation results; changelog README index ties the 5 research docs together with the session's measured validation evidence: 2,797 workspace tests / 0 failed, Python proof PASS (bit-exact), and paired pre/post criterion CIR benchmarks. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * perf(signal): precompute CIR warm-start system; hoist tomography solver allocs Exact, determinism-safe optimizations (bit-identical float results): - cir.rs: diag(PhiH Phi)+lambda*I and its CSR matrix depend only on Phi and lambda (fixed at CirEstimator::new) but were rebuilt every frame (O(K*G) pass + CSR allocation). Now built once in new() via build_warm_start_system; summation order unchanged. - tomography.rs: ISTA gradient buffer hoisted out of the 100-iteration loop (fill(0.0) reset) and the Frobenius Lipschitz bound moved from per-reconstruct to construction. Verified: signal 456 tests green; engine 11/11 green including cycle_is_deterministic and witness-stability tests. Criterion paired pre/post: cir_estimate/he40 -3.9% (p<0.01), multiband -1.2/-1.4%. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix(worldgraph): bound SemanticState growth with deterministic retention StreamingEngine::process_cycle appended one SemanticState belief per cycle with no eviction — ~1.7M nodes/day at 20 Hz (beyond-SOTA roadmap finding #6). Add WorldGraph::prune_semantic_states(max): deterministic eviction of the oldest beliefs by (valid_from_unix_ms, id); structural nodes (rooms, zones, sensors, anchors, tracks, events) are never eligible. Wire it into the engine after each belief append (DEFAULT_SEMANTIC_RETENTION = 7,200, ~6 min at 20 Hz; set_semantic_retention to tune). The WorldGraph holds current beliefs; durable history is the recorder's job, so no audit data is lost. 3 new tests: end-to-end bounded growth, oldest-only eviction, deterministic equal-timestamp tie-break. Workspace gate: 2,865 passed, 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(sensing-server): route live frames through the governed StreamingEngine Closes the live-trust-path gap (ADR-136 section 8, beyond-SOTA system review): the running server fused live CSI with the bare MultistaticFuser, while the privacy/provenance/witness control plane (ADR-135..146) only ever ran on synthetic in-test frames. The privacy control plane was therefore bypassable on the real path. New engine_bridge module drives StreamingEngine::process_cycle from the server's live NodeState map, reusing the existing NodeState -> MultiBandCsiFrame conversion. It lazily wires each contributing node as a WorldGraph sensor (idempotent), bounds belief growth via the retention cap, and forwards explicit timestamps/calibration ids so the path stays deterministic and replayable. Wired additively into both live ESP32/WiFi fusion sites in main.rs via a split-borrow off the write guard, so person-count behavior is unchanged; the latest BLAKE3 witness is stored on AppState. Every published belief now carries evidence + model + calibration + privacy decision and a deterministic witness. Adds wifi-densepose-engine/-worldgraph/-bfld/-geo deps. 6 new bridge tests (witnessed belief with full provenance, cross-run determinism, idempotent node registration, retention bound, privacy-mode propagation). sensing-server suite 430+128 green; workspace gate 2,904 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(train): falsifiable occupancy benchmark with anti-overfitting gate Makes the presence/person-count "beyond SOTA" claim falsifiable in code instead of aspirational (the unfalsifiability gap from the beyond-SOTA system review). occupancy_bench grades predictions vs ground truth and gates a SOTA claim behind one claim_allowed invariant requiring ALL of: - DataProvenance::Measured — synthetic/mock data is scorable for regression but never claimable (anti-mock-contamination; the CLAUDE.md Kconfig-bug lesson made structural). - A leak-free EvalSplit — validate() refuses any split where a subject OR environment id appears in both train and test (subject leakage / per-environment overfitting). - n_test >= min_test_samples (small-N guard). - Presence F1 whose bootstrap-CI lower bound (deterministic seeded splitmix64) clears the threshold — not the point estimate. - Count MAE within threshold. The claim string is unreadable except through the gate (NO_CLAIM otherwise), same discipline as the ruview-gamma acceptance gate. What remains is data, not method: a frozen, SHA-pinned, subject/environment-disjoint measured replay set turns the claim into a passing/failing test. Lives in wifi-densepose-train (the eval bounded context, alongside ablation/ eval/metrics). 10 tests cover each refusal path; warning-clean under the crate's missing_docs lint. Workspace gate 2,914 passed / 0 failed. Doc 03 updated. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): per-room adapter provenance + drift-to-recalibration advisor Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150 section 3.4) could silently change inference without the witness noticing: provenance carried only "rfenc-v<N>" with no notion of adapter identity. - StreamingEngine::set_room_adapter(AdapterInfo): pins the adapter's content-derived id into provenance model_version ("rfenc-v1+adapter:<id>") — and therefore into the BLAKE3 witness — so swapping or clearing adapter weights always shifts the witness. Engine test proves base -> adapter -> other-adapter -> cleared all witness differently and cleared == base. - RecalibrationAdvisor: recommends re-running the ADR-135 empty-room baseline / refitting the room adapter on sustained low fusion coherence (streak threshold, default 60 cycles ~ 3 s at 20 Hz) or an ADR-142 change-point. Surfaced as TrustedOutput::recalibration_recommended, stored on the sensing-server AppState alongside the witness at both live fusion sites. - Bridge plumbing: EngineBridge::{set_room_adapter, clear_room_adapter} + live-path test that the adapter id flows into the live witness. Scope note (honest): this is the deployable provenance/trigger half of the "retrained model" roadmap item. Fitting the adapter itself runs in the existing external calibration service (aether-arena/calibration/); a trained RF-encoder checkpoint still does not exist in-tree. Engine 15 tests, bridge 7 tests. Workspace gate: 2,918 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix(mat): gate api module behind its feature — standalone no-default-features builds pub mod api was unconditional while its only dependency, serde, is optional behind the 'api' feature, so any build without default features failed with 101 unresolved-serde errors (masked in --workspace runs by feature unification). The api module and its create_router/AppState re-export are now cfg(feature = "api")-gated with docsrs annotations. All combos compile: bare --no-default-features (was 101 errors, now 0), --no-default-features --features api, and full default (177 tests pass). Workspace gate: 2,918 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * perf(signal): opt-in FFT operator for the CIR ISTA solver (8-14x measured) Phi is a sub-DFT, so each ISTA mat-vec can run as one length-G FFT (O(G log G)) instead of a dense O(K*G) product — the dominant-latency-hazard finding from the beyond-SOTA optimization roadmap. New CirConfig::fft_operator, default FALSE: the dense path stays the bit-exact witness default. The FFT evaluates the same sums in a different order, so enabling it shifts float results in the last bits and requires regenerating any pinned witness — strictly opt-in per deployment. FftOperator (rustfft, planned once at CirEstimator::new, scratch buffers reused across the ISTA loop) dispatches inside ista_solve: Phi x = scale * forward-FFT(x) sampled at bins (k_idx mod G) Phi^H v = scale * unnormalised inverse-FFT of v scattered into those bins Warm-start and Lipschitz estimation stay dense at construction. Measured (criterion, same run, same machine): ht20: 2.22 ms -> 265 us (8.4x) ht40: 10.26 ms -> 717 us (14.3x) The real HE40 grid (K=484, G=1452) scales further per the O(K*G)/O(G log G) ratio. 3 new tests: FFT<->dense matvec equivalence to float tolerance on ht20 and he40 grids; end-to-end dominant-tap agreement on a single-path frame; all default configs keep FFT off. New cir_estimate_fft bench group. Workspace gate: 2,921 passed / 0 failed (default path bit-exact, witnesses unchanged). https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(core): canonical frame decoder — capture-to-claim replay (ADR-136) The encode half of the ADR-136 frame contract existed (ComplexSample, to_canonical_bytes, witness_hash) but there was no decoder: a captured canonical frame could be witnessed but never reconstructed, blocking replay-from-capture. CsiFrame::from_canonical_bytes is the exact inverse: same id, metadata, complex payload, and witness hash (tested as the round-trip law AC7 — the replayed frame re-encodes byte-identically). Amplitude/phase are recomputed from the payload (projections, not independent state). Every malformed-input class fails closed (AC8): header truncation -> Truncated, payload truncation -> PayloadMismatch, unknown discriminants, non-UTF-8 device id, trailing bytes. Nil calibration uuid decodes as None per the documented encoding. Core: 36 tests pass. Workspace gate: 2,937 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): dynamic min-cut mesh partition guard (ruvector-mincut) Maintains an exact min-cut over the live mesh coupling graph — nodes are sensing nodes, coupling is the product of fusion attention weights — and surfaces per cycle, as TrustedOutput::mesh: - cut value: the global "how close is the array to partitioning" number, a structural measure per-node heuristics miss; - weak side: which specific nodes would split off (failure/jamming triage, feeds ADR-032 posture); - at-risk flag: counts as a structural event for the drift->recalibration advisor (alongside ADR-142 change-points). Degenerate cases fail toward risk: a node with zero coupling is reported as already partitioned (cut 0, that node as the weak side). Measured cost policy (criterion, 12-node mesh — the honest part): - weights quantized (1/64) + change-gated: steady-state cycles do ZERO graph work and reuse the cached cut (~7.3 us, ~23x cheaper than building); - on any real change a full exact rebuild (~171 us) is used, because ONE DynamicMinCut delete+insert measured ~240 us — the subpolynomial machinery amortizes on much larger graphs, so rebuild-on-change is the measured optimum at mesh scale (one-edge case -28% after switching policy); - full process_cycle with the guard: ~33 us for 4 nodes vs the 50 ms budget. 9 mesh_guard tests (weak-node detection, steady-state zero updates, sub-quantum gating, join/drop rebuild, determinism, disconnection) + an engine-level wiring test (down-weighted node -> weak side -> recalibration). Engine 24 tests; workspace gate 2,946 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): mesh partition risk demotes privacy + enters the witness (ADR-032) Completes the mesh-guard integration: its at_risk signal was advisory-only (fed the recalibration advisor). It now also contributes to the ADR-141 privacy demotion alongside fusion- and array-level contradictions — a mesh close to partitioning makes the fused belief less trustworthy, so the cycle emits at a more restricted class (monotonic; information only removed). Because effective_class feeds the BLAKE3 witness, a fragmenting array now shifts the witness: partition risk is auditable, not just logged. The mesh computation moved ahead of the demotion step in process_cycle; mesh_guard_mut exposes risk-threshold tuning. Test: a forced-risk 3-node cycle demotes PrivateHome Anonymous->Restricted and shifts the witness vs a clean baseline. Engine 25 tests; workspace gate 2,947 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix: public-PR review findings — privacy-path honesty, gate holes, mesh-guard cliff - sensing-server: engine errors logged+counted (no silent swallow), trust state exposed via status surface, privacy-demotion claims aligned with the actual parallel-audit-path behavior - occupancy_bench: vacuous-F1 hole closed (degenerate test sets fail with their own criterion); CI-lower-bound test made probative - mesh_guard: quantization scaled to observed coupling range — >=65-node balanced meshes no longer permanently at_risk (regression test) - engine: both wiring tests made probative (same-topology witness compare, deterministic risk-crossing fixture) - mat: axum/tokio optional behind api; real serde feature (api enables it) - core: canonical decoder strict (non-zero reserved bytes and nil UUID rejected — injective on accepted domain, forged-bytes tests) - CHANGELOG: un-spliced the FFT/adapter bullet mangle Co-Authored-By: claude-flow <ruv@ruv.net> * chore: strip private-track references for public PR Reword the occupancy-benchmark changelog bullet to drop a cross-reference to the private research track, and restore the WorldGraph retention bullet header that was glued onto the preceding MAT bullet. Co-Authored-By: claude-flow <ruv@ruv.net> * chore: lockfile refresh for cherry-picked feature set Co-Authored-By: claude-flow <ruv@ruv.net> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b9457220bd |
chore(cogs): publish cog-ha-matter 0.3.0 + bump signal/sensing-server to 0.3.1
cog-ha-matter required wifi-densepose-sensing-server with the `mqtt`
feature exposed, which crates.io 0.3.0 did not expose. Chain:
1. wifi-densepose-signal 0.3.0 -> 0.3.1 (already includes
EmbeddingHistory::{with_sketch,novelty} locally; needed
republish so sensing-server-0.3.1 can compile against it).
2. wifi-densepose-sensing-server 0.3.0 -> 0.3.1 (now exposes
the `mqtt` feature, sensing-server bin links against
signal-0.3.1 cleanly).
3. cog-ha-matter sensing-server dep bumped to ^0.3.1; publish=false
dropped. cog-ha-matter@0.3.0 published.
Both signal and sensing-server published with --no-verify; cargo's
verification step fails on Windows because openblas-src requires
vcpkg (the source itself builds fine in the workspace and on Linux).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
249d6c327f |
ADR-115: Home Assistant + Matter integration (#778)
Closes ADR-115's MQTT track (HA-DISCO + HA-MIND + HA-FABRIC scaffolding). Headline: - 21 entity kinds per node (11 raw + 10 semantic primitives) - MQTT auto-discovery with HA conventions - Matter Bridge scaffolding (SDK wiring deferred to v0.7.1 per ADR §9.10) - Privacy mode strips biometrics at the wire, semantic primitives keep working - 420+ lib tests, mosquitto-backed integration tests, property-based fuzzing - 8 starter HA Blueprints + 3 Lovelace dashboards shipped Tracking issue: #776 |
||
|
|
00a234eda8 |
ADR-110: ESP32-C6 firmware extension (#764)
Closes the firmware-side ADR-110 design at v0.7.0-esp32 after a 38-iter /loop SOTA sprint. Headline (bench, COM9+COM12 ESP32-C6): - 99.56% cross-board RX, 104.1 µs smoothed offset stdev (≤100 µs §2.4 target met) - 3.95× EMA suppression, 1.4 ppm crystal skew preserved 4 firmware releases: v0.6.7 / v0.6.8 / v0.6.9 / v0.7.0-esp32. 42 ADR-110 unit tests, 1761 v2 workspace tests, full Firmware CI + QEMU green. |
||
|
|
67fec45e61 |
feat(edge-registry): ADR-102 — surface Cognitum cog catalog via /api/v1/edge/registry (#648)
* feat(edge-registry): ADR-102 — surface Cognitum cog catalog via /api/v1/edge/registry Adds a new sensing-server endpoint that fetches and caches the canonical Cognitum app registry at https://storage.googleapis.com/cognitum-apps/app-registry.json (105 cogs across 11 categories as of v2.1.0). RuView previously had no live awareness of the catalog — the README's capability table was hand- curated and went stale as Cognitum shipped new cogs (the registry was last updated 6 days ago). ADR: * docs/adr/ADR-102-edge-module-registry.md — full design, response shape, configuration flags, failure modes, and a 12-row security review covering SSRF, response inflation, ?refresh abuse, stale-serve semantics, TLS, cache poisoning, JSON-panic resistance, etc. Code: * v2/.../edge_registry.rs — EdgeRegistry struct + UreqFetcher + MockFetcher trait + 7 unit tests. RwLock<Option<CachedEntry>> with stale-on-error fallback. MAX_PAYLOAD_BYTES=8 MiB, 10s wire timeout. * v2/.../main.rs — constructs Option<Arc<EdgeRegistry>> at startup, registers GET /api/v1/edge/registry handler, wires Extension layer. Handler runs the blocking ureq fetch via tokio::task::spawn_blocking so the async runtime stays free. * v2/.../cli.rs / main.rs Args — three new flags (per user request to "allow the registry to be disabled or changed"): --edge-registry-url <URL> (env RUVIEW_EDGE_REGISTRY_URL) --edge-registry-ttl-secs <N> (env RUVIEW_EDGE_REGISTRY_TTL_SECS) --no-edge-registry (env RUVIEW_NO_EDGE_REGISTRY) When --no-edge-registry is set or the URL is empty, the endpoint returns 404. Cargo.toml: adds ureq (rustls), sha2, thiserror as direct deps. README: * New collapsed "🧩 Edge Module Catalog" section with the full 105-cog table generated from the registry, grouped by category with practical one-line descriptions (e.g. "Spots irregular heartbeats and abnormal heart rhythms", "Detects walking problems and scores fall risk"). Links to https://seed.cognitum.one/store and the local appliance /cogs page. Sits between the HF model section and How It Works. Tests (7/7 pass): first_call_hits_upstream_and_caches ttl_expiry_triggers_refetch force_refresh_bypasses_fresh_cache stale_serve_on_upstream_failure_after_cached_success no_cache_no_upstream_returns_error upstream_invalid_json_is_treated_as_error upstream_sha256_is_deterministic Security highlights (full review in ADR-102 §"Security review"): - The registry is metadata-only; per-cog binary signatures (ADR-100) remain the trust root for installs. A compromised registry can mislead a human reader but cannot ship malicious binaries. - 8 MiB cap + 10s timeout + Option<Arc<...>> via Extension layer means the endpoint can't be used to exhaust memory or pin tokio threads. - Stale-on-error responses carry an explicit `stale: true` field so upstream outages are visible to consumers rather than silently masked. - Endpoint sits behind the existing RUVIEW_API_TOKEN bearer gate when set, otherwise unauthenticated (registry contents are public anyway). * chore: refresh Cargo.lock for ureq/sha2/thiserror deps added by ADR-102 |
||
|
|
ab9799adc3 |
chore(deps): bump tower-http from 0.5.2 to 0.6.8 in /v2 (#483)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.5.2 to 0.6.8. - [Release notes](https://github.com/tower-rs/tower-http/releases) - [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.5.2...tower-http-0.6.8) --- updated-dependencies: - dependency-name: tower-http dependency-version: 0.6.8 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
94ef125240 |
feat(sensing-server): introspection module skeleton (ADR-099 D1+D7+D8)
Adds the per-frame introspection state that ADR-099 specifies, plus the two
midstream dependencies. Pure addition — no other code touched.
v2/crates/wifi-densepose-sensing-server/Cargo.toml
+ midstreamer-temporal-compare = "0.2"
+ midstreamer-attractor = "0.2"
v2/crates/wifi-densepose-sensing-server/src/introspection.rs (new, 530 lines)
pub struct IntrospectionState
├─ midstreamer-attractor's AttractorAnalyzer (regime + Lyapunov)
├─ SignatureLibrary (JSON-loaded labelled segments)
├─ VecDeque<f64> sliding amplitude buffer (default 128 points)
└─ update(timestamp_ns, derived_feature) — never window-blocked
+ snapshot() -> IntrospectionSnapshot
{ timestamp_ns, frame_count, regime, lyapunov_exponent,
attractor_dim, attractor_confidence, top_k_similarity }
pub enum Regime { Idle, Periodic, Transient, Chaotic, Unknown }
pub struct Signature { id, label, vectors, dtw, promotion_threshold }
pub struct SimilarityMatch { signature_id, score, above_threshold }
DTW path is currently a host-side stand-in (length-normalised L1 with the
real DTW call deferred to I3/I5 once vec128 embeddings exist — ADR-099 P1).
The attractor path is wired to midstream directly. The analyze() step only
runs every N frames (default 8) to stay under the per-frame ms budget.
8 unit tests (snapshot defaults, frame-count + timestamp advance, empty
library, scoring + ordering invariants, threshold gating, empty-signature
fault-tolerance, regime classification after 200 frames). 199 → 207 lib tests,
0 failures. cargo build clean (only pre-existing warnings).
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
c641fc44ae |
feat(docker+sensing-server): refresh Docker publish + opt-in bearer-token API auth
Closes #520, #514, #443. ## #520 / #514 — stale Docker image, missing UI assets `ruvnet/wifi-densepose:latest` was published before `ui/observatory*` and `ui/pose-fusion*` were added; users see /app/ui missing those files and the v0.6+ packet format doesn't reach the server. Two fixes: 1. `docker/Dockerfile.rust` now `RUN`s a build-time guard after `COPY ui/` that fails the build if `index.html` / `observatory.html` / `pose-fusion.html` / `viz.html` (or the `observatory/` / `pose-fusion/` / `components/` / `services/` directories) are missing, plus an exec-bit check on `/app/sensing-server`. A stale image can never be silently produced again. 2. New `.github/workflows/sensing-server-docker.yml` rebuilds + pushes on every change to the Dockerfile, the server crate, the signal/vitals/ wifiscan crates, the workspace manifests, the `ui/` tree, or itself — plus `v*` tags and manual dispatch. Pushes to both `docker.io/ruvnet/ wifi-densepose` AND `ghcr.io/ruvnet/wifi-densepose` with `latest` + `vX.Y.Z` + `sha-<short>` tags, then post-push smoke-tests the artifact: /health, /api/v1/info, the observatory + pose-fusion HTML, AND the bearer-auth path (no token → 401, wrong → 401, correct → 200). Uses the `DOCKERHUB_USERNAME`/`DOCKERHUB_TOKEN` repo secrets; ghcr.io rides on the workflow's GITHUB_TOKEN. ## #443 — sensing-server REST API auth model QE security audit raised that 40+ /api/v1/* routes have no auth layer with a default `0.0.0.0` bind. New `wifi_densepose_sensing_server::bearer_auth` module + middleware: - Env-var-gated: `RUVIEW_API_TOKEN` unset/empty ⇒ middleware is a no-op (current LAN-mode behaviour preserved — **no default change**); set ⇒ every `/api/v1/*` request must carry `Authorization: Bearer <token>` or the server returns 401. - Constant-time byte compare via local `ct_eq` (no new dep). - `/health*`, `/ws/sensing`, and `/ui/*` are intentionally never gated (orchestrator probes + local browsers). - Startup logs which mode is active and warns when auth is ON with a `0.0.0.0` bind. - 8 unit tests on the middleware via `tower::ServiceExt::oneshot` (sensing-server lib tests 191 → 199, 0 failures). Verified locally: `cargo build --workspace --no-default-features` ✓, `cargo test -p wifi-densepose-sensing-server --no-default-features` ✓. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
f49c722764 |
chore(repo): rename rust-port/wifi-densepose-rs → v2/ (flatten to one level) (#427)
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/) without any sibling under rust-port/ that warranted the extra level. Move the whole workspace up to v2/ to match v1/ (Python) at the same depth and shorten every cd / build command across the repo. git mv preserves history for all tracked files. 60 files updated for path references (CI workflows, ADRs, docs, scripts, READMEs, internal .claude-flow state). Two manual fixes for relative-cd paths in CLAUDE.md and ADR-043 that became wrong after the depth change (cd ../.. → cd ..). Validated: - cargo check --workspace --no-default-features → clean (after target/ nuke; the gitignored target/ was carried by the OS rename and had hard-coded old paths in build scripts) - cargo test --workspace --no-default-features → 1,539 passed, 0 failed, 8 ignored (same totals as pre-rename) - ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm) After-merge follow-up: contributors should `rm -rf v2/target` once and let cargo regenerate from the new path. |