feat: create hive Matrix Space on boot and invite all agents
This commit is contained in:
parent
d726a0d875
commit
60582c0be4
1 changed files with 143 additions and 1 deletions
|
|
@ -82,6 +82,13 @@ fn legacy_password_path(name: &str) -> PathBuf {
|
||||||
Coordinator::agent_notes_dir(name).join("matrix-password")
|
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]
|
||||||
|
pub fn hive_space_room_id_path() -> PathBuf {
|
||||||
|
PathBuf::from("/var/lib/hyperhive/matrix-space-room-id")
|
||||||
|
}
|
||||||
|
|
||||||
/// Probe whether `hive-matrix` exists as a nixos-container. Cheap —
|
/// Probe whether `hive-matrix` exists as a nixos-container. Cheap —
|
||||||
/// `nixos-container list` is just a directory scan in /etc. Same shape
|
/// `nixos-container list` is just a directory scan in /etc. Same shape
|
||||||
/// as `forge::is_present` — routed through hive-priv since
|
/// as `forge::is_present` — routed through hive-priv since
|
||||||
|
|
@ -639,6 +646,102 @@ pub fn read_admin_token() -> Result<String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// The Space is a private `m.space` room owned by `@hive`. All agents
|
||||||
|
/// are invited after their accounts are provisioned in [`ensure_all`].
|
||||||
|
pub async fn ensure_hive_space(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
admin_token: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/createRoom");
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"name": "hive",
|
||||||
|
"creation_content": { "type": "m.space" },
|
||||||
|
"preset": "private_chat",
|
||||||
|
"visibility": "private",
|
||||||
|
});
|
||||||
|
let resp = client
|
||||||
|
.post(&url)
|
||||||
|
.bearer_auth(admin_token)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("matrix: POST /createRoom (hive space)")?;
|
||||||
|
let status = resp.status();
|
||||||
|
let json = resp
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.await
|
||||||
|
.context("matrix: parse /createRoom response")?;
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("matrix: createRoom HTTP {status}, body: {json}");
|
||||||
|
}
|
||||||
|
let room_id = json["room_id"]
|
||||||
|
.as_str()
|
||||||
|
.with_context(|| format!("matrix: createRoom missing room_id: {json}"))?
|
||||||
|
.to_owned();
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invite `@{localpart}:{server_name}` to `room_id` using the admin
|
||||||
|
/// account. Idempotent — treats already-member responses as success.
|
||||||
|
async fn invite_to_room(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
admin_token: &str,
|
||||||
|
room_id: &str,
|
||||||
|
localpart: &str,
|
||||||
|
server_name: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
// `:` must be percent-encoded in the room-id path segment; `!` is
|
||||||
|
// permitted in URL path characters per RFC 3986.
|
||||||
|
let encoded_room_id = room_id.replace(':', "%3A");
|
||||||
|
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room_id}/invite");
|
||||||
|
let user_id = format!("@{localpart}:{server_name}");
|
||||||
|
let resp = client
|
||||||
|
.post(&url)
|
||||||
|
.bearer_auth(admin_token)
|
||||||
|
.json(&serde_json::json!({ "user_id": user_id }))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("matrix: POST /rooms/.../invite for {user_id}"))?;
|
||||||
|
let status = resp.status();
|
||||||
|
if status.is_success() {
|
||||||
|
tracing::debug!(%user_id, %room_id, "matrix: invited to hive space");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// 403 with M_FORBIDDEN or M_BAD_STATE typically means the user is
|
||||||
|
// already a member or has a pending invite — both are fine.
|
||||||
|
if status == StatusCode::FORBIDDEN {
|
||||||
|
let body = resp.json::<serde_json::Value>().await.unwrap_or_default();
|
||||||
|
let errcode = body["errcode"].as_str().unwrap_or("");
|
||||||
|
if errcode == "M_FORBIDDEN" || errcode == "M_BAD_STATE" {
|
||||||
|
tracing::debug!(%user_id, %room_id, %errcode, "matrix: invite skipped (already member/invited)");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
anyhow::bail!("matrix: invite {user_id} to {room_id}: HTTP {status}, body: {body}");
|
||||||
|
}
|
||||||
|
let body = resp.json::<serde_json::Value>().await.unwrap_or_default();
|
||||||
|
anyhow::bail!("matrix: invite {user_id} to {room_id}: HTTP {status}, body: {body}")
|
||||||
|
}
|
||||||
|
|
||||||
/// Sweep every existing container (manager + sub-agents) and ensure
|
/// Sweep every existing container (manager + sub-agents) and ensure
|
||||||
/// each has a matrix user + token on the local homeserver. Called once
|
/// each has a matrix user + token on the local homeserver. Called once
|
||||||
/// at hive-c0re startup, alongside `forge::ensure_all`. No-op when the
|
/// at hive-c0re startup, alongside `forge::ensure_all`. No-op when the
|
||||||
|
|
@ -678,11 +781,50 @@ pub async fn ensure_all() {
|
||||||
tracing::warn!("matrix: nixos-container list failed; skipping user sweep");
|
tracing::warn!("matrix: nixos-container list failed; skipping user sweep");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
for c in containers {
|
let mut agent_names: Vec<String> = Vec::new();
|
||||||
|
for c in &containers {
|
||||||
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
|
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
sync_agent(&client, name, ®ister_token).await;
|
sync_agent(&client, name, ®ister_token).await;
|
||||||
|
agent_names.push(name.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision the hive Space and invite all agents (+ the admin account).
|
||||||
|
// server_name is needed to form full Matrix user IDs for invites.
|
||||||
|
let admin_token = match read_admin_token() {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = ?e, "matrix: skipping hive space provisioning (no admin token)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let room_id = match ensure_hive_space(&client, &admin_token).await {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = ?e, "matrix: ensure_hive_space failed");
|
||||||
|
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, &admin_token, &room_id, HIVE_ADMIN_LOCALPART, &server_name).await
|
||||||
|
{
|
||||||
|
tracing::warn!(error = ?e, "matrix: invite @hive to space failed");
|
||||||
|
}
|
||||||
|
for name in &agent_names {
|
||||||
|
if let Err(e) =
|
||||||
|
invite_to_room(&client, &admin_token, &room_id, name, &server_name).await
|
||||||
|
{
|
||||||
|
tracing::warn!(%name, error = ?e, "matrix: invite agent to space failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue