v0.21.0 — the risk one tool cannot show you, and the money a refund does not cover - #22
Merged
Conversation
… does not cover Two rules that are right on one axis and were silently assumed to cover a second. Tool annotations are a risk vocabulary with asymmetric defaults: readOnlyHint false, destructiveHint true, idempotentHint false, openWorldHint true. Omitting them declares the MOST dangerous shape — correct fail-closed, and the opposite of what most authors think they are doing. Every one is a hint, not a contract: the spec says a client treats them as untrusted unless the server is, so an annotation may inform a default and may never decide whether a destructive call runs. The lethal trifecta now has a name here — private data, untrusted content, external communication; any two safe, all three an exfiltration path. It lands in tools.md rather than a permission section because it is a property of the tool SET in a session, so per-tool analysis cannot see it by construction. gateway.md gains the composer's half: granting a role one more server is a trifecta question, not only least-privilege. And: refund the iteration, charge the money. A recoverable provider error should not consume an attempt, but the provider still billed the call — a response that arrived and then failed to parse was generated, metered and charged. Accounting written on the success path under-counts on exactly the runs that go worst, and under-counting is the worse asymmetry: invisible until the invoice, and biased toward the failing runs. Written into references and one clause on purpose: B-129 records this body at capacity, and this release does not spend a third displacement. B-130, B-131, TR-1..5. Gate: npm test rc=0. Co-Authored-By: Claude Opus 5 (1M context) <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.
Two rules that are right on one axis, and were silently assumed to cover a second.
Annotations are a risk vocabulary, and the dangerous defaults are the defaults
readOnlyHintfalsedestructiveHinttrueidempotentHintfalseopenWorldHinttrueOmitting annotations declares the most dangerous shape. That is the correct fail-closed choice and the opposite of what most authors think they are doing — annotation narrows an assumption, silence widens it.
And every one is a hint, not a contract: the specification says a client must treat descriptions and annotations as untrusted unless the server is. So an annotation may inform a UI or a policy default, and may never be the thing that decides whether a destructive call runs.
The lethal trifecta, and why it lives beside tool design
Private data · untrusted content · external communication. Any two are safe; all three in one session are an exfiltration path that no prompt-level instruction reliably closes.
It lands in
agent-harness/references/tools.mdrather than a permission section for a structural reason: it is a property of the tool set assembled in a session, so per-tool analysis cannot see it by construction. Every tool passes its own review and the combination is still unsafe — adding afetch(url)beside a private-data reader closes the triangle and will not look like a security change in review.agent-interop/references/gateway.mdgains the composer's half: a gateway's whole value is assembling many servers into one surface, which is exactly the operation that can create a risk none of its inputs had. Granting a role one more server is a trifecta question, not only a least-privilege one.Refund the iteration, charge the money
Our loop refunds an iteration on a recoverable provider error — a 502 should not consume one of ten attempts. Right, and silent about money: the provider still billed the call. A response that arrived and then failed to parse was generated, metered and charged.
The observed failure is precise: a harness charged its cost on the success path of
query(), so a format error meant the cost was never added — and the code compensated inside the exception handler. Where a cost ceiling is the primary bound (that harness shipscost_limit = 3.0with the step limit off), a leak there is a leak in the only guard there is.Under-counting is the worse asymmetry. Over-counting stops a run early and visibly. Under-counting is invisible until the invoice, and it biases toward the failing runs — which are the expensive ones.
Evidence
npm testrc=00 GAP, 14 PASS, body 4655/4750Board B-130, B-131. Ledger TR-1 … TR-5.
🤖 Generated with Claude Code