From d5a81f91958a6b5b83d106b31e26d59c433e3042 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 11 Jul 2026 10:50:24 +0200 Subject: [PATCH] feat(#2377): forge-webhook-triggered config-PR merge flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the request_merge_config_pr MCP tool with a Forgejo pull_request webhook on the agent-configs org. Agents now open a config PR normally; hive-c0re auto-queues the MergeConfigPr approval from the webhook event — no extra tool call needed. Changes: - dashboard/webhook.rs: add POST /webhook/config-pr handler - parses Forgejo pull_request payload (opened/synchronize) - strips agent-configs/ prefix to extract agent name - calls submit_merge_config_pr → queues dashboard approval card - always 200 to prevent Forgejo retries; errors logged at warn - dashboard/mod.rs: wire /webhook/config-pr route - forge/mod.rs: add ensure_config_pr_webhook() — idempotent org-level hook registration on agent-configs at startup; CONFIG_ORG now pub(crate) for webhook handler - main.rs: call ensure_config_pr_webhook alongside knowledge webhook - socket_server/config_approvals.rs: drop handle_request_merge_config_pr; make submit_merge_config_pr pub(crate) for webhook handler - socket_server/mod.rs: re-export submit_merge_config_pr; drop dispatch arm - hive-sh4re/src/lib.rs: remove AgentRequest::RequestMergeConfigPr wire type; drop from ToolGroup::Approvals tool list - hive-ag3nt/src/mcp/: drop request_merge_config_pr tool + args struct - docs: update approvals.md (webhook trigger), conventions.md (tool group), agent-hierarchy.md, tools/lifecycle.md Hardening from #2375-merge-config-pr-hardening branch preserved: - pr_is_open check at queue time (rejects closed/merged PRs) - atomic fetched_sha INSERT via submit_kind(fetched_sha: Some(&sha)) Approve-handler machinery unchanged (run_merge_config_pr, ff_push_to_main, fetch_pr_head_into_applied, mark_pr_merged). --- docs/agent-hierarchy.md | 1 - docs/approvals.md | 23 +-- docs/conventions.md | 2 +- docs/tools/lifecycle.md | 19 --- hive-ag3nt/src/mcp/args.rs | 11 -- hive-ag3nt/src/mcp/mod.rs | 45 +----- hive-c0re/src/dashboard/mod.rs | 3 +- hive-c0re/src/dashboard/webhook.rs | 150 +++++++++++++++++- hive-c0re/src/forge/mod.rs | 69 +++++++- hive-c0re/src/main.rs | 17 +- .../src/socket_server/config_approvals.rs | 40 +---- hive-c0re/src/socket_server/mod.rs | 21 +-- hive-sh4re/src/lib.rs | 17 +- libnull.rlib | Bin 0 -> 5442 bytes 14 files changed, 255 insertions(+), 163 deletions(-) create mode 100644 libnull.rlib diff --git a/docs/agent-hierarchy.md b/docs/agent-hierarchy.md index 3243afa6..038a52c3 100644 --- a/docs/agent-hierarchy.md +++ b/docs/agent-hierarchy.md @@ -99,7 +99,6 @@ Once enforcement lands the rules collapse into: | `kill` / `start` / `restart` / `update` (any descendant) | any ancestor | | `request_init_config` (spawn a new child) | any agent, child added under self | | `request_apply_commit` (any descendant's config) | any ancestor | -| `request_merge_config_pr` (any descendant's forge config PR) | any ancestor | | `get_logs` (any descendant) | any ancestor | | moderate questions / reminders (cancel any open thread of a descendant) | any ancestor | | `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else | diff --git a/docs/approvals.md b/docs/approvals.md index b730db34..dfb6c486 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -111,15 +111,20 @@ kind-specific payload carrier. row (only `ApplyCommit` populates it). See the End-to-end flow above. - `MergeConfigPr` — the PR-based config flow's counterpart to - `ApplyCommit`. `commit_ref` stores the **PR number** (decimal), - and `fetched_sha` is the PR **head sha the operator reviewed**. - On approve, `run_merge_config_pr` re-reads the live PR head and - aborts if it drifted from `fetched_sha` (re-review), then fetches - that head into the applied repo, eval-verifies it, fast-forwards - the forge config repo's `main` to it (the merge), marks the PR - merged (best-effort — `main` is already there), and runs the same - shared deploy tail as `ApplyCommit` (`deploy_applied_target`). - Never a first spawn. + `ApplyCommit`. Triggered automatically: when an agent opens (or + force-pushes) a PR on its `agent-configs/` forge repo, + hive-c0re's `/webhook/config-pr` endpoint receives the Forgejo + pull_request event and queues this approval row. No MCP tool call + needed — the forge PR IS the request. `commit_ref` stores the + **PR number** (decimal), and `fetched_sha` is the PR **head sha + at queue time** (the "reviewed" sha). On approve, + `run_merge_config_pr` re-reads the live PR head and aborts if it + drifted from `fetched_sha` (submitter must push again to + re-trigger), then fetches that head into the applied repo, + eval-verifies it, fast-forwards the forge config repo's `main` to + it (the merge), marks the PR merged (best-effort — `main` is + already there), and runs the same shared deploy tail as + `ApplyCommit` (`deploy_applied_target`). Never a first spawn. - `Spawn` — direct container creation under the default `agent.nix` template. `commit_ref` is empty. Submitted via `HostRequest::RequestSpawn` (operator-gated, the diff --git a/docs/conventions.md b/docs/conventions.md index 9cf7e7be..2c43c4af 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -325,7 +325,7 @@ binary flavor. | `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind`, `request_next_turn` | | `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. | | `lifecycle` | `kill`, `start`, `restart`, `update` *(privileged)* | -| `approvals` | `request_init_config`, `request_apply_commit`, `request_merge_config_pr`, `request_update_meta_inputs` *(privileged)* | +| `approvals` | `request_init_config`, `request_apply_commit`, `request_update_meta_inputs` *(privileged)* | | `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* | | `diagnostics` | `get_logs` *(privileged)* | diff --git a/docs/tools/lifecycle.md b/docs/tools/lifecycle.md index 2f8490fd..16fe257a 100644 --- a/docs/tools/lifecycle.md +++ b/docs/tools/lifecycle.md @@ -64,24 +64,6 @@ 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 @@ -100,7 +82,6 @@ agents after the approval resolves. | `list_containers` | No | All descendants | | `request_init_config` | Yes (InitConfig) | New direct child only | | `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 f860f4a6..007504cf 100644 --- a/hive-ag3nt/src/mcp/args.rs +++ b/hive-ag3nt/src/mcp/args.rs @@ -222,17 +222,6 @@ 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 aa802296..563ce71f 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, RequestMergeConfigPrArgs, RequestSchedulePromptArgs, RestartArgs, - SendArgs, SetStatusArgs, StartArgs, UpdateArgs, UpdateMetaInputsArgs, + RequestInitConfigArgs, 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,47 +757,6 @@ 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/dashboard/mod.rs b/hive-c0re/src/dashboard/mod.rs index 60facd1f..dd2ab2ca 100644 --- a/hive-c0re/src/dashboard/mod.rs +++ b/hive-c0re/src/dashboard/mod.rs @@ -160,9 +160,10 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { post(schedules::post_rebuild_queue_cancel), ) .route("/webhook/knowledge", post(webhook::post_webhook_knowledge)) + .route("/webhook/config-pr", post(webhook::post_webhook_config_pr)) // Backend routes — the frontend calls these `/api/` paths. The // transitional bare top-level aliases were removed once the - // frontend migrated. `/webhook/knowledge` keeps its own prefix + // frontend migrated. `/webhook/*` keeps its own prefix // (forge-driven, not the SPA). .route("/api/approve/{id}", post(approvals::post_approve)) .route("/api/deny/{id}", post(approvals::post_deny)) diff --git a/hive-c0re/src/dashboard/webhook.rs b/hive-c0re/src/dashboard/webhook.rs index a10a4328..35afc781 100644 --- a/hive-c0re/src/dashboard/webhook.rs +++ b/hive-c0re/src/dashboard/webhook.rs @@ -1,15 +1,27 @@ -//! Forgejo push-webhook endpoint for the `internal/knowledge` repo. +//! Forgejo webhook endpoints. //! -//! Loopback-only; on a push to `main` of the knowledge repo it triggers a -//! read-only `git pull` on the local clone so agents see up-to-date -//! documents on their next turn. +//! - **`/webhook/knowledge`** — push events on `internal/knowledge` trigger a +//! `git pull` on the local clone so agents see up-to-date docs. +//! - **`/webhook/config-pr`** — pull_request events on any `agent-configs/*` +//! repo queue a [`hive_sh4re::ApprovalKind::MergeConfigPr`] approval row +//! so the operator can review + approve the merge from the dashboard. +//! +//! Both endpoints are loopback-only (the axum listener binds +//! `127.0.0.1:`) and have no signature verification (the risk is low: +//! loopback access implies host compromise already, and the config-PR path +//! still requires the operator to approve on the dashboard). use axum::{ + extract::State, http::StatusCode, response::{IntoResponse, Response}, }; use serde::Deserialize; +use super::AppState; + +// ── knowledge webhook ────────────────────────────────────────────────────────── + /// Minimal Forgejo push-webhook payload — only the fields we care about. #[derive(Deserialize)] pub(super) struct PushWebhookPayload { @@ -62,3 +74,133 @@ pub(super) async fn post_webhook_knowledge( }); (StatusCode::OK, "ok").into_response() } + +// ── config-PR webhook ────────────────────────────────────────────────────────── + +/// Minimal Forgejo pull_request-webhook payload. +/// +/// Forgejo fires this for actions: `opened`, `closed`, `reopened`, +/// `synchronize`, `assigned`, `unassigned`, `label_updated`, +/// `label_cleared`, `milestoned`, `demilestoned`, `review_requested`, +/// `review_request_removed`, `auto_merge_enabled`, `auto_merge_disabled`. +/// We only act on `opened` and `synchronize`. +#[derive(Deserialize)] +pub(super) struct PrWebhookPayload { + /// What triggered this event (`opened`, `closed`, `synchronize`, …). + action: Option, + /// PR index on the repo. + number: Option, + pull_request: Option, + repository: Option, +} + +#[derive(Deserialize)] +struct PrWebhookPr { + head: Option, +} + +#[derive(Deserialize)] +struct PrWebhookHead { + sha: Option, +} + +#[derive(Deserialize)] +struct PrWebhookRepo { + full_name: Option, +} + +/// POST `/webhook/config-pr` — Forgejo pull_request webhook for +/// `agent-configs/*` repos. +/// +/// On `opened` or `synchronize` for an `agent-configs/` PR: +/// fetches the current PR head sha, queues a `MergeConfigPr` approval row, +/// and emits the `ApprovalAdded` event so the dashboard card appears +/// immediately. +/// +/// All other actions (closed, label changes, etc.) are silently ignored — +/// the operator can deny a pending approval if the PR is later closed. +/// +/// Always returns HTTP 200 (even on queue failure) so Forgejo does not +/// retry the delivery. Failures are logged at `warn` level. +/// +/// Expected Forgejo webhook configuration: +/// - URL: `http://127.0.0.1:/webhook/config-pr` +/// - Content type: `application/json` +/// - Events: "Pull Request" only +/// - Organisation: `agent-configs` (org-level hook covers all config repos) +/// +/// hive-c0re registers this hook automatically at startup via +/// [`crate::forge::ensure_config_pr_webhook`]. +pub(super) async fn post_webhook_config_pr( + State(state): State, + axum::extract::Json(payload): axum::extract::Json, +) -> Response { + let action = payload.action.as_deref().unwrap_or(""); + // Only act on newly-opened or force-updated PRs. + if action != "opened" && action != "synchronize" { + tracing::debug!(action, "webhook/config-pr: ignoring action"); + return (StatusCode::OK, "ignored").into_response(); + } + + let full_name = payload + .repository + .as_ref() + .and_then(|r| r.full_name.as_deref()) + .unwrap_or(""); + + // Expect `agent-configs/`. + let agent = match full_name.strip_prefix(&format!("{}/", crate::forge::CONFIG_ORG)) { + Some(name) if !name.is_empty() && !name.contains('/') => name, + _ => { + tracing::debug!( + full_name, + "webhook/config-pr: ignoring non-config-repo event" + ); + return (StatusCode::OK, "ignored").into_response(); + } + }; + + let pr_number = match payload.number { + Some(n) if n > 0 => n, + _ => { + tracing::warn!(full_name, "webhook/config-pr: missing or zero PR number"); + return (StatusCode::OK, "ignored").into_response(); + } + }; + + // The payload already carries the head sha — use it as an early hint for + // logging, but the canonical sha comes from `submit_merge_config_pr`'s + // fresh forge API call so we don't trust a potentially-stale payload sha. + let payload_sha = payload + .pull_request + .as_ref() + .and_then(|pr| pr.head.as_ref()) + .and_then(|h| h.sha.as_deref()) + .unwrap_or(""); + + tracing::info!( + %full_name, %agent, %pr_number, %payload_sha, %action, + "webhook/config-pr: queuing MergeConfigPr approval" + ); + + // Queue the approval. The description surfaces the action and PR number + // on the dashboard card so the operator has context without opening the + // forge PR. + let description = format!("PR #{pr_number} on {full_name} ({action})"); + if let Err(e) = crate::socket_server::submit_merge_config_pr( + &state.coord, + agent, + pr_number, + Some(&description), + "forge", // submitter — identifies the webhook path in the audit trail + ) + .await + { + tracing::warn!( + %agent, %pr_number, error = ?e, + "webhook/config-pr: failed to queue MergeConfigPr approval" + ); + } + + (StatusCode::OK, "ok").into_response() +} diff --git a/hive-c0re/src/forge/mod.rs b/hive-c0re/src/forge/mod.rs index 78da1236..3a98c5f4 100644 --- a/hive-c0re/src/forge/mod.rs +++ b/hive-c0re/src/forge/mod.rs @@ -69,7 +69,7 @@ pub(crate) fn forge_git_url(token: &str, repo: &str) -> String { /// reach *another* agent's config. `main` is fast-forward-only — hive-c0re /// never force-pushes; the `push_config` mirror runs best-effort until the /// PR-merge flow retires it. -const CONFIG_ORG: &str = "agent-configs"; +pub(crate) const CONFIG_ORG: &str = "agent-configs"; /// Forgejo org hosting the operator-curated shared docs/skills repo /// that every agent gets read-only access to. Agents use it as a /// common reference without the operator having to bake content into @@ -294,3 +294,70 @@ pub async fn ensure_all() { sync_agent(name, core_token.as_deref()).await; } } + +/// Ensure a Forgejo pull_request org-webhook for `agent-configs` exists and +/// points at hive-c0re's `/webhook/config-pr` endpoint. Idempotent — lists +/// existing hooks first and skips creation when one is already targeting the +/// correct URL. `dashboard_port` is the TCP port hive-c0re's dashboard listens +/// on (default 7000); the webhook URL is +/// `http://127.0.0.1:/webhook/config-pr`. +/// +/// An org-level hook covers every repo in `agent-configs` automatically, +/// so no per-repo setup is needed as new agents are provisioned. +/// +/// Called at startup alongside `knowledge::ensure_webhook`. No-op when the +/// core token is absent (forge not yet provisioned). +pub async fn ensure_config_pr_webhook(core_token: &str, dashboard_port: u16) -> Result<()> { + use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType}; + use std::collections::BTreeMap; + + const HTTP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); + + let target_url = format!("http://127.0.0.1:{dashboard_port}/webhook/config-pr"); + let client = api(core_token)?; + + // List existing org hooks — skip creation if ours is already there. + // Best-effort: a listing failure falls through to the create attempt. + let listed = tokio::time::timeout(HTTP_TIMEOUT, client.org_list_hooks(CONFIG_ORG).all()) + .await + .map_err(anyhow::Error::from) + .and_then(|r| r.map_err(anyhow::Error::from)); + match listed { + Ok(hooks) => { + let already_exists = hooks.iter().any(|h| { + h.config + .as_ref() + .and_then(|c| c.get("url")) + .map(String::as_str) + == Some(target_url.as_str()) + }); + if already_exists { + tracing::debug!(%target_url, "forge: config-pr webhook already configured"); + return Ok(()); + } + } + Err(e) => { + tracing::debug!(error = %e, "forge: listing config-pr hooks failed; attempting create"); + } + } + + let hook = CreateHookOption { + active: Some(true), + authorization_header: None, + branch_filter: None, + config: CreateHookOptionConfig { + content_type: "json".to_owned(), + url: Url::parse(&target_url).context("parse config-pr webhook target url")?, + additional: BTreeMap::new(), + }, + events: Some(vec!["pull_request".to_owned()]), + r#type: CreateHookOptionType::Forgejo, + }; + tokio::time::timeout(HTTP_TIMEOUT, client.org_create_hook(CONFIG_ORG, hook)) + .await + .map_err(anyhow::Error::from) + .and_then(|r| r.map_err(anyhow::Error::from)) + .with_context(|| format!("create config-pr webhook on org {CONFIG_ORG}"))?; + tracing::info!(%target_url, "forge: config-pr webhook created on org {CONFIG_ORG}"); + Ok(()) +} diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 8efa4fcc..07421b5b 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -303,17 +303,22 @@ async fn cmd_serve( tokio::spawn(async move { forge::ensure_all().await; }); - // Knowledge webhook setup: ensure the Forgejo push webhook for - // `internal/knowledge` exists so `pull()` fires on merge. Runs - // after forge::ensure_all so the core token + repo are present. + // Webhook setup: ensure Forgejo webhooks are registered for both + // `internal/knowledge` (push → git pull) and the `agent-configs` org + // (pull_request → queue MergeConfigPr approval). Both run after + // forge::ensure_all so the core token + repos + org are present. // No-op when the core token or forge are absent. let webhook_port = dashboard_port; tokio::spawn(async move { - if let Some(token) = forge::core_token() - && let Err(e) = knowledge::ensure_webhook(&token, webhook_port).await - { + let Some(token) = forge::core_token() else { + return; + }; + if let Err(e) = knowledge::ensure_webhook(&token, webhook_port).await { tracing::warn!(error = ?e, "knowledge: ensure_webhook failed"); } + if let Err(e) = forge::ensure_config_pr_webhook(&token, webhook_port).await { + tracing::warn!(error = ?e, "forge: ensure_config_pr_webhook failed"); + } }); // Knowledge periodic pull: hourly fallback in case the webhook is // missed (e.g. hive-c0re was down during a push). First fires at diff --git a/hive-c0re/src/socket_server/config_approvals.rs b/hive-c0re/src/socket_server/config_approvals.rs index 76ede8a0..37bdc2e7 100644 --- a/hive-c0re/src/socket_server/config_approvals.rs +++ b/hive-c0re/src/socket_server/config_approvals.rs @@ -1,8 +1,13 @@ //! Config-approval request handlers: `RequestInitConfig` / -//! `RequestApplyCommit` / `RequestMergeConfigPr` / `RequestUpdateMetaInputs`, +//! `RequestApplyCommit` / `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`). +//! +//! `submit_merge_config_pr` is called from the dashboard webhook handler +//! (`dashboard::webhook`) — agents no longer need an MCP tool for this; +//! opening a config PR on `agent-configs/` is enough to trigger +//! hive-c0re's webhook-driven queue path. use std::sync::Arc; @@ -107,35 +112,6 @@ 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. @@ -146,7 +122,7 @@ pub(super) async fn handle_request_merge_config_pr( /// 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( +pub(crate) async fn submit_merge_config_pr( coord: &Arc, agent: &str, pr_number: u64, @@ -172,7 +148,7 @@ async fn submit_merge_config_pr( { anyhow::bail!( "PR #{pr_number} on {repo} is closed or already merged — \ - request_merge_config_pr requires an open PR" + merge_config_pr requires an open PR" ); } // Fetch the current PR head sha — becomes the "reviewed" sha. diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index fb3723da..9f0b78e6 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -25,12 +25,12 @@ mod lifecycle_handlers; mod reminders; mod schedules; +pub(crate) use config_approvals::{submit_init_config, submit_merge_config_pr}; 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_merge_config_pr, - handle_request_update_meta_inputs, + handle_request_apply_commit, handle_request_init_config, handle_request_update_meta_inputs, }; use lifecycle_handlers::{ handle_kill, handle_list_descendants, handle_restart, handle_start, handle_update, @@ -579,23 +579,6 @@ 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 4c0ca7a8..dd95e010 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -788,20 +788,6 @@ 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, @@ -1215,7 +1201,6 @@ impl ToolGroup { Self::Approvals => &[ "request_init_config", "request_apply_commit", - "request_merge_config_pr", "request_update_meta_inputs", ], Self::Scheduling => &[ @@ -1318,7 +1303,7 @@ impl ToolGroup { "kill, start, restart, update, list_containers — container lifecycle (privileged)" } Self::Approvals => { - "request_init_config, request_apply_commit, request_merge_config_pr, request_update_meta_inputs — config change flow (privileged)" + "request_init_config, request_apply_commit, request_update_meta_inputs — config change flow (privileged)" } Self::Scheduling => { "request_schedule_prompt and related — operator-visible scheduled prompts (privileged)" diff --git a/libnull.rlib b/libnull.rlib new file mode 100644 index 0000000000000000000000000000000000000000..a6c2cdcfcb983f046bfa6f83da250d6b7aab7a38 GIT binary patch literal 5442 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 H6i77y_?zfR literal 0 HcmV?d00001