Skip to content

feat(mcp): require a USD cap on every real purchase - #69

Open
cristim wants to merge 1 commit into
feat/51-usd-spend-capfrom
feat/51-usd-cap-wiring
Open

cristim wants to merge 1 commit into
feat/51-usd-spend-capfrom
feat/51-usd-cap-wiring

Conversation

@cristim

@cristim cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member

Summary

This is part 2 of 3 for #51. It is stacked on #66: the base is feat/51-usd-spend-cap, so merge #66 first and then retarget this PR to main.

The existing per-call caps bound request size, not money: CUDLY_MCP_MAX_COUNT=50 still admitted 50 x u-24tb1, roughly $286M over three years. Real purchases now also require CUDLY_MCP_MAX_COMMITMENT_USD.

  • Before credentials: authorizeRealPurchase reads the USD cap. Unset, empty, non-numeric, NaN, Inf or <= 0 refuses the purchase before ResolveClient, with no credential or network use. The parsed limit is passed down, so the env var is read once.
  • After ResolveClient, before the idempotency token or PurchaseCommitment: enforceUSDCap (from feat(tools): compute the total USD a purchase commits #66) prices the purchase and refuses a total above the cap. A purchase it cannot price is refused too.
  • GCP: real GCP CUD purchases are now refused, because the pinned library cannot price them (go#321). The owner chose this over bumping the pin first. Re-enabling is tracked in feat(gcp): bump cloud-commitments-go past go#321 and re-enable priced GCP CUD purchases #67.
  • ExecutePurchase's response tail moved into purchaseResponse, which keeps the function under the gocyclo limit. This is a pure extraction.
  • Operator surface:
    • mcpb/manifest.json: env var in all 3 platform blocks, plus a max_commitment_usd user setting whose title matches the refusal message.
    • server.json
    • README spend-cap paragraph and CHANGELOG Breaking / Added / Security entries.

Refs #51 (the issue stays open for part 3, the cumulative budget)

Test changes that narrow coverage (deliberate)

Under the GCP refusal, four tests that asserted a successful real GCP purchase now assert the refusal:

The over-cap refusals are still asserted. The library-boundary GCP cost proof stays in tools/gcp_computeengine_cud_safeguards_test.go. #67 lists all four tests to restore. No test-only bypass seam was added.

rdsOffering in tools/adopted_go_safeguards_test.go gains a price and currency, because the real RDS library path now has to clear the cap.

Verification (local, macOS, at d2e389b)

Fake-cloud and unit evidence only. No real purchase or cloud call is made.

  • gofmt, go vet ./... and make build pass.
  • go test -race -count=1 ./... passes in all 3 packages. golangci-lint and gosec, both run with GOTOOLCHAIN=go1.26.9, report 0 issues.
  • gocyclo -over 10 lists no non-test code. markdownlint passes on README and CHANGELOG, and both JSON files parse.
  • Regression: TestUSDCapRegressionExpensiveRI (EC2 u-24tb1 x 50, $5.72M each, cap $1000). Without the enforceUSDCap call the purchase executes (purchaseCalls == 1, the test fails). With it the call is refused, naming CUDLY_MCP_MAX_COMMITMENT_USD, with purchaseCalls == 0 after the client resolved.
  • TestUSDCapUnsetRefusesBeforeResolve checks that an unset cap leaves the client unresolved and makes no offering call.
  • Mutation probes by the reviewer (scratch copy, restored): each of these fails tests:
    • deleting the cap call
    • skipping the pre-resolve read
    • moving the cap check after PurchaseCommitment

Review

An independent reviewer (Sonnet, high effort, non-author) did two passes. Pass 1 found no high or medium issues. It traced all 8 ExecutePurchase call sites to the single choke point, confirmed previews stay offline, and confirmed the purchaseResponse extraction preserves behaviour. Its four low findings were handled as follows:

  1. CHANGELOG wording on the "required" setting: fixed.
  2. The GCP refusal runs after ResolveClient: accepted. It needs no network, and the docs only promise a pre-credential refusal for an unset cap.
  3. An unproven "same offering is bought" sentence: softened.
  4. Cap refusals are not audited, the same as every existing pre-purchase refusal: follow-up feat(audit): record pre-purchase refusals in the purchase audit log #68.

Pass 2 found no actionable findings. CodeRabbit is not pinged because the org's CR quota is exhausted.

🤖 Generated with Claude Code

Per-call caps bounded request size, not money: CUDLY_MCP_MAX_COUNT=50
still admitted 50 x u-24tb1, roughly $286M over three years. Real
purchases now also need CUDLY_MCP_MAX_COMMITMENT_USD. An unset or
invalid value refuses before credentials are resolved; after
ResolveClient the purchase is priced with the helpers from the
previous commit and refused when its total exceeds the cap, before an
idempotency token is derived or anything is bought.

Real GCP CUD purchases are now refused until the library pin includes
the CUD pricing fix (go#321, follow-up #67). GCP tests that asserted a
successful real purchase now assert the refusal; #67 restores them.
ExecutePurchase's response tail moves into purchaseResponse to stay
under the gocyclo limit. Manifest, server.json, README and CHANGELOG
document the new required setting.

Refs #51

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristim cristim added urgency/this-sprint Within the current sprint priority/p1 Next up; this sprint severity/high Significant harm impact/all-users Affects every user effort/m Days type/feat New capability triaged Item has been triaged labels Oct 16, 2026
@coderabbitai

coderabbitai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-mcp/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 0108b59d-084f-44f2-a280-d17c46844cdf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Merge-driver verdict: CLEAN at d2e389b86a365b5bafcee4b5f9e15f3bfc869543

Scope. This reviews the delta 5ffa001..d2e389b on top of #66. The same independent reviewer reviewed #66 (Sonnet, high effort, cold brief, not the author). It found no blocking findings.

Verified:

  • Every real purchase is capped. All eight purchase tools route through ExecutePurchase. The only PurchaseCommitment call is at tools/purchase.go:664. The order is authorize, then ResolveClient, then enforceUSDCap on the same rec, then PurchaseCommitment. No path bypasses the cap.
  • Previews are not blocked. They return before the cap is read.
  • The cap fails closed. An empty, whitespace, unparsable, NaN, Inf, zero or negative cap refuses the purchase before ResolveClient (TestUSDCapUnsetRefusesBeforeResolve, TestUSDCapEnvParsing).
  • Wording. Refusals use the refusing real purchase: prefix from spend_caps.go, and GCP is "refused" (go#321) with zero provider calls.
  • No weakened tests. The removed lines are GCP success assertions, replaced by refusal assertions. The new usd_cap_wiring_test.go covers the $286M regression case.

Mutation probes (3), all killed:

  1. Deleting the enforceUSDCap call made 6 tests fail.
  2. Replacing the cap read with a huge constant made 2 tests fail.
  3. Loosening zero-rejection from <= 0 to < 0 made the env-parsing test fail.

Local verification on macOS (real runs, fixture and mock pricing, no cloud calls):

  • Toolchain: GOTOOLCHAIN=go1.26.9, the version pinned in go.mod. The local go1.27.2 install is broken.
  • go build ./... passes.
  • go test -count=1 ./... passes: 809 tests.
  • Real-account coverage gap: no live purchase was exercised, by design.

Non-blocking notes (for follow-up):

  • GCP is refused only after ResolveClient, so credentials are resolved first. The README says scope checks run before credentials are touched. An early refusal in authorizeRealPurchase would make that claim true.
  • The manifest has max_commitment_usd min: 1, while the server accepts any value above 0.
  • The GCP real-success structured-cost coverage is gone; restore it with the go#321 pin bump.

Gate status:

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Follow-ups for the non-blocking notes:

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Automated review of d2e389b: no blocking findings and CI is green. Ready to merge.
USD cap is read before credentials, priced via the provider offering lookup, and fails closed on unset, invalid, non-USD, zero or unpriceable values. GCP CUDs are refused. Wiring and unit tests cover the main paths. No blocking bugs found.

  • (optional) tools/purchase.go:652: The USD cap passes the pre-purchase price check, but the returned total is discarded. Recording it in the audit record or response would help operators. Cap refusals write no audit entry, which is documented in the comment.
  • (optional) CHANGELOG.md:28: One bullet is a very long line, and the Breaking section now has a blank line splitting its bullets. Tidy the wrapping.

Posted by a read-only review sweep. It stays silent on this commit and re-reviews only after a new push.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/all-users Affects every user priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant