diff --git a/docs/approvals.md b/docs/approvals.md index eaa6f938..b48eefd4 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -643,7 +643,7 @@ live dial of the target agent's in-container todo socket (same `UpsertTodo` request in-container producers use); the `Spawn` approval uses this path, not a `HelperEvent`. Legacy approval rows that predate the submitter column fall back to the -root agent. Variants (`hive_sh4re::HelperEvent`): +root agent. Variants (`hive_sh4re::manager::HelperEvent`): - `ApprovalResolved { id, agent, commit_ref, status, note }` — fired by `actions::approve` + `actions::deny` whenever an diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index 354aa5d2..2b158426 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -975,7 +975,7 @@ impl AgentServer { let target_count = args.targets.len(); let (resp, retries) = self .dispatch(hive_core_agent_sock::Request::RequestSchedulePrompt( - hive_sh4re::SchedulePromptPayload { + hive_sh4re::manager::SchedulePromptPayload { targets: args.targets, body: args.body, first_fire_at_unix: args.first_fire_at_unix, diff --git a/hive-agent-mcp/src/send_allow.rs b/hive-agent-mcp/src/send_allow.rs index d7a18ad6..cb46409f 100644 --- a/hive-agent-mcp/src/send_allow.rs +++ b/hive-agent-mcp/src/send_allow.rs @@ -18,7 +18,7 @@ const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json"; /// the refusal as the tool result so claude knows the message didn't /// land and can react (e.g. route via `` instead). pub fn check_send_allowed(to: &str) -> Result<(), String> { - if to == hive_sh4re::PARENT_RECIPIENT { + if to == hive_sh4re::manager::PARENT_RECIPIENT { // Always allow `` — the allow-list constrains peer // chatter, not the structural reporting line; the operator // can rewire who the parent IS via `set_parent` without @@ -53,6 +53,6 @@ pub fn check_send_allowed(to: &str) -> Result<(), String> { (configured in agent.nix). Allowed: {allow:?}. Your structural \ parent is always reachable — route through `send(to: \"{}\", …)` \ if you need to reach someone outside the allow-list.", - hive_sh4re::PARENT_RECIPIENT + hive_sh4re::manager::PARENT_RECIPIENT )) } diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index c3045e7b..53da488c 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -71,7 +71,7 @@ use crate::turn_stats::TurnStats; use anyhow::Result; use clap::Parser; use hive_core_agent_sock::{Request, Response}; -use hive_sh4re::{HelperEvent, SYSTEM_SENDER}; +use hive_sh4re::manager::{HelperEvent, SYSTEM_SENDER}; use hive_sock_client::Retry; #[derive(Parser)] @@ -406,7 +406,7 @@ impl Surface for AgentSurface { let res = hive_sock_client::request::<_, Response>( socket, &Request::Send { - to: hive_sh4re::PARENT_RECIPIENT.into(), + to: hive_sh4re::manager::PARENT_RECIPIENT.into(), body, in_reply_to: None, }, diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 55849150..231fe442 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -6,8 +6,8 @@ use std::sync::Arc; use anyhow::{Context as _, Result, bail}; -use hive_sh4re::HelperEvent; use hive_sh4re::approvals::{ApprovalKind, ApprovalStatus}; +use hive_sh4re::manager::HelperEvent; use crate::coordinator::Coordinator; use crate::lifecycle; @@ -476,8 +476,9 @@ async fn run_approval_schedule_prompt( approval: hive_sh4re::approvals::Approval, ) -> Result<()> { let result: Result<()> = async { - let payload: hive_sh4re::SchedulePromptPayload = serde_json::from_str(&approval.commit_ref) - .context("decode SchedulePromptPayload from approval.commit_ref")?; + let payload: hive_sh4re::manager::SchedulePromptPayload = + serde_json::from_str(&approval.commit_ref) + .context("decode SchedulePromptPayload from approval.commit_ref")?; coord .scheduled_prompts .submit(&crate::scheduled_prompts::NewSchedule { @@ -961,7 +962,7 @@ pub async fn destroy(coord: &Arc, name: &str, purge: bool) -> Resul drop(guard); let _ = coord .push_todo( - hive_sh4re::MANAGER_AGENT, + hive_sh4re::manager::MANAGER_AGENT, "core", Some(format!("destroyed:{name}")), format!("agent '{name}' destroyed"), diff --git a/hive-c0re/src/agent_config/topology.rs b/hive-c0re/src/agent_config/topology.rs index ba030b86..69991727 100644 --- a/hive-c0re/src/agent_config/topology.rs +++ b/hive-c0re/src/agent_config/topology.rs @@ -104,7 +104,7 @@ pub fn top_level_agents_in(topo: &BTreeMap>) -> Vec>) -> Vec String { // Early exit: only sentinel recipients need topology lookup. This // keeps the cost of a normal `send` at one string comparison. - if to != hive_sh4re::PARENT_RECIPIENT { + if to != hive_sh4re::manager::PARENT_RECIPIENT { return to.to_owned(); } resolve_recipient_in(&read(), sender, to) @@ -131,11 +131,11 @@ pub fn resolve_recipient_in( sender: &str, to: &str, ) -> String { - if to == hive_sh4re::PARENT_RECIPIENT { + if to == hive_sh4re::manager::PARENT_RECIPIENT { topo.get(sender) .cloned() .flatten() - .unwrap_or_else(|| hive_sh4re::OPERATOR_RECIPIENT.to_owned()) + .unwrap_or_else(|| hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned()) } else { to.to_owned() } @@ -722,8 +722,8 @@ mod tests { assert_eq!(resolve_recipient_in(&topo, "bob", "alice"), "alice"); assert_eq!(resolve_recipient_in(&topo, "bob", "*"), "*"); assert_eq!( - resolve_recipient_in(&topo, "bob", hive_sh4re::OPERATOR_RECIPIENT), - hive_sh4re::OPERATOR_RECIPIENT + resolve_recipient_in(&topo, "bob", hive_sh4re::manager::OPERATOR_RECIPIENT), + hive_sh4re::manager::OPERATOR_RECIPIENT ); } @@ -732,12 +732,12 @@ mod tests { let topo = topo_three_level(); // bob's parent is alice → `` from bob goes to alice. assert_eq!( - resolve_recipient_in(&topo, "bob", hive_sh4re::PARENT_RECIPIENT), + resolve_recipient_in(&topo, "bob", hive_sh4re::manager::PARENT_RECIPIENT), "alice" ); // alice's parent is the manager — same one-hop rewrite. assert_eq!( - resolve_recipient_in(&topo, "alice", hive_sh4re::PARENT_RECIPIENT), + resolve_recipient_in(&topo, "alice", hive_sh4re::manager::PARENT_RECIPIENT), crate::lifecycle::MANAGER_NAME ); } @@ -752,9 +752,9 @@ mod tests { resolve_recipient_in( &topo, crate::lifecycle::MANAGER_NAME, - hive_sh4re::PARENT_RECIPIENT + hive_sh4re::manager::PARENT_RECIPIENT ), - hive_sh4re::OPERATOR_RECIPIENT + hive_sh4re::manager::OPERATOR_RECIPIENT ); } @@ -766,8 +766,8 @@ mod tests { // its row. let topo = topo_three_level(); assert_eq!( - resolve_recipient_in(&topo, "nobody", hive_sh4re::PARENT_RECIPIENT), - hive_sh4re::OPERATOR_RECIPIENT + resolve_recipient_in(&topo, "nobody", hive_sh4re::manager::PARENT_RECIPIENT), + hive_sh4re::manager::OPERATOR_RECIPIENT ); } diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index f7e44411..eff72de2 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -1025,7 +1025,7 @@ impl Coordinator { /// `` sentinel's "root → operator" routing (see /// `docs/conventions.md::Recipient sentinels`). The /// notifications fire as ordinary broker messages with - /// `from = hive_sh4re::SYSTEM_SENDER` so the dashboard renders + /// `from = hive_sh4re::manager::SYSTEM_SENDER` so the dashboard renders /// them under the existing system-source styling. /// /// First validation failure aborts the whole batch with no disk writes. @@ -1056,7 +1056,7 @@ impl Coordinator { let new_label = new_parent.unwrap_or(""); if let Some(op) = old_parent.as_deref() { let _ = self.broker.send(&hive_sh4re::Message { - from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER), + from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), to: op.to_owned(), body: format!("{child} moved out of your subtree to {new_label}"), in_reply_to: None, @@ -1064,7 +1064,7 @@ impl Coordinator { } if let Some(np) = new_parent { let _ = self.broker.send(&hive_sh4re::Message { - from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER), + from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), to: np.to_owned(), body: format!( "{child} just moved into your subtree (was previously under {old_label})" @@ -1345,7 +1345,7 @@ impl Coordinator { still in your window." ); if let Err(e) = self.broker.send(&hive_sh4re::Message { - from: hive_sh4re::trusted_sender(hive_sh4re::SYSTEM_SENDER), + from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), to: name.to_owned(), body, in_reply_to: None, @@ -1358,8 +1358,8 @@ impl Coordinator { /// `Message::body`; sender = `SYSTEM_SENDER`. The manager harness /// recognises the sender and parses the body. Best-effort: a serde or /// broker error is logged but does not propagate. - pub fn notify_manager(&self, event: &hive_sh4re::HelperEvent) { - self.notify_agent(hive_sh4re::MANAGER_AGENT, event); + pub fn notify_manager(&self, event: &hive_sh4re::manager::HelperEvent) { + self.notify_agent(hive_sh4re::manager::MANAGER_AGENT, event); } /// Route an approval-scoped helper event to the agent that submitted @@ -1367,7 +1367,7 @@ impl Coordinator { /// time). Legacy rows with no recorded submitter — and any lookup /// failure — fall back to the root agent, preserving the prior /// always-root behaviour. - pub fn notify_submitter(&self, approval_id: i64, event: &hive_sh4re::HelperEvent) { + pub fn notify_submitter(&self, approval_id: i64, event: &hive_sh4re::manager::HelperEvent) { let target = self.submitter_or_manager(approval_id); self.notify_agent(&target, event); } @@ -1381,7 +1381,7 @@ impl Coordinator { self.approvals .submitter_of(approval_id) .unwrap_or_default() - .unwrap_or_else(|| hive_sh4re::MANAGER_AGENT.to_owned()) + .unwrap_or_else(|| hive_sh4re::manager::MANAGER_AGENT.to_owned()) } /// Push a todo directly into `agent`'s in-container todo store — a @@ -1471,15 +1471,20 @@ impl Coordinator { /// body = JSON-encoded event). Used to route `QuestionAnswered` /// events back to the agent that called `ask`, `QuestionAsked` /// events to the target of a peer question, etc. - pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::HelperEvent) { - self.notify_agent_from(hive_sh4re::SYSTEM_SENDER, agent, event); + pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::manager::HelperEvent) { + self.notify_agent_from(hive_sh4re::manager::SYSTEM_SENDER, agent, event); } /// Same as `notify_agent` but with an explicit sender. Use this /// when the event originates from a known agent or the operator /// (e.g. `QuestionAnswered` — the answerer should be the `from`, /// not `system`) so the recipient's terminal shows the right name. - pub fn notify_agent_from(&self, from: &str, agent: &str, event: &hive_sh4re::HelperEvent) { + pub fn notify_agent_from( + &self, + from: &str, + agent: &str, + event: &hive_sh4re::manager::HelperEvent, + ) { let body = match serde_json::to_string(event) { Ok(s) => s, Err(e) => { @@ -1488,7 +1493,7 @@ impl Coordinator { } }; if let Err(e) = self.broker.send(&hive_sh4re::Message { - from: hive_sh4re::trusted_sender(from), + from: hive_sh4re::manager::trusted_sender(from), to: agent.to_owned(), body, in_reply_to: None, @@ -1510,7 +1515,7 @@ impl Coordinator { continue; } if let Err(e) = self.broker.send(&hive_sh4re::Message { - from: hive_sh4re::trusted_sender(from), + from: hive_sh4re::manager::trusted_sender(from), to: agent_name.clone(), body: broadcast_body.clone(), in_reply_to: None, diff --git a/hive-c0re/src/dashboard/misc_api.rs b/hive-c0re/src/dashboard/misc_api.rs index e817a698..f7987c64 100644 --- a/hive-c0re/src/dashboard/misc_api.rs +++ b/hive-c0re/src/dashboard/misc_api.rs @@ -220,7 +220,7 @@ pub(super) async fn post_op_send( if to == "*" { let errors = state .coord - .broadcast_send(hive_sh4re::OPERATOR_RECIPIENT, &body); + .broadcast_send(hive_sh4re::manager::OPERATOR_RECIPIENT, &body); if !errors.is_empty() { return error_response(&format!( "op-send broadcast partial fail: {}", @@ -228,7 +228,7 @@ pub(super) async fn post_op_send( )); } } else if let Err(e) = state.coord.broker.send(&hive_sh4re::Message { - from: hive_sh4re::trusted_sender(hive_sh4re::OPERATOR_RECIPIENT), + from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::OPERATOR_RECIPIENT), to: to.clone(), body, in_reply_to: None, diff --git a/hive-c0re/src/dashboard/questions.rs b/hive-c0re/src/dashboard/questions.rs index f3d998b7..c56417cf 100644 --- a/hive-c0re/src/dashboard/questions.rs +++ b/hive-c0re/src/dashboard/questions.rs @@ -64,33 +64,34 @@ pub(super) async fn post_answer_question( .with_detail("answer: required"), ); } - let resp = match state - .coord - .questions - .answer(id, answer, hive_sh4re::OPERATOR_RECIPIENT) - { - Ok((question, asker, target)) => { - tracing::info!(%id, %asker, "operator answered question"); - state.coord.notify_agent( - &asker, - &hive_sh4re::HelperEvent::QuestionAnswered { + let resp = + match state + .coord + .questions + .answer(id, answer, hive_sh4re::manager::OPERATOR_RECIPIENT) + { + Ok((question, asker, target)) => { + tracing::info!(%id, %asker, "operator answered question"); + state.coord.notify_agent( + &asker, + &hive_sh4re::manager::HelperEvent::QuestionAnswered { + id, + question, + answer: answer.to_owned(), + answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(), + }, + ); + state.coord.emit_question_resolved( id, - question, - answer: answer.to_owned(), - answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), - }, - ); - state.coord.emit_question_resolved( - id, - answer, - hive_sh4re::OPERATOR_RECIPIENT, - false, - target.as_deref(), - ); - (StatusCode::OK, "ok").into_response() - } - Err(e) => error_response(&format!("answer {id} failed: {e:#}")), - }; + answer, + hive_sh4re::manager::OPERATOR_RECIPIENT, + false, + target.as_deref(), + ); + (StatusCode::OK, "ok").into_response() + } + Err(e) => error_response(&format!("answer {id} failed: {e:#}")), + }; with_cors(resp) } @@ -120,25 +121,25 @@ pub(super) async fn post_cancel_question( match state .coord .questions - .answer(id, SENTINEL, hive_sh4re::OPERATOR_RECIPIENT) + .answer(id, SENTINEL, hive_sh4re::manager::OPERATOR_RECIPIENT) { Ok((question, asker, target)) => { tracing::info!(%id, %asker, "operator cancelled question"); state.coord.emit_question_resolved( id, SENTINEL, - hive_sh4re::OPERATOR_RECIPIENT, + hive_sh4re::manager::OPERATOR_RECIPIENT, true, target.as_deref(), ); state.coord.notify_agent_from( - hive_sh4re::OPERATOR_RECIPIENT, + hive_sh4re::manager::OPERATOR_RECIPIENT, &asker, - &hive_sh4re::HelperEvent::QuestionAnswered { + &hive_sh4re::manager::HelperEvent::QuestionAnswered { id, question, answer: SENTINEL.to_owned(), - answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), + answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(), }, ); (StatusCode::OK, "ok").into_response() diff --git a/hive-c0re/src/dashboard/schedules.rs b/hive-c0re/src/dashboard/schedules.rs index 89291974..321a31aa 100644 --- a/hive-c0re/src/dashboard/schedules.rs +++ b/hive-c0re/src/dashboard/schedules.rs @@ -69,7 +69,7 @@ pub(super) async fn api_schedules(State(state): State) -> Response { #[utoipa::path( post, path = "/api/schedules", - // `hive_sh4re::SchedulePromptPayload` (the actual body) has no `ToSchema` — + // `hive_sh4re::manager::SchedulePromptPayload` (the actual body) has no `ToSchema` — // same reasoning as the `Vec` placeholder on // `api_schedules` above. request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"), @@ -82,7 +82,7 @@ pub(super) async fn api_schedules(State(state): State) -> Response { )] pub(super) async fn post_schedule_new( State(state): State, - axum::Json(payload): axum::Json, + axum::Json(payload): axum::Json, ) -> Result { if payload.targets.is_empty() { return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST) @@ -97,7 +97,7 @@ pub(super) async fn post_schedule_new( .with_detail("interval_seconds must be > 0 (use None for one-shot)")); } let new = crate::scheduled_prompts::NewSchedule { - owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), + owner: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(), targets: payload.targets, body: payload.body, first_fire_at_unix: payload.first_fire_at_unix, diff --git a/hive-c0re/src/job_queue/exec.rs b/hive-c0re/src/job_queue/exec.rs index d8866327..fc75b524 100644 --- a/hive-c0re/src/job_queue/exec.rs +++ b/hive-c0re/src/job_queue/exec.rs @@ -164,7 +164,7 @@ async fn run_emit_rebuilt(coord: &Arc, agent: &str, dag_id: Option< let summary = crate::coordinator::rebuilt_todo_summary(agent, ok, note.as_deref(), None, None); let _ = coord .push_todo( - hive_sh4re::MANAGER_AGENT, + hive_sh4re::manager::MANAGER_AGENT, "core", Some(format!("rebuilt:{agent}")), summary, @@ -429,7 +429,7 @@ async fn run_stop(coord: &Arc, name: &str) -> Result<()> { coord.unregister_agent(name); let _ = coord .push_todo( - hive_sh4re::MANAGER_AGENT, + hive_sh4re::manager::MANAGER_AGENT, "core", Some(format!("killed:{name}")), format!("agent '{name}' killed"), diff --git a/hive-c0re/src/questions.rs b/hive-c0re/src/questions.rs index 391d6451..b924b8be 100644 --- a/hive-c0re/src/questions.rs +++ b/hive-c0re/src/questions.rs @@ -47,7 +47,7 @@ pub fn handle_ask( // "is this string the operator?". let target = match to { None => None, - Some(t) if t == hive_sh4re::OPERATOR_RECIPIENT => None, + Some(t) if t == hive_sh4re::manager::OPERATOR_RECIPIENT => None, Some("") => { return Err("ask: `to` cannot be empty (omit it for the operator path)".to_owned()); } @@ -78,7 +78,7 @@ pub fn handle_ask( if let Some(target_agent) = target { coord.notify_agent( target_agent, - &hive_sh4re::HelperEvent::QuestionAsked { + &hive_sh4re::manager::HelperEvent::QuestionAsked { id, asker: asker.to_owned(), question: question.to_owned(), @@ -125,7 +125,7 @@ pub fn handle_answer( coord.notify_agent_from( answerer, &asker, - &hive_sh4re::HelperEvent::QuestionAnswered { + &hive_sh4re::manager::HelperEvent::QuestionAnswered { id, question, answer: answer.to_owned(), @@ -174,7 +174,7 @@ pub fn handle_cancel_loose_end( coord.notify_agent_from( canceller, &asker, - &hive_sh4re::HelperEvent::QuestionAnswered { + &hive_sh4re::manager::HelperEvent::QuestionAnswered { id, question, answer: sentinel.clone(), diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index e72e62dc..799a3bf1 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -298,7 +298,7 @@ async fn handle_spawn(coord: &Arc, name: &str) -> Result, name: &str) -> Result, agent: &str, name: &st Ok(()) => { let _ = coord .push_todo( - hive_sh4re::MANAGER_AGENT, + hive_sh4re::manager::MANAGER_AGENT, "core", Some(format!("killed:{name}")), format!("agent '{name}' killed"), diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index d2c9f2e8..83a6d6fb 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -14,7 +14,8 @@ use std::sync::Arc; use anyhow::{Context, Result}; use hive_core_agent_sock::{Request, Response}; -use hive_sh4re::{MANAGER_AGENT, Message}; +use hive_sh4re::Message; +use hive_sh4re::manager::MANAGER_AGENT; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; use tokio::task::JoinHandle; @@ -340,7 +341,7 @@ fn handle_wake( body: &str, ) -> hive_core_agent_sock::Response { match coord.broker.send(&Message { - from: hive_sh4re::trusted_sender(from), + from: hive_sh4re::manager::trusted_sender(from), to: agent.to_owned(), body: body.to_owned(), in_reply_to: None, @@ -483,7 +484,7 @@ fn handle_operator_msg( body: &str, ) -> hive_core_agent_sock::Response { match coord.broker.send(&Message { - from: hive_sh4re::trusted_sender(hive_sh4re::OPERATOR_RECIPIENT), + from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::OPERATOR_RECIPIENT), to: agent.to_owned(), body: body.to_owned(), in_reply_to: None, @@ -938,7 +939,7 @@ pub(crate) fn fan_out_send( continue; } if let Err(e) = coord.broker.send(&Message { - from: hive_sh4re::trusted_sender(from), + from: hive_sh4re::manager::trusted_sender(from), to: target.clone(), body: body.to_owned(), in_reply_to, @@ -977,7 +978,7 @@ pub(crate) fn handle_send( // topology.json. Bypasses the allow-list check — structural fan-out // targets are never user-listed peers. No-op (returns Ok) for leaf // agents that have no children. - if to == hive_sh4re::CHILDREN_RECIPIENT { + if to == hive_sh4re::manager::CHILDREN_RECIPIENT { let children = crate::topology::children_of(agent); let errors = fan_out_send(coord, agent, body, in_reply_to, &children); return if errors.is_empty() { @@ -1007,7 +1008,7 @@ pub(crate) fn handle_send( ), }; } - if resolved != hive_sh4re::OPERATOR_RECIPIENT { + if resolved != hive_sh4re::manager::OPERATOR_RECIPIENT { // A name that doesn't parse as an Ident can't be a local agent, so // it collapses into the same "unknown recipient" error as a valid // name with no state dir. @@ -1023,7 +1024,7 @@ pub(crate) fn handle_send( } } match coord.broker.send(&Message { - from: hive_sh4re::trusted_sender(agent), + from: hive_sh4re::manager::trusted_sender(agent), to: resolved, body: body.to_owned(), in_reply_to, @@ -1086,12 +1087,12 @@ pub fn spawn_question_watchdog(coord: &Arc, id: i64, ttl_secs: u64) if let Ok((question, asker, target)) = coord .questions - .answer(id, TTL_SENTINEL, hive_sh4re::OPERATOR_RECIPIENT) + .answer(id, TTL_SENTINEL, hive_sh4re::manager::OPERATOR_RECIPIENT) { tracing::info!(%id, %asker, "question expired (ttl)"); coord.notify_agent( &asker, - &hive_sh4re::HelperEvent::QuestionAnswered { + &hive_sh4re::manager::HelperEvent::QuestionAnswered { id, question, answer: TTL_SENTINEL.to_owned(), diff --git a/hive-c0re/src/socket_server/schedules.rs b/hive-c0re/src/socket_server/schedules.rs index e31d91db..3e8c96d0 100644 --- a/hive-c0re/src/socket_server/schedules.rs +++ b/hive-c0re/src/socket_server/schedules.rs @@ -31,7 +31,7 @@ pub(super) fn handle_list_schedules(coord: &Arc) -> Response { pub(super) fn handle_request_schedule_prompt( coord: &Arc, requester: &str, - payload: &hive_sh4re::SchedulePromptPayload, + payload: &hive_sh4re::manager::SchedulePromptPayload, ) -> Response { if payload.targets.is_empty() { return Response::Err { @@ -275,7 +275,7 @@ fn cancel_authorized(requester: &str, owner: &str) -> bool { if requester == owner { return true; } - if requester == hive_sh4re::OPERATOR_RECIPIENT { + if requester == hive_sh4re::manager::OPERATOR_RECIPIENT { return true; } // Manager can cancel anything owned by an agent in its subtree. @@ -310,8 +310,9 @@ pub(crate) fn filter_ghost_schedule_targets( live: &std::collections::HashSet, ) { for s in schedules.iter_mut() { - s.targets - .retain(|t| t.target == hive_sh4re::OPERATOR_RECIPIENT || live.contains(&t.target)); + s.targets.retain(|t| { + t.target == hive_sh4re::manager::OPERATOR_RECIPIENT || live.contains(&t.target) + }); } } diff --git a/hive-c0re/src/stores/broker.rs b/hive-c0re/src/stores/broker.rs index 9a84bfde..3d4e500a 100644 --- a/hive-c0re/src/stores/broker.rs +++ b/hive-c0re/src/stores/broker.rs @@ -215,7 +215,8 @@ impl Broker { // Operator messages get elevated priority so they surface before // queued wakes (bash completions, forge events, etc.) when the // harness pops the next turn driver. All other senders stay at 0. - let priority: i64 = i64::from(message.from.as_str() == hive_sh4re::OPERATOR_RECIPIENT); + let priority: i64 = + i64::from(message.from.as_str() == hive_sh4re::manager::OPERATOR_RECIPIENT); conn.execute( "INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to, priority) \ VALUES (?1, ?2, ?3, ?4, ?5, ?6)", @@ -382,7 +383,11 @@ impl Broker { AND delivered_at IS NULL AND acked_at IS NULL LIMIT 1", - params![child, hive_sh4re::SYSTEM_SENDER, format!("{PREFIX}%")], + params![ + child, + hive_sh4re::manager::SYSTEM_SENDER, + format!("{PREFIX}%") + ], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)), ) .optional()?; @@ -400,7 +405,7 @@ impl Broker { drop(conn); let _ = self.events.send(MessageEvent::Sent { id: row_id, - from: hive_sh4re::SYSTEM_SENDER.to_owned(), + from: hive_sh4re::manager::SYSTEM_SENDER.to_owned(), to: child.to_owned(), body: new_body, at: now, @@ -411,13 +416,13 @@ impl Broker { conn.execute( "INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to) VALUES (?1, ?2, ?3, ?4, NULL)", - params![hive_sh4re::SYSTEM_SENDER, child, body, now], + params![hive_sh4re::manager::SYSTEM_SENDER, child, body, now], )?; let row_id = conn.last_insert_rowid(); drop(conn); let _ = self.events.send(MessageEvent::Sent { id: row_id, - from: hive_sh4re::SYSTEM_SENDER.to_owned(), + from: hive_sh4re::manager::SYSTEM_SENDER.to_owned(), to: child.to_owned(), body, at: now, @@ -583,7 +588,7 @@ impl Broker { id, redelivered, message: Message { - from: hive_sh4re::trusted_sender(&from), + from: hive_sh4re::manager::trusted_sender(&from), to, body, in_reply_to, diff --git a/hive-c0re/src/stores/operator_questions.rs b/hive-c0re/src/stores/operator_questions.rs index 089eef90..876eaf72 100644 --- a/hive-c0re/src/stores/operator_questions.rs +++ b/hive-c0re/src/stores/operator_questions.rs @@ -165,13 +165,15 @@ impl OperatorQuestions { // can additionally override agent-to-agent questions to close // stuck threads). let authorised = match target.as_deref() { - None => answerer == hive_sh4re::OPERATOR_RECIPIENT, - Some(t) => answerer == t || answerer == hive_sh4re::OPERATOR_RECIPIENT, + None => answerer == hive_sh4re::manager::OPERATOR_RECIPIENT, + Some(t) => answerer == t || answerer == hive_sh4re::manager::OPERATOR_RECIPIENT, }; if !authorised { bail!( "question {id} not addressed to '{answerer}' (target = {:?})", - target.as_deref().unwrap_or(hive_sh4re::OPERATOR_RECIPIENT) + target + .as_deref() + .unwrap_or(hive_sh4re::manager::OPERATOR_RECIPIENT) ); } conn.execute( @@ -216,8 +218,9 @@ impl OperatorQuestions { if answered_at.is_some() { bail!("question {id} already answered/cancelled"); } - let authorised = - privileged || canceller == asker || canceller == hive_sh4re::OPERATOR_RECIPIENT; + let authorised = privileged + || canceller == asker + || canceller == hive_sh4re::manager::OPERATOR_RECIPIENT; if !authorised { bail!("question {id}: '{canceller}' not allowed to cancel (asker = '{asker}')"); } diff --git a/hive-c0re/src/workers/crash_watch.rs b/hive-c0re/src/workers/crash_watch.rs index 113072a8..41c20735 100644 --- a/hive-c0re/src/workers/crash_watch.rs +++ b/hive-c0re/src/workers/crash_watch.rs @@ -105,7 +105,7 @@ fn emit_crash_transitions(coord: &Coordinator, prev: &HashSet, current: } tracing::warn!(agent = %stopped, "container crash detected"); coord.record_crash(stopped); - coord.notify_manager(&hive_sh4re::HelperEvent::ContainerCrash { + coord.notify_manager(&hive_sh4re::manager::HelperEvent::ContainerCrash { agent: stopped.clone(), note: Some("container stopped without an operator action".into()), }); @@ -138,7 +138,7 @@ async fn emit_login_transitions( tracing::info!(%agent, "agent logged in"); let _ = coord .push_todo( - hive_sh4re::MANAGER_AGENT, + hive_sh4re::manager::MANAGER_AGENT, "core", Some(format!("logged_in:{agent}")), format!("agent '{agent}' logged in"), @@ -169,7 +169,7 @@ async fn emit_login_transitions( tracing::info!(%agent, "agent needs login"); let _ = coord .push_todo( - hive_sh4re::MANAGER_AGENT, + hive_sh4re::manager::MANAGER_AGENT, "core", Some(format!("needs_login:{agent}")), format!("agent '{agent}' needs login"), diff --git a/hive-c0re/src/workers/knowledge.rs b/hive-c0re/src/workers/knowledge.rs index 180183c6..e8e2cdc5 100644 --- a/hive-c0re/src/workers/knowledge.rs +++ b/hive-c0re/src/workers/knowledge.rs @@ -344,7 +344,7 @@ async fn broadcast_change(coord: &Coordinator, before: &str, after: &str) { } else { format!("[system] /knowledge updated:\n{stat}") }; - let errors = coord.broadcast_send(hive_sh4re::SYSTEM_SENDER, &body); + let errors = coord.broadcast_send(hive_sh4re::manager::SYSTEM_SENDER, &body); if !errors.is_empty() { tracing::warn!(?errors, "knowledge: broadcast had per-agent failures"); } diff --git a/hive-c0re/src/workers/scheduled_prompts_worker.rs b/hive-c0re/src/workers/scheduled_prompts_worker.rs index 1f1940a3..315916da 100644 --- a/hive-c0re/src/workers/scheduled_prompts_worker.rs +++ b/hive-c0re/src/workers/scheduled_prompts_worker.rs @@ -100,7 +100,7 @@ async fn fire_schedule(coord: &Arc, schedule: &Schedule, now: i64) // no in-container todo inbox, so it keeps the regular broker // `Message` path; the dashboard mirrors `to == operator` into // its own pane. - if target != hive_sh4re::OPERATOR_RECIPIENT && !known.contains(target) { + if target != hive_sh4re::manager::OPERATOR_RECIPIENT && !known.contains(target) { let reason = format!("no such agent: {target}"); if let Err(e) = coord @@ -169,9 +169,9 @@ async fn deliver_to_target( target: &str, body: &str, ) -> Result<(), String> { - if target == hive_sh4re::OPERATOR_RECIPIENT { + if target == hive_sh4re::manager::OPERATOR_RECIPIENT { let msg = Message { - from: hive_sh4re::trusted_sender("scheduled"), + from: hive_sh4re::manager::trusted_sender("scheduled"), to: target.to_owned(), body: body.to_owned(), in_reply_to: None, @@ -205,8 +205,8 @@ fn notify_operator_missing_target(coord: &Coordinator, schedule: &Schedule, targ body = schedule.body ); let msg = Message { - from: hive_sh4re::trusted_sender("scheduled"), - to: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), + from: hive_sh4re::manager::trusted_sender("scheduled"), + to: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(), body, in_reply_to: None, }; @@ -291,7 +291,7 @@ pub async fn fire_now( continue; } let target = &target_row.target; - if target != hive_sh4re::OPERATOR_RECIPIENT && !known.contains(target) { + if target != hive_sh4re::manager::OPERATOR_RECIPIENT && !known.contains(target) { let reason = format!("manual fire: no such agent: {target}"); if let Err(e) = coord @@ -371,7 +371,7 @@ pub async fn fire_now( async fn known_agents() -> std::collections::HashSet { use std::collections::HashSet; let mut out: HashSet = HashSet::new(); - out.insert(hive_sh4re::MANAGER_AGENT.to_owned()); + out.insert(hive_sh4re::manager::MANAGER_AGENT.to_owned()); match crate::lifecycle::list().await { Ok(list) => { for raw in list { diff --git a/hive-core-agent-sock/src/lib.rs b/hive-core-agent-sock/src/lib.rs index cb390201..bf2ac37c 100644 --- a/hive-core-agent-sock/src/lib.rs +++ b/hive-core-agent-sock/src/lib.rs @@ -7,9 +7,10 @@ //! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …) //! stay in `hive-sh4re`, which this crate depends on. +use hive_sh4re::manager::SchedulePromptPayload; use hive_sh4re::{ CancelLooseEndKind, ContainerInfo, DeliveredMessage, InboxRow, JournalPriority, LooseEnd, - MatrixIdentity, SchedulePromptPayload, WireSchedule, + MatrixIdentity, WireSchedule, }; use hive_types::Ident; use serde::{Deserialize, Serialize}; diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 78c78380..48984a0c 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; pub mod approvals; pub mod assets; pub mod bash_task; +pub mod manager; pub mod paths; pub mod wire_time; @@ -289,134 +290,6 @@ pub struct MatrixIdentity { pub homeserver: String, } -// ----------------------------------------------------------------------------- -// Manager socket — /run/hyperhive/manager/mcp.sock on the host, bind-mounted -// into the manager container at /run/hive/mcp.sock. -// ----------------------------------------------------------------------------- - -/// Logical name the broker uses for the manager. -pub const MANAGER_AGENT: &str = "ruth"; - -/// Logical name the broker uses for the human operator. Messages with -/// `to = OPERATOR_RECIPIENT` accumulate in sqlite and surface on the -/// dashboard's inbox view — they are never `recv`'d by an agent harness. -pub const OPERATOR_RECIPIENT: &str = "operator"; - -/// Reserved magic recipient — `send(to: "", ...)` is rewritten -/// by hive-c0re at delivery time to whoever `topology::parent_of(sender)` -/// returns, or to [`OPERATOR_RECIPIENT`] when the sender is a root agent -/// (no parent). Lets agents address their parent without hardcoding the -/// label, so runtime reparenting requires no agent-side restart. The -/// angle brackets are not valid in agent names (validators reject -/// `<`/`>`), so this name can never collide with a real recipient. -pub const PARENT_RECIPIENT: &str = ""; - -/// Reserved magic recipient — `send(to: "", ...)` fans out to -/// every agent whose direct parent (per `topology.json`) is the sender. -/// Lets a sub-manager nudge its subtree without enumerating labels at -/// call-time; topology changes propagate for free. The angle brackets -/// are structurally safe — agent name validation rejects `<`/`>`. -/// Delivers to an empty set (no-op) for leaf agents that have no children. -pub const CHILDREN_RECIPIENT: &str = ""; - -/// Sender hive-c0re uses for events it pushes into the manager's inbox. -/// Manager harness recognises this and parses the body as a `HelperEvent`. -pub const SYSTEM_SENDER: &str = "system"; - -/// Parse `s` as a [`Ident`] for use as `Message.from`, falling back to -/// [`SYSTEM_SENDER`] on the (should-be-unreachable) case that `s` isn't -/// ident-shaped. `Message.from` is always either a fixed sentinel literal -/// (`SYSTEM_SENDER`, `OPERATOR_RECIPIENT`, `"scheduled"`, …) or an -/// already-registered agent's own name reaching this point through -/// hive-c0re's internal dispatch — never arbitrary external input — so -/// this is a defensive fallback for a programming-bug case, not a -/// validation gate. -/// -/// # Panics -/// -/// Never, unless [`SYSTEM_SENDER`] itself stops being ident-shaped (which -/// would also be a programming bug, caught by `hive-types`' own tests). -#[must_use] -pub fn trusted_sender(s: &str) -> Ident { - Ident::parse(s) - .unwrap_or_else(|_| Ident::parse(SYSTEM_SENDER).expect("SYSTEM_SENDER is a valid Ident")) -} - -/// Out-of-band events the host-side daemon pushes to the manager's inbox. -/// Serialised as JSON in `Message::body` (sender = `SYSTEM_SENDER`). -/// Per-variant triggers + the optional `sha`/`tag` semantics live in -/// `docs/approvals.md::Helper events to the manager`. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "event", rename_all = "snake_case")] -pub enum HelperEvent { - /// An approval transitioned to a terminal state. - ApprovalResolved { - id: i64, - agent: String, - commit_ref: String, - status: approvals::ApprovalStatus, - #[serde(default, skip_serializing_if = "Option::is_none")] - note: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - sha: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - tag: Option, - }, - /// A sub-agent's recorded flake rev is stale relative to hyperhive. - NeedsUpdate { agent: String }, - /// Container exited without an operator-initiated stop (crash). - ContainerCrash { - agent: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - note: Option, - }, - /// A question queued via `Ask` was answered. `id` matches the - /// originating `QuestionQueued.id`; `answerer` is `"operator"` / - /// a peer agent name / `"ttl-watchdog"` on expiry. - QuestionAnswered { - id: i64, - question: String, - answer: String, - answerer: String, - }, - /// A peer (or the manager) asked this agent a question. Recipient - /// replies via `Answer { id, answer }`; the answer routes back to - /// the asker as `QuestionAnswered`. - QuestionAsked { - id: i64, - asker: String, - question: String, - #[serde(default)] - options: Vec, - #[serde(default)] - multi: bool, - }, -} - -/// Submission payload for `RequestSchedulePrompt`. Lives outside the -/// enum so it can also serialize into the approval row's `commit_ref` -/// (the dispatcher re-parses it on approve and inserts the schedule). -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct SchedulePromptPayload { - /// Names of recipient agents. Operator + `root` allowed. - pub targets: Vec, - /// Message body delivered to each target's inbox at fire time. - /// Same size budget as `Send.body` — soft cap at the broker level. - pub body: String, - /// Absolute unix timestamp (seconds) for the FIRST fire. For - /// recurring schedules the worker then re-arms in - /// `interval_seconds` steps. - pub first_fire_at_unix: i64, - /// `None` = one-shot. `Some(n > 0)` = recurring every `n` seconds. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub interval_seconds: Option, - /// Optional description shown on the dashboard approval card AND - /// stored on the resulting schedule row for the operator's - /// "what is this?" reference later. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option, -} - /// Named group of MCP tools an agent may be granted. The harness reads /// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of /// `snake_case` group names written by the meta renderer from per-agent diff --git a/hive-sh4re/src/manager.rs b/hive-sh4re/src/manager.rs new file mode 100644 index 00000000..dcfd72e8 --- /dev/null +++ b/hive-sh4re/src/manager.rs @@ -0,0 +1,133 @@ +//! Manager socket — `/run/hyperhive/manager/mcp.sock` on the host, +//! bind-mounted into the manager container at `/run/hive/mcp.sock`. +//! Reserved recipient names/senders, the out-of-band `HelperEvent` +//! payload hive-c0re pushes into the manager's inbox, and the +//! schedule-prompt submission payload. + +use hive_types::Ident; +use serde::{Deserialize, Serialize}; + +use crate::approvals::ApprovalStatus; + +/// Logical name the broker uses for the manager. +pub const MANAGER_AGENT: &str = "ruth"; + +/// Logical name the broker uses for the human operator. Messages with +/// `to = OPERATOR_RECIPIENT` accumulate in sqlite and surface on the +/// dashboard's inbox view — they are never `recv`'d by an agent harness. +pub const OPERATOR_RECIPIENT: &str = "operator"; + +/// Reserved magic recipient — `send(to: "", ...)` is rewritten +/// by hive-c0re at delivery time to whoever `topology::parent_of(sender)` +/// returns, or to [`OPERATOR_RECIPIENT`] when the sender is a root agent +/// (no parent). Lets agents address their parent without hardcoding the +/// label, so runtime reparenting requires no agent-side restart. The +/// angle brackets are not valid in agent names (validators reject +/// `<`/`>`), so this name can never collide with a real recipient. +pub const PARENT_RECIPIENT: &str = ""; + +/// Reserved magic recipient — `send(to: "", ...)` fans out to +/// every agent whose direct parent (per `topology.json`) is the sender. +/// Lets a sub-manager nudge its subtree without enumerating labels at +/// call-time; topology changes propagate for free. The angle brackets +/// are structurally safe — agent name validation rejects `<`/`>`. +/// Delivers to an empty set (no-op) for leaf agents that have no children. +pub const CHILDREN_RECIPIENT: &str = ""; + +/// Sender hive-c0re uses for events it pushes into the manager's inbox. +/// Manager harness recognises this and parses the body as a `HelperEvent`. +pub const SYSTEM_SENDER: &str = "system"; + +/// Parse `s` as a [`Ident`] for use as `Message.from`, falling back to +/// [`SYSTEM_SENDER`] on the (should-be-unreachable) case that `s` isn't +/// ident-shaped. `Message.from` is always either a fixed sentinel literal +/// (`SYSTEM_SENDER`, `OPERATOR_RECIPIENT`, `"scheduled"`, …) or an +/// already-registered agent's own name reaching this point through +/// hive-c0re's internal dispatch — never arbitrary external input — so +/// this is a defensive fallback for a programming-bug case, not a +/// validation gate. +/// +/// # Panics +/// +/// Never, unless [`SYSTEM_SENDER`] itself stops being ident-shaped (which +/// would also be a programming bug, caught by `hive-types`' own tests). +#[must_use] +pub fn trusted_sender(s: &str) -> Ident { + Ident::parse(s) + .unwrap_or_else(|_| Ident::parse(SYSTEM_SENDER).expect("SYSTEM_SENDER is a valid Ident")) +} + +/// Out-of-band events the host-side daemon pushes to the manager's inbox. +/// Serialised as JSON in `Message::body` (sender = `SYSTEM_SENDER`). +/// Per-variant triggers + the optional `sha`/`tag` semantics live in +/// `docs/approvals.md::Helper events to the manager`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", rename_all = "snake_case")] +pub enum HelperEvent { + /// An approval transitioned to a terminal state. + ApprovalResolved { + id: i64, + agent: String, + commit_ref: String, + status: ApprovalStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + note: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + sha: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + tag: Option, + }, + /// A sub-agent's recorded flake rev is stale relative to hyperhive. + NeedsUpdate { agent: String }, + /// Container exited without an operator-initiated stop (crash). + ContainerCrash { + agent: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + note: Option, + }, + /// A question queued via `Ask` was answered. `id` matches the + /// originating `QuestionQueued.id`; `answerer` is `"operator"` / + /// a peer agent name / `"ttl-watchdog"` on expiry. + QuestionAnswered { + id: i64, + question: String, + answer: String, + answerer: String, + }, + /// A peer (or the manager) asked this agent a question. Recipient + /// replies via `Answer { id, answer }`; the answer routes back to + /// the asker as `QuestionAnswered`. + QuestionAsked { + id: i64, + asker: String, + question: String, + #[serde(default)] + options: Vec, + #[serde(default)] + multi: bool, + }, +} + +/// Submission payload for `RequestSchedulePrompt`. Lives outside the +/// enum so it can also serialize into the approval row's `commit_ref` +/// (the dispatcher re-parses it on approve and inserts the schedule). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SchedulePromptPayload { + /// Names of recipient agents. Operator + `root` allowed. + pub targets: Vec, + /// Message body delivered to each target's inbox at fire time. + /// Same size budget as `Send.body` — soft cap at the broker level. + pub body: String, + /// Absolute unix timestamp (seconds) for the FIRST fire. For + /// recurring schedules the worker then re-arms in + /// `interval_seconds` steps. + pub first_fire_at_unix: i64, + /// `None` = one-shot. `Some(n > 0)` = recurring every `n` seconds. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub interval_seconds: Option, + /// Optional description shown on the dashboard approval card AND + /// stored on the resulting schedule row for the operator's + /// "what is this?" reference later. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, +}