agents: remove send(operator) and the operator inbox; alert on turn failures
Agents reach the operator over matrix now (#5115). A `send` to `operator` is refused, by the broker and by the MCP allow-list check, with an error that names the matrix MCP tools. The dashboard's Y3R C4LL tab goes with it: the operator inbox, `GET /api/operator-inbox`, `Broker::unread_for_recipient`, and the browser notifications that only ever fired on operator-bound messages. The harness no longer posts turn failures and plugin-install failures to the operator inbox. Both are already logged at warn; a new Grafana rule, `hh-agent-turn-failed`, fires per agent on the harness's `turn failed` line. Dashboard-to-agent messaging (op-send, the agent web UI's message box, mark-all-read for agent inboxes) is unchanged. Decisions per mara on #5115: q1 a, q2 a plus a grafana alert, q3 keep dashboard-to-agent messaging and leave swarm-level code alone.
This commit is contained in:
parent
2bf809cbe5
commit
7e6a4c7727
37 changed files with 246 additions and 848 deletions
|
|
@ -7,7 +7,7 @@ use rmcp::schemars;
|
|||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct SendArgs {
|
||||
/// Logical agent name to deliver the message to (e.g. `"manager"`,
|
||||
/// `"alice"`, or the literal `"operator"` for the dashboard's T4LK box).
|
||||
/// `"alice"`). `"operator"` is refused: reach the operator over matrix.
|
||||
pub to: String,
|
||||
/// Message body. Plain text; the broker doesn't parse it.
|
||||
pub body: String,
|
||||
|
|
|
|||
|
|
@ -141,8 +141,8 @@ impl AgentServer {
|
|||
#[tool_router]
|
||||
impl AgentServer {
|
||||
#[tool(
|
||||
description = "Send a message to another hyperhive agent (or to the operator). \
|
||||
Use this to talk to peers or to surface output for the human at the dashboard."
|
||||
description = "Send a message to another hyperhive agent on this hive. \
|
||||
Not for the operator: reach them over matrix with the matrix MCP tools."
|
||||
)]
|
||||
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
|
|
@ -500,7 +500,7 @@ impl AgentServer {
|
|||
|
||||
#[tool_handler(
|
||||
instructions = "You are a hyperhive agent. Use `send` to talk to peers (by their logical \
|
||||
name) or to the operator (recipient `operator`). Use `recv` to drain your inbox one \
|
||||
name); reach the operator over matrix. Use `recv` to drain your inbox one \
|
||||
message at a time. Use `remind` to schedule a future wake-up message for yourself."
|
||||
)]
|
||||
impl ServerHandler for AgentServer {}
|
||||
|
|
|
|||
|
|
@ -12,24 +12,14 @@
|
|||
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).
|
||||
/// 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 {
|
||||
// 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(());
|
||||
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
|
||||
|
|
@ -53,9 +43,19 @@ pub fn check_send_allowed(to: &str) -> Result<(), String> {
|
|||
}
|
||||
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
|
||||
(configured in agent.nix). Allowed: {allow:?}. To reach the operator, use \
|
||||
the matrix MCP tools."
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::check_send_allowed;
|
||||
|
||||
#[test]
|
||||
fn operator_is_refused_with_the_matrix_pointer() {
|
||||
let refusal = check_send_allowed(hive_sh4re::manager::OPERATOR_RECIPIENT)
|
||||
.expect_err("operator must be refused");
|
||||
assert!(refusal.contains("mcp__matrix__"), "{refusal}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue