The prose added by this branch named the tracker item in seventeen places, which check-issue-refs.sh rejects: a `#N` tag is dead weight for anyone reading the public mirror, where no issue data exists. Each one now states the fact it was pointing at — the parent field is gone — so the sentence stands on its own. Two of those lines also carried a rustdoc break: `[`write`]` in topology.rs is ambiguous between the module's own `write` fn and the `write!` macro, which `-D rustdoc::broken-intra-doc-links` fails. Spelled `[`write()`]`, per rustdoc's own suggestion. The host_config.rs rewrite is two lines rather than three so the doc block stays under check-comment-blocks.sh's 30-line ceiling.
61 lines
2.8 KiB
Rust
61 lines
2.8 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, 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 `<parent>`, 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<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 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
|
|
))
|
|
}
|