58 lines
2.6 KiB
Rust
58 lines
2.6 KiB
Rust
//! 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, `<parent>` 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 `<parent>` instead).
|
|
pub fn check_send_allowed(to: &str) -> Result<(), String> {
|
|
if to == hive_sh4re::PARENT_RECIPIENT {
|
|
// Always allow `<parent>` — 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<String> = 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
|
|
))
|
|
}
|