hive-sh4re: split manager-socket constants + HelperEvent into their own topic module

This commit is contained in:
damocles 2026-08-10 22:38:35 +02:00 committed by mara
commit 138f6b6c10
24 changed files with 268 additions and 244 deletions

View file

@ -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 `UpsertTodo` request in-container producers use); the `Spawn` approval
uses this path, not a `HelperEvent`. Legacy approval rows that predate the uses this path, not a `HelperEvent`. Legacy approval rows that predate the
submitter column fall back to 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 }` - `ApprovalResolved { id, agent, commit_ref, status, note }`
fired by `actions::approve` + `actions::deny` whenever an fired by `actions::approve` + `actions::deny` whenever an

View file

@ -975,7 +975,7 @@ impl AgentServer {
let target_count = args.targets.len(); let target_count = args.targets.len();
let (resp, retries) = self let (resp, retries) = self
.dispatch(hive_core_agent_sock::Request::RequestSchedulePrompt( .dispatch(hive_core_agent_sock::Request::RequestSchedulePrompt(
hive_sh4re::SchedulePromptPayload { hive_sh4re::manager::SchedulePromptPayload {
targets: args.targets, targets: args.targets,
body: args.body, body: args.body,
first_fire_at_unix: args.first_fire_at_unix, first_fire_at_unix: args.first_fire_at_unix,

View file

@ -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 /// the refusal as the tool result so claude knows the message didn't
/// land and can react (e.g. route via `<parent>` instead). /// land and can react (e.g. route via `<parent>` instead).
pub fn check_send_allowed(to: &str) -> Result<(), String> { pub fn check_send_allowed(to: &str) -> Result<(), String> {
if to == hive_sh4re::PARENT_RECIPIENT { if to == hive_sh4re::manager::PARENT_RECIPIENT {
// Always allow `<parent>` — the allow-list constrains peer // Always allow `<parent>` — the allow-list constrains peer
// chatter, not the structural reporting line; the operator // chatter, not the structural reporting line; the operator
// can rewire who the parent IS via `set_parent` without // 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 \ (configured in agent.nix). Allowed: {allow:?}. Your structural \
parent is always reachable route through `send(to: \"{}\", …)` \ parent is always reachable route through `send(to: \"{}\", …)` \
if you need to reach someone outside the allow-list.", if you need to reach someone outside the allow-list.",
hive_sh4re::PARENT_RECIPIENT hive_sh4re::manager::PARENT_RECIPIENT
)) ))
} }

View file

