diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 0f7b54d1..40e42138 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -45,12 +45,6 @@ const PASSWORD_BYTES: usize = 32; /// first registered user automatically. Not an agent; has no state dir. pub const HIVE_ADMIN_LOCALPART: &str = "hive"; -/// Display name of the hive Space. Plain text, no special characters, so -/// the Space stays rediscoverable by name (no room alias needed) even when -/// the persisted room-id file is lost — preventing duplicate spaces from -/// being created on the next sweep. -pub const HIVE_SPACE_NAME: &str = "hive"; - /// Host path for the hive admin matrix access token. Outside every /// purgeable path — not deleted by `destroy --purge` on any agent. #[must_use] @@ -951,119 +945,30 @@ pub fn read_admin_token() -> Result { }) } -/// Persist the hive Space room id to [`hive_space_room_id_path()`] (0600). -fn persist_space_room_id(room_id: &str) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - let path = hive_space_room_id_path(); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).ok(); - } - std::fs::write(&path, format!("{room_id}\n")) - .with_context(|| format!("matrix: write space room_id to {}", path.display()))?; - let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)); - Ok(()) -} - -/// Scan the admin account's joined rooms for the canonical hive Space: the -/// `m.space` whose name is [`HIVE_SPACE_NAME`]. Returns the first match -/// (deterministic per the homeserver's joined-rooms order) so a lost -/// room-id file recovers the existing space instead of spawning a -/// duplicate. `None` if the homeserver is unreachable or no match exists. +/// Create the hive Matrix Space room using the admin account and persist +/// its room ID to [`hive_space_room_id_path()`]. Idempotent — returns +/// the stored room ID immediately if the file already exists. /// -/// Name-based (not alias-based) rediscovery keeps the Space free of any -/// special-char room alias — the hardcoded plain name is the anchor. -async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Option { - let joined_url = format!("{MATRIX_HTTP}/_matrix/client/v3/joined_rooms"); - let joined: serde_json::Value = client - .get(&joined_url) - .bearer_auth(admin_token) - .send() - .await - .ok()? - .json() - .await - .ok()?; - let rooms = joined["joined_rooms"].as_array()?; - for room in rooms { - let Some(room_id) = room.as_str() else { - continue; - }; - let encoded = encode_room_id_for_url(room_id); - // Must be an m.space (m.room.create `type`). - let create_url = - format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/"); - let is_space = match client - .get(&create_url) - .bearer_auth(admin_token) - .send() - .await - { - Ok(r) if r.status().is_success() => r - .json::() - .await - .ok() - .is_some_and(|c| c["type"].as_str() == Some("m.space")), - _ => false, - }; - if !is_space { - continue; - } - // …and named HIVE_SPACE_NAME (m.room.name `name`). - let name_url = - format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/"); - let name_matches = match client.get(&name_url).bearer_auth(admin_token).send().await { - Ok(r) if r.status().is_success() => r - .json::() - .await - .ok() - .is_some_and(|n| n["name"].as_str() == Some(HIVE_SPACE_NAME)), - _ => false, - }; - if name_matches { - return Some(room_id.to_owned()); - } - } - None -} - -/// Create (or recover) the hive Matrix Space and persist its room ID to -/// [`hive_space_room_id_path()`]. The Space is a private `m.space` owned by -/// `@hive`, identified by its hardcoded name [`HIVE_SPACE_NAME`] (no alias). -/// -/// Dedup strategy (single canonical space): -/// 1. If the room-id file exists, reuse it. -/// 2. Otherwise, rediscover by scanning the admin's joined rooms for the -/// `m.space` named [`HIVE_SPACE_NAME`] and adopt it (re-persisting the -/// file). This recovers the existing space after a state wipe instead -/// of creating a duplicate. -/// 3. Only if neither yields a room do we `createRoom`. +/// The Space is a private `m.space` room owned by `@hive`. All agents +/// are invited after their accounts are provisioned in [`ensure_all`]. /// /// # Errors /// -/// Returns an error if the homeserver is unreachable, `createRoom` fails, -/// or the room-ID file cannot be written. +/// Returns an error if the Matrix homeserver is unreachable, the +/// `createRoom` call fails, or the room-ID file cannot be written. pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> Result { - // 1. Stored room id wins (fast path). - if let Ok(existing) = std::fs::read_to_string(hive_space_room_id_path()) { + use std::os::unix::fs::PermissionsExt; + let path = hive_space_room_id_path(); + if let Ok(existing) = std::fs::read_to_string(&path) { let trimmed = existing.trim().to_owned(); if !trimmed.is_empty() { tracing::debug!(room_id = %trimmed, "matrix: hive space already provisioned"); return Ok(trimmed); } } - - // 2. No stored id — rediscover the existing space by its hardcoded name - // before creating a new one (prevents duplicate spaces after a wipe). - if let Some(room_id) = find_space_by_name(client, admin_token).await { - persist_space_room_id(&room_id)?; - tracing::info!(%room_id, "matrix: recovered hive space by name"); - return Ok(room_id); - } - - // 3. Create the space (plain hardcoded name, no alias). let url = format!("{MATRIX_HTTP}/_matrix/client/v3/createRoom"); let body = serde_json::json!({ - "name": HIVE_SPACE_NAME, + "name": "hive", "creation_content": { "type": "m.space" }, "preset": "private_chat", "visibility": "private", @@ -1087,7 +992,12 @@ pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> R .as_str() .with_context(|| format!("matrix: createRoom missing room_id: {json}"))? .to_owned(); - persist_space_room_id(&room_id)?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).ok(); + } + std::fs::write(&path, format!("{room_id}\n")) + .with_context(|| format!("matrix: write space room_id to {}", path.display()))?; + let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)); tracing::info!(%room_id, "matrix: created hive space"); Ok(room_id) } @@ -1190,14 +1100,6 @@ pub async fn ensure_all() { return; } }; - // server_name first — the agent invites need it (fully-qualified user ids). - let server_name = match discover_server_name(&client).await { - Ok(s) => s, - Err(e) => { - tracing::warn!(error = ?e, "matrix: discover_server_name failed; skipping space provisioning"); - return; - } - }; let room_id = match ensure_hive_space(&client, &admin_token).await { Ok(id) => id, Err(e) => { @@ -1205,6 +1107,13 @@ pub async fn ensure_all() { return; } }; + let server_name = match discover_server_name(&client).await { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = ?e, "matrix: discover_server_name failed; skipping space invites"); + return; + } + }; // Invite @hive admin first, then all agents. if let Err(e) = invite_to_room( &client,