Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3f2d246e3 | ||
|
|
bd06f81294 | ||
|
|
0e9b1c563d |
9 changed files with 238 additions and 119 deletions
|
|
@ -132,7 +132,7 @@ setups.
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
hyperhive.forge.url = "http://forge.example:3000"; # default: null
|
hyperhive.forge.url = "http://forge.example:3000"; # default: null
|
||||||
hyperhive.matrix.url = "http://localhost:8008"; # default
|
hyperhive.matrix.url = "https://matrix.example"; # default: null
|
||||||
```
|
```
|
||||||
|
|
||||||
**`hyperhive.forge.url`** — base URL of the Forgejo instance. Used by
|
**`hyperhive.forge.url`** — base URL of the Forgejo instance. Used by
|
||||||
|
|
@ -155,12 +155,22 @@ flake without one, so `null` only survives where the agent modules are
|
||||||
evaluated outside a hive.
|
evaluated outside a hive.
|
||||||
|
|
||||||
**`hyperhive.matrix.url`** — homeserver URL used by
|
**`hyperhive.matrix.url`** — homeserver URL used by
|
||||||
`hive-matrix-daemon` when connecting via the matrix-sdk. Default
|
`hive-matrix-daemon` when connecting via the matrix-sdk. hive-c0re
|
||||||
(`localhost:8008`) is overridden by hive-c0re at deploy time to the
|
writes it into every agent at deploy time as the gateway-routed
|
||||||
gateway-routed `matrix.<domain>` URL so isolated agents can reach the
|
`matrix.<domain>` URL, so isolated agents can reach the homeserver.
|
||||||
homeserver. Override per-agent when an agent should talk to a
|
Override per-agent when an agent should talk to a different homeserver
|
||||||
different homeserver — for example a remote hive's tuwunel reached
|
— for example a remote hive's tuwunel reached over a VPN, or an
|
||||||
over a VPN, or an external Matrix server for a federation-only agent.
|
external Matrix server for a federation-only agent.
|
||||||
|
|
||||||
|
**Defaults to `null`, meaning "no matrix" — for the same reason
|
||||||
|
`forge.url` does.** The homeserver may live on another host, and a
|
||||||
|
loopback default resolves inside the agent's own netns to the agent,
|
||||||
|
so it would be a value that evaluates fine and then talks to the wrong
|
||||||
|
machine. With `null` the daemon has no homeserver and no-ops exactly as
|
||||||
|
it does without a token. The hive only forwards `HIVE_MATRIX_URL` when
|
||||||
|
it actually has a matrix vhost to name, so `null` survives where a hive
|
||||||
|
runs no homeserver, or where the agent modules are evaluated outside a
|
||||||
|
hive.
|
||||||
|
|
||||||
## Claude Code plugins
|
## Claude Code plugins
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,39 @@ use reqwest::StatusCode;
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
|
|
||||||
/// nspawn container name for the matrix homeserver — mirrors
|
/// Client-server API base this daemon provisions against, from
|
||||||
/// `hive-forge` and matches the bare-name allow-list the lifecycle
|
/// `HIVE_MATRIX_API_URL` (set by `hive-c0re.nix` from
|
||||||
/// scanner skips over.
|
/// `hyperhive.matrix.apiUrl`).
|
||||||
const MATRIX_CONTAINER: &str = "hive-matrix";
|
///
|
||||||
/// Local-host URL of the tuwunel client-server API. Shares the host
|
/// `None` means **this hive has no homeserver to provision against** and
|
||||||
/// netns so `localhost:<port>` resolves both from the daemon and from
|
/// every matrix path no-ops — see [`is_present`]. There is deliberately no
|
||||||
/// inside any sub-agent container.
|
/// fallback: `localhost:8008` is right only when the homeserver happens to
|
||||||
const MATRIX_HTTP: &str = "http://localhost:8008";
|
/// share this daemon's netns, and an address baked into the binary is one
|
||||||
|
/// that builds fine and then talks to the wrong machine. The nix module
|
||||||
|
/// supplies the loopback address when it is itself the thing running
|
||||||
|
/// tuwunel, where it is not a guess but a fact about what it just started.
|
||||||
|
///
|
||||||
|
/// Also not an agent-facing address either way. An agent has its own netns;
|
||||||
|
/// agents are handed the gateway vhost via `HIVE_MATRIX_URL`, and get
|
||||||
|
/// nothing at all when the hive has no vhost to offer.
|
||||||
|
fn matrix_http() -> Option<&'static str> {
|
||||||
|
static BASE: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
|
||||||
|
BASE.get_or_init(|| std::env::var("HIVE_MATRIX_API_URL").ok())
|
||||||
|
.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`matrix_http`] for the call sites that propagate with `?`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// When no homeserver is configured. Reaching one of these paths at all
|
||||||
|
/// means an [`is_present`] gate was skipped, so the message names that
|
||||||
|
/// rather than the missing variable.
|
||||||
|
fn matrix_base() -> Result<&'static str> {
|
||||||
|
matrix_http().context(
|
||||||
|
"matrix: no homeserver configured (hyperhive.matrix.apiUrl / HIVE_MATRIX_API_URL) — \
|
||||||
|
this path should have been gated on matrix::is_present()",
|
||||||
|
)
|
||||||
|
}
|
||||||
/// Length (bytes) of the random registration token. 32 raw bytes ⇒
|
/// Length (bytes) of the random registration token. 32 raw bytes ⇒
|
||||||
/// 64-char hex string; comfortable for a long-lived shared secret.
|
/// 64-char hex string; comfortable for a long-lived shared secret.
|
||||||
const REGISTER_TOKEN_BYTES: usize = 32;
|
const REGISTER_TOKEN_BYTES: usize = 32;
|
||||||
|
|
@ -107,15 +132,17 @@ pub fn hive_chat_room_id_path() -> PathBuf {
|
||||||
crate::paths::matrix_chat_room_id()
|
crate::paths::matrix_chat_room_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Probe whether `hive-matrix` exists as a nixos-container. Cheap —
|
/// Whether this hive has a homeserver to provision against.
|
||||||
/// `nixos-container list` is just a directory scan in /etc. Same shape
|
///
|
||||||
/// as `forge::is_present` — routed through hive-priv since
|
/// **A configured API URL, not a local container.** It used to scan
|
||||||
/// `nixos-container` needs root and hive-c0re runs unprivileged.
|
/// `nixos-container list` for `hive-matrix`, which answers a different
|
||||||
pub async fn is_present() -> bool {
|
/// question — "is the homeserver a container on this host" — and so made a
|
||||||
let Ok(stdout) = crate::priv_client::list_containers().await else {
|
/// remote homeserver silently no-op no matter how it was addressed. The
|
||||||
return false;
|
/// nix module still supplies the loopback URL whenever it runs tuwunel
|
||||||
};
|
/// itself, so a co-located hive behaves exactly as before.
|
||||||
stdout.lines().any(|l| l.trim() == MATRIX_CONTAINER)
|
#[must_use]
|
||||||
|
pub fn is_present() -> bool {
|
||||||
|
matrix_http().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read `n` cryptographic-quality bytes from `/dev/urandom` and return
|
/// Read `n` cryptographic-quality bytes from `/dev/urandom` and return
|
||||||
|
|
@ -178,7 +205,8 @@ async fn register_post(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
body: &serde_json::Value,
|
body: &serde_json::Value,
|
||||||
) -> Result<(StatusCode, serde_json::Value)> {
|
) -> Result<(StatusCode, serde_json::Value)> {
|
||||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/register");
|
let base = matrix_base()?;
|
||||||
|
let url = format!("{base}/_matrix/client/v3/register");
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.json(body)
|
.json(body)
|
||||||
|
|
@ -275,7 +303,8 @@ fn extract_access_token(body: &serde_json::Value) -> Result<String> {
|
||||||
/// in which case manual recovery via `hivectl matrix create-user` is
|
/// in which case manual recovery via `hivectl matrix create-user` is
|
||||||
/// required.
|
/// required.
|
||||||
async fn login_user(client: &reqwest::Client, agent: &str, password: &str) -> Result<String> {
|
async fn login_user(client: &reqwest::Client, agent: &str, password: &str) -> Result<String> {
|
||||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/login");
|
let base = matrix_base()?;
|
||||||
|
let url = format!("{base}/_matrix/client/v3/login");
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
"identifier": {
|
"identifier": {
|
||||||
|
|
@ -340,9 +369,10 @@ async fn discover_admin_room_id(
|
||||||
admin_token: &str,
|
admin_token: &str,
|
||||||
server_name: &str,
|
server_name: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
let base = matrix_base()?;
|
||||||
// #admins:server → %23admins%3A<server>
|
// #admins:server → %23admins%3A<server>
|
||||||
let encoded_alias = format!("%23admins%3A{server_name}");
|
let encoded_alias = format!("%23admins%3A{server_name}");
|
||||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/directory/room/{encoded_alias}");
|
let url = format!("{base}/_matrix/client/v3/directory/room/{encoded_alias}");
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -479,10 +509,11 @@ async fn admin_room_send_and_poll<T>(
|
||||||
command: &str,
|
command: &str,
|
||||||
check: impl Fn(&str) -> Option<T>,
|
check: impl Fn(&str) -> Option<T>,
|
||||||
) -> Result<T> {
|
) -> Result<T> {
|
||||||
|
let base = matrix_base()?;
|
||||||
// Send the command; record the event_id so we can use it as an anchor.
|
// Send the command; record the event_id so we can use it as an anchor.
|
||||||
let txn_id = random_hex(8)?;
|
let txn_id = random_hex(8)?;
|
||||||
let send_url =
|
let send_url =
|
||||||
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{room_url}/send/m.room.message/{txn_id}");
|
format!("{base}/_matrix/client/v3/rooms/{room_url}/send/m.room.message/{txn_id}");
|
||||||
let send_resp = client
|
let send_resp = client
|
||||||
.put(&send_url)
|
.put(&send_url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -507,8 +538,7 @@ async fn admin_room_send_and_poll<T>(
|
||||||
// on each tick. Walk the list until we hit our own command event_id;
|
// on each tick. Walk the list until we hit our own command event_id;
|
||||||
// everything *before* that marker arrived after our command.
|
// everything *before* that marker arrived after our command.
|
||||||
let own_user_id = format!("@{HIVE_ADMIN_LOCALPART}:{server_name}");
|
let own_user_id = format!("@{HIVE_ADMIN_LOCALPART}:{server_name}");
|
||||||
let poll_url =
|
let poll_url = format!("{base}/_matrix/client/v3/rooms/{room_url}/messages?dir=b&limit=20");
|
||||||
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{room_url}/messages?dir=b&limit=20");
|
|
||||||
for _ in 0..15_u8 {
|
for _ in 0..15_u8 {
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
let poll_json = client
|
let poll_json = client
|
||||||
|
|
@ -763,7 +793,7 @@ pub async fn sync_agent(client: &reqwest::Client, name: &str, register_token: &s
|
||||||
/// setup in [`ensure_all`] so the rebuild path and the startup sweep
|
/// setup in [`ensure_all`] so the rebuild path and the startup sweep
|
||||||
/// stay equivalent. No-op when the matrix container is absent.
|
/// stay equivalent. No-op when the matrix container is absent.
|
||||||
pub async fn sync_agent_standalone(name: &str) {
|
pub async fn sync_agent_standalone(name: &str) {
|
||||||
if !is_present().await {
|
if !is_present() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let register_token = match ensure_register_token() {
|
let register_token = match ensure_register_token() {
|
||||||
|
|
@ -933,7 +963,8 @@ fn persist_password(localpart: &str, password: &str) {
|
||||||
/// `GET /_matrix/key/v2/server` (unauthenticated federation key endpoint).
|
/// `GET /_matrix/key/v2/server` (unauthenticated federation key endpoint).
|
||||||
/// The response JSON always includes `"server_name"` per the matrix spec.
|
/// The response JSON always includes `"server_name"` per the matrix spec.
|
||||||
pub async fn discover_server_name(client: &reqwest::Client) -> Result<String> {
|
pub async fn discover_server_name(client: &reqwest::Client) -> Result<String> {
|
||||||
let url = format!("{MATRIX_HTTP}/_matrix/key/v2/server");
|
let base = matrix_base()?;
|
||||||
|
let url = format!("{base}/_matrix/key/v2/server");
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.send()
|
.send()
|
||||||
|
|
@ -995,7 +1026,8 @@ fn persist_space_room_id(room_id: &str) -> Result<()> {
|
||||||
/// Name-based (not alias-based) rediscovery keeps the Space free of any
|
/// Name-based (not alias-based) rediscovery keeps the Space free of any
|
||||||
/// special-char room alias — the hardcoded plain name is the anchor.
|
/// 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> {
|
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 base = matrix_http()?;
|
||||||
|
let joined_url = format!("{base}/_matrix/client/v3/joined_rooms");
|
||||||
let joined: serde_json::Value = client
|
let joined: serde_json::Value = client
|
||||||
.get(&joined_url)
|
.get(&joined_url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -1012,8 +1044,7 @@ async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Opti
|
||||||
};
|
};
|
||||||
let encoded = encode_room_id_for_url(room_id);
|
let encoded = encode_room_id_for_url(room_id);
|
||||||
// Must be an m.space (m.room.create `type`).
|
// Must be an m.space (m.room.create `type`).
|
||||||
let create_url =
|
let create_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
|
||||||
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
|
|
||||||
let is_space = match client
|
let is_space = match client
|
||||||
.get(&create_url)
|
.get(&create_url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -1031,8 +1062,7 @@ async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Opti
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// …and named HIVE_SPACE_NAME (m.room.name `name`).
|
// …and named HIVE_SPACE_NAME (m.room.name `name`).
|
||||||
let name_url =
|
let name_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/");
|
||||||
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 {
|
let name_matches = match client.get(&name_url).bearer_auth(admin_token).send().await {
|
||||||
Ok(r) if r.status().is_success() => r
|
Ok(r) if r.status().is_success() => r
|
||||||
.json::<serde_json::Value>()
|
.json::<serde_json::Value>()
|
||||||
|
|
@ -1065,6 +1095,7 @@ async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Opti
|
||||||
/// Returns an error if the homeserver is unreachable, `createRoom` fails,
|
/// Returns an error if the homeserver is unreachable, `createRoom` 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> {
|
||||||
|
let base = matrix_base()?;
|
||||||
// 1. Stored room id wins (fast path).
|
// 1. Stored room id wins (fast path).
|
||||||
if let Ok(existing) = std::fs::read_to_string(hive_space_room_id_path()) {
|
if let Ok(existing) = std::fs::read_to_string(hive_space_room_id_path()) {
|
||||||
let trimmed = existing.trim().to_owned();
|
let trimmed = existing.trim().to_owned();
|
||||||
|
|
@ -1083,7 +1114,7 @@ pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> R
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create the space (plain hardcoded name, no alias).
|
// 3. Create the space (plain hardcoded name, no alias).
|
||||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/createRoom");
|
let url = format!("{base}/_matrix/client/v3/createRoom");
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
"name": HIVE_SPACE_NAME,
|
"name": HIVE_SPACE_NAME,
|
||||||
"creation_content": { "type": "m.space" },
|
"creation_content": { "type": "m.space" },
|
||||||
|
|
@ -1133,7 +1164,8 @@ fn persist_chat_room_id(room_id: &str) -> Result<()> {
|
||||||
/// room instead of spawning a duplicate. `None` if the homeserver is
|
/// room instead of spawning a duplicate. `None` if the homeserver is
|
||||||
/// unreachable or no match exists.
|
/// unreachable or no match exists.
|
||||||
async fn find_chat_room_by_name(client: &reqwest::Client, admin_token: &str) -> Option<String> {
|
async fn find_chat_room_by_name(client: &reqwest::Client, admin_token: &str) -> Option<String> {
|
||||||
let joined_url = format!("{MATRIX_HTTP}/_matrix/client/v3/joined_rooms");
|
let base = matrix_http()?;
|
||||||
|
let joined_url = format!("{base}/_matrix/client/v3/joined_rooms");
|
||||||
let joined: serde_json::Value = client
|
let joined: serde_json::Value = client
|
||||||
.get(&joined_url)
|
.get(&joined_url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -1150,8 +1182,7 @@ async fn find_chat_room_by_name(client: &reqwest::Client, admin_token: &str) ->
|
||||||
};
|
};
|
||||||
let encoded = encode_room_id_for_url(room_id);
|
let encoded = encode_room_id_for_url(room_id);
|
||||||
// Skip the Space itself (and any other m.space).
|
// Skip the Space itself (and any other m.space).
|
||||||
let create_url =
|
let create_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
|
||||||
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
|
|
||||||
let is_space = match client
|
let is_space = match client
|
||||||
.get(&create_url)
|
.get(&create_url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -1169,8 +1200,7 @@ async fn find_chat_room_by_name(client: &reqwest::Client, admin_token: &str) ->
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// …and named HIVE_CHAT_ROOM_NAME (m.room.name `name`).
|
// …and named HIVE_CHAT_ROOM_NAME (m.room.name `name`).
|
||||||
let name_url =
|
let name_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/");
|
||||||
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 {
|
let name_matches = match client.get(&name_url).bearer_auth(admin_token).send().await {
|
||||||
Ok(r) if r.status().is_success() => r
|
Ok(r) if r.status().is_success() => r
|
||||||
.json::<serde_json::Value>()
|
.json::<serde_json::Value>()
|
||||||
|
|
@ -1196,11 +1226,11 @@ async fn set_room_state(
|
||||||
state_key: &str,
|
state_key: &str,
|
||||||
content: &serde_json::Value,
|
content: &serde_json::Value,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let base = matrix_base()?;
|
||||||
let encoded_room = encode_room_id_for_url(room_id);
|
let encoded_room = encode_room_id_for_url(room_id);
|
||||||
let encoded_key = encode_room_id_for_url(state_key);
|
let encoded_key = encode_room_id_for_url(state_key);
|
||||||
let url = format!(
|
let url =
|
||||||
"{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room}/state/{event_type}/{encoded_key}"
|
format!("{base}/_matrix/client/v3/rooms/{encoded_room}/state/{event_type}/{encoded_key}");
|
||||||
);
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.put(&url)
|
.put(&url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -1243,6 +1273,7 @@ pub async fn ensure_hive_chat_room(
|
||||||
space_room_id: &str,
|
space_room_id: &str,
|
||||||
server_name: &str,
|
server_name: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
let base = matrix_base()?;
|
||||||
// 1. Stored room id wins (fast path). 2. Rediscover by name before
|
// 1. Stored room id wins (fast path). 2. Rediscover by name before
|
||||||
// creating (prevents duplicates after a state wipe). 3. Create.
|
// creating (prevents duplicates after a state wipe). 3. Create.
|
||||||
let room_id = if let Some(id) = std::fs::read_to_string(hive_chat_room_id_path())
|
let room_id = if let Some(id) = std::fs::read_to_string(hive_chat_room_id_path())
|
||||||
|
|
@ -1259,7 +1290,7 @@ pub async fn ensure_hive_chat_room(
|
||||||
} else {
|
} else {
|
||||||
// `initial_state` is applied after the preset-derived state, so the
|
// `initial_state` is applied after the preset-derived state, so the
|
||||||
// restricted join rule overrides private_chat's invite-only default.
|
// restricted join rule overrides private_chat's invite-only default.
|
||||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/createRoom");
|
let url = format!("{base}/_matrix/client/v3/createRoom");
|
||||||
let body = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
"name": HIVE_CHAT_ROOM_NAME,
|
"name": HIVE_CHAT_ROOM_NAME,
|
||||||
"topic": HIVE_CHAT_ROOM_TOPIC,
|
"topic": HIVE_CHAT_ROOM_TOPIC,
|
||||||
|
|
@ -1359,11 +1390,12 @@ async fn room_membership(
|
||||||
encoded_room_id: &str,
|
encoded_room_id: &str,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
|
let base = matrix_http()?;
|
||||||
// `:` must be percent-encoded in both the room-id and user-id path
|
// `:` must be percent-encoded in both the room-id and user-id path
|
||||||
// segments; `@` and `!` are permitted path characters per RFC 3986.
|
// segments; `@` and `!` are permitted path characters per RFC 3986.
|
||||||
let encoded_user = user_id.replace(':', "%3A");
|
let encoded_user = user_id.replace(':', "%3A");
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room_id}/state/m.room.member/{encoded_user}"
|
"{base}/_matrix/client/v3/rooms/{encoded_room_id}/state/m.room.member/{encoded_user}"
|
||||||
);
|
);
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
|
|
@ -1391,6 +1423,7 @@ async fn invite_user_id(
|
||||||
room_id: &str,
|
room_id: &str,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let base = matrix_base()?;
|
||||||
// `:` must be percent-encoded in the room-id path segment; `!` is
|
// `:` must be percent-encoded in the room-id path segment; `!` is
|
||||||
// permitted in URL path characters per RFC 3986.
|
// permitted in URL path characters per RFC 3986.
|
||||||
let encoded_room_id = room_id.replace(':', "%3A");
|
let encoded_room_id = room_id.replace(':', "%3A");
|
||||||
|
|
@ -1405,7 +1438,7 @@ async fn invite_user_id(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room_id}/invite");
|
let url = format!("{base}/_matrix/client/v3/rooms/{encoded_room_id}/invite");
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -1485,8 +1518,9 @@ async fn resolve_room_alias(
|
||||||
admin_token: &str,
|
admin_token: &str,
|
||||||
alias: &str,
|
alias: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
let base = matrix_base()?;
|
||||||
let encoded = alias.replace('#', "%23").replace(':', "%3A");
|
let encoded = alias.replace('#', "%23").replace(':', "%3A");
|
||||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/directory/room/{encoded}");
|
let url = format!("{base}/_matrix/client/v3/directory/room/{encoded}");
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.bearer_auth(admin_token)
|
.bearer_auth(admin_token)
|
||||||
|
|
@ -1517,7 +1551,7 @@ async fn resolve_room_alias(
|
||||||
/// dashboard banner on persistent failure (this sweep re-runs every 30
|
/// dashboard banner on persistent failure (this sweep re-runs every 30
|
||||||
/// minutes, so a one-off blip self-heals without ever bannering).
|
/// minutes, so a one-off blip self-heals without ever bannering).
|
||||||
pub async fn ensure_all() -> bool {
|
pub async fn ensure_all() -> bool {
|
||||||
if !is_present().await {
|
if !is_present() {
|
||||||
tracing::debug!("matrix: hive-matrix container absent, skipping user sweep");
|
tracing::debug!("matrix: hive-matrix container absent, skipping user sweep");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -463,13 +463,19 @@ async fn handle_set_resource_limits(
|
||||||
)]))
|
)]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Guard: matrix provisioning needs the homeserver container running.
|
/// Guard: matrix provisioning needs a homeserver to provision against.
|
||||||
async fn require_matrix_present() -> Result<()> {
|
///
|
||||||
if crate::matrix::is_present().await {
|
/// Answers "is one configured", not "is one running here" — the message names
|
||||||
|
/// both ways to get there, since a hive that talks to someone else's
|
||||||
|
/// homeserver never enables the local container at all.
|
||||||
|
fn require_matrix_present() -> Result<()> {
|
||||||
|
if crate::matrix::is_present() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"hive-matrix container not running — start it (services.hyperhive.matrix.enable = true) before provisioning matrix users"
|
"no matrix homeserver configured — set services.hyperhive.matrix.enable = true to run one \
|
||||||
|
here, or services.hyperhive.matrix.apiUrl to point at an existing one, before \
|
||||||
|
provisioning matrix users"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,7 +483,7 @@ async fn handle_matrix_create_user(
|
||||||
name: &hive_types::Ident,
|
name: &hive_types::Ident,
|
||||||
password: Option<&str>,
|
password: Option<&str>,
|
||||||
) -> Result<HostResponse> {
|
) -> Result<HostResponse> {
|
||||||
require_matrix_present().await?;
|
require_matrix_present()?;
|
||||||
let register_token =
|
let register_token =
|
||||||
crate::matrix::ensure_register_token().context("read matrix register token")?;
|
crate::matrix::ensure_register_token().context("read matrix register token")?;
|
||||||
let client = matrix_http_client()?;
|
let client = matrix_http_client()?;
|
||||||
|
|
@ -689,7 +695,7 @@ async fn handle_push_snapshot(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_matrix_sync_admin() -> Result<HostResponse> {
|
async fn handle_matrix_sync_admin() -> Result<HostResponse> {
|
||||||
require_matrix_present().await?;
|
require_matrix_present()?;
|
||||||
let register_token =
|
let register_token =
|
||||||
crate::matrix::ensure_register_token().context("read matrix register token")?;
|
crate::matrix::ensure_register_token().context("read matrix register token")?;
|
||||||
let client = matrix_http_client()?;
|
let client = matrix_http_client()?;
|
||||||
|
|
@ -707,7 +713,7 @@ async fn handle_matrix_sync_admin() -> Result<HostResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_matrix_promote_user(name: &str) -> Result<HostResponse> {
|
async fn handle_matrix_promote_user(name: &str) -> Result<HostResponse> {
|
||||||
require_matrix_present().await?;
|
require_matrix_present()?;
|
||||||
let admin_token = crate::matrix::read_admin_token()?;
|
let admin_token = crate::matrix::read_admin_token()?;
|
||||||
let client = matrix_http_client()?;
|
let client = matrix_http_client()?;
|
||||||
let server_name = crate::matrix::discover_server_name(&client)
|
let server_name = crate::matrix::discover_server_name(&client)
|
||||||
|
|
@ -722,7 +728,7 @@ async fn handle_matrix_promote_user(name: &str) -> Result<HostResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_matrix_invite(user: &str, room: Option<&str>) -> Result<HostResponse> {
|
async fn handle_matrix_invite(user: &str, room: Option<&str>) -> Result<HostResponse> {
|
||||||
require_matrix_present().await?;
|
require_matrix_present()?;
|
||||||
let admin_token = crate::matrix::read_admin_token()?;
|
let admin_token = crate::matrix::read_admin_token()?;
|
||||||
let client = matrix_http_client()?;
|
let client = matrix_http_client()?;
|
||||||
let server_name = crate::matrix::discover_server_name(&client)
|
let server_name = crate::matrix::discover_server_name(&client)
|
||||||
|
|
@ -742,7 +748,7 @@ async fn handle_matrix_invite(user: &str, room: Option<&str>) -> Result<HostResp
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_matrix_reset_password(name: &str) -> Result<HostResponse> {
|
async fn handle_matrix_reset_password(name: &str) -> Result<HostResponse> {
|
||||||
require_matrix_present().await?;
|
require_matrix_present()?;
|
||||||
let admin_token = crate::matrix::read_admin_token()?;
|
let admin_token = crate::matrix::read_admin_token()?;
|
||||||
let client = matrix_http_client()?;
|
let client = matrix_http_client()?;
|
||||||
let server_name = crate::matrix::discover_server_name(&client)
|
let server_name = crate::matrix::discover_server_name(&client)
|
||||||
|
|
|
||||||
|
|
@ -46,13 +46,15 @@ pub struct AccountCfg {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountCfg {
|
impl AccountCfg {
|
||||||
/// Resolve the effective homeserver URL (per-account override or
|
/// The effective homeserver URL — this account's own, else the
|
||||||
/// the daemon-wide default).
|
/// daemon-wide `HIVE_MATRIX_URL` — or `None` when neither is set.
|
||||||
|
///
|
||||||
|
/// `None` is a real answer, not a failure: the account is skipped, the
|
||||||
|
/// same way [`discover_token_accounts_in`] already skips a discovered
|
||||||
|
/// token whose homeserver sidecar is missing.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn homeserver(&self) -> String {
|
pub fn homeserver(&self) -> Option<String> {
|
||||||
self.homeserver
|
self.homeserver.clone().or_else(paths::homeserver_url)
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(paths::homeserver_url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,17 @@ async fn bring_up_account(
|
||||||
tag: Option<String>,
|
tag: Option<String>,
|
||||||
is_primary: bool,
|
is_primary: bool,
|
||||||
) -> Result<Option<(Client, SyncLoop)>> {
|
) -> Result<Option<(Client, SyncLoop)>> {
|
||||||
let homeserver = cfg.homeserver();
|
let Some(homeserver) = cfg.homeserver() else {
|
||||||
|
// No homeserver for this account: the hive has none to offer (no
|
||||||
|
// matrix vhost) or this agent's `hyperhive.matrix.url` is null. Same
|
||||||
|
// no-op as a missing token — an absent integration, not a guess at
|
||||||
|
// one.
|
||||||
|
tracing::info!(
|
||||||
|
account = %cfg.name,
|
||||||
|
"no homeserver configured (HIVE_MATRIX_URL unset); skipping account"
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
if !tokio::fs::try_exists(&cfg.token_file)
|
if !tokio::fs::try_exists(&cfg.token_file)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,6 @@
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Default homeserver URL when `HIVE_MATRIX_URL` isn't set. Tuwunel
|
|
||||||
/// (the local hive-matrix container) listens on `localhost:8008` by
|
|
||||||
/// default; shared host netns means every agent container resolves
|
|
||||||
/// `localhost` to the same machine.
|
|
||||||
pub const DEFAULT_HOMESERVER: &str = "http://localhost:8008";
|
|
||||||
|
|
||||||
/// Resolve the matrix access-token file path. Override via
|
/// Resolve the matrix access-token file path. Override via
|
||||||
/// `HIVE_MATRIX_TOKEN_FILE`; default is `<HYPERHIVE_STATE_DIR>/matrix-token`,
|
/// `HIVE_MATRIX_TOKEN_FILE`; default is `<HYPERHIVE_STATE_DIR>/matrix-token`,
|
||||||
/// the path `hive-c0re::matrix::ensure_user_for` writes to on agent
|
/// the path `hive-c0re::matrix::ensure_user_for` writes to on agent
|
||||||
|
|
@ -25,11 +19,19 @@ pub fn token_file() -> PathBuf {
|
||||||
PathBuf::from(format!("{state_dir}/matrix-token"))
|
PathBuf::from(format!("{state_dir}/matrix-token"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the homeserver URL. Override via `HIVE_MATRIX_URL`; default
|
/// Resolve the homeserver URL from `HIVE_MATRIX_URL`, or `None` when the
|
||||||
/// is the in-container `localhost:8008` tuwunel.
|
/// harness didn't set one.
|
||||||
|
///
|
||||||
|
/// There is deliberately no built-in default. A hardcoded `localhost:8008`
|
||||||
|
/// used to stand in here, on the reasoning that the tuwunel container shares
|
||||||
|
/// the host netns — but *this daemon runs inside an agent*, which does not, so
|
||||||
|
/// that address named the agent itself. Unset means the hive has no homeserver
|
||||||
|
/// to offer this agent, and the daemon no-ops exactly as it does without a
|
||||||
|
/// token; guessing would be a value that starts fine and then talks to the
|
||||||
|
/// wrong machine.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn homeserver_url() -> String {
|
pub fn homeserver_url() -> Option<String> {
|
||||||
std::env::var("HIVE_MATRIX_URL").unwrap_or_else(|_| DEFAULT_HOMESERVER.to_owned())
|
std::env::var("HIVE_MATRIX_URL").ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persistent sqlite store directory for matrix-sdk's state (event
|
/// Persistent sqlite store directory for matrix-sdk's state (event
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,6 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
userName = config.hyperhive.user.name;
|
userName = config.hyperhive.user.name;
|
||||||
# Single source of truth for the default matrix homeserver URL, shared
|
|
||||||
# by the `hyperhive.matrix.url` option default and the daemon-unit guard
|
|
||||||
# that decides whether to set a unit-level HIVE_MATRIX_URL (so the two
|
|
||||||
# cannot drift). Matches the daemon's own built-in default
|
|
||||||
# (`paths::DEFAULT_HOMESERVER`).
|
|
||||||
matrixUrlDefault = "http://localhost:8008";
|
|
||||||
# Rasterize the operator-set agent icon (`hyperhive.icon`, an SVG) to a
|
# Rasterize the operator-set agent icon (`hyperhive.icon`, an SVG) to a
|
||||||
# 512x512 PNG so the matrix daemon can upload it as each account's avatar
|
# 512x512 PNG so the matrix daemon can upload it as each account's avatar
|
||||||
# over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar).
|
# over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar).
|
||||||
|
|
@ -36,13 +30,13 @@ in
|
||||||
When true (the default), the harness:
|
When true (the default), the harness:
|
||||||
|
|
||||||
- runs `hive-matrix-daemon` as a systemd unit that holds a
|
- runs `hive-matrix-daemon` as a systemd unit that holds a
|
||||||
matrix-sdk Client + sync against the homeserver at
|
matrix-sdk Client + sync against the homeserver named by
|
||||||
`HIVE_MATRIX_URL` (default `http://localhost:8008` — the
|
`HIVE_MATRIX_URL` (see `hyperhive.matrix.url` — there is no
|
||||||
in-host tuwunel from `nix/host-modules/hive-matrix.nix`). The
|
default, since an agent's own netns makes a loopback guess
|
||||||
daemon auto-skips when `<state>/matrix-token` is missing,
|
wrong). The daemon auto-skips when that URL or
|
||||||
and a `systemd.paths` watcher restarts it the moment
|
`<state>/matrix-token` is missing, and a `systemd.paths`
|
||||||
hive-c0re provisions the token (same path-trigger shape
|
watcher restarts it the moment hive-c0re provisions the token
|
||||||
as `forge-avatar-sync`).
|
(same path-trigger shape as `forge-avatar-sync`).
|
||||||
- exposes the matrix tool surface (send_message, send_dm,
|
- exposes the matrix tool surface (send_message, send_dm,
|
||||||
send_reaction, send_reply, mark_read, list_rooms,
|
send_reaction, send_reply, mark_read, list_rooms,
|
||||||
list_room_members, read_room) to claude via an auto-injected
|
list_room_members, read_room) to claude via an auto-injected
|
||||||
|
|
@ -63,17 +57,27 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
options.hyperhive.matrix.url = lib.mkOption {
|
options.hyperhive.matrix.url = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = matrixUrlDefault;
|
default = null;
|
||||||
example = "https://matrix.darkest.space";
|
example = "https://matrix.darkest.space";
|
||||||
description = ''
|
description = ''
|
||||||
Matrix homeserver URL the agent's `hive-matrix-daemon` connects
|
Matrix homeserver URL the agent's `hive-matrix-daemon` connects
|
||||||
to. At runtime hive-c0re forwards the isolation-aware URL
|
to. hive-c0re writes this per agent from the hive's own
|
||||||
(`matrix.<domain>` via the gateway) so isolated agents reach
|
isolation-aware URL (`matrix.<domain>` via the gateway), so a
|
||||||
the homeserver without crossing host loopback. Override
|
generated agent config always carries a real value; set it by
|
||||||
per-agent when an agent should talk to an external homeserver
|
hand only when an agent should talk to an external homeserver
|
||||||
instead (e.g. a federation-only setup or a remote hive's
|
instead (a federation-only setup, or a remote hive's tuwunel
|
||||||
tuwunel reached via a vpn).
|
reached over a vpn).
|
||||||
|
|
||||||
|
**`null` means "no matrix", not "guess one".** There is
|
||||||
|
deliberately no loopback default: the homeserver may run on a
|
||||||
|
different host from the agents, and inside an agent's network
|
||||||
|
namespace `localhost` reaches the agent rather than the
|
||||||
|
homeserver, so a default would be a value that builds fine and
|
||||||
|
then talks to the wrong machine. When this is `null` the daemon
|
||||||
|
is left without a homeserver and no-ops, exactly as it does when
|
||||||
|
the token file is absent --- an absent integration, never a
|
||||||
|
misdirected one.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -240,15 +244,15 @@ in
|
||||||
HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock";
|
HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock";
|
||||||
RUST_LOG = "info";
|
RUST_LOG = "info";
|
||||||
}
|
}
|
||||||
# Homeserver URL: by default the daemon inherits the host-forwarded
|
# Homeserver URL. hive-c0re writes this option per agent from the
|
||||||
# HIVE_MATRIX_URL (set by hive-c0re to `matrix.<domain>` via the
|
# hive's own `matrix.<domain>` gateway URL (agents run in a private
|
||||||
# gateway, since agents run in private netns and can't reach host
|
# netns and cannot reach host loopback), so on a real hive it is
|
||||||
# loopback directly), falling back to the daemon's built-in
|
# always set; `null` is the honest "this agent has no homeserver"
|
||||||
# localhost default if the forward is absent. A per-agent
|
# and leaves the daemon without one, which it treats like a missing
|
||||||
# `hyperhive.matrix.url` override (non-default) is set unit-level
|
# token and no-ops. Nothing here falls back to loopback: that would
|
||||||
# so it wins over the forwarded value; at the default we
|
# be a value that evaluates fine and then addresses the agent's own
|
||||||
# deliberately DON'T set it so the forwarded value isn't shadowed.
|
# netns instead of the homeserver.
|
||||||
// lib.optionalAttrs (config.hyperhive.matrix.url != matrixUrlDefault) {
|
// lib.optionalAttrs (config.hyperhive.matrix.url != null) {
|
||||||
HIVE_MATRIX_URL = config.hyperhive.matrix.url;
|
HIVE_MATRIX_URL = config.hyperhive.matrix.url;
|
||||||
}
|
}
|
||||||
# Multi-account: serialize the *extra* accounts to the JSON the
|
# Multi-account: serialize the *extra* accounts to the JSON the
|
||||||
|
|
|
||||||
|
|
@ -100,18 +100,38 @@ in
|
||||||
# gated on hyperhive being enabled). See `docs/gateway.md::HIVE_FORGE_URL`.
|
# gated on hyperhive being enabled). See `docs/gateway.md::HIVE_FORGE_URL`.
|
||||||
HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}";
|
HIVE_FORGE_URL = "http://${config.services.hyperhive.forge.domain}";
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs config.services.hyperhive.matrix.enable {
|
//
|
||||||
|
lib.optionalAttrs
|
||||||
|
(config.services.hyperhive.matrix.enable && config.services.hyperhive.matrix.gatewayHost != null)
|
||||||
|
{
|
||||||
# In-cluster matrix homeserver URL for each agent's
|
# In-cluster matrix homeserver URL for each agent's
|
||||||
# hive-matrix-daemon — the gateway vhost (`matrix.<domain>`). The
|
# hive-matrix-daemon — the gateway vhost (`matrix.<domain>`).
|
||||||
# gatewayHost null-guard falls back to loopback so a domain-less
|
# Forwarded to agents by meta.rs alongside HIVE_FORGE_URL; shares the
|
||||||
# config still evals. Forwarded to agents by meta.rs alongside
|
# same env-forwarding ordering caveat (value baked at
|
||||||
# HIVE_FORGE_URL; shares the same env-forwarding ordering caveat
|
# config-generation time).
|
||||||
# (value baked at config-generation time).
|
#
|
||||||
HIVE_MATRIX_URL =
|
# A domain-less config forwards nothing rather than falling back to
|
||||||
if config.services.hyperhive.matrix.gatewayHost != null then
|
# loopback. The old fallback read as harmless because hive-c0re shares
|
||||||
"http://${config.services.hyperhive.matrix.gatewayHost}"
|
# the host netns — but the value it produced was handed to *agents*,
|
||||||
else
|
# which do not, so `127.0.0.1` there names the agent itself. An absent
|
||||||
"http://127.0.0.1:${toString config.services.hyperhive.matrix.httpPort}";
|
# forward leaves `hyperhive.matrix.url` null and the daemon no-ops;
|
||||||
|
# that is the honest answer when the hive has no matrix vhost to point
|
||||||
|
# at.
|
||||||
|
HIVE_MATRIX_URL = "http://${config.services.hyperhive.matrix.gatewayHost}";
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (config.services.hyperhive.matrix.apiUrl != null) {
|
||||||
|
# Client-server API base hive-c0re uses to provision matrix (register
|
||||||
|
# agent users, create the hive space + chat room, invite members).
|
||||||
|
# Supplied by `services.hyperhive.matrix.apiUrl`, which the matrix
|
||||||
|
# module fills in with its own loopback listener when it is the thing
|
||||||
|
# running tuwunel — and which the operator sets by hand when the
|
||||||
|
# homeserver lives on another machine.
|
||||||
|
#
|
||||||
|
# NOT the agent-facing HIVE_MATRIX_URL above: that one is the gateway
|
||||||
|
# vhost, and it is absent whenever there is no vhost. Reusing it here
|
||||||
|
# would silently stop provisioning on a hive that runs matrix without
|
||||||
|
# one.
|
||||||
|
HIVE_MATRIX_API_URL = config.services.hyperhive.matrix.apiUrl;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs config.services.hyperhive.matrix.gui.enable {
|
// lib.optionalAttrs config.services.hyperhive.matrix.gui.enable {
|
||||||
# Availability flags read by the dashboard's `/api/state`.
|
# Availability flags read by the dashboard's `/api/state`.
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,37 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
apiUrl = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = if cfg.enable then "http://127.0.0.1:${toString cfg.httpPort}" else null;
|
||||||
|
defaultText = lib.literalExpression ''
|
||||||
|
if services.hyperhive.matrix.enable
|
||||||
|
then "http://127.0.0.1:''${toString services.hyperhive.matrix.httpPort}"
|
||||||
|
else null
|
||||||
|
'';
|
||||||
|
example = "https://matrix.example.com";
|
||||||
|
description = ''
|
||||||
|
Client-server API base URL **hive-c0re itself** uses to
|
||||||
|
provision matrix (register agent users, create the hive space
|
||||||
|
and chat room, invite members). Distinct from the agent-facing
|
||||||
|
`hyperhive.matrix.url`, which is the gateway vhost handed to
|
||||||
|
each agent's `hive-matrix-daemon`.
|
||||||
|
|
||||||
|
Defaults to the loopback listener **only when this module is the
|
||||||
|
thing running tuwunel** — in that case the address is not a
|
||||||
|
guess, it is where this module just put the container. Set it
|
||||||
|
explicitly (with `enable = false`) when the homeserver runs on
|
||||||
|
another machine; "everything on one host" is a special case of
|
||||||
|
the full deployment, not the assumption.
|
||||||
|
|
||||||
|
`null` means hive-c0re has no homeserver to provision against
|
||||||
|
and matrix provisioning no-ops. There is deliberately no
|
||||||
|
fallback compiled into the daemon: an address baked into the
|
||||||
|
binary is one that builds fine and then talks to the wrong
|
||||||
|
machine.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
gatewayHost = lib.mkOption {
|
gatewayHost = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = "matrix.${hyperhiveDomain}";
|
default = "matrix.${hyperhiveDomain}";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue