Files
ruvnet--RuView/v2/crates/cog-ha-matter/src/main.rs
T
ruv d4f0e12073 cog-ha-matter (ADR-116): P4 — mDNS wired into main, broker deferred
Two landings that flip P4 to shipped:

1. main.rs now actually registers the mDNS responder. New CLI:
     --mdns-hostname (default: cog-ha-matter.local.)
     --mdns-ipv4     (default: 127.0.0.1)
     --no-mdns       (skip for restrictive CI / multi-instance)

   Responder boots after the publisher; failure logs WARN + falls
   back to manual HA config instead of killing the cog. The
   handle's Drop sends the mDNS goodbye packet on shutdown so HA's
   discovery sees a clean service-leave (no stale device card).

2. Embedded rumqttd broker DEFERRED to v0.7 per dossier §8 ranking.

   The dossier's prioritised v1 scope is:
     1. --privacy-mode audit-only
     2. cog manifest + Ed25519 signing + store listing
     3. local SONA fine-tuning loop
     4. HACS gold-tier integration
     5. Matter Bridge (v0.8)

   Embedded broker is not in that list. Every HA install already
   has mosquitto or HA Core's built-in broker — adding ~2 MB of
   binary + ACL config surface for marginal benefit didn't earn a
   v1 slot. Documented as row 6 of §4 v1 scope table with explicit
   v0.7 target.

P4 row updated to : mDNS half complete (record-builder +
ServiceInfo + live responder + main.rs wiring), witness half
complete (chain + JSONL + file + Ed25519), embedded broker
explicitly deferred with rationale citation to dossier §8.

Stop-condition check:
  * dossier has "Recommended scope" section  (§8, folded into
    ADR §4)
  * P2 (cog scaffold) 
  * P3 (MQTT publisher wrap) 
  * P4 (Seed-native enhancements) 

Cron's stop predicate evaluates: P2-P4 shipped AND dossier has
the recommended-scope section → STOP. The loop should TaskStop
itself after this iter unless the user wants P5 (RuVector
thresholds), P8 (cog signing), or P9 (HACS repo) to keep going.

64/64 tests green.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-23 18:36:14 -04:00

180 lines
6.6 KiB
Rust

