hive-agent-mcp: the send allow-list is gone, agents talk to peers over matrix
This commit is contained in:
parent
f23ce106b1
commit
c1f7bb798e
8 changed files with 14 additions and 97 deletions
|
|
@ -44,9 +44,7 @@ runs on the change.
|
||||||
`get_agent_meta`'s `matrix_accounts`); `send` is for peers without a matrix
|
`get_agent_meta`'s `matrix_accounts`); `send` is for peers without a matrix
|
||||||
account and agents without matrix tools. Optional
|
account and agents without matrix tools. Optional
|
||||||
`in_reply_to: i64` links the message to a prior id for thread
|
`in_reply_to: i64` links the message to a prior id for thread
|
||||||
rendering. Per-agent `services.hyperhive.agent.allowedRecipients`
|
rendering. The broker refuses `to: "operator"`. The error
|
||||||
(default: empty = unrestricted) limits which names `send` accepts —
|
|
||||||
useful for sandboxing. The broker refuses `to: "operator"`. The error
|
|
||||||
points an agent that has the matrix MCP tools at matrix
|
points an agent that has the matrix MCP tools at matrix
|
||||||
([matrix](../agent-tools/matrix.md)), and tells one without them that the
|
([matrix](../agent-tools/matrix.md)), and tells one without them that the
|
||||||
operator reads its `set_status` line, its turn output on the dashboard,
|
operator reads its `set_status` line, its turn output on the dashboard,
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ or their own daemon instead of living here.
|
||||||
(`/run/hive/mcp.sock`) or, for loose-ends v2 (todos/reminders), the
|
(`/run/hive/mcp.sock`) or, for loose-ends v2 (todos/reminders), the
|
||||||
in-agent socket the `hive-agent` harness serves.
|
in-agent socket the `hive-agent` harness serves.
|
||||||
- **`client.rs`** — socket client to the hyperhive broker.
|
- **`client.rs`** — socket client to the hyperhive broker.
|
||||||
- **`send_allow.rs`** — enforces the per-agent
|
|
||||||
`services.hyperhive.agent.allowedRecipients` allow-list on `send`.
|
|
||||||
- **`paths.rs`** — socket + state path resolution shared with the
|
- **`paths.rs`** — socket + state path resolution shared with the
|
||||||
harness's own `paths.rs` conventions.
|
harness's own `paths.rs` conventions.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ use clap::Parser;
|
||||||
|
|
||||||
mod mcp;
|
mod mcp;
|
||||||
mod paths;
|
mod paths;
|
||||||
mod send_allow;
|
|
||||||
|
|
||||||
/// Per-agent MCP socket, bind-mounted from the host into every container.
|
/// Per-agent MCP socket, bind-mounted from the host into every container.
|
||||||
const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock";
|
const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock";
|
||||||
|
|
|
||||||
|
|
@ -150,11 +150,6 @@ impl AgentServer {
|
||||||
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
|
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
|
||||||
let log = format!("{args:?}");
|
let log = format!("{args:?}");
|
||||||
let to = args.to.clone();
|
let to = args.to.clone();
|
||||||
// Check per-agent allow-list (services.hyperhive.agent.allowedRecipients). When no
|
|
||||||
// policy file is present the check is a no-op.
|
|
||||||
if let Err(refusal) = crate::send_allow::check_send_allowed(&to) {
|
|
||||||
return run_tool_envelope("send", log, async move { refusal }).await;
|
|
||||||
}
|
|
||||||
run_tool_envelope("send", log, async move {
|
run_tool_envelope("send", log, async move {
|
||||||
let (resp, retries) = self
|
let (resp, retries) = self
|
||||||
.dispatch(hive_core_agent_sock::Request::Send {
|
.dispatch(hive_core_agent_sock::Request::Send {
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
//! 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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -34,6 +34,12 @@ let
|
||||||
sets it. Pin it host-side with
|
sets it. Pin it host-side with
|
||||||
services.hyperhive.deploy.hive-controller.claudeCodePackage.
|
services.hyperhive.deploy.hive-controller.claudeCodePackage.
|
||||||
'';
|
'';
|
||||||
|
sendAllowRemoved = ''
|
||||||
|
The `send` allow-list is gone: it limited only the hive-local `send` tool,
|
||||||
|
which is not the channel agents use to reach peers, and it was never a
|
||||||
|
boundary. Agents talk to peers over matrix. Remove the setting.
|
||||||
|
'';
|
||||||
|
|
||||||
queueRemoved = ''
|
queueRemoved = ''
|
||||||
The harness reads the queue's address at runtime from HIVE_AGENT_NATS_URL,
|
The harness reads the queue's address at runtime from HIVE_AGENT_NATS_URL,
|
||||||
in /etc/hyperhive-hive-facts, and authenticates with the
|
in /etc/hyperhive-hive-facts, and authenticates with the
|
||||||
|
|
@ -115,6 +121,12 @@ in
|
||||||
"agent"
|
"agent"
|
||||||
"claudeCodePath"
|
"claudeCodePath"
|
||||||
] claudeCodePathRemoved)
|
] claudeCodePathRemoved)
|
||||||
|
(lib.mkRemovedOptionModule [
|
||||||
|
"services"
|
||||||
|
"hyperhive"
|
||||||
|
"agent"
|
||||||
|
"allowedRecipients"
|
||||||
|
] sendAllowRemoved)
|
||||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "queue" "natsUrl" ] queueRemoved)
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "queue" "natsUrl" ] queueRemoved)
|
||||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "queue" "tokenEndpoint" ] queueRemoved)
|
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "queue" "tokenEndpoint" ] queueRemoved)
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -22,27 +22,6 @@ let
|
||||||
subagentReadsStore = config.services.hyperhive.agent.bao.addr != null;
|
subagentReadsStore = config.services.hyperhive.agent.bao.addr != null;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.hyperhive.agent.allowedRecipients = lib.mkOption {
|
|
||||||
type = lib.types.listOf lib.types.str;
|
|
||||||
default = [ ];
|
|
||||||
example = [
|
|
||||||
"alice"
|
|
||||||
"bob"
|
|
||||||
];
|
|
||||||
description = ''
|
|
||||||
Names this agent is allowed to `send` to via
|
|
||||||
`mcp__hyperhive__send`. Empty list (the default) means
|
|
||||||
unrestricted — the agent can message any peer. A non-empty list
|
|
||||||
allows only the listed names; anything else returns an error
|
|
||||||
string to claude without touching the broker.
|
|
||||||
`operator` is refused either way: agents reach the operator
|
|
||||||
over matrix.
|
|
||||||
|
|
||||||
Useful for sandboxing untrusted sub-agents — set
|
|
||||||
`[ "alice" ]` to scope one to chatter with alice.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
options.services.hyperhive.agent.extraMcpServers = lib.mkOption {
|
options.services.hyperhive.agent.extraMcpServers = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf (
|
||||||
lib.types.submodule {
|
lib.types.submodule {
|
||||||
|
|
@ -304,9 +283,6 @@ in
|
||||||
environment.etc."hyperhive/extra-mcp.json".text =
|
environment.etc."hyperhive/extra-mcp.json".text =
|
||||||
builtins.toJSON config.services.hyperhive.agent.extraMcpServers;
|
builtins.toJSON config.services.hyperhive.agent.extraMcpServers;
|
||||||
|
|
||||||
environment.etc."hyperhive/send-allow.json".text =
|
|
||||||
builtins.toJSON config.services.hyperhive.agent.allowedRecipients;
|
|
||||||
|
|
||||||
# Bash task runner daemon — long-running process that owns subprocess
|
# Bash task runner daemon — long-running process that owns subprocess
|
||||||
# monitoring + completion wake signals, and serves the MCP tools
|
# monitoring + completion wake signals, and serves the MCP tools
|
||||||
# (`run`/`status`/`kill`) directly over streamable-http on
|
# (`run`/`status`/`kill`) directly over streamable-http on
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ let
|
||||||
grafana, the swarm controller, authelia, …).
|
grafana, the swarm controller, authelia, …).
|
||||||
- [per-agent options](agent.md) — options declared in
|
- [per-agent options](agent.md) — options declared in
|
||||||
`nix/agent-modules/`, visible from every `agent.nix`
|
`nix/agent-modules/`, visible from every `agent.nix`
|
||||||
(`services.hyperhive.agent.model`, `services.hyperhive.agent.allowedRecipients`,
|
(`services.hyperhive.agent.model`,
|
||||||
`services.hyperhive.agent.extraMcpServers`, `services.hyperhive.agent.frontend.*`,
|
`services.hyperhive.agent.extraMcpServers`, `services.hyperhive.agent.frontend.*`,
|
||||||
`services.hyperhive.agent.forge.*`, `services.hyperhive.agent.matrix.*`, `services.hyperhive.agent.gui.*`).
|
`services.hyperhive.agent.forge.*`, `services.hyperhive.agent.matrix.*`, `services.hyperhive.agent.gui.*`).
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue