diff --git a/docs/forge.md b/docs/forge.md index e247e747..4d4cbf62 100644 --- a/docs/forge.md +++ b/docs/forge.md @@ -56,10 +56,11 @@ Two things live in the `agent-configs` Forgejo organization: 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 + 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`.) - The dashboard links each container's "config" anchor to this config repo, so operators can click straight from the SW4RM tab into diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index e7acf4fa..e2bb8756 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -37,8 +37,9 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token"; /// 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-merge flow replaces it. +/// 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. 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 @@ -708,7 +709,7 @@ pub async fn ensure_config_repo(name: &str) -> Result<()> { // 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). + // Protect `main` core-only, fast-forward-only (no auto force-push). apply_config_repo_branch_protection(name, &token).await } @@ -997,20 +998,20 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()> /// (`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 agent-opened PR-merge flow -/// 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, flip this to `false`; keep `core` in the push-whitelist so -/// `ff_push_to_main` can still land fast-forwards. +/// - **`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":true}}"# + 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() {