//! 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/agent-modules/mcp.nix`). 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, the operator, 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 to the /// operator instead). pub fn check_send_allowed(to: &str) -> Result<(), String> { if to == hive_sh4re::manager::OPERATOR_RECIPIENT { // Always allow the operator — the allow-list constrains peer // chatter, not the reporting line out, and an agent with no way // to say "I am stuck" is an agent that fails silently. // // This bypass used to be spelled ``, which the broker // resolved per `topology.json` and which fell back to `operator` // for a root agent. The parent field is gone now, so every // agent is what that fallback called a root — the exemption is // now written as the name it always resolved to. Same reachable // set, one fewer indirection. 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 services.hyperhive.agent.allowedRecipients \ (configured in agent.nix). Allowed: {allow:?}. The operator is always \ reachable — route through `send(to: \"{}\", …)` if you need to reach \ someone outside the allow-list.", hive_sh4re::manager::OPERATOR_RECIPIENT )) }