remove the create_repo agent tool
mara ruled on #4849 (c88934): "remove create_repo tool". The tool ran in
hive-c0re with the hive's core token, so it only ever worked for agents
on the hive that runs the forge.
Removed:
- the create_repo MCP tool and CreateRepoArgs (hive-agent-mcp)
- wire variants Request::CreateRepo and Response::RepoCreated
(hive-core-agent-sock)
- hive-c0re's handle_create_repo, its valid_repo_name check and the
dispatch arm
- forge::create_agent_repo and apply_operator_branch_protection, which
had no other caller, plus AGENTS_ORG and OPERATORS_TEAM, whose only
users they were
- the tool's docs (docs/tools/forge.md repo management, docs/turn-loop/
mcp.md, the conventions tool-group table) and the doc comments that
named it (hive-sock-client's response timeout, ensure_repo_creation_
disabled, the security doc's merge-gate bullet)
ToolGroup::Forge is kept with no tools, the same way b88a5b24 kept
Lifecycle, so existing meta/capabilities.json grants still parse.
Forge state is untouched: existing agents/* repos keep their collaborators
and operators-team branch protection. The swarm-controller's own
create_repo (config-org repos) is a different path and is unchanged.
Closes #4849
This commit is contained in:
parent
bab15e2ba6
commit
7d217f8267
14 changed files with 43 additions and 230 deletions
|
|
@ -17,9 +17,9 @@ pub use pr_merge::{
|
|||
};
|
||||
pub use reconcile::{reconcile_config_apply, reconcile_config_status};
|
||||
pub use repos::{
|
||||
clone_config_into_proposed, create_agent_repo, ensure_config_repo, ensure_meta_remote,
|
||||
ensure_repo, fast_forward_applied_main, fetch_config_main_into_applied, meta_read_access,
|
||||
push_config, push_meta, shared_docs_access,
|
||||
clone_config_into_proposed, ensure_config_repo, ensure_meta_remote, ensure_repo,
|
||||
fast_forward_applied_main, fetch_config_main_into_applied, meta_read_access, push_config,
|
||||
push_meta, shared_docs_access,
|
||||
};
|
||||
pub use users::core_token;
|
||||
|
||||
|
|
@ -112,21 +112,6 @@ const SHARED_ORG: &str = "internal";
|
|||
/// The shared docs repo inside `SHARED_ORG`. Cloneable by every agent
|
||||
/// at `{forge_http_base()}/internal/docs.git`.
|
||||
const SHARED_DOCS_REPO: &str = "docs";
|
||||
/// 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. The swarm-controller
|
||||
/// ensures the org itself.
|
||||
const AGENTS_ORG: &str = "agents";
|
||||
/// Operator merge-gate team inside [`AGENTS_ORG`]. Provisioned **empty** by
|
||||
/// the swarm-controller (so perms can be set before anyone joins); the
|
||||
/// operator adds herself via the forge UI. 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";
|
||||
|
||||
/// Leak `s` to get a `&'static str` warning `kind` for the small, bounded
|
||||
/// set of boot warnings in [`ensure_all`] keyed by a runtime name (at
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ use reqwest::StatusCode;
|
|||
use crate::coordinator::Coordinator;
|
||||
|
||||
use super::{
|
||||
AGENTS_ORG, CONFIG_ORG, OPERATORS_TEAM, SHARED_DOCS_REPO, SHARED_ORG, api, core_auth_header,
|
||||
core_token, forge_git_url, forge_http_base, is_present,
|
||||
CONFIG_ORG, SHARED_DOCS_REPO, SHARED_ORG, api, core_auth_header, core_token, forge_git_url,
|
||||
forge_http_base, is_present,
|
||||
};
|
||||
|
||||
/// Creation options for an empty repo defaulting to `main`.
|
||||
|
|
@ -659,52 +659,6 @@ fn main_branch_protection_option() -> CreateBranchProtectionOption {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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, but **verify-don't-trust**: a create failure is ambiguous —
|
||||
/// "rule already exists" (success) OR a silent rejection that created NO rule
|
||||
/// (e.g. a 422 where `OPERATORS_TEAM` doesn't exist in `AGENTS_ORG`). The old
|
||||
/// code folded 200/409/422 into `Ok` and left the repo unprotected with no
|
||||
/// error — a fail-open merge gate. So on any create error, GET the `main` rule
|
||||
/// and only treat it as success if the rule is actually present (the exact fix
|
||||
/// already applied to [`apply_config_repo_branch_protection`]).
|
||||
async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()> {
|
||||
let client = api(token)?;
|
||||
let mut rule = main_branch_protection_option();
|
||||
rule.enable_merge_whitelist = Some(true);
|
||||
rule.merge_whitelist_teams = Some(vec![OPERATORS_TEAM.to_owned()]);
|
||||
rule.enable_approvals_whitelist = Some(true);
|
||||
rule.approvals_whitelist_teams = Some(vec![OPERATORS_TEAM.to_owned()]);
|
||||
rule.required_approvals = Some(1);
|
||||
rule.block_on_official_review_requests = Some(true);
|
||||
let Err(create_err) = client
|
||||
.repo_create_branch_protection(AGENTS_ORG, repo, rule)
|
||||
.await
|
||||
else {
|
||||
tracing::info!(%repo, "forge: applied operator branch protection");
|
||||
return Ok(());
|
||||
};
|
||||
match client
|
||||
.repo_get_branch_protection(AGENTS_ORG, repo, "main")
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::debug!(
|
||||
%repo, create_error = %create_err,
|
||||
"forge: operator branch protection already present"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(check_err) => anyhow::bail!(
|
||||
"branch protection for {AGENTS_ORG}/{repo} not applied: create failed \
|
||||
({create_err}); GET main rule failed ({check_err}), no `main` rule present"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply branch protection to an `agent-configs/<name>` repo's `main` so it
|
||||
/// can serve as the agent-editable, PR-merge config surface:
|
||||
/// - **`main` is never directly pushable** — no push is enabled on the
|
||||
|
|
@ -809,24 +763,3 @@ fn config_repo_protection_edit() -> EditBranchProtectionOption {
|
|||
unprotected_file_patterns: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<String> {
|
||||
ensure_org_repo(AGENTS_ORG, repo, core_token).await?;
|
||||
add_collaborator(
|
||||
AGENTS_ORG,
|
||||
repo,
|
||||
agent,
|
||||
AddCollaboratorOptionPermission::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}"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,10 +208,9 @@ pub(super) 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` on its Forgejo account. 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`),
|
||||
|
|
|
|||
|
|
@ -316,7 +316,6 @@ pub(crate) async fn dispatch_shared(
|
|||
|()| hive_core_agent_sock::Response::Ok,
|
||||
)
|
||||
}
|
||||
hive_core_agent_sock::Request::CreateRepo { repo } => handle_create_repo(agent, repo).await,
|
||||
hive_core_agent_sock::Request::AckTurn => handle_ack_turn(coord, agent),
|
||||
hive_core_agent_sock::Request::AckUntil { up_to } => handle_ack_until(coord, agent, *up_to),
|
||||
hive_core_agent_sock::Request::RequeueInflight => handle_requeue_inflight(coord, agent),
|
||||
|
|
@ -423,46 +422,6 @@ fn handle_set_status(coord: &Arc<Coordinator>, text: &str) -> hive_core_agent_so
|
|||
hive_core_agent_sock::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_core_agent_sock::Response {
|
||||
if !valid_repo_name(repo) {
|
||||
return hive_core_agent_sock::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_core_agent_sock::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_core_agent_sock::Response::RepoCreated {
|
||||
clone_url: format!("{}/{full_name}.git", crate::forge::forge_http_base()),
|
||||
full_name,
|
||||
},
|
||||
Err(e) => hive_core_agent_sock::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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue