feat: emit structured block effect on shadow-MCP deny responses - #5081
feat: emit structured block effect on shadow-MCP deny responses#5081bradcypert wants to merge 3 commits into
Conversation
…es (DNO-802) When the canonical ingest path denies a tool call for a shadow-MCP policy and mints a "Request access" link, mirror that link as a structured effects["block"] entry (category, request token/URL/expiry, observed server, policy, tool, block-row URL) so the hooks binary can hand the device agent something it can act on without parsing prose. Absence of the key is the "not requestable" signal: scan denies (PII, secrets, spend, prompt policies) carry nothing. Duplicate deliveries re-mint the link (the prose re-sends it too) but never a second block row, so their effect omits block_url. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqNdUoJxcz85saAAyPodoW
|
There was a problem hiding this comment.
cubic analysis
All reported issues were addressed across 6 files
Linked issue analysis
Linked issue: DNO-802: feat(server): emit structured block effect on shadow-MCP deny responses
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Emit a structured effects["block"] payload on shadow‑MCP denies with v=1, category "shadow_mcp", requestable=true, a short rpbr2.* request_token, request_url containing the fragment token, and request_expires_at in RFC3339. | The new withBlockEffect builds the payload including v, category, requestable, request_token, request_url, and formatted request_expires_at. Tests parse and assert token prefix, URL fragment, and RFC3339 expiry. |
| ✅ | Include server_name and server_url when available, plus policy_name and tool_name in the effect payload. | withBlockEffect maps server_name/server_url/policy_name/tool_name when present; shadowMCP approval link returns ServerName/ServerURL, and tests assert policy_name, tool_name, and server_name presence and server_url absence in identity-only cases. |
| ✅ | Attach block_url (durable block-row URL) on the first delivery only; duplicate deliveries must omit block_url while still including a re‑minted request_token/request_url. | evaluateCanonicalShadowMCP calls setBlockEffectBlockURL when a block row is created; withBlockEffect includes block_url only if present. Tests verify first delivery contains block_url and retry omits block_url but still contains an rpbr2.* token. |
| ✅ | Do not emit effects["block"] for allow decisions, scan-based denies (PII/prompt policies), or when link minting fails (e.g. missing site URL). | The collector is only populated for requestable shadow‑MCP denies; code paths and checks prevent creating the effect when siteURL is nil or when the deny is from scans. Tests assert absence on allow, scan denies, and mint-failure scenarios. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqNdUoJxcz85saAAyPodoW
NormalizeAccessEvidence keeps the query string and any embedded userinfo; the machine-readable channel must not re-expose credentials the deny prose never carried. scheme/host/path only, matching CanonicalizeInventoryURL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqNdUoJxcz85saAAyPodoW
Part 1 of Request access for blocked AI tool calls (DNO-802).
Why
When a shadow-MCP policy denies a tool call, the "Request access" link is buried as prose in the deny message the AI tool prints. The device agent wants to surface a native request flow, and it can't (and shouldn't) parse prose.
What
effects["block"]entry mirroring the minted request link: category,rpbr2.token + URL + expiry, observed server, policy, tool, and the durable block-row URL.block_url, matching the existing no-second-block-row rule.withRiskScanTracker) soevaluateCanonicalHook's ten deny branches keep their signature.Test plan
go test ./internal/hooks/ ./internal/risk/green; golangci-lint clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01LqNdUoJxcz85saAAyPodoW
Summary by cubic
Adds a structured
effects["block"]payload to shadow‑MCP deny responses on the canonical ingest path so device agents can start a native “Request access” flow without parsing prose. Part of DNO-802 (Request access for blocked AI tool calls).New Features
effects["block"]with v=1,category: "shadow_mcp",requestable: true,request_token(rpbr2.*),request_url,request_expires_at, plusserver_name/server_url,policy_name,tool_name, andblock_url(first delivery only).block_url.withBlockEffectCollector/withBlockEffectandsetBlockEffect/setBlockEffectBlockURL; wired into the canonical ingest deny path.shadow_mcp_request_linknow returns URL, token, expiry and sets the effect;PolicyNameplumbed through canonical and per-provider handlers (Claude/Codex/Cursor).GeneratePolicyBypassRequestURLnow returns(url, token, expiry, err).Bug Fixes
server_urlin the block effect to the inventory convention (scheme/host/path only); strips credentials, query, and fragment.Written for commit aed18f5. Summary will update on new commits.