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,116 +0,0 @@
//! Delivering an agent's external-account credential from the swarm's secret
//! store into that agent's own state dir.
//!
//! The controller publishes a [`CredentialNotice`] naming an agent and an
//! account; this reads the value out of the store and hands it to `hive-priv`,
//! which writes it where the agent's matrix daemon already watches. The write
//! goes through the privileged helper because the file lands in a directory
//! owned by the agent and has to be chowned to it — written from here it
//! arrives owned by `hive-core` and the agent cannot read its own credential.
//! `hive-priv` also builds the filename, so nothing in this module decides it.
//!
//! Nothing here activates anything: `nix/agent-modules/matrix.nix` has a
//! `systemd.paths` unit globbing `matrix-token*` inside that agent's own state
//! dir, which re-fires the daemon when a token appears, so arrival is the
//! whole trigger.
//!
//! 🔑 The notice carries no secret — see [`swarm_queue_client::credential_subject`]
//! for why that is a requirement rather than a preference. The value is read
//! from the store under this hive's own identity.
use anyhow::{Context, Result};
use hive_types::Ident;
use swarm_queue_client::CredentialNotice;
use swarm_secret_client::{SecretStore, matrix};
/// Read the credential `notice` names and write it into the agent's state dir.
///
/// `cert_role` is the role on the store's `cert` auth mount whose policy scopes
/// what this hive may read.
///
/// # Errors
/// The store refusing, being unreachable, or holding nothing at that path; a
/// name that is not a single path segment; or the write failing.
pub async fn deliver(notice: &CredentialNotice, cert_role: &str) -> Result<()> {
// Parsed before anything is read, so a malformed name costs a decode and
// not a round trip to the store.
let agent = Ident::parse(&notice.agent)
.map_err(|e| anyhow::anyhow!("agent name {:?} off the queue: {e}", notice.agent))?;
let secret_path = matrix::account_path(&notice.agent, &notice.account)
.context("building the credential's path in the store")?;
let store = SecretStore::from_env(cert_role)
.await
.context("connecting to the swarm secret store")?;
let credential: matrix::Credential = store
.read(&secret_path)
.await
.with_context(|| format!("reading {secret_path} from the store"))?;
// Through hive-priv rather than writing here: the file lands in a directory
// owned by the agent, and only root can chown it there. Written directly it
// arrives owned by `hive-core` at 0600 — the daemon wakes on it appearing
// and cannot read it. hive-priv also builds the filename, so the name the
// watcher globs for is decided in one place now.
//
// A homeserver is passed through when the stored credential carries one;
// hive-priv then writes the `matrix-account-<name>.json` sidecar beside the
// token, which is how the daemon discovers an extra account's homeserver
// without a static `matrixAccounts` entry. Credentials written before that
// field existed carry `None`, and the sidecar is simply not written — the
// account then needs a configured entry, exactly as it did before.
crate::priv_client::write_agent_matrix_token(
agent.as_str(),
&credential.value,
Some(&notice.account),
credential.homeserver.as_deref(),
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_name_that_could_address_another_agent_is_refused() {
// `matrix::account_path` owns this rule; asserted here because this is
// the module that feeds it names off the wire.
assert!(matrix::account_path("../argus", "ccc").is_err());
assert!(matrix::account_path("dmatrix", "../../etc/x").is_err());
assert!(matrix::account_path("dmatrix", "ccc").is_ok());
}
#[test]
fn an_agent_name_off_the_queue_must_pass_the_ident_parser_too() {
// Two independent refusals, not one restated: `matrix::account_path`
// guards the address in the *store*, and `Ident` guards the name this
// module hands to hive-priv, which builds the on-disk path from it.
for bad in ["../argus", "dmatrix/../argus", "Dmatrix", "d matrix", ""] {
assert!(Ident::parse(bad).is_err(), "{bad:?} must be refused");
}
// The control: without it, a parser that rejected everything would
// satisfy the loop above.
assert!(Ident::parse("dmatrix").is_ok());
}
/// The account half of that same rule now lives where the filename is
/// built — `hive-priv`'s `validate_account_name`, asserted there with the
/// same arms and the same two controls. It is not restated here because
/// this module no longer builds the path.
#[test]
fn an_account_name_off_the_queue_is_refused_for_the_store_path() {
for bad in ["../argus", "a/b", "a b", ""] {
assert!(
matrix::account_path("dmatrix", bad).is_err(),
"account {bad:?} must be refused"
);
}
// Controls: the legal charset stays reachable, so the loop above is not
// passing because everything is refused. Uppercase and underscore are
// deliberate — `matrixAccounts` is an attrset, so both are names an
// operator can already write, and hive-priv must accept them too.
assert!(matrix::account_path("dmatrix", "ops-relay").is_ok());
assert!(matrix::account_path("dmatrix", "Ops_Relay9").is_ok());
}
}

View file

@ -8,7 +8,6 @@
pub mod agent_sockets;
pub mod auto_update;
pub mod crash_watch;
pub mod credential;
pub mod knowledge;
pub mod mcp_sockets;
pub mod scheduled_prompts_worker;