diff --git a/docs/turn-loop/config.md b/docs/turn-loop/config.md index 044bed95..0cfba598 100644 --- a/docs/turn-loop/config.md +++ b/docs/turn-loop/config.md @@ -132,7 +132,7 @@ setups. ```nix hyperhive.forge.url = "http://forge.example:3000"; # default: null -hyperhive.matrix.url = "http://localhost:8008"; # default +hyperhive.matrix.url = "https://matrix.example"; # default: null ``` **`hyperhive.forge.url`** — base URL of the Forgejo instance. Used by @@ -155,12 +155,22 @@ flake without one, so `null` only survives where the agent modules are evaluated outside a hive. **`hyperhive.matrix.url`** — homeserver URL used by -`hive-matrix-daemon` when connecting via the matrix-sdk. Default -(`localhost:8008`) is overridden by hive-c0re at deploy time to the -gateway-routed `matrix.` URL so isolated agents can reach the -homeserver. Override per-agent when an agent should talk to a -different homeserver — for example a remote hive's tuwunel reached -over a VPN, or an external Matrix server for a federation-only agent. +`hive-matrix-daemon` when connecting via the matrix-sdk. hive-c0re +writes it into every agent at deploy time as the gateway-routed +`matrix.` URL, so isolated agents can reach the homeserver. +Override per-agent when an agent should talk to a different homeserver +— for example a remote hive's tuwunel reached over a VPN, or an +external Matrix server for a federation-only agent. + +**Defaults to `null`, meaning "no matrix" — for the same reason +`forge.url` does.** The homeserver may live on another host, and a +loopback default resolves inside the agent's own netns to the agent, +so it would be a value that evaluates fine and then talks to the wrong +machine. With `null` the daemon has no homeserver and no-ops exactly as +it does without a token. The hive only forwards `HIVE_MATRIX_URL` when +it actually has a matrix vhost to name, so `null` survives where a hive +runs no homeserver, or where the agent modules are evaluated outside a +hive. ## Claude Code plugins diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index eff37f84..4f98ebc9 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -18,9 +18,14 @@ use crate::coordinator::Coordinator; /// `hive-forge` and matches the bare-name allow-list the lifecycle /// scanner skips over. const MATRIX_CONTAINER: &str = "hive-matrix"; -/// Local-host URL of the tuwunel client-server API. Shares the host -/// netns so `localhost:` resolves both from the daemon and from -/// inside any sub-agent container. +/// Local-host URL of the tuwunel client-server API, for **this daemon +/// only**: hive-c0re and the homeserver share the host netns, so +/// `localhost:` reaches it from here. +/// +/// Deliberately not an agent-facing address. An agent has its own netns, +/// where `localhost` is the agent — agents are handed the gateway vhost +/// (`matrix.`) via `HIVE_MATRIX_URL` instead, and get nothing at +/// all when the hive has no vhost to offer. const MATRIX_HTTP: &str = "http://localhost:8008"; /// Length (bytes) of the random registration token. 32 raw bytes ⇒ /// 64-char hex string; comfortable for a long-lived shared secret. diff --git a/hive-matrix-mcp/src/accounts.rs b/hive-matrix-mcp/src/accounts.rs index 8e2ec5fd..d7cdb9ec 100644 --- a/hive-matrix-mcp/src/accounts.rs +++ b/hive-matrix-mcp/src/accounts.rs @@ -46,13 +46,15 @@ pub struct AccountCfg { } impl AccountCfg { - /// Resolve the effective homeserver URL (per-account override or - /// the daemon-wide default). + /// The effective homeserver URL — this account's own, else the + /// daemon-wide `HIVE_MATRIX_URL` — or `None` when neither is set. + /// + /// `None` is a real answer, not a failure: the account is skipped, the + /// same way [`discover_token_accounts_in`] already skips a discovered + /// token whose homeserver sidecar is missing. #[must_use] - pub fn homeserver(&self) -> String { - self.homeserver - .clone() - .unwrap_or_else(paths::homeserver_url) + pub fn homeserver(&self) -> Option { + self.homeserver.clone().or_else(paths::homeserver_url) } } diff --git a/hive-matrix-mcp/src/main.rs b/hive-matrix-mcp/src/main.rs index 73b76995..3bbb77ab 100644 --- a/hive-matrix-mcp/src/main.rs +++ b/hive-matrix-mcp/src/main.rs @@ -265,7 +265,17 @@ async fn bring_up_account( tag: Option, is_primary: bool, ) -> Result> { - let homeserver = cfg.homeserver(); + let Some(homeserver) = cfg.homeserver() else { + // No homeserver for this account: the hive has none to offer (no + // matrix vhost) or this agent's `hyperhive.matrix.url` is null. Same + // no-op as a missing token — an absent integration, not a guess at + // one. + tracing::info!( + account = %cfg.name, + "no homeserver configured (HIVE_MATRIX_URL unset); skipping account" + ); + return Ok(None); + }; if !tokio::fs::try_exists(&cfg.token_file) .await .unwrap_or(false) diff --git a/hive-matrix-mcp/src/paths.rs b/hive-matrix-mcp/src/paths.rs index cac9bbf1..bb6e8f59 100644 --- a/hive-matrix-mcp/src/paths.rs +++ b/hive-matrix-mcp/src/paths.rs @@ -6,12 +6,6 @@ use std::path::PathBuf; -/// Default homeserver URL when `HIVE_MATRIX_URL` isn't set. Tuwunel -/// (the local hive-matrix container) listens on `localhost:8008` by -/// default; shared host netns means every agent container resolves -/// `localhost` to the same machine. -pub const DEFAULT_HOMESERVER: &str = "http://localhost:8008"; - /// Resolve the matrix access-token file path. Override via /// `HIVE_MATRIX_TOKEN_FILE`; default is `/matrix-token`, /// the path `hive-c0re::matrix::ensure_user_for` writes to on agent @@ -25,11 +19,19 @@ pub fn token_file() -> PathBuf { PathBuf::from(format!("{state_dir}/matrix-token")) } -/// Resolve the homeserver URL. Override via `HIVE_MATRIX_URL`; default -/// is the in-container `localhost:8008` tuwunel. +/// Resolve the homeserver URL from `HIVE_MATRIX_URL`, or `None` when the +/// harness didn't set one. +/// +/// There is deliberately no built-in default. A hardcoded `localhost:8008` +/// used to stand in here, on the reasoning that the tuwunel container shares +/// the host netns — but *this daemon runs inside an agent*, which does not, so +/// that address named the agent itself. Unset means the hive has no homeserver +/// to offer this agent, and the daemon no-ops exactly as it does without a +/// token; guessing would be a value that starts fine and then talks to the +/// wrong machine. #[must_use] -pub fn homeserver_url() -> String { - std::env::var("HIVE_MATRIX_URL").unwrap_or_else(|_| DEFAULT_HOMESERVER.to_owned()) +pub fn homeserver_url() -> Option { + std::env::var("HIVE_MATRIX_URL").ok() } /// Persistent sqlite store directory for matrix-sdk's state (event diff --git a/nix/agent-modules/matrix.nix b/nix/agent-modules/matrix.nix index efcf39fd..23f5d3d2 100644 --- a/nix/agent-modules/matrix.nix +++ b/nix/agent-modules/matrix.nix @@ -11,12 +11,6 @@ }: let userName = config.hyperhive.user.name; - # Single source of truth for the default matrix homeserver URL, shared - # by the `hyperhive.matrix.url` option default and the daemon-unit guard - # that decides whether to set a unit-level HIVE_MATRIX_URL (so the two - # cannot drift). Matches the daemon's own built-in default - # (`paths::DEFAULT_HOMESERVER`). - matrixUrlDefault = "http://localhost:8008"; # Rasterize the operator-set agent icon (`hyperhive.icon`, an SVG) to a # 512x512 PNG so the matrix daemon can upload it as each account's avatar # over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar). @@ -36,13 +30,13 @@ in When true (the default), the harness: - runs `hive-matrix-daemon` as a systemd unit that holds a - matrix-sdk Client + sync against the homeserver at - `HIVE_MATRIX_URL` (default `http://localhost:8008` — the - in-host tuwunel from `nix/host-modules/hive-matrix.nix`). The - daemon auto-skips when `/matrix-token` is missing, - and a `systemd.paths` watcher restarts it the moment - hive-c0re provisions the token (same path-trigger shape - as `forge-avatar-sync`). + matrix-sdk Client + sync against the homeserver named by + `HIVE_MATRIX_URL` (see `hyperhive.matrix.url` — there is no + default, since an agent's own netns makes a loopback guess + wrong). The daemon auto-skips when that URL or + `/matrix-token` is missing, and a `systemd.paths` + watcher restarts it the moment hive-c0re provisions the token + (same path-trigger shape as `forge-avatar-sync`). - exposes the matrix tool surface (send_message, send_dm, send_reaction, send_reply, mark_read, list_rooms, list_room_members, read_room) to claude via an auto-injected @@ -63,17 +57,27 @@ in }; options.hyperhive.matrix.url = lib.mkOption { - type = lib.types.str; - default = matrixUrlDefault; + type = lib.types.nullOr lib.types.str; + default = null; example = "https://matrix.darkest.space"; description = '' Matrix homeserver URL the agent's `hive-matrix-daemon` connects - to. At runtime hive-c0re forwards the isolation-aware URL - (`matrix.` via the gateway) so isolated agents reach - the homeserver without crossing host loopback. Override - per-agent when an agent should talk to an external homeserver - instead (e.g. a federation-only setup or a remote hive's - tuwunel reached via a vpn). + to. hive-c0re writes this per agent from the hive's own + isolation-aware URL (`matrix.` via the gateway), so a + generated agent config always carries a real value; set it by + hand only when an agent should talk to an external homeserver + instead (a federation-only setup, or a remote hive's tuwunel + reached over a vpn). + + **`null` means "no matrix", not "guess one".** There is + deliberately no loopback default: the homeserver may run on a + different host from the agents, and inside an agent's network + namespace `localhost` reaches the agent rather than the + homeserver, so a default would be a value that builds fine and + then talks to the wrong machine. When this is `null` the daemon + is left without a homeserver and no-ops, exactly as it does when + the token file is absent --- an absent integration, never a + misdirected one. ''; }; @@ -240,15 +244,15 @@ in HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock"; RUST_LOG = "info"; } - # Homeserver URL: by default the daemon inherits the host-forwarded - # HIVE_MATRIX_URL (set by hive-c0re to `matrix.` via the - # gateway, since agents run in private netns and can't reach host - # loopback directly), falling back to the daemon's built-in - # localhost default if the forward is absent. A per-agent - # `hyperhive.matrix.url` override (non-default) is set unit-level - # so it wins over the forwarded value; at the default we - # deliberately DON'T set it so the forwarded value isn't shadowed. - // lib.optionalAttrs (config.hyperhive.matrix.url != matrixUrlDefault) { + # Homeserver URL. hive-c0re writes this option per agent from the + # hive's own `matrix.` gateway URL (agents run in a private + # netns and cannot reach host loopback), so on a real hive it is + # always set; `null` is the honest "this agent has no homeserver" + # and leaves the daemon without one, which it treats like a missing + # token and no-ops. Nothing here falls back to loopback: that would + # be a value that evaluates fine and then addresses the agent's own + # netns instead of the homeserver. + // lib.optionalAttrs (config.hyperhive.matrix.url != null) { HIVE_MATRIX_URL = config.hyperhive.matrix.url; } # Multi-account: serialize the *extra* accounts to the JSON the diff --git a/nix/host-modules/hive-c0re/environment.nix b/nix/host-modules/hive-c0re/environment.nix index 3021eb18..f5fe30f4 100644 --- a/nix/host-modules/hive-c0re/environment.nix +++ b/nix/host-modules/hive-c0re/environment.nix @@ -100,19 +100,25 @@ in # gated on hyperhive being enabled). See `docs/gateway.md::HIVE_FORGE_URL`. HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}"; } -// lib.optionalAttrs config.services.hyperhive.matrix.enable { - # In-cluster matrix homeserver URL for each agent's - # hive-matrix-daemon — the gateway vhost (`matrix.`). The - # gatewayHost null-guard falls back to loopback so a domain-less - # config still evals. Forwarded to agents by meta.rs alongside - # HIVE_FORGE_URL; shares the same env-forwarding ordering caveat - # (value baked at config-generation time). - HIVE_MATRIX_URL = - if config.services.hyperhive.matrix.gatewayHost != null then - "http://${config.services.hyperhive.matrix.gatewayHost}" - else - "http://127.0.0.1:${toString config.services.hyperhive.matrix.httpPort}"; -} +// + lib.optionalAttrs + (config.services.hyperhive.matrix.enable && config.services.hyperhive.matrix.gatewayHost != null) + { + # In-cluster matrix homeserver URL for each agent's + # hive-matrix-daemon — the gateway vhost (`matrix.`). + # Forwarded to agents by meta.rs alongside HIVE_FORGE_URL; shares the + # same env-forwarding ordering caveat (value baked at + # config-generation time). + # + # A domain-less config forwards nothing rather than falling back to + # loopback. The old fallback read as harmless because hive-c0re shares + # the host netns — but the value it produced was handed to *agents*, + # which do not, so `127.0.0.1` there names the agent itself. An absent + # forward leaves `hyperhive.matrix.url` null and the daemon no-ops; + # that is the honest answer when the hive has no matrix vhost to point + # at. + HIVE_MATRIX_URL = "http://${config.services.hyperhive.matrix.gatewayHost}"; + } // lib.optionalAttrs config.services.hyperhive.matrix.gui.enable { # Availability flags read by the dashboard's `/api/state`. # Matrix GUI lives entirely on the gateway nginx (matrix tab