Skip to content

Commit 3d8f854

Browse files
authored
Merge branch 'ruvnet:main' into main
2 parents 97ddcce + 82c1b8f commit 3d8f854

21 files changed

Lines changed: 204 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- **`@ruvnet/rvagent` startup optimization — stdio time-to-first-response ~242 ms → ~189 ms (−22%; MEASURED, median of repeated `initialize` round-trips against `dist/index.js`, this container, reproduce with a piped-stdin timer).** Two changes: (1) `./http-transport.js` is now imported **lazily** inside the `RVAGENT_HTTP_PORT` branch — it chain-loads the MCP SDK's `streamableHttp` module (~48 ms MEASURED via per-module `import()` timing), which the default stdio path never uses; (2) the advertised JSON Schemas generated from the Zod sources are memoized per tool instead of re-walking the Zod tree on every `tools/list` (matters under the session-per-server HTTP model where each session lists tools). No behavior change: 99/99 jest tests, HTTP session flow re-smoke-tested through the lazy path. The `@ruvnet/ruview` harness CLI was profiled too and left alone — 50 ms vs the ~29 ms bare `node -e ''` floor on the same box (MEASURED), i.e. already near the interpreter floor with zero dependencies.
1212

1313
### Fixed
14+
- **EngineBridge now honors `WDP_GUARD_INTERVAL_US`/`WDP_SOFT_GUARD_US`/`WDP_TDM_SLOTS`+`WDP_TDM_SLOT_US`** (#1309, PR #1312, @erichkusuki). The governed trust path previously built its multistatic fuser from a hardcoded `MultistaticConfig::default()` (60 ms guard), so multi-node deployments with WiFi/ESP-NOW time sync (10–150 ms drift) failed every governed cycle regardless of configuration — while the startup log claimed the override took effect. New `StreamingEngine::set_multistatic_config()`; `EngineBridge::new()` takes an `Option<MultistaticConfig>` threaded from the same env-derived config as `AppState.multistatic_fuser`. Hardware-verified on a live 2-node ESP32-S3 setup (90 s window, 0 fusion errors; previously every cycle failed).
15+
- **`/api/v1/stream/pose` WebSocket reachable with `RUVIEW_API_TOKEN` set + dashboard bearer-token field** (#1310, PR #1313, @erichkusuki). Browsers cannot attach an `Authorization` header to a WS upgrade, so the Live Demo pose stream always failed when auth was on; the path is now on a narrow exact-match exemption list (mirrors `/ws/sensing`), with a regression test pinning that the exemption doesn't leak to other `/api/v1/*` paths. The QuickSettings panel gains an "API Access" field storing the bearer token in `localStorage`; the token is applied at `api.service.js` module load so the very first request carries it.
16+
- **Display-less DevKitC-1 boards: `sdkconfig.defaults.devkitc` build overlay** (#1308, PR #1311, @erichkusuki). The ADR-045 runtime display probe false-positives on stock ESP32-S3-DevKitC-1 (floating QSPI pins), which silently skipped the RuView#893 MGMT+DATA CSI upgrade and collapsed CSI yield to 0 pps. The overlay compiles display support out (`has_display` constant-false). Also fixes stale `espressif/idf:v5.2` README references to v5.4 (source requires `esp_driver_uart`, IDF ≥5.3). Hardware-verified on 2× DevKitC-1-N16R8 (0 → 40–45 pps).
1417
- **ADR-263/264/265 implemented — the RuView npm surface fixed end-to-end (`@ruvnet/ruview@0.2.0`, `@ruvnet/rvagent@0.2.0`, `@ruv/ruview-cli`).** Harness (ADR-263 O1–O9): `claim-check` now **fails closed** on empty input (CLI exit 2 + `empty_text` tool error); the MCP stdio server dispatches `tools/call` asynchronously over promise-based `spawn` — `ping` answers while a long `verify`/`calibrate` runs (pinned by a new e2e test that runs a 3 s fake proof and asserts sub-second ping); the two `optionalDependencies` are gone so a cold `npx` installs exactly 1 package (MEASURED: was 4 packages / 620 kB / 71 files, `npm i` in a clean prefix); child output is captured as bounded rolling tails (no more 1 MiB `maxBuffer` kills); `node_monitor` passes the port via `sys.argv` instead of splicing it into `python -c` source; the MCP `serverInfo.version` reads package.json; `.claude/skills/*/SKILL.md` are generated from `skills/*.md` by a `prepack` sync script (byte-equality pinned by test); `which()` is a memoized dep-free PATH scan; tools are underscore-canonical (`ruview_claim_check`, …) with the dotted names accepted as call-time aliases, plus `resources/list`/`prompts/list` stubs; the guardrail's `METRIC_TERMS` matching is precision-fixed (word-boundary `map`/`f1`/`auc`/`iou`, code-span + label scrubbing, quantitative-claims-only) — ADR-263/264/265 and both package READMEs now PASS `claim-check` while real untagged claims still flag. 30/30 tests (MEASURED, `node --test`). rvagent (ADR-264 O1–O9): `exports` fixed (types-first, the never-built `dist/index.cjs` `require` target removed — verified broken in the published 0.1.0 tarball); tarball is map-free (127,704 B unpacked / 46 files / 0 maps — MEASURED, `npm pack --dry-run`, down from 188 kB with 44 maps); the Streamable HTTP transport is **actually wired** behind `RVAGENT_HTTP_PORT` with one transport + one MCP server per session (`mcp-session-id` routing), a 1 MiB body cap (413), and a port-aware localhost origin gate — the "dual-transport" description is now true; tools renamed to underscore-canonical with dotted router aliases; ONE Zod validation gate per call with the advertised JSON Schema generated from the same Zod source (`zod-to-json-schema`); `train_count` closes its log fds (was leaking 2/job) and persists job records to `<jobsDir>/<id>.json` so `job_status` survives restarts, with bounded log-tail reads; `detectCogBinary` actually probes its candidate paths; version reads package.json; `@types/express` dropped, `@types/jest` aligned to jest 29; README rewritten to match reality (no phantom `stdio`/`http`/`policy grant` subcommands; unimplemented ADR-124 catalog tools labeled roadmap). 99/99 jest tests (MEASURED); stdio handshake + HTTP session flow + 403/400/404/413 gates smoke-tested live. CLI: bin renamed `ruview-cli` (the `ruview` bin belongs to `@ruvnet/ruview`, ADR-265 D4), version single-sourced. Distribution (ADR-265 D1–D4): new `npm-packages.yml` (3-package × Node 20/22 matrix: tests, version-literal grep gate, pack-content/size gate, tarball-install smoke test incl. the fail-closed claim-check and an ESM-import probe that would have caught the broken `require` export, README claim-check) and `ruview-npm-release.yml` (publish from CI only, `npm publish --provenance`); `ci.yml` NODE_VERSION 18→20.
1518
- **Multistatic fusion never ran on a mixed-mode ESP32 mesh — live bridge fed raw, un-canonicalized per-node CSI to the fuser (#1170).** `node_frame_from_state` (`multistatic_bridge.rs`) wrapped each node's **raw** amplitude vector (HT20 ≈ 64 bins, HT40 ≈ 128/192) into a struct *named* `CanonicalCsiFrame` without ever resampling, so `MultistaticFuser::fuse` tripped `DimensionMismatch` on every cycle, silently fell back to per-node sum/dedup, and spun `total_engine_errors` unbounded. Added `HardwareNormalizer::resample_to_canonical` (resample-only, **no z-score** — preserves the amplitude scale the person-score's `variance/mean²` relies on) and run every node frame through it onto the canonical 56-tone grid before fusion. Heterogeneous meshes now fuse instead of erroring. Pinned by `heterogeneous_node_counts_canonicalize_and_fuse` (mixed 64/192 → fuses), `resample_to_canonical_is_length_only_no_zscore`, and an updated `test_node_frame_conversion`; the pre-existing `engine_bridge::observe_cycle_counts_engine_errors` was retargeted to force a `TimestampMismatch` (its old 56-vs-30 setup now canonicalizes cleanly). `wifi-densepose-signal` 501 / `wifi-densepose-sensing-server` 677 tests, 0 failed.
1619
- **`csi_fps_ema` reported the CSI frame rate 40–840× too high under bursty UDP delivery (#1180).** `update_csi_fps_ema` only rejected deltas `≤ 0` or `≥ 1 s`, so a 36 µs intra-burst arrival delta yielded `1/dt ≈ 27 kHz` straight into the EMA — the metric measured server arrival jitter, not the node's ~40 fps production rate. Added a `MIN_PLAUSIBLE_CSI_DT_SEC = 0.005` floor (derived from the firmware's 50 fps `CSI_MIN_SEND_INTERVAL_US` ceiling, ×4 slack) and made `observe_csi_frame_arrival` keep its anchor across sub-floor bursts so the next genuine inter-frame gap measures true cadence. Pinned by `subms_burst_delta_rejected`, `burst_interleaved_with_nominal_stays_in_band`, and `observe_csi_frame_arrival_ignores_subms_bursts`.

firmware/esp32-csi-node/README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This firmware captures WiFi Channel State Information (CSI) from an ESP32-S3 (production) or ESP32-C6 (research target — Wi-Fi 6 / 802.15.4 / TWT / LP-core hibernation, see [ADR-110](../../docs/adr/ADR-110-esp32-c6-firmware-extension.md)) and transforms it into real-time presence detection, vital sign monitoring, and programmable sensing -- all without cameras or wearables. Part of the [WiFi-DensePose](../../README.md) project.
66

7-
[![ESP-IDF v5.2](https://img.shields.io/badge/ESP--IDF-v5.2-blue.svg)](https://docs.espressif.com/projects/esp-idf/en/v5.2/)
7+
[![ESP-IDF v5.4](https://img.shields.io/badge/ESP--IDF-v5.4-blue.svg)](https://docs.espressif.com/projects/esp-idf/en/v5.4/)
88
[![Target: ESP32-S3 / ESP32-C6](https://img.shields.io/badge/target-ESP32--S3%20%7C%20ESP32--C6-purple.svg)](https://www.espressif.com/en/products/socs/esp32-s3)
99
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-green.svg)](../../LICENSE)
1010
[![Binary: ~943 KB](https://img.shields.io/badge/binary-~943%20KB-orange.svg)](#memory-budget)
@@ -48,10 +48,18 @@ with `--flash_size 4MB`.
4848
# From the repository root:
4949
MSYS_NO_PATHCONV=1 docker run --rm \
5050
-v "$(pwd)/firmware/esp32-csi-node:/project" -w /project \
51-
espressif/idf:v5.2 bash -c \
51+
espressif/idf:v5.4 bash -c \
5252
"rm -rf build sdkconfig && idf.py set-target esp32s3 && idf.py build"
5353
```
5454

55+
> **Display-less boards (ESP32-S3-DevKitC-1 and similar):** build with the
56+
> `sdkconfig.defaults.devkitc` overlay instead — the default build compiles
57+
> display support in, and the runtime panel probe false-positives on boards
58+
> with no panel, which disables the RuView#893 MGMT+DATA CSI upgrade and
59+
> collapses CSI yield to 0 pps. See the header of
60+
> [`sdkconfig.defaults.devkitc`](sdkconfig.defaults.devkitc) for the exact
61+
> build command.
62+
5563
### 2. Flash
5664

5765
Offsets must match `partitions_display.csv` (8 MB) or `partitions_4mb.csv` (4 MB):
@@ -250,7 +258,7 @@ Offset Size Field
250258
# From the repository root:
251259
MSYS_NO_PATHCONV=1 docker run --rm \
252260
-v "$(pwd)/firmware/esp32-csi-node:/project" -w /project \
253-
espressif/idf:v5.2 bash -c \
261+
espressif/idf:v5.4 bash -c \
254262
"rm -rf build sdkconfig && idf.py set-target esp32s3 && idf.py build"
255263
```
256264

@@ -268,7 +276,7 @@ To change Kconfig settings before building:
268276
```bash
269277
MSYS_NO_PATHCONV=1 docker run --rm -it \
270278
-v "$(pwd)/firmware/esp32-csi-node:/project" -w /project \
271-
espressif/idf:v5.2 bash -c \
279+
espressif/idf:v5.4 bash -c \
272280
"idf.py set-target esp32s3 && idf.py menuconfig"
273281
```
274282

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# DevKitC-1 (display-less) production overlay.
2+
#
3+
# The stock ESP32-S3-DevKitC-1 has no AMOLED panel, but the ADR-045 runtime
4+
# probe false-positives on it: with no TCA9554 and floating QSPI pins, the
5+
# SH8601 init sequence reports success, display_is_active() returns true, and
6+
# main.c skips the RuView#893 MGMT+DATA promiscuous upgrade — CSI yield
7+
# collapses to 0 pps (the exact symptom #893 fixed). Compiling display support
8+
# out makes has_display constant-false so the upgrade always applies.
9+
#
10+
# Build (from repo root, per README "Docker — the only reliable method"):
11+
# MSYS_NO_PATHCONV=1 docker run --rm \
12+
# -v "$(pwd)/firmware/esp32-csi-node:/project" -w /project \
13+
# espressif/idf:v5.4 bash -c \
14+
# "rm -rf build sdkconfig && idf.py -DSDKCONFIG_DEFAULTS='sdkconfig.defaults;sdkconfig.defaults.devkitc' set-target esp32s3 && idf.py -DSDKCONFIG_DEFAULTS='sdkconfig.defaults;sdkconfig.defaults.devkitc' build"
15+
16+
# CONFIG_DISPLAY_ENABLE is not set

ui/services/api.service.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,17 @@ export class ApiService {
147147
}
148148

149149
// Create singleton instance
150-
export const apiService = new ApiService();
150+
export const apiService = new ApiService();
151+
152+
// Storage key shared with the QuickSettings "API Access" panel.
153+
export const API_TOKEN_STORAGE_KEY = 'ruview-api-token';
154+
155+
// Apply a previously-saved bearer token at module load — before app init
156+
// dispatches its first request — so a configured RUVIEW_API_TOKEN works from
157+
// the very first /api/v1/* call. The server only ever checks the
158+
// `Authorization: Bearer` header (see bearer_auth.rs) — this intentionally
159+
// never puts the token in a URL query string.
160+
try {
161+
const storedToken = localStorage.getItem(API_TOKEN_STORAGE_KEY);
162+
if (storedToken) apiService.setAuthToken(storedToken);
163+
} catch { /* storage unavailable (private browsing etc.) */ }

ui/style.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4088,6 +4088,20 @@ a:focus-visible,
40884088
color: #fff;
40894089
}
40904090

4091+
.qs-text-input {
4092+
padding: var(--space-6) var(--space-8);
4093+
border-radius: var(--radius-sm);
4094+
border: 1px solid var(--color-border);
4095+
background: var(--color-secondary);
4096+
color: var(--color-text);
4097+
font-size: var(--font-size-sm);
4098+
}
4099+
4100+
.qs-text-input:focus {
4101+
outline: 2px solid var(--color-primary);
4102+
outline-offset: 1px;
4103+
}
4104+
40914105
/* --- Screenshot Flash --- */
40924106

40934107
.screenshot-flash {

ui/utils/quick-settings.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Quick Settings Panel - Centralized configuration for all UI features
22
// Accessible via gear icon in header
33

4+
import { apiService, API_TOKEN_STORAGE_KEY } from '../services/api.service.js';
5+
46
export class QuickSettings {
57
constructor(app) {
68
this.app = app;
@@ -9,6 +11,8 @@ export class QuickSettings {
911
this.isOpen = false;
1012
}
1113

14+
// A stored token is applied at api.service.js module load (before any
15+
// request fires) — this panel only saves/clears it.
1216
init() {
1317
this.createButton();
1418
this.createPanel();
@@ -70,6 +74,18 @@ export class QuickSettings {
7074
<span class="qs-switch"></span>
7175
</label>
7276
</div>
77+
<div class="qs-section">
78+
<div class="qs-section-title">API Access</div>
79+
<div class="qs-row" style="flex-direction: column; align-items: stretch; gap: 6px;">
80+
<span>Bearer token (set only if the server enforces RUVIEW_API_TOKEN)</span>
81+
<input type="password" id="qs-api-token" class="qs-text-input" placeholder="Paste token..." autocomplete="off" style="width: 100%; box-sizing: border-box;">
82+
<div style="display: flex; gap: 8px;">
83+
<button class="qs-btn" id="qs-api-token-save">Save & Apply</button>
84+
<button class="qs-btn-danger" id="qs-api-token-clear">Clear</button>
85+
</div>
86+
<span id="qs-api-token-status" style="font-size: 0.85em; opacity: 0.75;"></span>
87+
</div>
88+
</div>
7389
<div class="qs-section">
7490
<div class="qs-section-title">Data</div>
7591
<div class="qs-row">
@@ -112,6 +128,30 @@ export class QuickSettings {
112128
}
113129
});
114130

131+
this.panel.querySelector('#qs-api-token-save').addEventListener('click', () => {
132+
const input = this.panel.querySelector('#qs-api-token');
133+
const status = this.panel.querySelector('#qs-api-token-status');
134+
const token = input.value.trim();
135+
if (!token) {
136+
status.textContent = 'Enter a token first, or use Clear to remove one.';
137+
return;
138+
}
139+
try { localStorage.setItem(API_TOKEN_STORAGE_KEY, token); } catch { /* noop */ }
140+
apiService.setAuthToken(token);
141+
status.textContent = 'Token saved and applied. Reloading...';
142+
setTimeout(() => window.location.reload(), 600);
143+
});
144+
145+
this.panel.querySelector('#qs-api-token-clear').addEventListener('click', () => {
146+
const input = this.panel.querySelector('#qs-api-token');
147+
const status = this.panel.querySelector('#qs-api-token-status');
148+
try { localStorage.removeItem(API_TOKEN_STORAGE_KEY); } catch { /* noop */ }
149+
apiService.setAuthToken(null);
150+
input.value = '';
151+
status.textContent = 'Token cleared. Reloading...';
152+
setTimeout(() => window.location.reload(), 600);
153+
});
154+
115155
this.panel.querySelector('#qs-clear-data').addEventListener('click', () => {
116156
try {
117157
localStorage.clear();
@@ -154,6 +194,10 @@ export class QuickSettings {
154194
if (this.getSetting('compact')) {
155195
document.body.classList.add('compact-mode');
156196
}
197+
const status = this.panel.querySelector('#qs-api-token-status');
198+
let hasToken = false;
199+
try { hasToken = !!localStorage.getItem(API_TOKEN_STORAGE_KEY); } catch { /* noop */ }
200+
if (status) status.textContent = hasToken ? 'A token is currently set.' : 'No token set (auth is off or unnecessary).';
157201
}
158202

159203
prefersReducedMotion() {

v2/Cargo.lock

Lines changed: 4 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v2/crates/wifi-densepose-engine/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "wifi-densepose-engine"
33
description = "RuView streaming-engine integration layer — composes the ADR-135..146 building blocks into one trust-traceable pipeline cycle"
4-
version = "0.3.0"
4+
version = "0.3.1"
55
edition.workspace = true
66
authors.workspace = true
77
license.workspace = true

v2/crates/wifi-densepose-engine/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,19 @@ impl StreamingEngine {
226226
}
227227
}
228228

229+
/// Override the multistatic fuser's timestamp guard interval (#1049/#1057).
230+
/// Without this, `StreamingEngine::new` always builds
231+
/// `MultistaticFuser::with_config(MultistaticConfig::default())` — a
232+
/// hardcoded 60 ms hard guard that ignores whatever schedule/override the
233+
/// caller derived from `WDP_TDM_SLOTS`/`WDP_GUARD_INTERVAL_US`, so
234+
/// WiFi/ESP-NOW-synced multi-node deployments spuriously fail governed
235+
/// trust cycles even after widening the guard elsewhere.
236+
///
237+
/// Rebuilds the fuser, so call before any frames are processed.
238+
pub fn set_multistatic_config(&mut self, cfg: MultistaticConfig) {
239+
self.fuser = MultistaticFuser::with_config(cfg);
240+
}
241+
229242
/// Activate a per-room calibration adapter (ADR-150 §3.4). From the next
230243
/// cycle on, the adapter id is part of provenance `model_version` — and
231244
/// therefore of the witness — so the exact weights shaping inference are

v2/crates/wifi-densepose-rufield/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ repository.workspace = true
1515
# (serde / serde_json / toml / sha2 / ed25519-dalek only — no tch / openblas /
1616
# ndarray / candle), so they build under `--no-default-features`.
1717
[dependencies]
18-
rufield-core = { path = "../../../vendor/rufield/crates/rufield-core" }
19-
rufield-provenance = { path = "../../../vendor/rufield/crates/rufield-provenance" }
20-
rufield-privacy = { path = "../../../vendor/rufield/crates/rufield-privacy" }
21-
rufield-fusion = { path = "../../../vendor/rufield/crates/rufield-fusion" }
18+
rufield-core = { version = "0.1.0", path = "../../../vendor/rufield/crates/rufield-core" }
19+
rufield-provenance = { version = "0.1.0", path = "../../../vendor/rufield/crates/rufield-provenance" }
20+
rufield-privacy = { version = "0.1.0", path = "../../../vendor/rufield/crates/rufield-privacy" }
21+
rufield-fusion = { version = "0.1.0", path = "../../../vendor/rufield/crates/rufield-fusion" }
2222
serde = { workspace = true }
2323
serde_json = { workspace = true }
2424

0 commit comments

Comments
 (0)