hive-c0re: deliver an agent's credential from the store to its state dir

mara on #4015: "not merging code without callers", and on the same PR
"see issue, we decided what the first thing should be". #3726 decided it:
the controller writes a token to the store and tells the hive; the hive
reads it back and writes /agents/<agent>/state/matrix-token-<account> at
0600, where matrix.nix's existing systemd.paths glob re-fires the daemon.
So this is the hive half of that, and the library's first caller.

The notice names a credential and never carries one, and deploy_subject's
own doc is why: the auth-callout responder scopes publish and leaves sub
unrestricted, so a hive that wanted another's messages could subscribe to
them. A secret in that payload would be readable swarm-wide. The value is
read from the store under the reading hive's own certificate, where the
store's policy is what actually scopes it.

Two boundaries guard the two addresses, and they are not the same check.
`path::matrix_account` guards the address in the store. `Ident` guards the
address on disk -- `agent_state_dir` takes one, so an unvalidated name off
the queue cannot reach a directory. I had written the first and assumed it
covered both; the compiler refused the `&str` and was right. `token_path`
now takes the newtype so a call site cannot forget.

The write is atomic because the path-watcher fires on the file appearing:
written in place it would be visible while partial, and the daemon would
read a truncated credential exactly once, which is the hardest possible
failure to reproduce. The temp name is dot-prefixed so it cannot match the
`matrix-token*` glob on its way past.

The publish grant is here because without it the failure is invisible.
policy.rs already says why for its siblings: a refused publish reaches the
client as a timeout, so the symptom is a hive that never receives a
credential with nothing in either log naming a permission. Two tests: the
controller may publish, a hive may not -- its own subject included. A
forged notice leaks nothing, but it would make a hive fetch and overwrite
a token file for a name the forger chose.

Refs #3726
This commit is contained in:
atlas 2026-09-02 23:28:31 +02:00 committed by mara
commit 7a03ce096a
7 changed files with 282 additions and 1 deletions

View file

@ -212,6 +212,42 @@ pub const DEPLOY_SUBJECT_WILDCARD: &str = "$SWARM.deploy.*";
/// cannot drift into naming different families.
const DEPLOY_SUBJECT_PREFIX: &str = "$SWARM.deploy";
/// The subject the controller publishes on to tell `hive` that a credential
/// for one of its agents is waiting in the secret store. Same per-hive family
/// as [`deploy_subject`], here for the same three-crate reason.
///
/// 🔑 **The message NAMES a credential and never carries one**, and the note
/// on [`deploy_subject`] is why: the family split buys quiet, not
/// confidentiality — the auth-callout responder scopes `pub` and leaves `sub`
/// unrestricted, so any hive that wanted another's messages could subscribe to
/// them. A secret in this payload would be readable swarm-wide. The hive reads
/// the value from the store under its own identity instead, where the store's
/// policy is the thing that actually scopes it.
#[must_use]
pub fn credential_subject(hive: &str) -> String {
format!("{CREDENTIAL_SUBJECT_PREFIX}.{hive}")
}
/// The publish grant covering every [`credential_subject`] — a wildcard for
/// the same no-roster reason as [`DEPLOY_SUBJECT_WILDCARD`].
pub const CREDENTIAL_SUBJECT_WILDCARD: &str = "$SWARM.credential.*";
/// Shared by [`credential_subject`] and [`CREDENTIAL_SUBJECT_WILDCARD`] so the
/// two cannot drift into naming different families.
const CREDENTIAL_SUBJECT_PREFIX: &str = "$SWARM.credential";
/// What a [`credential_subject`] message says: which agent's credential
/// changed, and which account it belongs to. Deliberately the whole payload —
/// anything more would be either derivable by the reader or a secret that
/// must not be on the wire.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CredentialNotice {
/// The agent whose state dir receives the credential.
pub agent: String,
/// The external account the credential authenticates as.
pub account: String,
}
/// What a [`deploy_subject`] message carries.
///
/// Only the agent: the subject already names the hive, and repeating it here