Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d043b1ed4e | ||
|
|
134a40a5e2 |
1 changed files with 114 additions and 23 deletions
|
|
@ -45,6 +45,12 @@ const PASSWORD_BYTES: usize = 32;
|
||||||
/// first registered user automatically. Not an agent; has no state dir.
|
/// first registered user automatically. Not an agent; has no state dir.
|
||||||
pub const HIVE_ADMIN_LOCALPART: &str = "hive";
|
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
|
/// Host path for the hive admin matrix access token. Outside every
|
||||||
/// purgeable path — not deleted by `destroy --purge` on any agent.
|
/// purgeable path — not deleted by `destroy --purge` on any agent.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -945,30 +951,119 @@ pub fn read_admin_token() -> Result<String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create the hive Matrix Space room using the admin account and persist
|
/// Persist the hive Space room id to [`hive_space_room_id_path()`] (0600).
|
||||||
/// its room ID to [`hive_space_room_id_path()`]. Idempotent — returns
|
fn persist_space_room_id(room_id: &str) -> Result<()> {
|
||||||
/// the stored room ID immediately if the file already exists.
|
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.
|
||||||
///
|
///
|
||||||
/// The Space is a private `m.space` room owned by `@hive`. All agents
|
/// Name-based (not alias-based) rediscovery keeps the Space free of any
|
||||||
/// are invited after their accounts are provisioned in [`ensure_all`].
|
/// special-char room alias — the hardcoded plain name is the anchor.
|
||||||
|
async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Option<String> {
|
||||||
|
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::<serde_json::Value>()
|
||||||
|
.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::<serde_json::Value>()
|
||||||
|
.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`.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if the Matrix homeserver is unreachable, the
|
/// Returns an error if the homeserver is unreachable, `createRoom` fails,
|
||||||
/// `createRoom` call fails, or the room-ID file cannot be written.
|
/// or the room-ID file cannot be written.
|
||||||
pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> Result<String> {
|
pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> Result<String> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
// 1. Stored room id wins (fast path).
|
||||||
let path = hive_space_room_id_path();
|
if let Ok(existing) = std::fs::read_to_string(hive_space_room_id_path()) {
|
||||||
if let Ok(existing) = std::fs::read_to_string(&path) {
|
|
||||||
let trimmed = existing.trim().to_owned();
|
let trimmed = existing.trim().to_owned();
|
||||||
if !trimmed.is_empty() {
|
if !trimmed.is_empty() {
|
||||||
tracing::debug!(room_id = %trimmed, "matrix: hive space already provisioned");
|
tracing::debug!(room_id = %trimmed, "matrix: hive space already provisioned");
|
||||||
return Ok(trimmed);
|
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 url = format!("{MATRIX_HTTP}/_matrix/client/v3/createRoom");
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
"name": "hive",
|
"name": HIVE_SPACE_NAME,
|
||||||
"creation_content": { "type": "m.space" },
|
"creation_content": { "type": "m.space" },
|
||||||
"preset": "private_chat",
|
"preset": "private_chat",
|
||||||
"visibility": "private",
|
"visibility": "private",
|
||||||
|
|
@ -992,12 +1087,7 @@ pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> R
|
||||||
.as_str()
|
.as_str()
|
||||||
.with_context(|| format!("matrix: createRoom missing room_id: {json}"))?
|
.with_context(|| format!("matrix: createRoom missing room_id: {json}"))?
|
||||||
.to_owned();
|
.to_owned();
|
||||||
if let Some(parent) = path.parent() {
|
persist_space_room_id(&room_id)?;
|
||||||
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");
|
tracing::info!(%room_id, "matrix: created hive space");
|
||||||
Ok(room_id)
|
Ok(room_id)
|
||||||
}
|
}
|
||||||
|
|
@ -1100,6 +1190,14 @@ pub async fn ensure_all() {
|
||||||
return;
|
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 {
|
let room_id = match ensure_hive_space(&client, &admin_token).await {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -1107,13 +1205,6 @@ pub async fn ensure_all() {
|
||||||
return;
|
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.
|
// Invite @hive admin first, then all agents.
|
||||||
if let Err(e) = invite_to_room(
|
if let Err(e) = invite_to_room(
|
||||||
&client,
|
&client,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue