matrix: provision per-agent accounts on startup via UIAA registration (#548 phase 2)
This commit is contained in:
parent
b3f675e367
commit
50ceb929d7
4 changed files with 397 additions and 7 deletions
|
|
@ -107,6 +107,15 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched)
|
||||||
and meta read access; mirrors each applied repo
|
and meta read access; mirrors each applied repo
|
||||||
into `agent-configs/<n>` (core-only); agents are
|
into `agent-configs/<n>` (core-only); agents are
|
||||||
read-only collaborators on `core/meta`
|
read-only collaborators on `core/meta`
|
||||||
|
src/matrix.rs optional matrix-tuwunel wiring: generates the
|
||||||
|
shared registration token at
|
||||||
|
`/var/lib/hyperhive/matrix-register-token`,
|
||||||
|
provisions per-agent matrix accounts via the
|
||||||
|
matrix-spec UIAA two-leg registration flow,
|
||||||
|
persists each agent's access_token to
|
||||||
|
`<state>/matrix-token`. Idempotent — skips
|
||||||
|
registration when token file already exists.
|
||||||
|
No-op when `hive-matrix` container absent (#548).
|
||||||
src/dashboard.rs axum HTTP: /api/state JSON + actions
|
src/dashboard.rs axum HTTP: /api/state JSON + actions
|
||||||
+ journald viewer + bind-with-retry (SO_REUSEADDR)
|
+ journald viewer + bind-with-retry (SO_REUSEADDR)
|
||||||
+ deployed_sha chip per container +
|
+ deployed_sha chip per container +
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ mod scheduled_prompts_worker;
|
||||||
mod limits;
|
mod limits;
|
||||||
mod loose_ends;
|
mod loose_ends;
|
||||||
mod manager_server;
|
mod manager_server;
|
||||||
|
mod matrix;
|
||||||
mod meta;
|
mod meta;
|
||||||
mod migrate;
|
mod migrate;
|
||||||
mod operator_questions;
|
mod operator_questions;
|
||||||
|
|
@ -231,6 +232,14 @@ async fn cmd_serve(
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
forge::ensure_all().await;
|
forge::ensure_all().await;
|
||||||
});
|
});
|
||||||
|
// Matrix user sweep: same shape — ensure every container has
|
||||||
|
// an account on the local matrix-tuwunel homeserver with an
|
||||||
|
// access_token persisted to `<state>/matrix-token` (#548). No-op
|
||||||
|
// when the hive-matrix container isn't running. Backgrounded
|
||||||
|
// because UIAA is a two-roundtrip dance per agent.
|
||||||
|
tokio::spawn(async move {
|
||||||
|
matrix::ensure_all().await;
|
||||||
|
});
|
||||||
// Periodic broker vacuum: drop fully-acked messages older
|
// Periodic broker vacuum: drop fully-acked messages older
|
||||||
// than 30 days. Delivered-but-unacked rows (recoverable via
|
// than 30 days. Delivered-but-unacked rows (recoverable via
|
||||||
// requeue_inflight) and undelivered rows are always kept.
|
// requeue_inflight) and undelivered rows are always kept.
|
||||||
|
|
|
||||||
331
hive-c0re/src/matrix.rs
Normal file
331
hive-c0re/src/matrix.rs
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
//! Optional matrix-tuwunel wiring. When the `hive-matrix` nixos-container
|
||||||
|
//! is present and running, hive-c0re ensures:
|
||||||
|
//!
|
||||||
|
//! 1. A shared `registration_token` exists at
|
||||||
|
//! `/var/lib/hyperhive/matrix-register-token` (mode 0600, generated
|
||||||
|
//! once on first boot). The hive-matrix module bind-mounts that file
|
||||||
|
//! read-only into the tuwunel container so tuwunel can resolve its
|
||||||
|
//! `registration_token_file` setting against it.
|
||||||
|
//! 2. Every agent (and the manager) has a matrix account on the local
|
||||||
|
//! homeserver with an `access_token` written to
|
||||||
|
//! `<agent-state>/matrix-token`. Idempotent: skips registration when
|
||||||
|
//! the token file already exists.
|
||||||
|
//!
|
||||||
|
//! Agents never see the registration token — only their own `access_token`.
|
||||||
|
//! Account provisioning rides the matrix-spec UIAA flow:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! POST /_matrix/client/v3/register
|
||||||
|
//! {"username": "<agent>", "password": "<random>"}
|
||||||
|
//! → 401 {"flows":[{"stages":["m.login.registration_token"]}],
|
||||||
|
//! "session": "<id>", ...}
|
||||||
|
//!
|
||||||
|
//! POST /_matrix/client/v3/register
|
||||||
|
//! {"username": "<agent>", "password": "<random>",
|
||||||
|
//! "auth": {"type": "m.login.registration_token",
|
||||||
|
//! "token": "<reg_token>", "session": "<id>"}}
|
||||||
|
//! → 200 {"user_id": "@agent:server", "access_token": "<at>", ...}
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! No-op when the `hive-matrix` container isn't running (detected via
|
||||||
|
//! `nixos-container list`), so operators who haven't flipped
|
||||||
|
//! `hyperhive.matrix.enable = true` pay nothing.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
use crate::coordinator::Coordinator;
|
||||||
|
|
||||||
|
/// nspawn container name for the matrix homeserver — mirrors
|
||||||
|
/// `hive-forge` and matches the bare-name allow-list the lifecycle
|
||||||
|
/// scanner skips over.
|
||||||
|
const MATRIX_CONTAINER: &str = "hive-matrix";
|
||||||
|
/// Local-host URL of the tuwunel client-server API. Shares the host
|
||||||
|
/// netns so `localhost:<port>` resolves both from the daemon and from
|
||||||
|
/// inside any sub-agent container.
|
||||||
|
const MATRIX_HTTP: &str = "http://localhost:8008";
|
||||||
|
/// Host path of the matrix registration token. Must match
|
||||||
|
/// `hyperhive.matrix.registrationTokenFile` in `nix/modules/hive-matrix.nix`
|
||||||
|
/// (same path is bind-mounted read-only into the tuwunel container so
|
||||||
|
/// the homeserver can read it via `registration_token_file`).
|
||||||
|
const REGISTER_TOKEN_PATH: &str = "/var/lib/hyperhive/matrix-register-token";
|
||||||
|
/// Length (bytes) of the random registration token. 32 raw bytes ⇒
|
||||||
|
/// 64-char hex string; comfortable for a long-lived shared secret.
|
||||||
|
const REGISTER_TOKEN_BYTES: usize = 32;
|
||||||
|
/// HTTP timeout for registration round-trips. UIAA is two POSTs; even
|
||||||
|
/// the slow path should finish well inside this budget.
|
||||||
|
const HTTP_TIMEOUT_SECS: u64 = 10;
|
||||||
|
/// Length (bytes) of the throwaway per-agent matrix password. Random
|
||||||
|
/// 32-byte hex — agents never log in with the password (they
|
||||||
|
/// authenticate by `access_token`), so it's protocol overhead. We
|
||||||
|
/// store it nowhere.
|
||||||
|
const PASSWORD_BYTES: usize = 32;
|
||||||
|
|
||||||
|
/// Token file inside the agent's bind-mounted state dir (visible as
|
||||||
|
/// `/state/matrix-token` from inside the container).
|
||||||
|
fn token_path(name: &str) -> PathBuf {
|
||||||
|
Coordinator::agent_notes_dir(name).join("matrix-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Probe whether `hive-matrix` exists as a nixos-container. Cheap —
|
||||||
|
/// `nixos-container list` is just a directory scan in /etc. Same shape
|
||||||
|
/// as `forge::is_present`.
|
||||||
|
pub async fn is_present() -> bool {
|
||||||
|
let Ok(out) = Command::new("nixos-container").arg("list").output().await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !out.status.success() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.any(|l| l.trim() == MATRIX_CONTAINER)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `n` cryptographic-quality bytes from `/dev/urandom` and return
|
||||||
|
/// them hex-encoded. Avoids pulling a workspace `rand` dep just for
|
||||||
|
/// 32 bytes of randomness; the kernel's CSPRNG is more than enough for
|
||||||
|
/// a long-lived shared secret on the same host.
|
||||||
|
fn random_hex(n: usize) -> Result<String> {
|
||||||
|
use std::io::Read;
|
||||||
|
let mut buf = vec![0_u8; n];
|
||||||
|
let mut f = std::fs::File::open("/dev/urandom").context("open /dev/urandom")?;
|
||||||
|
f.read_exact(&mut buf).context("read /dev/urandom")?;
|
||||||
|
let mut hex = String::with_capacity(n * 2);
|
||||||
|
for b in &buf {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
write!(hex, "{b:02x}").ok();
|
||||||
|
}
|
||||||
|
Ok(hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure the registration token file exists; returns its contents.
|
||||||
|
/// Generates a fresh 32-byte hex token on first call (mode 0600,
|
||||||
|
/// root-only), then re-reads on subsequent calls. The hive-matrix
|
||||||
|
/// nixos module bind-mounts this file read-only into the tuwunel
|
||||||
|
/// container so the homeserver can authenticate registration requests
|
||||||
|
/// against the same secret hive-c0re holds.
|
||||||
|
pub fn ensure_register_token() -> Result<String> {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let path = Path::new(REGISTER_TOKEN_PATH);
|
||||||
|
if let Ok(existing) = std::fs::read_to_string(path) {
|
||||||
|
let trimmed = existing.trim().to_owned();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
return Ok(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let token = random_hex(REGISTER_TOKEN_BYTES)?;
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
std::fs::write(path, format!("{token}\n"))
|
||||||
|
.with_context(|| format!("write registration token to {}", path.display()))?;
|
||||||
|
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
|
||||||
|
tracing::info!(path = %path.display(), "matrix: generated registration token");
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the localpart of a matrix user id for `agent`. Matrix
|
||||||
|
/// usernames are 1-255 chars from the `[a-z0-9._=-/]` alphabet; agent
|
||||||
|
/// names already conform (hyperhive enforces a strict subset), so no
|
||||||
|
/// escaping is needed at the boundary.
|
||||||
|
fn user_localpart(agent: &str) -> &str {
|
||||||
|
agent
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a single registration POST and parse the response. Returns
|
||||||
|
/// `Ok((status, body))` on any successful HTTP round-trip (including
|
||||||
|
/// the expected 401 from the first UIAA leg); errors only on transport
|
||||||
|
/// failure. Body is the parsed JSON value — UIAA passes session +
|
||||||
|
/// flow state via JSON, never via headers.
|
||||||
|
async fn register_post(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
body: &serde_json::Value,
|
||||||
|
) -> Result<(StatusCode, serde_json::Value)> {
|
||||||
|
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/register");
|
||||||
|
let resp = client
|
||||||
|
.post(&url)
|
||||||
|
.json(body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("matrix: POST /register")?;
|
||||||
|
let status = resp.status();
|
||||||
|
let json = resp
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.await
|
||||||
|
.context("matrix: parse /register response")?;
|
||||||
|
Ok((status, json))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the matrix-spec UIAA flow to register `agent` and return the
|
||||||
|
/// resulting access token. Two round-trips: first POST elicits the
|
||||||
|
/// 401 + session id, second POST supplies the registration token in
|
||||||
|
/// the `auth` block. If the homeserver returns 200 on the first POST
|
||||||
|
/// (no flow stages required — `allow_registration` with no token), we
|
||||||
|
/// take the access token directly.
|
||||||
|
async fn register_user(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
agent: &str,
|
||||||
|
register_token: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
let localpart = user_localpart(agent);
|
||||||
|
let password = random_hex(PASSWORD_BYTES)?;
|
||||||
|
let initial = serde_json::json!({
|
||||||
|
"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": format!("hyperhive-{agent}"),
|
||||||
|
"initial_device_display_name": format!("hyperhive ({agent})"),
|
||||||
|
"inhibit_login": false,
|
||||||
|
});
|
||||||
|
let (status, body) = register_post(client, &initial).await?;
|
||||||
|
// Some servers accept the first POST when allow_registration is
|
||||||
|
// open. Take the access_token + we're done.
|
||||||
|
if status.is_success() {
|
||||||
|
return extract_access_token(&body);
|
||||||
|
}
|
||||||
|
if status != StatusCode::UNAUTHORIZED {
|
||||||
|
anyhow::bail!("matrix: /register first leg HTTP {status}, body: {body}");
|
||||||
|
}
|
||||||
|
let session = body["session"]
|
||||||
|
.as_str()
|
||||||
|
.with_context(|| format!("matrix: missing UIAA session in 401, body: {body}"))?;
|
||||||
|
let authed = serde_json::json!({
|
||||||
|
"username": localpart,
|
||||||
|
"password": password,
|
||||||
|
"device_id": format!("hyperhive-{agent}"),
|
||||||
|
"initial_device_display_name": format!("hyperhive ({agent})"),
|
||||||
|
"inhibit_login": false,
|
||||||
|
"auth": {
|
||||||
|
"type": "m.login.registration_token",
|
||||||
|
"token": register_token,
|
||||||
|
"session": session,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let (status, body) = register_post(client, &authed).await?;
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("matrix: /register auth leg HTTP {status}, body: {body}");
|
||||||
|
}
|
||||||
|
extract_access_token(&body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pull `access_token` out of a successful /register response.
|
||||||
|
fn extract_access_token(body: &serde_json::Value) -> Result<String> {
|
||||||
|
body["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.map(str::to_owned)
|
||||||
|
.with_context(|| format!("matrix: missing access_token in response: {body}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure `name` has a matrix user + token file on the local
|
||||||
|
/// homeserver. Skips registration entirely if the token file already
|
||||||
|
/// exists (treating a present token as proof the account is good).
|
||||||
|
/// To force re-registration, delete the token file.
|
||||||
|
pub async fn ensure_user_for(name: &str, register_token: &str) -> Result<()> {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let path = token_path(name);
|
||||||
|
if path.exists()
|
||||||
|
&& let Ok(existing) = std::fs::read_to_string(&path)
|
||||||
|
&& !existing.trim().is_empty()
|
||||||
|
{
|
||||||
|
tracing::debug!(%name, "matrix: token already present");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS))
|
||||||
|
.build()
|
||||||
|
.context("matrix: build HTTP client")?;
|
||||||
|
let access_token = register_user(&client, name, register_token).await?;
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
std::fs::write(&path, format!("{access_token}\n"))
|
||||||
|
.with_context(|| format!("matrix: write token to {}", path.display()))?;
|
||||||
|
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
|
||||||
|
tracing::info!(%name, path = %path.display(), "matrix: registered user + persisted access token");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(name: &str, register_token: &str) {
|
||||||
|
if let Err(e) = ensure_user_for(name, register_token).await {
|
||||||
|
tracing::warn!(%name, error = ?e, "matrix: ensure_user failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sweep every existing container (manager + sub-agents) and ensure
|
||||||
|
/// each has a matrix user + token on the local homeserver. Called once
|
||||||
|
/// at hive-c0re startup, alongside `forge::ensure_all`. No-op when the
|
||||||
|
/// hive-matrix container isn't running. Per-step failures are logged
|
||||||
|
/// but don't abort the sweep.
|
||||||
|
pub async fn ensure_all() {
|
||||||
|
if !is_present().await {
|
||||||
|
tracing::debug!("matrix: hive-matrix container absent, skipping user sweep");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let register_token = match ensure_register_token() {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = ?e, "matrix: ensure_register_token failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Ok(containers) = crate::lifecycle::list().await else {
|
||||||
|
tracing::warn!("matrix: nixos-container list failed; skipping user sweep");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for c in containers {
|
||||||
|
let name = if c == crate::lifecycle::MANAGER_NAME {
|
||||||
|
c
|
||||||
|
} else if let Some(n) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
|
||||||
|
n.to_owned()
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
sync_agent(&name, ®ister_token).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn random_hex_is_well_formed_and_correct_length() {
|
||||||
|
let h = random_hex(16).expect("/dev/urandom readable");
|
||||||
|
assert_eq!(h.len(), 32);
|
||||||
|
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn random_hex_two_calls_differ() {
|
||||||
|
// Sanity check — not a statistical claim, just guards
|
||||||
|
// against ever accidentally returning a constant.
|
||||||
|
let a = random_hex(16).expect("/dev/urandom readable");
|
||||||
|
let b = random_hex(16).expect("/dev/urandom readable");
|
||||||
|
assert_ne!(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_access_token_pulls_from_success_body() {
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"user_id": "@alice:matrix.example.org",
|
||||||
|
"access_token": "syt_abc123",
|
||||||
|
"device_id": "ABC",
|
||||||
|
});
|
||||||
|
assert_eq!(extract_access_token(&body).unwrap(), "syt_abc123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_access_token_errors_on_missing_field() {
|
||||||
|
let body = serde_json::json!({"user_id": "@alice:matrix.example.org"});
|
||||||
|
let err = extract_access_token(&body).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("missing access_token"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,9 +29,21 @@ in
|
||||||
#
|
#
|
||||||
# Initial rollout (#548): federation enabled (needed for multi-hive
|
# Initial rollout (#548): federation enabled (needed for multi-hive
|
||||||
# swarms; trusted_servers starts empty so no actual federation traffic
|
# swarms; trusted_servers starts empty so no actual federation traffic
|
||||||
# leaves until peers are explicitly listed), registration via admin
|
# leaves until peers are explicitly listed), registration enabled via
|
||||||
# API only, e2ee disabled per operator call (tracked for follow-up at
|
# a `registration_token_file` known only to hive-c0re (so agents can't
|
||||||
# #551).
|
# self-register without going through the coordinator), e2ee disabled
|
||||||
|
# per operator call (tracked for follow-up at #551).
|
||||||
|
#
|
||||||
|
# Provisioning model (matches `nix/modules/hive-forge.nix` shape):
|
||||||
|
# hive-c0re generates a 32-byte random `registration_token` on first
|
||||||
|
# boot, writes it to `/var/lib/hyperhive/matrix-register-token` (mode
|
||||||
|
# 0600, root-only), and bind-mounts that file read-only into the
|
||||||
|
# tuwunel container at the same path so tuwunel can read it via
|
||||||
|
# `registration_token_file`. hive-c0re then uses the token to register
|
||||||
|
# each agent account via the matrix-spec UIAA registration flow, and
|
||||||
|
# persists the returned `access_token` to `<agent-state>/matrix-token`
|
||||||
|
# so the agent's matrix MCP client can authenticate without ever
|
||||||
|
# seeing the shared registration token.
|
||||||
|
|
||||||
options.hyperhive.matrix = {
|
options.hyperhive.matrix = {
|
||||||
enable = lib.mkOption {
|
enable = lib.mkOption {
|
||||||
|
|
@ -127,6 +139,22 @@ in
|
||||||
media uploads + the upstream tuwunel default.
|
media uploads + the upstream tuwunel default.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
registrationTokenFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "/var/lib/hyperhive/matrix-register-token";
|
||||||
|
description = ''
|
||||||
|
Host path to a file containing the matrix registration token
|
||||||
|
tuwunel reads to authorise new-account creation. The token is
|
||||||
|
generated automatically by `hive-c0re` on first boot (32-byte
|
||||||
|
random hex, mode 0600) and is bind-mounted read-only into the
|
||||||
|
tuwunel container at the same path. Agents never see this
|
||||||
|
token — hive-c0re uses it to provision per-agent accounts
|
||||||
|
and the agent only receives the resulting `access_token`.
|
||||||
|
Override only when integrating with externally-managed
|
||||||
|
registration tokens.
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -160,6 +188,16 @@ in
|
||||||
# host-side services, no port-forward plumbing, and agent
|
# host-side services, no port-forward plumbing, and agent
|
||||||
# containers (also host netns) reach it via plain `localhost`.
|
# containers (also host netns) reach it via plain `localhost`.
|
||||||
privateNetwork = false;
|
privateNetwork = false;
|
||||||
|
# Read-only bind of the host-managed registration token so
|
||||||
|
# tuwunel can resolve `registration_token_file` to a real
|
||||||
|
# file inside the container. The host path doesn't need to
|
||||||
|
# exist at eval time (the file is generated by hive-c0re's
|
||||||
|
# `matrix::ensure_register_token` on first boot); nspawn will
|
||||||
|
# create the bind mount on container start either way.
|
||||||
|
bindMounts.${cfg.registrationTokenFile} = {
|
||||||
|
hostPath = cfg.registrationTokenFile;
|
||||||
|
isReadOnly = true;
|
||||||
|
};
|
||||||
config =
|
config =
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
|
|
@ -181,10 +219,13 @@ in
|
||||||
# keeps it effectively closed until peers are listed.
|
# keeps it effectively closed until peers are listed.
|
||||||
allow_federation = true;
|
allow_federation = true;
|
||||||
trusted_servers = cfg.trustedServers;
|
trusted_servers = cfg.trustedServers;
|
||||||
# Registration off — operator seeds agent accounts via
|
# Token-gated registration: hive-c0re holds the token,
|
||||||
# the tuwunel admin API (mirrors the forge pattern;
|
# agents never see it. allow_registration must be true
|
||||||
# see `hive-c0re/src/matrix.rs` once #548 PR 2 lands).
|
# for the token flow to engage; the absent
|
||||||
allow_registration = false;
|
# `yes_i_am_very_very_sure_…_open_registration_…` flag
|
||||||
|
# keeps the server closed to anyone without the token.
|
||||||
|
allow_registration = true;
|
||||||
|
registration_token_file = toString cfg.registrationTokenFile;
|
||||||
# E2EE disabled in initial rollout per operator call
|
# E2EE disabled in initial rollout per operator call
|
||||||
# (#548) — re-enabling tracked at #551.
|
# (#548) — re-enabling tracked at #551.
|
||||||
allow_encryption = false;
|
allow_encryption = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue