address review: drop backwards-compat request/response aliases, use canonical names

This commit is contained in:
damocles 2026-07-19 15:12:14 +02:00 committed by mara
commit 144912f8e0
14 changed files with 183 additions and 229 deletions

View file

@ -1,7 +1,7 @@
//! Embedded MCP server. Claude Code (running inside the agent container) //! Embedded MCP server. Claude Code (running inside the agent container)
//! connects to this over streamable-HTTP via `--mcp-config` (the long-lived //! connects to this over streamable-HTTP via `--mcp-config` (the long-lived
//! `hive-mcp-http` daemon); tool calls land here and are translated to //! `hive-mcp-http` daemon); tool calls land here and are translated to
//! `AgentRequest::*` / `ManagerRequest::*` against hyperhive's own //! `Request::*` against hyperhive's own
//! per-container unix socket at `/run/hive/mcp.sock`. //! per-container unix socket at `/run/hive/mcp.sock`.
//! //!
//! Two protocols, two surfaces: //! Two protocols, two surfaces:
@ -99,11 +99,9 @@ where
/// Unified MCP tool surface for both sub-agent and manager roles. /// Unified MCP tool surface for both sub-agent and manager roles.
/// ///
/// `AgentRequest = ManagerRequest = Request` and `AgentResponse = /// Both sockets speak the same `Request` / `Response` wire, so a single
/// ManagerResponse = Response` are type aliases in hive-sh4re, so a single
/// `dispatch` call covers both sockets — the only real difference is which /// `dispatch` call covers both sockets — the only real difference is which
/// socket path is used and which tools the flavor enables. /// socket path is used and which tools the flavor enables.
///
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AgentServer { pub struct AgentServer {
socket: PathBuf, socket: PathBuf,
@ -119,8 +117,8 @@ impl AgentServer {
/// `Response` plus the retry count so tool handlers can annotate their /// `Response` plus the retry count so tool handlers can annotate their
/// result (see `annotate_retries`). /// result (see `annotate_retries`).
/// ///
/// `AgentRequest` / `ManagerRequest` / `Request` are all the same type /// Both sockets speak the same `Request` type, so this single method
/// (hive-sh4re type aliases), so this single method covers both sockets. /// covers both.
async fn dispatch( async fn dispatch(
&self, &self,
req: hive_agent_sock::Request, req: hive_agent_sock::Request,

View file

@ -21,9 +21,6 @@ fn default_true() -> bool {
/// Unified request enum for both agent and manager sockets. The agent's /// Unified request enum for both agent and manager sockets. The agent's
/// identity is the socket it arrived on. Privileged variants are marked /// identity is the socket it arrived on. Privileged variants are marked
/// `*(privileged)*` — an agent socket returns `Err` for them server-side. /// `*(privileged)*` — an agent socket returns `Err` for them server-side.
///
/// `AgentRequest` and `ManagerRequest` are type aliases for this enum;
/// existing callers continue to compile unchanged.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")] #[serde(tag = "cmd", rename_all = "snake_case")]
pub enum Request { pub enum Request {
@ -308,16 +305,8 @@ pub enum Request {
}, },
} }
/// Backwards-compatible aliases. Both sockets now speak the unified `Request`
/// / `Response` wire; the server-side privilege gate rejects privileged
/// variants on agent sockets with `Err { message: "privileged variant..." }`.
pub type AgentRequest = Request;
pub type ManagerRequest = Request;
/// Unified response enum for both agent and manager sockets. Privileged /// Unified response enum for both agent and manager sockets. Privileged
/// variants (`Logs`, `Schedules`) are never returned on agent sockets. /// variants (`Logs`, `Schedules`) are never returned on agent sockets.
///
/// `AgentResponse` and `ManagerResponse` are type aliases for this enum.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum Response { pub enum Response {
@ -408,7 +397,3 @@ pub enum Response {
/// the agent's next start. /// the agent's next start.
GracefulStop, GracefulStop,
} }
/// Backwards-compatible response aliases.
pub type AgentResponse = Response;
pub type ManagerResponse = Response;

View file

@ -13,7 +13,7 @@ use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use hive_agent_sock::{AgentRequest, AgentResponse}; use hive_agent_sock::{Request, Response};
/// Per-agent MCP socket, bind-mounted from the host into every container. /// Per-agent MCP socket, bind-mounted from the host into every container.
const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock"; const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock";
@ -50,17 +50,17 @@ async fn main() -> Result<()> {
} else { } else {
cli.body cli.body
}; };
let resp: AgentResponse = client::request( let resp: Response = client::request(
&cli.socket, &cli.socket,
&AgentRequest::Wake { &Request::Wake {
from: cli.from, from: cli.from,
body, body,
}, },
) )
.await?; .await?;
match resp { match resp {
AgentResponse::Ok => Ok(()), Response::Ok => Ok(()),
AgentResponse::Err { message } => anyhow::bail!("wake: {message}"), Response::Err { message } => anyhow::bail!("wake: {message}"),
other => anyhow::bail!("wake: unexpected response {other:?}"), other => anyhow::bail!("wake: unexpected response {other:?}"),
} }
} }

View file

@ -42,7 +42,7 @@ use crate::login::LoginState;
use crate::turn_stats::TurnStats; use crate::turn_stats::TurnStats;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use hive_agent_sock::{AgentRequest, AgentResponse}; use hive_agent_sock::{Request, Response};
use hive_sh4re::{HelperEvent, SYSTEM_SENDER}; use hive_sh4re::{HelperEvent, SYSTEM_SENDER};
#[derive(Parser)] #[derive(Parser)]
@ -191,9 +191,8 @@ fn graceful_stop_message() -> hive_sh4re::DeliveredMessage {
// ---------- surface trait ---------- // ---------- surface trait ----------
/// What a `Recv` long-poll returned. Decoupled from the per-role /// What a `Recv` long-poll returned. Decoupled from the `Response`
/// Response enum so `serve_loop` can pattern-match without seeing /// enum so `serve_loop` can pattern-match without seeing it directly.
/// either `AgentResponse` or `ManagerResponse` directly.
enum RecvOutcome { enum RecvOutcome {
/// Long-poll returned at least one message; first one is detached. /// Long-poll returned at least one message; first one is detached.
Message(hive_sh4re::DeliveredMessage), Message(hive_sh4re::DeliveredMessage),
@ -213,7 +212,7 @@ enum RecvOutcome {
/// Wire surface abstraction. `AgentSurface` is the only impl — the trait /// Wire surface abstraction. `AgentSurface` is the only impl — the trait
/// exists to keep the turn loop generic and testable. Every function that /// exists to keep the turn loop generic and testable. Every function that
/// talks to the broker goes through this so there are zero hard-coded /// talks to the broker goes through this so there are zero hard-coded
/// `AgentRequest` / `AgentResponse` references in the turn loop itself. /// `Request` / `Response` references in the turn loop itself.
trait Surface { trait Surface {
/// Ack the in-flight turn. Logs warnings on transport/broker /// Ack the in-flight turn. Logs warnings on transport/broker
/// errors but never propagates — turn loop continues either way. /// errors but never propagates — turn loop continues either way.
@ -252,17 +251,17 @@ trait Surface {
// ---------- AgentSurface ---------- // ---------- AgentSurface ----------
/// Zero-sized type tag for the agent wire surface. /// Zero-sized type tag for the agent wire surface.
/// Talks `AgentRequest` / `AgentResponse`. /// Talks `Request` / `Response`.
struct AgentSurface; struct AgentSurface;
/// Issue an `Ok`-expecting fire-and-forget broker request, logging any /// Issue an `Ok`-expecting fire-and-forget broker request, logging any
/// rejection / unexpected response / transport error under `label`. Shared by /// rejection / unexpected response / transport error under `label`. Shared by
/// the `Surface` methods that don't need the reply (`ack_turn`, /// the `Surface` methods that don't need the reply (`ack_turn`,
/// `requeue_inflight`, `graceful_stop_complete`). /// `requeue_inflight`, `graceful_stop_complete`).
async fn fire_and_forget(socket: &Path, req: AgentRequest, label: &str) { async fn fire_and_forget(socket: &Path, req: Request, label: &str) {
match client::request::<_, AgentResponse>(socket, &req).await { match client::request::<_, Response>(socket, &req).await {
Ok(AgentResponse::Ok) => {} Ok(Response::Ok) => {}
Ok(AgentResponse::Err { message }) => { Ok(Response::Err { message }) => {
tracing::warn!(%message, "{label} rejected by broker"); tracing::warn!(%message, "{label} rejected by broker");
} }
Ok(other) => tracing::warn!(?other, "{label} unexpected response"), Ok(other) => tracing::warn!(?other, "{label} unexpected response"),
@ -272,55 +271,53 @@ async fn fire_and_forget(socket: &Path, req: AgentRequest, label: &str) {
impl Surface for AgentSurface { impl Surface for AgentSurface {
async fn ack_turn(socket: &Path) { async fn ack_turn(socket: &Path) {
fire_and_forget(socket, AgentRequest::AckTurn, "ack_turn").await; fire_and_forget(socket, Request::AckTurn, "ack_turn").await;
} }
async fn requeue_inflight(socket: &Path) { async fn requeue_inflight(socket: &Path) {
fire_and_forget(socket, AgentRequest::RequeueInflight, "requeue_inflight").await; fire_and_forget(socket, Request::RequeueInflight, "requeue_inflight").await;
} }
async fn graceful_stop_complete(socket: &Path) { async fn graceful_stop_complete(socket: &Path) {
fire_and_forget( fire_and_forget(
socket, socket,
AgentRequest::GracefulStopComplete, Request::GracefulStopComplete,
"graceful_stop_complete", "graceful_stop_complete",
) )
.await; .await;
} }
async fn inbox_unread(socket: &Path) -> u64 { async fn inbox_unread(socket: &Path) -> u64 {
match client::request::<_, AgentResponse>(socket, &AgentRequest::Status).await { match client::request::<_, Response>(socket, &Request::Status).await {
Ok(AgentResponse::Status { unread }) => unread, Ok(Response::Status { unread }) => unread,
_ => 0, _ => 0,
} }
} }
async fn post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) { async fn post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) {
let threads = match client::request::<_, AgentResponse>( let threads =
match client::request::<_, Response>(socket, &Request::GetLooseEnds { agent: None })
.await
{
Ok(Response::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
_ => None,
};
let reminders = match client::request::<_, Response>(
socket, socket,
&AgentRequest::GetLooseEnds { agent: None }, &Request::CountPendingReminders { agent: None },
) )
.await .await
{ {
Ok(AgentResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(), Ok(Response::PendingRemindersCount { count }) => Some(count),
_ => None,
};
let reminders = match client::request::<_, AgentResponse>(
socket,
&AgentRequest::CountPendingReminders { agent: None },
)
.await
{
Ok(AgentResponse::PendingRemindersCount { count }) => Some(count),
_ => None, _ => None,
}; };
(threads, reminders) (threads, reminders)
} }
async fn send_to_parent(socket: &Path, body: String) { async fn send_to_parent(socket: &Path, body: String) {
let res = client::request::<_, AgentResponse>( let res = client::request::<_, Response>(
socket, socket,
&AgentRequest::Send { &Request::Send {
to: hive_sh4re::PARENT_RECIPIENT.into(), to: hive_sh4re::PARENT_RECIPIENT.into(),
body, body,
in_reply_to: None, in_reply_to: None,
@ -333,22 +330,22 @@ impl Surface for AgentSurface {
} }
async fn recv_next(socket: &Path) -> RecvOutcome { async fn recv_next(socket: &Path) -> RecvOutcome {
let recv: Result<AgentResponse> = client::request( let recv: Result<Response> = client::request(
socket, socket,
&AgentRequest::Recv { &Request::Recv {
wait_seconds: Some(180), wait_seconds: Some(180),
max: None, max: None,
}, },
) )
.await; .await;
match recv { match recv {
Ok(AgentResponse::Messages { messages, .. }) if !messages.is_empty() => { Ok(Response::Messages { messages, .. }) if !messages.is_empty() => {
let first = messages.into_iter().next().expect("checked non-empty"); let first = messages.into_iter().next().expect("checked non-empty");
RecvOutcome::Message(first) RecvOutcome::Message(first)
} }
Ok(AgentResponse::Messages { .. }) => RecvOutcome::Empty, Ok(Response::Messages { .. }) => RecvOutcome::Empty,
Ok(AgentResponse::GracefulStop) => RecvOutcome::GracefulStop, Ok(Response::GracefulStop) => RecvOutcome::GracefulStop,
Ok(AgentResponse::Err { message }) => { Ok(Response::Err { message }) => {
tracing::warn!(%message, "recv error"); tracing::warn!(%message, "recv error");
RecvOutcome::TransportError RecvOutcome::TransportError
} }

View file

@ -760,7 +760,7 @@ pub(crate) async fn send_wake(
); );
} }
} }
let req = hive_agent_sock::AgentRequest::Wake { let req = hive_agent_sock::Request::Wake {
from: format!("bash-task-{id}"), from: format!("bash-task-{id}"),
body, body,
}; };

View file

@ -23,7 +23,7 @@ pub const MESSAGE_MAX_BYTES: usize = 4096;
/// Validate that `body` fits under [`MESSAGE_MAX_BYTES`]. Returns a /// Validate that `body` fits under [`MESSAGE_MAX_BYTES`]. Returns a
/// caller-ready error string (caller wraps in /// caller-ready error string (caller wraps in
/// `AgentResponse::Err`/`ManagerResponse::Err`) on failure. /// `Response::Err`) on failure.
/// ///
/// `label` shows up in the error message verbatim — pass a short /// `label` shows up in the error message verbatim — pass a short
/// noun like `"send"`, `"question"`, `"broadcast"` so the model can /// noun like `"send"`, `"question"`, `"broadcast"` so the model can

View file

@ -77,7 +77,7 @@ pub(super) async fn post_kill(
// host-side approval queue without the manager up, and // host-side approval queue without the manager up, and
// operator-driven meta-input updates work from the dashboard // operator-driven meta-input updates work from the dashboard
// either way. The MCP-surface self-kill guard in // either way. The MCP-surface self-kill guard in
// `socket_server.rs::ManagerRequest::Kill` stays in place: a // `socket_server.rs::Request::Kill` stays in place: a
// manager calling Kill on its own container is self-suicide // manager calling Kill on its own container is self-suicide
// mid-call, not a legitimate operator action. // mid-call, not a legitimate operator action.
submit::stop( submit::stop(

View file

@ -1,8 +1,8 @@
//! Loose-ends aggregator. Walks the `approvals` + `operator_questions` //! Loose-ends aggregator. Walks the `approvals` + `operator_questions`
//! tables once per call and assembles a `Vec<LooseEnd>` for either //! tables once per call and assembles a `Vec<LooseEnd>` for either
//! a single agent (`for_agent`) or the whole hive (`hive_wide`). Both //! a single agent (`for_agent`) or the whole hive (`hive_wide`).
//! `AgentRequest::GetLooseEnds` and `ManagerRequest::GetLooseEnds` //! `Request::GetLooseEnds` from either the agent or manager socket
//! land here so the routing logic + age-seconds derivation stay in //! lands here so the routing logic + age-seconds derivation stay in
//! one place. //! one place.
//! //!
//! Call frequency is low (an agent doing self-introspection between //! Call frequency is low (an agent doing self-introspection between

View file

@ -9,7 +9,7 @@
use std::sync::Arc; use std::sync::Arc;
use hive_agent_sock::AgentResponse; use hive_agent_sock::Response;
use super::require_new_child; use super::require_new_child;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
@ -24,14 +24,14 @@ pub(super) fn handle_request_init_config(
agent: &str, agent: &str,
name: &str, name: &str,
description: Option<String>, description: Option<String>,
) -> AgentResponse { ) -> Response {
if let Some(err) = require_new_child(agent, name, "request_init_config for") { if let Some(err) = require_new_child(agent, name, "request_init_config for") {
return err; return err;
} }
tracing::info!(%agent, %name, "request_init_config"); tracing::info!(%agent, %name, "request_init_config");
match submit_init_config(coord, name, Some(agent), description) { match submit_init_config(coord, name, Some(agent), description) {
Ok(_id) => AgentResponse::Ok, Ok(_id) => Response::Ok,
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
} }
@ -45,7 +45,7 @@ pub(super) fn handle_request_update_meta_inputs(
requester: &str, requester: &str,
inputs: &[String], inputs: &[String],
description: Option<&str>, description: Option<&str>,
) -> AgentResponse { ) -> Response {
let label = if inputs.is_empty() { let label = if inputs.is_empty() {
"all inputs".to_string() "all inputs".to_string()
} else { } else {
@ -67,7 +67,7 @@ pub(super) fn handle_request_update_meta_inputs(
{ {
Ok(id) => id, Ok(id) => id,
Err(e) => { Err(e) => {
return AgentResponse::Err { return Response::Err {
message: format!("queue update_meta_inputs approval: {e:#}"), message: format!("queue update_meta_inputs approval: {e:#}"),
}; };
} }
@ -81,7 +81,7 @@ pub(super) fn handle_request_update_meta_inputs(
description: description.map(str::to_owned), description: description.map(str::to_owned),
pr_number: None, pr_number: None,
}); });
AgentResponse::Ok Response::Ok
} }
/// Submit-time half of the PR-merge flow: fetch the PR head sha from the /// Submit-time half of the PR-merge flow: fetch the PR head sha from the

View file

@ -5,18 +5,14 @@
use std::sync::Arc; use std::sync::Arc;
use hive_agent_sock::AgentResponse; use hive_agent_sock::Response;
use super::require_descendant; use super::require_descendant;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
/// `Start` — start a container, kicking its next turn. The caller must be an /// `Start` — start a container, kicking its next turn. The caller must be an
/// ancestor of `name` in the topology (the root covers every agent). /// ancestor of `name` in the topology (the root covers every agent).
pub(super) async fn handle_start( pub(super) async fn handle_start(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Response {
coord: &Arc<Coordinator>,
agent: &str,
name: &str,
) -> AgentResponse {
if let Some(err) = require_descendant(agent, name, "start") { if let Some(err) = require_descendant(agent, name, "start") {
return err; return err;
} }
@ -31,18 +27,14 @@ pub(super) async fn handle_start(
format!("agent `{agent}` start tool"), format!("agent `{agent}` start tool"),
) )
.await; .await;
AgentResponse::Ok Response::Ok
} }
/// `Restart` — enqueue a restart for a container. The caller must be an /// `Restart` — enqueue a restart for a container. The caller must be an
/// ancestor of `name` in the topology. The infra-container branch is /// ancestor of `name` in the topology. The infra-container branch is
/// orthogonal: it is gated on the `infra_admin` capability and audited, so it /// orthogonal: it is gated on the `infra_admin` capability and audited, so it
/// stays ahead of the topology guard. /// stays ahead of the topology guard.
pub(super) async fn handle_restart( pub(super) async fn handle_restart(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Response {
coord: &Arc<Coordinator>,
agent: &str,
name: &str,
) -> AgentResponse {
// Infra-container restart: an agent holding the `infra_admin` // Infra-container restart: an agent holding the `infra_admin`
// capability can restart a hive infrastructure container (hive-ci / // capability can restart a hive infrastructure container (hive-ci /
// hive-gateway / hive-forge / hive-matrix) by passing its name to the // hive-gateway / hive-forge / hive-matrix) by passing its name to the
@ -63,7 +55,7 @@ pub(super) async fn handle_restart(
format!("agent `{agent}` restart tool"), format!("agent `{agent}` restart tool"),
) )
.await; .await;
AgentResponse::Ok Response::Ok
} }
/// Restart a hive infrastructure container on behalf of an agent that /// Restart a hive infrastructure container on behalf of an agent that
@ -75,7 +67,7 @@ async fn handle_restart_infra(
coord: &Arc<Coordinator>, coord: &Arc<Coordinator>,
agent: &str, agent: &str,
container: hive_priv_sock::InfraContainer, container: hive_priv_sock::InfraContainer,
) -> AgentResponse { ) -> Response {
let name = container.unit_name(); let name = container.unit_name();
// Record the attempt in the operator-visible privileged-action audit // Record the attempt in the operator-visible privileged-action audit
// trail, then emit a live `AuditEntryAdded` so the dashboard audit view // trail, then emit a live `AuditEntryAdded` so the dashboard audit view
@ -97,7 +89,7 @@ async fn handle_restart_infra(
crate::audit_log::AuditOutcome::Err, crate::audit_log::AuditOutcome::Err,
Some("denied: missing infra_admin capability"), Some("denied: missing infra_admin capability"),
); );
return AgentResponse::Err { return Response::Err {
message: format!( message: format!(
"restarting infra container `{name}` requires the `infra_admin` capability" "restarting infra container `{name}` requires the `infra_admin` capability"
), ),
@ -107,23 +99,19 @@ async fn handle_restart_infra(
match crate::priv_client::restart_infra_container(container).await { match crate::priv_client::restart_infra_container(container).await {
Ok(()) => { Ok(()) => {
audit(crate::audit_log::AuditOutcome::Ok, None); audit(crate::audit_log::AuditOutcome::Ok, None);
AgentResponse::Ok Response::Ok
} }
Err(e) => { Err(e) => {
let msg = format!("{e:#}"); let msg = format!("{e:#}");
audit(crate::audit_log::AuditOutcome::Err, Some(&msg)); audit(crate::audit_log::AuditOutcome::Err, Some(&msg));
AgentResponse::Err { message: msg } Response::Err { message: msg }
} }
} }
} }
/// `Kill` — kill a container, unregister it, notify the manager. The caller /// `Kill` — kill a container, unregister it, notify the manager. The caller
/// must be an ancestor of `name` in the topology. /// must be an ancestor of `name` in the topology.
pub(super) async fn handle_kill( pub(super) async fn handle_kill(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Response {
coord: &Arc<Coordinator>,
agent: &str,
name: &str,
) -> AgentResponse {
if let Some(err) = require_descendant(agent, name, "kill") { if let Some(err) = require_descendant(agent, name, "kill") {
return err; return err;
} }
@ -144,9 +132,9 @@ pub(super) async fn handle_kill(
coord.notify_manager(&hive_sh4re::HelperEvent::Killed { coord.notify_manager(&hive_sh4re::HelperEvent::Killed {
agent: name.to_owned(), agent: name.to_owned(),
}); });
AgentResponse::Ok Response::Ok
} }
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
} }
@ -154,7 +142,7 @@ pub(super) async fn handle_kill(
/// `Update` — enqueue a rebuild for a container. The caller must be an /// `Update` — enqueue a rebuild for a container. The caller must be an
/// ancestor of `name` in the topology. /// ancestor of `name` in the topology.
pub(super) fn handle_update(coord: &Arc<Coordinator>, agent: &str, name: &str) -> AgentResponse { pub(super) fn handle_update(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Response {
if let Some(err) = require_descendant(agent, name, "rebuild") { if let Some(err) = require_descendant(agent, name, "rebuild") {
return err; return err;
} }
@ -165,12 +153,12 @@ pub(super) fn handle_update(coord: &Arc<Coordinator>, agent: &str, name: &str) -
crate::job_queue::Source::Manual, crate::job_queue::Source::Manual,
format!("agent `{agent}` update tool"), format!("agent `{agent}` update tool"),
); );
AgentResponse::Ok Response::Ok
} }
/// `ListDescendants` — every topological descendant of `agent` with /// `ListDescendants` — every topological descendant of `agent` with
/// its running/stopped state, parents before children. /// its running/stopped state, parents before children.
pub(super) async fn handle_list_descendants(agent: &str) -> AgentResponse { pub(super) async fn handle_list_descendants(agent: &str) -> Response {
tracing::debug!(%agent, "agent: list descendants"); tracing::debug!(%agent, "agent: list descendants");
// All containers known to nixos-container (running only). // All containers known to nixos-container (running only).
let running_set: std::collections::HashSet<String> = match crate::lifecycle::list().await { let running_set: std::collections::HashSet<String> = match crate::lifecycle::list().await {
@ -182,7 +170,7 @@ pub(super) async fn handle_list_descendants(agent: &str) -> AgentResponse {
}) })
.collect(), .collect(),
Err(e) => { Err(e) => {
return AgentResponse::Err { return Response::Err {
message: format!("list containers failed: {e:#}"), message: format!("list containers failed: {e:#}"),
}; };
} }
@ -203,5 +191,5 @@ pub(super) async fn handle_list_descendants(agent: &str) -> AgentResponse {
hive_sh4re::ContainerInfo { name, running } hive_sh4re::ContainerInfo { name, running }
}) })
.collect(); .collect();
AgentResponse::Containers { containers } Response::Containers { containers }
} }

View file

@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use hive_agent_sock::{AgentRequest, AgentResponse}; use hive_agent_sock::{Request, Response};
use hive_sh4re::{MANAGER_AGENT, Message}; use hive_sh4re::{MANAGER_AGENT, Message};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream}; use tokio::net::{UnixListener, UnixStream};
@ -146,9 +146,9 @@ async fn serve(stream: UnixStream, agent: String, coord: Arc<Coordinator>) -> Re
if n == 0 { if n == 0 {
return Ok(()); return Ok(());
} }
let resp = match serde_json::from_str::<AgentRequest>(line.trim()) { let resp = match serde_json::from_str::<Request>(line.trim()) {
Ok(req) => dispatch(&req, &agent, &coord).await, Ok(req) => dispatch(&req, &agent, &coord).await,
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("parse error: {e}"), message: format!("parse error: {e}"),
}, },
}; };
@ -557,37 +557,37 @@ fn handle_requeue_inflight(coord: &Arc<Coordinator>, agent: &str) -> hive_agent_
/// queries require the `QueryAgentState` capability; hive-wide orchestration /// queries require the `QueryAgentState` capability; hive-wide orchestration
/// verbs (schedules / meta-inputs) require the matching tool-group (the /// verbs (schedules / meta-inputs) require the matching tool-group (the
/// grantable capability). /// grantable capability).
async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) -> AgentResponse { async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
if let Some(resp) = dispatch_shared(req, agent, coord).await { if let Some(resp) = dispatch_shared(req, agent, coord).await {
return resp; return resp;
} }
match req { match req {
// Lifecycle + config: caller must be an ancestor of the target // Lifecycle + config: caller must be an ancestor of the target
// (a parent owns its whole subtree; the root covers every agent). // (a parent owns its whole subtree; the root covers every agent).
AgentRequest::Start { name } => handle_start(coord, agent, name).await, Request::Start { name } => handle_start(coord, agent, name).await,
AgentRequest::Restart { name } => handle_restart(coord, agent, name).await, Request::Restart { name } => handle_restart(coord, agent, name).await,
AgentRequest::Kill { name } => handle_kill(coord, agent, name).await, Request::Kill { name } => handle_kill(coord, agent, name).await,
AgentRequest::Update { name } => handle_update(coord, agent, name), Request::Update { name } => handle_update(coord, agent, name),
AgentRequest::ListDescendants => handle_list_descendants(agent).await, Request::ListDescendants => handle_list_descendants(agent).await,
AgentRequest::RequestInitConfig { name, description } => { Request::RequestInitConfig { name, description } => {
handle_request_init_config(coord, agent, name, description.clone()) handle_request_init_config(coord, agent, name, description.clone())
} }
// Agent-state queries: own subtree is free; other agents + the // Agent-state queries: own subtree is free; other agents + the
// hive-wide `"*"` sweep require `QueryAgentState`. // hive-wide `"*"` sweep require `QueryAgentState`.
AgentRequest::GetLooseEnds { agent: target } => { Request::GetLooseEnds { agent: target } => {
handle_get_loose_ends(coord, agent, target.as_deref()) handle_get_loose_ends(coord, agent, target.as_deref())
} }
AgentRequest::CountPendingReminders { agent: target } => { Request::CountPendingReminders { agent: target } => {
handle_count_pending_reminders(coord, agent, target.as_deref()) handle_count_pending_reminders(coord, agent, target.as_deref())
} }
AgentRequest::ReminderRollup { Request::ReminderRollup {
since_secs, since_secs,
agent: target, agent: target,
} => handle_reminder_rollup(coord, agent, target.as_deref(), *since_secs), } => handle_reminder_rollup(coord, agent, target.as_deref(), *since_secs),
// Todos (loose-ends v2): in-container subsystems push/clear // Todos (loose-ends v2): in-container subsystems push/clear
// their own; the agent lists / marks its own done. Scoped to the // their own; the agent lists / marks its own done. Scoped to the
// calling agent (the socket identity) — no cross-agent access. // calling agent (the socket identity) — no cross-agent access.
AgentRequest::UpsertTodo { Request::UpsertTodo {
subsystem, subsystem,
key, key,
summary, summary,
@ -600,15 +600,13 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
summary, summary,
source.as_deref(), source.as_deref(),
), ),
AgentRequest::ClearTodo { Request::ClearTodo {
subsystem, subsystem,
key, key,
all, all,
} => handle_clear_todo(coord, agent, subsystem, key.as_deref(), *all), } => handle_clear_todo(coord, agent, subsystem, key.as_deref(), *all),
AgentRequest::ListTodos { subsystem } => { Request::ListTodos { subsystem } => handle_list_todos(coord, agent, subsystem.as_deref()),
handle_list_todos(coord, agent, subsystem.as_deref()) Request::MarkTodoDone { id } => handle_mark_todo_done(coord, agent, *id),
}
AgentRequest::MarkTodoDone { id } => handle_mark_todo_done(coord, agent, *id),
// Orchestration / diagnostics verbs — gated per-verb on tool-group // Orchestration / diagnostics verbs — gated per-verb on tool-group
// membership or topology (see `dispatch_orchestration`). // membership or topology (see `dispatch_orchestration`).
_ => dispatch_orchestration(req, agent, coord).await, _ => dispatch_orchestration(req, agent, coord).await,
@ -621,13 +619,9 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
/// (`scheduling` / `approvals`), or `is_descendant_of` for `get_logs` /// (`scheduling` / `approvals`), or `is_descendant_of` for `get_logs`
/// (a parent reads its subtree's logs). Any other variant is a host-admin / /// (a parent reads its subtree's logs). Any other variant is a host-admin /
/// unknown request invalid on either socket. /// unknown request invalid on either socket.
async fn dispatch_orchestration( async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
req: &AgentRequest,
agent: &str,
coord: &Arc<Coordinator>,
) -> AgentResponse {
match req { match req {
AgentRequest::RequestUpdateMetaInputs { Request::RequestUpdateMetaInputs {
inputs, inputs,
description, description,
} => { } => {
@ -636,19 +630,19 @@ async fn dispatch_orchestration(
} }
handle_request_update_meta_inputs(coord, agent, inputs, description.as_deref()) handle_request_update_meta_inputs(coord, agent, inputs, description.as_deref())
} }
AgentRequest::RequestSchedulePrompt(payload) => { Request::RequestSchedulePrompt(payload) => {
if let Some(err) = require_group(agent, "scheduling", "schedule a prompt") { if let Some(err) = require_group(agent, "scheduling", "schedule a prompt") {
return err; return err;
} }
handle_request_schedule_prompt(coord, agent, payload) handle_request_schedule_prompt(coord, agent, payload)
} }
AgentRequest::CancelSchedule { id, targets } => { Request::CancelSchedule { id, targets } => {
if let Some(err) = require_group(agent, "scheduling", "cancel a schedule") { if let Some(err) = require_group(agent, "scheduling", "cancel a schedule") {
return err; return err;
} }
handle_cancel_schedule(coord, agent, *id, targets.as_deref()) handle_cancel_schedule(coord, agent, *id, targets.as_deref())
} }
AgentRequest::EditSchedule { Request::EditSchedule {
id, id,
body, body,
description, description,
@ -674,19 +668,19 @@ async fn dispatch_orchestration(
}, },
) )
} }
AgentRequest::ListSchedules => { Request::ListSchedules => {
if let Some(err) = require_group(agent, "scheduling", "list schedules") { if let Some(err) = require_group(agent, "scheduling", "list schedules") {
return err; return err;
} }
handle_list_schedules(coord) handle_list_schedules(coord)
} }
AgentRequest::FireScheduleNow { id } => { Request::FireScheduleNow { id } => {
if let Some(err) = require_group(agent, "scheduling", "fire a schedule") { if let Some(err) = require_group(agent, "scheduling", "fire a schedule") {
return err; return err;
} }
handle_fire_schedule_now(coord, agent, *id).await handle_fire_schedule_now(coord, agent, *id).await
} }
AgentRequest::GetLogs { Request::GetLogs {
agent: target, agent: target,
lines, lines,
} => { } => {
@ -696,7 +690,7 @@ async fn dispatch_orchestration(
handle_get_logs(target, *lines).await handle_get_logs(target, *lines).await
} }
// Host-admin-only / unknown variants: never valid on either socket. // Host-admin-only / unknown variants: never valid on either socket.
_ => AgentResponse::Err { _ => Response::Err {
message: "request not handled on this socket".to_owned(), message: "request not handled on this socket".to_owned(),
}, },
} }
@ -708,11 +702,11 @@ async fn dispatch_orchestration(
/// a consequence, with no positional/hardcoded privilege. Returns `Some(Err)` /// a consequence, with no positional/hardcoded privilege. Returns `Some(Err)`
/// to short-circuit the dispatch arm when it isn't, `None` when authorised. /// to short-circuit the dispatch arm when it isn't, `None` when authorised.
/// `action` is the verb phrase for the message (e.g. `"start"`). /// `action` is the verb phrase for the message (e.g. `"start"`).
fn require_descendant(agent: &str, target: &str, action: &str) -> Option<AgentResponse> { fn require_descendant(agent: &str, target: &str, action: &str) -> Option<Response> {
if crate::topology::is_descendant_of(target, agent) { if crate::topology::is_descendant_of(target, agent) {
None None
} else { } else {
Some(AgentResponse::Err { Some(Response::Err {
message: format!( message: format!(
"agent `{agent}` cannot {action} `{target}`: \ "agent `{agent}` cannot {action} `{target}`: \
not in its subtree (topology)" not in its subtree (topology)"
@ -727,14 +721,14 @@ fn require_descendant(agent: &str, target: &str, action: &str) -> Option<AgentRe
/// capability — granting it to an orchestrator (e.g. the root) authorises /// capability — granting it to an orchestrator (e.g. the root) authorises
/// these verbs without any positional/hardcoded privilege. `action` is the /// these verbs without any positional/hardcoded privilege. `action` is the
/// verb phrase for the message. /// verb phrase for the message.
fn require_group(agent: &str, group: &str, action: &str) -> Option<AgentResponse> { fn require_group(agent: &str, group: &str, action: &str) -> Option<Response> {
if crate::tool_groups::groups_for(agent) if crate::tool_groups::groups_for(agent)
.iter() .iter()
.any(|g| g == group) .any(|g| g == group)
{ {
None None
} else { } else {
Some(AgentResponse::Err { Some(Response::Err {
message: format!("agent `{agent}` cannot {action}: requires the `{group}` tool group"), message: format!("agent `{agent}` cannot {action}: requires the `{group}` tool group"),
}) })
} }
@ -753,9 +747,9 @@ fn require_group(agent: &str, group: &str, action: &str) -> Option<AgentResponse
/// could never be a descendant): a brand-new name now flows straight to /// could never be a descendant): a brand-new name now flows straight to
/// `submit_init_config`, which builds filesystem paths from it, so validate /// `submit_init_config`, which builds filesystem paths from it, so validate
/// before that. /// before that.
fn require_new_child(agent: &str, target: &str, action: &str) -> Option<AgentResponse> { fn require_new_child(agent: &str, target: &str, action: &str) -> Option<Response> {
if let Some(reason) = crate::dashboard::validate_agent_name(target) { if let Some(reason) = crate::dashboard::validate_agent_name(target) {
return Some(AgentResponse::Err { return Some(Response::Err {
message: format!("agent `{agent}` cannot {action} `{target}`: {reason}"), message: format!("agent `{agent}` cannot {action} `{target}`: {reason}"),
}); });
} }
@ -771,7 +765,7 @@ fn require_new_child(agent: &str, target: &str, action: &str) -> Option<AgentRes
if crate::topology::is_descendant_of(target, agent) { if crate::topology::is_descendant_of(target, agent) {
None None
} else { } else {
Some(AgentResponse::Err { Some(Response::Err {
message: format!( message: format!(
"agent `{agent}` cannot {action} `{target}`: it already exists \ "agent `{agent}` cannot {action} `{target}`: it already exists \
outside its subtree in the topology tree" outside its subtree in the topology tree"
@ -784,14 +778,10 @@ fn require_new_child(agent: &str, target: &str, action: &str) -> Option<AgentRes
/// descendant resolve freely (a parent sees its subtree, the root sees all); /// descendant resolve freely (a parent sees its subtree, the root sees all);
/// any other named agent needs `QueryAgentState`; `"*"` is a hive-wide sweep /// any other named agent needs `QueryAgentState`; `"*"` is a hive-wide sweep
/// gated on `QueryAgentState`. /// gated on `QueryAgentState`.
fn handle_get_loose_ends( fn handle_get_loose_ends(coord: &Arc<Coordinator>, agent: &str, target: Option<&str>) -> Response {
coord: &Arc<Coordinator>,
agent: &str,
target: Option<&str>,
) -> AgentResponse {
let result = if target == Some("*") { let result = if target == Some("*") {
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::QueryAgentState) { if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::QueryAgentState) {
return AgentResponse::Err { return Response::Err {
message: "query_agent_state capability required for hive-wide loose ends" message: "query_agent_state capability required for hive-wide loose ends"
.to_owned(), .to_owned(),
}; };
@ -800,12 +790,12 @@ fn handle_get_loose_ends(
} else { } else {
match resolve_agent_state_target(agent, target) { match resolve_agent_state_target(agent, target) {
Ok(name) => crate::loose_ends::for_agent(coord, name), Ok(name) => crate::loose_ends::for_agent(coord, name),
Err(message) => return AgentResponse::Err { message }, Err(message) => return Response::Err { message },
} }
}; };
match result { match result {
Ok(loose_ends) => AgentResponse::LooseEnds { loose_ends }, Ok(loose_ends) => Response::LooseEnds { loose_ends },
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
} }
@ -821,7 +811,7 @@ fn handle_upsert_todo(
key: Option<&str>, key: Option<&str>,
summary: &str, summary: &str,
source: Option<&str>, source: Option<&str>,
) -> AgentResponse { ) -> Response {
match coord.todos.upsert(agent, subsystem, key, summary, source) { match coord.todos.upsert(agent, subsystem, key, summary, source) {
Ok((_, changed)) => { Ok((_, changed)) => {
if changed { if changed {
@ -832,9 +822,9 @@ fn handle_upsert_todo(
in_reply_to: None, in_reply_to: None,
}); });
} }
AgentResponse::Ok Response::Ok
} }
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
} }
@ -848,17 +838,17 @@ fn handle_clear_todo(
subsystem: &str, subsystem: &str,
key: Option<&str>, key: Option<&str>,
all: bool, all: bool,
) -> AgentResponse { ) -> Response {
let result = if all { let result = if all {
coord.todos.clear_subsystem(agent, subsystem) coord.todos.clear_subsystem(agent, subsystem)
} else { } else {
coord.todos.clear(agent, subsystem, key) coord.todos.clear(agent, subsystem, key)
}; };
match result { match result {
Ok(count) => AgentResponse::Acked { Ok(count) => Response::Acked {
count: u64::try_from(count).unwrap_or(0), count: u64::try_from(count).unwrap_or(0),
}, },
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
} }
@ -866,14 +856,10 @@ fn handle_clear_todo(
/// `ListTodos` — enumerate this agent's todos (optionally one subsystem's) /// `ListTodos` — enumerate this agent's todos (optionally one subsystem's)
/// as `LooseEnd::Todo` rows, so a producer can reconcile its own set. /// as `LooseEnd::Todo` rows, so a producer can reconcile its own set.
fn handle_list_todos( fn handle_list_todos(coord: &Arc<Coordinator>, agent: &str, subsystem: Option<&str>) -> Response {
coord: &Arc<Coordinator>,
agent: &str,
subsystem: Option<&str>,
) -> AgentResponse {
match crate::loose_ends::todos_for(coord, agent, subsystem) { match crate::loose_ends::todos_for(coord, agent, subsystem) {
Ok(loose_ends) => AgentResponse::LooseEnds { loose_ends }, Ok(loose_ends) => Response::LooseEnds { loose_ends },
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
} }
@ -881,12 +867,12 @@ fn handle_list_todos(
/// `MarkTodoDone` — the agent clears one of its own todos by id (scoped to /// `MarkTodoDone` — the agent clears one of its own todos by id (scoped to
/// the agent, so it can't touch another agent's). /// the agent, so it can't touch another agent's).
fn handle_mark_todo_done(coord: &Arc<Coordinator>, agent: &str, id: i64) -> AgentResponse { fn handle_mark_todo_done(coord: &Arc<Coordinator>, agent: &str, id: i64) -> Response {
match coord.todos.mark_done(agent, id) { match coord.todos.mark_done(agent, id) {
Ok(count) => AgentResponse::Acked { Ok(count) => Response::Acked {
count: u64::try_from(count).unwrap_or(0), count: u64::try_from(count).unwrap_or(0),
}, },
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
} }
@ -898,15 +884,15 @@ fn handle_count_pending_reminders(
coord: &Arc<Coordinator>, coord: &Arc<Coordinator>,
agent: &str, agent: &str,
target: Option<&str>, target: Option<&str>,
) -> AgentResponse { ) -> Response {
match resolve_agent_state_target(agent, target) { match resolve_agent_state_target(agent, target) {
Ok(name) => match coord.broker.count_pending_reminders_for(name) { Ok(name) => match coord.broker.count_pending_reminders_for(name) {
Ok(count) => AgentResponse::PendingRemindersCount { count }, Ok(count) => Response::PendingRemindersCount { count },
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
}, },
Err(message) => AgentResponse::Err { message }, Err(message) => Response::Err { message },
} }
} }
@ -918,15 +904,15 @@ fn handle_reminder_rollup(
agent: &str, agent: &str,
target: Option<&str>, target: Option<&str>,
since_secs: u64, since_secs: u64,
) -> AgentResponse { ) -> Response {
match resolve_agent_state_target(agent, target) { match resolve_agent_state_target(agent, target) {
Ok(name) => match coord.broker.reminder_rollup_for(name, since_secs) { Ok(name) => match coord.broker.reminder_rollup_for(name, since_secs) {
Ok(stats) => AgentResponse::ReminderRollup(stats), Ok(stats) => Response::ReminderRollup(stats),
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
}, },
Err(message) => AgentResponse::Err { message }, Err(message) => Response::Err { message },
} }
} }
@ -949,7 +935,7 @@ pub struct HostJournalArgs<'a> {
/// ///
/// The manager is not exempt - grant `read_host_journal` in /// The manager is not exempt - grant `read_host_journal` in
/// `meta/capabilities.json` to enable it for any agent including the manager. /// `meta/capabilities.json` to enable it for any agent including the manager.
pub async fn dispatch_host_journal(agent: &str, args: HostJournalArgs<'_>) -> AgentResponse { pub async fn dispatch_host_journal(agent: &str, args: HostJournalArgs<'_>) -> Response {
let HostJournalArgs { let HostJournalArgs {
unit, unit,
container, container,
@ -960,7 +946,7 @@ pub async fn dispatch_host_journal(agent: &str, args: HostJournalArgs<'_>) -> Ag
until, until,
} = args; } = args;
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::ReadHostJournal) { if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::ReadHostJournal) {
return AgentResponse::Err { return Response::Err {
message: "agent does not have the read_host_journal capability".to_owned(), message: "agent does not have the read_host_journal capability".to_owned(),
}; };
} }
@ -988,9 +974,9 @@ pub async fn dispatch_host_journal(agent: &str, args: HostJournalArgs<'_>) -> Ag
{ {
Ok((stdout, stderr)) => { Ok((stdout, stderr)) => {
let content = if stdout.is_empty() { stderr } else { stdout }; let content = if stdout.is_empty() { stderr } else { stdout };
AgentResponse::HostJournal { content } Response::HostJournal { content }
} }
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("journal read: {e:#}"), message: format!("journal read: {e:#}"),
}, },
}; };
@ -1032,9 +1018,9 @@ pub async fn dispatch_host_journal(agent: &str, args: HostJournalArgs<'_>) -> Ag
let stderr = String::from_utf8_lossy(&out.stderr); let stderr = String::from_utf8_lossy(&out.stderr);
format!("journalctl exited {}: {stderr}", out.status) format!("journalctl exited {}: {stderr}", out.status)
}; };
AgentResponse::HostJournal { content } Response::HostJournal { content }
} }
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("journalctl spawn failed: {e:#}"), message: format!("journalctl spawn failed: {e:#}"),
}, },
} }
@ -1077,16 +1063,16 @@ pub(crate) fn handle_send(
to: &str, to: &str,
body: &str, body: &str,
in_reply_to: Option<i64>, in_reply_to: Option<i64>,
) -> AgentResponse { ) -> Response {
if let Err(message) = crate::limits::check_size("send", body) { if let Err(message) = crate::limits::check_size("send", body) {
return AgentResponse::Err { message }; return Response::Err { message };
} }
if to == "*" { if to == "*" {
let errors = coord.broadcast_send(agent, body); let errors = coord.broadcast_send(agent, body);
return if errors.is_empty() { return if errors.is_empty() {
AgentResponse::Ok Response::Ok
} else { } else {
AgentResponse::Err { Response::Err {
message: format!("broadcast failed for agents: {}", errors.join(", ")), message: format!("broadcast failed for agents: {}", errors.join(", ")),
} }
}; };
@ -1099,9 +1085,9 @@ pub(crate) fn handle_send(
let children = crate::topology::children_of(agent); let children = crate::topology::children_of(agent);
let errors = fan_out_send(coord, agent, body, in_reply_to, &children); let errors = fan_out_send(coord, agent, body, in_reply_to, &children);
return if errors.is_empty() { return if errors.is_empty() {
AgentResponse::Ok Response::Ok
} else { } else {
AgentResponse::Err { Response::Err {
message: format!("children fan-out failed for agents: {}", errors.join(", ")), message: format!("children fan-out failed for agents: {}", errors.join(", ")),
} }
}; };
@ -1118,7 +1104,7 @@ pub(crate) fn handle_send(
// Cross-hive messaging (`name@hive` qualified names) is not routed // Cross-hive messaging (`name@hive` qualified names) is not routed
// through the broker — use the Matrix MCP tools for that instead. // through the broker — use the Matrix MCP tools for that instead.
if resolved.contains('@') { if resolved.contains('@') {
return AgentResponse::Err { return Response::Err {
message: format!( message: format!(
"send failed: cross-hive recipient `{resolved}` is not supported \ "send failed: cross-hive recipient `{resolved}` is not supported \
via the broker use Matrix MCP tools for cross-hive messaging" via the broker use Matrix MCP tools for cross-hive messaging"
@ -1128,7 +1114,7 @@ pub(crate) fn handle_send(
if resolved != hive_sh4re::OPERATOR_RECIPIENT { if resolved != hive_sh4re::OPERATOR_RECIPIENT {
let state_root = crate::paths::agent_state_dir(&resolved); let state_root = crate::paths::agent_state_dir(&resolved);
if !state_root.exists() { if !state_root.exists() {
return AgentResponse::Err { return Response::Err {
message: format!( message: format!(
"send failed: unknown recipient `{resolved}` \ "send failed: unknown recipient `{resolved}` \
(no agent with that name exists on this hive)" (no agent with that name exists on this hive)"
@ -1142,8 +1128,8 @@ pub(crate) fn handle_send(
body: body.to_owned(), body: body.to_owned(),
in_reply_to, in_reply_to,
}) { }) {
Ok(()) => AgentResponse::Ok, Ok(()) => Response::Ok,
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("{e:#}"), message: format!("{e:#}"),
}, },
} }
@ -1152,7 +1138,7 @@ pub(crate) fn handle_send(
/// `GetLogs` — read a child container's journal via hive-priv (the /// `GetLogs` — read a child container's journal via hive-priv (the
/// `-M` read needs root). `journalctl -M` wants the `h-<name>` machine /// `-M` read needs root). `journalctl -M` wants the `h-<name>` machine
/// name, which `container_name` derives. /// name, which `container_name` derives.
async fn handle_get_logs(agent: &str, lines: Option<u32>) -> AgentResponse { async fn handle_get_logs(agent: &str, lines: Option<u32>) -> Response {
let n = lines.unwrap_or(50); let n = lines.unwrap_or(50);
let machine = crate::lifecycle::container_name(agent); let machine = crate::lifecycle::container_name(agent);
tracing::info!(%agent, %machine, %n, "manager: get_logs"); tracing::info!(%agent, %machine, %n, "manager: get_logs");
@ -1167,9 +1153,9 @@ async fn handle_get_logs(agent: &str, lines: Option<u32>) -> AgentResponse {
{ {
Ok((stdout, stderr)) => { Ok((stdout, stderr)) => {
let content = if stdout.is_empty() { stderr } else { stdout }; let content = if stdout.is_empty() { stderr } else { stdout };
AgentResponse::Logs { content } Response::Logs { content }
} }
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("get_logs: {e:#}"), message: format!("get_logs: {e:#}"),
}, },
} }

View file

@ -5,7 +5,7 @@
use std::sync::Arc; use std::sync::Arc;
use hive_agent_sock::AgentResponse; use hive_agent_sock::Response;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
@ -15,10 +15,10 @@ pub(super) fn handle_remind(
message: &str, message: &str,
timing: &hive_sh4re::ReminderTiming, timing: &hive_sh4re::ReminderTiming,
file_path: Option<&str>, file_path: Option<&str>,
) -> AgentResponse { ) -> Response {
match store_remind(coord, agent, message, timing, file_path) { match store_remind(coord, agent, message, timing, file_path) {
Ok(()) => AgentResponse::Ok, Ok(()) => Response::Ok,
Err(message) => AgentResponse::Err { message }, Err(message) => Response::Err { message },
} }
} }

View file

@ -6,17 +6,17 @@
use std::sync::Arc; use std::sync::Arc;
use hive_agent_sock::AgentResponse; use hive_agent_sock::Response;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
/// `ListSchedules` — snapshot every scheduled prompt onto the wire. /// `ListSchedules` — snapshot every scheduled prompt onto the wire.
pub(super) fn handle_list_schedules(coord: &Arc<Coordinator>) -> AgentResponse { pub(super) fn handle_list_schedules(coord: &Arc<Coordinator>) -> Response {
match coord.scheduled_prompts.list() { match coord.scheduled_prompts.list() {
Ok(schedules) => AgentResponse::Schedules { Ok(schedules) => Response::Schedules {
schedules: schedules.into_iter().map(schedule_to_wire).collect(), schedules: schedules.into_iter().map(schedule_to_wire).collect(),
}, },
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("list scheduled prompts: {e:#}"), message: format!("list scheduled prompts: {e:#}"),
}, },
} }
@ -32,26 +32,26 @@ pub(super) fn handle_request_schedule_prompt(
coord: &Arc<Coordinator>, coord: &Arc<Coordinator>,
requester: &str, requester: &str,
payload: &hive_sh4re::SchedulePromptPayload, payload: &hive_sh4re::SchedulePromptPayload,
) -> AgentResponse { ) -> Response {
if payload.targets.is_empty() { if payload.targets.is_empty() {
return AgentResponse::Err { return Response::Err {
message: "schedule must have at least one target".into(), message: "schedule must have at least one target".into(),
}; };
} }
if payload.body.trim().is_empty() { if payload.body.trim().is_empty() {
return AgentResponse::Err { return Response::Err {
message: "schedule body must be non-empty".into(), message: "schedule body must be non-empty".into(),
}; };
} }
if let Some(0) = payload.interval_seconds { if let Some(0) = payload.interval_seconds {
return AgentResponse::Err { return Response::Err {
message: "interval_seconds must be > 0 (use None for one-shot)".into(), message: "interval_seconds must be > 0 (use None for one-shot)".into(),
}; };
} }
let commit_ref = match serde_json::to_string(payload) { let commit_ref = match serde_json::to_string(payload) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
return AgentResponse::Err { return Response::Err {
message: format!("encode SchedulePromptPayload: {e:#}"), message: format!("encode SchedulePromptPayload: {e:#}"),
}; };
} }
@ -66,7 +66,7 @@ pub(super) fn handle_request_schedule_prompt(
) { ) {
Ok(id) => id, Ok(id) => id,
Err(e) => { Err(e) => {
return AgentResponse::Err { return Response::Err {
message: format!("queue schedule_prompt approval: {e:#}"), message: format!("queue schedule_prompt approval: {e:#}"),
}; };
} }
@ -87,7 +87,7 @@ pub(super) fn handle_request_schedule_prompt(
description: payload.description.clone(), description: payload.description.clone(),
pr_number: None, pr_number: None,
}); });
AgentResponse::Ok Response::Ok
} }
/// Cancel a schedule (whole or per-target). Manager-surface /// Cancel a schedule (whole or per-target). Manager-surface
@ -101,22 +101,22 @@ pub(super) fn handle_cancel_schedule(
requester: &str, requester: &str,
schedule_id: i64, schedule_id: i64,
targets: Option<&[String]>, targets: Option<&[String]>,
) -> AgentResponse { ) -> Response {
let schedule = match coord.scheduled_prompts.get(schedule_id) { let schedule = match coord.scheduled_prompts.get(schedule_id) {
Ok(Some(s)) => s, Ok(Some(s)) => s,
Ok(None) => { Ok(None) => {
return AgentResponse::Err { return Response::Err {
message: format!("schedule {schedule_id} not found"), message: format!("schedule {schedule_id} not found"),
}; };
} }
Err(e) => { Err(e) => {
return AgentResponse::Err { return Response::Err {
message: format!("read schedule {schedule_id}: {e:#}"), message: format!("read schedule {schedule_id}: {e:#}"),
}; };
} }
}; };
if !cancel_authorized(requester, &schedule.owner) { if !cancel_authorized(requester, &schedule.owner) {
return AgentResponse::Err { return Response::Err {
message: format!( message: format!(
"not authorized: {requester} cannot cancel schedule owned by {owner}", "not authorized: {requester} cannot cancel schedule owned by {owner}",
owner = schedule.owner owner = schedule.owner
@ -136,9 +136,9 @@ pub(super) fn handle_cancel_schedule(
match result { match result {
Ok(()) => { Ok(()) => {
coord.emit_schedules_snapshot(); coord.emit_schedules_snapshot();
AgentResponse::Ok Response::Ok
} }
Err(message) => AgentResponse::Err { message }, Err(message) => Response::Err { message },
} }
} }
@ -151,22 +151,22 @@ pub(super) async fn handle_fire_schedule_now(
coord: &Arc<Coordinator>, coord: &Arc<Coordinator>,
requester: &str, requester: &str,
schedule_id: i64, schedule_id: i64,
) -> AgentResponse { ) -> Response {
let schedule = match coord.scheduled_prompts.get(schedule_id) { let schedule = match coord.scheduled_prompts.get(schedule_id) {
Ok(Some(s)) => s, Ok(Some(s)) => s,
Ok(None) => { Ok(None) => {
return AgentResponse::Err { return Response::Err {
message: format!("schedule {schedule_id} not found"), message: format!("schedule {schedule_id} not found"),
}; };
} }
Err(e) => { Err(e) => {
return AgentResponse::Err { return Response::Err {
message: format!("read schedule {schedule_id}: {e:#}"), message: format!("read schedule {schedule_id}: {e:#}"),
}; };
} }
}; };
if !cancel_authorized(requester, &schedule.owner) { if !cancel_authorized(requester, &schedule.owner) {
return AgentResponse::Err { return Response::Err {
message: format!( message: format!(
"not authorized: {requester} cannot fire schedule owned by {owner}", "not authorized: {requester} cannot fire schedule owned by {owner}",
owner = schedule.owner owner = schedule.owner
@ -178,9 +178,9 @@ pub(super) async fn handle_fire_schedule_now(
match crate::scheduled_prompts_worker::fire_now(coord, schedule_id, false).await { match crate::scheduled_prompts_worker::fire_now(coord, schedule_id, false).await {
Ok(_report) => { Ok(_report) => {
coord.emit_schedules_snapshot(); coord.emit_schedules_snapshot();
AgentResponse::Ok Response::Ok
} }
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("fire schedule {schedule_id} now: {e:#}"), message: format!("fire schedule {schedule_id} now: {e:#}"),
}, },
} }
@ -217,7 +217,7 @@ pub(super) fn handle_edit_schedule(
requester: &str, requester: &str,
schedule_id: i64, schedule_id: i64,
patch: EditSchedulePatch, patch: EditSchedulePatch,
) -> AgentResponse { ) -> Response {
let EditSchedulePatch { let EditSchedulePatch {
body, body,
description, description,
@ -229,18 +229,18 @@ pub(super) fn handle_edit_schedule(
let schedule = match coord.scheduled_prompts.get(schedule_id) { let schedule = match coord.scheduled_prompts.get(schedule_id) {
Ok(Some(s)) => s, Ok(Some(s)) => s,
Ok(None) => { Ok(None) => {
return AgentResponse::Err { return Response::Err {
message: format!("schedule {schedule_id} not found"), message: format!("schedule {schedule_id} not found"),
}; };
} }
Err(e) => { Err(e) => {
return AgentResponse::Err { return Response::Err {
message: format!("read schedule {schedule_id}: {e:#}"), message: format!("read schedule {schedule_id}: {e:#}"),
}; };
} }
}; };
if !cancel_authorized(requester, &schedule.owner) { if !cancel_authorized(requester, &schedule.owner) {
return AgentResponse::Err { return Response::Err {
message: format!( message: format!(
"not authorized: {requester} cannot edit schedule owned by {owner}", "not authorized: {requester} cannot edit schedule owned by {owner}",
owner = schedule.owner owner = schedule.owner
@ -258,9 +258,9 @@ pub(super) fn handle_edit_schedule(
match coord.scheduled_prompts.update(schedule_id, patch) { match coord.scheduled_prompts.update(schedule_id, patch) {
Ok(()) => { Ok(()) => {
coord.emit_schedules_snapshot(); coord.emit_schedules_snapshot();
AgentResponse::Ok Response::Ok
} }
Err(e) => AgentResponse::Err { Err(e) => Response::Err {
message: format!("edit schedule {schedule_id}: {e:#}"), message: format!("edit schedule {schedule_id}: {e:#}"),
}, },
} }

View file

@ -3,7 +3,7 @@
//! //!
//! Same wire shape as `hive-agent::forge_notify`'s wake: a single JSON //! Same wire shape as `hive-agent::forge_notify`'s wake: a single JSON
//! line written to the hyperhive control socket (`/run/hive/mcp.sock` //! line written to the hyperhive control socket (`/run/hive/mcp.sock`
//! by default) carrying an `AgentRequest::Wake { from, body }`. //! by default) carrying an `Request::Wake { from, body }`.
//! The agent harness's `agent_server` parses it and treats it as a //! The agent harness's `agent_server` parses it and treats it as a
//! `Wake` from the matrix subsystem. //! `Wake` from the matrix subsystem.
//! //!
@ -19,7 +19,7 @@ use anyhow::{Context, Result};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::UnixStream; use tokio::net::UnixStream;
/// Send an `AgentRequest::Wake { from: "matrix", body }` to the hyperhive /// Send an `Request::Wake { from: "matrix", body }` to the hyperhive
/// control socket at `socket`. Best-effort: returns Err on any plumbing /// control socket at `socket`. Best-effort: returns Err on any plumbing
/// failure; callers log + ignore so a wake delivery hiccup doesn't tear /// failure; callers log + ignore so a wake delivery hiccup doesn't tear
/// down the matrix sync loop. /// down the matrix sync loop.