fix(#3529): create agent config repos in agent-configs, not agents
WIP — compiles per an earlier build, but the verifying build/test run was cut short by a graceful stop. Re-run gate.sh before pushing. The controller created every agent config repo, its collaborator entry, its branch protection and its seeded agent.nix/flake.nix in the agents org. Config repos live in agent-configs, which is where hive-c0re reconciles, merges and mirrors them — so a repo created in agents is invisible to all of those, and nothing errors, because both orgs exist and both accept a repo. Root cause was a doc comment asserting something false: AGENTS_ORG claimed to be the same org hive-c0re uses for its config-repo path. It is not — hive-c0re's agents org is the namespace repos an AGENT ASKS FOR land in, and its config repos use agent-configs. The whole flow inherited the wrong premise from that sentence. The merge gate survives the move: hive-c0re provisions the operators team in both orgs, with a comment recording that missing the agent-configs copy once left every config repo unprotected.
This commit is contained in:
parent
c5bea0b8c4
commit
36810ae598
2 changed files with 34 additions and 32 deletions
|
|
@ -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<String> {
|
||||
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<bool> {
|
||||
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}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue