Skip to content

feat: forward requestable-block metadata to the device-agent socket - #5083

Open
bradcypert wants to merge 2 commits into
brad/dno-803-featserver-allow-device-keys-agent_user-onfrom
brad/dno-804-feathooks-forward-requestable-block-metadata-to-the-device
Open

feat: forward requestable-block metadata to the device-agent socket#5083
bradcypert wants to merge 2 commits into
brad/dno-803-featserver-allow-device-keys-agent_user-onfrom
brad/dno-804-feathooks-forward-requestable-block-metadata-to-the-device

Conversation

@bradcypert

@bradcypert bradcypert commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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

  • Decode the ingest response's block effect and thread it onto the deny verdict (tool-pre / permission gating events only).
  • New best-effort notify: a synchronous fire-and-forget POST /v1/blocks/report to 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.
  • Guards: never on the allow path; only requestable effects with a token and a known contract version; spooled/drained replays never reach the gating handlers, so stale denies can't re-notify. A 404 means the daemon predates the contract and is ignored.

The deny itself is byte-for-byte unchanged in every case — machines without the agent fail the dial instantly.

Test plan

  • New relay tests with a fake agent socket: payload contents; a hung daemon costs ≤ the notify budget; 404 / absent socket / unusable effects never change the deny; allow never dials.
  • go test ./... in hooks/ 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.

  • New Features
    • Decode ingest block effect into a structured blockEffect in hooks/relay/client.go.
    • On deny for tool-pre and permission events, synchronously POST to the device agent at /v1/blocks/report over the identity socket; payload includes effect fields plus provider, session_id, and blocked_at.
    • Transport: hard 300 ms timeout, one attempt (redirects not followed), DisableKeepAlives; all errors swallowed. 404 means 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.

Review in cubic

@bradcypert
bradcypert requested a review from a team as a code owner August 8, 2026 01:11
@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

DNO-804

@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 59b67a4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread hooks/relay/agent_block_notify.go Outdated
Comment thread hooks/relay/agent_block_notify_test.go Outdated
if !ok {
return nil
}
encoded, err := json.Marshal(object)

@cubic-dev-ai cubic-dev-ai Bot Aug 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

@bradcypert
bradcypert force-pushed the brad/dno-803-featserver-allow-device-keys-agent_user-on branch from d9536e3 to a009fc3 Compare August 8, 2026 02:05
@bradcypert
bradcypert requested a review from a team as a code owner August 8, 2026 02:05
@bradcypert
bradcypert force-pushed the brad/dno-804-feathooks-forward-requestable-block-metadata-to-the-device branch from 4ad917c to c67e2fb Compare August 8, 2026 02:05
…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
@bradcypert
bradcypert force-pushed the brad/dno-803-featserver-allow-device-keys-agent_user-on branch from a009fc3 to e49fbc6 Compare August 8, 2026 02:09
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
@bradcypert
bradcypert force-pushed the brad/dno-804-feathooks-forward-requestable-block-metadata-to-the-device branch from c67e2fb to 59b67a4 Compare August 8, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant