feat: forward requestable-block metadata to the device-agent socket - #5083
Conversation
|
There was a problem hiding this comment.
cubic analysis
1 issue found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="hooks/relay/agent_block_notify.go">
<violation number="1" location="hooks/relay/agent_block_notify.go:120">
P3: `decodeBlockEffect` takes a value that is already a decoded JSON `map[string]any` and then marshals it back to bytes and unmarshals it into `blockEffect`, a redundant allocation round-trip. The map's fields could be read directly (matching how the sibling `org_settings`/`skill_capture` decoders in client.go assert fields off the map), or the raw bytes kept around to unmarshal once. This is minor, but it adds avoidable work on the deny path and diverges from the file's own decoding pattern.</violation>
</file>
Linked issue analysis
Linked issue: DNO-804: feat(hooks): forward requestable-block metadata to the device-agent socket
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Decode ingest response Effects["block"] into a typed blockEffect and attach it to ingestResult | client.go adds blockEffect type, decodeBlockEffect, and sets out.blockEffect when a block effect is present. |
| ✅ | Thread blockEffect onto the verdict for denied gating events and call the notifier from tool-pre and permission handlers | verdict carries blockEffect; evaluate sets it for denied responses; onToolPre and onPermission call notifyAgentBlock when v.block is true. |
| ✅ | Implement best-effort synchronous POST to device agent at /v1/blocks/report with a hard 300ms budget, one attempt, DisableKeepAlives, and swallow errors (404 ignored) | notifyAgentBlock uses a 300ms timeout, posts to http://speakeasy-agent/v1/blocks/report over agent socket via dialAgentSocket, uses DisableKeepAlives, and logs/ignores errors and non-202 responses. |
| ✅ | Only notify on deny path and only for requestable, supported-version effects with a token (never notify on allow or for unusable effects) | notifyAgentBlock short-circuits unless effect != nil, Requestable true, V <= 1, and RequestToken non-empty; notify is only invoked when verdict.block is true (deny). |
| ✅ | Add tests: assert payload contents, that a hung daemon costs ≤ budget, 404/absent socket do not change the deny, and allow never dials | agent_block_notify_test.go includes tests that start a fake agent socket, assert the recorded payload, verify hung-daemon timing bound, 404/absent socket behavior, and that allow path does not dial. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| if !ok { | ||
| return nil | ||
| } | ||
| encoded, err := json.Marshal(object) |
There was a problem hiding this comment.
P3: decodeBlockEffect takes a value that is already a decoded JSON map[string]any and then marshals it back to bytes and unmarshals it into blockEffect, a redundant allocation round-trip. The map's fields could be read directly (matching how the sibling org_settings/skill_capture decoders in client.go assert fields off the map), or the raw bytes kept around to unmarshal once. This is minor, but it adds avoidable work on the deny path and diverges from the file's own decoding pattern.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/relay/agent_block_notify.go, line 120:
<comment>`decodeBlockEffect` takes a value that is already a decoded JSON `map[string]any` and then marshals it back to bytes and unmarshals it into `blockEffect`, a redundant allocation round-trip. The map's fields could be read directly (matching how the sibling `org_settings`/`skill_capture` decoders in client.go assert fields off the map), or the raw bytes kept around to unmarshal once. This is minor, but it adds avoidable work on the deny path and diverges from the file's own decoding pattern.</comment>
<file context>
@@ -0,0 +1,129 @@
+ if !ok {
+ return nil
+ }
+ encoded, err := json.Marshal(object)
+ if err != nil {
+ return nil
</file context>
d9536e3 to
a009fc3
Compare
4ad917c to
c67e2fb
Compare
…ocket (DNO-804) Decode the ingest response's "block" effect and, on a denied gating event (tool pre / permission), best-effort POST it to the device agent's /v1/blocks/report so the agent can surface a native "request access" notification. Synchronous fire-and-forget with a hard 300ms budget: hook processes are one-shot (a detached goroutine dies at exit), the call is already denied and paid a WAN round trip, and a missing socket fails the dial instantly — only a hung daemon ever pays the budget. Never on the allow path; all errors swallowed; 404 = daemon predates the contract. Spooled/drained replays never reach the gating handlers, so stale denies cannot re-notify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqNdUoJxcz85saAAyPodoW
a009fc3 to
e49fbc6
Compare
One attempt means one request; also tighten the hung-agent test bound so a budget regression actually fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqNdUoJxcz85saAAyPodoW
c67e2fb to
59b67a4
Compare
Part 3 of Request access for blocked AI tool calls (DNO-804). Stacked on #5082.
Why
The device agent can only offer a native "request access" notification if it learns about requestable denies. The hooks binary is the process holding that verdict at the moment it happens.
What
blockeffect and thread it onto the deny verdict (tool-pre / permission gating events only).POST /v1/blocks/reportto the device agent's socket, hard 300 ms budget, one attempt, all errors swallowed. Reuses the identity socket's path + build-tagged dial pair, so Windows named-pipe parity comes free.The deny itself is byte-for-byte unchanged in every case — machines without the agent fail the dial instantly.
Test plan
go test ./...inhooks/green (two pre-existing MCP-inventory test failures reproduce on pristine main and are unrelated).🤖 Generated with Claude Code
https://claude.ai/code/session_01LqNdUoJxcz85saAAyPodoW
Summary by cubic
Forward requestable-block metadata to the device agent so it can show a native “Request access” notification on blocked AI tool calls (DNO-804). Deny behavior is unchanged; notify is best-effort with a 300 ms cap.
blockeffect into a structuredblockEffectinhooks/relay/client.go./v1/blocks/reportover the identity socket; payload includes effect fields plusprovider,session_id, andblocked_at.DisableKeepAlives; all errors swallowed.404means an older daemon and is ignored. Guards: only when requestable,v <= 1, and token present; never on allow; replayed events never trigger.Written for commit 59b67a4. Summary will update on new commits.