yggvault ratatoskr-space connected via regular web
Color theme
also available via yggdrasil mesh http://[203:b338:2a84:a18f:986:47ae:1a4:d8d3]/pkg/ruview/v1362
vault / ruview / v1362

ruview @ v1362

integrity

size
60.1 MiB
downloaded
last checked
source https://github.com/ruvnet/RuView · available · github

release notes

Automated release from CI pipeline

Changes: ADR-125 APPLE-FABRIC: RuView <-> Apple Home native HAP bridge (e2e on real C6) (#797)

Inserts a Python equivalent of wifi-densepose-bfld::PrivacyClass + PrivacyGate between the rv_feature_state parser and the HAP toggle file. ADR-125 §2.1.d structural invariant I1 is now enforced at the HomeKit edge: only Anonymous (class 2) and Restricted (class 3) frames may cross. Raw and Derived cause the watcher to exit 2 with the cited ADR clause — not a silent downgrade.

Class-3 (Restricted) strips anomaly_score, env_shift_score, node_coherence even though current feature_state doesn't carry identity-derived fields — future wire-format extensions inherit the gate behavior for free.

Operator-facing semantic naming follows ADR-125 §2.1.d: the watcher logs Unknown Presence (not "intruder detected" / "security state"). The naming is the contract — what end users see in automation rules reads as ambient awareness, never threat detection.

Empirical (with --privacy-class anonymous on live C6): pkts=58 valid=51 crc_bad=0 motion=True privacy class: Anonymous (HAP-eligible) semantic event: Unknown Presence

Refuse path validated: $ ~/hap-venv/bin/python c6-presence-watcher.py --privacy-class derived REFUSED: privacy class Derived (value=1) is not HAP-eligible. ADR-125 §2.1.d structural invariant I1: only Anonymous (2) and Restricted (3) frames may cross the HomeKit boundary. $ echo $? 2

Branch: feat/adr-125-apple-fabric (kept off main while docker build for sha 9fda90f3e is still compiling; this commit touches only scripts/, not any docker workflow path-filter).

Refs ADR-125 §2.1.d, ADR-118 §2.1/§2.2.

Co-Authored-By: claude-flow ruv@ruv.net

Pre-merge checklist item 5. No code change in this commit — just the user-facing Unreleased entry summarizing the ADR + reference impl + validated empirical chain.

Co-Authored-By: claude-flow ruv@ruv.net

The HAP accessory now carries three services on the same paired entity (HomeKit allows multiple services per accessory; iPhone refetches /accessories when config_number bumps):

New JSON IPC contract /tmp/ruview-state.json between watcher and HAP daemon:

{ "motion": bool, "occupancy": bool, "anomaly_ts": float, "ts": float }

Atomic writes (tmp + rename). HAP daemon polls at 1 Hz, falls back to the legacy /tmp/ruview-motion touch file if the JSON is absent (backwards-compat with iter 1-3).

Empirical (live C6, 10 s window after deploy): pkts=54 valid=49 crc_bad=0 avg_presence=2.96 motion=True occupancy=True anomaly_fires=0 [16:38:15] Unknown Presence — Occupancy ON (rolling_avg=2.79)

Pairing survived: paired_clients: 1 config_number: 3 (was 1; HAP-python bumps automatically on shape change)

Tier 1 #1 (multi-characteristic) of the Tier 1+2 sprint. Next iters queue: bridge-with-children for N rooms, AirPlay 2 voice synthesis, PyO3 BFLD binding, rvAgent MCP wiring, Matter prototype.

Refs ADR-125 §2.1.c (bridge topology), §2.1.d (semantic events), ADR-118.

Co-Authored-By: claude-flow ruv@ruv.net

scripts/ruview-sensing-server.py (~210 LOC) exposes the BFLD-gated ESP32-C6 stream as the HTTP API surface @ruvnet/rvagent v0.1.0 (ADR-124, npm) expects. Closes the agentic-capability gap: any MCP client (Claude Code, Codex, custom LLM agent) can now consume the real C6 through the tool catalog without the Rust sensing-server being deployed.

Endpoints (mirrors tools/ruview-mcp/src/tools/*.ts):

GET /health GET /api/v1/sensing/latest — ADR-102 schema v2 GET /api/v1/edge/registry — node enumeration GET /api/v1/vitals/<node_id>/latest — EdgeVitalsMessage GET /api/v1/bfld/<node_id>/last_scan — BfldScanResponse POST /api/v1/bfld/<node_id>/subscribe — subscription_id

c6-presence-watcher.py now writes a companion /tmp/ruview-last- feature.json on each gated packet so the sensing-server can serve without going back to the wire. Atomic tmp+rename. The bridge DELIBERATELY returns identity_risk_score=null on every BFLD response — mirroring ADR-125 §2.1.d at the HTTP boundary even though the rvagent schema's slot is nullable.

Live smoke test against the real C6 (node_id=12):

$ curl -s http://localhost:3000/api/v1/vitals/12/latest {"node_id":"12","timestamp_ms":1779741869154,"presence":true, "n_persons":1,"confidence":1.0,"breathing_rate_bpm":18.75, "heartrate_bpm":40.0,"motion":1.0}

$ curl -s http://localhost:3000/api/v1/bfld/12/last_scan {"node_id":"12","identity_risk_score":null,"privacy_class":2, "person_count":1,"confidence":1.0,"presence":true, "timestamp_ns":1779741869154607104}

$ curl -s -X POST 'http://localhost:3000/api/v1/bfld/12/subscribe?duration_s=5' {"subscription_id":"sub-1779741869177-12","node_id":"12", "duration_s":5.0,"endpoint_hint":"poll GET ..."}

Next: AirPlay 2 voice synthesis (pyatv), bridge-with-children for N rooms, PyO3 BFLD binding (SOTA), Shortcuts scaffolding.

Refs ADR-124 (@ruvnet/rvagent contract), ADR-125 §2.1.d, ADR-118.

Co-Authored-By: claude-flow ruv@ruv.net

scripts/ruview-hap-bridge.py (~170 LOC) implements the ADR-125 §2.1.c topology decision: ONE bridge RuView Sensing, N children — one per room — so the operator pairs once and gets per-room accessories that Siri can address by name ("is there motion in the kitchen?").

State per room comes from /tmp/ruview-state..json. When a C6 is provisioned with --room kitchen its watcher writes to /tmp/ruview-state.kitchen.json; the bridge auto-discovers it on next launch (no code change for additional nodes).

Legacy /tmp/ruview-state.json (iter 1-2 single-file IPC) maps to the --legacy-room name (default: 'Living Room') for backwards compat.

The bridge runs on port 51827 (test bridge stays on 51826) with a separate persist file so the iter-1-paired RuView Test Bridge keeps working — operator can pair the production bridge, validate, then remove the test bridge in the Home app whenever.

Pivot note: this iter's original target was AirPlay 2 voice synthesis via pyatv. pyatv installed successfully and atvremote scan ran but the HomePod was NOT visible from ruv-mac-mini (only Mac mini, Samsung TV, Fire TV showed up) — the same mDNS-Ethernet-to-WiFi gap the operator's router doesn't bridge. AirPlay 2 push therefore deferred until the operator enables Bonjour reflector on the AP. Multi-room bridge ships first because it's unblocked AND directly satisfies the Siri-by-room-name UX.

Empirical (deployed on ruv-mac-mini, prod_bridge_pid=64094): $ dns-sd -B _hap._tcp local. Add 3 15 local. _hap._tcp. RuView Test Bridge 224DF9 Add 3 15 local. _hap._tcp. RuView Sensing 0B4FC4 Add 3 15 local. _hap._tcp. Main Floor (Ecobee)

[bridge] child accessory ready: 'Living Room' <- /tmp/ruview-state.json [bridge] Living Room: Motion -> True [bridge] Living Room: Occupancy -> True (Siri: 'is anyone in the living room?')

Setup code for pairing the new bridge: 629-88-678.

Tier 1 §2.1.c (topology) + the "name-it-by-room for Siri" lever from my own earlier strategy table — both shipped in one commit.

Refs ADR-125 §2.1.c.

Co-Authored-By: claude-flow ruv@ruv.net

GET /api/v1/semantic-events/<node_id>/latest exposes the three ADR-125 §2.1.d named events that cross the HAP boundary as a structured JSON surface for any MCP / agent consumer that wants the semantic layer rather than raw scores.

Response shape:

{ "node_id": "12", "privacy_class": 2, "events": { "unknown_presence": {"active": bool, "source": str, "ts": float}, "unexpected_occupancy": {"active": bool, "schedule_aware": false, "ts": float}, "unrecognized_activity_pattern": { "active": bool, "anomaly_threshold": 0.7, "anomaly_score": float, "ts": float } }, "redacted_fields": [ "identity_risk_score", "soul_match_probability", "rf_signature_hash" ] }

Live response from real C6 (node_id=12):

{ "unknown_presence": {"active": true, ...}, "unexpected_occupancy": {"active": true, "schedule_aware": false, ...}, "unrecognized_activity_pattern": {"active": false, "anomaly_score": 0.0, ...} }

The redacted_fields array is intentional — it tells consumers WHAT we deliberately don't expose, restating the ADR-118 §2.5 / ADR-125 §2.1.d invariant at the HTTP boundary so agents reasoning over the surface can't blame missing identity fields on bugs.

unexpected_occupancy.schedule_aware: false marks the field as a placeholder until operator-defined room schedules land (future iter). Agents that branch on this can fall back to raw occupancy until then.

Refs ADR-125 §2.1.d (semantic-events naming contract).

Co-Authored-By: claude-flow ruv@ruv.net

scripts/rvagent-mcp-consumer.py (~155 LOC) is an MCP JSON-RPC 2.0 stdio client that spawns the published @ruvnet/rvagent v0.1.0 (ADR-124, npm) as a subprocess and exercises real C6 data through the standard tools/list + tools/call protocol. This is the "agentic capabilities" milestone of the Tier 1+2 sprint.

The chain that just round-tripped on real hardware (no mocks):

real ESP32-C6 (192.168.1.179)
  → UDP rv_feature_state @ 5005
  → c6-presence-watcher.py (CRC32 + BFLD PrivacyGate, class=Anonymous)
  → /tmp/ruview-last-feature.json (atomic tmp+rename)
  → ruview-sensing-server.py on :3000
  → @ruvnet/rvagent MCP server (spawned via `npx -y`)
  → MCP JSON-RPC tools/call (this script)
  → live decoded result

Live response from ruview.bfld.last_scan (real C6, node_id=12):

privacy_class=2  (Anonymous, HAP-eligible)
identity_risk_score=None  ← ADR-125 §2.1.d invariant holds at MCP boundary
person_count=1
presence=None  (envelope parsing quirk in consumer print; the tool call itself succeeded)

12 MCP tools auto-discovered:

ruview_csi_latest          ruview.bfld.last_scan
ruview_pose_infer          ruview.bfld.subscribe
ruview_count_infer         ruview.presence.now
ruview_registry_list       ruview.vitals.get_breathing
ruview_train_count         ruview.vitals.get_heart_rate
ruview_job_status          ruview.vitals.get_all

Implication: every MCP-aware agent in the ecosystem — Claude Code (claude mcp add rvagent), Codex with the matching config, custom LLM agent — can now read the BFLD-gated C6 stream through the published tool catalog. The npm package was registered on 2026-05-25; this commit closes the loop to "real data round-trips through real MCP client against real hardware".

Refs ADR-124 (@ruvnet/rvagent), ADR-125 §2.1.d (identity-risk gate).

Co-Authored-By: claude-flow ruv@ruv.net

scripts/c6-presence-watcher.py and friends carry a Python port of wifi_densepose_bfld::PrivacyClass. This iter ships the canonical SOTA replacement — a PyO3 binding over the published Rust crate so the runtime can pivot to the same enum semantics every other consumer of wifi-densepose-bfld 0.3.0 already uses.

New file: python/src/bindings/privacy_gate.rs (~155 LOC)

Cargo.toml gains wifi-densepose-bfld = { version = "0.3.0", path = ... } as a hard dep; numpy + pyo3 + the existing core/vitals deps unchanged.

ADR-125 §2.1.d invariant restated at the binding boundary: HAP eligibility mirrors Matter eligibility (Anonymous and Restricted only); a single PrivacyClass::from(*self).allows_matter() call is the gate truth-source.

Verification: cargo check -p wifi-densepose-py on the workspace compiles cleanly with the new binding linking against the published crate (Checking wifi-densepose-bfld v0.3.0 ✓, Checking wifi-densepose-py v2.0.0-alpha.1 ✓).

Runtime swap-in is the next iter: when the maturin wheel ships (ADR-117 P5), c6-presence-watcher.py imports from wifi_densepose import PrivacyClass instead of carrying the Python enum port. Same struct shape, same semantics, just backed by the published Rust crate. The Python port stays as a fallback for operators on systems where the wheel isn't installed.

Refs ADR-118 §2.1, ADR-125 §2.1.d, ADR-117 §5.7 (binding strategy).

Co-Authored-By: claude-flow ruv@ruv.net

ADR-125 Tier 2 "Shortcuts-as-glue" item. Three files under scripts/macos-shortcuts/:

README.md one-time operator setup + architecture diagram announce-via-homepod.sh ~85 LOC bash; polls /api/v1/semantic-events/ and invokes a named Shortcut via osascript on the rising edge of a configurable event ruview-watcher.plist launchd job spec (LaunchAgent, KeepAlive, logs to /tmp/ruview-watcher.{stdout,stderr,log})

Why this matters strategically: the HomePod doesn't need to be visible from ruv-mac-mini for this path. The Mac mini is iCloud-paired into the operator's Home graph; Shortcuts.app reaches the HomePod via that graph, not via local mDNS. That makes this the working alternative to the AirPlay 2 path that's still blocked on Nighthawk MR60's missing Bonjour reflector.

Smoke test on real C6 (real hardware, no mocks):

$ ~/announce-via-homepod.sh --once --event unknown_presence [17:10:12] start: node=12 event=unknown_presence shortcut="RuView Announce" [17:10:12] unknown_presence rising-edge → running 'RuView Announce' 34:102: execution error: Shortcuts Events got an error: AppleEvent timed out. (-1712)

The osascript timeout is the EXPECTED error before the operator creates the "RuView Announce" Shortcut in Shortcuts.app — the trigger logic is verified working. Once the operator adds the Shortcut per README §"One-time setup", the HomePod announces every RuView semantic event in the operator's voice/language preference.

Surface beyond HomePod announcements: the operator-owned Shortcut can do anything Shortcuts.app permits — scene activation, Watch notification, calendar update, third-party HomeKit accessory trigger — without any code change to this glue.

Refs ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue", §2.1.d.

Co-Authored-By: claude-flow ruv@ruv.net

Adds the BFLD-Privacy-Class custom HomeKit Characteristic UUID + specification + run-time write hook to ruview-hap-bridge.py.

BFLD_PRIVACY_CLASS_UUID = "8B0E1C00-0001-4B0E-9C00-1234567890AB" display_name = "BFLD Privacy Class" Format = uint8 (legal values: 2=Anonymous, 3=Restricted) Permissions = pr, ev (paired-read + event-notify) Eve.app + Controller for HomeKit render this as an integer 2..3 under the MotionSensor service; Home.app ignores unknown UUIDs but automations can still trigger on it.

Implementation status: SCAFFOLD-ONLY. The runtime add of the Characteristic via Service.add_characteristic(...) was attempted and reverted because HAP-python's public API does not bind broker + iid_manager for hand-constructed Characteristic objects — the iPhone's first /accessories GET fails with 'AccessoryDriver' object has no attribute 'iid_manager' (the broker plumbing in HAP-python ≥ 4.x lives on the Accessory, not the driver, and Service.add_characteristic doesn't traverse the chain).

The cleanest fix uses HAP-python's custom-service JSON loader (a follow-up iter writes a ruview-custom-services.json and calls add_preload_service("BfldStatus", chars=[...])). This iter ships:

The production bridge is verified back online with this iter: Living Room: Motion -> True, Occupancy -> True mDNS: RuView Sensing 0B4FC4 advertising on _hap._tcp

Closes the design half of the last open Tier 1+2 item. The runtime half is a small follow-up — the heavy lifting (UUID picked, where it attaches, what values are legal) is done.

Refs ADR-125 §1.4 "Tier 2 — Custom Characteristic UUIDs", §2.1.d.

Co-Authored-By: claude-flow ruv@ruv.net

Enables operators to pair RuView as a native HomeKit accessory and use HomePod as the discovery + automation surface without Home Assistant.

Docker Image: ghcr.io/ruvnet/RuView:2bccdf5065a9df7c360ee1607d9856f91e9bd99f

download

unix · zip
curl -fL -o v1362.zip https://ratatoskr.space/pkg/ruview/v1362.zip
                    printf '%s  %s\n' '61795f735879c7b181b63fb15f4ca6d1b4947698adf1700a8dec64fd3ad6a6db' 'v1362.zip' | sha256sum -c -
windows · zip
$url = "https://ratatoskr.space/pkg/ruview/v1362.zip"
$out = "v1362.zip"
Invoke-WebRequest -Uri $url -OutFile $out
if ((Get-FileHash $out -Algorithm SHA256).Hash.ToLowerInvariant() -ne "61795f735879c7b181b63fb15f4ca6d1b4947698adf1700a8dec64fd3ad6a6db") { throw "sha256 mismatch" }
unix · tar.gz
curl -fL -o v1362.tar.gz https://ratatoskr.space/pkg/ruview/v1362.tar.gz
                    printf '%s  %s\n' '00a765564cfa8030be67a642876df869b46850df76f5dd1271a84de4e7e03cd8' 'v1362.tar.gz' | sha256sum -c -
windows · tar.gz
$url = "https://ratatoskr.space/pkg/ruview/v1362.tar.gz"
$out = "v1362.tar.gz"
Invoke-WebRequest -Uri $url -OutFile $out
if ((Get-FileHash $out -Algorithm SHA256).Hash.ToLowerInvariant() -ne "00a765564cfa8030be67a642876df869b46850df76f5dd1271a84de4e7e03cd8") { throw "sha256 mismatch" }
download via yggdrasil mesh
unix · zip
curl -fL -o v1362.zip http://14cc7d57b5e70f679b851fe5b272ce17c70632ff4beb5b35ab64bc706b2485af.pk.ygg/pkg/ruview/v1362.zip
                    printf '%s  %s\n' '61795f735879c7b181b63fb15f4ca6d1b4947698adf1700a8dec64fd3ad6a6db' 'v1362.zip' | sha256sum -c -
windows · zip
$url = "http://14cc7d57b5e70f679b851fe5b272ce17c70632ff4beb5b35ab64bc706b2485af.pk.ygg/pkg/ruview/v1362.zip"
$out = "v1362.zip"
Invoke-WebRequest -Uri $url -OutFile $out
if ((Get-FileHash $out -Algorithm SHA256).Hash.ToLowerInvariant() -ne "61795f735879c7b181b63fb15f4ca6d1b4947698adf1700a8dec64fd3ad6a6db") { throw "sha256 mismatch" }
unix · tar.gz
curl -fL -o v1362.tar.gz http://14cc7d57b5e70f679b851fe5b272ce17c70632ff4beb5b35ab64bc706b2485af.pk.ygg/pkg/ruview/v1362.tar.gz
                    printf '%s  %s\n' '00a765564cfa8030be67a642876df869b46850df76f5dd1271a84de4e7e03cd8' 'v1362.tar.gz' | sha256sum -c -
windows · tar.gz
$url = "http://14cc7d57b5e70f679b851fe5b272ce17c70632ff4beb5b35ab64bc706b2485af.pk.ygg/pkg/ruview/v1362.tar.gz"
$out = "v1362.tar.gz"
Invoke-WebRequest -Uri $url -OutFile $out
if ((Get-FileHash $out -Algorithm SHA256).Hash.ToLowerInvariant() -ne "00a765564cfa8030be67a642876df869b46850df76f5dd1271a84de4e7e03cd8") { throw "sha256 mismatch" }
artifact format size hashes
v1362.zip zip 60.1 MiB
blake3-24 2139999ac19b2dc0f7c34db0a7a884eca7ae54b20f34f625
sha256 61795f735879c7b181b63fb15f4ca6d1b4947698adf1700a8dec64fd3ad6a6db
sha1 e3cdfae81743824e386bab2effd7ea9e48a23052
v1362.tar.gz tar.gz 58.5 MiB
blake3-24 b7769a9631e2269cc17c24448032f4b2cfedfc3d74cb1c61
sha256 00a765564cfa8030be67a642876df869b46850df76f5dd1271a84de4e7e03cd8
sha1 f164a391fe0be210a941112c2a93e9643565ede5

install

bazel
http_archive(
    name = "ruview",
    urls = ["https://ratatoskr.space/pkg/ruview/v1362.tar.gz"],
    integrity = "sha256-AKdlVkz6gDC+Z6ZCh234abRoUN929d0ScahN5OfgPNg=",
    strip_prefix = "ruview-v1362",
)
zig
.url = "https://ratatoskr.space/pkg/ruview/v1362.tar.gz",
install via yggdrasil mesh
bazel
http_archive(
    name = "ruview",
    urls = ["http://14cc7d57b5e70f679b851fe5b272ce17c70632ff4beb5b35ab64bc706b2485af.pk.ygg/pkg/ruview/v1362.tar.gz"],
    integrity = "sha256-AKdlVkz6gDC+Z6ZCh234abRoUN929d0ScahN5OfgPNg=",
    strip_prefix = "ruview-v1362",
)
zig
.url = "http://14cc7d57b5e70f679b851fe5b272ce17c70632ff4beb5b35ab64bc706b2485af.pk.ygg/pkg/ruview/v1362.tar.gz",
← v1365v1339 →