fix(mcp): the operator's spend cap can be narrowed, never widened - #44
Open
enot3615 wants to merge 1 commit into
Open
fix(mcp): the operator's spend cap can be narrowed, never widened#44enot3615 wants to merge 1 commit into
enot3615 wants to merge 1 commit into
Conversation
const maxAmountUsd = typeof args.max_amount_usd === "number"
? args.max_amount_usd // ← whatever the model asked for
: ctx.config.maxAmountUsd;
A tool argument replaced the operator's limit outright. AIFINPAY_MAX_USD=0.10
and a model that asked for max_amount_usd: 1000 got 1000. The cap is the one
control an operator cannot express any other way in this server, and it was the
one a prompt could remove.
Raised in the v1.3 audit follow-up as C-9, "fail-closed spend cap invariant",
with exactly this remedy: Math.min(operator_cap, requested_cap).
Latent rather than exploited — payable_fetch is not registered on the current
server, which registers only agent_address, agent_passport_resolve and
agent_quota. But this file is what SDK 2.0 re-registers, and a spend cap a
caller can raise is not a cap.
Math.min only where both exist. An unset operator cap means there is no policy
to violate, so a model-supplied value stands on its own; refusing there would
make the tool unusable for anyone who has not set the variable.
Also requires Number.isFinite on the argument. NaN and Infinity are numbers, so
`typeof x === "number"` accepted both, and Math.min(0.1, NaN) is NaN — a cap
that compares false against every amount, which is the same as no cap while
looking like one.
The tool's schema description said "Defaults to AIFINPAY_MAX_USD env or no cap",
which described the bug. It now says the value can only lower the operator's cap.
Tests assert the arithmetic AND the shape: the regression is a one-token edit
back to `?`, which the arithmetic tests alone would not catch if someone rewrote
the resolution.
81 tests pass across 7 files.
Refs AIFINP-211
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We1vVZLdj2vYtYaj7fjahX
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.
A tool argument replaced the operator's limit outright.
AIFINPAY_MAX_USD=0.10and a model that asked formax_amount_usd: 1000got 1000.The cap is the one control an operator cannot express any other way in this server — and it was the one a prompt could remove.
Raised in the v1.3 audit follow-up as C-9, "fail-closed spend cap invariant", with exactly this remedy:
Math.min(operator_cap, requested_cap).Latent, not exploited
payable_fetchis not registered on the current server — it registers onlyagent_address,agent_passport_resolve,agent_quota. But this file is what SDK 2.0 re-registers, and a spend cap a caller can raise is not a cap.The arithmetic
Math.minonly where both exist. Refusing when the operator has set nothing would make the tool unusable for anyone who has not set the variable.Also:
NaNandInfinityare numberstypeof x === "number"accepted both, andMath.min(0.1, NaN)is NaN — a cap that compares false against every amount. That is no cap, wearing the shape of one. Now requiresNumber.isFinite.The schema description said "Defaults to AIFINPAY_MAX_USD env or no cap" — which described the bug. It now says the value can only lower the operator's cap.
Tests assert the shape too
The regression is a one-token edit back to
?. Arithmetic tests alone would not catch a rewrite, soMath.min(operatorMax, requestedMax)is asserted directly.81 tests pass across 7 files.
@aifinpay/mcp→ 2.0.0-rc.4.Refs AIFINP-211.