diff --git a/docs/forge.md b/docs/forge.md index 4d4cbf62..527ca5ac 100644 --- a/docs/forge.md +++ b/docs/forge.md @@ -51,19 +51,12 @@ read it without touching c0re's host-side credential store. Two things live in the `agent-configs` Forgejo organization: -- A config repo per agent (`agent-configs/`). 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. `main` is - fast-forward-only — no auto force-push (the merge handler's ff push lands - fine; the legacy `push_config` mirror, which force-pushes to re-point status - tags and rewind on rollback, runs best-effort until the PR flow retires it). - Repos stay private, so an agent can't read another - agent's config. (Agents remain read-only collaborators on `core/meta`.) +- A mirror repo per agent (`agent-configs/`) — 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. - The dashboard links each container's "config" anchor to this - config repo, so operators can click straight from the SW4RM tab into + mirror, so operators can click straight from the SW4RM tab into the rendered repo without an extra `git` step. The `hive-forge` CLI (separate workspace crate, see diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index e2bb8756..e2d953c1 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -30,16 +30,12 @@ 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 config repo. Core is a site admin -/// and reads + writes every repo here. As of the agent-config-PR flow each -/// agent is a **write collaborator on its own** `agent-configs/` 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. `main` is fast-forward-only (no auto -/// force-push); the legacy `push_config` mirror runs best-effort until the -/// PR-merge flow retires it. +/// 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. 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 @@ -692,12 +688,9 @@ pub async fn push_meta(dir: &Path) -> Result<()> { } /// Ensure the `agent-configs/` repo exists so the first -/// `push_config` doesn't 404, and wire it as the agent-editable PR surface: -/// 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). +/// `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. pub async fn ensure_config_repo(name: &str) -> Result<()> { if !is_present().await { return Ok(()); @@ -705,12 +698,7 @@ 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?; - // 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, fast-forward-only (no auto force-push). - apply_config_repo_branch_protection(name, &token).await + ensure_org_repo(CONFIG_ORG, name, &token).await } /// Ensure the `internal/docs` repo exists. Called once at startup @@ -990,45 +978,6 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()> } } -/// Apply branch protection to an `agent-configs/` repo's `main` so it -/// can serve as the agent-editable, PR-merge config surface: -/// - **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 `false`** — `main` only ever advances by -/// fast-forward. The merge handler's `ff_push_to_main` is already a -/// non-force push, so it lands fine. The legacy `push_config` mirror DOES -/// force-push (it re-points status tags and rewinds `main` on a failed-build -/// rollback), so the protection now rejects those non-ff updates — that -/// mirror runs best-effort until the agent-opened PR-merge flow retires it. -/// (Auto force-push is intentionally not allowed: per operator directive a -/// silent force-push is a bug, not a feature.) -/// -/// 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":false}}"# - ); - 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