Watch
0
0
Fork
You've already forked hyperhive
0

matrix: the agent's daemon pulls its linked accounts from bao itself

hive-matrix-daemon now learns which external matrix accounts it has from
the swarm secret store, under the agent's own certificate, and the hive
push chain for matrix is gone.

The daemon lists swarm/agents/<agent>/matrix/ (the `list` its policy
grants on its own metadata subtree), reads each account's homeserver
from its credential, and brings the accounts up with their tokens from
the store. Every two minutes it lists again and exits with 75 when the
set of linked accounts changed; the unit restarts on 75 without counting
a failure. A listed name whose credential reads as absent is skipped and
logged once. At start it removes the matrix-token-<a> /
matrix-account-<a>.json pairs a hive delivered (a sidecar marks a pair
as delivered; a declared tokenFile keeps its token).

Removed: CredentialNotice and the $SWARM.credential.* subject and NATS
grant, the controller's publish and its queue precondition on the PUT
route, hive-c0re's credential subscription arm and workers/credential.rs,
priv_client::write_agent_matrix_token, hive-priv's WriteAgentMatrixToken
and its helpers, and the daemon's state-dir account discovery.

Kept: WriteAgentGithubToken and the external-forge path
(WriteAgentExtraForgeAccount, extra_forges.rs) are untouched, and a
declared matrixAccounts tokenFile is still read when the store has no
token for that account.

Refs #4348
This commit is contained in:
atlas 2026-10-01 17:41:20 +02:00
commit 97fb76ce99
22 changed files with 553 additions and 813 deletions

View file

