Repository navigation
feat(tools): Tool-7 — app tools in the object menu - #432
Merged
Merged
Conversation
Menu-surfaced tools render as contributed actions in the object ⋯ menu
and invoke through tools.call. AS-4's anti-rot policy is reused, not
rebuilt: a tool is projected onto the shared ContributedAction shape and
passes through the same grouping, so a sideloaded tool is quarantined
under "More app actions" and the inline cap counts tools and intents
against ONE budget.
Three things the rung's premise did not anticipate:
- the shared dedupe keys on (verb, kind), and a tool has no verb, so
every tool would have collapsed into one row — the exact collision
this track exists to remove. ContributedAction gains an optional
dedupeKey, used when a contribution is already individually
addressable; intent contributions keep the old key.
- tools.list stamped no trust tier at all, so the quarantine could not
have applied. It now stamps trustTier + appLabel from the apps
registry, never the provider's manifest.
- tools.list would have produced dead rows: tools.read buys the
catalogue, tools.call:<appId> buys the invocation.
The security review then found the rung breaking that same invariant in
four more places, all fixed here: a tool with a required input was
refused Invalid on every click and an external tool NeedsConfirm on
every click; the list filter omitted the provider's tools.provide check
the call enforces; a tool from an app with no running renderer always
failed Unavailable; and rows rendered for hosts that could not report an
outcome, so every refusal vanished into an unhandled rejection. Also:
attribution rendered as nothing (it was a `hint`, which the shared menu
runtime drops), and appLabel was the one provider-authored string that
never passed Tool-2's screen.
One fix was attempted and reverted — refusing " — " in titles to stop
attribution forgery rejects legitimate titles, so the residual is
documented in code rather than paid for.
The three editor surfaces are split out as Tool-7b: they route through
BlockCommand, whose run() is synchronous and void-returning, so there is
nowhere to report a named refusal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Menu-surfaced app tools render as contributed actions in the object ⋯ menu and invoke through
tools.call. AS-4's anti-rot policy is reused, not rebuilt — a tool is projected onto the sharedContributedActionshape and passes through the same grouping, so a sideloaded tool is quarantined under "More app actions" and the inline cap counts tools and intents against one budget. Two independently-capped blocks would have been exactly the rot the policy exists to stop.Three things the rung's premise did not anticipate
dedupewould have collapsed every tool into one row. It keys on(verb, kind)and a tool has no verb — which is precisely the verb collision this track exists to remove, reappearing in the presentation layer.ContributedActiongains an optionaldedupeKey(the tool id), used when a contribution is already individually addressable; intent contributions keep the old key, where collapsing two apps'sharegenuinely is one labelled choice.tools.liststamped no trust tier at all — that resolver existed only on the intents bus — so AS-4's quarantine could not have applied to tools. It now stampstrustTier+appLabelfrom the apps registry, never the provider's manifest, defaulting toSideloaded.tools.readbuys the catalogue;tools.call:<appId>buys the invocation.Security review — five findings, all fixed here
The sharpest were the rung breaking its own "no dead menu rows" invariant, in four more places than I'd closed:
Invalidon every click (a click carries no arguments);externaltool → refusedNeedsConfirmon every click (nothing setconfirmed);tools.providecheck that the call enforces — so revoking it, an advertised way to stop an app answering, left rows that always failed. This also falsifiedtool-effect-gate's own "the rule lives in one place";Unavailable.Argument-taking tools are now not offered (an argument prompt is
Tool-8's tray); confirm-requiring ones only when the host supplies anonToolConfirmseam returning a real human answer; listing enforcestools.provideand filters to live providers.Two more: attribution rendered as nothing — it was a
hint, which the shared menu runtime drops, so two apps' same-titled tools showed as two identical rows with no way to tell trusted from sideloaded. AndappLabelwas the one provider-authored string that never passed Tool-2's screen (manifest.nameis validated only as non-empty, unlike a tooltitle) — harmless while it rendered as nothing, a problem the moment it rendered.One fix was attempted and reverted. Refusing
" — "in titles to stop attribution forgery rejects legitimate titles — the repo's own example is"Rewrite — tone"— so the cure cost more than the disease. The residual is documented in code: a visible-text spoof against an already length-capped, invisible-text-screened string, whose real fix is a menu item type that can render attribution as a separate element.Also corrects a pre-existing false comment in
intents-busclaiming every contributed label carries attribution — only the fallback does.Scope
The rung named four surfaces; this ships one. The three editor surfaces are split out as
Tool-7b— they all route throughBlockCommand, whoserun(ctx) => voidis synchronous and void-returning, so there is nowhere to put an async call's result or its named refusal, and a contributed tool that cannot reportDenied/NeedsConfirm/Timeoutis a dead row by construction. The projection, policy reuse and invocation path land here and are reused unchanged.Verification
Lint clean, typecheck clean, 3190 tests pass. 7 projection/label + 4 listing + 7 menu-row tests, the last including the click paths.
🤖 Generated with Claude Code