Automated release from CI pipeline
Changes:
HOMECORE: native Rust/WASM/TS port of Home Assistant — ADRs 125-134 implementation (#800)
- feat(adr-125 iter 3): BFLD PrivacyGate + semantic-event naming at HAP boundary
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 9fda90f 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
- docs(adr-125 iter 4): CHANGELOG bullet for the APPLE-FABRIC e2e
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
- feat(adr-125 tier1 #1): multi-characteristic accessory + JSON-state IPC
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):
- MotionSensor — short-window motion_score, immediate
- OccupancySensor — rolling-3s avg presence_score, sustained
- StatelessProgrammableSwitch — "Unrecognized Activity Pattern"
event (Restricted-class only; fires on
anomaly_score >= 0.7); ADR-125 §2.1.d
semantic naming, not security state
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
- feat(adr-125 tier1+2 iter 2): sensing-server-equivalent for @ruvnet/rvagent
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
- feat(adr-125 tier1+2 iter 3): production HAP bridge with N child accessories
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
- feat(adr-125 tier1+2 iter 4): semantic-events MCP endpoint per §2.1.d
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
- feat(adr-125 tier1+2 iter 5): rvagent MCP consumer — agentic chain proven
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
- feat(adr-125 tier1+2 iter 6 SOTA): PyO3 BFLD PrivacyClass binding
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)
#[pyclass] PrivacyClass {Raw, Derived, Anonymous, Restricted}.allows_network,.allows_matter,.allows_hap,.as_u8gettersPrivacyClass.from_u8(v)/PrivacyClass.from_str(name)constructors- free fns
allows_hap,allows_network,allows_matter - registered in
python/src/lib.rsviabindings::privacy_gate::register
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
- feat(adr-125 tier1+2 iter 7): Shortcuts-as-glue scaffold (Tier 2)
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
- feat(adr-125 tier1+2 iter 8): custom characteristic UUID scaffold (Tier 2)
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 UUID constant (won't change across implementations)
- the design spec inline in the code (Format / Permissions / range)
- the run-time write path under
if self.c_privacy_class is not None
(no-op until the next iter wires the loader)
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
- docs(adr-125): Apple HomePod user guide + README badge
- Add docs/user-guide-apple-homepod.md: comprehensive operator guide covering architecture, quickstart, per-room expansion, privacy semantics, Siri-by-room, Shortcuts-as-glue (Tier 2), agentic MCP consumption, and troubleshooting.
- Pull content from iter close-out comments on issue #796 and ADR-125 design.
- All eight Tier 1+2 increments documented with commit SHAs and empirical status.
- Update README.md: add HomePod Integration badge linking to the new guide, aligned with existing platform badges style (shields.io format, Apple logo, black background).
Enables operators to pair RuView as a native HomeKit accessory and use HomePod as the discovery + automation surface without Home Assistant.
- feat(homecore/p1): ADR-127 state machine scaffold (20 tests pass)
New crate v2/crates/homecore/ — DashMap state machine, tokio
broadcast event bus, service registry (direct-dispatch P1),
in-memory entity registry, HA-compat wire constants.
20/20 unit tests pass. EntityId rejects unicode per ADR-127 Q1
(ASCII strict P1). State machine suppresses no-op writes,
preserves last_changed on attribute-only updates, fires
state_changed broadcast for every real write.
Critical path foundation — ADR-130 (API) and ADR-128 (plugins)
can begin P1 once this is in main.
Refs: docs/adr/ADR-127-homecore-state-machine-rust.md
Refs: #798
Co-Authored-By: claude-flow ruv@ruv.net
- docs(readme): link ecosystem badges + move Beta callout to bottom
Three operator-feedback corrections to the README:
-
Every ecosystem badge in the top row now links to a real
destination — Home Assistant -> integrations/home-assistant.md,
Matter -> ADR-122, Apple Home -> user-guide-apple-homepod.md,
Google Home + Alexa -> the HA integration doc (both ecosystems
reach RuView through HA's bridge today). Added an Alexa badge
alongside the existing four so all four major ecosystems are
represented. Dropped the now-redundant separate "HomePod
Integration" badge — the Apple Home badge linking to the same
guide is enough. -
Beta callout moved from line 14 (under the hero image) to a
dedicated## Beta softwaresection immediately before the
License. The callout's content is unchanged; it just no longer
gates the elevator pitch. Readers see the value proposition
first, the caveats at the bottom alongside license + support. -
The intro paragraph ("Turn ordinary WiFi into ...") now ends
with a one-line summary of native ecosystem support naming all
four — Home Assistant, Apple Home & HomePod, Google Home, Alexa —
plus the Matter endpoint, each linked. The previous mention of
ecosystems was buried further down the page; this surfaces it
in the intro where the user reads first.
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-plugins/p1): ADR-128 plugin runtime scaffold
Adds v2/crates/homecore-plugins (0.1.0-alpha.0) — the P1 scaffold for
the HOMECORE-PLUGINS WASM integration system (ADR-128):
manifest.rs:PluginManifest— superset of HA manifest.json; serde
round-trip + required-field validation (domain/name/version).error.rs:PluginErrortyped enum (InvalidManifest, AlreadyLoaded,
NotFound, RuntimeError, SetupFailed, UnloadFailed, Io).plugin.rs:HomeCorePluginasync trait +PluginIdnewtype.runtime.rs:PluginRuntimetrait +InProcessRuntime(native Rust,
first-party plugins).WasmtimeRuntimestub gated on--features wasmtime
(default-off; 30 MB dep deferred to P2).registry.rs:PluginRegistry<R>— load/unload/list/contains via RwLock.- 10 unit tests, 0 failed.
Wasmtime vs wasm3 runtime selection is still open (ADR-128 §8 Q2);
this scaffold makes the choice swappable via the PluginRuntime trait.
The wasmtime and wasm3 features are default-off; P2 resolves the choice
and wires host ABI (hc_state_get/hc_state_set/etc.) to ADR-127.
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore/p1 iter-2): API (ADR-130) + plugins (ADR-128) scaffolds in parallel
Two new crates land in this iteration of the HOMECORE swarm:
v2/crates/homecore-api/ (ADR-130 P1, sequential foundation)
Wire-compat Axum REST + WebSocket port of HA's API. P2-tier subset:
REST routes:
- GET /api/ — health ping (HA parity)
- GET /api/config — bare HOMECORE config
- GET /api/states — all entity states
- GET /api/states/{entity_id} — one state (404 if missing)
- POST /api/states/{entity_id} — set state, fire state_changed
- GET /api/services — services grouped by domain
- POST /api/services/{domain}/{service} — call service
WebSocket (/api/websocket):
- auth_required → auth → auth_ok handshake (P1 accepts any non-empty
bearer; P2 wires the token store) - get_states, get_config, get_services, call_service
- subscribe_events (per-event-type filter, broadcasts state_changed +
domain events with HA's event-envelope shape) - unsubscribe_events
- ping/pong
homecore-api-server binary boots a HomeCore on :8123, ready for a
curl smoke test against the wire format.
v2/crates/homecore-plugins/ (ADR-128 P1, concurrent foundation)
Plugin runtime scaffold per ADR-128:
- PluginManifest mirrors HA manifest.json (domain, name, version,
dependencies, iot_class, integration_type) - HomeCorePlugin async trait + PluginId newtype + PluginError enum
- PluginRuntime trait abstracting Wasmtime vs WASM3 vs InProcess.
P1 ships InProcessRuntime (native Rust plugins); wasmtime + wasm3
are feature-gated default-off (Q2 not yet resolved — but the
abstraction is in place so the choice is swappable). - PluginRegistry: load/unload/list by PluginId.
Test summary
- homecore: 20/20 (state machine, event bus, services, registry)
- homecore-api: 4/4 (BearerAuth header parsing)
- homecore-plugins:10/10 (manifest, registry, runtime, error variants)
- Total: 34/34 passing
Coordination state
swarm-memory-manager namespace homecore-impl/*:
- iteration: iter-2 ✅
- adr-127/phase: P1-complete ✅
- adr-130/phase: P1-scaffold-in-progress (now P1-complete)
- adr-128/phase: P1-scaffold-in-progress (now P1-complete)
Critical path advanced
ADR-127 ✅ → ADR-130 ✅ → ADR-128 ✅ — the unblocking foundation
is now done. Next iteration can fan out 129/131/132/133/134/125
concurrently. Tracking issue #798.
Refs: docs/adr/ADR-130-homecore-rest-websocket-api.md
Refs: docs/adr/ADR-128-homecore-integration-plugin-system.md
Refs: #798
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-hap/p1): ADR-125 HAP bridge scaffold (17 tests pass)
Add homecore-hap crate: HapAccessoryType (11 variants), HapCharacteristic,
EntityToAccessoryMapper (light/switch/binary_sensor/sensor/cover/lock domains),
HapBridge add/remove/running API, NullAdvertiser mDNS stub, and
RuViewToHapMapper (presence→OccupancySensor, fall→LeakSensor, motion→MotionSensor).
P2 hap-server feature gates the real hap = "0.1" server + mdns-sd integration.
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-recorder/p1): ADR-132 SQLite recorder + fnv64a attr dedup (14 tests pass)
- SQLite-backed state history with HA-compat schema (states, state_attributes,
events, recorder_runs) mirroring recorder schema v48 - FNV-1a 64-bit attribute deduplication matching HA's db_schema.py fnv64a
- RecorderListener subscribes to StateMachine broadcast and persists every
state change; subscription created at construction to avoid missed events - SemanticIndex trait + NullSemanticIndex for P1; ruvector-backed impl stub
feature-gated behind --features ruvector for P2 hand-off
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-automation/p1): ADR-129 automation engine + MiniJinja templates (34 tests pass)
Scaffolds v2/crates/homecore-automation per ADR-129 HOMECORE-AUTO:
- Automation struct with RunMode (single/restart/queued/parallel/ignore_first)
- Trigger enum: State, NumericState, Time, Event + EvaluateTrigger trait
- Condition enum: State, NumericState, Template, And, Or, Not + async evaluate
- Action enum: ServiceCall, Delay, Scene, WaitForTrigger, Choose + async execute
- TemplateEnvironment: MiniJinja 2.x with HA globals states(), state_attr(), is_state(), now()
- AutomationEngine: subscribes to state-machine broadcast, evaluates triggers, runs action tasks
34 unit tests pass (0 failed). MiniJinja filter coverage: states, state_attr, is_state, now (P1 set).
Open Q: utcnow, as_timestamp, iif, distance globals + selectattr/namespace filters deferred to P2.
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-migrate/p1): ADR-134 .storage parser + entity-registry import (19 tests pass)
- HaStorageEnvelope: outer {version, minor_version, key, data} shape for all .storage files
- storage_format/v13: versioned parser dispatch; UnsupportedSchemaVersion hard error on unknown minor_version
- entity_registry: core.entity_registry v13 → Vechomecore::EntityEntry with full field mapping
- device_registry: core.device_registry → Vec (P2 HOMECORE wiring stub)
- config_entries: envelope read + domain count diagnostic (P2 plugin manifest conversion)
- secrets: secrets.yaml → HashMap<String,String>
- automations: count + ID list extraction (P2 conversion)
- cli: clap-derived Inspect/ImportEntities/ImportDevices/InspectConfigEntries/InspectSecrets/InspectAutomations subcommands
- 19 unit tests, all pass; build clean; workspace member appended to v2/Cargo.toml
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-assist/p1): ADR-133 intent pipeline + ruflo runner stub (23 tests pass)
- Creates v2/crates/homecore-assist with intent, recognizer, handler,
runner, and pipeline modules per ADR-133 §2 design - RegexIntentRecognizer: HA-style named-capture-group pattern matching
- Built-in handlers: HassTurnOn, HassTurnOff, HassLightSet, HassNevermind,
HassCancelAll — dispatch to homecore ServiceRegistry - RufloRunner trait + NoopRunner P1 stub (Windows-safe subprocess teardown
deferred to P2 per ADR-133 §Q3) - AssistPipeline + default_pipeline() wires recognizer → handler → response
- SemanticIntentRecognizer P2 stub (ruvector HNSW deferred)
- 23 unit tests, 0 failures; cargo build -p homecore-assist clean
Co-Authored-By: claude-flow ruv@ruv.net
- docs(adr-131/recon): cognitum-one/v0-appliance design recon for HOMECORE-FRONTEND
Captures the full design system from the live cognitum-v0:9000 dashboard
(all 10 nav pages fetched, HTTP 200, unauthenticated). Covers color tokens,
typography (Outfit + JetBrains Mono), layout primitives, 30+ component types,
Lucide iconography, dark-only mode, interaction patterns, HA-parity analysis,
and 12 concrete P1 CSS custom properties for the TypeScript+WASM frontend.
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-frontend/p1): @ruvnet/homecore-frontend Lit+TS+Vite scaffold (3 tests)
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-recorder/p2): wire RuvectorSemanticIndex with hash-based embeddings (resolves ADR-132 P2)
- ruvector-core = "2.2.0" + sha2 = "0.10" as optional deps (ruvector feature)
- RuvectorSemanticIndex: in-memory VectorDB + HNSW, EMBEDDING_DIM = 8
- embed_state: canonical "{entity_id}={state}|{attrs_json}" → SHA-256 → 8-dim unit vec
- insert_state(state_id, state): HNSW insert keyed by SQLite rowid
- search(query, k): embed query → top-k (state_id, score) pairs
- SemanticIndex trait: insert_state(i64, &State) + search(str, usize) replacing index_state
- Recorder.semantic: Arc<RwLock> for interior mutability
- Recorder::search_semantic(query, k): HNSW → SQLite JOIN → Vec
- Tests: 20 passed (was 14 at P1): determinism, unit-norm, dim, insert+search, ranking, e2e
- P3 note: swap embed_bytes for ruvector-attention; raise dim to 384
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-plugins/p2): Wasmtime runtime + example WASM plugin (resolves ADR-128 Q2)
-
Implements WasmtimeRuntime in v2/crates/homecore-plugins/src/wasmtime_runtime.rs
with a Wasmtime 25 Cranelift JIT engine. Registers 4 host imports via Linker:
hc_state_get, hc_state_set, hc_state_subscribe, hc_log. Each plugin gets an
isolated Store holding a HomeCore handle + subscription list. -
Adds host_abi.rs documenting the JSON-over-linear-memory wire format (public
ABI spec for plugin authors). Max buffer 64 KiB. ConfigEntryJson and
StateChangedEventJson are the canonical wire types. -
Creates v2/crates/homecore-plugin-example/ (wasm32-unknown-unknown, excluded
from workspace per wifi-densepose-wasm-edge pattern). The plugin monitors
sensor.test_temp and sets binary_sensor.test_alert on/off at 25/20 thresholds. -
Adds tests/integration.rs with 3 tests: compiled .wasm end-to-end round-trip,
WAT-based fallback (always runs), and linker smoke test. All 15 tests pass
(12 unit + 3 integration) under --features wasmtime. -
ADR-128 Q2 resolved: Wasmtime is the chosen runtime for P2. WASM3 stays as
future fallback under --features wasm3 for constrained hardware (ADR-128 §8).
Co-Authored-By: claude-flow ruv@ruv.net
- feat(homecore-server/iter-9): integration binary tying all 8 HOMECORE crates together
New crate v2/crates/homecore-server/ boots one process that wires
every HOMECORE surface into a single HA-compatible runtime:
- HomeCore runtime (ADR-127) — state machine + event bus + service
registry online at boot. - Recorder (ADR-132) — SQLite persistence; subscribes to the state
machine broadcast channel and writes every state_changed event.
Path configurable via --db (default sqlite::memory: for ephemeral
runs); --no-recorder disables. ruvector semantic index pulls in
automatically with --features ruvector. - Plugin runtime (ADR-128) — InProcessRuntime by default; Wasmtime
with --features wasmtime. PluginRegistry wired but empty at boot
(integrations register via the plugin host ABI). - Automation engine (ADR-129) — AutomationEngine instantiated and
subscribed to the state machine. No automations loaded at boot
yet; that's a YAML-loading P3 task. - Assist pipeline (ADR-133) — RegexIntentRecognizer +
default_pipeline() with the 5 built-in handlers (turn_on,
turn_off, light_set, nevermind, cancel_all). - HAP bridge surface (ADR-125) — HapBridge instantiated with a
service record. Accessory registration via the API. - REST + WebSocket API (ADR-130) — Axum router on :8123, HA-compat.
/api/, /api/config, /api/states[/{eid}], /api/services[/...],
/api/websocket.
Configuration via CLI flags + env vars:
- --bind / HOMECORE_BIND (default 0.0.0.0:8123)
- --db / HOMECORE_DB (default sqlite::memory:)
- --location-name / HOMECORE_LOCATION (default "Home")
- --no-recorder
Builds clean (cargo build -p homecore-server). Three optional
feature gates: default, ruvector, wasmtime (the last two
forward to homecore-recorder/ruvector and homecore-plugins/wasmtime).
Refs: docs/adr/ADR-126-ruview-native-ha-port-master.md §5 phase roadmap
Refs: #798
Co-Authored-By: claude-flow ruv@ruv.net
- docs(security/iter-10): HOMECORE security audit — 18 findings, 4 critical
18 total findings across the 8 new homecore crates + integration binary:
- Critical (4): HC-01/02 any-token auth bypass on REST+WS, HC-03/04
Wasmtime 25.0.3 sandbox-escape CVEs (RUSTSEC-2026-0095/0096, CVSS 9.0) - High (3): permissive CORS, sqlx 0.7.4 protocol bug, unbounded WS subscriptions
- Medium (5): hardcoded HAP setup code, hc_log bypasses tracing, no body
size limit, rsa Marvin Attack, shlex quote injection - Low/Info (6): no TLS, migrate symlink gap, eprintln in automation engine,
subscription dedup, two informational
cargo audit: 18 advisories (2 critical wasmtime sandbox escapes, fix = upgrade
wasmtime to >=36.0.7; upgrade sqlx to >=0.8.1)
Co-Authored-By: claude-flow ruv@ruv.net
- fix(homecore-recorder/sec): bump sqlx 0.7.4 → 0.8.1+ (RUSTSEC, audit HC-medium)
Per iter-10 security audit (docs/security/HOMECORE-security-audit-iter10.md):
sqlx 0.7.4 ships an advisory for binary protocol misinterpretation.
Bump to 0.8.1+ — cargo resolved to 0.8.6.
Feature set unchanged (default-features = false +
runtime-tokio-native-tls, sqlite, chrono, uuid). Tests still pass:
cargo test -p homecore-recorder --features ruvector
→ 20 passed; 0 failed
No code changes required. The 0.7 → 0.8 API surface we touch in
db.rs is stable across the bump.
Deferred to a later iter:
- shlex 0.1.1 → ≥1.3.0 (transitive via wasm3-sys, only on
--features wasm3 which is default-off; will be addressed when
the wasm3 path is removed per ADR-128 Q2 Wasmtime resolution) - wasmtime 25 → 36+/42+ (HC-03/04 CVSS 9.0 sandbox-escape) — being
handled by a background coder agent this iter, separate commit.
Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-09 sqlx)
Refs: #798
Co-Authored-By: claude-flow ruv@ruv.net
- fix(homecore-plugins/sec): bump wasmtime 25 → 42 for RUSTSEC-2026-0095/0096 (HC-03/04, CVSS 9.0)
Remediates iter-11 security audit findings HC-03 (RUSTSEC-2026-0095) and
HC-04 (RUSTSEC-2026-0096) — Cranelift/Winch sandbox-escape CVEs (CVSS 9.0).
Version specifier updated from "25" → "42"; lockfile already pinned at
42.0.2. Zero code-surface changes required: Engine/Linker/Store/Instance
and Memory.data/data_mut APIs are ABI-compatible across this range.
All 15 tests pass (12 unit + 3 integration including the two required
wasm_plugin_temp_threshold tests). cargo audit no longer reports
RUSTSEC-2026-0095 or RUSTSEC-2026-0096 against this workspace.
Co-Authored-By: claude-flow ruv@ruv.net
- perf(homecore): criterion benches for state-machine hot paths
cargo bench -p homecore --bench state_machine covers:
- set/first_write — cold-path insert + alloc + broadcast
- set/warm_write_state_change — same-entity update fires broadcast
- set/noop_suppressed — same state+attrs, no broadcast (HA semantic)
- get/hit + get/miss — zero-copy Arc read paths
- all_snapshot/{10,100,1000} — Vec<Arc> snapshot for REST
- all_by_domain_light_20_of_100 — domain prefix filter
- broadcast_fan_out/{1,4,16,64} — 1 sender + N subscribers, async,
measures end-to-end deliver-and-recv latency
The broadcast fan-out is the most load-bearing measurement for
HOMECORE — every integration, the recorder, the automation engine,
and every WS subscriber holds a receiver, so the per-subscriber
delivery cost determines how many add-ons the runtime can host.
criterion 0.5 with sample_size=20 (fast tick, the fast-path benches
run in nanoseconds and don't need 100 samples).
Refs: docs/adr/ADR-127-homecore-state-machine-rust.md
Refs: #798
Co-Authored-By: claude-flow ruv@ruv.net
- fix(homecore-api/sec): close HC-01/HC-02 — real bearer-token store
Replaces the P1 "any non-empty bearer" placeholder with a real
LongLivedTokenStore (HashSet) on SharedState. Closes the
two Critical findings from the iter-10 security audit
(docs/security/HOMECORE-security-audit-iter10.md HC-01 + HC-02).
New module homecore-api::tokens:
- LongLivedTokenStore::empty() — default-deny
- LongLivedTokenStore::from_env() — reads HOMECORE_TOKENS=t1,t2,t3
- LongLivedTokenStore::allow_any_non_empty() — DEV-only, warns
on every check, preserves legacy behaviour for migrating users - register / revoke / is_valid / len / is_dev_mode — full API
Wired through:
- SharedState gains
tokens: LongLivedTokenStore; constructors
with_tokens(...) for explicit injection; with_metadata defaults
to DEV (allow_any) for backwards compat with existing smoke tests - BearerAuth::from_headers now async + takes &LongLivedTokenStore;
checks store.is_valid(token) before returning Ok - All 6 REST handlers updated to thread the store and await the
validation - homecore-server reads HOMECORE_TOKENS at boot; if set, builds
the store from env; if unset, falls back to DEV with a warn log
Test count: 4 → 15 (+11 token-store + auth-with-store tests).
Smoke verified end-to-end:
HOMECORE_TOKENS=good homecore-server --bind 127.0.0.1:8126
→ "LongLivedTokenStore provisioned with 1 bearer token(s)"
curl -H "Authorization: Bearer good" .../api/states → 200
curl -H "Authorization: Bearer wrong" .../api/states → 401
curl -H "Authorization: Bearer " .../api/states → 401
curl .../api/states → 401
Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-01 + HC-02)
Refs: docs/adr/ADR-130-homecore-rest-websocket-api.md §3 auth
Refs: #798
Refs: #800
Co-Authored-By: claude-flow ruv@ruv.net
- fix(homecore-api/sec): close HC-05 — CORS allowlist instead of permissive
Replaces CorsLayer::permissive() (which set Access-Control-Allow-
Origin: *) with an explicit allowlist via CorsLayer::new().
Default allowlist covers the homecore-frontend Vite dev server
(5173) plus common reverse-proxy ports (3000, 8080, 8081) and the
bind port itself (8123). Production deployments override via
HOMECORE_CORS_ORIGINS=https://app.example.com,https://hass.example.com
(comma-separated).
Method allowlist: GET, POST, OPTIONS, DELETE (no PUT/PATCH yet).
Header allowlist: Authorization, Content-Type, Accept.
Credentials: disabled (no cookies in HOMECORE-API path).
Test count: 15 → 18 (+3 CORS allowlist tests).
Closes audit finding HC-05 (High). The HC-01/02 bearer-store fix
in commit 408cfd4 only mattered if the cross-origin path was
also locked down — without HC-05 a malicious page could still
make authenticated calls with a stored bearer.
Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-05)
Refs: #800
Co-Authored-By: claude-flow ruv@ruv.net
Docker Image:
ghcr.io/ruvnet/RuView:e96ebaea810c20c402f48967c44f12d1d3d9f4ea