feat: add hosted cost confirmation policy - #379
Draft
barryroodt wants to merge 4 commits into
Draft
Conversation
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.
commit: |
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.
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.
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.