Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-agent-mcp/src/send_allow.rs
atlas 746c522f4e drop the manager socket, the no-op migrate branch and other ruth special cases
start_manager bound the same path register_agent binds. The migrate branch, hive-priv-sock::MANAGER_NAME and MANAGER_CONTAINER were no-ops or dead. The send allow-list docs promised a manager exemption the code never had. Tests that used ruth as the manager fixture use plain names.
2026-10-16 21:42:26 +02:00

61 lines
2.5 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.
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, 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. `operator` is refused whatever
/// the list says, with the same matrix-pointing text the broker returns.
pub fn check_send_allowed(to: &str) -> Result<(), String> {
if to == hive_sh4re::manager::OPERATOR_RECIPIENT {
return Err(hive_sh4re::manager::OPERATOR_SEND_REFUSAL.to_owned());
}
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:?}. To reach the operator, use \
the matrix MCP tools if you have them."
))
}
#[cfg(test)]
mod tests {
use super::check_send_allowed;
#[test]
fn operator_is_refused_naming_matrix_and_the_no_matrix_fallback() {
let refusal = check_send_allowed(hive_sh4re::manager::OPERATOR_RECIPIENT)
.expect_err("operator must be refused");
assert!(refusal.contains("mcp__matrix__"), "{refusal}");
assert!(refusal.contains("set_status"), "{refusal}");
}
}