@ -1,5 +1,5 @@
//! The matrix access token as a value this process holds, and the two places
//! it comes from.
//! it comes from; and the accounts the store links to this agent.
//!
//! 🩸 **A secret is a path, not a value.** Nothing here writes the token
//! anywhere, interpolates it into a command, or lets it reach a log line or an
@ -14,10 +14,12 @@
//! systemd credential (`nix/agent-modules/bao.nix`). The hive is not in the
//! path of the value at all.
//!
//! The file arm is the hive-side delivery that still runs beside this one for
//! extra accounts (`hive_c0re::workers::credential`), and the `main` token a
//! hive minted before the swarm did. That is what this replaces, not something
//! it depends on, and it is the arm that goes when the hive-side loop does.
//! Which accounts exist is read the same way: [`linked_accounts`] lists this
//! agent's `matrix/` directory, which its policy grants `list` on
//! (`swarm_secret_client::policy::render_agent`).
//!
//! The file arm is a `tokenFile` an operator declared in `matrixAccounts`, or
//! the `main` token a hive minted before the swarm did.
use std::path::{Path, PathBuf};
@ -44,7 +46,7 @@ pub const ENV_AGENT: &str = "HIVE_AGENT_NAME";
/// the whole point of this module is that the thing beside it never is.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Origin {
/// A file in this agent's state dir, written by the hive-side delivery.
/// A `tokenFile` declared for this account.
File(PathBuf),
/// A path in the swarm secret store, read by this agent as itself.
Store(String),
@ -102,34 +104,21 @@ impl Token {
/// Read this agent's credential for `account` out of the swarm secret
/// store, under this agent's own certificate.
///
/// `Ok(None)` means this deployment has no store: the agent's hive was
/// given no `BAO_ADDR` to forward, so `nix/agent-modules/bao.nix` minted no
/// identity check and there is nothing here to log in to. That is an absent
/// integration, not a failure — the caller falls back to the file the hive
/// delivered.
/// `Ok(None)` means this container was given no store (no `BAO_ADDR`).
///
/// # Errors
/// A name that is not a single path segment, an environment naming an
/// identity that cannot be read, a store that refuses the certificate or
/// the path, or a credential stored empty.
pub async fn from_store(agent: &str, account: &str) -> Result<Option<Self>> {
let Some(settings) = store_settings(|k| std::env::var(k).ok())? else {
return Ok(None);
};
let path = matrix::account_path(agent, account)
.context("building this agent's credential path in the store")?;
let role = policy::agent_object_name(agent)
.context("building this agent's cert-auth role name")?;
let store = SecretStore::connect(&settings, &role, DEFAULT_CERT_MOUNT)
.await
.context("logging in to the swarm secret store as this agent")?;
// The stored object also carries the account's homeserver, and it is
// deliberately dropped here: the account's URL is already settled by
// the time this runs (`AccountCfg::homeserver`), and taking it from the
// store instead would change which accounts come up at all — that is
// the discovery half of this move, which goes with the hive-side loop
// rather than with the read.
let Some(store) = connect(agent).await? else {
return Ok(None);
};
// The stored object also carries the account's homeserver, dropped
// here: the account's URL is settled before this runs
// (`AccountCfg::homeserver`), from config or from [`linked_accounts`].
let credential: matrix::Credential = store
.read(&path)
.await
@ -145,7 +134,7 @@ impl Token {
}))
}
/// Read a token out of the file the hive-side delivery wrote.
/// Read a token out of an account's declared `tokenFile`.
///
/// `Ok(None)` when the file is not there — the account has not been
/// provisioned yet, which the caller treats as "skip", not "fail".
@ -190,11 +179,93 @@ impl Token {
}
}
/// An account the store links to this agent: its name, and the homeserver
/// stored beside its token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Linked {
/// The account name, as the store lists it.
pub name: String,
/// `None` for a credential stored without one.
pub homeserver: Option<String>,
}
/// What [`linked_accounts`] found.
#[derive(Debug, Default)]
pub struct LinkedAccounts {
/// The listed accounts the store holds a credential for.
pub found: Vec<Linked>,
/// Listed names whose newest version the store answers 404 for: an account
/// whose credential was deleted while its metadata stays listed.
pub missing: Vec<String>,
}
/// The accounts the store lists under `agent`'s `matrix/` directory, each read
/// for its homeserver.
///
/// `Ok(None)` means this container was given no store (no `BAO_ADDR`). An
/// empty directory is an agent with no linked accounts, not an error.
///
/// # Errors
/// A name that is not a single path segment, an environment naming an
/// identity that cannot be read, or a store that refuses the certificate, the
/// listing or a path. A policy minted without `list` on the agent's metadata
/// path is refused the listing.
pub async fn linked_accounts(agent: &str) -> Result<Option<LinkedAccounts>> {
let dir = matrix::accounts_dir(agent).context("building this agent's accounts path")?;
let Some(store) = connect(agent).await? else {
return Ok(None);
};
let keys = store
.list(&dir)
.await
.with_context(|| format!("listing {dir} in the store"))?;
let mut out = LinkedAccounts::default();
// A key ending in `/` is a directory below `dir`, not an account.
for name in keys.into_iter().filter(|k| !k.ends_with('/')) {
let path = matrix::account_path(agent, &name)
.with_context(|| format!("building the path of listed account {name:?}"))?;
let stored: Option<matrix::Credential> = store
.read_optional(&path)
.await
.with_context(|| format!("reading {path} from the store"))?;
match stored {
Some(credential) => out.found.push(Linked {
name,
homeserver: credential.homeserver,
}),
None => out.missing.push(name),
}
}
Ok(Some(out))
}
/// Log in to the store as `agent`, or `None` when this container was given no
/// store.
///
/// `None` is an absent integration, not a failure: the agent's hive was given
/// no `BAO_ADDR` to forward, so `nix/agent-modules/bao.nix` minted no identity
/// and there is nothing to log in to.
///
/// # Errors
/// An environment naming an identity that cannot be read, or a store that
/// refuses the certificate.
async fn connect(agent: &str) -> Result<Option<SecretStore>> {
let Some(settings) = store_settings(|k| std::env::var(k).ok())? else {
return Ok(None);
};
let role =
policy::agent_object_name(agent).context("building this agent's cert-auth role name")?;
let store = SecretStore::connect(&settings, &role, DEFAULT_CERT_MOUNT)
.await
.context("logging in to the swarm secret store as this agent")?;
Ok(Some(store))
}
/// This agent's name, or `None` when the harness did not say.
///
/// `None` is not a failure: it is what an agent whose harness predates
/// [`ENV_AGENT`] looks like, and such an agent keeps working off the file its
/// hive delivers.
/// [`ENV_AGENT`] looks like, and such an agent keeps working off its declared
/// token files.
#[must_use]
pub fn agent_name() -> Option<String> {
std::env::var(ENV_AGENT).ok().filter(|v| !v.is_empty())