Skip to content

feat(tools): Tool-5 — rug-pull re-prompt for app tools - #436

Merged
th3-br41n merged 1 commit into
mainfrom
feat/tool-5-descriptor-hardening
Aug 2, 2026
Merged

th3-br41n merged 1 commit into
mainfrom
feat/tool-5-descriptor-hardening

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

Most of what this rung named already shipped in Tool-2 — length caps, invisible-text refusal (incl. Unicode Tags), reserved intent-verb names, and ids that cannot collide with a curated verb or an mcp.* id. Those are now pinned by tests rather than assumed. What was genuinely missing is the rug-pull re-prompt.

The threat, adjusted for app tools

MCP's rug-pull is a remote server swapping a benign description for a malicious one after approval. An app tool's descriptors are manifest-derived, so the analogue is narrower but real: an app update silently rewrites a tool you already approved, and it keeps whatever friction the old wording earned. A pure tool becomes external; a tool grows a required argument; a description is rewritten.

A tool now carries a fingerprint of the approved surface — title, description, effect, the full input declaration, and surfaces (adding agent moves a tool from human-clicked to autonomous, which is a change in what it does, not merely where it appears). Excluded: appliesTo and registeredAt, which change without changing anything a person read. Object-valued modifiers are flattened to tuples so a manifest key reorder is not a "change" — spurious re-prompts are exactly the click-through training this mechanism exists to avoid.

A Changed or New tool confirms regardless of its declared effect, and tools.list stamps the state onto each record, so a caller asks the right question before clicking rather than discovering the refusal by being refused.

The review caught a regression that would have shipped

Rated HIGH, and correctly: the check made every never-approved tool refuse server-side, while the only caller derived confirmed from effect alone. So a pure tool sent no confirmation, the approval was never recorded, and the row failed on every click forever — and since nothing could reach an approved state and agents are refused outright, no app tool would have been agent-callable at all, quietly undoing Tool-4.

My tests missed it because the fixture pre-approved everything — they tested the harness, not the product. Now pinned from an unapproved starting state.

Four more, all fixed here:

  • Fail closed on an unreadable approvals store — both a failed open and a throwing read. The identically-shaped disable-set read 45 lines above already failed closed, with a comment saying why.
  • The Changed reason now reaches the human. It was computed, logged, and discarded, so a rug-pull showed the same dialog as a first approval.
  • Approvals are keyed per caller. confirmed is an assertion a caller cannot prove; a global row turned a forged flag from a one-call problem into a standing one for every other caller.
  • Re-baseline only after the call succeeded, not before dispatch.

Known constraint, filed not hidden

An agent cannot mint an approval (agent-initiated confirms are refused outright since Tool-4), so agent tool-calling stays blocked until Tool-8's shell-owned approval. Tool-6 will project tools into the agent's layer, but its calls refuse until then. That ordering is now explicit in the plan.

Verification

Lint clean, typecheck clean, 3300 tests. Includes: no fingerprint collision constructible, no TOCTOU, approvals dropped only on uninstall (not by the update path), and the id-collision claim checked against the actual minting code.

🤖 Generated with Claude Code

Most of the hardening this rung named shipped in Tool-2 (length caps,
invisible-text refusal incl. Unicode Tags, reserved intent-verb names).
What is genuinely missing is the rug-pull re-prompt, ported from the MCP
path with the threat model adjusted: an app tool's descriptors are
manifest-derived, so the analogue is an app UPDATE silently rewriting a
tool the user already approved and inheriting the friction the old
wording earned.

A tool now carries a fingerprint of the approved surface — title,
description, effect, the full input declaration, and `surfaces` (adding
`agent` moves a tool from human-clicked to autonomous, which is a change
in what it does). Excluded: `appliesTo` and `registeredAt`, which change
without changing anything a person read; object-valued modifiers are
flattened to tuples so a manifest key reorder is not a "change".

A Changed or New tool is forced to confirm regardless of its declared
effect, and `tools.list` stamps the state onto each record so a caller
asks the right question BEFORE clicking rather than discovering the
refusal by being refused.

The security review found this shipping a regression that would have
made every app tool permanently uncallable: the only caller derived
`confirmed` from `effect` alone, so a `pure` tool sent none, the
approval was never recorded, and the row failed on every click forever
— and no tool could ever become agent-callable. My tests missed it
because the fixture pre-approved everything. Fixed, and pinned by a test
that starts from an unapproved tool.

Also from review: fail CLOSED on an unreadable approvals store (both a
failed open and a throwing read); pass the REASON to the confirm seam so
a rug-pull reads differently from a first approval; key approvals per
CALLER, because `confirmed` is an assertion a caller cannot prove and a
global row would turn a forged flag from a one-call problem into a
standing one; and re-baseline only after the call actually succeeded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@th3-br41n
th3-br41n merged commit 670fd0a into main Aug 2, 2026
2 of 3 checks passed
@th3-br41n
th3-br41n deleted the feat/tool-5-descriptor-hardening branch August 2, 2026 11:04
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