`nix/templates/harness/` is cited from `send_allow.rs` and twice from `mcp_config.rs`, in the present tense, as where the NixOS module writes the send allow-list, writes the extra-MCP spec, and defines the MCP http port default. There is no such directory — `nix/templates/` holds only `agent.nix` and `ruth.nix`. All three describe things that live in `nix/agent-modules/mcp.nix`: send-allow.json is written at :230, extra-mcp.json at :228, and the `hyperhive.mcp.httpPort` option is declared at :143. These are the mechanical half of #3923, which explicitly carved the repointing out from the question of whether the reference gate should widen. Nothing here changes that question.
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/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, `<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::manager::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::manager::PARENT_RECIPIENT
|
|
))
|
|
}
|