//! Operations that are exposed through more than one surface (the host admin //! socket *and* the dashboard's POST endpoints). Each function takes a //! `&Coordinator` and the request parameters; callers stitch the response //! shape they want (HTTP redirect vs JSON). use std::sync::Arc; use anyhow::{Result, bail}; use hive_sh4re::{ApprovalKind, ApprovalStatus, HelperEvent, MANAGER_AGENT}; use crate::coordinator::{Coordinator, TransientKind}; use crate::lifecycle::{self, MANAGER_NAME}; /// Approve a pending request and run the underlying action. Dispatches on the /// approval kind: /// - `ApplyCommit`: read agent.nix at the approval's commit from the proposed /// repo, copy into the applied repo, commit there, rebuild the container. /// Synchronous — returns once the rebuild completes. /// - `Spawn`: create + start a brand-new sub-agent container. Runs in a /// background task so the operator's approve click returns immediately; /// the dashboard surfaces a transient `Spawning` state until the container /// is up. On failure, the approval is marked failed. /// /// In all cases an `ApprovalResolved` helper event lands in the manager's /// inbox when the work resolves. pub async fn approve(coord: Arc, id: i64) -> Result<()> { let approval = coord.approvals.mark_approved(id)?; tracing::info!( %approval.id, %approval.agent, kind = ?approval.kind, %approval.commit_ref, "approval: running action", ); let agent_dir = coord.ensure_runtime(&approval.agent)?; let proposed_dir = Coordinator::agent_proposed_dir(&approval.agent); let applied_dir = Coordinator::agent_applied_dir(&approval.agent); let claude_dir = Coordinator::agent_claude_dir(&approval.agent); let notes_dir = Coordinator::agent_notes_dir(&approval.agent); match approval.kind { ApprovalKind::ApplyCommit => { let result = async { lifecycle::apply_commit(&applied_dir, &proposed_dir, &approval.commit_ref).await?; lifecycle::rebuild( &approval.agent, &coord.hyperhive_flake, &agent_dir, &applied_dir, &claude_dir, ¬es_dir, coord.dashboard_port, ) .await } .await; finish_approval(&coord, &approval, result) } ApprovalKind::Spawn => { // Run the spawn in the background so the approve POST returns // immediately. The dashboard reads `transient` to render a spinner. coord.set_transient(&approval.agent, TransientKind::Spawning); let coord_bg = coord.clone(); let approval_bg = approval.clone(); tokio::spawn(async move { let agent_bg = approval_bg.agent.clone(); let result = lifecycle::spawn( &approval_bg.agent, &coord_bg.hyperhive_flake, &agent_dir, &proposed_dir, &applied_dir, &claude_dir, ¬es_dir, coord_bg.dashboard_port, ) .await; coord_bg.clear_transient(&agent_bg); if let Err(e) = finish_approval(&coord_bg, &approval_bg, result) { tracing::warn!(agent = %agent_bg, error = ?e, "spawn approval failed"); } }); Ok(()) } } } fn finish_approval( coord: &Coordinator, approval: &hive_sh4re::Approval, result: Result<()>, ) -> Result<()> { let (status, note, ok) = match &result { Ok(()) => (ApprovalStatus::Approved, None, true), Err(e) => { let note = format!("{e:#}"); let _ = coord.approvals.mark_failed(approval.id, ¬e); (ApprovalStatus::Failed, Some(note), false) } }; coord.notify_manager(&HelperEvent::ApprovalResolved { id: approval.id, agent: approval.agent.clone(), commit_ref: approval.commit_ref.clone(), status, note: note.clone(), }); // For spawn/rebuild approvals, also surface the underlying action so // the manager knows whether the container actually came up. The // ApprovalResolved event already carries the same `ok` signal but // separating it lets the manager react to the lifecycle change // without having to special-case approvals. match approval.kind { ApprovalKind::Spawn => coord.notify_manager(&HelperEvent::Spawned { agent: approval.agent.clone(), ok, note, }), ApprovalKind::ApplyCommit => coord.notify_manager(&HelperEvent::Rebuilt { agent: approval.agent.clone(), ok, note, }), } result } /// Tear down a sub-agent container. By default this is non-destructive to /// persistent state: the proposed/applied config repos and the Claude /// credentials dir under `/var/lib/hyperhive/{agents,applied}//` are /// kept, so recreating an agent of the same name reuses prior config + creds /// (no re-login). The ephemeral runtime dir under `/run/hyperhive/agents/` /// is cleared because its contents (the mcp socket) don't survive restarts /// anyway. A future `--purge` path can wipe state when the operator opts in. /// Refuses the manager (declarative; would fight with the host's nixos config). pub async fn destroy(coord: &Coordinator, name: &str) -> Result<()> { if name == MANAGER_NAME || name == MANAGER_AGENT { bail!("refusing to destroy the manager ({name})"); } tracing::info!(%name, "destroy"); lifecycle::destroy(name).await?; coord.unregister_agent(name); let runtime = Coordinator::agent_dir(name); if runtime.exists() { let _ = std::fs::remove_dir_all(&runtime); } let _ = coord .approvals .fail_pending_for_agent(name, "agent destroyed"); coord.notify_manager(&HelperEvent::Destroyed { agent: name.to_owned(), }); Ok(()) } pub fn deny(coord: &Coordinator, id: i64) -> Result<()> { let approval = coord.approvals.get(id)?; coord.approvals.mark_denied(id)?; tracing::info!(%id, "approval denied"); if let Some(a) = approval { coord.notify_manager(&HelperEvent::ApprovalResolved { id: a.id, agent: a.agent, commit_ref: a.commit_ref, status: ApprovalStatus::Denied, note: None, }); } Ok(()) }