From 89a5dd752c4efbe822ebab7673294520536c944f Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 25 Sep 2026 02:08:23 +0200 Subject: [PATCH] hive-c0re: stop minting agents' matrix accounts The swarm mints each agent's `main` account now, so the hive's own mint goes: `ensure_user_for`, `finish_user_provisioning`, `sync_agent`, `sync_agent_standalone`, `token_path`, `legacy_password_path`, `auto_reset_password` and `token_file_present`, and the calls from the startup sweep and the rebuild bookkeeping. Both mints pinned the device `hyperhive-`, so leaving this one would have each re-login kill the other's token. `hivectl matrix create-user` refuses an agent's name and says where its account comes from. Everything that still uses the hive's appservice token stays: the hive's own account, the Space and chat room, and operator accounts. --- docs/tools/hivectl-cli.md | 8 +- docs/tools/hivectl.md | 7 +- hive-c0re/src/job_queue/exec.rs | 10 +- hive-c0re/src/matrix.rs | 311 ++------------------------------ hive-c0re/src/server.rs | 62 +++---- hivectl/src/cli.rs | 14 +- 6 files changed, 61 insertions(+), 351 deletions(-) diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 06ce86e9..7ec4afa4 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -142,7 +142,7 @@ Manual entry point to the same idempotent provisioning c0re runs at boot — for ###### **Subcommands:** -* `create-user` — Create or refresh the matrix account + access token for `` +* `create-user` — Create a matrix account for a person or other non-agent `` and print its access token to stdout * `sync-admin` — Provision (or re-provision) the matrix appservice's sender account * `promote-user` — Promote a matrix user to homeserver admin * `reset-password` — Reset a matrix user's password via the admin API @@ -152,15 +152,15 @@ Manual entry point to the same idempotent provisioning c0re runs at boot — for ## `hivectl matrix create-user` -Create or refresh the matrix account + access token for ``. +Create a matrix account for a person or other non-agent `` and print its access token to stdout. -For an existing agent, persists the token to its state dir; for a human/other account, prints the access token to stdout. Set a password to enable matrix web-client login (otherwise it uses a random throwaway). +Refuses an agent's name: its account comes from the swarm (`swarm-controller` creates it and stores its token where the agent reads it). Set a password to enable matrix web-client login (otherwise it uses a random throwaway). **Usage:** `hivectl matrix create-user [OPTIONS] ` ###### **Arguments:** -* `` — Matrix localpart. For agents: the container/agent name. For humans: any matrix localpart — `mara`, `damocles`, etc +* `` — Matrix localpart of a non-agent account — `mara`, `damocles`, etc ###### **Options:** diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 358ed60a..3cb96b87 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -50,7 +50,6 @@ Manual entry to the same idempotent matrix provisioning flow running (`services.hyperhive.deploy.matrix.enable = true`). ```bash -hivectl matrix create-user iris # provision (or re-provision) matrix account for agent `iris` hivectl matrix create-user mara # create matrix account for a human; prints access_token to stdout hivectl matrix create-user mara --password hunter2 # set a client-login password hivectl matrix sync-admin # provision / refresh the appservice's sender account @@ -60,9 +59,9 @@ hivectl matrix invite mara # invite a user to the hive Space hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a specific room/alias ``` -- `create-user`: for agents, persists the `access_token` to - `/matrix-token`. Skips registration when the file already - exists — delete it first to force re-registration. +- `create-user`: for people and other non-agent accounts. It refuses + an agent's name: `swarm-controller` creates an agent's account and + stores its token where the agent's daemon reads it. - `sync-admin`: ensures this hive's appservice sender account (`@hive-:`, one per hive) exists (the account `hive-c0re` provisions rooms with). Token persisted to the diff --git a/hive-c0re/src/job_queue/exec.rs b/hive-c0re/src/job_queue/exec.rs index 4d3da7d0..85385c12 100644 --- a/hive-c0re/src/job_queue/exec.rs +++ b/hive-c0re/src/job_queue/exec.rs @@ -465,7 +465,7 @@ async fn run_swap(coord: &Arc, name: &str, id: NodeId) -> Result<() /// The post-`Swap` bookkeeping tail, split into its own node for dashboard /// visibility + retry granularity. Deps `AfterOk(Swap)`, so reaching here -/// means the profile swap succeeded. Store/forge/matrix work only — no nix +/// means the profile swap succeeded. Store/forge work only — no nix /// build (build-slot-exempt); the agent lease taken at `Swap` is still held /// (the whole chain up to `Reconcile` is one agent's subgraph). async fn run_rebuild_bookkeeping(coord: &Arc, name: &str) -> Result<()> { @@ -477,11 +477,11 @@ async fn run_rebuild_bookkeeping(coord: &Arc, name: &str) -> Result // The `Rebuilt` manager event is emitted exactly once per agent by the DAG's // `EmitRebuilt` tail — emitting ok here and letting a failed tail `Reconcile` // add a contradictory !ok would double-report the same rebuild. - // Full forge + matrix sync on every successful rebuild so the rebuild - // path is equivalent to the startup sweep: tokens, config-repo mirror, - // meta access all recover without a hive-c0re restart. + // Full forge sync on every successful rebuild so the rebuild path is + // equivalent to the startup sweep: tokens, config-repo mirror, meta + // access all recover without a hive-c0re restart. (No matrix step: the + // swarm mints an agent's matrix account, not this hive.) crate::forge::sync_agent(name, crate::forge::core_token().as_deref()).await; - crate::matrix::sync_agent_standalone(name).await; // Wake the agent on its next turn so claude sees a "you were rebuilt" // hint; rescan so dashboards drop the "needs update" chip; lock bump → // meta-inputs re-render. diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 59f222ab..4440c4d1 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -20,8 +20,6 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use reqwest::StatusCode; -use crate::coordinator::Coordinator; - /// Client-server API base this daemon provisions against, from /// `HIVE_MATRIX_API_URL` (set by the hyperhive NixOS module from /// `services.hyperhive.swarm.matrix.apiUrl`). @@ -126,45 +124,15 @@ pub fn sender_token_path() -> PathBuf { crate::paths::matrix_sender_token() } -/// Token file inside the agent's bind-mounted state dir (visible as -/// `/state/matrix-token` from inside the container). -fn token_path(name: &hive_types::Ident) -> PathBuf { - Coordinator::agent_notes_dir(name).join("matrix-token") -} - -/// Whether an agent's token file is present: a non-empty regular file. -/// Decided from metadata alone, because hive-priv writes the file 0600 -/// and owned by the agent, so `hive-core` cannot read it but can stat it -/// through the 0755 state dir. A check that reads the file always -/// fails here and makes every sweep re-mint the token. -fn token_file_present(path: &std::path::Path) -> bool { - std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.len() > 0) -} - -/// Password file for the agent's matrix account. Stored OUTSIDE the -/// purgeable `agent_state_root` tree so it survives `destroy --purge` -/// and allows re-login recovery when the same agent name is re-spawned. +/// Password file for a matrix account this hive holds a password for: its own +/// `@hive-:` account, or one reset through the admin room. Stored OUTSIDE +/// the purgeable `agent_state_root` tree so it survives `destroy --purge`. /// /// Path: `/var/lib/hyperhive/matrix/creds/-password` -/// -/// The token file lives inside the agent's bind-mounted state dir (under -/// `agent_notes_dir`) so the agent container can read it; the password -/// file is host-side only (agents never log in by password — they use -/// the access token exclusively) and belongs with other hive-c0re -/// credential state, not inside the purgeable per-agent tree. fn password_path(name: &str) -> PathBuf { crate::paths::matrix_creds_dir().join(format!("{name}-password")) } -/// Legacy password path (inside the old purgeable `agent_notes_dir`). -/// Used only during the one-time migration in [`ensure_user_for`] to -/// move credentials from old deployments to the new location. Safe to -/// call after `destroy --purge` — the path will simply not exist and -/// the migration is a no-op. -fn legacy_password_path(name: &hive_types::Ident) -> PathBuf { - Coordinator::agent_notes_dir(name).join("matrix-password") -} - /// Host path where the hive Matrix Space room ID is persisted. /// Outside every purgeable path — not deleted by `destroy --purge`. #[must_use] @@ -319,8 +287,8 @@ async fn register_user( "type": "m.login.application_service", "username": localpart, "password": password, - // device_id stays stable across re-runs of ensure_user_for so - // a re-mint doesn't strand orphan devices in tuwunel. + // device_id stays stable across re-runs so a re-mint doesn't + // strand orphan devices in tuwunel. "device_id": format!("hyperhive-{agent}"), "initial_device_display_name": format!("hyperhive ({agent})"), "inhibit_login": false, @@ -416,32 +384,6 @@ async fn login_user(client: &reqwest::Client, agent: &str, password: &str) -> Re extract_access_token(&json) } -/// Auto-recovery helper: reset a user's matrix password through the admin -/// room when the locally stored password is missing. Returns the new -/// password (already persisted to [`password_path`]) on success. -/// -/// ⚠️ Needs an **admin sender**, which `@hive-:` is not — the reset is a -/// `!admin` command and tuwunel only treats a message as a command when -/// its sender is an admin in that room. So this recovery path fails until -/// the two admin operations are rehomed at swarm level; the ordinary -/// route (the stored password, or an appservice login) is unaffected. -/// -/// Called by [`ensure_user_for`] when registration returns `M_USER_IN_USE` -/// but the password file is absent — covers the case where agent state dirs -/// were wiped but the homeserver still has the accounts. -async fn auto_reset_password(client: &reqwest::Client, name: &str) -> anyhow::Result { - let sender_token = read_sender_token() - .context("matrix: the matrix sender token is unavailable for auto-recovery")?; - let server_name = discover_server_name(client) - .await - .context("matrix: discover_server_name for auto-recovery")?; - let effective_password = reset_user_password(client, &sender_token, name, &server_name) - .await - .with_context(|| format!("matrix: admin-room password reset for {name} (auto-recovery)"))?; - tracing::info!(%name, "matrix: auto-recovered password via admin-room reset"); - Ok(effective_password) -} - // --------------------------------------------------------------------------- // Admin-room fallback for password reset // --------------------------------------------------------------------------- @@ -711,162 +653,13 @@ async fn admin_room_reset_password( }) } -/// Ensure `name` has a matrix user + token file on the local -/// homeserver. Skips provisioning entirely if the token file already -/// exists (treating a present token as proof the account is good). -/// To force re-provisioning, delete the token file. -/// -/// When registration fails with `M_USER_IN_USE` (account exists in the -/// homeserver but the token file was deleted) this falls back to -/// `m.login.password` using the persisted `matrix-password` file. If -/// that file is also missing, recovery requires manual intervention: -/// `hivectl matrix create-user --password `. -/// -/// `client` is shared across the sweep so we build one reqwest -/// connection pool for all agents rather than one per call. -pub async fn ensure_user_for(client: &reqwest::Client, name: &str, as_token: &str) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - let agent = hive_types::Ident::parse(name) - .map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?; - if token_file_present(&token_path(&agent)) { - tracing::debug!(%name, "matrix: token already present"); - return Ok(()); - } - - // One-time migration: move the password from the old location inside - // agent_notes_dir (purgeable) to the new location outside it. - let new_pw_path = password_path(name); - let old_pw_path = legacy_password_path(&agent); - if !new_pw_path.exists() && old_pw_path.exists() { - if let Some(parent) = new_pw_path.parent() { - std::fs::create_dir_all(parent).ok(); - } - if let Err(e) = std::fs::rename(&old_pw_path, &new_pw_path) { - // Rename across filesystems or read-only src — copy + delete. - if let Ok(content) = std::fs::read(&old_pw_path) { - if std::fs::write(&new_pw_path, &content).is_ok() { - let _ = std::fs::remove_file(&old_pw_path); - tracing::info!(%name, "matrix: migrated password file to non-purgeable location"); - } - } else { - tracing::warn!(%name, rename_error = ?e, "matrix: password migration failed — could not read old path (old path stays)"); - } - } else { - tracing::info!(%name, "matrix: migrated password file to non-purgeable location"); - } - } - - let password = random_password()?; - let access_token = match register_user(client, name, as_token, &password).await { - Ok(token) => { - // Successful registration — persist the password so we can - // fall back to login if the token file is deleted later. - let pw_path = password_path(name); - if let Some(parent) = pw_path.parent() { - std::fs::create_dir_all(parent).ok(); - } - if let Err(e) = std::fs::write(&pw_path, format!("{password}\n")) { - tracing::warn!(%name, error = ?e, "matrix: failed to persist password (token still saved)"); - } else { - let _ = std::fs::set_permissions(&pw_path, std::fs::Permissions::from_mode(0o600)); - } - token - } - Err(reg_err) if reg_err.to_string().contains("M_USER_IN_USE") => { - // Account already exists and its token file was lost. The - // appservice can mint a session for any account in its - // namespace, so this needs no password and no admin rights — - // try it before the password paths below, which exist for - // accounts created before the appservice did (or named - // outside its namespace) and stay as the fallback. - tracing::info!(%name, "matrix: user already exists, logging in as the appservice"); - match appservice_login(client, as_token, name).await { - Ok(token) => return finish_user_provisioning(name, &token).await, - Err(e) => tracing::warn!( - %name, - error = ?e, - "matrix: appservice login failed; falling back to the stored password" - ), - } - let pw_path = password_path(name); - let stored = if let Some(pw) = std::fs::read_to_string(&pw_path) - .ok() - .map(|s| s.trim().to_owned()) - .filter(|s| !s.is_empty()) - { - pw - } else { - // Password file missing — attempt auto-recovery through the - // admin room. - // This covers the case where agent state dirs were wiped but the - // homeserver still has the accounts. Requires the hive's sender - // token at /var/lib/hyperhive/matrix/access-token, and an admin - // sender, which `@hive-:` is not. - tracing::info!( - %name, - "matrix: stored password missing, attempting admin-room auto-recovery" - ); - match auto_reset_password(client, name).await { - Ok(new_pw) => new_pw, - Err(e) => { - anyhow::bail!( - "matrix: user {name} already exists but password is missing \ - and admin auto-recovery failed ({e:#}) — run:\n\ - hivectl matrix reset-password {name}\n\ - hivectl matrix create-user {name}" - ) - } - } - }; - login_user(client, name, &stored).await.with_context(|| { - format!( - "matrix: login fallback for {name} failed — if homeserver was wiped, delete \ - the matrix-password file and retry" - ) - })? - } - Err(other) => return Err(other), - }; - - finish_user_provisioning(name, &access_token).await -} - -/// Persist a freshly-obtained agent access token and kick the agent's -/// matrix daemon. Shared by every way [`ensure_user_for`] can end up -/// holding a token — creation, appservice login, password login — so a -/// new recovery path cannot forget half of it. -async fn finish_user_provisioning(name: &str, access_token: &str) -> Result<()> { - // Write the token via hive-priv (root helper): hive-c0re runs as the - // unprivileged `hive-core` user and cannot write to agent-owned state - // directories directly. hive-priv writes the file 0600 and chowns it - // to the agent user so it is readable from inside the container. - crate::priv_client::write_agent_matrix_token(name, access_token, None, None) - .await - .with_context(|| format!("matrix: write matrix-token for {name} via hive-priv"))?; - tracing::info!(%name, "matrix: provisioned access token"); - - // Kick the daemon so it picks up the new token without waiting for a - // full container restart — see docs/integrations/matrix.md::Provisioning flow. - if let Err(e) = crate::priv_client::restart_matrix_daemon(name).await { - tracing::warn!(%name, error = ?e, "matrix: could not restart hive-matrix-daemon (token written; daemon will reload on next container start)"); - } else { - tracing::info!(%name, "matrix: restarted hive-matrix-daemon to pick up new token"); - } - - Ok(()) -} - /// Register a matrix account for `name` with the supplied `password` -/// and return the freshly-minted access token. Unlike [`ensure_user_for`], -/// the token is **not** persisted to disk — the caller is responsible -/// 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. For operator accounts the caller passes a real +/// and return the freshly-minted access token. The token is **not** +/// persisted to disk — the caller is responsible for storing it. Used by +/// `hivectl matrix create-user` for human (non-agent) accounts. 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. +/// clients afterwards; without one the caller passes +/// [`random_password`]. /// /// **Not idempotent**: the matrix `/register` endpoint returns /// `M_USER_IN_USE` (HTTP 400) on a second call for the same localpart, @@ -882,44 +675,6 @@ pub async fn provision_user_token( register_user(client, name, as_token, password).await } -/// Per-agent matrix sync: ensure the agent has a matrix account + token. -/// All operations are idempotent; failures are logged as warnings but -/// don't abort the caller. -pub async fn sync_agent(client: &reqwest::Client, name: &str, as_token: &str) { - if let Err(e) = ensure_user_for(client, name, as_token).await { - tracing::warn!(%name, error = ?e, "matrix: ensure_user failed"); - } -} - -/// Standalone per-agent sync that handles its own setup: checks if -/// hive-matrix is present, reads the appservice token, and builds -/// an HTTP client before delegating to [`sync_agent`]. Mirrors the -/// setup in [`ensure_all`] so the rebuild path and the startup sweep -/// stay equivalent. No-op when the matrix container is absent. -pub async fn sync_agent_standalone(name: &str) { - if !is_present() { - return; - } - let as_token = match read_appservice_token() { - Ok(t) => t, - Err(e) => { - tracing::warn!(%name, error = ?e, "matrix: read_appservice_token failed"); - return; - } - }; - let client = match reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS)) - .build() - { - Ok(c) => c, - Err(e) => { - tracing::warn!(%name, error = ?e, "matrix: build HTTP client failed"); - return; - } - }; - sync_agent(&client, name, &as_token).await; -} - /// Ensure the hive's `@hive-:` matrix user exists and that its access token is /// persisted at [`sender_token_path()`]. /// @@ -1179,8 +934,7 @@ pub async fn promote_user_to_admin( /// /// Sends `!admin users reset-password @:` to the admin room as /// `@hive-:`, polls for the bot's response containing the new password, and persists -/// it to the non-purgeable creds path so [`ensure_user_for`] can re-login -/// on the next provisioning sweep. +/// it to the non-purgeable creds path. /// /// ⚠️ Same admin-**sender** requirement as [`promote_user_to_admin`], and /// the same consequence: `@hive-:` is an ordinary account with no admin @@ -1874,10 +1628,11 @@ async fn resolve_room_alias( .ok_or_else(|| anyhow::anyhow!("matrix: alias {alias} response missing room_id: {json}")) } -/// Sweep every existing container (manager + sub-agents) and ensure -/// each has a matrix user + token on the local homeserver. Called at -/// hive-c0re startup, alongside `forge::ensure_all`, and then -/// periodically (see the caller in `main.rs`). No-op when the +/// Make sure the hive's own `@hive-:` account exists, then provision +/// the hive Space and chat room and invite every agent container to both. +/// Agents' own accounts are not created here: `swarm-controller` mints them +/// with the swarm's appservice token. Called at hive-c0re startup, alongside +/// `forge::ensure_all`, and then periodically (see the caller in `main.rs`). No-op when the /// hive-matrix container isn't running. Per-step failures are logged /// but don't abort the sweep. /// @@ -1893,9 +1648,7 @@ pub async fn ensure_all() -> bool { } let mut ok = true; // Loud and non-destructive: with no appservice token this sweep can - // create nothing, so it does nothing. Agents that already hold a - // token keep using it — their accounts and sessions are untouched by - // anything in here. + // create nothing, so it does nothing. let as_token = match read_appservice_token() { Ok(t) => t, Err(e) => { @@ -1903,8 +1656,7 @@ pub async fn ensure_all() -> bool { return false; } }; - // One HTTP client for the whole sweep — connection pool is - // reused across agents. + // One HTTP client for the whole sweep. let client = match reqwest::Client::builder() .timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS)) .build() @@ -1932,7 +1684,6 @@ pub async fn ensure_all() -> bool { let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else { continue; }; - sync_agent(&client, name, &as_token).await; agent_names.push(name.to_owned()); } @@ -2101,30 +1852,4 @@ mod tests { let err = extract_access_token(&body).unwrap_err(); assert!(err.to_string().contains("missing access_token")); } - - #[test] - fn token_file_present_only_for_a_non_empty_regular_file() { - let dir = tempfile::tempdir().expect("tempdir"); - let token = dir.path().join("matrix-token"); - assert!(!token_file_present(&token), "missing file"); - std::fs::write(&token, "").unwrap(); - assert!(!token_file_present(&token), "empty file"); - std::fs::write(&token, "tok\n").unwrap(); - assert!(token_file_present(&token), "non-empty file"); - assert!(!token_file_present(dir.path()), "directory"); - } - - /// The sweep runs as `hive-core`, which cannot read the agent-owned - /// 0600 token file. `chmod 000` does not stop root, so this test uses - /// content that `read_to_string` rejects instead: the predicate must - /// still report the file as present, which holds only if it never - /// reads the content. - #[test] - fn token_file_present_does_not_read_the_content() { - let dir = tempfile::tempdir().expect("tempdir"); - let token = dir.path().join("matrix-token"); - std::fs::write(&token, [0xff, 0xfe]).unwrap(); - assert!(std::fs::read_to_string(&token).is_err()); - assert!(token_file_present(&token)); - } } diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 8b1ae675..411b616d 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -539,51 +539,37 @@ async fn handle_matrix_create_user( password: Option<&str>, ) -> Result { require_matrix_present()?; + if agent_exists(name)? { + // The swarm mints an agent's account and stores its token where the + // agent reads it; a second minter here would replace that token on the + // same device. + anyhow::bail!( + "matrix create-user: '{name}' is an agent, and an agent's matrix account comes from \ + the swarm: swarm-controller creates it and re-checks it every five minutes" + ); + } let as_token = crate::matrix::read_appservice_token().context("read matrix appservice token")?; let client = matrix_http_client()?; let mut out = Vec::new(); - if agent_exists(name)? { - if password.is_some() { - // Agents auth by access_token, never by password — the - // boot-sweep provisioning path doesn't accept one. Refuse - // rather than silently dropping it. - anyhow::bail!( - "matrix create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via access_token" - ); - } - crate::matrix::ensure_user_for(&client, name.as_str(), &as_token) + let effective_password = match password { + Some(p) => p.to_owned(), + None => crate::matrix::random_password().context("generate random matrix password")?, + }; + let token = + crate::matrix::provision_user_token(&client, name.as_str(), &as_token, &effective_password) .await .with_context(|| format!("matrix create-user {name}"))?; - let path = Coordinator::agent_notes_dir(name).join("matrix-token"); - out.push(format!("matrix: provisioned agent user '{name}'")); - out.push(format!("token persisted at: {}", path.display())); + out.push(format!( + "matrix: provisioned user '{name}' (not an agent — token not persisted)" + )); + out.push(format!("token: {token}")); + if password.is_some() { + out.push("password: set as supplied — use it to log into a matrix web client".to_owned()); } else { - let effective_password = match password { - Some(p) => p.to_owned(), - None => crate::matrix::random_password().context("generate random matrix password")?, - }; - let token = crate::matrix::provision_user_token( - &client, - name.as_str(), - &as_token, - &effective_password, - ) - .await - .with_context(|| format!("matrix create-user {name}"))?; - out.push(format!( - "matrix: provisioned user '{name}' (not an agent — token not persisted)" - )); - out.push(format!("token: {token}")); - if password.is_some() { - out.push( - "password: set as supplied — use it to log into a matrix web client".to_owned(), - ); - } else { - out.push( - "password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(), - ); - } + out.push( + "password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(), + ); } Ok(HostResponse::messages(out)) } diff --git a/hivectl/src/cli.rs b/hivectl/src/cli.rs index 42c2b6fd..f94da8cf 100644 --- a/hivectl/src/cli.rs +++ b/hivectl/src/cli.rs @@ -286,15 +286,15 @@ impl From for hive_host_sock::ReconcileDirection { #[derive(Subcommand)] pub enum MatrixCmd { - /// Create or refresh the matrix account + access token for ``. + /// Create a matrix account for a person or other non-agent `` and + /// print its access token to stdout. /// - /// For an existing agent, persists the token to its state dir; for a - /// human/other account, prints the access token to stdout. Set a - /// password to enable matrix web-client login (otherwise it uses a - /// random throwaway). + /// Refuses an agent's name: its account comes from the swarm + /// (`swarm-controller` creates it and stores its token where the agent + /// reads it). Set a password to enable matrix web-client login + /// (otherwise it uses a random throwaway). CreateUser { - /// Matrix localpart. For agents: the container/agent name. - /// For humans: any matrix localpart — `mara`, `damocles`, etc. + /// Matrix localpart of a non-agent account — `mara`, `damocles`, etc. name: String, /// Set the account password to this string instead of a random /// throwaway. Use this for operator accounts that need to log