Skip to content

feat(tools): Tool-7 — app tools in the object menu - #432

Merged
th3-br41n merged 1 commit into
mainfrom
feat/tool-7-menu-presentation
Aug 2, 2026
Merged

th3-br41n merged 1 commit into
mainfrom
feat/tool-7-menu-presentation

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

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 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. Two independently-capped blocks would have been exactly the rot the policy exists to stop.

Three things the rung's premise did not anticipate

  1. The shared dedupe would 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. ContributedAction gains an optional dedupeKey (the tool id), used when a contribution is already individually addressable; intent contributions keep the old key, where collapsing two apps' share genuinely is one labelled choice.
  2. tools.list stamped 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 stamps trustTier + appLabel from the apps registry, never the provider's manifest, defaulting to Sideloaded.
  3. It would have produced dead rows. tools.read buys 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:

  • a tool with a required input → refused Invalid on every click (a click carries no arguments);
  • an external tool → refused NeedsConfirm on every click (nothing set confirmed);
  • the list filter omitted the provider's tools.provide check that the call enforces — so revoking it, an advertised way to stop an app answering, left rows that always failed. This also falsified tool-effect-gate's own "the rule lives in one place";
  • a tool from an app with no running renderer always failed Unavailable.

Argument-taking tools are now not offered (an argument prompt is Tool-8's tray); confirm-requiring ones only when the host supplies an onToolConfirm seam returning a real human answer; listing enforces tools.provide and 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. And appLabel was the one provider-authored string that never passed Tool-2's screen (manifest.name is validated only as non-empty, unlike a tool title) — 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-bus claiming 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 through BlockCommand, whose run(ctx) => void is 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 report Denied/NeedsConfirm/Timeout is 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

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>
@th3-br41n
th3-br41n merged commit 038b1e2 into main Aug 2, 2026
3 checks passed
@th3-br41n
th3-br41n deleted the feat/tool-7-menu-presentation branch August 2, 2026 09:25
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