Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5caec9c1a9 | ||
|
|
60da6c14b4 | ||
|
|
558552ec54 | ||
|
|
2fe79aaef5 |
2 changed files with 73 additions and 15 deletions
|
|
@ -51,12 +51,19 @@ 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. `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
|
||||
mirror, so operators can click straight from the SW4RM tab into
|
||||
config repo, 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
|
||||
|
|
|
|||
|
|
@ -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 the agent-config-PR flow 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. `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
|
||||
|
|
@ -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:
|
||||
/// 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, fast-forward-only (no auto force-push).
|
||||
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:
|
||||
/// - **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
|
||||
|
|
|
|||
Loading…
Reference in a new issue