docs/adr/ADR-125-ruview-apple-home-native-hap-bridge.md
| Field | Value |
|---|---|
| Status | Proposed |
| Date | 2026-05-25 |
| Deciders | ruv |
| Codename | APPLE-FABRIC — RuView speaks HomeKit directly so Apple HomePod / Apple TV act as the discovery + automation surface with zero Home-Assistant middle layer |
| Relates to | ADR-115 (HA-DISCO MQTT publisher), ADR-116 (cog-ha-matter §P7 left HAP/Matter as a feature-flag stub), ADR-118 (BFLD presence + identity-risk events), ADR-122 (BFLD HA/Matter exposure) |
| Tracking issue | TBD |
A naive integration tries to push data to a HomePod — open a socket, send a JSON-RPC, call an MQTT topic on homepod.local. Apple intentionally does not expose that surface. The HomePod is not an endpoint; it is the Home Hub + Matter Controller + HomeKit Controller + Siri endpoint for the Apple Home ecosystem on the LAN. It discovers accessories that advertise themselves on the local network via Bonjour/mDNS using the HomeKit Accessory Protocol (HAP) or Matter.
The correct direction of flow is therefore:
RuView / Seed
↓ (advertise HAP / Matter accessory on LAN)
HomeKit / Matter accessory
↓ (mDNS discovery)
HomePod
↓ (forwards to Apple Home automation graph)
Apple Home ecosystem (iPhone, Watch, Mac, Siri, automations)
ADR-115 ships an MQTT auto-discovery publisher that talks to Home Assistant. ADR-116's cog-ha-matter Cognitum cog wraps that publisher into a Seed-installable artifact with mDNS, an embedded rumqttd broker, RuVector-backed thresholds, and an Ed25519 witness chain. ADR-122 explicitly extends the same publisher with the BFLD presence / identity-risk / Soul-Match topics so a Home Assistant install sees them as auto-discovered entities. The current path to HomePod therefore runs:
RuView sensing-server ──► cog-ha-matter (MQTT HA-DISCO + HA-MIND)
↓
Home Assistant broker
↓
Home Assistant HomeKit Bridge add-on
↓
HomePod
This works and the auto-discovery is real, but it introduces a hard dependency: an operator must run Home Assistant, install its HomeKit Bridge integration, and pair the bridge in the Apple Home app. The Seed alone does not appear in Apple Home.
ADR-116 §P7 anticipated this — the cog-ha-matter Cargo.toml already carries a matter = [] feature stub with the comment "matter-rs is added in P7; intentionally absent in P1 to keep the dep surface small until the SDK choice is validated." This ADR closes that box.
Three forces line up in 2026-05:
@ruvnet/rvagent (ADR-124) is on npm. The MCP surface that lets agents query RuView is live. A first-class Apple-Home presence widens RuView's reach from "agents that speak MCP" to "anyone with an iPhone and a HomePod" — the consumer wedge.cog-ha-matter (this branch's Dockerfile.rust change, see #794) — the runtime where a HAP advertiser would live is finally a single-image deployment.The combination is asymmetric:
| Layer | RuView contributes | Apple Home contributes |
|---|---|---|
| Sensing | Passive RF presence, breathing, heart rate, fall risk, BFLD identity-risk, through-wall occupancy, longitudinal wellness | (none — Apple has no native RF sensing surface) |
| Adoption | (limited — researcher-grade hardware today) | iPhone, Watch, Mac, HomePod, Apple TV installed base; consumer trust; voice; on-device intelligence |
| UX | (utility CLI + a Web UI) | Home app, Siri, automation engine, notifications, accessibility |
| Trust | Ed25519 witness chain, privacy class gate, local-first | Apple HomeKit local pairing, end-to-end encrypted, no cloud requirement |
RuView supplies the invisible cognition layer Apple cannot provide on its own; Apple supplies the distribution and UX that an open sensing stack cannot bootstrap. Direct HAP integration removes the only structural barrier between those two layers — Home Assistant as a mandatory intermediary.
Ship a native HomeKit / Matter accessory in the Seed runtime so a freshly-imaged Cognitum Seed appears in the Apple Home app under Add Accessory → More Options with zero Home-Assistant dependency.
Concretely:
hap-accessory workspace component that advertises a set of HomeKit characteristics over mDNS using HAP-1.1 (HomeKit Accessory Protocol).wifi-densepose-sensing-server's WebSocket / BFLD MqttEvent stream and maps each privacy-class-2/3 event onto a HomeKit characteristic update.sensing-server and cog-ha-matter ships the new advertiser as a third entrypoint:docker run --network host ruvnet/wifi-densepose:latest hap-accessory --privacy-mode
--network host (or a macvlan bridge) is required because HAP pairing depends on the accessory and the controller seeing each other's mDNS broadcasts on the same L2 segment — same constraint Home Assistant's HomeKit Bridge has.
Add a tiny Python entrypoint bridges/hap-python/ruview_hap.py using the well-maintained HAP-python library. The Dockerfile gets a thin Python runtime stage; the entrypoint script polls sensing-server over HTTP and pushes characteristic updates into the HAP loop.
# bridges/hap-python/ruview_hap.py (≈80 LOC)
from pyhap.accessory import Accessory
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import CATEGORY_SENSOR
import urllib.request, json, threading, time
SENSING_URL = "http://127.0.0.1:3000/api/v1"
class RuViewSensor(Accessory):
category = CATEGORY_SENSOR
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
s_motion = self.add_preload_service('MotionSensor')
self.c_motion = s_motion.configure_char('MotionDetected')
s_occ = self.add_preload_service('OccupancySensor')
self.c_occ = s_occ.configure_char('OccupancyDetected')
s_temp = self.add_preload_service('TemperatureSensor')
self.c_temp = s_temp.configure_char('CurrentTemperature')
threading.Thread(target=self._poll, daemon=True).start()
def _poll(self):
while True:
try:
v = json.loads(urllib.request.urlopen(f"{SENSING_URL}/vitals").read())
self.c_motion.set_value(bool(v.get("motion_present")))
self.c_occ.set_value(int(bool(v.get("occupancy"))))
if "ambient_temp_c" in v:
self.c_temp.set_value(v["ambient_temp_c"])
except Exception:
pass
time.sleep(1.0)
driver = AccessoryDriver(port=51826)
driver.add_accessory(accessory=RuViewSensor(driver, 'RuView Sense'))
driver.start()
Pairing flow on the operator's iPhone:
Add Accessory → More OptionsRuView Sense (appears via mDNS automatically)docker logs (or pinned in env)Replace the motion_present / occupancy mappings progressively as RuView capabilities mature: BFLD class-2 presence event → OccupancyDetected; BFLD class-3 identity_risk_score > threshold → SecuritySystemCurrentState; breathing_present → OccupancyDetected (sleep room); fall_risk → a programmable switch that fires an Apple Home automation.
Acceptance criteria for 2.1.a:
docker run ... hap-accessory --privacy-mode advertises an _hap._tcp service that the HomePod sees within 30s (dns-sd -B _hap._tcp local. on a peer Mac shows RuView Sense).MotionDetected flips within 2 s of an actual RF presence detection from a calibrated ESP32 source (CSI_SOURCE=esp32)./var/lib/ruview-hap/).--privacy-mode when RUVIEW_BFLD_PRIVACY_CLASS is unset, matching the structural invariant I1 (Raw BFI never exits the node — ADR-118 §2.2).Wire one of the maintained Rust HAP crates into cog-ha-matter so the Python sidecar can be removed. Candidate crates:
hap (Sebastian Schmidt) — last published 0.1.0-pre.16, MIT, active in 2024, supports HAP-1.1, has examples for MotionSensor, LightBulb, OccupancySensor. First choice.accessory-server — narrower scope, fewer servicesmatter-rs crate from project-chip — once stable (CHIP SDK Rust bindings are still emerging in 2026-05)The matter = [] feature stub in cog-ha-matter/Cargo.toml (added in ADR-116 P1) becomes:
[features]
default = []
mqtt = ["dep:rumqttc"]
matter = ["dep:hap"] # ADR-125 §2.1.b
with a runtime subcommand cog-ha-matter --mode hap that mirrors the Python advertiser's accessory set. Single binary, no Python interpreter in the image, matches the all-Rust ethos of the Cognitum Seed (ADR-116 §1.4).
The advertiser publishes a single HAP bridge (RuView Sense) that owns N child accessories — one per logical sensor surface (presence-bedroom, presence-office, vitals-bedroom, semantic-events, …). Operators pair the bridge once; child accessories appear automatically and can be re-assigned to rooms in the Apple Home app.
The alternative — N independent accessories each advertised separately — was rejected. It forces operators to pair RuView once per room (RuView Bedroom, RuView Office, RuView Wellness, RuView Presence, …), which becomes messy after the second or third room, and diverges from how every reference HomeKit accessory in the Home app behaves (a Hue bridge with bulbs, an Eve Energy bridge, etc.). Single pairing also makes container restart / re-image trivial — one persisted pairing key, not N.
identity_risk_score is a continuous 0..1 confidence from the BFLD identity-features pipeline (ADR-121 §2.6). It must NOT cross the HomeKit boundary as a raw value, and must NOT be wired to SecuritySystemCurrentState. Apple-Home users read security-system state as "intruder detected" — exposing a probability there turns RuView into surveillance UX with all the false-positive blame that entails.
Instead, the bridge exposes thresholded semantic events that read like ambient awareness, not threat detection:
| Semantic event | HomeKit primitive | Trigger (illustrative) |
|---|---|---|
Unknown Presence | MotionSensor (programmable; stateful) | BFLD class-2 presence + no matching SoulMatch oracle hit (ADR-121 §2.6) for > 30 s |
Unexpected Occupancy | OccupancySensor (programmable) | Occupancy in a room outside its operator-defined "expected schedule" window |
Unrecognized Activity Pattern | Programmable Switch (stateful, momentary) | BFLD longitudinal drift gate (ADR-118 §2.3 / ADR-122 §2.7) fires Reject or Recalibrate |
What stays internal:
identity_risk_score (numeric 0..1) — never publishedrf_signature_hash — never published (already enforced by ADR-118 §2.5 / ADR-122 §2.4 — this is the structural invariant restated at the HAP boundary)The naming is the contract. "Unknown Presence" is who's-here-and-it's-fine-but-worth-noting; an end user will write an automation ("turn on the porch light when Unknown Presence is detected after 9pm") without ever thinking it accuses anyone of being an intruder. That semantic framing is the difference between RuView becoming the calm-tech ambient substrate Apple Home needs vs. another paranoid surveillance widget.
This is the part of the ADR that determines whether RuView's HomeKit story ages well or generates the wrong kind of headlines.
Raw) BFI exposure. Structural invariant I1 (ADR-118 §2.2) holds. Only privacy-class-2 (Anonymous) and class-3 (Restricted) frames may be mapped onto HomeKit characteristics. The advertiser refuses to start in any other mode.bridges/hap-python/ becomes an archived reference implementation.RuView Sense in the Home app within seconds of docker run. No HA, no MQTT broker, no Home-Assistant HomeKit Bridge add-on.--network host or a macvlan — same constraint HA's HomeKit Bridge has, but worth documenting./var/lib/ruview-hap/ to a persistent location.MqttEvent stream (which is already gated by PrivacyGate per ADR-120), never raw BFI; tests assert this in the same style as ADR-122 §4.3.The advertiser is a separate entrypoint — pulling it out is docker run without the hap-accessory first-arg, identical to today's behavior. Zero impact on sensing-server and cog-ha-matter operations.
# 1. Start a sensing server (simulated source so the test runs anywhere)
docker run -d --name rs -p 3000:3000 -e CSI_SOURCE=simulated \
ruvnet/wifi-densepose:latest
# 2. Launch the HAP advertiser sidecar in privacy mode
docker run -d --name hap --network host \
-v /var/lib/ruview-hap:/var/lib/ruview-hap \
-e RUVIEW_BFLD_PRIVACY_CLASS=2 \
ruvnet/wifi-densepose:latest hap-accessory --privacy-mode
# 3. From a Mac on the same LAN: should see RuView Sense as HAP
dns-sd -B _hap._tcp local. # expect: "RuView Sense" within 30 s
# 4. From iPhone Home app: Add Accessory → More Options → RuView Sense
# Enter setup code from `docker logs hap`
# Expect: pairing completes, entity appears in selected Room
# 5. Cycle the container; re-open Home app: entity is still paired
docker restart hap
# Expect: no re-pairing prompt; characteristic updates resume
Two questions from the original draft were resolved during review (§2.1.c and §2.1.d). Genuinely-open questions that follow-up PRs will close:
cog-ha-matter Seed cog (this is where the matter feature stub lives)hap (Rust) — https://crates.io/crates/hap