Summary
Every MCP tool is wrapped by withAudit (src/mcp-server/src/server.ts), which sets details.success by whether the tool threw (src/mcp-server/src/audit.ts). The checkAgentAccess denials in src/mcp-server/src/tools/chat.ts return a JSON string ({"error": "Access denied", "reason": …}) instead of throwing, so the audit row for a refused agent-to-agent call says success: true and carries no denial marker. An operator reading the audit log cannot tell a permitted call from a refused one.
Context
Found while building the J10 journey harness (#2349): its "the denial is observable" criterion holds for the caller (the tool result says why) but not for the operator. The harness asserts today that the row exists, and carries the denial marker as a strict=True xfail.
Acceptance Criteria
Technical Notes
Smallest shape: the deny branch records the audit outcome itself (success=false, reason) before returning, or throws an error the wrapper classifies while a thin catch preserves the caller-visible JSON.
Summary
Every MCP tool is wrapped by
withAudit(src/mcp-server/src/server.ts), which setsdetails.successby whether the tool threw (src/mcp-server/src/audit.ts). ThecheckAgentAccessdenials insrc/mcp-server/src/tools/chat.tsreturn a JSON string ({"error": "Access denied", "reason": …}) instead of throwing, so the audit row for a refused agent-to-agent call sayssuccess: trueand carries no denial marker. An operator reading the audit log cannot tell a permitted call from a refused one.Context
Found while building the J10 journey harness (#2349): its "the denial is observable" criterion holds for the caller (the tool result says why) but not for the operator. The harness asserts today that the row exists, and carries the denial marker as a
strict=Truexfail.Acceptance Criteria
checkAgentAccessdenial inchat_with_agent, the dedicatedchat_with_<slug>tools andfan_outproduces an audit row withdetails.success == false(or an explicitdenied: trueplus the reason), without changing the JSON the caller receives — agents parse ittest_the_operator_can_see_that_a_call_was_refusedpasses and the marker is removed in the same PRTechnical Notes
Smallest shape: the deny branch records the audit outcome itself (success=false, reason) before returning, or throws an error the wrapper classifies while a thin catch preserves the caller-visible JSON.