Skip to content

v0.21.0 — the risk one tool cannot show you, and the money a refund does not cover - #22

Merged
sshlg merged 1 commit into
mainfrom
feat/tool-risk
Aug 31, 2026
Merged

v0.21.0 — the risk one tool cannot show you, and the money a refund does not cover#22
sshlg merged 1 commit into
mainfrom
feat/tool-risk

Conversation

@sshlg

@sshlg sshlg commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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

Hint Default So an unannotated tool is assumed to be
readOnlyHint false one that writes
destructiveHint true destructive
idempotentHint false unsafe to repeat
openWorldHint true reaching outside your system

Omitting 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.md rather 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 a fetch(url) beside a private-data reader closes the triangle and will not look like a security change in review.

agent-interop/references/gateway.md gains 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 ships cost_limit = 3.0 with 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

Check Result
npm test rc=0
Auditor 0 GAP, 14 PASS, body 4655/4750
Placement written into references and one clause on purpose — B-129 records this body at capacity, and this release does not spend a third displacement

Board B-130, B-131. Ledger TR-1 … TR-5.

🤖 Generated with Claude Code

… 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>
@sshlg
sshlg merged commit 4eaa2ed into main Aug 31, 2026
2 checks passed
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