diff --git a/docs/conventions.md b/docs/conventions.md index 37155c62..f61a56ae 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -21,29 +21,6 @@ bind-mount." A sub-agent only sees its own `/run/hive/mcp.sock`; the manager has access to its privileged socket; hive-c0re owns the host admin socket. -## Recipient sentinels - -A few recipient names are reserved by the broker and have special -meaning that ordinary agent labels can never collide with — agent -name validation rejects any character outside `[a-z0-9_-]`, so the -angle-bracket and asterisk shapes below are structurally safe. - -- `*` — broadcast: deliver to every running agent except the sender - (`agent_server::handle_send` fans out via `Coordinator::broadcast_send`). -- `operator` — the human at the dashboard. Messages accumulate in the - inbox view; no agent ever `recv`'s them. -- `` — the sender's parent per `topology.json` (`#692`). - Rewritten at send time by `topology::resolve_recipient`: looks up - `parent_of(sender)` and falls back to `operator` when the sender is - a root agent (or absent from topology entirely). Lets agents address - their parent without learning the label, so runtime reparenting - (`#486`) propagates with zero agent-side restart. - -When the resolver rewrites ``, the broker stores the -*resolved* label as the message's recipient — the dashboard and -recv side both see the real route. The sentinel is purely a -send-time addressing convenience. - ## Wire protocol JSON line-delimited over unix sockets in both directions (host admin diff --git a/hive-ag3nt/prompts/system.md b/hive-ag3nt/prompts/system.md index 6514fca7..43332915 100644 --- a/hive-ag3nt/prompts/system.md +++ b/hive-ag3nt/prompts/system.md @@ -8,7 +8,7 @@ You are the hyperhive manager `{label}` (qualified: `{qualified_label}`) in a mu Tools (hyperhive surface): - `mcp__hyperhive__recv(wait_seconds?, max?)` — drain inbox messages (returns `(empty)` if nothing pending). Without `wait_seconds` (or with `0`) it returns immediately — a cheap "anything pending?" peek you can sprinkle between tool calls. To **wait** for work when you have nothing else useful to do this turn, call with a long wait (e.g. `wait_seconds: 180`, the max) — incoming messages wake you instantly, otherwise the call returns empty at the timeout. That's strictly better than a fixed `sleep` shell command: lower latency on new work, no busy-loop. `max` (default 1, cap 32) drains several queued messages in one call — the wake prompt tells you the pending count. -- `mcp__hyperhive__send(to, body, in_reply_to?)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). Use `to: ""` to address your structural parent without hardcoding their name — hive-c0re rewrites it at delivery time per `topology.json`, falling back to `operator` if you're a root agent. Lets the operator reparent you at runtime with zero change on your side. Optional `in_reply_to: ` threads this message under a prior one — the dashboard and per-agent inbox render it with a `↳ reply` link. Some agents have a per-agent allow-list (`hyperhive.allowedRecipients` in their `agent.nix`) — if so the tool refuses recipients outside the list with a clear error; route through the manager (`send(to: "manager", …)`) which is always reachable. +- `mcp__hyperhive__send(to, body, in_reply_to?)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). Optional `in_reply_to: ` threads this message under a prior one — the dashboard and per-agent inbox render it with a `↳ reply` link. Some agents have a per-agent allow-list (`hyperhive.allowedRecipients` in their `agent.nix`) — if so the tool refuses recipients outside the list with a clear error; route through the manager (`send(to: "manager", …)`) which is always reachable. - (some agents only) **extra MCP tools** surfaced as `mcp____` — these are agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. Treat them as first-class tools alongside the hyperhive surface; the operator already auto-approved them at deploy time. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 580e6e93..ca94aa6c 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -1883,16 +1883,6 @@ fn check_send_allowed(to: &str) -> Result<(), String> { // to ask for help. return Ok(()); } - if to == hive_sh4re::PARENT_RECIPIENT { - // Always allow `` — same escape-hatch rationale as - // the manager exception. The allow-list constrains peer - // chatter, not the structural reporting line; the operator - // can rewire who the parent IS via `set_parent` without - // having to remember to update the per-agent allow-list. - // The broker resolves the sentinel to the real parent label - // on the host side per topology.json (#692). - return Ok(()); - } let Ok(raw) = std::fs::read_to_string(SEND_ALLOW_PATH) else { return Ok(()); // file missing → no policy configured → unrestricted }; diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index a46a456d..f6ae52d3 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -328,14 +328,9 @@ fn handle_send( } }; } - // Resolve magic-recipient sentinels (currently ``) against - // topology.json; no-op for ordinary names. Lets agents address - // structural roles without learning the label — runtime reparenting - // (#486) propagates for free (#692). - let resolved = crate::topology::resolve_recipient(agent, to); match coord.broker.send(&Message { from: agent.to_owned(), - to: resolved, + to: to.to_owned(), body: body.to_owned(), in_reply_to, }) { diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index d867740d..9d253e2d 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -113,15 +113,9 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc) -> ManagerResp } } } else { - // Resolve magic-recipient sentinels (currently ``) - // against topology.json; no-op for ordinary names. The - // manager has no parent in topology, so `` - // resolves to OPERATOR_RECIPIENT — matching mara's - // "no parent → tell the operator" rule (#692). - let resolved = crate::topology::resolve_recipient(MANAGER_AGENT, to); match coord.broker.send(&Message { from: MANAGER_AGENT.to_owned(), - to: resolved, + to: to.clone(), body: body.clone(), in_reply_to: *in_reply_to, }) { diff --git a/hive-c0re/src/topology.rs b/hive-c0re/src/topology.rs index 128332cb..71ef41e1 100644 --- a/hive-c0re/src/topology.rs +++ b/hive-c0re/src/topology.rs @@ -58,60 +58,15 @@ pub fn read() -> BTreeMap> { /// or absent from the file. Cheap convenience over `read()` for /// callers that want a single entry. #[must_use] +#[allow( + dead_code, + reason = "convenience API; callers go through `read()` today, kept for the \ + dashboard/manager-server surfaces landing in #361 follow-ups" +)] pub fn parent_of(name: &str) -> Option { read().get(name).cloned().flatten() } -/// Resolve a magic recipient sentinel (currently just -/// [`hive_sh4re::PARENT_RECIPIENT`]) to a real broker recipient at -/// send time. Lets agents address structural roles -/// (`send(to: "", ...)`) without learning their parent's -/// label — runtime reparenting (#486) propagates for free. -/// -/// Resolution rules: -/// - `` → `topology.json`'s parent for `sender` if some, -/// else [`hive_sh4re::OPERATOR_RECIPIENT`] (the "no parent → tell -/// mara" fallback from #692 / #8592). -/// - Anything else: returned unchanged. -/// -/// Returns an owned `String` for the rewritten recipient so callers -/// can plug it straight into [`crate::broker::Broker::send`] without -/// borrow-juggling around the temporary lookup. -/// -/// Fast path: ordinary recipient names (the overwhelming majority of -/// sends) short-circuit before touching the disk — only `` -/// triggers the `read()` of `topology.json`. Sentinel-free traffic -/// pays a single string compare; the disk read is amortised across -/// every `` send (per argus on #703#issuecomment-8750). -#[must_use] -pub fn resolve_recipient(sender: &str, to: &str) -> 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 { - return to.to_owned(); - } - resolve_recipient_in(&read(), sender, to) -} - -/// Pure form of [`resolve_recipient`] taking the topology map -/// explicitly. Split out so unit tests can exercise the sentinel -/// rules without writing a `topology.json` to disk. -#[must_use] -pub fn resolve_recipient_in( - topo: &BTreeMap>, - sender: &str, - to: &str, -) -> String { - if to == hive_sh4re::PARENT_RECIPIENT { - topo.get(sender) - .cloned() - .flatten() - .unwrap_or_else(|| hive_sh4re::OPERATOR_RECIPIENT.to_owned()) - } else { - to.to_owned() - } -} - /// True when `candidate` is `ancestor` or any descendant of /// `ancestor` per the current topology. Walks parents from /// `candidate` upward; the walk terminates at root or on a cycle @@ -396,61 +351,4 @@ mod tests { let next = apply_set_parent(&topo_three_level(), "bob", Some("alice")).unwrap(); assert_eq!(next, topo_three_level()); } - - #[test] - fn resolve_recipient_passes_through_ordinary_names() { - let topo = topo_three_level(); - // Real labels, broadcast, and the operator literal all - // shortcut through unchanged — no resolution magic. - 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 - ); - } - - #[test] - fn resolve_recipient_rewrites_parent_sentinel_to_parent_label() { - 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), - "alice" - ); - // alice's parent is the manager — same one-hop rewrite. - assert_eq!( - resolve_recipient_in(&topo, "alice", hive_sh4re::PARENT_RECIPIENT), - crate::lifecycle::MANAGER_NAME - ); - } - - #[test] - fn resolve_recipient_falls_back_to_operator_for_root_agent() { - let topo = topo_three_level(); - // Manager is structurally root (parent = None) → `` - // resolves to the operator (matching the "no parent → tell mara" - // rule from #692#issuecomment-8592). - assert_eq!( - resolve_recipient_in( - &topo, - crate::lifecycle::MANAGER_NAME, - hive_sh4re::PARENT_RECIPIENT - ), - hive_sh4re::OPERATOR_RECIPIENT - ); - } - - #[test] - fn resolve_recipient_falls_back_to_operator_for_unknown_sender() { - // Sender absent from topology entirely — defensive fallback - // covers the race window where an agent's spawn has registered - // its socket but the meta-flake `sync_agents` hasn't yet added - // its row. - let topo = topo_three_level(); - assert_eq!( - resolve_recipient_in(&topo, "nobody", hive_sh4re::PARENT_RECIPIENT), - hive_sh4re::OPERATOR_RECIPIENT - ); - } } diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 4a257a1a..000edcc9 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -598,15 +598,6 @@ pub const MANAGER_AGENT: &str = "manager"; /// 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 (#486) 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 = ""; - /// 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";