//! Per-agent `send` allow-list enforcement. Driven by the operator config at //! `/etc/hyperhive/send-allow.json` (written by the NixOS harness module); the //! MCP server calls [`check_send_allowed`] before forwarding a `send` tool call //! so a blocked recipient surfaces as a claude-readable tool result rather than //! a silent drop. /// Where the NixOS module writes the per-agent send allow-list (see /// `nix/templates/harness/`). Empty list = unrestricted (the /// default). Non-empty list constrains `mcp__hyperhive__send`'s `to` /// field; the manager is always implicitly permitted regardless of /// the list contents. const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json"; /// Enforce the per-agent send allow-list. Returns `Ok` when the /// recipient is permitted (no list configured, `` sentinel /// always allowed, or `to` is in the list); returns `Err(refusal)` /// with a claude-readable string when blocked — the harness surfaces /// 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 { // 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 // 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 (falls back to `operator` // for root agents). return Ok(()); } let Ok(raw) = std::fs::read_to_string(SEND_ALLOW_PATH) else { return Ok(()); // file missing → no policy configured → unrestricted }; let allow: Vec = match serde_json::from_str(&raw) { Ok(v) => v, Err(e) => { tracing::warn!( path = SEND_ALLOW_PATH, error = ?e, "send allow-list parse failed; falling back to unrestricted", ); return Ok(()); } }; if allow.is_empty() { return Ok(()); // empty list = unrestricted (back-compat) } if allow.iter().any(|n| n == to) { return Ok(()); } Err(format!( "send refused: recipient '{to}' not in hyperhive.allowedRecipients \ (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 )) }