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;
|
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
|
/// The `operators` team, whitelisted for the merge gate on every repo
|
||||||
/// this client protects — provisioned by `hive-c0re::forge::repos`
|
/// this client protects — provisioned by `hive-c0re::forge::repos`
|
||||||
/// already (`ensure_operators_team`), not re-provisioned here. If that
|
/// 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.
|
/// [`Client::create_repo`]'s doc comment.
|
||||||
const OPERATORS_TEAM: &str = "operators";
|
const OPERATORS_TEAM: &str = "operators";
|
||||||
|
|
||||||
/// The org owning per-agent config repos, and where the `pull_request` hook
|
/// The org owning per-agent config repos — where this daemon creates them,
|
||||||
/// lives. Same value as `hive-c0re::forge::CONFIG_ORG` — one forge, one org.
|
/// 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
|
/// ⚠️ Duplicated across the crate boundary (this crate deliberately does not
|
||||||
/// depend on `hive-c0re`), so nothing makes the two fail together. The
|
/// 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
|
/// 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.
|
/// 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";
|
const CONFIG_ORG: &str = "agent-configs";
|
||||||
|
|
||||||
/// The hive-wide knowledge repo, where the `push` hook lives. Same values as
|
/// 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.
|
/// (409/422) is folded into success.
|
||||||
async fn ensure_org_repo(&self, name: &str) -> Result<()> {
|
async fn ensure_org_repo(&self, name: &str) -> Result<()> {
|
||||||
match self
|
match self
|
||||||
.api
|
.api
|
||||||
.create_org_repo(AGENTS_ORG, Self::repo_option(name, true))
|
.create_org_repo(CONFIG_ORG, Self::repo_option(name, true))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::info!(%name, "swarm forge: created repo in {AGENTS_ORG}");
|
tracing::info!(%name, "swarm forge: created repo in {CONFIG_ORG}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) if is_already_exists(&e) => {
|
Err(e) if is_already_exists(&e) => {
|
||||||
tracing::debug!(%name, "swarm forge: repo already exists");
|
tracing::debug!(%name, "swarm forge: repo already exists");
|
||||||
Ok(())
|
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
|
/// Idempotent: re-adding an existing collaborator just updates its
|
||||||
/// permission (forgejo answers 204 either way; a 201 from older
|
/// permission (forgejo answers 204 either way; a 201 from older
|
||||||
/// server versions is tolerated defensively).
|
/// server versions is tolerated defensively).
|
||||||
|
|
@ -159,7 +161,7 @@ impl Client {
|
||||||
let res = self
|
let res = self
|
||||||
.api
|
.api
|
||||||
.repo_add_collaborator(
|
.repo_add_collaborator(
|
||||||
AGENTS_ORG,
|
CONFIG_ORG,
|
||||||
repo,
|
repo,
|
||||||
user,
|
user,
|
||||||
AddCollaboratorOption {
|
AddCollaboratorOption {
|
||||||
|
|
@ -172,7 +174,7 @@ impl Client {
|
||||||
Err(ForgejoError::UnexpectedStatusCode(s)) if s == StatusCode::CREATED => {}
|
Err(ForgejoError::UnexpectedStatusCode(s)) if s == StatusCode::CREATED => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return 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");
|
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
|
/// only treats it as success if the rule is actually present — a
|
||||||
/// fail-open merge gate is a security bug, not a shrug.
|
/// 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`
|
/// (provisioned by `hive-c0re::forge::repos::ensure_operators_team`
|
||||||
/// on its own startup sweep, not re-provisioned here) — if it
|
/// on its own startup sweep, not re-provisioned here) — if it
|
||||||
/// doesn't yet, this fails loudly rather than silently leaving the
|
/// doesn't yet, this fails loudly rather than silently leaving the
|
||||||
|
|
@ -226,7 +228,7 @@ impl Client {
|
||||||
};
|
};
|
||||||
let Err(create_err) = self
|
let Err(create_err) = self
|
||||||
.api
|
.api
|
||||||
.repo_create_branch_protection(AGENTS_ORG, repo, rule)
|
.repo_create_branch_protection(CONFIG_ORG, repo, rule)
|
||||||
.await
|
.await
|
||||||
else {
|
else {
|
||||||
tracing::info!(%repo, "swarm forge: applied operator branch protection");
|
tracing::info!(%repo, "swarm forge: applied operator branch protection");
|
||||||
|
|
@ -234,7 +236,7 @@ impl Client {
|
||||||
};
|
};
|
||||||
match self
|
match self
|
||||||
.api
|
.api
|
||||||
.repo_get_branch_protection(AGENTS_ORG, repo, "main")
|
.repo_get_branch_protection(CONFIG_ORG, repo, "main")
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|
@ -245,13 +247,13 @@ impl Client {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(check_err) => anyhow::bail!(
|
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_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.
|
/// its default branch — the whole job of the `CreateRepo` node.
|
||||||
/// Branch protection is folded in here rather than a separate node: it
|
/// Branch protection is folded in here rather than a separate node: it
|
||||||
/// has no independent retry value apart from the repo existing (there
|
/// 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> {
|
pub async fn create_repo(&self, repo: &str) -> Result<String> {
|
||||||
self.ensure_org_repo(repo).await?;
|
self.ensure_org_repo(repo).await?;
|
||||||
self.apply_operator_branch_protection(repo).await?;
|
self.apply_operator_branch_protection(repo).await?;
|
||||||
tracing::info!(%repo, "swarm forge: created repo in {AGENTS_ORG} with operator merge gate");
|
tracing::info!(%repo, "swarm forge: created repo in {CONFIG_ORG} with operator merge gate");
|
||||||
Ok(format!("{AGENTS_ORG}/{repo}"))
|
Ok(format!("{CONFIG_ORG}/{repo}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add `agent` as a **write** collaborator on `repo` (can push + open
|
/// Add `agent` as a **write** collaborator on `repo` (can push + open
|
||||||
|
|
@ -313,7 +315,7 @@ impl Client {
|
||||||
];
|
];
|
||||||
self.api
|
self.api
|
||||||
.repo_change_files(
|
.repo_change_files(
|
||||||
AGENTS_ORG,
|
CONFIG_ORG,
|
||||||
repo,
|
repo,
|
||||||
ChangeFilesOptions {
|
ChangeFilesOptions {
|
||||||
author: None,
|
author: None,
|
||||||
|
|
@ -328,7 +330,7 @@ impl Client {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.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");
|
tracing::info!(%repo, %agent, "swarm forge: seeded agent.nix + flake.nix");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -340,12 +342,12 @@ impl Client {
|
||||||
async fn file_exists(&self, repo: &str, path: &str) -> Result<bool> {
|
async fn file_exists(&self, repo: &str, path: &str) -> Result<bool> {
|
||||||
match self
|
match self
|
||||||
.api
|
.api
|
||||||
.repo_get_contents(AGENTS_ORG, repo, path, RepoGetContentsQuery::default())
|
.repo_get_contents(CONFIG_ORG, repo, path, RepoGetContentsQuery::default())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => Ok(true),
|
Ok(_) => Ok(true),
|
||||||
Err(ForgejoError::UnexpectedStatusCode(s)) if s == StatusCode::NOT_FOUND => Ok(false),
|
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 {
|
enum SwarmNodeKind {
|
||||||
/// Ensure `agent` exists as an authelia subject at the swarm level.
|
/// Ensure `agent` exists as an authelia subject at the swarm level.
|
||||||
CreateIdentity { agent: String },
|
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`.
|
/// merge gate on its default branch. See `forge::Client::create_repo`.
|
||||||
CreateRepo { agent: String },
|
CreateRepo { agent: String },
|
||||||
/// Add `agent` as a write collaborator on its own repo. See
|
/// 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
|
/// 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
|
/// same string as `name`: one repo per agent, named after it, same
|
||||||
/// convention `hive-c0re::forge` already uses for its own single-hive
|
/// convention `hive-c0re::forge` already uses for its own single-hive
|
||||||
/// `CreateRepo` path.
|
/// `CreateRepo` path.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue