From 867be7bb98e121e02bbf867cfbb7c3a950897754 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 19 Jun 2026 12:45:43 +0200 Subject: [PATCH 1/3] hive-c0re: block direct agent repo creation, add c0re-mediated create (#1787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents must no longer create repos with their own forge token (a write-scoped token otherwise creates + owns repos and can self-merge, bypassing operator-only-merge). Instead: - Set max_repo_creation=0 on every agent forge user (marker-guarded PATCH in sync_agent; covers agents provisioned before this). Blocks new direct creation; push/PR/clone and existing repos are untouched. - Provision a c0re-owned 'agents' org (the namespace agent repos land in) plus an empty 'operators' team inside it. The org owns the repos so perms stay c0re-managed; the operator joins the team out-of-band. - create_agent_repo() is the sanctioned path: creates the repo in the agents org, adds the requesting agent as a *write* collaborator (not owner), and applies branch protection that whitelists merge + required approval to the operators team — so the author can't merge its own PR. - is_hive_managed_namespace() guards 'internal'/'agent-configs'/'core' against a future create surface passing an explicit owner. No existing repos are modified. The agent/hivectl surface that invokes create_agent_repo is a follow-up commit. --- hive-c0re/src/forge.rs | 183 ++++++++++++++++++++++++++++++++++++++++- hive-c0re/src/paths.rs | 10 +++ 2 files changed, 192 insertions(+), 1 deletion(-) diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 9449700d..4a6fba4d 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -51,9 +51,30 @@ const SHARED_DOCS_REPO: &str = "docs"; /// Bind-mounted read-only into every container at `/knowledge`. /// See `hive-c0re/src/knowledge.rs`. const KNOWLEDGE_REPO: &str = crate::knowledge::REPO; +/// Forgejo org that owns agent-created repos (#1787). Agents can't create +/// repos with their own token (`max_repo_creation = 0`); instead hive-c0re +/// creates them here and adds the requesting agent as a **write** member +/// (not owner/admin). Because the org — not the agent — owns the repo, +/// perms stay c0re-managed and branch protection (referencing +/// [`OPERATORS_TEAM`]) can block the author from merging their own PR. This +/// is the "agents namespace" repos land in by default. +const AGENTS_ORG: &str = "agents"; +/// Operator merge-gate team inside [`AGENTS_ORG`]. Provisioned **empty** by +/// hive-c0re (so perms can be set before anyone joins); the operator adds +/// herself via the forge UI / hivectl. Branch protection on agents-org repos +/// references this team by name for the merge/approval whitelist, so the +/// rule never hardcodes a specific reviewer agent (which may not exist). +const OPERATORS_TEAM: &str = "operators"; +/// Hive-managed Forgejo namespaces that agent-initiated repo creation must +/// never target (#1787). `internal` is operator-curated shared content; +/// `agent-configs` + `core` are hive-c0re-internal mirror/meta namespaces. +/// (`hyperhive` is NOT managed — it's just a repo that happens to be built +/// by this hive.) hive-c0re's create path forces [`AGENTS_ORG`], so this is +/// a defensive guard against any future caller passing an explicit owner. +const HIVE_MANAGED_NAMESPACES: &[&str] = &[SHARED_ORG, CONFIG_ORG, "core"]; /// Forgejo orgs hive-c0re ensures on startup. The meta repo lives at /// `core/meta` (the `core` user's own namespace — no org needed). -const SEEDED_ORGS: &[&str] = &[CONFIG_ORG, SHARED_ORG]; +const SEEDED_ORGS: &[&str] = &[CONFIG_ORG, SHARED_ORG, AGENTS_ORG]; /// Per-agent token scopes (broad-but-not-admin). See /// `docs/forge.md::Token scopes` for the per-scope rationale. const TOKEN_SCOPES: &str = "read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc"; @@ -249,6 +270,57 @@ async fn ensure_user_email(name: &str) { } } +/// Disable direct repo creation for agent `name` by setting +/// `max_repo_creation = 0` on its Forgejo account (#1787). Agents must +/// create repos *through hive-c0re* (which owns the perms), never with +/// their own token — a write-scoped token can otherwise create + own +/// repos and self-merge, bypassing the operator-only-merge policy. +/// +/// `max_repo_creation = 0` means `CanCreateRepo()` is false for any +/// count (Forgejo: `MaxRepoCreation >= 0 && NumRepos >= MaxRepoCreation`), +/// so creation is refused while push / PR / clone stay intact. **Existing +/// repos are untouched** — this only blocks *new* direct creation. +/// +/// Marker-guarded like [`ensure_user_email`]: the PATCH runs once per +/// agent (delete the marker to re-apply). Body carries `login_name` + +/// `source_id` for the same reason `ensure_user_email` does — omitting +/// `login_name` makes Forgejo's `EditUserOption` validator reset +/// `use_custom_avatar`. Best-effort: failures warn, don't propagate. +async fn ensure_repo_creation_disabled(name: &str) { + let marker = crate::paths::forge_repo_creation_disabled_marker(name); + if marker.exists() { + return; + } + let Some(token) = core_token() else { + tracing::debug!(%name, "forge: skipping ensure_repo_creation_disabled — no core token yet"); + return; + }; + let body = format!(r#"{{"login_name":"{name}","source_id":0,"max_repo_creation":0}}"#); + let url = format!("{FORGE_HTTP}/api/v1/admin/users/{name}"); + match forge_http(reqwest::Method::PATCH, &url, &token, &body).await { + Ok(status) if status.is_success() => { + if let Some(parent) = marker.parent() { + std::fs::create_dir_all(parent).ok(); + } + std::fs::write(&marker, "").ok(); + tracing::info!(%name, "forge: disabled direct repo creation (max_repo_creation=0)"); + } + Ok(status) if status == reqwest::StatusCode::FORBIDDEN => { + tracing::warn!( + %name, %status, + "forge: PATCH max_repo_creation forbidden — core token likely missing admin scope. \ + Delete {CORE_TOKEN_PATH} and restart hive-c0re to re-mint with the new scopes." + ); + } + Ok(status) => { + tracing::warn!(%name, %status, "forge: PATCH max_repo_creation returned non-success"); + } + Err(e) => { + tracing::warn!(%name, error = %e, "forge: PATCH max_repo_creation transport error") + } + } +} + /// Mint a fresh access token for `name`. Token name is suffixed with /// a monotonic clock so re-issuing doesn't collide with an existing /// token of the same name in the DB. `scopes` is the scope string @@ -821,6 +893,105 @@ async fn ensure_org(name: &str, admin_token: &str) -> Result<()> { } } +/// Whether `ns` is a hive-managed Forgejo namespace that agent-initiated +/// repo creation must never target (#1787) — `internal` (operator-curated +/// shared content) + `agent-configs` / `core` (hive-c0re-internal). The +/// create path forces [`AGENTS_ORG`], so this guards a future surface that +/// might accept an explicit owner. +#[must_use] +pub fn is_hive_managed_namespace(ns: &str) -> bool { + HIVE_MANAGED_NAMESPACES.contains(&ns) +} + +/// Provision the [`OPERATORS_TEAM`] inside [`AGENTS_ORG`] as an **empty** +/// team (#1787). Branch protection on agents-org repos references it as the +/// merge/approval whitelist; the operator adds herself as a member via the +/// forge UI / hivectl. `includes_all_repositories` so the gate applies to +/// every agent repo; `write` is enough to approve + merge. hive-c0re never +/// manages membership. Idempotent (422/409 = already exists). +async fn ensure_operators_team(token: &str) -> Result<()> { + let url = format!("{FORGE_HTTP}/api/v1/orgs/{AGENTS_ORG}/teams"); + let body = format!( + r#"{{"name":"{OPERATORS_TEAM}","description":"hyperhive operators — merge gate for agent repos","permission":"write","includes_all_repositories":true,"can_create_org_repo":false}}"# + ); + let status = forge_http(reqwest::Method::POST, &url, token, &body).await?; + match status.as_u16() { + 201 => { + tracing::info!("forge: created {OPERATORS_TEAM} team in {AGENTS_ORG}"); + Ok(()) + } + 409 | 422 => { + tracing::debug!("forge: {OPERATORS_TEAM} team already exists"); + Ok(()) + } + other => { + anyhow::bail!("POST /orgs/{AGENTS_ORG}/teams ({OPERATORS_TEAM}) returned HTTP {other}") + } + } +} + +/// Add `user` as a collaborator on `owner/repo` at `permission` +/// (`read` / `write` / `admin`). Idempotent: 201 (added) and 204 (already a +/// collaborator / permission updated) both count as success. +async fn add_collaborator( + owner: &str, + repo: &str, + user: &str, + permission: &str, + token: &str, +) -> Result<()> { + let url = format!("{FORGE_HTTP}/api/v1/repos/{owner}/{repo}/collaborators/{user}"); + let body = format!(r#"{{"permission":"{permission}"}}"#); + let status = forge_http(reqwest::Method::PUT, &url, token, &body).await?; + match status.as_u16() { + 201 | 204 => { + tracing::debug!(%owner, %repo, %user, %permission, "forge: collaborator set"); + Ok(()) + } + other => { + anyhow::bail!("PUT {owner}/{repo}/collaborators/{user} returned HTTP {other}") + } + } +} + +/// Apply the operator merge-gate branch protection to `repo`'s default +/// branch (#1787): only [`OPERATORS_TEAM`] members can merge, and an +/// approving review from that team is required — so the author (a write-level +/// agent, not in the team) cannot merge its own PR. Idempotent: an existing +/// rule for the branch (200/409/422) is treated as success. +async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()> { + let url = format!("{FORGE_HTTP}/api/v1/repos/{AGENTS_ORG}/{repo}/branch_protections"); + let body = format!( + r#"{{"branch_name":"main","enable_merge_whitelist":true,"merge_whitelist_teams":["{OPERATORS_TEAM}"],"enable_approvals_whitelist":true,"approvals_whitelist_teams":["{OPERATORS_TEAM}"],"required_approvals":1,"block_on_official_review_requests":true}}"# + ); + let status = forge_http(reqwest::Method::POST, &url, token, &body).await?; + match status.as_u16() { + 201 => { + tracing::info!(%repo, "forge: applied operator branch protection"); + Ok(()) + } + 200 | 409 | 422 => { + tracing::debug!(%repo, "forge: branch protection already present"); + Ok(()) + } + other => anyhow::bail!("POST {AGENTS_ORG}/{repo}/branch_protections returned HTTP {other}"), + } +} + +/// Create a repo for `agent` in the c0re-owned [`AGENTS_ORG`] and wire the +/// #1787 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 +/// bypass branch protection), and the default branch gets the operator +/// merge gate. This is the sanctioned create path now that agents can't +/// create repos directly (`max_repo_creation = 0`). Idempotent. +pub async fn create_agent_repo(agent: &str, repo: &str, core_token: &str) -> Result<()> { + ensure_org_repo(AGENTS_ORG, repo, core_token).await?; + add_collaborator(AGENTS_ORG, repo, agent, "write", core_token).await?; + apply_operator_branch_protection(repo, core_token).await?; + tracing::info!(%agent, %repo, "forge: created agent repo in {AGENTS_ORG} with operator merge gate"); + Ok(()) +} + /// Per-agent forge sync: ensure the agent has a forgejo user + token, /// a mirrored config repo, read access to `core/meta`, and the `meta` /// remote in its proposed repo. All operations are idempotent; failures @@ -840,6 +1011,10 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) { // so commits link to the agent's Forgejo profile. Best-effort; // also patches up agents created before this fix (old @hive.local). ensure_user_email(name).await; + // Block direct agent-initiated repo creation (#1787): agents create + // repos through hive-c0re, never with their own token. Idempotent + + // marker-guarded; also covers agents provisioned before this landed. + ensure_repo_creation_disabled(name).await; // Mirror the agent's applied config repo into agent-configs. // ensure_config_repo is idempotent; push_config catches any // drift since the last run — e.g. the startup migration just @@ -896,6 +1071,12 @@ pub async fn ensure_all() { tracing::warn!(%org, error = ?e, "forge: ensure_org failed"); } } + // Provision the operator merge-gate team (empty) inside the agents + // org so branch protection can reference it before anyone joins + // (#1787). The operator adds herself as a member out-of-band. + if let Err(e) = ensure_operators_team(token).await { + tracing::warn!(error = ?e, "forge: ensure_operators_team failed"); + } // Meta repo lives at core/meta — pushed from git_commit in // meta.rs on every deploy/lock-update. Make sure it exists // before the first push hits a 404. diff --git a/hive-c0re/src/paths.rs b/hive-c0re/src/paths.rs index 3ffcf974..72887b7f 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -60,6 +60,16 @@ pub fn forge_email_aligned_marker(name: &str) -> PathBuf { forge_dir().join(format!("email-aligned-{name}")) } +/// `forge/repo-creation-disabled-` — marker: ``'s forge user +/// has had `max_repo_creation = 0` applied (blocks direct agent-initiated +/// repo creation — see #1787). One-shot guard so the PATCH runs once per +/// agent (including agents provisioned before the change); delete to +/// re-apply. +#[must_use] +pub fn forge_repo_creation_disabled_marker(name: &str) -> PathBuf { + forge_dir().join(format!("repo-creation-disabled-{name}")) +} + /// `matrix/` — host-side matrix provisioning state (admin token, hive /// Space room id, per-agent password creds). The shared registration /// token is bind-mounted into the tuwunel container via nix and stays From f1d54ce12c181f19680968e05d9c1d11e4834073 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 19 Jun 2026 12:57:01 +0200 Subject: [PATCH 2/3] agent surface: create_repo through hive-c0re (#1787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the #1787 loop — the sanctioned create path now that agents can't create repos directly. Adds: - wire: Request::CreateRepo{repo} + Response::RepoCreated{full_name, clone_url} (hive-sh4re). - agent_server: dispatch_shared arm + handle_create_repo — validates the repo name, then forge::create_agent_repo (org-owned repo, agent=write collaborator, operator-team branch protection). Returns the full name + clone url so the agent can git clone immediately. - MCP: create_repo tool + CreateRepoArgs in the harness. - a new opt-in ToolGroup::Forge (=[create_repo]) so the operator controls which agents can spin up repos (least privilege). Workspace clippy -D warnings, cargo test, nix fmt all green. --- hive-ag3nt/src/mcp.rs | 48 +++++++++++++++++++++++++++++++++++ hive-c0re/src/agent_server.rs | 41 ++++++++++++++++++++++++++++++ hive-c0re/src/forge.rs | 6 ++--- hive-sh4re/src/lib.rs | 23 +++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index e4ddebc0..6bbdeb60 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -59,6 +59,11 @@ pub enum SocketReply { hive_name: Option, swarm_name: Option, }, + /// `create_repo` result — the new repo's full name + clone URL. + RepoCreated { + full_name: String, + clone_url: String, + }, } impl From for SocketReply { @@ -100,6 +105,13 @@ impl From for SocketReply { hive_name, swarm_name, }, + hive_sh4re::Response::RepoCreated { + full_name, + clone_url, + } => Self::RepoCreated { + full_name, + clone_url, + }, } } } @@ -910,6 +922,35 @@ impl AgentServer { .await } + #[tool( + description = "Create a git repo through hive-c0re. You CANNOT create repos with your \ + own forge token (creation is disabled) — this is the only path. The repo is created in \ + the c0re-owned `agents` org, you're added as a write collaborator (not owner), and the \ + default branch gets branch protection so merges require an operator-team approval — you \ + cannot merge your own PRs. `repo` is a single name segment (letters, digits, `-`, `_`, \ + `.`). Returns the new repo's full name + clone URL; clone it over \ + `http://localhost:3000/agents/.git` and push/open PRs as normal." + )] + async fn create_repo(&self, Parameters(args): Parameters) -> String { + let log = format!("{args:?}"); + run_tool_envelope("create_repo", log, async move { + let (resp, retries) = self + .dispatch(hive_sh4re::Request::CreateRepo { repo: args.repo }) + .await; + let s = match resp { + Ok(SocketReply::RepoCreated { + full_name, + clone_url, + }) => format!("created repo {full_name} — clone: {clone_url}"), + Ok(SocketReply::Err(m)) => format!("create_repo failed: {m}"), + Ok(other) => format!("create_repo unexpected response: {other:?}"), + Err(e) => format!("create_repo transport error: {e:#}"), + }; + annotate_retries(s, retries) + }) + .await + } + #[tool( description = "Schedule a reminder that lands in this agent's own inbox at a future \ time (sender will appear as `reminder`). Use for self-paced follow-ups: 'check task \ @@ -1505,6 +1546,13 @@ pub struct SetStatusArgs { pub text: String, } +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct CreateRepoArgs { + /// Repo name — a single segment of letters, digits, `-`, `_`, `.` + /// (no leading `-`/`.`). The repo is created as `agents/`. + pub repo: String, +} + #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct GetAgentMetaArgs { /// Logical name of the agent to query (e.g. `"iris"`, `"manager"`). diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 4b61bedb..0032911b 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -183,6 +183,7 @@ pub(crate) async fn dispatch_shared( |()| hive_sh4re::Response::Ok, ) } + hive_sh4re::Request::CreateRepo { repo } => handle_create_repo(agent, repo).await, hive_sh4re::Request::AckTurn => handle_ack_turn(coord, agent), hive_sh4re::Request::RequeueInflight => handle_requeue_inflight(coord, agent), hive_sh4re::Request::GracefulStopComplete => { @@ -304,6 +305,46 @@ fn handle_set_status(coord: &Arc, text: &str) -> hive_sh4re::Respon hive_sh4re::Response::Ok } +/// Validate an agent-supplied repo name: a single safe slug segment, no +/// path traversal. Forgejo validates server-side too, but rejecting early +/// gives a clear message and avoids building odd API paths. +fn valid_repo_name(name: &str) -> bool { + !name.is_empty() + && name.len() <= 100 + && !name.starts_with(['-', '.']) + && name + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')) +} + +/// `CreateRepo` — create a repo for `agent` *through hive-c0re* in the +/// c0re-owned `agents` org with operator-team branch protection (#1787). +/// The sanctioned create path now that agents can't create repos directly. +async fn handle_create_repo(agent: &str, repo: &str) -> hive_sh4re::Response { + if !valid_repo_name(repo) { + return hive_sh4re::Response::Err { + message: format!( + "invalid repo name {repo:?} — single segment of letters, digits, '-', '_', '.' \ + (no leading '-'/'.', max 100 chars)" + ), + }; + } + let Some(core_token) = crate::forge::core_token() else { + return hive_sh4re::Response::Err { + message: "forge unavailable (no core token) — cannot create repo".to_owned(), + }; + }; + match crate::forge::create_agent_repo(agent, repo, &core_token).await { + Ok(full_name) => hive_sh4re::Response::RepoCreated { + clone_url: format!("{}/{full_name}.git", crate::forge::FORGE_HTTP), + full_name, + }, + Err(e) => hive_sh4re::Response::Err { + message: format!("create repo {repo:?} failed: {e:#}"), + }, + } +} + /// `GetAgentMeta` — identity + live status for `name` (defaults to the /// caller). Reads the live container-view status and the hive/swarm /// display names. diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 4a6fba4d..e44e8dc6 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -316,7 +316,7 @@ async fn ensure_repo_creation_disabled(name: &str) { tracing::warn!(%name, %status, "forge: PATCH max_repo_creation returned non-success"); } Err(e) => { - tracing::warn!(%name, error = %e, "forge: PATCH max_repo_creation transport error") + tracing::warn!(%name, error = %e, "forge: PATCH max_repo_creation transport error"); } } } @@ -984,12 +984,12 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()> /// bypass branch protection), and the default branch gets the operator /// merge gate. This is the sanctioned create path now that agents can't /// create repos directly (`max_repo_creation = 0`). Idempotent. -pub async fn create_agent_repo(agent: &str, repo: &str, core_token: &str) -> Result<()> { +pub async fn create_agent_repo(agent: &str, repo: &str, core_token: &str) -> Result { ensure_org_repo(AGENTS_ORG, repo, core_token).await?; add_collaborator(AGENTS_ORG, repo, agent, "write", core_token).await?; apply_operator_branch_protection(repo, core_token).await?; tracing::info!(%agent, %repo, "forge: created agent repo in {AGENTS_ORG} with operator merge gate"); - Ok(()) + Ok(format!("{AGENTS_ORG}/{repo}")) } /// Per-agent forge sync: ensure the agent has a forgejo user + token, diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 8cdb9f47..bc4a3623 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -572,6 +572,13 @@ pub enum Request { /// per-kind semantics in /// `docs/conventions.md::Loose-ends wire shape`. CancelLooseEnd { kind: CancelLooseEndKind, id: i64 }, + /// Create a git repo *through hive-c0re* (#1787). Agents can't create + /// repos with their own forge token (`max_repo_creation = 0`); this is + /// the sanctioned path. hive-c0re creates `repo` in the c0re-owned + /// `agents` org, adds the calling agent as a write collaborator (not + /// owner), and applies operator-team branch protection so the author + /// can't merge its own PRs. Returns the new repo's full name. + CreateRepo { repo: String }, /// Mark every message popped since the last `AckTurn` as handled. /// Harness↔broker pairing fired after `TurnOutcome::Ok`. See /// `docs/conventions.md::Broker delivery + ack cycle`. @@ -753,6 +760,12 @@ pub enum Response { /// `ListSchedules` result. Snapshot of every schedule. /// Returned on the manager socket only. Schedules { schedules: Vec }, + /// `CreateRepo` result: the new repo's full name (`agents/`) + /// and clone URL, so the agent can immediately `git clone` it. + RepoCreated { + full_name: String, + clone_url: String, + }, /// `ListDescendants` result: all descendant containers, with running /// status. Ordered by topology depth (parents before children), then /// alphabetically within each depth tier. @@ -952,6 +965,10 @@ pub enum ToolGroup { Scheduling, /// `get_logs` - *(privileged)* Diagnostics, + /// `create_repo` — create git repos through hive-c0re (the only path + /// now that agents can't create them directly; see #1787). Opt-in per + /// agent so the operator controls who can spin up repos. + Forge, /// `run`, `status` (via `mcp__bash__*`) Execution, /// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and @@ -991,6 +1008,7 @@ impl ToolGroup { "list_schedules", ], Self::Diagnostics => &["get_logs"], + Self::Forge => &["create_repo"], Self::Execution => &["run", "status"], Self::WebTools => &[], } @@ -1044,6 +1062,7 @@ impl ToolGroup { Self::Approvals, Self::Scheduling, Self::Diagnostics, + Self::Forge, Self::Execution, Self::WebTools, ]; @@ -1060,6 +1079,7 @@ impl ToolGroup { Self::Approvals => "approvals", Self::Scheduling => "scheduling", Self::Diagnostics => "diagnostics", + Self::Forge => "forge", Self::Execution => "execution", Self::WebTools => "web_tools", } @@ -1088,6 +1108,9 @@ impl ToolGroup { Self::Diagnostics => { "get_logs — read a sub-agent container's systemd journal (privileged)" } + Self::Forge => { + "create_repo — create git repos through hive-c0re (operator-gated merge)" + } Self::Execution => { "run, status — run shell commands via mcp__bash__run / mcp__bash__status" } From 27ac0153c4015793adef8e42f0e69f79a964f8f4 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 19 Jun 2026 13:15:43 +0200 Subject: [PATCH 3/3] forge authz: scrub tracker tags from comments Replace the #-number tracker references in code comments with prose (tracker-tag lint; hive convention is prose in source). No behaviour change. Branch-protection / collaborator / team / repo API field names were verified against the live Forgejo swagger. --- hive-c0re/src/agent_server.rs | 2 +- hive-c0re/src/forge.rs | 18 +++++++++--------- hive-c0re/src/paths.rs | 2 +- hive-sh4re/src/lib.rs | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 0032911b..66002c6d 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -318,7 +318,7 @@ fn valid_repo_name(name: &str) -> bool { } /// `CreateRepo` — create a repo for `agent` *through hive-c0re* in the -/// c0re-owned `agents` org with operator-team branch protection (#1787). +/// c0re-owned `agents` org with operator-team branch protection. /// The sanctioned create path now that agents can't create repos directly. async fn handle_create_repo(agent: &str, repo: &str) -> hive_sh4re::Response { if !valid_repo_name(repo) { diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index e44e8dc6..d0f4f8eb 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -51,7 +51,7 @@ const SHARED_DOCS_REPO: &str = "docs"; /// Bind-mounted read-only into every container at `/knowledge`. /// See `hive-c0re/src/knowledge.rs`. const KNOWLEDGE_REPO: &str = crate::knowledge::REPO; -/// Forgejo org that owns agent-created repos (#1787). Agents can't create +/// Forgejo org that owns agent-created repos. Agents can't create /// repos with their own token (`max_repo_creation = 0`); instead hive-c0re /// creates them here and adds the requesting agent as a **write** member /// (not owner/admin). Because the org — not the agent — owns the repo, @@ -66,7 +66,7 @@ const AGENTS_ORG: &str = "agents"; /// rule never hardcodes a specific reviewer agent (which may not exist). const OPERATORS_TEAM: &str = "operators"; /// Hive-managed Forgejo namespaces that agent-initiated repo creation must -/// never target (#1787). `internal` is operator-curated shared content; +/// never target. `internal` is operator-curated shared content; /// `agent-configs` + `core` are hive-c0re-internal mirror/meta namespaces. /// (`hyperhive` is NOT managed — it's just a repo that happens to be built /// by this hive.) hive-c0re's create path forces [`AGENTS_ORG`], so this is @@ -271,7 +271,7 @@ async fn ensure_user_email(name: &str) { } /// Disable direct repo creation for agent `name` by setting -/// `max_repo_creation = 0` on its Forgejo account (#1787). Agents must +/// `max_repo_creation = 0` on its Forgejo account. Agents must /// create repos *through hive-c0re* (which owns the perms), never with /// their own token — a write-scoped token can otherwise create + own /// repos and self-merge, bypassing the operator-only-merge policy. @@ -894,7 +894,7 @@ async fn ensure_org(name: &str, admin_token: &str) -> Result<()> { } /// Whether `ns` is a hive-managed Forgejo namespace that agent-initiated -/// repo creation must never target (#1787) — `internal` (operator-curated +/// repo creation must never target — `internal` (operator-curated /// shared content) + `agent-configs` / `core` (hive-c0re-internal). The /// create path forces [`AGENTS_ORG`], so this guards a future surface that /// might accept an explicit owner. @@ -904,7 +904,7 @@ pub fn is_hive_managed_namespace(ns: &str) -> bool { } /// Provision the [`OPERATORS_TEAM`] inside [`AGENTS_ORG`] as an **empty** -/// team (#1787). Branch protection on agents-org repos references it as the +/// team. Branch protection on agents-org repos references it as the /// merge/approval whitelist; the operator adds herself as a member via the /// forge UI / hivectl. `includes_all_repositories` so the gate applies to /// every agent repo; `write` is enough to approve + merge. hive-c0re never @@ -955,7 +955,7 @@ async fn add_collaborator( } /// Apply the operator merge-gate branch protection to `repo`'s default -/// branch (#1787): only [`OPERATORS_TEAM`] members can merge, and an +/// branch: only [`OPERATORS_TEAM`] members can merge, and an /// approving review from that team is required — so the author (a write-level /// agent, not in the team) cannot merge its own PR. Idempotent: an existing /// rule for the branch (200/409/422) is treated as success. @@ -979,7 +979,7 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()> } /// Create a repo for `agent` in the c0re-owned [`AGENTS_ORG`] and wire the -/// #1787 perms: the org owns it (perms stay c0re-managed), the agent is added +/// 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 /// bypass branch protection), and the default branch gets the operator /// merge gate. This is the sanctioned create path now that agents can't @@ -1011,7 +1011,7 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) { // so commits link to the agent's Forgejo profile. Best-effort; // also patches up agents created before this fix (old @hive.local). ensure_user_email(name).await; - // Block direct agent-initiated repo creation (#1787): agents create + // Block direct agent-initiated repo creation: agents create // repos through hive-c0re, never with their own token. Idempotent + // marker-guarded; also covers agents provisioned before this landed. ensure_repo_creation_disabled(name).await; @@ -1073,7 +1073,7 @@ pub async fn ensure_all() { } // Provision the operator merge-gate team (empty) inside the agents // org so branch protection can reference it before anyone joins - // (#1787). The operator adds herself as a member out-of-band. + //. The operator adds herself as a member out-of-band. if let Err(e) = ensure_operators_team(token).await { tracing::warn!(error = ?e, "forge: ensure_operators_team failed"); } diff --git a/hive-c0re/src/paths.rs b/hive-c0re/src/paths.rs index 72887b7f..df63d19c 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -62,7 +62,7 @@ pub fn forge_email_aligned_marker(name: &str) -> PathBuf { /// `forge/repo-creation-disabled-` — marker: ``'s forge user /// has had `max_repo_creation = 0` applied (blocks direct agent-initiated -/// repo creation — see #1787). One-shot guard so the PATCH runs once per +/// repo creation). One-shot guard so the PATCH runs once per /// agent (including agents provisioned before the change); delete to /// re-apply. #[must_use] diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index bc4a3623..b49f1963 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -572,7 +572,7 @@ pub enum Request { /// per-kind semantics in /// `docs/conventions.md::Loose-ends wire shape`. CancelLooseEnd { kind: CancelLooseEndKind, id: i64 }, - /// Create a git repo *through hive-c0re* (#1787). Agents can't create + /// Create a git repo *through hive-c0re*. Agents can't create /// repos with their own forge token (`max_repo_creation = 0`); this is /// the sanctioned path. hive-c0re creates `repo` in the c0re-owned /// `agents` org, adds the calling agent as a write collaborator (not @@ -966,7 +966,7 @@ pub enum ToolGroup { /// `get_logs` - *(privileged)* Diagnostics, /// `create_repo` — create git repos through hive-c0re (the only path - /// now that agents can't create them directly; see #1787). Opt-in per + /// now that agents can't create them directly). Opt-in per /// agent so the operator controls who can spin up repos. Forge, /// `run`, `status` (via `mcp__bash__*`)