From 97edd6baac63d8a5a90d8f56eef9fd0ea01d966b Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 11 Jul 2026 09:55:05 +0200 Subject: [PATCH] =?UTF-8?q?feat(#2346):=20request=5Fmerge=5Fconfig=5Fpr=20?= =?UTF-8?q?=E2=80=94=20submission=20path=20for=20the=20PR-based=20config?= =?UTF-8?q?=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MergeConfigPr approvals had a fully-implemented approve handler (run_merge_config_pr, ff_push_to_main, mark_pr_merged) and dashboard display, but no way to submit one. An agent with the `approvals` tool group calling request_merge_config_pr(agent, pr_number) is the missing piece. What this adds: - RequestMergeConfigPr variant in hive-sh4re AgentRequest + ToolGroup::Approvals - submit_merge_config_pr: fetches PR head sha (the drift-gate reviewed sha), queues a MergeConfigPr row, sets fetched_sha, emits approval_added with pr_number so the dashboard card links to the forge PR - handle_request_merge_config_pr: topology (require_descendant) + tool-group (require_group(approvals)) guards before submit - socket_server/mod.rs: dispatch arm for RequestMergeConfigPr - hive-ag3nt MCP tool: request_merge_config_pr with full description - docs/tools/lifecycle.md: documents the new tool + boundary table row Unlike submit_apply_commit, no flake pre-flight at submission time (eval- verify happens at approval time inside run_merge_config_pr, same as the rest of the merge pipeline). Applied repo must already exist (guard added with a clear error message pointing at request_apply_commit for first-spawn). --- docs/tools/lifecycle.md | 23 ++++- hive-ag3nt/src/mcp/args.rs | 11 +++ hive-ag3nt/src/mcp/mod.rs | 45 ++++++++- .../src/socket_server/config_approvals.rs | 94 ++++++++++++++++++- hive-c0re/src/socket_server/mod.rs | 20 +++- hive-sh4re/src/lib.rs | 17 +++- 6 files changed, 201 insertions(+), 9 deletions(-) diff --git a/docs/tools/lifecycle.md b/docs/tools/lifecycle.md index 5ad65e21..2f8490fd 100644 --- a/docs/tools/lifecycle.md +++ b/docs/tools/lifecycle.md @@ -64,6 +64,24 @@ pinned commit. rejected — the approval pins the exact commit). `agent` must be a direct child. Topology-enforced. +### `request_merge_config_pr(agent, pr_number, description?)` + +Submit an open PR on the agent's `agent-configs/` forge repo for +operator review and merge. The PR-based config flow's counterpart to +`request_apply_commit`: instead of pinning a commit sha from the proposed +repo, the submitter references an already-open forge PR. + +hive-c0re fetches the PR head sha at submission time (the "reviewed" sha); +on operator approval it re-checks for drift, eval-verifies the commit, +fast-forwards the forge repo's `main` to the reviewed sha, marks the PR +merged, and rebuilds the agent container. If the PR head moves between +submission and approval the approve handler aborts — the submitter must +re-submit. + +`agent` must be in the caller's subtree. The agent must already be fully +provisioned (applied repo present); this tool is not for first-spawn. +Requires the `approvals` tool group. + ### `request_update_meta_inputs(inputs?, description?)` Queue an approval to run `nix flake update [inputs...]` on the meta @@ -81,8 +99,9 @@ agents after the approval resolves. | `kill` / `start` / `restart` / `update` | No | Direct children | | `list_containers` | No | All descendants | | `request_init_config` | Yes (InitConfig) | New direct child only | -| `request_apply_commit` | Yes (ApplyCommit) | Direct children | -| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | +| `request_apply_commit` | Yes (ApplyCommit) | Direct children | +| `request_merge_config_pr` | Yes (MergeConfigPr) | Descendants | +| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | ## See also diff --git a/hive-ag3nt/src/mcp/args.rs b/hive-ag3nt/src/mcp/args.rs index 007504cf..f860f4a6 100644 --- a/hive-ag3nt/src/mcp/args.rs +++ b/hive-ag3nt/src/mcp/args.rs @@ -222,6 +222,17 @@ pub struct RequestApplyCommitArgs { pub description: Option, } +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct RequestMergeConfigPrArgs { + /// Logical agent name whose `agent-configs/` forge repo holds the PR. + pub agent: String, + /// Open PR index on the `agent-configs/` repo. + pub pr_number: u64, + /// Optional description shown on the dashboard approval card. + #[serde(default)] + pub description: Option, +} + #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct UpdateMetaInputsArgs { /// Flake input names to update (e.g. `["bitburner-agent", "nixpkgs"]`). diff --git a/hive-ag3nt/src/mcp/mod.rs b/hive-ag3nt/src/mcp/mod.rs index 563ce71f..aa802296 100644 --- a/hive-ag3nt/src/mcp/mod.rs +++ b/hive-ag3nt/src/mcp/mod.rs @@ -29,8 +29,8 @@ pub use args::{ AckUntilArgs, AgentGetLooseEndsArgs, AnswerArgs, AskArgs, CancelLooseEndArgs, CancelScheduleArgs, CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, GetLogsArgs, KillArgs, RecvArgs, RemindArgs, RequestApplyCommitArgs, - RequestInitConfigArgs, RequestSchedulePromptArgs, RestartArgs, SendArgs, SetStatusArgs, - StartArgs, UpdateArgs, UpdateMetaInputsArgs, + RequestInitConfigArgs, RequestMergeConfigPrArgs, RequestSchedulePromptArgs, RestartArgs, + SendArgs, SetStatusArgs, StartArgs, UpdateArgs, UpdateMetaInputsArgs, }; pub use render::{ IDLE_WAIT_HINT, REDELIVERY_HINT, annotate_retries, format_ack, format_agent_meta, format_recv, @@ -757,6 +757,47 @@ impl AgentServer { .await } + // IMPORTANT: this tool is only available when the `approvals` tool group + // is configured for the agent. hive-c0re enforces both the tool-group check + // and topology: the target must be in the caller's subtree. + #[tool( + description = "Submit an open forge PR on `agent-configs/` for the operator \ + to review and merge into the agent's running config. Requires the `approvals` \ + tool group. `agent` must be in this agent's subtree. `pr_number` is the PR \ + index on the `agent-configs/` repo. hive-c0re fetches the PR head sha \ + at submission time (the drift-gate sha); if the PR head moves before the \ + operator approves, the approve handler aborts without making any changes — the \ + submitter must re-submit. On approval hive-c0re eval-verifies the head, \ + fast-forwards the forge repo's `main`, marks the PR merged, and rebuilds the \ + agent container." + )] + async fn request_merge_config_pr( + &self, + Parameters(args): Parameters, + ) -> String { + let log = format!("{args:?}"); + let agent = args.agent.clone(); + let pr_number = args.pr_number; + run_tool_envelope("request_merge_config_pr", log, async move { + let (resp, retries) = self + .dispatch(hive_sh4re::Request::RequestMergeConfigPr { + agent: args.agent, + pr_number: args.pr_number, + description: args.description, + }) + .await; + annotate_retries( + format_ack( + resp, + "request_merge_config_pr", + format!("merge_config_pr approval queued for {agent} PR #{pr_number}"), + ), + retries, + ) + }) + .await + } + // IMPORTANT: this tool is only available when the `lifecycle` tool group // is granted to this agent. hive-c0re enforces the topology check // server-side: the call is rejected unless `name` is a direct child. diff --git a/hive-c0re/src/socket_server/config_approvals.rs b/hive-c0re/src/socket_server/config_approvals.rs index 492322b9..1516a4cf 100644 --- a/hive-c0re/src/socket_server/config_approvals.rs +++ b/hive-c0re/src/socket_server/config_approvals.rs @@ -1,7 +1,8 @@ //! Config-approval request handlers: `RequestInitConfig` / -//! `RequestApplyCommit` / `RequestUpdateMetaInputs`, plus the shared -//! submit helpers (`submit_init_config` / `submit_apply_commit`) and the -//! commit-sha shape check (`validate_commit_ref`). +//! `RequestApplyCommit` / `RequestMergeConfigPr` / `RequestUpdateMetaInputs`, +//! plus the shared submit helpers (`submit_init_config` / `submit_apply_commit` +//! / `submit_merge_config_pr`) and the commit-sha shape check +//! (`validate_commit_ref`). use std::sync::Arc; @@ -105,6 +106,93 @@ pub(super) fn handle_request_update_meta_inputs( AgentResponse::Ok } +/// `RequestMergeConfigPr` — queue a `MergeConfigPr` approval for a PR on an +/// agent's `agent-configs/` forge repo. The target must be in the +/// caller's subtree. hive-c0re fetches the PR head sha at submission time; +/// that sha is stored as `fetched_sha` and forms the drift gate in the +/// approve handler: if the PR head moves between submission and approval, +/// the approve handler aborts without making any changes. +pub(super) async fn handle_request_merge_config_pr( + coord: &Arc, + agent: &str, + target_agent: &str, + pr_number: u64, + description: Option<&str>, +) -> AgentResponse { + if let Some(err) = super::require_descendant(agent, target_agent, "request_merge_config_pr for") + { + return err; + } + tracing::info!(%agent, %target_agent, %pr_number, "request_merge_config_pr"); + match submit_merge_config_pr(coord, target_agent, pr_number, description, agent).await { + Ok(id) => { + tracing::info!(%id, %target_agent, %pr_number, "merge_config_pr approval queued"); + AgentResponse::Ok + } + Err(e) => AgentResponse::Err { + message: format!("{e:#}"), + }, + } +} + +/// Submit-time half of the PR-merge flow: fetch the PR head sha from the +/// forge, queue the approval row, and emit the `approval_added` event so the +/// dashboard shows the pending card immediately. +/// +/// The PR head sha is stored as `fetched_sha` on the approval row — the +/// "reviewed sha" the approve handler (`run_merge_config_pr`) drift-gates +/// against before doing anything irreversible. Unlike `submit_apply_commit` +/// this does NOT fetch the commit into the applied repo at submission time +/// (that happens inside the approve handler, step 2, after the drift check). +/// No flake pre-flight either — eval-verify happens at approval time too. +async fn submit_merge_config_pr( + coord: &Arc, + agent: &str, + pr_number: u64, + description: Option<&str>, + submitter: &str, +) -> anyhow::Result { + let applied_dir = crate::paths::applied_dir(agent); + if !applied_dir.join(".git").exists() { + anyhow::bail!( + "applied repo missing for agent '{agent}' (expected at {}) — \ + merge_config_pr requires the agent to be fully provisioned; \ + use request_apply_commit for the first config deploy", + applied_dir.display() + ); + } + let repo = crate::forge::config_repo(agent); + // Fetch the current PR head sha — becomes the "reviewed" sha. + let sha = crate::forge::pr_head_sha(&repo, pr_number) + .await + .map_err(|e| anyhow::anyhow!("fetch PR head sha for {agent} PR #{pr_number}: {e}"))?; + let id = coord + .approvals + .submit_kind( + agent, + hive_sh4re::ApprovalKind::MergeConfigPr, + &pr_number.to_string(), + description, + submitter, + ) + .map_err(|e| anyhow::anyhow!("queue merge_config_pr approval row: {e:#}"))?; + coord + .approvals + .set_fetched_sha(id, &sha) + .map_err(|e| anyhow::anyhow!("persist fetched_sha: {e:#}"))?; + let sha_short = sha[..sha.len().min(12)].to_owned(); + coord.emit_approval_added(crate::coordinator::ApprovalAdded { + id, + agent, + approval_kind: "merge_config_pr", + sha_short: Some(sha_short), + diff: None, // diff is not pre-computed; the dashboard fetches it on demand + description: description.map(str::to_owned), + pr_number: Some(pr_number), + }); + Ok(id) +} + /// `request_apply_commit` takes a commit SHA only — not a branch or /// tag name. A branch is mutable; pinning the proposal to a concrete /// sha keeps "what the manager asked to deploy" unambiguous and means diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index e88e6de8..fb3723da 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -29,7 +29,8 @@ pub(crate) use schedules::filter_ghost_schedule_targets; pub use schedules::schedule_to_wire_public; use config_approvals::{ - handle_request_apply_commit, handle_request_init_config, handle_request_update_meta_inputs, + handle_request_apply_commit, handle_request_init_config, handle_request_merge_config_pr, + handle_request_update_meta_inputs, }; use lifecycle_handlers::{ handle_kill, handle_list_descendants, handle_restart, handle_start, handle_update, @@ -578,6 +579,23 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> ) .await } + AgentRequest::RequestMergeConfigPr { + agent: target_agent, + pr_number, + description, + } => { + if let Some(err) = require_group(agent, "approvals", "request merge_config_pr") { + return err; + } + handle_request_merge_config_pr( + coord, + agent, + target_agent, + *pr_number, + description.as_deref(), + ) + .await + } // Agent-state queries: own subtree is free; other agents + the // hive-wide `"*"` sweep require `QueryAgentState`. AgentRequest::GetLooseEnds { agent: target } => { diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index dd95e010..4c0ca7a8 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -788,6 +788,20 @@ pub enum Request { #[serde(default, skip_serializing_if = "Option::is_none")] description: Option, }, + /// *(privileged)* Submit a forge config PR for the operator to review and + /// merge. `agent` is the child whose `agent-configs/` forge repo + /// holds the PR; `pr_number` is the open PR index on that repo. + /// hive-c0re fetches the PR head sha at submission time (the "reviewed" + /// sha for the drift gate) and queues a `MergeConfigPr` approval. On + /// approval, hive-c0re re-verifies the head hasn't drifted, eval-verifies + /// the commit, fast-forwards the forge repo's `main`, marks the PR merged, + /// and rebuilds the agent container. + RequestMergeConfigPr { + agent: String, + pr_number: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + description: Option, + }, /// *(privileged)* Fetch recent journal lines for a sub-agent container. GetLogs { agent: String, @@ -1201,6 +1215,7 @@ impl ToolGroup { Self::Approvals => &[ "request_init_config", "request_apply_commit", + "request_merge_config_pr", "request_update_meta_inputs", ], Self::Scheduling => &[ @@ -1303,7 +1318,7 @@ impl ToolGroup { "kill, start, restart, update, list_containers — container lifecycle (privileged)" } Self::Approvals => { - "request_init_config, request_apply_commit, request_update_meta_inputs — config change flow (privileged)" + "request_init_config, request_apply_commit, request_merge_config_pr, request_update_meta_inputs — config change flow (privileged)" } Self::Scheduling => { "request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"