feat: add hosted cost confirmation policy - #379
Conversation
commit: |
48f2872 to
218808d
Compare
Coverage Report for CI Build 33077359325Warning No base build found for commit Coverage: 96.601%Details
Uncovered Changes
Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
Adds the Supabase cost confirmation contract at policy version 2, together with the authoritative rate contract it reads. Version 2 takes consent from the wire action on a property-less requested schema. Version 1 read a Boolean out of the response body, so the two cannot interpret each other's state: the runtime rejects a version it does not own before it looks at any response, which makes a rolling deployment safe in both directions. Rates now come from the Management API. `AccountOperations` and `BranchingOperations` each gain one read-only creation-rate method, the API platform implements them against the new v2 endpoints, and `pricing.ts` becomes the adapter between an authoritative rate and the legacy cost shape. `PROJECT_COST_MONTHLY` is gone: `get_cost` and the legacy `create_project` check both read the organization's authoritative rate, which returns the same value. The hourly branch rate stays as one clearly named legacy value because the authoritative branch rate is scoped to a parent project and legacy `get_cost` is only given an organization; both halves of that legacy pair quote it, so a legacy confirmation still matches. No confirmation or creation path reads it, and no monthly-hours constant exists anywhere. Types generation gains v2 as an addition. The v1 pull and its complete output are untouched and byte-identical after regeneration. v2 is generated from a byte copy of the Management API v2 document at rates head af464cca85, because `/api/v2-json` is not served yet and a URL pull would make regeneration depend on a deployment. Both v2 artifacts join `src/management-api/types.ts` in the root formatter's ignore list, because a generated artifact is owned by its generator and hand-formatting one would break the next regeneration. Absorbs plan step C2's action-only consent implementation into this commit: `ElicitationPolicy` is a total interface, so a policy carrying the version this step defines does not compile without the `inputRequests` and `resolve` that step C2 specifies. It also absorbs plan step C3's final authoritative check, because the guard is the only reason the resolution type exists and a commit that defined the ceiling without spending it would ship an unused type. Breaking change for platform implementers: a `SupabasePlatform` with an `account` or `branching` implementation must add the matching creation-rate method. There is no fallback price to fall back to.
Attaches the cost policy to `create_project` and `create_branch`, and routes each request to the lane it belongs in before the elicitation runtime is consulted. A request that can carry a form takes the confirmed lane: the legacy token is hidden from its schema, stripped before canonicalization so a token supplied anyway cannot bind an approval, and `confirm_cost` disappears from its tool list while staying callable with migration guidance. Every other request takes the legacy lane unchanged, and the router composes the runtime's output-schema hook rather than re-deciding availability, so a legacy request keeps its pre-normalization bytes: no `outputSchema` in discovery, no `structuredContent`, single-encoded text. A request carrying verified continuation state stays on the confirmed lane even when this leg is no longer capable. Continuation wins routing, so capability lost mid-flow is answered with the runtime's recovery text instead of a demand for a token the caller was never given. The whole surface is opt-in through one server option. A consumer that injects no elicitation options gets policy-free tools, which is how deprecated stdio, classic hosted, and a hosted connection that opted out keep the contract they have today. The two byte fixtures land with this commit because they defend exactly what it changes. Their expected values were measured against base main (302d2ad) with fixed platform objects, and they cover both discovery entries and both call results on the legacy path.
Completes the PR C-owned matrix and the explicit product text that goes with an accepted creation. The policy suite owns what a rate and an answer mean: an authoritative rate reaching the proposal with its currency and recurrence, a zero rate executing unprompted while still carrying the ceiling, consent read from the wire action whatever the response body says, decline and cancel staying distinct, an unanswered confirmation asking again, and the approved-ceiling comparison across amount, recurrence and currency. The integration suite drives the product rows through `createSupabaseMcpHandler`: accepted project and branch, declined, cancelled, zero rate, equal or lower final rate, higher rate, changed recurrence, changed currency, the rate read immediately before each creation call, a legacy token that cannot bypass the confirmation, capable discovery without `confirm_cost`, migration guidance on a direct call, and every surface that stays legacy, including a classic client that declares form support. It relies on PR B for state lifetime, expiry, continuation and repeated-state identity rather than repeating that matrix, and proves client labels are not read with one contract instead of a table. Accepted creations now render explicit text: what the client reported, the rate it reported against, and that the resource was created. A zero rate says instead that no confirmation was requested, because nothing was asked. The ceiling travels from execution to rendering through a per-result weak map, so nothing is added to the business output, and a legacy request skips the rendering hook entirely and keeps its single-encoded text. The packed platform consumer now drives a full hosted confirmation through the packed artifact, checks the confirmation carries the rate and no properties, and fails if the entry point ever exports a runtime, state, codec, policy or interaction symbol. Draft copy remains draft: the required facts have one owning test each, and the projection stays an empty slot pending Billing approval.
The one case kept from the Boolean confirmation contract, kept as a policy-version rejection: state a previous deployment issued carries `confirm: false`, and version 2 refuses it without reading that content. Version binding belongs to the runtime; this pins the product consequence, which is that nothing is created and the caller is told to run the tool again.
4ee3797 to
6d93d47
Compare
|
Moving this back to draft alongside #378: the simplification under discussion replaces the private runtime with SDK-native inputRequired and drops the public v2 creation-rate REST dependency (hosted already reads rates in-process). The cost-confirmation product behavior carries over. Please hold further review until after tomorrow's chat. |
|
Closing this as superseded by #391, which integrates project cost confirmation directly in |
What
Adds policy version 2 cost confirmation to
create_projectandcreate_branchfor modern, form-capable hosted requests, following RFC v0.4. Consent comes only from wireaction;requestedSchemahas no confirmation property. Decline or cancel creates nothing.The typed Management API contract supplies the proposed and final pre-side-effect rates. Zero skips the prompt while retaining the final check. A higher amount or changed currency or recurrence returns
approved_rate_staleand creates nothing. v1 and v2 continuation states cannot authorize each other’s policy.Modern capable discovery hides
confirm_costandconfirm_cost_id. Directconfirm_costcalls receive migration guidance, and the old token cannot bypass confirmation. Modern incapable, URL-only, opted-out, classic hosted, and deprecated stdio flows keep existingconfirm_costbehavior.Why
Hosted creation needs explicit consent tied to the rate charged at execution. Package behavior stays dormant until Platform injects its dependencies. Final projected-cost wording remains a launch approval gate; this PR pins the required facts and wire actions.
PR B owns continuation lifecycle and repeat execution. This PR makes no replay-prevention claim.
How to Review
Trace routing first
packages/mcp-server-supabase/src/server.tsCheck the consent policy
packages/mcp-server-supabase/src/policies/cost-confirmation.tsandpackages/mcp-server-supabase/src/pricing.tsFollow business integration
packages/mcp-server-supabase/src/tools/account-tools.ts,src/tools/branching-tools.ts, andsrc/platform/api-platform.tsRead the focused tests
src/policies/cost-confirmation.test.ts,src/elicitations.test.ts,src/policies/legacy-cost-bytes.test.ts, andsrc/platform/api-platform.test.tsscripts/fixtures/packed-platform-consumer/against both routing paths.Review questions
Verification
All listed checks passed:
CI=1 mise exec -- pnpm --filter @supabase/mcp-server-supabase test run --project unitCI=1 mise exec -- pnpm --filter @supabase/mcp-server-supabase test run --project integrationmise exec -- pnpm test:packed-platform-consumermise exec -- pnpm buildmise exec -- pnpm format:checkReview confirmed useful-test ownership and the diff boundary: 25 files under
packages/mcp-server-supabaseandscripts.Accepted risk: best-effort cost ceiling
The approved cost ceiling is enforced best-effort for launch. The tool checks the latest rate immediately before creating the resource (
account-tools.tsguards withassertRateStillApprovedright beforecreateProject; branch path inbranching-tools.ts), but the Management API cannot enforce that ceiling atomically when it commits the create.That race window is unquantified and bounded by read-to-commit latency. Neither the application nor the transport retries the create.
We're accepting this launch risk without weakening the existing guard. Full enforcement needs a Management API conditional create that accepts an approved maximum or rate/version token and rejects a higher committed rate. An idempotency key would protect against duplicate creates, but it would not close the ceiling race.
Breaking change:
SupabasePlatformSupabasePlatformnow requires two methods:AccountOperations.getProjectCreationRate(organizationId): Promise<CreationRate>BranchingOperations.getBranchCreationRate(projectId): Promise<CreationRate>They stay required as a clean cutover. The only known in-repo consumer is the hosted platform, migrated in supabase/platform#37514 and supabase/platform#37523.
Custom
SupabasePlatformimplementations must add both methods before upgrading.