--password puts the WiFi password in argv, where ps and shell history can
see it (#1754). This adds two ways to give it without that:
- --password-file PATH reads it from a file, dropping one trailing newline
(\n or \r\n). The file must exist and not be empty. If group or others can
read it, the script refuses and suggests chmod 600.
--allow-insecure-password-file accepts such a file with a warning, for
read-only mounts such as 0444 secrets.
- When --ssid is given with no password flag and stdin is a terminal, the
script asks with getpass. It reuses a saved password only if it was saved
for the same SSID. Without a terminal (scripts, CI) nothing is asked and
the existing missing-credentials check applies.
--password still works, but now prints a one-line warning on stderr that
points at --password-file. The two flags are mutually exclusive. The
firmware README examples now use --password-file.
Tests: 10 new tests in tests/test_provision_secrets.py, with placeholder
credentials and getpass and the flasher stubbed. 8 fail on the previous
commit; the other 2 check that nothing is asked without a terminal or when a
saved password matches the SSID. The suite goes from 25 to 35 passing.
Refs #1754
Co-Authored-By: claude-flow <ruv@ruv.net>
The per-port state file holds the WiFi password and seed token in clear, but
save_state() created the directory and file with the default umask, so they
landed 0755/0644 and any local user could read them (#1754). --state also
printed both values verbatim, and that output gets pasted into issues.
- The state dir is created 0700 and state files 0600. Staging uses mkstemp
(unique name, O_EXCL, 0600) plus fchmod, so a stale temp file or a planted
symlink can't receive the secret.
- harden_state_dir() tightens the dir and every .json/.tmp file in it,
including ones left by older versions. main() runs it before any read or
early exit, so --state repairs old files too. It skips symlinks and files
owned by other users, and only warns if chmod fails.
- The --dry-run NVS binary and the generator-failure CSV fallback are written
0600 even when the file already exists. The generator's CSV and binary now
live in a private mkdtemp() dir instead of the shared temp dir.
- --state shows password and seed_token as (set)/(empty). --show-secrets
prints them.
Windows is unchanged: chmod can't set owner-only ACLs there.
Tests: tests/test_provision_secrets.py adds 11 tests; all 11 fail on the
base and the suite goes from 14 to 25 passing.
Refs #1754
Co-Authored-By: claude-flow <ruv@ruv.net>
--dry-run persisted the merged state record, so the next real run merged
on top of values that never reached the device. It now skips save_state.
The NVS binary is still written to nvs_provision.bin because ADR-061 and
scripts/qemu-mesh-test.sh consume it.
--reset deleted the per-port state file before argument validation, so a
run that then failed (e.g. missing WiFi credentials) still lost the record.
It now only ignores the prior state; a successful flash overwrites the file
as before.
Also fix the module docstring's package name: the PyPI package is
esp-idf-nvs-partition-gen, matching the runtime hint.
Fixes#2096
Co-Authored-By: claude-flow <ruv@ruv.net>
compute_verifier returns the minimal big-endian encoding, so about one
provisioning in 256 produced a verifier with a zero top byte, encoded to
383 bytes, and failed the store's own 384-byte check
("SRP verifier must contain exactly 384 bytes"). Seen in CI on
first_provisioning_returns_code_but_restart_does_not. Left-pad to the
3072-bit group width at creation: the same number, the fixed length.
Co-Authored-By: claude-flow <ruv@ruv.net>
Bump the harness and the umbrella (which pins it), point README and the
user guide at 0.9.1, sync the stale lockfile header (0.5.1, old bin name)
with package.json, and add a plain-language changelog entry for the
live sensing views in Claude Code (ADR-377, ADR-378).
Co-Authored-By: claude-flow <ruv@ruv.net>
- Stale frames shown as live: spectrumWith now keeps only nodes in the
latest capture, so a node that stops streaming (or a failed capture)
leaves the waterfall and the "no CSI frames" card returns.
- Multi-antenna ESP32 nodes never reached the waterfall: with --spectrum
the parser adds chain-0 amplitudes (ADR-018 antenna-major) in a separate
field the SpectrumCollector reads; the kernel path is unchanged.
- Frames from different CSI layouts could be joined: spectrum results
carry their shape, and frames merge only when shape and bins match.
- Animation ran on assumed time: frames read the real clock (the one the
full render uses), so pulses keep the reported rates and the replay
keeps up when a frame fires late; replay lag is pruned with the nodes.
Tests: regression tests for each; npm test 193 pass, test:security 91
pass, engine tests 4 pass, validate passes.
Co-Authored-By: claude-flow <ruv@ruv.net>
Hotkeys reach a pane only while it holds the keyboard, and `focus` alone
was not taking it. The engine's documented dialog form does: open with
focus + closeOnEscape + holdToasts (Escape closes, as originally). Each
open also requests `rows` for its view (overview 16, waterfall 30,
radar 26) so the inline pane opens tall enough for the bordered design
instead of a third of the screen; switching views re-requests the size,
and picture heights leave room for the chrome (rows - 15).
Co-Authored-By: claude-flow <ruv@ruv.net>
The compact inline layout read as broken next to the bordered cards the
user prefers; restore the previous design (78a147405). This reverts
commit 629e1b9c6.
Co-Authored-By: claude-flow <ruv@ruv.net>
Outside fullscreen the pane sits inline above the prompt with a few rows,
so the bordered cards, rule and footer were cut off (Refresh/Close never
visible). Inline (or under 18 rows) the pane is now compact: views and
actions share the top row, no borders/rule/gaps, pictures sized to the
rows left, radar readings beside a small fan. The top row always says
whether the pane holds the keyboard ("keys active · Esc returns them" or
"click the pane or press ctrl+x tab to use the keys").
Co-Authored-By: claude-flow <ruv@ruv.net>
Button hotkeys reach a Pane only while it holds the keyboard; the pane
opened without it, so 1/2/3/r/c went to the prompt. Open (and
/ruview waterfall|radar) now request `focus: true`; closeOnEscape is
dropped so Escape hands the keys back without closing; an unfocused pane
shows "click the pane or press ctrl+x tab to use the keys".
Co-Authored-By: claude-flow <ruv@ruv.net>
The /ruview pane gains three views (keys 1/2/3) drawn in terminal Rasters
and animated in place with $.ui.blit (~12 fps, no redraws):
- CSI waterfall: per-subcarrier amplitude over time from the new
`esp32 --spectrum` capture, two frames per row via the half block,
5-95th percentile colour limits, replayed at the frames' arrival rate,
labelled MEASURED or SYNTHETIC from the packets' own flag; n cycles nodes.
- Radar: a 120-degree range fan with rings, a fading distance trail and a
sonar ping to the measured range; braille heart/breathing/distance charts
scaled to the data; a heart and breathing gauge that pulse at the
device-reported rates ("a metronome, not a waveform").
- A shimmer rule under the title; surfaces without Raster get a note.
Harness: captureEsp32 / ruview_esp32_capture take spectrum,
spectrum_bins (8-128) and spectrum_frames (8-256): a bounded ring of
binned amplitude frames per node, dominant shape, rate and synthetic flag.
The mod is split into model/raster/anim/views/register (own-file imports
only). Tests: 11 showcase + 11 mod Node tests, a --spectrum loopback test,
4 engine tests (waterfall tab sends --spectrum, draws the Raster, blits);
npm test 189 pass, test:security 90 pass, manifest verified, validate passes.
Evidence: MEASURED on an ESP32-C6 (COM11, release c6-adr110 v0.7.0, node 6)
streaming 7-18 Hz CSI to this host; a 1x256 capture renders the occupied
band against guard/null tones. Unpacked budget 384 -> 448 KiB (+~37 KB).
Co-Authored-By: claude-flow <ruv@ruv.net>
- Header: RuView + LIVE/STALE badge (fresh within 2 refresh periods),
"updated <time> (Ns ago)", a 5 s redraw tick keeps the age current.
- Bordered cards for the radar and the CSI nodes, side by side from 100
columns, stacked below.
- Radar: PRESENCE DETECTED (+ target count) in green; distance, heart and
breathing with per-refresh sparklines; device-reported vitals outside
resting ranges (heart 40-180, breathing 4-40 bpm) are flagged in yellow
instead of shown as fact (the live kit reported breathing 1.0 bpm).
- Nodes: per-node rate sparkline; "no packets" is a short hint in the card
rather than a long top alert (status no longer counts it as an alert);
an unconfigured radar says how to configure it.
- Buttons show their hotkeys.
Tests: 11 Node unit tests, 3 engine tests (mounted pane validates on the
terminal surface), validate passes.
Co-Authored-By: claude-flow <ruv@ruv.net>
A hot reload (or a resumed session) re-runs register with fresh variables
while the engine keeps the pane open, so no timer ran and the pane stayed
on "waiting for the first capture". Drawing the pane now resumes polling
when no timer is running. Engine test mounts the pane with no /ruview in
the module instance: fails without the fix, passes with it.
Co-Authored-By: claude-flow <ruv@ruv.net>
$.clock.now() resolves a promise of epoch milliseconds; refresh passed the
promise into the model. Await it, and have the pane print "—" for any
non-finite timestamp. Seen live in Claude Code 2.1.287 (/ruview pane).
Regression tests fail without the fix (2/2) and pass with it.
Co-Authored-By: claude-flow <ruv@ruv.net>
- mod/: a Claude Code mod (function hooks). /ruview opens a pane beside
the transcript with CSI nodes (rate, loss, RSSI, shape, SYNTHETIC) and an
optional ESPHome radar (presence, distance, device-reported vitals),
alerts (heartbeat-only boards + buffer-starvation hint), Refresh/Close
buttons and a one-line status; refreshed on $.clock.every.
- Reads only through the harness CLI beside it (fixed read-only argv,
--json) located via $.plugin.root (mods may import nothing else).
- Packaged in @ruvnet/ruview (mod/.claude-plugin, mod/hooks), listed in the
repo marketplace as ruview-live@ruview; `ruview mod` prints how to load it.
- @ruvnet/ruview 0.9.0; umbrella ruview 0.9.0 (still unpublished).
Verified: claude plugin validate (mod + marketplace); claude plugin test
2/2 x3 runs; harness 172 tests + 89 security; real Claude Code headless
`/ruview refresh` from an installed tarball and with the live radar kit.
Co-Authored-By: claude-flow <ruv@ruv.net>
- harness/ruview-umbrella -> npm `ruview` 0.8.0: one CLI and one MCP server
for the operator harness, Homecore and the vitals kernel. `ruview homecore
...`, `ruview kernel <doctor|info|selftest|parity|synth|analyze|bench>`,
`ruview capabilities`; everything else delegates to @ruvnet/ruview. MCP
serves ruview_* + homecore_* tools and the ui:// console (stdio + HTTP).
- @ruvnet/ruview 0.8.0: bin renamed `ruview-harness` (the umbrella owns
`ruview`; a clean tarball install showed the bin collision). New exports
(cli, cli-ui, mcp, mcp-http, ui, esphome, kernel); injectable MCP handler;
kernel as optional peer + setKernelImporter so the kernel resolves from
the umbrella's tree under linked/pnpm installs.
- mmwave --source esphome: dependency-free read-only ESPHome native API
client (private hosts only; encrypted/password APIs reported). Live Seeed
MR60BHA2 kit: presence, distance, device-reported HR/BR (MEASURED).
- monitor detects Ameba CSI buffer starvation; radar views in terminal UI
and console widget.
- homecore 0.1.0 and @ruvnet/ruview-kernel 0.1.0 gain ./cli exports.
Gates: harness 166 + 89 security, homecore 27 + 23, kernel 21 + 12,
umbrella 4; clean-dir tarball install smoke incl. live radar over HTTP MCP.
Local publish is a one-off exception to ADR-265 D2, recorded in ADR-376.
Co-Authored-By: claude-flow <ruv@ruv.net>
Live RTL8721Dx capture (node 3, COM10) reported 97% loss: one stray
sequence value (+52,835, then straight back) was counted as 52,834 lost
frames, and every late UDP packet re-counted its gap. The tracker now
treats a single out-of-window value as a stray, needs two agreeing frames
to accept a counter reset, and lets a late packet fill its own gap
(seqReordered / seqStrays / seqResyncs in the summary). Against ground
truth on the live board (unique sequence numbers over the span): 4,783 of
4,783 present, loss 0, 9 strays; the tracker matched both.
ADR-373 Amendment 2 records the live Realtek run: devices -> realtek,
7,404/7,404 packets decoded, monitor at 1500000 baud without a reset, and
12,549 live frames analyzed by the WASM kernel.
Co-Authored-By: claude-flow <ruv@ruv.net>
runProcess() ends the child's stdin with no error handler. A child that
never reads stdin (git check-ignore) can exit and close the pipe before the
write flushes; the unhandled EPIPE then crashes `agent.mjs validate`. This
failed harness/ruview (node 22) on PR #2062 and reproduces 12/12 locally
under CPU load (0/12 after this change). The command's real outcome is
still reported by the 'close' handler; this mirrors the existing handler in
harness/ruview/src/process-runner.js.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
The flash flow's boot-log capture decoded serial lines and wrote text to a
stdout that the scrubbed child env leaves on cp1252. The ESP32-C6 release
firmware logs U+2192, so the capture died mid-boot (bootLog.captured=false);
an earlier abort would have hidden the CSI evidence and reported a good
flash as unvalidated. Write raw bytes instead. Regression test forces
PYTHONIOENCODING=cp1252 through a stand-in serial module (fails without
the fix). ADR-373 Amendment 2 records the real C6 flash evidence.
Co-Authored-By: claude-flow <ruv@ruv.net>
Findings from a hardware run of this branch against an RTL8721Dx board
(PL2303GC) and an ESP32-C6 (CP210x) on Windows 11:
- esp32 capture decodes Realtek RAC1 (ADR-323, CRC-verified, 8/16-bit
tones) and RHB1 heartbeats, ADR-110 sync and ADR-081 mesh packets, and
reports every CSI shape a node sends.
- Capture fails (no_decodable_packets / heartbeat_only) instead of
returning ok:true + MEASURED when nothing decodes; names
heartbeat-only senders.
- --analyze runs live single-antenna CSI through @ruvnet/ruview-kernel at
the measured frame rate with a bounded buffer.
- monitor opens the port with DTR/RTS deasserted (no node reboot), takes
--baud, and explains the C6/S3 USB-Serial/JTAG console.
- devices classifies Prolific 067B:23A3 as a Realtek board.
- Windows: owner-only ACL on the hosts file (fails closed); symlink test
skips only where Windows refuses symlinks.
- kernel build names uninitialized submodules when cargo fails.
- Parser ~3.2x faster (MEASURED, 0.42-0.52M -> 1.33-1.67M packets/s).
Co-Authored-By: claude-flow <ruv@ruv.net>
CI fixes (both failures on PR #2062 were in this branch):
- harness/ruview: remove `2.11.0` literals from training remedies; the
ADR-265 D3 version-literal gate rejects them.
- ruview-kernel workflow: install wasm32 for the toolchain pinned by
v2/rust-toolchain (the action only added it to `stable`); build.mjs now
fails fast with the exact `rustup target add` remedy.
Kernel optimization (ADR-368 amendment):
- New `analyze_flat` / `session_push_flat` ops via `call_f64`: WASM
`rvk_call_f64` (alignment-agnostic bytes) and napi `callF64(Float64Array)`.
`analyze()`/`session.push()` pack uniform frames automatically; irregular
input keeps the JSON path and its per-frame errors. `analyzeFlat()` is the
zero-copy entry. Shared `Session::step` makes both paths byte-identical on
identical f64 inputs (Rust test), and the binary path avoids serde_json's
non-correctly-rounded float parsing.
- MEASURED (`ruview-kernel bench --seconds 120`, one Linux x64 host):
2.82x (wasm) and 2.45x (napi) faster than the JSON transport.
- Profiled remaining cost (examples/profile_stages.rs): heart-rate ACF in
wifi-densepose-vitals dominates; left for a separate, output-reviewed
change. Rejected WASM +simd128 and opt-level=s after three measured runs
(bench/wasm-variants.mjs).
Device e2e (ADR-373 amendment):
- test/e2e/devices.e2e.mjs + `npm run test:e2e:devices` + new workflow
ruview-device-e2e.yml: pty-emulated MR60BHA2 and RPLIDAR through real
pyserial, a UDP ESP32 node, and the real iPhone relay, via the CLI.
- mmWave auto-detect keeps probe frames and reads only the remainder;
iPhone LiDAR gains max_frames and returns immediately when refused.
Suite wall time 19.6 s -> 5.6 s (MEASURED, dev container).
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
@ruvnet/ruview 0.7.0 gives CLI, MCP, and SDK access to RuView sensing
hardware on the machine it is attached to, and to other machines over SSH.
Devices (ADR-373, src/devices/):
- ruview_devices_scan: pyserial port list classified by USB VID:PID
(Espressif native USB, CP210x, CH34x, FTDI) into ESP32 / mmWave / RPLIDAR
roles with the command that confirms each.
- ruview_esp32_capture: receive-only UDP listener parsing ADR-018 CSI
frames, vitals, and the other 0xC511000x packets per node: rate,
sequence loss, RSSI, subcarriers, frequency, device vitals.
- ruview_mmwave_read: MR60BHA2 (60 GHz) and LD2410 (24 GHz) parsers ported
byte-for-byte from firmware mmwave_sensor.c; baud/frame auto-detect.
- ruview_lidar_read: RPLIDAR SCAN over serial (start/stop, DTR best-effort)
and the iPhone LiDAR relay as a WebSocket client (token only via
RUVIEW_LIDAR_TOKEN; statistics only, never raw depth).
- Dependency-free serial via a fixed pyserial pump (argv-only, bounded,
hex output immune to secret redaction). Schema-level port pattern
validation added to the policy validator.
- All device tools require the new `device-access` MCP grant.
Remote hosts (ADR-374, src/remote.js):
- ~/.config/ruview/hosts.json (0600), CLI-only `hosts add`; strict
name/target validation against ssh option and shell injection.
- `ssh -o BatchMode=yes -o StrictHostKeyChecking=yes -- <target>` running a
version-pinned `ruview call <tool> --read-only`, every token single-quoted;
read-only enforced locally and remotely; no mutations, credentialed reads,
or host hopping. MCP needs `remote-host` plus the remote tool's grant.
Also: device-access skill, doctor `devices`/`remote` groups, SDK
`devices.*`/`hosts.*` with types, generic `ruview call` verb, guidance
capability, 320 KiB package budget, CI smoke expects 22 tools.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
@ruvnet/ruview 0.6.0 turns the harness into a complete npm operator
surface: one policy-checked tool registry behind the CLI, the MCP server,
and a new typed SDK.
- SDK (ADR-369): `@ruvnet/ruview/sdk` createRuView() with doctor,
firmware.{ports,plan,flash,monitor}, training.{plan,run,gate,calibrate},
kernel.{selfTest,load}, guidance/claimCheck/verify/spaces; strict mode
throws RuViewError; TypeScript declarations in src/sdk.d.ts.
- Firmware (ADR-370): cross-platform `python -m esptool` flashing replaces
the Windows-only stub. SHA256SUMS verification (mismatch always fatal),
bundle containment, variant/partition checks, argv-only command, chip_id
preflight that refuses a mismatched chip, boot-log capture;
hardwareValidated only when CSI callbacks appear without a panic.
Read-only ruview_firmware_plan / ruview_firmware_ports.
- Training (ADR-371): ruview_train / ruview_train_plan run pose-smoke,
pose (MM-Fi), or room training from the trusted checkout with in-repo
paths only, logs/checkpoints under v2/target/ruview-train, and libtorch
remedies. ruview_train_gate enforces mean-pose baseline, leakage-free
splits, subject/time disjointness, and evidence tagging.
- Doctor (ADR-372): structured pass/warn/fail/skip checks with remedies
across runtime, integrity, hosts, repo, Rust, Python/esptool, firmware
bundles, serial, sensing server, and the compute kernel; --json/--group;
hardware probe and network checks are CLI/SDK-only.
- Plan tools cannot take `confirm`; acting tools keep grant+confirm over
MCP. Policy files and TOOL_POLICY are drift-tested.
- Unpacked budget 160 KiB → 224 KiB in both npm workflows; CI tarball smoke
now checks the SDK and the training gate.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
Adds a portable RuView compute kernel for JavaScript, built from the Rust
source of truth and wired into the @ruvnet/ruview metaharness.
Rust
- v2/crates/ruview-kernel: bounded single-function JSON ABI
(call(op, json) -> envelope) over the ADR-021 wifi-densepose-vitals
pipeline: streaming sessions, batch analyze, deterministic SplitMix64
synthetic CSI (SYNTHETIC), bounded session registry, strict input
validation (deny_unknown_fields, width/finite/magnitude checks, size and
count limits). wasm32-only zero-import C ABI (rvk_alloc/free/call).
- v2/crates/ruview-kernel-napi: napi-rs transport exposing only
abiVersion()/call(); standalone workspace (excluded from v2) so napi never
enters `cargo test --workspace`; panic=unwind + catch_unwind.
npm (harness/ruview-kernel, zero runtime deps)
- loadKernel({backend: wasm|napi|auto}); WASM default (no host authority),
napi never silently falls back, auto reports fallbackReason.
- SHA256SUMS integrity verification; mismatch is always fatal; loader
rejects any WASM module that imports host functions.
- Browser-safe ./wasm entry; CLI doctor/info/selftest/parity/synth/
analyze/bench; build.mjs builds artifacts from source (no shell).
- Unit, security, and e2e tests (CLI round trip, cross-backend parity).
Metaharness
- New read-only tool ruview_kernel_selftest / `ruview kernel`: imports the
fixed optional package, fails closed with kernel_not_installed; policy,
claims, MCP policy, guidance capability, and manifest updated.
CI: .github/workflows/ruview-kernel-npm.yml builds both transports on
Linux/macOS/Windows, requires napi in tests, runs parity, verifies
artifacts, packs, and runs the cross-package e2e. No publish step.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
Verified on a physical ESP32-C6 (4 MB, esp32c6 defaults, ESP-IDF v5.4):
- App image: 1,085,424 B on main, 1,042,368 B with this change (43,056 B smaller).
- The 'Failed to find FATFS partition storage' error seen on every 4 MB C6 boot is gone.
- Wi-Fi join, CSI init and CSI UDP delivery to the host were confirmed.
CI: 35/35 green.
The new schema constant was defined and asserted as a raw string literal
directly in main.rs, which the semconv registry test scans and requires
every "ruview.*" literal there to be a registered attribute/event. This
isn't a tracing event, though -- it's a wire-schema tag structurally
identical to CALIBRATION_MODEL_RECEIPT_SCHEMA, which already lives in
field_bridge.rs specifically to stay out of that scan. Move the new
constant there and reference it by identifier from main.rs, matching the
existing precedent instead of hand-editing the generated semconv registry.
Co-Authored-By: claude-flow <ruv@ruv.net>
calibration_status unconditionally overwrote status with "none" whenever
active was false, which also erased the legitimate "expired" status for
a calibration that ran long ago and has since expired -- losing the
distinction between "never calibrated" and "was calibrated, now expired".
Fixes the pre-existing failure in
calibration_expiry_tests::long_running_expiry_is_not_reported_active_for_runtime_or_bootstrap,
called out as a known, unrelated gap in PR #1936.
Co-Authored-By: claude-flow <ruv@ruv.net>