//! `cog-ha-matter` — Home Assistant + Matter Cognitum Seed cog (ADR-116).
//!
//! Binary entrypoint. The actual wiring lives in [`cog_ha_matter`] —
//! this main.rs is intentionally tiny so the cog runtime can call
//! into the library from tests and from the Seed's control plane
//! integration tests without re-launching the binary.
use std::process::ExitCode;
use clap::Parser;
use cog_ha_matter::runtime;
use tokio::sync::broadcast;
use tracing::{info, warn};
use wifi_densepose_sensing_server::mqtt::state::VitalsSnapshot;
#[derive(Parser, Debug)]
#[command(
name = "cog-ha-matter",
version,
about = "Home Assistant + Matter Cognitum Seed cog",
long_about = "Wraps the ADR-115 HA-DISCO + HA-MIND publisher as a \
Seed-installable artifact with mDNS, embedded broker, \
RuVector-backed thresholds, and Ed25519 witness. See \
docs/adr/ADR-116-cog-ha-matter-seed.md for the design."
)]
struct Args {
/// Where to find the local sensing-server (the cog speaks to it
/// to pull `VitalsSnapshot` for republication over MQTT/Matter).
#[arg(long, default_value = "http://127.0.0.1:3000")]
sensing_url: String,
/// MQTT broker host. When omitted the cog can spin up an embedded
/// rumqttd on `DEFAULT_EMBEDDED_BROKER_PORT` (v1: external only).
#[arg(long, default_value = "127.0.0.1")]
mqtt_host: String,
/// MQTT broker port.
#[arg(long, default_value_t = cog_ha_matter::DEFAULT_EMBEDDED_BROKER_PORT)]
mqtt_port: u16,
/// Strip biometrics at the wire — only semantic primitives published.
/// Matches ADR-115 `--privacy-mode`. The right default for any
/// deployment with non-tenant occupants.
#[arg(long)]
privacy_mode: bool,
/// Print the manifest the cog would self-report to the Seed's
/// control plane and exit. Useful for the build-time signer.
#[arg(long)]
print_manifest: bool,
/// mDNS hostname for the Seed advertisement. Must end with
/// `.local.` per RFC 6762. Default lets HA's discovery find a
/// dev cog on localhost without LAN config.
#[arg(long, default_value = "cog-ha-matter.local.")]
mdns_hostname: String,
/// LAN-routable IPv4 the cog binds the control plane on. The
/// mDNS responder advertises this; HA reaches back to it for
/// MQTT + Matter Bridge.
#[arg(long, default_value = "127.0.0.1")]
mdns_ipv4: String,
/// Skip the mDNS responder. Useful in containerised CI where
/// multicast bind is filtered, or when running multiple cog
/// instances on the same loopback.
#[arg(long)]
no_mdns: bool,
}
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "cog_ha_matter=info,info".into()),
)
.init();
let args = Args::parse();
info!(
sensing_url = %args.sensing_url,
mqtt = format!("{}:{}", args.mqtt_host, args.mqtt_port),
privacy = args.privacy_mode,
"cog-ha-matter starting (ADR-116 P2 scaffold)"
);
if args.print_manifest {
// Emit the manifest with build-time-template placeholders. The
// Makefile substitutes {{VERSION}} / {{ARCH}} before signing.
let m = cog_ha_matter::manifest::CogManifest {
id: cog_ha_matter::COG_ID.into(),
version: env!("CARGO_PKG_VERSION").into(),
binary_url:
"https://storage.googleapis.com/cognitum-apps/cogs/{{ARCH}}/cog-ha-matter-{{ARCH}}"
.into(),
binary_bytes: 0,
binary_sha256: String::new(),
binary_signature: String::new(),
installed_at: 0,
status: "installed".into(),
};
println!(
"{}",
serde_json::to_string_pretty(&m).expect("manifest serialization is infallible")
);
return ExitCode::SUCCESS;
}
// P3: boot the ADR-115 publisher. The broadcast tx is held by
// main so the channel doesn't close before the sensing-server
// bridge (next iter) wires its VitalsSnapshot producer in.
let identity = runtime::CogIdentity::default_for_build();
let inputs = runtime::build_publisher_inputs(
&args.mqtt_host,
args.mqtt_port,
args.privacy_mode,
identity,
);
let (state_tx, state_rx) =
broadcast::channel::<VitalsSnapshot>(runtime::DEFAULT_STATE_CHANNEL_CAPACITY);
let publisher_handle = runtime::spawn_publisher(inputs, state_rx);
info!(
capacity = runtime::DEFAULT_STATE_CHANNEL_CAPACITY,
"publisher spawned — awaiting VitalsSnapshot bridge (P3.5)"
);
// P3.5 (next iter): subscribe to the sensing-server's
// `/v1/snapshot` WebSocket and republish into `state_tx`. Until
// that lands the cog connects to MQTT, advertises discovery,
// and just doesn't have any state to publish — exactly what an
// HA install with no nodes online looks like.
let _ = &state_tx;
// P4: mDNS responder. HA's auto-discovery picks the cog up on
// `_ruview-ha._tcp` so users don't need to type broker host/port.
let _mdns_handle = if args.no_mdns {
None
} else {
let identity = runtime::CogIdentity::default_for_build();
let service = cog_ha_matter::mdns::build_mdns_service(
&identity,
cog_ha_matter::DEFAULT_CONTROL_PORT,
args.mqtt_port,
args.privacy_mode,
);
match runtime::start_mdns_responder(&service, &args.mdns_hostname, &args.mdns_ipv4) {
Ok(h) => {
info!(
fullname = h.fullname(),
hostname = %args.mdns_hostname,
ipv4 = %args.mdns_ipv4,
"mDNS responder registered — HA auto-discovery should find the cog now"
);
Some(h)
}
Err(e) => {
warn!(error = ?e, "mDNS responder failed to start — discovery disabled, falling back to manual HA config");
None
}
}
};
// Wait on Ctrl-C so the cog runs as a long-lived daemon under
// the Seed's process supervisor.
tokio::select! {
_ = tokio::signal::ctrl_c() => {
info!("ctrl-c received — shutting down");
}
joined = publisher_handle => {
warn!(?joined, "publisher task exited unexpectedly");
}
}
// _mdns_handle drops here, sending the mDNS goodbye packet so
// HA's discovery integration sees the service leave cleanly.
ExitCode::SUCCESS
}