feat(#1787): make agent-configs/<n> the agent-editable, PR-merge config surface
Wires the per-agent config repo as the editable PR surface the #1838 merge handler (run_merge_config_pr) consumes, without yet retiring the push_config force-mirror (that waits for #1838 P2 — the agent-opens-PR MCP surface — so config changes keep working through the transition). ensure_config_repo now, after creating agent-configs/<name>: - adds the agent as a WRITE collaborator (can push config-change branches + open PRs); - branch-protects main core-only via apply_config_repo_branch_protection: push + merge whitelists are core-only (only hive-c0re lands on main, via its verify-and-ff-push handler), operator-team approval required, the agent can't push main directly or self-merge. The protection sets enable_force_push=true as a TRANSITIONAL allowance so push_config's applied->main force-mirror keeps working until P2 retires it (a protected branch otherwise rejects force-push). Forgejo's force-push allowlist is deploy-keys-only — no per-user list — so this is a plain enable_force_push toggle; only core is in the push-whitelist so only core can force-push anyway. At P2, flip it to false and keep core in the push-whitelist so ff_push_to_main still lands. Agent code repos (AGENTS_ORG) are intentionally untouched — their merge flow has no auto-merge handler yet, so switching them to core-only-merge would strand those PRs. All steps idempotent (runs on every spawn + startup sweep). Updated the CONFIG_ORG doc comment + docs/forge.md to drop the "mirror-only" framing.
This commit is contained in:
parent
f7d84f4847
commit
2fe79aaef5
2 changed files with 71 additions and 14 deletions
|
|
@ -51,10 +51,16 @@ read it without touching c0re's host-side credential store.
|
|||
|
||||
Two things live in the `agent-configs` Forgejo organization:
|
||||
|
||||
- A mirror repo per agent (`agent-configs/<name>`) — c0re pushes the
|
||||
agent's applied config repo on each `↻ R3BU1LD`. Agents are
|
||||
read-only collaborators on `core/meta` (the hive-c0re-owned meta
|
||||
flake) so they can fetch but never push.
|
||||
- A config repo per agent (`agent-configs/<name>`). As of #1787 the
|
||||
agent is a **write collaborator on its own** repo — it can push
|
||||
config-change branches and (once #1838 P2 lands) open config PRs — but
|
||||
`main` is branch-protected core-only: only hive-c0re's verify-and-ff-push
|
||||
merge handler lands on `main`, an operator-team approval is required, and
|
||||
the agent can neither push `main` directly nor self-merge. During the
|
||||
transition c0re still force-mirrors the agent's applied config repo here
|
||||
on each `↻ R3BU1LD` (the `enable_force_push` allowance), until the PR
|
||||
flow replaces that. Repos stay private, so an agent can't read another
|
||||
agent's config. (Agents remain read-only collaborators on `core/meta`.)
|
||||
- The dashboard links each container's "config" anchor to this
|
||||
mirror, so operators can click straight from the SW4RM tab into
|
||||
the rendered repo without an extra `git` step.
|
||||
|
|
|
|||
|
|
@ -30,12 +30,16 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token";
|
|||
// helpers in `hive_sh4re::assets`. The `agent-configs.png` is
|
||||
// rendered from its SVG during the `hyperhive-assets` derivation's
|
||||
// build.
|
||||
/// Forgejo org grouping every agent's applied config repo. Core is a
|
||||
/// site admin and reads + writes every repo here; agents are NOT
|
||||
/// members and the repos are private, so no agent — not even the one
|
||||
/// a repo describes — can reach a config repo through the forge. The
|
||||
/// applied repos stay hive-c0re-owned on disk; this org is just a
|
||||
/// mirror target core pushes to.
|
||||
/// Forgejo org grouping every agent's config repo. Core is a site admin
|
||||
/// and reads + writes every repo here. As of #1787 each agent is a **write
|
||||
/// collaborator on its own** `agent-configs/<name>` repo — the editable
|
||||
/// PR surface it pushes config-change branches to — but `main` is
|
||||
/// branch-protected core-only, so only hive-c0re's verify-and-ff-push merge
|
||||
/// handler lands on it (operator approval required; the agent can't push
|
||||
/// `main` or self-merge). The repos remain private, so an agent still can't
|
||||
/// reach *another* agent's config. During the transition `push_config` also
|
||||
/// force-mirrors `applied → main` here until the PR flow (#1838 P2) replaces
|
||||
/// it.
|
||||
const CONFIG_ORG: &str = "agent-configs";
|
||||
/// Forgejo org hosting the operator-curated shared docs/skills repo
|
||||
/// that every agent gets read-only access to. Agents use it as a
|
||||
|
|
@ -688,9 +692,12 @@ pub async fn push_meta(dir: &Path) -> Result<()> {
|
|||
}
|
||||
|
||||
/// Ensure the `agent-configs/<name>` repo exists so the first
|
||||
/// `push_config` doesn't 404. No-op when the forge isn't running or
|
||||
/// the core token isn't minted yet. Safe to call on every spawn and
|
||||
/// on every startup.
|
||||
/// `push_config` doesn't 404, and wire it as the agent-editable PR surface
|
||||
/// (#1787): the agent is a **write** collaborator (can push feature branches +
|
||||
/// open config PRs) and `main` is branch-protected core-only (only hive-c0re's
|
||||
/// merge handler lands on it; operator approval required). No-op when the forge
|
||||
/// isn't running or the core token isn't minted yet. Safe to call on every
|
||||
/// spawn and on every startup (all steps idempotent).
|
||||
pub async fn ensure_config_repo(name: &str) -> Result<()> {
|
||||
if !is_present().await {
|
||||
return Ok(());
|
||||
|
|
@ -698,7 +705,12 @@ pub async fn ensure_config_repo(name: &str) -> Result<()> {
|
|||
let Some(token) = core_token() else {
|
||||
return Ok(());
|
||||
};
|
||||
ensure_org_repo(CONFIG_ORG, name, &token).await
|
||||
ensure_org_repo(CONFIG_ORG, name, &token).await?;
|
||||
// Agent = write collaborator: it can push config-PR branches + open PRs,
|
||||
// but the branch protection below keeps it off `main` directly.
|
||||
add_collaborator(CONFIG_ORG, name, name, "write", &token).await?;
|
||||
// Protect `main` core-only (+ transitional force-push for push_config).
|
||||
apply_config_repo_branch_protection(name, &token).await
|
||||
}
|
||||
|
||||
/// Ensure the `internal/docs` repo exists. Called once at startup
|
||||
|
|
@ -978,6 +990,45 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()>
|
|||
}
|
||||
}
|
||||
|
||||
/// Apply branch protection to an `agent-configs/<name>` repo's `main` so it
|
||||
/// can serve as the agent-editable, PR-merge config surface (#1787 / #1838):
|
||||
/// - **push + merge whitelists are `core`-only** — the agent (a write
|
||||
/// collaborator) can push feature branches and open config PRs, but only
|
||||
/// hive-c0re lands on `main`, via its verify-and-ff-push merge handler
|
||||
/// (`run_merge_config_pr`). The agent can never push `main` directly.
|
||||
/// - **operator-team approval is required** to merge, and the author (not in
|
||||
/// the team) cannot self-approve.
|
||||
/// - **`enable_force_push` is a TRANSITIONAL allowance**: `push_config` still
|
||||
/// force-mirrors `applied → main` until the PR flow (#1838 P2) replaces it,
|
||||
/// and a protected branch otherwise rejects force-push. Forgejo's force-push
|
||||
/// allowlist is deploy-keys-only (no per-user list), so this is a plain
|
||||
/// `enable_force_push` toggle — only `core` is in the push-whitelist, so
|
||||
/// only `core` can force-push anyway. When `push_config` is retired (#1838
|
||||
/// P2), flip this to `false`; keep `core` in the push-whitelist so
|
||||
/// `ff_push_to_main` can still land fast-forwards.
|
||||
///
|
||||
/// Idempotent: an existing rule for the branch (200/409/422) is success.
|
||||
async fn apply_config_repo_branch_protection(repo: &str, token: &str) -> Result<()> {
|
||||
let url = format!("{FORGE_HTTP}/api/v1/repos/{CONFIG_ORG}/{repo}/branch_protections");
|
||||
let body = format!(
|
||||
r#"{{"branch_name":"main","enable_push_whitelist":true,"push_whitelist_usernames":["core"],"enable_merge_whitelist":true,"merge_whitelist_usernames":["core"],"enable_approvals_whitelist":true,"approvals_whitelist_teams":["{OPERATORS_TEAM}"],"required_approvals":1,"block_on_official_review_requests":true,"allow_manual_merge":true,"enable_force_push":true}}"#
|
||||
);
|
||||
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||
match status.as_u16() {
|
||||
201 => {
|
||||
tracing::info!(%repo, "forge: applied config-repo branch protection");
|
||||
Ok(())
|
||||
}
|
||||
200 | 409 | 422 => {
|
||||
tracing::debug!(%repo, "forge: config-repo branch protection already present");
|
||||
Ok(())
|
||||
}
|
||||
other => {
|
||||
anyhow::bail!("POST {CONFIG_ORG}/{repo}/branch_protections returned HTTP {other}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a repo for `agent` in the c0re-owned [`AGENTS_ORG`] and wire the
|
||||
/// perms: the org owns it (perms stay c0re-managed), the agent is added
|
||||
/// as a **write** collaborator (not owner — can push + open PRs but can't
|
||||
|
|
|
|||
Loading…
Reference in a new issue