@ -71,7 +71,7 @@ use crate::turn_stats::TurnStats;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use hive_core_agent_sock::{Request, Response}; 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; use hive_sock_client::Retry;
#[derive(Parser)] #[derive(Parser)]
@ -406,7 +406,7 @@ impl Surface for AgentSurface {
let res = hive_sock_client::request::<_, Response>( let res = hive_sock_client::request::<_, Response>(
socket, socket,
&Request::Send { &Request::Send {
to: hive_sh4re::PARENT_RECIPIENT.into(), to: hive_sh4re::manager::PARENT_RECIPIENT.into(),
body, body,
in_reply_to: None, in_reply_to: None,
}, },

View file

@ -6,8 +6,8 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context as _, Result, bail}; use anyhow::{Context as _, Result, bail};
use hive_sh4re::HelperEvent;
use hive_sh4re::approvals::{ApprovalKind, ApprovalStatus}; use hive_sh4re::approvals::{ApprovalKind, ApprovalStatus};
use hive_sh4re::manager::HelperEvent;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
use crate::lifecycle; use crate::lifecycle;
@ -476,8 +476,9 @@ async fn run_approval_schedule_prompt(
approval: hive_sh4re::approvals::Approval, approval: hive_sh4re::approvals::Approval,
) -> Result<()> { ) -> Result<()> {
let result: Result<()> = async { let result: Result<()> = async {
let payload: hive_sh4re::SchedulePromptPayload = serde_json::from_str(&approval.commit_ref) let payload: hive_sh4re::manager::SchedulePromptPayload =
.context("decode SchedulePromptPayload from approval.commit_ref")?; serde_json::from_str(&approval.commit_ref)
.context("decode SchedulePromptPayload from approval.commit_ref")?;
coord coord
.scheduled_prompts .scheduled_prompts
.submit(&crate::scheduled_prompts::NewSchedule { .submit(&crate::scheduled_prompts::NewSchedule {
@ -961,7 +962,7 @@ pub async fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) -> Resul
drop(guard); drop(guard);
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::MANAGER_AGENT, hive_sh4re::manager::MANAGER_AGENT,
"core", "core",
Some(format!("destroyed:{name}")), Some(format!("destroyed:{name}")),
format!("agent '{name}' destroyed"), format!("agent '{name}' destroyed"),

View file

@ -104,7 +104,7 @@ pub fn top_level_agents_in(topo: &BTreeMap<String, Option<String>>) -> Vec<Strin
} }
/// Resolve a magic recipient sentinel (currently just /// Resolve a magic recipient sentinel (currently just
/// [`hive_sh4re::PARENT_RECIPIENT`]) to a real broker recipient at /// [`hive_sh4re::manager::PARENT_RECIPIENT`]) to a real broker recipient at
/// send time. Returns an owned `String` so callers can plug it /// send time. Returns an owned `String` so callers can plug it
/// straight into [`crate::broker::Broker::send`] without /// straight into [`crate::broker::Broker::send`] without
/// borrow-juggling around the temporary lookup. /// borrow-juggling around the temporary lookup.
@ -116,7 +116,7 @@ pub fn top_level_agents_in(topo: &BTreeMap<String, Option<String>>) -> Vec<Strin
pub fn resolve_recipient(sender: &str, to: &str) -> String { pub fn resolve_recipient(sender: &str, to: &str) -> String {
// Early exit: only sentinel recipients need topology lookup. This // Early exit: only sentinel recipients need topology lookup. This
// keeps the cost of a normal `send` at one string comparison. // 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(); return to.to_owned();
} }
resolve_recipient_in(&read(), sender, to) resolve_recipient_in(&read(), sender, to)
@ -131,11 +131,11 @@ pub fn resolve_recipient_in(
sender: &str, sender: &str,
to: &str, to: &str,
) -> String { ) -> String {
if to == hive_sh4re::PARENT_RECIPIENT { if to == hive_sh4re::manager::PARENT_RECIPIENT {
topo.get(sender) topo.get(sender)
.cloned() .cloned()
.flatten() .flatten()
.unwrap_or_else(|| hive_sh4re::OPERATOR_RECIPIENT.to_owned()) .unwrap_or_else(|| hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned())
} else { } else {
to.to_owned() 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", "alice"), "alice");
assert_eq!(resolve_recipient_in(&topo, "bob", "*"), "*"); assert_eq!(resolve_recipient_in(&topo, "bob", "*"), "*");
assert_eq!( assert_eq!(
resolve_recipient_in(&topo, "bob", hive_sh4re::OPERATOR_RECIPIENT), resolve_recipient_in(&topo, "bob", hive_sh4re::manager::OPERATOR_RECIPIENT),
hive_sh4re::OPERATOR_RECIPIENT hive_sh4re::manager::OPERATOR_RECIPIENT
); );
} }
@ -732,12 +732,12 @@ mod tests {
let topo = topo_three_level(); let topo = topo_three_level();
// bob's parent is alice → `<parent>` from bob goes to alice. // bob's parent is alice → `<parent>` from bob goes to alice.
assert_eq!( assert_eq!(
resolve_recipient_in(&topo, "bob", hive_sh4re::PARENT_RECIPIENT), resolve_recipient_in(&topo, "bob", hive_sh4re::manager::PARENT_RECIPIENT),
"alice" "alice"
); );
// alice's parent is the manager — same one-hop rewrite. // alice's parent is the manager — same one-hop rewrite.
assert_eq!( 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 crate::lifecycle::MANAGER_NAME
); );
} }
@ -752,9 +752,9 @@ mod tests {
resolve_recipient_in( resolve_recipient_in(
&topo, &topo,
crate::lifecycle::MANAGER_NAME, 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. // its row.
let topo = topo_three_level(); let topo = topo_three_level();
assert_eq!( assert_eq!(
resolve_recipient_in(&topo, "nobody", hive_sh4re::PARENT_RECIPIENT), resolve_recipient_in(&topo, "nobody", hive_sh4re::manager::PARENT_RECIPIENT),
hive_sh4re::OPERATOR_RECIPIENT hive_sh4re::manager::OPERATOR_RECIPIENT
); );
} }

View file

@ -1025,7 +1025,7 @@ impl Coordinator {
/// `<parent>` sentinel's "root → operator" routing (see /// `<parent>` sentinel's "root → operator" routing (see
/// `docs/conventions.md::Recipient sentinels`). The /// `docs/conventions.md::Recipient sentinels`). The
/// notifications fire as ordinary broker messages with /// 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. /// them under the existing system-source styling.
/// ///
/// First validation failure aborts the whole batch with no disk writes. /// First validation failure aborts the whole batch with no disk writes.
@ -1056,7 +1056,7 @@ impl Coordinator {
let new_label = new_parent.unwrap_or("<root>"); let new_label = new_parent.unwrap_or("<root>");
if let Some(op) = old_parent.as_deref() { if let Some(op) = old_parent.as_deref() {
let _ = self.broker.send(&hive_sh4re::Message { 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(), to: op.to_owned(),
body: format!("{child} moved out of your subtree to {new_label}"), body: format!("{child} moved out of your subtree to {new_label}"),
in_reply_to: None, in_reply_to: None,
@ -1064,7 +1064,7 @@ impl Coordinator {
} }
if let Some(np) = new_parent { if let Some(np) = new_parent {
let _ = self.broker.send(&hive_sh4re::Message { 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(), to: np.to_owned(),
body: format!( body: format!(
"{child} just moved into your subtree (was previously under {old_label})" "{child} just moved into your subtree (was previously under {old_label})"
@ -1345,7 +1345,7 @@ impl Coordinator {
still in your window." still in your window."
); );
if let Err(e) = self.broker.send(&hive_sh4re::Message { 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(), to: name.to_owned(),
body, body,
in_reply_to: None, in_reply_to: None,
@ -1358,8 +1358,8 @@ impl Coordinator {
/// `Message::body`; sender = `SYSTEM_SENDER`. The manager harness /// `Message::body`; sender = `SYSTEM_SENDER`. The manager harness
/// recognises the sender and parses the body. Best-effort: a serde or /// recognises the sender and parses the body. Best-effort: a serde or
/// broker error is logged but does not propagate. /// broker error is logged but does not propagate.
pub fn notify_manager(&self, event: &hive_sh4re::HelperEvent) { pub fn notify_manager(&self, event: &hive_sh4re::manager::HelperEvent) {
self.notify_agent(hive_sh4re::MANAGER_AGENT, event); self.notify_agent(hive_sh4re::manager::MANAGER_AGENT, event);
} }
/// Route an approval-scoped helper event to the agent that submitted /// 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 /// time). Legacy rows with no recorded submitter — and any lookup
/// failure — fall back to the root agent, preserving the prior /// failure — fall back to the root agent, preserving the prior
/// always-root behaviour. /// 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); let target = self.submitter_or_manager(approval_id);
self.notify_agent(&target, event); self.notify_agent(&target, event);
} }
@ -1381,7 +1381,7 @@ impl Coordinator {
self.approvals self.approvals
.submitter_of(approval_id) .submitter_of(approval_id)
.unwrap_or_default() .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 /// 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` /// body = JSON-encoded event). Used to route `QuestionAnswered`
/// events back to the agent that called `ask`, `QuestionAsked` /// events back to the agent that called `ask`, `QuestionAsked`
/// events to the target of a peer question, etc. /// events to the target of a peer question, etc.
pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::HelperEvent) { pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::manager::HelperEvent) {
self.notify_agent_from(hive_sh4re::SYSTEM_SENDER, agent, event); self.notify_agent_from(hive_sh4re::manager::SYSTEM_SENDER, agent, event);
} }
/// Same as `notify_agent` but with an explicit sender. Use this /// Same as `notify_agent` but with an explicit sender. Use this
/// when the event originates from a known agent or the operator /// when the event originates from a known agent or the operator
/// (e.g. `QuestionAnswered` — the answerer should be the `from`, /// (e.g. `QuestionAnswered` — the answerer should be the `from`,
/// not `system`) so the recipient's terminal shows the right name. /// 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) { let body = match serde_json::to_string(event) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
@ -1488,7 +1493,7 @@ impl Coordinator {
} }
}; };
if let Err(e) = self.broker.send(&hive_sh4re::Message { 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(), to: agent.to_owned(),
body, body,
in_reply_to: None, in_reply_to: None,
@ -1510,7 +1515,7 @@ impl Coordinator {
continue; continue;
} }
if let Err(e) = self.broker.send(&hive_sh4re::Message { 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(), to: agent_name.clone(),
body: broadcast_body.clone(), body: broadcast_body.clone(),
in_reply_to: None, in_reply_to: None,

View file

@ -220,7 +220,7 @@ pub(super) async fn post_op_send(
if to == "*" { if to == "*" {
let errors = state let errors = state
.coord .coord
.broadcast_send(hive_sh4re::OPERATOR_RECIPIENT, &body); .broadcast_send(hive_sh4re::manager::OPERATOR_RECIPIENT, &body);
if !errors.is_empty() { if !errors.is_empty() {
return error_response(&format!( return error_response(&format!(
"op-send broadcast partial fail: {}", "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 { } 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(), to: to.clone(),
body, body,
in_reply_to: None, in_reply_to: None,

View file

@ -64,33 +64,34 @@ pub(super) async fn post_answer_question(
.with_detail("answer: required"), .with_detail("answer: required"),
); );
} }
let resp = match state let resp =
.coord match state
.questions .coord
.answer(id, answer, hive_sh4re::OPERATOR_RECIPIENT) .questions
{ .answer(id, answer, hive_sh4re::manager::OPERATOR_RECIPIENT)
Ok((question, asker, target)) => { {
tracing::info!(%id, %asker, "operator answered question"); Ok((question, asker, target)) => {
state.coord.notify_agent( tracing::info!(%id, %asker, "operator answered question");
&asker, state.coord.notify_agent(
&hive_sh4re::HelperEvent::QuestionAnswered { &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, id,
question, answer,
answer: answer.to_owned(), hive_sh4re::manager::OPERATOR_RECIPIENT,
answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), false,
}, target.as_deref(),
); );
state.coord.emit_question_resolved( (StatusCode::OK, "ok").into_response()
id, }
answer, Err(e) => error_response(&format!("answer {id} failed: {e:#}")),
hive_sh4re::OPERATOR_RECIPIENT, };
false,
target.as_deref(),
);
(StatusCode::OK, "ok").into_response()
}
Err(e) => error_response(&format!("answer {id} failed: {e:#}")),
};
with_cors(resp) with_cors(resp)
} }
@ -120,25 +121,25 @@ pub(super) async fn post_cancel_question(
match state match state
.coord .coord
.questions .questions
.answer(id, SENTINEL, hive_sh4re::OPERATOR_RECIPIENT) .answer(id, SENTINEL, hive_sh4re::manager::OPERATOR_RECIPIENT)
{ {
Ok((question, asker, target)) => { Ok((question, asker, target)) => {
tracing::info!(%id, %asker, "operator cancelled question"); tracing::info!(%id, %asker, "operator cancelled question");
state.coord.emit_question_resolved( state.coord.emit_question_resolved(
id, id,
SENTINEL, SENTINEL,
hive_sh4re::OPERATOR_RECIPIENT, hive_sh4re::manager::OPERATOR_RECIPIENT,
true, true,
target.as_deref(), target.as_deref(),
); );
state.coord.notify_agent_from( state.coord.notify_agent_from(
hive_sh4re::OPERATOR_RECIPIENT, hive_sh4re::manager::OPERATOR_RECIPIENT,
&asker, &asker,
&hive_sh4re::HelperEvent::QuestionAnswered { &hive_sh4re::manager::HelperEvent::QuestionAnswered {
id, id,
question, question,
answer: SENTINEL.to_owned(), answer: SENTINEL.to_owned(),
answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), answerer: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
}, },
); );
(StatusCode::OK, "ok").into_response() (StatusCode::OK, "ok").into_response()

View file

@ -69,7 +69,7 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/schedules", 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<serde_json::Value>` placeholder on // same reasoning as the `Vec<serde_json::Value>` placeholder on
// `api_schedules` above. // `api_schedules` above.
request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"), request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"),
@ -82,7 +82,7 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
)] )]
pub(super) async fn post_schedule_new( pub(super) async fn post_schedule_new(
State(state): State<AppState>, State(state): State<AppState>,
axum::Json(payload): axum::Json<hive_sh4re::SchedulePromptPayload>, axum::Json(payload): axum::Json<hive_sh4re::manager::SchedulePromptPayload>,
) -> Result<Response, ProblemDetails> { ) -> Result<Response, ProblemDetails> {
if payload.targets.is_empty() { if payload.targets.is_empty() {
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST) 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)")); .with_detail("interval_seconds must be > 0 (use None for one-shot)"));
} }
let new = crate::scheduled_prompts::NewSchedule { let new = crate::scheduled_prompts::NewSchedule {
owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), owner: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
targets: payload.targets, targets: payload.targets,
body: payload.body, body: payload.body,
first_fire_at_unix: payload.first_fire_at_unix, first_fire_at_unix: payload.first_fire_at_unix,

View file

@ -164,7 +164,7 @@ async fn run_emit_rebuilt(coord: &Arc<Coordinator>, agent: &str, dag_id: Option<
let summary = crate::coordinator::rebuilt_todo_summary(agent, ok, note.as_deref(), None, None); let summary = crate::coordinator::rebuilt_todo_summary(agent, ok, note.as_deref(), None, None);
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::MANAGER_AGENT, hive_sh4re::manager::MANAGER_AGENT,
"core", "core",
Some(format!("rebuilt:{agent}")), Some(format!("rebuilt:{agent}")),
summary, summary,
@ -429,7 +429,7 @@ async fn run_stop(coord: &Arc<Coordinator>, name: &str) -> Result<()> {
coord.unregister_agent(name); coord.unregister_agent(name);
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::MANAGER_AGENT, hive_sh4re::manager::MANAGER_AGENT,
"core", "core",
Some(format!("killed:{name}")), Some(format!("killed:{name}")),
format!("agent '{name}' killed"), format!("agent '{name}' killed"),

View file

@ -47,7 +47,7 @@ pub fn handle_ask(
// "is this string the operator?". // "is this string the operator?".
let target = match to { let target = match to {
None => None, None => None,
Some(t) if t == hive_sh4re::OPERATOR_RECIPIENT => None, Some(t) if t == hive_sh4re::manager::OPERATOR_RECIPIENT => None,
Some("") => { Some("") => {
return Err("ask: `to` cannot be empty (omit it for the operator path)".to_owned()); 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 { if let Some(target_agent) = target {
coord.notify_agent( coord.notify_agent(
target_agent, target_agent,
&hive_sh4re::HelperEvent::QuestionAsked { &hive_sh4re::manager::HelperEvent::QuestionAsked {
id, id,
asker: asker.to_owned(), asker: asker.to_owned(),
question: question.to_owned(), question: question.to_owned(),
@ -125,7 +125,7 @@ pub fn handle_answer(
coord.notify_agent_from( coord.notify_agent_from(
answerer, answerer,
&asker, &asker,
&hive_sh4re::HelperEvent::QuestionAnswered { &hive_sh4re::manager::HelperEvent::QuestionAnswered {
id, id,
question, question,
answer: answer.to_owned(), answer: answer.to_owned(),
@ -174,7 +174,7 @@ pub fn handle_cancel_loose_end(
coord.notify_agent_from( coord.notify_agent_from(
canceller, canceller,
&asker, &asker,
&hive_sh4re::HelperEvent::QuestionAnswered { &hive_sh4re::manager::HelperEvent::QuestionAnswered {
id, id,
question, question,
answer: sentinel.clone(), answer: sentinel.clone(),

View file

@ -298,7 +298,7 @@ async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostRespon
coord.register_agent(name)?; coord.register_agent(name)?;
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::MANAGER_AGENT, hive_sh4re::manager::MANAGER_AGENT,
"core", "core",
Some(format!("spawned:{name}")), Some(format!("spawned:{name}")),
format!("agent '{name}' spawned"), format!("agent '{name}' spawned"),
@ -313,7 +313,7 @@ async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostRespon
// nothing to unregister. Notify the manager and propagate. // nothing to unregister. Notify the manager and propagate.
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::MANAGER_AGENT, hive_sh4re::manager::MANAGER_AGENT,
"core", "core",
Some(format!("spawned:{name}")), Some(format!("spawned:{name}")),
format!("agent '{name}' spawn FAILED: {e:#}"), format!("agent '{name}' spawn FAILED: {e:#}"),

View file

@ -123,7 +123,7 @@ pub(super) async fn handle_kill(coord: &Arc<Coordinator>, agent: &str, name: &st
Ok(()) => { Ok(()) => {
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::MANAGER_AGENT, hive_sh4re::manager::MANAGER_AGENT,
"core", "core",
Some(format!("killed:{name}")), Some(format!("killed:{name}")),
format!("agent '{name}' killed"), format!("agent '{name}' killed"),

View file

@ -14,7 +14,8 @@ use std::sync::Arc;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use hive_core_agent_sock::{Request, Response}; 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::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream}; use tokio::net::{UnixListener, UnixStream};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
@ -340,7 +341,7 @@ fn handle_wake(
body: &str, body: &str,
) -> hive_core_agent_sock::Response { ) -> hive_core_agent_sock::Response {
match coord.broker.send(&Message { match coord.broker.send(&Message {
from: hive_sh4re::trusted_sender(from), from: hive_sh4re::manager::trusted_sender(from),
to: agent.to_owned(), to: agent.to_owned(),
body: body.to_owned(), body: body.to_owned(),
in_reply_to: None, in_reply_to: None,
@ -483,7 +484,7 @@ fn handle_operator_msg(
body: &str, body: &str,
) -> hive_core_agent_sock::Response { ) -> hive_core_agent_sock::Response {
match coord.broker.send(&Message { 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(), to: agent.to_owned(),
body: body.to_owned(), body: body.to_owned(),
in_reply_to: None, in_reply_to: None,
@ -938,7 +939,7 @@ pub(crate) fn fan_out_send(
continue; continue;
} }
if let Err(e) = coord.broker.send(&Message { if let Err(e) = coord.broker.send(&Message {
from: hive_sh4re::trusted_sender(from), from: hive_sh4re::manager::trusted_sender(from),
to: target.clone(), to: target.clone(),
body: body.to_owned(), body: body.to_owned(),
in_reply_to, in_reply_to,
@ -977,7 +978,7 @@ pub(crate) fn handle_send(
// topology.json. Bypasses the allow-list check — structural fan-out // topology.json. Bypasses the allow-list check — structural fan-out
// targets are never user-listed peers. No-op (returns Ok) for leaf // targets are never user-listed peers. No-op (returns Ok) for leaf
// agents that have no children. // 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 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() {
@ -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 // 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 // it collapses into the same "unknown recipient" error as a valid
// name with no state dir. // name with no state dir.
@ -1023,7 +1024,7 @@ pub(crate) fn handle_send(
} }
} }
match coord.broker.send(&Message { match coord.broker.send(&Message {
from: hive_sh4re::trusted_sender(agent), from: hive_sh4re::manager::trusted_sender(agent),
to: resolved, to: resolved,
body: body.to_owned(), body: body.to_owned(),
in_reply_to, in_reply_to,
@ -1086,12 +1087,12 @@ pub fn spawn_question_watchdog(coord: &Arc<Coordinator>, id: i64, ttl_secs: u64)
if let Ok((question, asker, target)) = if let Ok((question, asker, target)) =
coord coord
.questions .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)"); tracing::info!(%id, %asker, "question expired (ttl)");
coord.notify_agent( coord.notify_agent(
&asker, &asker,
&hive_sh4re::HelperEvent::QuestionAnswered { &hive_sh4re::manager::HelperEvent::QuestionAnswered {
id, id,
question, question,
answer: TTL_SENTINEL.to_owned(), answer: TTL_SENTINEL.to_owned(),

View file

@ -31,7 +31,7 @@ pub(super) fn handle_list_schedules(coord: &Arc<Coordinator>) -> Response {
pub(super) fn handle_request_schedule_prompt( pub(super) fn handle_request_schedule_prompt(
coord: &Arc<Coordinator>, coord: &Arc<Coordinator>,
requester: &str, requester: &str,
payload: &hive_sh4re::SchedulePromptPayload, payload: &hive_sh4re::manager::SchedulePromptPayload,
) -> Response { ) -> Response {
if payload.targets.is_empty() { if payload.targets.is_empty() {
return Response::Err { return Response::Err {
@ -275,7 +275,7 @@ fn cancel_authorized(requester: &str, owner: &str) -> bool {
if requester == owner { if requester == owner {
return true; return true;
} }
if requester == hive_sh4re::OPERATOR_RECIPIENT { if requester == hive_sh4re::manager::OPERATOR_RECIPIENT {
return true; return true;
} }
// Manager can cancel anything owned by an agent in its subtree. // 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<String>, live: &std::collections::HashSet<String>,
) { ) {
for s in schedules.iter_mut() { for s in schedules.iter_mut() {
s.targets s.targets.retain(|t| {
.retain(|t| t.target == hive_sh4re::OPERATOR_RECIPIENT || live.contains(&t.target)); t.target == hive_sh4re::manager::OPERATOR_RECIPIENT || live.contains(&t.target)
});
} }
} }

View file

@ -215,7 +215,8 @@ impl Broker {
// Operator messages get elevated priority so they surface before // Operator messages get elevated priority so they surface before
// queued wakes (bash completions, forge events, etc.) when the // queued wakes (bash completions, forge events, etc.) when the
// harness pops the next turn driver. All other senders stay at 0. // 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( conn.execute(
"INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to, priority) \ "INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to, priority) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)", VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
@ -382,7 +383,11 @@ impl Broker {
AND delivered_at IS NULL AND delivered_at IS NULL
AND acked_at IS NULL AND acked_at IS NULL
LIMIT 1", 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)?)), |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
) )
.optional()?; .optional()?;
@ -400,7 +405,7 @@ impl Broker {
drop(conn); drop(conn);
let _ = self.events.send(MessageEvent::Sent { let _ = self.events.send(MessageEvent::Sent {
id: row_id, id: row_id,
from: hive_sh4re::SYSTEM_SENDER.to_owned(), from: hive_sh4re::manager::SYSTEM_SENDER.to_owned(),
to: child.to_owned(), to: child.to_owned(),
body: new_body, body: new_body,
at: now, at: now,
@ -411,13 +416,13 @@ impl Broker {
conn.execute( conn.execute(
"INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to) "INSERT INTO messages (sender, recipient, body, sent_at, in_reply_to)
VALUES (?1, ?2, ?3, ?4, NULL)", 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(); let row_id = conn.last_insert_rowid();
drop(conn); drop(conn);
let _ = self.events.send(MessageEvent::Sent { let _ = self.events.send(MessageEvent::Sent {
id: row_id, id: row_id,
from: hive_sh4re::SYSTEM_SENDER.to_owned(), from: hive_sh4re::manager::SYSTEM_SENDER.to_owned(),
to: child.to_owned(), to: child.to_owned(),
body, body,
at: now, at: now,
@ -583,7 +588,7 @@ impl Broker {
id, id,
redelivered, redelivered,
message: Message { message: Message {
from: hive_sh4re::trusted_sender(&from), from: hive_sh4re::manager::trusted_sender(&from),
to, to,
body, body,
in_reply_to, in_reply_to,

View file

@ -165,13 +165,15 @@ impl OperatorQuestions {
// can additionally override agent-to-agent questions to close // can additionally override agent-to-agent questions to close
// stuck threads). // stuck threads).
let authorised = match target.as_deref() { let authorised = match target.as_deref() {
None => answerer == hive_sh4re::OPERATOR_RECIPIENT, None => answerer == hive_sh4re::manager::OPERATOR_RECIPIENT,
Some(t) => answerer == t || answerer == hive_sh4re::OPERATOR_RECIPIENT, Some(t) => answerer == t || answerer == hive_sh4re::manager::OPERATOR_RECIPIENT,
}; };
if !authorised { if !authorised {
bail!( bail!(
"question {id} not addressed to '{answerer}' (target = {:?})", "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( conn.execute(
@ -216,8 +218,9 @@ impl OperatorQuestions {
if answered_at.is_some() { if answered_at.is_some() {
bail!("question {id} already answered/cancelled"); bail!("question {id} already answered/cancelled");
} }
let authorised = let authorised = privileged
privileged || canceller == asker || canceller == hive_sh4re::OPERATOR_RECIPIENT; || canceller == asker
|| canceller == hive_sh4re::manager::OPERATOR_RECIPIENT;
if !authorised { if !authorised {
bail!("question {id}: '{canceller}' not allowed to cancel (asker = '{asker}')"); bail!("question {id}: '{canceller}' not allowed to cancel (asker = '{asker}')");
} }

View file

@ -105,7 +105,7 @@ fn emit_crash_transitions(coord: &Coordinator, prev: &HashSet<String>, current:
} }
tracing::warn!(agent = %stopped, "container crash detected"); tracing::warn!(agent = %stopped, "container crash detected");
coord.record_crash(stopped); coord.record_crash(stopped);
coord.notify_manager(&hive_sh4re::HelperEvent::ContainerCrash { coord.notify_manager(&hive_sh4re::manager::HelperEvent::ContainerCrash {
agent: stopped.clone(), agent: stopped.clone(),
note: Some("container stopped without an operator action".into()), note: Some("container stopped without an operator action".into()),
}); });
@ -138,7 +138,7 @@ async fn emit_login_transitions(
tracing::info!(%agent, "agent logged in"); tracing::info!(%agent, "agent logged in");
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::MANAGER_AGENT, hive_sh4re::manager::MANAGER_AGENT,
"core", "core",
Some(format!("logged_in:{agent}")), Some(format!("logged_in:{agent}")),
format!("agent '{agent}' logged in"), format!("agent '{agent}' logged in"),
@ -169,7 +169,7 @@ async fn emit_login_transitions(
tracing::info!(%agent, "agent needs login"); tracing::info!(%agent, "agent needs login");
let _ = coord let _ = coord
.push_todo( .push_todo(
hive_sh4re::MANAGER_AGENT, hive_sh4re::manager::MANAGER_AGENT,
"core", "core",
Some(format!("needs_login:{agent}")), Some(format!("needs_login:{agent}")),
format!("agent '{agent}' needs login"), format!("agent '{agent}' needs login"),

View file

@ -344,7 +344,7 @@ async fn broadcast_change(coord: &Coordinator, before: &str, after: &str) {
} else { } else {
format!("[system] /knowledge updated:\n{stat}") 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() { if !errors.is_empty() {
tracing::warn!(?errors, "knowledge: broadcast had per-agent failures"); tracing::warn!(?errors, "knowledge: broadcast had per-agent failures");
} }

View file

@ -100,7 +100,7 @@ async fn fire_schedule(coord: &Arc<Coordinator>, schedule: &Schedule, now: i64)
// no in-container todo inbox, so it keeps the regular broker // no in-container todo inbox, so it keeps the regular broker
// `Message` path; the dashboard mirrors `to == operator` into // `Message` path; the dashboard mirrors `to == operator` into
// its own pane. // 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}"); let reason = format!("no such agent: {target}");
if let Err(e) = if let Err(e) =
coord coord
@ -169,9 +169,9 @@ async fn deliver_to_target(
target: &str, target: &str,
body: &str, body: &str,
) -> Result<(), String> { ) -> Result<(), String> {
if target == hive_sh4re::OPERATOR_RECIPIENT { if target == hive_sh4re::manager::OPERATOR_RECIPIENT {
let msg = Message { let msg = Message {
from: hive_sh4re::trusted_sender("scheduled"), from: hive_sh4re::manager::trusted_sender("scheduled"),
to: target.to_owned(), to: target.to_owned(),
body: body.to_owned(), body: body.to_owned(),
in_reply_to: None, in_reply_to: None,
@ -205,8 +205,8 @@ fn notify_operator_missing_target(coord: &Coordinator, schedule: &Schedule, targ
body = schedule.body body = schedule.body
); );
let msg = Message { let msg = Message {
from: hive_sh4re::trusted_sender("scheduled"), from: hive_sh4re::manager::trusted_sender("scheduled"),
to: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), to: hive_sh4re::manager::OPERATOR_RECIPIENT.to_owned(),
body, body,
in_reply_to: None, in_reply_to: None,
}; };
@ -291,7 +291,7 @@ pub async fn fire_now(
continue; continue;
} }
let target = &target_row.target; 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}"); let reason = format!("manual fire: no such agent: {target}");
if let Err(e) = if let Err(e) =
coord coord
@ -371,7 +371,7 @@ pub async fn fire_now(
async fn known_agents() -> std::collections::HashSet<String> { async fn known_agents() -> std::collections::HashSet<String> {
use std::collections::HashSet; use std::collections::HashSet;
let mut out: HashSet<String> = HashSet::new(); let mut out: HashSet<String> = HashSet::new();
out.insert(hive_sh4re::MANAGER_AGENT.to_owned()); out.insert(hive_sh4re::manager::MANAGER_AGENT.to_owned());
match crate::lifecycle::list().await { match crate::lifecycle::list().await {
Ok(list) => { Ok(list) => {
for raw in list { for raw in list {

View file

@ -7,9 +7,10 @@
//! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …) //! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …)
//! stay in `hive-sh4re`, which this crate depends on. //! stay in `hive-sh4re`, which this crate depends on.
use hive_sh4re::manager::SchedulePromptPayload;
use hive_sh4re::{ use hive_sh4re::{
CancelLooseEndKind, ContainerInfo, DeliveredMessage, InboxRow, JournalPriority, LooseEnd, CancelLooseEndKind, ContainerInfo, DeliveredMessage, InboxRow, JournalPriority, LooseEnd,
MatrixIdentity, SchedulePromptPayload, WireSchedule, MatrixIdentity, WireSchedule,
}; };
use hive_types::Ident; use hive_types::Ident;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
pub mod approvals; pub mod approvals;
pub mod assets; pub mod assets;
pub mod bash_task; pub mod bash_task;
pub mod manager;
pub mod paths; pub mod paths;
pub mod wire_time; pub mod wire_time;
@ -289,134 +290,6 @@ pub struct MatrixIdentity {
pub homeserver: String, 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: "<parent>", ...)` 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 = "<parent>";
/// Reserved magic recipient — `send(to: "<children>", ...)` 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 = "<children>";
/// 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tag: Option<String>,
},
/// 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<String>,
},
/// 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<String>,
#[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<String>,
/// 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<u64>,
/// 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<String>,
}
/// Named group of MCP tools an agent may be granted. The harness reads /// Named group of MCP tools an agent may be granted. The harness reads
/// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of /// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of
/// `snake_case` group names written by the meta renderer from per-agent /// `snake_case` group names written by the meta renderer from per-agent

133
hive-sh4re/src/manager.rs Normal file
View file

@ -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: "<parent>", ...)` 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 = "<parent>";
/// Reserved magic recipient — `send(to: "<children>", ...)` 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 = "<children>";
/// 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tag: Option<String>,
},
/// 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<String>,
},
/// 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<String>,
#[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<String>,
/// 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<u64>,
/// 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<String>,
}