Repository navigation
feat(plugins): let a plugin tool ask the owner before it runs - #2953
Open
LordMelkor wants to merge 2 commits into
Open
LordMelkor wants to merge 2 commits into
LordMelkor wants to merge 2 commits into
Conversation
LordMelkor
force-pushed
the
clod/plugin-tool-approval
branch
2 times, most recently
from
November 12, 2026 17:08
9150c64 to
08647b1
Compare
Plugin tools run in the app's webview, so `client.approve({ title, detail })`
pauses the call on a notice with Allow once, Always allow and Deny, reusing
the toast notice the interrupted-agents prompt uses. Deny throws, so the agent
sees a tool error; Always allow is saved per plugin tool on this device.
Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
A turn Claude starts itself, such as one a finished background task starts, has no pending message, so settle() never ran and its Buzz tool calls (and any approval they asked for) outlived the turn. Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: LordMelkor <kray@squareup.com>
LordMelkor
force-pushed
the
clod/plugin-tool-approval
branch
from
November 15, 2026 14:36
08647b1 to
c68a2e0
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A plugin tool's
call(client, context, args)can't pause for a person.ctx.decisionsreturns a model's judgment, and protection review is a model review that runs only for protected agents.Harness permission prompts (Claude Code, Codex) can't see inside a plugin tool. So a plugin that gives agents destructive tools, such as the MCP servers plugin's
mcp_manager_call(delete files, send mail, merge PRs), can only switch each tool fully on or fully off.Change
client.approve({ title, detail })pauses the call and shows a notice. The notice names the agent, the plugin's tool,titleanddetail, and offers Allow once, Always allow and Deny."once"or"always".awaitit is safe by default.buzz.plugin-tool-allowed.v1:<plugin>/<tool>). Later calls resolve"always"with no notice. When a person picks Always allow, the other calls already waiting on the same tool are allowed too.addToolbindsapproveto the plugin and the tool, so a tool can't ask as another tool.approveis absent where no owner can be asked, such as a standalone server.ToolApprovalNotices). At most 16 asks can be pending.check(), because most harnesses end a turn with no event.HarnessKit.toolsuses the delivery's newendedsignal, which aborts when the run and everything it handed toworkingUntilhave settled. It isn't the advisory deadlinesignal. The Claude adapter gives each turn's tool calls a signal that aborts when the turn settles, whether it succeeds, fails or is stopped, so a failed turn's ask ends too. The store checks again before it settles, so an answer given after an interrupt is never taken and saves nothing.notifications/cancelledfor that call.docs/plugin-architecture.mdanddocs/plugin-authoring-guide.md. The generated@buzz/authordeclarations includeapprove.Intent
Original request: kray's #buzz-feedback post "let a plugin tool ask the agent's owner for approval before it runs" (Nov 3).
Validation
tool-approval.test.ts):ToolApprovalNotices.test.tsx): each button gives the right answer.service.test.ts):app-client.test.ts): no Always allow is saved for a call whose turn was interrupted. This uses a Goose-shaped client over the real store.kit.test.ts): an ask made throughHarnessKit.toolssurvives the delivery's deadline and ends with its turn.service.test.ts):endedstays live past the 60 s deadline while handed-off work runs, and aborts when that work settles.external.test.ts):notifications/cancelledaborts the namedtools/calland its ask.tsc --noEmitandbiome checkpass. Full suite at22907841, on main41e64bf0: 19,441 passed, none failed.sq agents reviewand Sigil): two P2s, both fixed. A pending approval survived a Goose interrupt, and it survived its plugin unloading. Sigil reproduced the first with the real ACP session, app client and store.sq agents review): three P2s, all fixed.HarnessKit.toolshad no turn signal, so an ask outlived its turn. A conversation now carries its turn's end, and a standalone turn passes its own signal.notifications/cancellednow ends the call it names, and the ask with it. An HTTP connection that drops without a cancel message is ended by its turn or by the plugin unloading, because the host transport has no per-request signal.Delivery.signal, which is an advisory handoff deadline. A harness still working throughworkingUntilwould have had every tool call fail after 60 s. Fixed:Deliverygains an optionalendedsignal, which aborts once the run and everything it handed off have settled, or the agent is removed. The kit uses it.sq agents reviewat22907841, after rebasing onto main41e64bf0: no findings. The rebase resolved one import conflict insrc/buzz-mcp/client.tsby keeping both sides.sq agents review) at34c18b72, after squashing and rebasing onto mainb1436b09: one P2, fixed in9150c642(+7 production lines). When a native Claude turn failed while a plugin call waited for approval, the ask stayed answerable, because the adapter's signals covered only process exit and Stop. Its tool calls now also get a per-turn signal. The new test inclaude.test.tsruns two turns on one process and fails without the fix. The rebase dropped the owner-read tools' wrapper fromapp-client.ts, because main removed those tools.Full suite at
9150c642: 20,530 passed, 1 failed. The failure was a spawn timeout under load inbrowser-host/vite-config.test.mjs, which this PR doesn't touch, and it passes alone.f10e7771, squashed again to one feature commit (6389350a). The approval's abort list also takes main's new Stop signal (callSignal). The review of the rebase found one P2, fixed in08647b11(+7/−1 production lines): a turn Claude starts itself, such as one a finished background task starts, settled nothing when it went idle, so its tool calls and any approval they asked for outlived it. Its calls now end when Claude reports idle. The new test inclaude.test.tsfails without the fix. A finalsq agents reviewat08647b11found nothing.Full suite at
08647b11: 21,256 passed and 20 failed, all in 3 files this PR doesn't touch. Those files pass alone (101 tests), so the failures were timeouts on the shared machine.2ccfbb34, keeping the two commits (headc68a2e00). Conflicts were in the docs, imports and the Claude adapter, where the turn signal now sits beside main'scallEpoch. Two adaptations to main's new operation proofs:addToolwrapper passes main's newproofargument through.sq agents review): one P2, fixed: a provider plugin that claimed an operation proof got the rawapproveTool, so it could ask as another plugin's tool. The claimed authority's client now leaves it out, while the consumer tool keeps its boundapprove. The new test inservice.test.tsfails without the fix.design:checkpass. Full suite before the fix: 22,104 passed and 1 failed, a load timeout instore-discovery.test.ts, which this PR doesn't touch and which passes alone (74 tests). After the fix, the four affected test files pass (955 tests).1ada6c2d, now22907841after the rebase) that probe passes, an independent source review confirms thatendedfollows the run, its handed-off work and retirement, and 559 affected tests pass.Try it (AGENTS.md step 3, for a human)
calldoesawait client.approve?.({ title: "Delete file", detail: "notes.txt" }); return "deleted";.🤖 Generated with Claude Code