hives read the forge through their own swarm-minted read account
swarm-controller now gives every hive in its directory a forge account, `hive-<hive>`, in a new `hives` team of `agent-configs` (read on `repo.code` + `repo.pulls`, every repo including later ones), and mints one `read:repository` token named `swarm-hive` into the store at `swarm/hives/<hive>/forge/token`. A five-minute pass queues a `MintHiveForgeToken` node for a hive whose token is missing or no longer matches the forge, whose account is missing, or whose team membership or team settings drifted. A hive whose store or forge read failed is never queued, so an outage cannot rotate every hive's token. The token decision reuses `agent_token`'s classifier and replace step, now parameterised by name and scopes. An agent may be called `hive-<x>`. The hive account is created with `hive-<hive>@hives.hyperhive.local` and the job refuses an account by that name with any other email before it locks it down, adds it to the team or mints for it; agent creation already refuses the hive's account by the same email check. The controller's bao policy gains the `swarm/hives/+/forge/token` leaf (create/read/update), pinned by the bao-grants module-eval assertion. The hive's own policy already reads `swarm/hives/<hive>/*`. hive-c0re reads that token through `forge::reader_token()`, falling back to the core token file when the store holds none, and uses it for: fetching a merged config rev (swarm deploy, `hivectl forge reconcile-config`), reseeding a purged `applied`, the meta-update cascade's fast-forward, seeding `proposed` on first deploy, and finding the PR that merged a refused rev. Those reads no longer require a local `hive-forge` container. The `/knowledge` clone moves from the forge host's boot sweep into the knowledge pull, so every hive clones it. Posting the deploy-failure comment on that PR stays on the core token. Refs #4851
This commit is contained in:
parent
c8472966e9
commit
21e3f626f4
17 changed files with 1222 additions and 138 deletions
|
|
@ -134,6 +134,10 @@ enum SwarmNodeKind {
|
|||
/// in the swarm store, where that hive's matrix sweep reads it. See
|
||||
/// `matrix_account::hive_sender`.
|
||||
MintHiveSenderToken { hive: String },
|
||||
/// Make sure `hive`'s read-only forge account (`hive-<hive>`) exists, is in
|
||||
/// the `hives` team, and holds a live token in the swarm store, where
|
||||
/// that hive's hive-c0re reads it. See `forge::hive_token`.
|
||||
MintHiveForgeToken { hive: String },
|
||||
/// Re-mint `agent`'s queue secret if it is still stored and old enough.
|
||||
/// See `agent_renewal`.
|
||||
///
|
||||
|
|
@ -172,6 +176,7 @@ impl hive_jobq_wire::WireNode for SwarmNodeKind {
|
|||
SwarmNodeKind::MintAgentForgeToken { .. } => "mint_agent_forge_token".to_owned(),
|
||||
SwarmNodeKind::MintAgentMatrixAccount { .. } => "mint_agent_matrix_account".to_owned(),
|
||||
SwarmNodeKind::MintHiveSenderToken { .. } => "mint_hive_sender_token".to_owned(),
|
||||
SwarmNodeKind::MintHiveForgeToken { .. } => "mint_hive_forge_token".to_owned(),
|
||||
SwarmNodeKind::RenewAgentQueueCredential { .. } => {
|
||||
"renew_agent_queue_credential".to_owned()
|
||||
}
|
||||
|
|
@ -199,7 +204,8 @@ impl hive_jobq_wire::WireNode for SwarmNodeKind {
|
|||
| SwarmNodeKind::RenewAgentQueueCredential { agent } => {
|
||||
serde_json::json!({ "agent": agent })
|
||||
}
|
||||
SwarmNodeKind::MintHiveSenderToken { hive } => serde_json::json!({ "hive": hive }),
|
||||
SwarmNodeKind::MintHiveSenderToken { hive }
|
||||
| SwarmNodeKind::MintHiveForgeToken { hive } => serde_json::json!({ "hive": hive }),
|
||||
SwarmNodeKind::TriggerDeploy { hive, agent, rev } => {
|
||||
serde_json::json!({ "agent": agent, "hive": hive, "rev": rev })
|
||||
}
|
||||
|
|
@ -353,12 +359,10 @@ async fn run_swarm_node(
|
|||
SwarmNodeKind::MintHiveSenderToken { hive } => {
|
||||
mint_hive_sender(deps.matrix_homeserver.as_deref(), &hive).await
|
||||
}
|
||||
SwarmNodeKind::RenewAgentQueueCredential { agent } => {
|
||||
match agent_renewal::renew(&agent).await {
|
||||
Ok(()) => Outcome::Done,
|
||||
Err(e) => Outcome::Failed(format!("{e:#}")),
|
||||
}
|
||||
SwarmNodeKind::MintHiveForgeToken { hive } => {
|
||||
mint_hive_forge_token(deps.forge, &hive).await
|
||||
}
|
||||
SwarmNodeKind::RenewAgentQueueCredential { agent } => renew_queue_credential(&agent).await,
|
||||
SwarmNodeKind::SetAgentWanted { hive, agent } => match deps.wanted {
|
||||
None => Outcome::Failed(
|
||||
"no swarm queue is configured on this host, so no wanted-state \
|
||||
|
|
@ -410,6 +414,36 @@ async fn mint_forge_token(
|
|||
}
|
||||
}
|
||||
|
||||
/// The `RenewAgentQueueCredential` arm, lifted out for the same reason.
|
||||
async fn renew_queue_credential(agent: &str) -> hive_jobq::scheduler::Outcome {
|
||||
use hive_jobq::scheduler::Outcome;
|
||||
|
||||
match agent_renewal::renew(agent).await {
|
||||
Ok(()) => Outcome::Done,
|
||||
Err(e) => Outcome::Failed(format!("{e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `MintHiveForgeToken` arm, lifted out for the same reason.
|
||||
async fn mint_hive_forge_token(
|
||||
forge: Option<Arc<forge::Client>>,
|
||||
hive: &str,
|
||||
) -> hive_jobq::scheduler::Outcome {
|
||||
use hive_jobq::scheduler::Outcome;
|
||||
|
||||
let Some(client) = forge else {
|
||||
return Outcome::Failed(
|
||||
"no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \
|
||||
SWARM_CONTROLLER_FORGE_TOKEN_FILE unset), so no hive forge token can be minted"
|
||||
.to_owned(),
|
||||
);
|
||||
};
|
||||
match client.ensure_hive_forge_token(hive).await {
|
||||
Ok(()) => Outcome::Done,
|
||||
Err(e) => Outcome::Failed(format!("{e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `MintAgentMatrixAccount` arm, lifted out for the same reason.
|
||||
async fn mint_matrix_account(
|
||||
homeserver: Option<&str>,
|
||||
|
|
@ -2275,6 +2309,27 @@ fn queue_hive_sender_mints(
|
|||
Ok(ids)
|
||||
}
|
||||
|
||||
/// Insert one `MintHiveForgeToken` job per hive and return the nodes' ids.
|
||||
/// `forge::hive_token::spawn`'s periodic pass is the one caller.
|
||||
fn queue_hive_forge_token_mints(
|
||||
sched: &Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>,
|
||||
hives: Vec<String>,
|
||||
) -> Result<Vec<hive_jobq::NodeId>> {
|
||||
let mut sched = sched
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut ids = Vec::with_capacity(hives.len());
|
||||
for hive in hives {
|
||||
let queued = sched
|
||||
.insert_job(None, |b| {
|
||||
vec![b.node(SwarmNodeKind::MintHiveForgeToken { hive }).guid()]
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
ids.extend(queued);
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// The homeserver agents' own matrix accounts are minted on. Unset is this
|
||||
/// controller creating agents with no matrix account, which the mint node
|
||||
/// reports by name rather than `main` refusing to start.
|
||||
|
|
@ -2315,13 +2370,14 @@ fn spawn_matrix_account_backfill(
|
|||
}
|
||||
|
||||
/// Start the forge's periodic passes — the swarm-wide objects, the runner
|
||||
/// registration token, and agents' tokens (which also sweeps legacy tokens;
|
||||
/// see `forge::legacy_tokens`) —
|
||||
/// registration token, hives' read tokens, and agents' tokens (which also
|
||||
/// sweeps legacy tokens; see `forge::legacy_tokens`) —
|
||||
/// when a forge is configured. Lifted out of `main` for
|
||||
/// `clippy::too_many_lines`.
|
||||
fn spawn_forge_workers(
|
||||
jobq: &Arc<Mutex<hive_jobq::scheduler::Scheduler<SwarmNodeKind, SwarmResourceKind>>>,
|
||||
forge_client: Option<Arc<forge::Client>>,
|
||||
hives: &[HiveEntry],
|
||||
) {
|
||||
let Some(client) = forge_client else {
|
||||
return;
|
||||
|
|
@ -2329,6 +2385,13 @@ fn spawn_forge_workers(
|
|||
forge::objects::spawn(Arc::clone(&client));
|
||||
forge::runner_token::spawn(Arc::clone(&client));
|
||||
let sched = Arc::clone(jobq);
|
||||
let hive_names = hives.iter().map(|h| h.name.clone()).collect();
|
||||
forge::hive_token::spawn(Arc::clone(&client), hive_names, move |hives| {
|
||||
if let Err(e) = queue_hive_forge_token_mints(&sched, hives) {
|
||||
tracing::warn!(error = %format!("{e:#}"), "hive forge tokens: queueing failed");
|
||||
}
|
||||
});
|
||||
let sched = Arc::clone(jobq);
|
||||
forge::agent_token::spawn(client, move |agents| {
|
||||
if let Err(e) = queue_forge_token_mints(&sched, agents) {
|
||||
tracing::warn!(error = %format!("{e:#}"), "agent forge tokens: queueing failed");
|
||||
|
|
@ -2953,7 +3016,7 @@ async fn main() -> Result<()> {
|
|||
);
|
||||
}
|
||||
|
||||
spawn_forge_workers(&jobq, forge_client.clone());
|
||||
spawn_forge_workers(&jobq, forge_client.clone(), &hives);
|
||||
let config_prs = forge_client.clone().map(config_pr::spawn);
|
||||
let state_forge = keep_forge_for_state(forge_client, webhook_secret.clone());
|
||||
|
||||
|
|
@ -4647,6 +4710,39 @@ mod tests {
|
|||
assert_eq!(hives, ["alpha", "beta"]);
|
||||
}
|
||||
|
||||
/// The hive forge-token pass's queueing: one node per hive, naming the
|
||||
/// hive rather than an agent.
|
||||
#[test]
|
||||
fn a_queued_hive_forge_token_mint_is_one_node_per_hive() {
|
||||
use hive_jobq_wire::WireNode as _;
|
||||
|
||||
let sched = std::sync::Mutex::new(hive_jobq::scheduler::Scheduler::new(
|
||||
hive_jobq::Graph::new(),
|
||||
hive_jobq::resources::ResourceTable::new(),
|
||||
));
|
||||
let ids = super::queue_hive_forge_token_mints(
|
||||
&sched,
|
||||
vec!["alpha".to_owned(), "beta".to_owned()],
|
||||
)
|
||||
.expect("two jobs insert");
|
||||
assert_eq!(ids.len(), 2);
|
||||
let guard = sched
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut hives: Vec<String> = guard
|
||||
.graph()
|
||||
.nodes()
|
||||
.map(|n| {
|
||||
assert_eq!(n.payload.label(), "mint_hive_forge_token");
|
||||
let data = n.payload.data(n.id.get());
|
||||
assert!(data.get("agent").is_none(), "{data}");
|
||||
data["hive"].as_str().expect("hive is a string").to_owned()
|
||||
})
|
||||
.collect();
|
||||
hives.sort();
|
||||
assert_eq!(hives, ["alpha", "beta"]);
|
||||
}
|
||||
|
||||
/// The manual route and the periodic pass both go through
|
||||
/// `queue_forge_token_mints`, so this is the assertion that a backfilled
|
||||
/// mint is the same pair of nodes agent creation inserts: the forge user,
|
||||
|
|
|
|||
Loading…
Reference in a new issue