diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 6bbdeb60..e4ddebc0 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -59,11 +59,6 @@ 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 { @@ -105,13 +100,6 @@ impl From for SocketReply { hive_name, swarm_name, }, - hive_sh4re::Response::RepoCreated { - full_name, - clone_url, - } => Self::RepoCreated { - full_name, - clone_url, - }, } } } @@ -922,35 +910,6 @@ 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 \ @@ -1546,13 +1505,6 @@ 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 66002c6d..4b61bedb 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -183,7 +183,6 @@ 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 => { @@ -305,46 +304,6 @@ 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. -/// 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 d0f4f8eb..9449700d 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -51,30 +51,9 @@ 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. 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. `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, AGENTS_ORG]; +const SEEDED_ORGS: &[&str] = &[CONFIG_ORG, SHARED_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"; @@ -270,57 +249,6 @@ async fn ensure_user_email(name: &str) { } } -/// Disable direct repo creation for agent `name` by setting -/// `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. -/// -/// `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 @@ -893,105 +821,6 @@ 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 — `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. 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: 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 -/// 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(format!("{AGENTS_ORG}/{repo}")) -} - /// 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 @@ -1011,10 +840,6 @@ 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: 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 @@ -1071,12 +896,6 @@ 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 - //. 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 df63d19c..3ffcf974 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -60,16 +60,6 @@ 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). 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 diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index b49f1963..8cdb9f47 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -572,13 +572,6 @@ 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*. 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`. @@ -760,12 +753,6 @@ 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. @@ -965,10 +952,6 @@ 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). 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 @@ -1008,7 +991,6 @@ impl ToolGroup { "list_schedules", ], Self::Diagnostics => &["get_logs"], - Self::Forge => &["create_repo"], Self::Execution => &["run", "status"], Self::WebTools => &[], } @@ -1062,7 +1044,6 @@ impl ToolGroup { Self::Approvals, Self::Scheduling, Self::Diagnostics, - Self::Forge, Self::Execution, Self::WebTools, ]; @@ -1079,7 +1060,6 @@ impl ToolGroup { Self::Approvals => "approvals", Self::Scheduling => "scheduling", Self::Diagnostics => "diagnostics", - Self::Forge => "forge", Self::Execution => "execution", Self::WebTools => "web_tools", } @@ -1108,9 +1088,6 @@ 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" }