From c433bdef923943995155bf8b900aab445386dd9c Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 8 Oct 2026 08:33:27 +0200 Subject: [PATCH] hive-runtime: log the acp permission decision at the point it is made --- hive-runtime/src/acp/rpc.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/hive-runtime/src/acp/rpc.rs b/hive-runtime/src/acp/rpc.rs index 3875fd9f..acccdb8b 100644 --- a/hive-runtime/src/acp/rpc.rs +++ b/hive-runtime/src/acp/rpc.rs @@ -252,15 +252,17 @@ pub(super) fn permission_outcome( let tool_call = params.get("toolCall"); let field = |k: &str| tool_call.and_then(|t| t.get(k)).and_then(Value::as_str); let kind = field("kind").unwrap_or("other"); + let title = field("title"); let ask = PermissionAsk { kind, // MCP tool calls are kind `other`; a title alone must not lift another // kind (`execute`, `fetch`) into one. mcp_server: (kind == "other") - .then(|| field("title").and_then(|title| mcp_server_of(title, servers))) + .then(|| title.and_then(|title| mcp_server_of(title, servers))) .flatten(), }; - let wanted = if permit(&ask) { + let permitted = permit(&ask); + let wanted = if permitted { ["allow_once", "allow_always"] } else { ["reject_once", "reject_always"] @@ -275,6 +277,30 @@ pub(super) fn permission_outcome( .find(|o| o.get("kind").and_then(Value::as_str) == Some(want)) .and_then(|o| o.get("optionId")) }); + // Log what the agent was actually told, not just the policy's + // pre-resolution answer: `permit` saying yes doesn't mean the agent + // offered a matching `allow_*` option, and that silent-cancel case is + // exactly the shape a reporter sees as "tool call refused, nothing + // actionable in the logs" — the earlier version of this log line + // reported `permitted` and would have called that case "allowed". + // `title` is only logged for `kind == "other"` (it identifies the MCP + // tool/server) — an `execute` title can carry a command line and must + // not land in the journal at this level. + let other_title = (kind == "other").then_some(title).flatten(); + match chosen { + Some(option) if permitted => { + tracing::debug!(kind, mcp_server = ?ask.mcp_server, option = %option, "ACP permission request allowed"); + } + Some(option) => { + tracing::info!(kind, mcp_server = ?ask.mcp_server, title = other_title, option = %option, "ACP permission request denied by policy"); + } + None => { + // An anomaly, not a policy decision: the agent offered no + // option matching what the policy wanted, so the request is + // cancelled regardless of `permitted`. + tracing::warn!(kind, mcp_server = ?ask.mcp_server, title = other_title, permitted, "ACP permission request cancelled — agent offered no matching option"); + } + } match chosen { Some(option) => json!({ "outcome": { "outcome": "selected", "optionId": option } }), None => json!({ "outcome": { "outcome": "cancelled" } }),