diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 0e0099b6..35fb0958 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -1,35 +1,11 @@ -//! Optional matrix-tuwunel wiring. When the `hive-matrix` nixos-container -//! is present and running, hive-c0re ensures: +//! Optional matrix-tuwunel wiring: shared registration token (host) + +//! per-agent UIAA registration → `/matrix-token`. No-op +//! when the `hive-matrix` container isn't running, so operators who +//! haven't flipped `hyperhive.matrix.enable = true` pay nothing. //! -//! 1. A shared `registration_token` exists at -//! `/var/lib/hyperhive/matrix-register-token` (mode 0600, generated -//! once on first boot). The hive-matrix module bind-mounts that file -//! read-only into the tuwunel container so tuwunel can resolve its -//! `registration_token_file` setting against it. -//! 2. Every agent (and the manager) has a matrix account on the local -//! homeserver with an `access_token` written to -//! `/matrix-token`. Idempotent: skips registration when -//! the token file already exists. -//! -//! Agents never see the registration token — only their own `access_token`. -//! Account provisioning rides the matrix-spec UIAA flow: -//! -//! ```text -//! POST /_matrix/client/v3/register -//! {"username": "", "password": ""} -//! → 401 {"flows":[{"stages":["m.login.registration_token"]}], -//! "session": "", ...} -//! -//! POST /_matrix/client/v3/register -//! {"username": "", "password": "", -//! "auth": {"type": "m.login.registration_token", -//! "token": "", "session": ""}} -//! → 200 {"user_id": "@agent:server", "access_token": "", ...} -//! ``` -//! -//! No-op when the `hive-matrix` container isn't running (detected via -//! `nixos-container list`), so operators who haven't flipped -//! `hyperhive.matrix.enable = true` pay nothing. +//! See `docs/matrix.md::Provisioning flow (registration token)` for +//! the full UIAA round-trip, token-file shape, and host/container +//! bind-mount layout. use std::path::{Path, PathBuf}; @@ -123,15 +99,12 @@ pub fn ensure_register_token() -> Result { } std::fs::write(path, format!("{token}\n")) .with_context(|| format!("write registration token to {}", path.display()))?; - // Mode 0640: tuwunel inside the hive-matrix container runs as a - // non-root user; the file gets `chown :tuwunel` via the activation - // script in `nix/modules/hive-matrix.nix` so the tuwunel group - // gains read. 0600 would block tuwunel with `Permission denied - // (os error 13)` (#644); 0644 would world-read the token (mara - // veto: footgun). 0640 with a pinned group is the sweet spot. - // The host-side activation script also chowns + chmods on every - // boot, so this is best-effort: hive-c0re may write the file - // before the chown lands, but the next activation reconciles it. + // Mode 0640 (group-readable for tuwunel via the host-side + // activation script's chown). 0600 blocks tuwunel; 0644 + // world-reads the token. The activation script reconciles + // chown + chmod on every boot, so this write is best-effort. + // See `docs/matrix.md::Provisioning flow (registration token)` + // for the host/container permission split. let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o640)); tracing::info!(path = %path.display(), "matrix: generated registration token"); Ok(token) @@ -172,9 +145,9 @@ async fn register_post( /// Generate a throwaway random password for matrix UIAA registration. /// `PASSWORD_BYTES` raw bytes ⇒ 64-char hex string. Agents authenticate /// by `access_token` so the password is protocol overhead we never -/// persist; the operator path in `hivectl` (#663) lets the caller -/// supply a real password instead so they can log into a matrix web -/// client (`m.login.password`). +/// persist; the operator path in `hivectl` lets the caller supply a +/// real password instead so they can log into a matrix web client +/// (`m.login.password`). pub fn random_password() -> Result { random_hex(PASSWORD_BYTES) } @@ -188,7 +161,7 @@ pub fn random_password() -> Result { /// /// Caller picks the password: agents use [`random_password`] (throwaway /// — they auth by `access_token`), operators on the `hivectl` path -/// supply their own so they can log into matrix web clients (#663). +/// supply their own so they can log into matrix web clients. async fn register_user( client: &reqwest::Client, agent: &str, @@ -250,8 +223,7 @@ fn extract_access_token(body: &serde_json::Value) -> Result { /// To force re-registration, delete the token file. /// /// `client` is shared across the sweep so we build one reqwest -/// connection pool for all agents rather than one per call (argus -/// nit on #565 — bounded but wasteful). +/// connection pool for all agents rather than one per call. pub async fn ensure_user_for( client: &reqwest::Client, name: &str, @@ -285,11 +257,11 @@ pub async fn ensure_user_for( /// for storing it. Used by `hivectl matrix create-user` for human /// (non-agent) accounts so we don't create stray /// `/var/lib/hyperhive/agents//` directories for users that -/// aren't agents (#662). For operator accounts the caller passes a -/// real password so the operator can `m.login.password` into matrix -/// web clients afterwards (#663); for headless agent re-provisioning -/// the caller can pass [`random_password`] to keep the existing -/// throwaway behaviour. +/// aren't agents. For operator accounts the caller passes a real +/// password so the operator can `m.login.password` into matrix web +/// clients afterwards; for headless agent re-provisioning the caller +/// can pass [`random_password`] to keep the existing throwaway +/// behaviour. /// /// **Not idempotent** (unlike [`forge::provision_user_token`]): the /// matrix UIAA `/register` endpoint returns `M_USER_IN_USE` (HTTP 400)