diff --git a/swarm-controller/src/forge.rs b/swarm-controller/src/forge.rs index 7b726dfe..d524ccae 100644 --- a/swarm-controller/src/forge.rs +++ b/swarm-controller/src/forge.rs @@ -28,16 +28,6 @@ use std::collections::BTreeMap; use crate::webhook::DeliveryKind; -/// The forge org that owns agent repos. Same org `hive-c0re::forge` -/// already uses for its own single-hive `CreateRepo` path — this is -/// the same forge instance, not a separate one, so the same org. -/// -/// There is no per-agent repo-vs-agent naming convention to maintain here: -/// agent names are unique and every agent's repo lives in this one org, so -/// the agent name IS the repo name at every call site (dropped the -/// `agent_repo` identity function this used to go through, per review). -pub const AGENTS_ORG: &str = "agents"; - /// 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 @@ -47,13 +37,25 @@ pub const AGENTS_ORG: &str = "agents"; /// [`Client::create_repo`]'s doc comment. const OPERATORS_TEAM: &str = "operators"; -/// The org owning per-agent config repos, and where the `pull_request` hook -/// lives. Same value as `hive-c0re::forge::CONFIG_ORG` — one forge, one org. +/// The org owning per-agent config repos — where this daemon creates them, +/// seeds them, and where the `pull_request` hook lives. Same value as +/// `hive-c0re::forge::CONFIG_ORG` — one forge, one org. /// /// ⚠️ Duplicated across the crate boundary (this crate deliberately does not /// depend on `hive-c0re`), so nothing makes the two fail together. The /// failure mode if they drift is quiet: the hook is created on an org that /// exists, forgejo reports it healthy, and it simply never fires. +/// +/// ⚠️ **Not `agents`.** That org exists and is a different thing: it is the +/// namespace repos an *agent asks for* land in, per +/// `hive-c0re::forge::AGENTS_ORG`. Agent **config** repos have always lived +/// here, which is where `hive-c0re` reconciles, merges and mirrors them — a +/// config repo created in `agents` is invisible to every one of those paths, +/// and nothing errors, because both orgs exist and both accept a repo. +/// +/// The merge gate survives the distinction: `hive-c0re` provisions the +/// `operators` team in **both** orgs precisely so branch protection can be +/// applied in either. const CONFIG_ORG: &str = "agent-configs"; /// The hive-wide knowledge repo, where the `push` hook lives. Same values as @@ -126,27 +128,27 @@ impl Client { } } - /// Create `name` inside [`AGENTS_ORG`]. Idempotent: an existing repo + /// Create `name` inside [`CONFIG_ORG`]. Idempotent: an existing repo /// (409/422) is folded into success. async fn ensure_org_repo(&self, name: &str) -> Result<()> { match self .api - .create_org_repo(AGENTS_ORG, Self::repo_option(name, true)) + .create_org_repo(CONFIG_ORG, Self::repo_option(name, true)) .await { Ok(_) => { - tracing::info!(%name, "swarm forge: created repo in {AGENTS_ORG}"); + tracing::info!(%name, "swarm forge: created repo in {CONFIG_ORG}"); Ok(()) } Err(e) if is_already_exists(&e) => { tracing::debug!(%name, "swarm forge: repo already exists"); Ok(()) } - Err(e) => Err(e).with_context(|| format!("create repo {AGENTS_ORG}/{name}")), + Err(e) => Err(e).with_context(|| format!("create repo {CONFIG_ORG}/{name}")), } } - /// Add `user` as a collaborator on `AGENTS_ORG/repo` at `permission`. + /// Add `user` as a collaborator on `CONFIG_ORG/repo` at `permission`. /// Idempotent: re-adding an existing collaborator just updates its /// permission (forgejo answers 204 either way; a 201 from older /// server versions is tolerated defensively). @@ -159,7 +161,7 @@ impl Client { let res = self .api .repo_add_collaborator( - AGENTS_ORG, + CONFIG_ORG, repo, user, AddCollaboratorOption { @@ -172,7 +174,7 @@ impl Client { Err(ForgejoError::UnexpectedStatusCode(s)) if s == StatusCode::CREATED => {} Err(e) => { return Err(e) - .with_context(|| format!("add collaborator {user} to {AGENTS_ORG}/{repo}")); + .with_context(|| format!("add collaborator {user} to {CONFIG_ORG}/{repo}")); } } tracing::debug!(%repo, %user, ?permission, "swarm forge: collaborator set"); @@ -190,7 +192,7 @@ impl Client { /// only treats it as success if the rule is actually present — a /// fail-open merge gate is a security bug, not a shrug. /// - /// Assumes [`OPERATORS_TEAM`] already exists in [`AGENTS_ORG`] + /// Assumes [`OPERATORS_TEAM`] already exists in [`CONFIG_ORG`] /// (provisioned by `hive-c0re::forge::repos::ensure_operators_team` /// on its own startup sweep, not re-provisioned here) — if it /// doesn't yet, this fails loudly rather than silently leaving the @@ -226,7 +228,7 @@ impl Client { }; let Err(create_err) = self .api - .repo_create_branch_protection(AGENTS_ORG, repo, rule) + .repo_create_branch_protection(CONFIG_ORG, repo, rule) .await else { tracing::info!(%repo, "swarm forge: applied operator branch protection"); @@ -234,7 +236,7 @@ impl Client { }; match self .api - .repo_get_branch_protection(AGENTS_ORG, repo, "main") + .repo_get_branch_protection(CONFIG_ORG, repo, "main") .await { Ok(_) => { @@ -245,13 +247,13 @@ impl Client { Ok(()) } Err(check_err) => anyhow::bail!( - "branch protection for {AGENTS_ORG}/{repo} not applied: create failed \ + "branch protection for {CONFIG_ORG}/{repo} not applied: create failed \ ({create_err}); GET main rule failed ({check_err}), no `main` rule present" ), } } - /// Create `repo` in [`AGENTS_ORG`] and apply the operator merge gate to + /// Create `repo` in [`CONFIG_ORG`] and apply the operator merge gate to /// its default branch — the whole job of the `CreateRepo` node. /// Branch protection is folded in here rather than a separate node: it /// has no independent retry value apart from the repo existing (there @@ -262,8 +264,8 @@ impl Client { pub async fn create_repo(&self, repo: &str) -> Result { self.ensure_org_repo(repo).await?; self.apply_operator_branch_protection(repo).await?; - tracing::info!(%repo, "swarm forge: created repo in {AGENTS_ORG} with operator merge gate"); - Ok(format!("{AGENTS_ORG}/{repo}")) + tracing::info!(%repo, "swarm forge: created repo in {CONFIG_ORG} with operator merge gate"); + Ok(format!("{CONFIG_ORG}/{repo}")) } /// Add `agent` as a **write** collaborator on `repo` (can push + open @@ -313,7 +315,7 @@ impl Client { ]; self.api .repo_change_files( - AGENTS_ORG, + CONFIG_ORG, repo, ChangeFilesOptions { author: None, @@ -328,7 +330,7 @@ impl Client { }, ) .await - .with_context(|| format!("seed config files in {AGENTS_ORG}/{repo}"))?; + .with_context(|| format!("seed config files in {CONFIG_ORG}/{repo}"))?; tracing::info!(%repo, %agent, "swarm forge: seeded agent.nix + flake.nix"); Ok(()) } @@ -340,12 +342,12 @@ impl Client { async fn file_exists(&self, repo: &str, path: &str) -> Result { match self .api - .repo_get_contents(AGENTS_ORG, repo, path, RepoGetContentsQuery::default()) + .repo_get_contents(CONFIG_ORG, repo, path, RepoGetContentsQuery::default()) .await { Ok(_) => Ok(true), Err(ForgejoError::UnexpectedStatusCode(s)) if s == StatusCode::NOT_FOUND => Ok(false), - Err(e) => Err(e).with_context(|| format!("check for {path} in {AGENTS_ORG}/{repo}")), + Err(e) => Err(e).with_context(|| format!("check for {path} in {CONFIG_ORG}/{repo}")), } } diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index b3a84a69..703faf65 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -56,7 +56,7 @@ mod webhook; enum SwarmNodeKind { /// Ensure `agent` exists as an authelia subject at the swarm level. CreateIdentity { agent: String }, - /// Create the agent's repo in `forge::AGENTS_ORG` with the operator + /// Create the agent's repo in `forge::CONFIG_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 @@ -555,7 +555,7 @@ async fn get_hives_status( } /// Body of `POST /api/agents` — the agent name to create, and the hive the -/// creation is aimed at. The repo name inside `forge::AGENTS_ORG` is the +/// creation is aimed at. The repo name inside `forge::CONFIG_ORG` is the /// same string as `name`: one repo per agent, named after it, same /// convention `hive-c0re::forge` already uses for its own single-hive /// `CreateRepo` path.