diff --git a/swarm-controller/src/forge.rs b/swarm-controller/src/forge.rs index e4675b06..5bd19bff 100644 --- a/swarm-controller/src/forge.rs +++ b/swarm-controller/src/forge.rs @@ -33,6 +33,18 @@ use crate::webhook::DeliveryKind; /// the same forge instance, not a separate one, so the same org. pub const AGENTS_ORG: &str = "agents"; +/// The repo an agent's config lives in — one repo per agent inside +/// [`AGENTS_ORG`], named after the agent. +/// +/// The identity here is the point, not an accident to be inlined: this is +/// the single home for the naming convention, so a caller holding an agent +/// name never writes the repo name itself. Callers used to pass both, which +/// meant every node payload carried the same string twice and any future +/// change to the convention would have been a search rather than an edit. +pub fn agent_repo(agent: &str) -> &str { + agent +} + /// The `operators` team, whitelisted for the merge gate on every repo /// this client protects — provisioned by `hive-c0re::forge::repos` /// already (`ensure_operators_team`), not re-provisioned here. If that diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 3d8bd633..37100d6a 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -56,13 +56,13 @@ mod webhook; enum SwarmNodeKind { /// Ensure `agent` exists as an authelia subject at the swarm level. CreateIdentity { agent: String }, - /// Create `repo` in `forge::AGENTS_ORG` with the operator merge gate - /// on its default branch. See `forge::Client::create_repo`. - CreateRepo { repo: String }, - /// Add `agent` as a write collaborator on `repo`. See + /// Create the agent's repo in `forge::AGENTS_ORG` with the operator + /// merge gate on its default branch. See `forge::Client::create_repo`. + CreateRepo { agent: String }, + /// Add `agent` as a write collaborator on its own repo. See /// `forge::Client::add_repo_member`. - AddRepoMember { repo: String, agent: String }, - /// Seed `repo` with `agent.nix` + `flake.nix`. See + AddRepoMember { agent: String }, + /// Seed the agent's repo with `agent.nix` + `flake.nix`. See /// `forge::Client::seed_agent_config`. /// /// Deliberately carries no hive: seeding a config repo is the same @@ -70,7 +70,7 @@ enum SwarmNodeKind { /// states nothing about where it runs. The hive is an address the /// swarm routes a deploy message to — it belongs on the node that /// sends that message, not on this one. - InitAgentConfigRepo { repo: String, agent: String }, + InitAgentConfigRepo { agent: String }, } impl hive_jobq_wire::WireNode for SwarmNodeKind { @@ -84,20 +84,18 @@ impl hive_jobq_wire::WireNode for SwarmNodeKind { } fn data(&self, _id: hive_jobq_wire::WireId) -> serde_json::Value { + // Every node in this graph is about exactly one agent, so they all + // render the same and `label()` is what distinguishes them. Spelled + // out as an or-pattern rather than a catch-all on purpose: a fifth + // variant then fails to compile here instead of silently rendering + // as an agent name. match self { - SwarmNodeKind::CreateIdentity { agent } => { + SwarmNodeKind::CreateIdentity { agent } + | SwarmNodeKind::CreateRepo { agent } + | SwarmNodeKind::AddRepoMember { agent } + | SwarmNodeKind::InitAgentConfigRepo { agent } => { serde_json::json!({ "agent": agent }) } - SwarmNodeKind::CreateRepo { repo } => { - serde_json::json!({ "repo": repo }) - } - // Same rendering, and that is not a coincidence to be split - // apart later: both nodes act on one repo for one agent, and - // `label()` is what tells a viewer which of the two it is. - SwarmNodeKind::AddRepoMember { repo, agent } - | SwarmNodeKind::InitAgentConfigRepo { repo, agent } => { - serde_json::json!({ "repo": repo, "agent": agent }) - } } } } @@ -151,13 +149,13 @@ async fn run_swarm_node( Err(e) => Outcome::Failed(format!("{e:#}")), }, }, - SwarmNodeKind::CreateRepo { repo } => match deps.forge { + SwarmNodeKind::CreateRepo { agent } => match deps.forge { None => Outcome::Failed( "no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \ SWARM_CONTROLLER_FORGE_TOKEN_FILE unset)" .to_owned(), ), - Some(client) => match client.create_repo(&repo).await { + Some(client) => match client.create_repo(forge::agent_repo(&agent)).await { Ok(full_name) => { tracing::info!(%full_name, "swarm jobq: create_repo done"); Outcome::Done @@ -165,24 +163,30 @@ async fn run_swarm_node( Err(e) => Outcome::Failed(format!("{e:#}")), }, }, - SwarmNodeKind::AddRepoMember { repo, agent } => match deps.forge { + SwarmNodeKind::AddRepoMember { agent } => match deps.forge { None => Outcome::Failed( "no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \ SWARM_CONTROLLER_FORGE_TOKEN_FILE unset)" .to_owned(), ), - Some(client) => match client.add_repo_member(&repo, &agent).await { + Some(client) => match client + .add_repo_member(forge::agent_repo(&agent), &agent) + .await + { Ok(()) => Outcome::Done, Err(e) => Outcome::Failed(format!("{e:#}")), }, }, - SwarmNodeKind::InitAgentConfigRepo { repo, agent } => match deps.forge { + SwarmNodeKind::InitAgentConfigRepo { agent } => match deps.forge { None => Outcome::Failed( "no forge configured on this host (SWARM_CONTROLLER_FORGE_URL / \ SWARM_CONTROLLER_FORGE_TOKEN_FILE unset)" .to_owned(), ), - Some(client) => match client.seed_agent_config(&repo, &agent).await { + Some(client) => match client + .seed_agent_config(forge::agent_repo(&agent), &agent) + .await + { Ok(()) => Outcome::Done, Err(e) => Outcome::Failed(format!("{e:#}")), }, @@ -639,7 +643,6 @@ async fn create_agent( let agent = hive_types::Ident::parse(&req.name) .map_err(|reason| error_problem(axum::http::StatusCode::BAD_REQUEST, reason))? .into_string(); - let repo = agent.clone(); // Two checks, and the second is the one that makes the field worth // having: `Ident::parse` says the string is *shaped* like a hive name, @@ -678,19 +681,20 @@ async fn create_agent( agent: agent.clone(), }); let create_repo = b - .node(SwarmNodeKind::CreateRepo { repo: repo.clone() }) + .node(SwarmNodeKind::CreateRepo { + agent: agent.clone(), + }) .after_ok(create_identity); // Both fan out from `create_repo` directly — independent // operations on the same repo, no ordering requirement on // each other (see the doc comment above). let _add_repo_member = b .node(SwarmNodeKind::AddRepoMember { - repo: repo.clone(), agent: agent.clone(), }) .after_ok(create_repo); let _init_config = b - .node(SwarmNodeKind::InitAgentConfigRepo { repo, agent }) + .node(SwarmNodeKind::InitAgentConfigRepo { agent }) .after_ok(create_repo); vec![create_identity.guid()] }) @@ -1144,7 +1148,7 @@ mod tests { let id = sched .append( SwarmNodeKind::CreateRepo { - repo: "atlas".to_owned(), + agent: "atlas".to_owned(), }, Vec::new(), None,