1587 Commits
Author SHA1 Message Date
rUv fefb0f918e Merge pull request #2289 from ruvnet/integrate/release-20261017
chore: integrate open PRs, rufield b74bc03, STBC edge fix (#2286)
v0.8.15-esp32 v3370
2026-10-22 11:08:33 -04:00
ruv 73750f8b3d docs(firmware): record ESP32-C6 0.8.15 validation 2026-10-21 15:14:21 -04:00
ruv f03d5ac063 docs(release): keep firmware notes after unreleased changelog 2026-10-21 15:06:32 -04:00
ruv b3d5885c26 fix(firmware): harden config rollback and prepare 0.8.15 release 2026-10-21 14:51:34 -04:00
ruv a9b46055db merge: integrate whole-request configuration validation 2026-10-21 13:18:57 -04:00
ruv da177d60f9 merge: integrate reviewed release changes and dependency remediation 2026-10-21 13:18:14 -04:00
Dan Alec 1e40a0fab0 fix(firmware): validate the whole config request before writing anything
The configuration API could leave a REJECTED request half applied, and a
second request could replace the known-good recovery bank with settings
nobody had proved. Both are fixed here, together with the validation
limits that the API and the boot loader did not share.

1. A rejected request changed the node.

   nvs_set_* writes through to flash in the setter itself. In ESP-IDF 5.4
   nvs_open() hands back an NVSHandleSimple, whose setters call
   Storage::writeItem() straight away and whose commit() only returns
   ESP_OK -- closing the handle does NOT discard what was written. The
   handler validated and wrote in the same pass, so
   {"wifi_ssid":"wrong-network","edge_tier":"invalid"} answered 400 with
   the SSID already changed and no recovery bank armed: the next reboot
   could leave the node unreachable with no way back.

   The request is now validated in full before anything is written, so a
   body with one bad field changes nothing at all. The recovery bank is
   built from the CURRENT values and persisted to the trial namespace
   BEFORE the first live write, so the node can always get back. A write
   that fails part way through restores the whole bank and disarms the
   trial instead of leaving a mix of old and new values.

2. A second request could bank unproven settings as the fallback.

   s_trial_armed is only set at boot, so the three seconds between
   accepting a change and rebooting onto it were a window in which a
   second request passed the guard and banked the FIRST request's
   unproven settings as the recovery bank -- a revert would then restore
   settings that were never shown to work.

   A separate pending-reboot state closes the window and answers 409
   while a change is in flight. It is deliberately not s_trial_armed: an
   association event on the OLD running configuration clears
   s_trial_armed and erases the trial bank, which would unlock the API
   exactly when it must stay locked.

3. The API accepted settings the boot loader silently ignored.

   The API table and the loader now share one definition of every
   accepted range, in config_limits.h, so they cannot drift again. The
   ranges that had drifted are tightened to what the loader actually
   applies:

     vital_window       8..4096   ->  32..256
     top_k_count        1..64     ->  1..32
     channel_hop_count  1..14     ->  1..6     (NVS_CFG_HOP_MAX, the size
                                                   of the channel list)

   A value the API accepts but the loader ignores is stored, reported as
   applied, and then replaced by the default at boot -- it looks applied
   and is not.

The value rules move to config_rules.c, free of ESP-IDF and of cJSON, so
the host test in test/test_config_rules.c pins them against the same
translation unit the firmware links. The fuzzer now takes its bounds
from the same header instead of repeating them as literals. A number
that cannot be represented is rejected instead of being truncated into a
value that looks valid: the old code cast before it range-checked, so
1e300 became an arbitrary integer.

The acceptance criteria for this change are the four checks in the
review: a mixed valid/invalid payload must answer 400 and leave NVS
untouched, a second change within three seconds must answer 409, the
out-of-range values must be rejected before any write, and a failure
while persisting the recovery bank must leave the live configuration
alone.

Verified: make run_config_rules (host, no device) passes; the rest of
the host test suite is unchanged.
2026-10-21 11:36:55 -03:00
Dan Alec 6d4ca03bb6 fix(ci): make the Rust audit gate load the reviewed ignore list
The gating "Rust Dependency Audit" job ran

  cargo audit --file v2/Cargo.lock --json

from the repository root. cargo-audit 0.22 resolves its config file as
./.cargo/audit.toml relative to the CURRENT WORKING DIRECTORY, never
relative to the --file lockfile it audits, and it has no flag to point at a
config path. From the repository root that file does not exist, so the job
audited with cargo-audit's defaults and applied NONE of the reviewed ignores
in v2/.cargo/audit.toml — every run of the job reports "ignore":[] in its own
JSON artifact, which is the machine-readable proof that the policy file was
never read.

Run the audit inside the v2 workspace so that v2/.cargo/audit.toml is loaded,
and record the discovery rule in the file header so the next reader does not
move the policy file back out of reach.

Also record the three zip advisories, with the reachability proof in the file:

  RUSTSEC-2026-0367  SUID/SGID bits survive extraction
  RUSTSEC-2026-0368  extract() creates a symlink pointing outside the destination
  RUSTSEC-2026-0369  extract() applies permissions through such a symlink

All three are fixed only in zip >= 9.0.0, and no published release of the
parents moves there — ndarray-npy 0.10.0 -> zip ^6, candle-core 0.11.0 ->
zip ^8.6, tch/torch-sys 0.26.0 -> zip ^0.6 — so the bump is not reachable
today.

They are also not reachable from this workspace. All three live in the
extraction path (ZipArchive::extract / ZipStreamReader::extract) and nothing
in the resolved graph calls either one: ndarray-npy and candle-core use
ZipArchive::new plus by_name / by_index and ZipWriter, and torch-sys
extracts the libtorch archive with its own per-entry loop (archive.by_index
+ fs::File::create + io::copy), so it never calls extract(), never creates a
symlink and never applies the archive's Unix permission bits. The single
archive it does extract is the pinned libtorch build downloaded over HTTPS
from download.pytorch.org.

proc-macro-error2 (RUSTSEC-2026-0173) is added next to the existing
proc-macro-error entry for the same reason: unmaintained build-time
proc-macro, no runtime exposure.

Verified: cargo audit --file Cargo.lock from v2/ reports 0 vulnerabilities and
exits 0; the policy file now loads 30 documented ignores.
2026-10-21 08:54:36 -03:00
Dan Alec 3b1479b9a6 fix(deps): bump wasmtime, rustls-webpki and the webpki roots past the 2026 advisories
cargo-audit reports five advisories against the checked-in v2/Cargo.lock
that are cleared by patch-level bumps:

  wasmtime 36.0.18 -> 36.0.19       RUSTSEC-2026-0379, RUSTSEC-2026-0380
  rustls-webpki 0.103.15 -> 0.103.16 RUSTSEC-2026-0382
  webpki-roots 1.0.7 -> 1.0.10      RUSTSEC-2026-0381
  webpki-root-certs 1.0.6 -> 1.0.10 RUSTSEC-2026-0383

The two wasmtime advisories are Cranelift miscompilations on aarch64: one
can jump to the wrong target on a large `br_table`, the other overflows
silently while bit-packing. The three webpki advisories are crypto-failures
where a distrusted emSign intermediate — whose private key is public — could
be used to build a certificate chain the verifier accepts; the fix removes the
emSign ECC Root CA - G3 anchor that made those paths reachable.

These crates are transitive, so the requirement is only enforceable through
the lockfile that the gating Security Scanning job audits. v2/Cargo.toml now
records the floors next to the existing quinn-proto / rustls / wasmtime ones,
so a plain `cargo update` cannot walk them backwards without explanation.

Verified: cargo audit --file Cargo.lock reports 0 vulnerabilities and exits 0
with the reviewed policy loaded; cargo check --workspace --no-default-features
is clean.
2026-10-21 08:54:36 -03:00
ruv 0bdae80ca7 docs(validation): record attached C6 backup limitations 2026-10-20 12:47:16 -04:00
ruv eccf381ac0 fix(firmware): preserve STBC DSP input and bound CSI probe stalls 2026-10-20 12:44:47 -04:00
ruv bf715c9cf0 fix(provision): avoid backslash in f-string expression (SyntaxError on Python <3.12)
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-17 10:51:08 -04:00
ruv bf46d113f3 fix(firmware): analyse first 128 entries of STBC CSI frames instead of dropping them (#2286)
Reporter MEASURED on ESP32-S3: vitals/feature packets 2 -> 59 per 60 s.
Not reproducible on C6 (EDGE_MAX_SUBCARRIERS=256); C6 build verified only.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-17 09:16:41 -04:00
ruv fde7514128 Merge branch 'pr-2267' into integrate/release-20261017 2026-10-17 08:58:35 -04:00
ruv 1e3ffc1bce Merge branch 'pr-2278' into integrate/release-20261017 2026-10-17 08:58:35 -04:00
ruv 001f36853a Merge branch 'pr-2280' into integrate/release-20261017 2026-10-17 08:58:34 -04:00
ruv add14700f6 Merge branch 'pr-2281' into integrate/release-20261017 2026-10-17 08:58:34 -04:00
ruv 8e446ce5ca chore: drop contributor-branch-only workflow from #2269 (UI fix kept)
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-17 08:58:33 -04:00
ruv 12fdb007f6 Merge branch 'pr-2269' into integrate/release-20261017 2026-10-17 08:58:20 -04:00
ruv 802c92f929 Merge branch 'pr-2261' into integrate/release-20261017 2026-10-17 08:58:20 -04:00
ruv e2dac179e6 Merge branch 'pr-2263' into integrate/release-20261017 2026-10-17 08:58:19 -04:00
ruv a9f6ec96b5 Merge branch 'pr-2262' into integrate/release-20261017 2026-10-17 08:58:19 -04:00
ruv 03ac12dfe4 Merge branch 'pr-2283' into integrate/release-20261017 2026-10-17 08:58:18 -04:00
github-actions[bot] 9b8fe5c3a1 chore: update vendor submodules to latest upstream 2026-10-17 12:18:34 +00:00
Brian Richterandclaude-flow ff284f77fb fix(dashboard): exclude Playwright specs from Vitest
npm test (vitest run) collected tests/a11y.spec.ts, a Playwright spec,
and failed. Exclude tests/** from Vitest and pass when no unit tests
exist; Playwright specs still run via npm run test:e2e / test:a11y.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-10-16 17:45:38 -07:00
Dan Alec d45a04381d fix(ci): make the container image pipelines version-safe and fork-safe
nvsim-server -> ghcr.io was failing for three separate reasons, each one
masked by the previous. The Dockerfile carried two literals that had drifted
from their source of truth, and both workflows hard-coded the `ruvnet`
registry namespace that only the upstream can write to.

nvsim-server/Dockerfile

  1. Stub workspace version. The Dockerfile builds a throwaway workspace
     containing only nvsim and nvsim-server. That stub hard-coded
     version = "0.3.0" (PR #440); the real workspace is now 0.3.1 (PR
     #1439) and nvsim-server requires nvsim = { path = "../nvsim",
     version = "0.3.1" }. The build died with "failed to select a version
     for the requirement nvsim". The stub now extracts the version from
     the real Cargo.toml and fails loudly if it cannot read one.

  2. Builder toolchain. FROM was pinned to rust:1.81 while
     rust-toolchain.toml declares channel = "1.89", so async-compression
     0.4.42 and compression-codecs 0.4.38 (both MSRV 1.83, reached through
     tower-http's compression-gzip feature) aborted the release build with
     "rustc 1.81.0 is not supported". The builder is now 1.89.

     Rather than re-pinning another literal that will drift again, the
     declared channel travels into the build context and is compared
     against the effective rustc before compiling. Bumping one without the
     other now fails the build loudly instead of resurfacing as an MSRV
     error three steps later. It is copied under a separate name on
     purpose: letting rustup resolve it would silently download a
     toolchain and hide the drift.

nvsim-server-docker.yml

  The image name was hard-coded to ghcr.io/ruvnet/nvsim-server, so the push
  failed with `denied: The requested installation does not exist` outside
  the upstream namespace. It now derives from github.repository_owner,
  which resolves to ruvnet for the upstream and to the fork owner for
  every fork, so the workflow keeps working unchanged here.

  The smoke test also pulled :sha-<short> but ran :latest. `latest` is
  emitted with `enable={{is_default_branch}}`, so a tag build never had
  it, and the `|| latest` fallback masked a failed push by testing a stale
  image. It now pulls and runs the sha tag, which type=sha emits on every
  event.

sensing-server-docker.yml

  Same namespace problem: the image was fixed to ruvnet/wifi-densepose on
  both Docker Hub and ghcr.io, which a fork cannot push to. Publishing
  under github.repository_owner fixes that.

  docker login was also unconditional, so a repository without
  DOCKERHUB_USERNAME/DOCKERHUB_TOKEN died at "Must provide --username with
  --password-stdin" before building anything. ghcr.io needs no secret, so
  it stays the always-on target and Docker Hub is added only when both
  credentials are present.

Verified: cargo check -p nvsim-server --release under rustc 1.89.0
compiles nvsim v0.3.1, nvsim-server v0.3.1 and async-compression 0.4.42;
the resulting image builds, pushes and its /api/health reports
{"nvsim_version":"0.3.1"}. CI on this branch runs the full pipeline
green.
2026-10-16 16:06:56 -03:00
Dan Alec 8e846a9a36 fix: security floors, dashboard a11y violations, README visual assets
Three independent fixes that each resolve a red job on main.

Security floors (v2/Cargo.lock, v2/Cargo.toml)
  cargo audit reported 10 advisories across three transitive crates:
  quinn-proto 0.11.15, rustls 0.23.45 and wasmtime 36.0.16. Bump to
  0.11.18 / 0.23.46 / 36.0.18. All three arrive through
  midstreamer-quic = 0.3, so only the lockfile enforces them; v2/Cargo.toml
  now records the floors so a plain `cargo update` cannot walk them back
  without explanation.

Dashboard accessibility
  18 axe failures (3 browsers x 6 views): 58 role=switch toggles with no
  accessible name, two unlabelled range inputs, low-contrast timestamp /
  chip / badge text, and two scrollable regions with no keyboard access.

  The welcome modal also left the accessibility tree only by fading to
  opacity 0. A closed state at opacity 0 still gets scanned, and axe
  composes the semi-transparent text over the darkened backdrop, which
  produced a false color-contrast failure (ratio 1.55 reported where the
  real ratio was ~17:1). It now leaves via visibility, and
  connectedCallback exposes a `ready` attribute once its async storage read
  settles so the smoke test stops sampling the modal mid-transition.

README visual asset validation
  The workflow regenerates the 20 SVGs and requires byte-identical output.
  scripts/render-readme-visuals.py stripped unused CSS rules but left the
  trailing newline behind, so 14 removed rules collapsed into 16 blank
  lines. The producer now consumes the orphan newline and the assets are
  regenerated (94,576 -> 94,027 bytes).

Verified locally: cargo audit 0.22.2 exits 0, cargo check --workspace
--no-default-features --all-targets is clean, the README renderer is
idempotent with verify passing, and the axe suite is 18/18 across chromium,
firefox and webkit on six consecutive runs.
2026-10-16 16:06:38 -03:00
Joe 6dfd19ea28 Merge contrib/remote-config into contrib/link-reassoc 2026-10-16 11:34:00 -04:00
Joe 852f6fc2e9 Merge upstream main into contrib/remote-config 2026-10-16 11:30:57 -04:00
JoeandClaude Opus 5.5 13d62b98c0 feat(firmware): POST /link/reassoc -- rejoin the AP when the uplink rate collapses
An ESP32-C6 node whose uplink PHY rate drops to 1-2 Mbps stays there for
24-425 min without help, and that one slow node fills the channel for every
node on it. A fresh association restarts rate control and restores the rate.

POST /link/reassoc (same PSK auth as /config) replies, then drops the link
200 ms later; the existing STA_DISCONNECTED path reconnects with its usual
backoff. A 60 s floor returns 429 to a runaway caller. GET /config reports
reassoc_count and reassoc_last_s_ago. Detection stays with the caller.

Measured on a 9-node C6 fleet, 10-09..10-16: 15 of 15 requests that reached
the node restored its rate within 3 minutes.

Stacked on #1827 (remote configuration).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:34:45 -04:00
Kolomiets94 07af33a070 ci: validate desktop React UI accessibility change 2026-10-14 10:23:26 -07:00
Kolomiets94 50395d69d6 fix(ui): make interactive node cards keyboard accessible 2026-10-14 10:18:57 -07:00
rUv 285aea8f2d feat(sensing): add W1700K advisory RF diagnostics
Source tree verified against ruOS tested commit fa6b1bdf92e7959b8500d42e2a5c620e207e81b0. Synthetic pipeline validation only; hardware validation pending.
2026-10-14 04:13:50 -07:00
VG31OP 39e2e6ab40 test(scripts): add unit test suite and configurable manifest/root support for fix-marker regression checker 2026-10-13 19:21:09 +05:30
VG31OP 9d7db3290a fix(cli): execute jest javascript entrypoint directly and add config unit tests 2026-10-13 18:25:25 +05:30
VG31OP ddbe5645e9 fix(firmware): sanitize Windows reserved device names in state paths
On Windows, files whose basenames match DOS reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) cannot be created, causing provision.py to fail with FileNotFoundError whenever Windows COM ports (e.g. COM7) were used. This also caused 16 test failures in the esp32-csi-node test suite on Windows.

- Prefix reserved device names with '_' in _state_path_for.
- Add legacy unprefixed path fallback in load_state and load_board_state for backward compatibility.
- Add regression tests in test_provision_state.py for reserved device prefixing.
- Add harness/homecore/** text=auto eol=lf in .gitattributes to maintain byte-for-byte provenance and prevent manifest.sha256 mismatch on Windows checkouts.

Validation: All 73 esp32-csi-node tests pass on Windows, and all 120 harness/ruview security tests, package manifest, and brain verification pass.
2026-10-13 17:57:38 +05:30
rUv a1ca023ee8 Animate anatomical pose with opposing gait and perspective depth v3263 v3261 v3262 2026-10-12 21:04:27 -07:00
rUv 2b3138d9d0 Replace README stick figure with shaded anatomical SVG human 2026-10-12 20:58:45 -07:00
rUv 1c678a71fa Improve README spatial visuals with distinct sensing motion and typography v3259 2026-10-12 20:52:23 -07:00
rUv 6699a2fc44 Match README spatial SVGs to RuView header palette and add RF wave motion v3258 2026-10-12 20:33:57 -07:00
rUv 964377765a Improve README SVG spatial motion and typography (#2257) 2026-10-12 20:28:12 -07:00
rUv 5cfbb8d98d docs: restore original GIF at top of README (#2252) v3220 2026-10-12 14:30:42 -07:00
rUv 0f95193190 Merge pull request #2248 from ruvnet/codex/toolkit-release-20261012
fix(toolkit): integrate capture/device corrections for 0.9.2
v3216 ruview-harness-v0.9.2 v3215
2026-10-12 09:54:03 -07:00
rUv 15fd5eebdc Merge pull request #2246 from ruvnet/codex/sensing-release-20261012
fix(calibration): finish failed sessions and reconcile decision diagnostics
v3211
2026-10-12 09:27:10 -07:00
ruv b064acf57c test(harness): cover Node 20 WebSocket capability boundary 2026-10-12 12:20:40 -04:00
rUv 4413491370 Merge pull request #2247 from ruvnet/codex/firmware-release-20261012
fix(firmware): correct identity and secure OTA provisioning for 0.8.14
v0.8.14-esp32
2026-10-12 09:17:31 -07:00
ruv b265454b93 fix(remote): isolate Windows PowerShell module search paths 2026-10-12 12:09:26 -04:00
ruv 99d9851173 test(devices): align end-to-end medians with even-sample correction 2026-10-12 12:01:15 -04:00
ruv 720cfd7c8a fix(remote): replace explicit Windows ACL grants with current-user access 2026-10-12 11:56:13 -04:00
ruv 2c1222789d Merge remote-tracking branch 'origin/main' into codex/toolkit-release-20261012 2026-10-12 11:49:52 -04:00