From e96ebaea810c20c402f48967c44f12d1d3d9f4ea Mon Sep 17 00:00:00 2001 From: rUv Date: Mon, 25 May 2026 22:47:48 -0400 Subject: [PATCH] =?UTF-8?q?HOMECORE:=20native=20Rust/WASM/TS=20port=20of?= =?UTF-8?q?=20Home=20Assistant=20=E2=80=94=20ADRs=20125-134=20implementati?= =?UTF-8?q?on=20(#800)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 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 * 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 * 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 * 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//latest — EdgeVitalsMessage GET /api/v1/bfld//last_scan — BfldScanResponse POST /api/v1/bfld//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 * 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 * feat(adr-125 tier1+2 iter 4): semantic-events MCP endpoint per §2.1.d GET /api/v1/semantic-events//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 * 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 * 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_u8` getters - `PrivacyClass.from_u8(v)` / `PrivacyClass.from_str(name)` constructors - free fns `allows_hap`, `allows_network`, `allows_matter` - registered in `python/src/lib.rs` via `bindings::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 * 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 * 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 * 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 * docs(readme): link ecosystem badges + move Beta callout to bottom Three operator-feedback corrections to the README: 1. 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. 2. Beta callout moved from line 14 (under the hero image) to a dedicated `## Beta software` section 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. 3. 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 * 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`: `PluginError` typed enum (InvalidManifest, AlreadyLoaded, NotFound, RuntimeError, SetupFailed, UnloadFailed, Io). - `plugin.rs`: `HomeCorePlugin` async trait + `PluginId` newtype. - `runtime.rs`: `PluginRuntime` trait + `InProcessRuntime` (native Rust, first-party plugins). `WasmtimeRuntime` stub gated on `--features wasmtime` (default-off; 30 MB dep deferred to P2). - `registry.rs`: `PluginRegistry` — 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 * 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 * 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 * 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 * 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 * 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 → Vec 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 - 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 * 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 * 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 * feat(homecore-frontend/p1): @ruvnet/homecore-frontend Lit+TS+Vite scaffold (3 tests) Co-Authored-By: claude-flow * 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> 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 * 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 * 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: 1. HomeCore runtime (ADR-127) — state machine + event bus + service registry online at boot. 2. 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. 3. Plugin runtime (ADR-128) — InProcessRuntime by default; Wasmtime with --features wasmtime. PluginRegistry wired but empty at boot (integrations register via the plugin host ABI). 4. 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. 5. Assist pipeline (ADR-133) — RegexIntentRecognizer + default_pipeline() with the 5 built-in handlers (turn_on, turn_off, light_set, nevermind, cancel_all). 6. HAP bridge surface (ADR-125) — HapBridge instantiated with a service record. Accessory registration via the API. 7. 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 * 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 * 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 * 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 * 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> 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 * 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 * 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 408cfd4f0 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 --- docs/adr/ADR-133-homecore-assist-ruflo.md | 176 + docs/design/HOMECORE-FRONTEND-design-recon.md | 301 ++ .../HOMECORE-security-audit-iter10.md | 160 + frontend/.gitignore | 5 + frontend/README.md | 69 + frontend/index.html | 18 + frontend/package-lock.json | 4429 +++++++++++++++++ frontend/package.json | 24 + frontend/src/__tests__/StateCard.test.ts | 82 + frontend/src/__tests__/client.test.ts | 67 + frontend/src/__tests__/tokens.test.ts | 66 + frontend/src/api/client.ts | 132 + frontend/src/api/types.ts | 98 + frontend/src/components/AppShell.ts | 194 + frontend/src/components/StateCard.ts | 132 + frontend/src/icons/lucide.ts | 39 + frontend/src/main.ts | 11 + frontend/src/styles/base.css | 224 + frontend/src/styles/tokens.css | 45 + frontend/tsconfig.json | 23 + frontend/vite.config.ts | 25 + frontend/vitest.config.ts | 13 + v2/Cargo.lock | 1460 +++++- v2/Cargo.toml | 14 + v2/crates/homecore-api/Cargo.toml | 40 + v2/crates/homecore-api/src/app.rs | 116 + v2/crates/homecore-api/src/auth.rs | 117 + v2/crates/homecore-api/src/bin/server.rs | 33 + v2/crates/homecore-api/src/error.rs | 37 + v2/crates/homecore-api/src/lib.rs | 15 + v2/crates/homecore-api/src/rest.rs | 147 + v2/crates/homecore-api/src/state.rs | 63 + v2/crates/homecore-api/src/tokens.rs | 201 + v2/crates/homecore-api/src/ws.rs | 349 ++ v2/crates/homecore-assist/Cargo.toml | 47 + v2/crates/homecore-assist/src/handler.rs | 288 ++ v2/crates/homecore-assist/src/intent.rs | 131 + v2/crates/homecore-assist/src/lib.rs | 42 + v2/crates/homecore-assist/src/pipeline.rs | 262 + v2/crates/homecore-assist/src/recognizer.rs | 232 + v2/crates/homecore-assist/src/runner.rs | 174 + v2/crates/homecore-automation/Cargo.toml | 48 + v2/crates/homecore-automation/src/action.rs | 191 + .../homecore-automation/src/automation.rs | 120 + .../homecore-automation/src/condition.rs | 240 + v2/crates/homecore-automation/src/engine.rs | 252 + v2/crates/homecore-automation/src/error.rs | 29 + v2/crates/homecore-automation/src/lib.rs | 30 + v2/crates/homecore-automation/src/template.rs | 194 + v2/crates/homecore-automation/src/trigger.rs | 296 ++ v2/crates/homecore-hap/Cargo.toml | 36 + v2/crates/homecore-hap/src/accessory.rs | 124 + v2/crates/homecore-hap/src/bridge.rs | 196 + v2/crates/homecore-hap/src/error.rs | 22 + v2/crates/homecore-hap/src/lib.rs | 34 + v2/crates/homecore-hap/src/mapping.rs | 273 + v2/crates/homecore-hap/src/mdns.rs | 79 + v2/crates/homecore-hap/src/ruview.rs | 158 + v2/crates/homecore-migrate/Cargo.toml | 60 + v2/crates/homecore-migrate/src/automations.rs | 130 + v2/crates/homecore-migrate/src/cli.rs | 77 + .../homecore-migrate/src/config_entries.rs | 128 + .../homecore-migrate/src/device_registry.rs | 99 + .../homecore-migrate/src/entity_registry.rs | 269 + v2/crates/homecore-migrate/src/lib.rs | 76 + v2/crates/homecore-migrate/src/main.rs | 103 + v2/crates/homecore-migrate/src/secrets.rs | 105 + v2/crates/homecore-migrate/src/storage.rs | 101 + .../src/storage_format/mod.rs | 13 + .../src/storage_format/v13.rs | 80 + v2/crates/homecore-plugin-example/Cargo.lock | 7 + v2/crates/homecore-plugin-example/Cargo.toml | 39 + v2/crates/homecore-plugin-example/README.md | 31 + v2/crates/homecore-plugin-example/src/abi.rs | 106 + v2/crates/homecore-plugin-example/src/lib.rs | 133 + v2/crates/homecore-plugins/Cargo.toml | 64 + v2/crates/homecore-plugins/src/error.rs | 35 + v2/crates/homecore-plugins/src/host_abi.rs | 128 + v2/crates/homecore-plugins/src/lib.rs | 56 + v2/crates/homecore-plugins/src/manifest.rs | 144 + v2/crates/homecore-plugins/src/plugin.rs | 59 + v2/crates/homecore-plugins/src/registry.rs | 102 + v2/crates/homecore-plugins/src/runtime.rs | 95 + v2/crates/homecore-plugins/src/tests.rs | 233 + .../homecore-plugins/src/wasmtime_runtime.rs | 553 ++ .../homecore-plugins/tests/integration.rs | 374 ++ v2/crates/homecore-recorder/Cargo.toml | 63 + v2/crates/homecore-recorder/src/db.rs | 562 +++ v2/crates/homecore-recorder/src/dedup.rs | 81 + v2/crates/homecore-recorder/src/lib.rs | 38 + v2/crates/homecore-recorder/src/listener.rs | 117 + v2/crates/homecore-recorder/src/schema.rs | 90 + v2/crates/homecore-recorder/src/semantic.rs | 273 + v2/crates/homecore-server/Cargo.toml | 46 + v2/crates/homecore-server/src/main.rs | 156 + v2/crates/homecore/Cargo.toml | 48 + v2/crates/homecore/benches/state_machine.rs | 205 + v2/crates/homecore/src/bus.rs | 90 + v2/crates/homecore/src/entity.rs | 238 + v2/crates/homecore/src/event.rs | 163 + v2/crates/homecore/src/homecore.rs | 75 + v2/crates/homecore/src/lib.rs | 59 + v2/crates/homecore/src/registry.rs | 130 + v2/crates/homecore/src/service.rs | 170 + v2/crates/homecore/src/state.rs | 221 + 105 files changed, 18623 insertions(+), 25 deletions(-) create mode 100644 docs/adr/ADR-133-homecore-assist-ruflo.md create mode 100644 docs/design/HOMECORE-FRONTEND-design-recon.md create mode 100644 docs/security/HOMECORE-security-audit-iter10.md create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/__tests__/StateCard.test.ts create mode 100644 frontend/src/__tests__/client.test.ts create mode 100644 frontend/src/__tests__/tokens.test.ts create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/types.ts create mode 100644 frontend/src/components/AppShell.ts create mode 100644 frontend/src/components/StateCard.ts create mode 100644 frontend/src/icons/lucide.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/styles/base.css create mode 100644 frontend/src/styles/tokens.css create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 frontend/vitest.config.ts create mode 100644 v2/crates/homecore-api/Cargo.toml create mode 100644 v2/crates/homecore-api/src/app.rs create mode 100644 v2/crates/homecore-api/src/auth.rs create mode 100644 v2/crates/homecore-api/src/bin/server.rs create mode 100644 v2/crates/homecore-api/src/error.rs create mode 100644 v2/crates/homecore-api/src/lib.rs create mode 100644 v2/crates/homecore-api/src/rest.rs create mode 100644 v2/crates/homecore-api/src/state.rs create mode 100644 v2/crates/homecore-api/src/tokens.rs create mode 100644 v2/crates/homecore-api/src/ws.rs create mode 100644 v2/crates/homecore-assist/Cargo.toml create mode 100644 v2/crates/homecore-assist/src/handler.rs create mode 100644 v2/crates/homecore-assist/src/intent.rs create mode 100644 v2/crates/homecore-assist/src/lib.rs create mode 100644 v2/crates/homecore-assist/src/pipeline.rs create mode 100644 v2/crates/homecore-assist/src/recognizer.rs create mode 100644 v2/crates/homecore-assist/src/runner.rs create mode 100644 v2/crates/homecore-automation/Cargo.toml create mode 100644 v2/crates/homecore-automation/src/action.rs create mode 100644 v2/crates/homecore-automation/src/automation.rs create mode 100644 v2/crates/homecore-automation/src/condition.rs create mode 100644 v2/crates/homecore-automation/src/engine.rs create mode 100644 v2/crates/homecore-automation/src/error.rs create mode 100644 v2/crates/homecore-automation/src/lib.rs create mode 100644 v2/crates/homecore-automation/src/template.rs create mode 100644 v2/crates/homecore-automation/src/trigger.rs create mode 100644 v2/crates/homecore-hap/Cargo.toml create mode 100644 v2/crates/homecore-hap/src/accessory.rs create mode 100644 v2/crates/homecore-hap/src/bridge.rs create mode 100644 v2/crates/homecore-hap/src/error.rs create mode 100644 v2/crates/homecore-hap/src/lib.rs create mode 100644 v2/crates/homecore-hap/src/mapping.rs create mode 100644 v2/crates/homecore-hap/src/mdns.rs create mode 100644 v2/crates/homecore-hap/src/ruview.rs create mode 100644 v2/crates/homecore-migrate/Cargo.toml create mode 100644 v2/crates/homecore-migrate/src/automations.rs create mode 100644 v2/crates/homecore-migrate/src/cli.rs create mode 100644 v2/crates/homecore-migrate/src/config_entries.rs create mode 100644 v2/crates/homecore-migrate/src/device_registry.rs create mode 100644 v2/crates/homecore-migrate/src/entity_registry.rs create mode 100644 v2/crates/homecore-migrate/src/lib.rs create mode 100644 v2/crates/homecore-migrate/src/main.rs create mode 100644 v2/crates/homecore-migrate/src/secrets.rs create mode 100644 v2/crates/homecore-migrate/src/storage.rs create mode 100644 v2/crates/homecore-migrate/src/storage_format/mod.rs create mode 100644 v2/crates/homecore-migrate/src/storage_format/v13.rs create mode 100644 v2/crates/homecore-plugin-example/Cargo.lock create mode 100644 v2/crates/homecore-plugin-example/Cargo.toml create mode 100644 v2/crates/homecore-plugin-example/README.md create mode 100644 v2/crates/homecore-plugin-example/src/abi.rs create mode 100644 v2/crates/homecore-plugin-example/src/lib.rs create mode 100644 v2/crates/homecore-plugins/Cargo.toml create mode 100644 v2/crates/homecore-plugins/src/error.rs create mode 100644 v2/crates/homecore-plugins/src/host_abi.rs create mode 100644 v2/crates/homecore-plugins/src/lib.rs create mode 100644 v2/crates/homecore-plugins/src/manifest.rs create mode 100644 v2/crates/homecore-plugins/src/plugin.rs create mode 100644 v2/crates/homecore-plugins/src/registry.rs create mode 100644 v2/crates/homecore-plugins/src/runtime.rs create mode 100644 v2/crates/homecore-plugins/src/tests.rs create mode 100644 v2/crates/homecore-plugins/src/wasmtime_runtime.rs create mode 100644 v2/crates/homecore-plugins/tests/integration.rs create mode 100644 v2/crates/homecore-recorder/Cargo.toml create mode 100644 v2/crates/homecore-recorder/src/db.rs create mode 100644 v2/crates/homecore-recorder/src/dedup.rs create mode 100644 v2/crates/homecore-recorder/src/lib.rs create mode 100644 v2/crates/homecore-recorder/src/listener.rs create mode 100644 v2/crates/homecore-recorder/src/schema.rs create mode 100644 v2/crates/homecore-recorder/src/semantic.rs create mode 100644 v2/crates/homecore-server/Cargo.toml create mode 100644 v2/crates/homecore-server/src/main.rs create mode 100644 v2/crates/homecore/Cargo.toml create mode 100644 v2/crates/homecore/benches/state_machine.rs create mode 100644 v2/crates/homecore/src/bus.rs create mode 100644 v2/crates/homecore/src/entity.rs create mode 100644 v2/crates/homecore/src/event.rs create mode 100644 v2/crates/homecore/src/homecore.rs create mode 100644 v2/crates/homecore/src/lib.rs create mode 100644 v2/crates/homecore/src/registry.rs create mode 100644 v2/crates/homecore/src/service.rs create mode 100644 v2/crates/homecore/src/state.rs diff --git a/docs/adr/ADR-133-homecore-assist-ruflo.md b/docs/adr/ADR-133-homecore-assist-ruflo.md new file mode 100644 index 00000000..6a712e89 --- /dev/null +++ b/docs/adr/ADR-133-homecore-assist-ruflo.md @@ -0,0 +1,176 @@ +# ADR-133: HOMECORE-ASSIST — Voice/Intent Pipeline + Ruflo Agent Bridge + +| Field | Value | +|-------|-------| +| **Status** | Proposed | +| **Date** | 2026-05-25 | +| **Deciders** | ruv | +| **Codename** | **HOMECORE-ASSIST** | +| **Relates to** | [ADR-126](ADR-126-ruview-native-ha-port-master.md) (HOMECORE master), [ADR-127](ADR-127-homecore-state-machine-rust.md) (HOMECORE-CORE), [ADR-130](ADR-130-homecore-rest-websocket-api.md) (HOMECORE-API), [ADR-124](ADR-124-rvagent-mcp-ruvector-npm-integration.md) (SENSE-BRIDGE) | +| **Tracking issue** | TBD | +| **Crate** | `v2/crates/homecore-assist` | + +--- + +## 1. Context + +Home Assistant's Assist pipeline (`homeassistant/components/assist_pipeline/`) provides +voice-to-intent-to-response processing. It chains: + +1. **STT** (speech-to-text) — Whisper, cloud, or satellite +2. **NLU** (natural language understanding) — intent recognition via regex/slots +3. **Intent handler** — maps intent to a HA service call +4. **TTS** (text-to-speech) — synthesises the response for the caller + +HA's intent model (`homeassistant/helpers/intent.py`) is keyword/regex based. Every +intent is a named template with slot definitions and a handler that dispatches to HA +services. The built-in intents (`homeassistant/components/conversation/default_agent.py`) +cover `HassTurnOn`, `HassTurnOff`, `HassLightSet`, `HassNevermind`, `HassCancelAll`, +`HassGetState`, `HassGetWeather`, and many others. + +HOMECORE needs a wire-compatible Assist pipeline so that: +- The HA iOS/Android companion app's "Assist" button works against HOMECORE. +- The HOMECORE-API WebSocket `assist` command (ADR-130 §2.2) has a handler. +- The ruflo agent toolchain (ADR-124) can provide LLM-grade intent disambiguation as a + drop-in upgrade path for the P1 regex recognizer. + +### 1.1 Ruflo integration approach + +Ruflo's agent runner exposes an MCP-over-stdio interface (`node ruflo-agent.js`). +HOMECORE-ASSIST manages a long-lived subprocess (Q3 Windows concern below), sends +utterance JSON, and receives intent JSON back. In P1 we ship only the trait surface +and a `NoopRunner` stub; the real subprocess management is P2. + +### 1.2 Ruvector semantic intent matching (P2) + +`ruvector-core` provides embedding + cosine-similarity primitives. P2 will add a +`SemanticIntentRecognizer` that embeds the utterance and compares it to a HNSW index +of intent exemplars, falling back to the P1 regex recognizer when similarity < 0.75. +This is the mechanism that allows "dim the lights" to match `HassLightSet` without an +explicit regex entry. + +--- + +## 2. Design + +### 2.1 Module layout (`v2/crates/homecore-assist/`) + +| Module | Contents | +|--------|----------| +| `intent` | `IntentName` newtype, `Intent` (name + slots), `IntentResponse` (speech + optional card + optional data) | +| `recognizer` | `IntentRecognizer` trait; `RegexIntentRecognizer` (P1); `SemanticIntentRecognizer` stub (P2) | +| `handler` | `IntentHandler` trait; built-in handlers: `HassTurnOn`, `HassTurnOff`, `HassLightSet`, `HassNevermind`, `HassCancelAll` | +| `runner` | `RufloRunner` trait + `RufloRunnerOpts`; `NoopRunner` (P1 stub); real subprocess runner (P2) | +| `pipeline` | `AssistPipeline`: wires recognizer → handler → response; exposes `async fn process(utterance, language) -> IntentResponse` | + +### 2.2 Built-in intent handlers (P1) + +| Handler | HA service call | Slot | +|---------|-----------------|------| +| `HassTurnOn` | `homeassistant.turn_on` / `light.turn_on` / `switch.turn_on` | `entity_id` | +| `HassTurnOff` | `homeassistant.turn_off` / `light.turn_off` / `switch.turn_off` | `entity_id` | +| `HassLightSet` | `light.turn_on` | `entity_id`, `brightness` (0–255), `color_name` | +| `HassNevermind` | — (no-op, returns acknowledgement) | — | +| `HassCancelAll` | — (fires `homeassistant_stop_all_scripts` domain event) | — | + +### 2.3 IntentResponse + +```rust +pub struct IntentResponse { + pub speech: String, + pub card: Option, + pub data: Option, +} + +pub struct Card { + pub title: String, + pub content: String, +} +``` + +### 2.4 RufloRunner trait + +```rust +#[async_trait] +pub trait RufloRunner: Send + Sync + 'static { + async fn spawn(&mut self, opts: RufloRunnerOpts) -> Result<(), AssistError>; + async fn send_request(&self, payload: serde_json::Value) -> Result; + async fn shutdown(&mut self) -> Result<(), AssistError>; +} +``` + +`RufloResponse` is `{ intent: Option, speech: Option }`. + +### 2.5 Pipeline + +```rust +pub struct AssistPipeline { + recognizer: R, + handler: H, + runner: Option>, +} + +impl AssistPipeline { + pub async fn process(&self, utterance: &str, language: &str, hc: &HomeCore) + -> Result; +} +``` + +--- + +## 3. Questions & Answers + +### Q1 — Why not reuse HA's existing `homeassistant.helpers.intent` via PyO3? + +PyO3 bridges add a GIL lock on every cross-language call; the Assist pipeline processes +hundreds of short utterances per day from voice satellites. A native Rust recognizer is +simpler and faster. Python HA can still connect as an external integration via MQTT or +the HOMECORE WebSocket API. + +### Q2 — How does `RegexIntentRecognizer` handle ambiguity? + +Patterns are tried in registration order; the first match wins. Slot extraction uses +named capture groups. A future P2 upgrade can run all patterns, score them by slot +completeness, and return the highest-scoring match. + +### Q3 — Windows subprocess teardown (ruflo runner subprocess on Windows) + +`tokio::process::Child` on Windows does not automatically kill the child process when +the `Child` struct is dropped — `SIGTERM` is not a Windows concept, and `TerminateProcess` +is not called automatically. Options for P2: + +1. Call `child.start_kill()` in a `Drop` impl (requires a `Runtime` handle — tricky in sync Drop). +2. Wrap `Child` in an `Arc>>` and call `kill()` in an `async fn shutdown()`. +3. Use a Windows job object to bind the subprocess lifetime to the parent process. + +**P2 decision**: implement option 2 (explicit `async shutdown()`) + register a `tokio::signal` +handler for `Ctrl+C` / `SIGINT` that calls `shutdown()` before exit. Document the Windows caveat +in the crate README and in `runner.rs`. Job object approach (option 3) is deferred to P3 only +if option 2 proves insufficient in fleet testing. + +### Q4 — Why is `SemanticIntentRecognizer` a P2 stub? + +The ruvector HNSW index requires the vector store to be populated at startup with intent +exemplars. That startup path requires deciding on a serialization format (HNSW index files +vs. an in-memory array at compile time), which intersects with ADR-084 (RabitQ) and ADR-067 +(ruvector v2.0.5). P2 will define the exemplar format and populate the index. + +--- + +## 4. Consequences + +- **Positive**: HOMECORE-API `assist` WebSocket command gets a functional backend. +- **Positive**: Ruflo LLM pipelines can upgrade intent matching by swapping the `RufloRunner` impl. +- **Positive**: P1 ships with zero new heavy dependencies (no subprocess spawning, no ML runtime). +- **Negative**: Regex matching has limited coverage; long-tail utterances will return "I'm not sure". +- **Deferral**: ruvector semantic recognizer and real subprocess runner both land in P2. + +--- + +## 5. Implementation phases + +| Phase | Scope | +|-------|-------| +| **P1** (this ADR) | `intent`, `recognizer` (regex), `handler` (5 built-ins), `runner` (trait + noop), `pipeline` (end-to-end wiring), 10–15 tests | +| **P2** | Real `tokio::process::Child` runner with Windows-safe teardown; `SemanticIntentRecognizer` with ruvector HNSW | +| **P3** | STT/TTS bridge, satellite protocol, cloud fallback | diff --git a/docs/design/HOMECORE-FRONTEND-design-recon.md b/docs/design/HOMECORE-FRONTEND-design-recon.md new file mode 100644 index 00000000..13023229 --- /dev/null +++ b/docs/design/HOMECORE-FRONTEND-design-recon.md @@ -0,0 +1,301 @@ +# HOMECORE-FRONTEND Design Recon — ADR-131 + +**Source:** cognitum-one/v0-appliance dashboard at `http://cognitum-v0:9000/` +**Captured:** 2026-05-25 by browser-recon agent (session `20260525-181819-adr131-recon`) +**Pages fetched:** dashboard, cogs, seeds, edge, analytics, settings, cluster, tailscale, aidefence, guide (all HTTP 200) +**Auth:** dashboard is unauthenticated; `/api/*` requires bearer token — all recon confined to dashboard pages + +--- + +## 1. Color Palette + +The entire UI is dark-only. There is no light mode and no `prefers-color-scheme` media query anywhere in the stylesheet. Every surface is drawn from a tight family of near-black navy blues with two accent hues: a cool teal (`--primary`) and a green (`--accent`). + +### Core tokens (hex conversions from HSL source) + +| CSS variable | HSL value | Hex | Role | +|---|---|---|---| +| `--background` | `220 25% 6%` | `#0b0e13` | Page background, modal overlay base | +| `--foreground` | `210 20% 92%` | `#e6eaee` | Body text, headings | +| `--primary` | `185 80% 50%` | `#19d4e5` | Teal — active nav underline, CTA borders, ring focus, brand slash | +| `--primary-foreground` | `220 25% 6%` | `#0b0e13` | Text on filled primary buttons | +| `--accent` | `142 70% 50%` | `#26d867` | Green — secondary CTA, success state, deploy button text | +| `--accent-foreground` | `220 25% 6%` | `#0b0e13` | Text on filled accent buttons | +| `--secondary` | `220 20% 14%` | `#1c212a` | Button fill, pill-tab background | +| `--card` | `220 20% 10%` | `#14171e` | Card surface (also popover) | +| `--surface-elevated` | `220 20% 12%` | `#181c24` | Slightly elevated card variant | +| `--surface-overlay` | `220 20% 8%` | `#111318` | Modal scrim, sticky navbar | +| `--muted` | `220 15% 15%` | `#20242b` | Muted chip backgrounds, scrollbar track | +| `--muted-foreground` | `215 15% 55%` | `#7b899d` | Secondary text, labels, timestamps | +| `--border` | `220 15% 18%` | `#272b34` | All borders (at 50% opacity by default) | +| `--destructive` | `0 65% 50%` | `#d22c2c` | Error state, danger button | +| `--ring` | `185 80% 50%` | `#19d4e5` | Focus ring (same hue as primary) | + +### Semantic status colors (inline, not variables) + +| State | Color | Hex | Usage | +|---|---|---|---| +| Online / success | `hsl(142 70% 50%)` | `#26d867` | `.badge.online`, `.dot.up`, `.heat-cell.up` | +| Warning | `hsl(38 80% 60%)` | `#e69940` | `.badge.unpaired`, `.hero-dot.warn`, banner backgrounds | +| Error / offline | `hsl(0 65% 50%)` | `#d22c2c` | `.badge.offline`, `.badge.danger`, `.dot.down` | +| Info (log line) | `hsl(205 80% 65%)` | `#4db8f5` | Log viewer `.info` class | +| Paired | `hsl(185 80% 50%)` | `#19d4e5` | `.badge.paired` (same as primary) | + +--- + +## 2. Typography + +### Font families + +The CSS declares two font families via CSS custom properties: + +- `--font-display: 'Outfit', system-ui, sans-serif` — all headings, nav items, buttons, card titles, KPI values. Outfit is a modern geometric sans loaded locally (no Google Fonts outbound call; the source comment says "ship from local chrome.css fallback"). +- `--font-mono: 'JetBrains Mono', monospace` — timestamps, port numbers, version strings, table cells, log output, KPI labels, chip text. + +### Type scale + +| Token name / usage | Size | Weight | Notes | +|---|---|---|---| +| Hero title (`h1.hero-title`) | `clamp(1.5rem, 2.4vw, 2.1rem)` | 600 | Fluid, capped at ~33.6px | +| Page h1 (`.page`) | `1.5rem` (24px) | 600 | All inner pages | +| Section heading (`.row-h h2`) | `1.125rem` (18px) | 700 | Section openers on Cogs/Dashboard | +| Card title (`.card-title`) | `0.9375rem` (15px) | 600 | | +| Body / button | `0.8125rem` (13px) | 400/500 | Default body, nav links, buttons | +| Secondary body / lede | `0.875rem` (14px) | 400 | Page lede text | +| Small label | `0.75rem` (12px) | 400–600 | Table cells, modal sub-text | +| Micro label | `0.6875rem` (11px) | 600 | Section eyebrows, uppercase KPI labels, badge text | +| Mono micro | `0.625rem` (10px) | 400 | Heatmap cells, chip category text | + +Letter-spacing: `0.1em` on section eyebrows (`.section h2`), `0.08em` on filter-rail headings and chip category text, `-0.02em` on all `h1–h4` display headings. Line-height for body is `1.5`; lede text uses `1.45`. + +--- + +## 3. Layout Primitives + +### Page shell + +``` +┌─────────────────────────────────────────────────────────┐ +│ .appbar (sticky, z-50, backdrop-filter:blur(8px)) │ +│ [brand-mark] [brand-text] [nav links scrollable] │ +├─────────────────────────────────────────────────────────┤ +│ .wrap (max-width: 1400px, padding: 1.5rem 1.25rem) │ +│ ┌── .hero (full-width, gradient bg, radial accents) │ +│ ├── .kpi-grid (auto-fill, min 170px columns) │ +│ ├── .section > h2 (eyebrow) + content │ +│ └── .grid / .grid-2 / .grid-3 (auto-fit) │ +├─────────────────────────────────────────────────────────┤ +│ footer.appfoot (border-top, centered text) │ +└─────────────────────────────────────────────────────────┘ +``` + +**Appbar:** `position: sticky; top: 0; z-index: 50`. Background is the page background at 90% opacity with 8px blur backdrop-filter, so the page content bleeds through. Nav links overflow-scroll horizontally with a right-fade mask gradient. + +**Active nav state:** primary-colored text + a 2px bottom border line (`::after` pseudo-element) positioned at bottom: -2px of the link. Hover reveals secondary background fill on the link. + +**Content wrap:** max-width 1400px, centered, 1.25rem horizontal padding. Inner page sections are separated by margin-bottom spacing in multiples of 0.75rem (base unit = 12px at 16px root). + +### Cogs page: app-store sub-navigation + +The Cogs page adds a sticky secondary nav bar (`.subnav`) at `top: 3.25rem` (just below the appbar). Tabs are borderless buttons with a 2px bottom underline indicator when active. A `flex: 1` spacer pushes a gear icon to the right edge. + +### Card patterns + +Three card variants, all sharing the same surface gradient and border: + +1. **Standard card (`.card`)** — `background: var(--gradient-card)` (linear 180deg from `--surface-elevated` to `--surface-overlay`), 1px border at 50% opacity, `--radius` (0.75rem), `box-shadow` 8px/32px dark drop shadow. +2. **KPI card (`.kpi`)** — 38px icon square left + text right, same gradient, 1rem/1.125rem padding, smaller vertical rhythm. +3. **Empty-state card (`.empty-card`)** — dashed 1px border (instead of solid), centered text, optional compact variant. The headline in `.empty-card h3` uses the primary teal, body explains what to do next. + +### Spacing rhythm + +Base unit is 4px. Gaps between grid items are universally `0.75rem` (12px). Card padding is `1.25rem` (20px) for standard, `0.875rem` (14px) for compact. Section margin-bottom is `1.5rem` (24px). The hero section uses `1.75rem` (28px) horizontal padding. + +--- + +## 4. Component Vocabulary + +### Navigation components + +- **Appbar** — sticky top bar with brand + horizontal nav links. Brand mark is a 32px rounded SVG icon square. +- **Nav link** — 0.4rem × 0.7rem padding, 0.4rem radius, transitions on color + background. Active state: primary text + 2px underline pseudo-element. Mobile: wraps below brand row at 720px. +- **Sub-nav / secondary tab bar** (`.subnav`) — app-store style horizontal tab strip, sticky under appbar. Used exclusively on Cogs. +- **Pill tabs** (`.pill-tabs` + `.pill-tab`) — smaller rounded-rect tab group for in-card filter switching. Active state fills with primary color. +- **Page tabs** (`.page-tabs`) — used on Analytics for domain view switching. Underline-style, same pattern as sub-nav but at content level. + +### Card & data display + +- **Card** (`.card`) — base data container with gradient surface, subtle border, shadow. +- **KPI tile** (`.kpi`, `.kpi-tile`) — metric display with icon, label (uppercase micro mono), large value, and optional sub-line. Two variants: `.kpi` (icon-left layout) and `.kpi-tile` (stack layout, used on Seeds/Edge/AIDefence). +- **Node card** (`.node`) — cluster member card with mono metadata rows. Key-value pairs in `.node-meta` with dimmed label prefix (`.l` class). +- **Cog card** (`.cog`) — product-catalog card with emoji icon, name, description, category chips, and a "Get" pill button. Hover lifts 2px with primary glow border. +- **Pick card** (`.pick-card`) — horizontal-scroll featured card (220px fixed width), snap-scroll container. Smaller emoji + name + category + pill CTA. +- **Category tile small** (`.cat-tile-sm`) — 180px min-width grid item, emoji + name + count. +- **Category tile large** (`.cat-tile-big`) — 16:9 aspect-ratio card, full-bleed with gradient per category. +- **Nav tile** (`.nav-tile`) — dashboard home navigation card with icon square, title, description, and a chevron arrow that translates +2px on hover. +- **Architecture action card** (`.arch-card`, `.arch-action-card`) — setup wizard launcher cards on the dashboard. + +### Status & feedback + +- **Badge** (`.badge`) — pill with 1px border, 11px mono text. Variants: `role-master` (teal), `role-worker` (green), `online` (green), `offline` (red), `unknown` (muted), `paired` (teal), `unpaired` (amber), `danger` (red). +- **Dot** (`.dot`) — 8px circle status indicator. `.up` glows green with box-shadow, `.down` is red, default is muted gray. +- **Hero dot** (`.hero-dot`) — 7px circle in the dashboard hero status row. Same three states: `.ok` (green glow), `.warn` (amber glow), `.down` (red glow). +- **Op-pill** (`.op-pill`) — "operational status" pill with colored dot inside. Used in dashboard architecture hub. +- **AI pill / status chip** (`.pill` on AIDefence, `.md-badge` in cluster) — inline classification badge at 0.68rem. States: `.ok`, `.warn`, `.bad`. +- **Chip** (`.chip`) — tiny category/difficulty label, all-caps, 0.5625rem, pill-shaped. Category-colored variants (`.cat-ai`, `.cat-health`, `.cat-security`, etc.) each get a hue-appropriate 15% opacity background. + +### Actions + +- **Button** (`.btn`) — 0.5rem × 0.875rem padding, 0.4rem radius, secondary fill. Variants: `.primary` (filled teal, 600 weight, box-shadow), `.outline` (transparent fill), `.danger` (red tint), `.sm` (compact). +- **Hero button** (`.hero-btn`) — slightly larger, display-font, 0.9rem padding, glass-effect dark fill. `.primary` variant uses the green accent gradient. +- **Pill CTA** (`.get`, `.pget`) — full pill-radius (9999px), primary-tint background at rest, fills solid on hover. Used on cog cards and pick cards. +- **Gear button** (`.gear-btn`) — icon-only square button, transparent at rest, border appears on hover. +- **Context menu** (`.ctx-menu`) — dark card dropdown (min-width 180px), each item is a full-width button with secondary hover fill. +- **Copy button** (`.copy-btn`) — positioned absolute in `.copy-row`, 0.7rem opacity at rest, `.copied` state turns green/accent. + +### Forms & inputs + +- **Input** — all ``, `