From 3fa31a414f68b164554f788e98190a6603da8b14 Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 3 Jun 2026 22:39:45 +0200 Subject: [PATCH] feat(#791): add request_init_config + request_apply_commit to AgentServer (topology-scoped) --- hive-ag3nt/prompts/system.md | 2 + hive-ag3nt/src/mcp.rs | 77 +++++++++++++++++++++++++++++++++ hive-c0re/src/agent_server.rs | 54 +++++++++++++++++++++++ hive-c0re/src/manager_server.rs | 58 ++++++++++++------------- 4 files changed, 161 insertions(+), 30 deletions(-) diff --git a/hive-ag3nt/prompts/system.md b/hive-ag3nt/prompts/system.md index bb5db67f..0275a9b7 100644 --- a/hive-ag3nt/prompts/system.md +++ b/hive-ag3nt/prompts/system.md @@ -45,6 +45,8 @@ Tools (hyperhive surface): - `mcp__hyperhive__restart(name)` — *(requires `lifecycle` tool group)* restart a direct child sub-agent (stop + start). The server enforces topology: the call is rejected unless `name` is a direct child of yours per `topology.json`. No approval required. - `mcp__hyperhive__kill(name)` — *(requires `lifecycle` tool group)* stop a direct child sub-agent (graceful). Direct children only — server enforces topology. State dir kept; recreating reuses prior config + credentials. No approval required. - `mcp__hyperhive__update(name)` — *(requires `lifecycle` tool group)* rebuild a direct child sub-agent: re-applies the current hyperhive flake + agent.nix and restarts it. Direct children only — server enforces topology. No approval required. Idempotent. +- `mcp__hyperhive__request_init_config(name, description?)` — *(requires `approvals` tool group)* initialise a brand-new direct child agent's proposed config repo. Queues an `InitConfig` approval; on approval hive-c0re seeds `/agents//config/agent.nix`. `name` must be a direct child in the topology tree — server enforces. Fails if the config repo already exists (use `request_apply_commit` instead). +- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` — *(requires `approvals` tool group)* submit a config commit for a direct child agent, queued for operator approval. `agent` must be a direct child in the topology tree — server enforces. `commit_ref` must be a 7-40 char hex sha (not a branch/tag name). On approval hive-c0re rebuilds the container with the pinned commit. Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `root`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index b40d0ee9..004d5e48 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -1047,6 +1047,83 @@ impl AgentServer { }) .await } + + // IMPORTANT: this tool is only available when the `approvals` tool group + // is configured for the agent (`HIVE_TOOL_GROUPS` contains `approvals`). + // hive-c0re performs a topology check server-side: only direct children + // of the calling agent are accepted; all other names are rejected. + #[tool( + description = "Initialise a brand-new direct child agent's proposed config repo and \ + queue an `InitConfig` approval for the operator to review. Requires the `approvals` \ + tool group. `name` must be a direct child of this agent in the topology tree. \ + Fails if a config repo for that child already exists — use `request_apply_commit` \ + to update an existing agent's config. On approval hive-c0re seeds \ + `/agents//config/agent.nix` with the default template so you can \ + customise it and then call `request_apply_commit` with the commit sha." + )] + async fn request_init_config( + &self, + Parameters(args): Parameters, + ) -> String { + let log = format!("{args:?}"); + let name = args.name.clone(); + run_tool_envelope("request_init_config", log, async move { + let (resp, retries) = self + .dispatch(hive_sh4re::AgentRequest::RequestInitConfig { + name: args.name, + description: args.description, + }) + .await; + annotate_retries( + format_ack( + resp, + "request_init_config", + format!("init_config approval queued for {name}"), + ), + retries, + ) + }) + .await + } + + // IMPORTANT: this tool is only available when the `approvals` tool group + // is configured for the agent (`HIVE_TOOL_GROUPS` contains `approvals`). + // hive-c0re performs a topology check server-side: only direct children + // of the calling agent are accepted; all other names are rejected. + #[tool( + description = "Submit a config change for a direct child agent, queued for operator \ + approval. Requires the `approvals` tool group. `agent` must be a direct child \ + of this agent in the topology tree. Pass a commit sha (7-40 hex chars, full or \ + short) from that agent's proposed config repo — branch/tag names like `main` are \ + rejected, the approval pins the exact commit. On approval hive-c0re rebuilds \ + the container with the new config." + )] + async fn request_apply_commit( + &self, + Parameters(args): Parameters, + ) -> String { + let log = format!("{args:?}"); + let agent = args.agent.clone(); + let commit_ref = args.commit_ref.clone(); + run_tool_envelope("request_apply_commit", log, async move { + let (resp, retries) = self + .dispatch(hive_sh4re::AgentRequest::RequestApplyCommit { + agent: args.agent, + commit_ref: args.commit_ref, + description: args.description, + }) + .await; + annotate_retries( + format_ack( + resp, + "request_apply_commit", + format!("apply approval queued for {agent} @ {commit_ref}"), + ), + retries, + ) + }) + .await + } } #[tool_handler( diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 807182df..a123e866 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -425,6 +425,60 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> coord.emit_rebuild_queue_snapshot(); AgentResponse::Ok } + AgentRequest::RequestInitConfig { name, description } => { + if !crate::topology::children_of(agent) + .iter() + .any(|c| c == name) + { + return AgentResponse::Err { + message: format!( + "agent `{agent}` cannot request_init_config for `{name}`: \ + not a direct child in the topology tree" + ), + }; + } + tracing::info!(%agent, %name, "agent: request_init_config for child"); + match crate::manager_server::submit_init_config(coord, name, description.clone()).await { + Ok(_id) => AgentResponse::Ok, + Err(e) => AgentResponse::Err { + message: format!("{e:#}"), + }, + } + } + AgentRequest::RequestApplyCommit { + agent: target_agent, + commit_ref, + description, + } => { + if !crate::topology::children_of(agent) + .iter() + .any(|c| c == target_agent) + { + return AgentResponse::Err { + message: format!( + "agent `{agent}` cannot request_apply_commit for `{target_agent}`: \ + not a direct child in the topology tree" + ), + }; + } + tracing::info!(%agent, %target_agent, %commit_ref, "agent: request_apply_commit for child"); + match crate::manager_server::submit_apply_commit( + coord, + target_agent, + commit_ref, + description.as_deref(), + ) + .await + { + Ok((id, sha)) => { + tracing::info!(%id, %target_agent, %sha, "agent: apply_commit approval queued"); + AgentResponse::Ok + } + Err(e) => AgentResponse::Err { + message: format!("{e:#}"), + }, + } + } // Manager-only variants are not valid on the agent socket. _ => AgentResponse::Err { message: "request not supported on agent socket".to_owned(), diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index 39bba5d0..e5a0a54f 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -83,34 +83,8 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc) -> ManagerResp match req { ManagerRequest::RequestInitConfig { name, description } => { tracing::info!(%name, "manager: request_init_config"); - let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name); - if proposed_dir.join(".git").exists() { - return ManagerResponse::Err { - message: format!( - "proposed config repo for '{name}' already exists at {} - \ - use request_apply_commit to update an existing agent's config", - proposed_dir.display() - ), - }; - } - match coord.approvals.submit_kind( - name, - hive_sh4re::ApprovalKind::InitConfig, - "", - description.as_deref(), - ) { - Ok(id) => { - tracing::info!(%id, %name, "init_config approval queued"); - coord.emit_approval_added( - id, - name, - "init_config", - None, - None, - description.clone(), - ); - ManagerResponse::Ok - } + match submit_init_config(coord, name, description.clone()).await { + Ok(_id) => ManagerResponse::Ok, Err(e) => ManagerResponse::Err { message: format!("{e:#}"), }, @@ -352,7 +326,7 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc) -> ManagerResp /// Accepts a 7..=40 char hex string (short or full sha); the exact /// commit is resolved + existence-checked against the proposed repo /// later in `lifecycle::git_fetch_to_tag`. -fn validate_commit_ref(commit_ref: &str) -> Result<()> { +pub(crate) fn validate_commit_ref(commit_ref: &str) -> Result<()> { let n = commit_ref.len(); let hex = commit_ref.chars().all(|c| c.is_ascii_hexdigit()); if !(7..=40).contains(&n) || !hex { @@ -364,6 +338,30 @@ fn validate_commit_ref(commit_ref: &str) -> Result<()> { Ok(()) } +/// Queue an `InitConfig` approval for a brand-new agent whose config repo +/// does not yet exist. Shared between the manager and agent sockets. +pub(crate) async fn submit_init_config( + coord: &Arc, + name: &str, + description: Option, +) -> anyhow::Result { + let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name); + if proposed_dir.join(".git").exists() { + anyhow::bail!( + "proposed config repo for '{name}' already exists at {} - \ + use request_apply_commit to update an existing agent's config", + proposed_dir.display() + ); + } + let id = coord + .approvals + .submit_kind(name, hive_sh4re::ApprovalKind::InitConfig, "", description.as_deref()) + .map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?; + tracing::info!(%id, %name, "init_config approval queued"); + coord.emit_approval_added(id, name, "init_config", None, None, description); + Ok(id) +} + /// Submit-time half of the apply flow: queue the approval row, then /// fetch the manager's commit from the proposed repo into applied and /// pin it as `refs/tags/proposal/`. From this point on the manager @@ -375,7 +373,7 @@ fn validate_commit_ref(commit_ref: &str) -> Result<()> { /// proposed, fs error, git plumbing crash) we mark the row failed and /// surface the error to the manager. We don't try to roll the row /// back — the failure is part of the audit trail. -async fn submit_apply_commit( +pub(crate) async fn submit_apply_commit( coord: &Arc, agent: &str, commit_ref: &str,