Skip to content

feat: add hosted cost confirmation policy - #379

Draft
barryroodt wants to merge 4 commits into
barryroodt/ai-1091-stateless-elicitation-runtimefrom
barryroodt/ai-1091-hosted-cost-policy
Draft

feat: add hosted cost confirmation policy#379
barryroodt wants to merge 4 commits into
barryroodt/ai-1091-stateless-elicitation-runtimefrom
barryroodt/ai-1091-hosted-cost-policy

Conversation

@barryroodt

Copy link
Copy Markdown
Contributor

Stacked PR: PR C of three for AI-1091, stacked on PR B. Draft review is useful now, but verification cannot complete until Platform lands the authoritative Management API creation-rate contract.

What

Adds policy version 2 cost confirmation to create_project and create_branch for modern, form-capable hosted requests, following RFC v0.4. Consent comes only from wire action; requestedSchema has 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_stale and creates nothing. v1 and v2 continuation states cannot authorize each other’s policy.

Modern capable discovery hides confirm_cost and confirm_cost_id. Direct confirm_cost calls receive migration guidance, and the old token cannot bypass confirmation. Modern incapable, URL-only, opted-out, classic hosted, and deprecated stdio flows keep existing confirm_cost behavior.

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 unit
  • CI=1 mise exec -- pnpm --filter @supabase/mcp-server-supabase test run --project integration
  • mise exec -- pnpm test:packed-platform-consumer
  • mise exec -- pnpm build
  • mise exec -- pnpm format:check

Review confirmed useful-test ownership and the diff boundary: 25 files under packages/mcp-server-supabase and scripts.

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.
@barryroodt barryroodt added the publish-preview Runs `publish-preview` workflow to publish preview packages via https://pkg.pr.new/ label Aug 25, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/@supabase/mcp-server-postgrest@48f2872
pnpm add https://pkg.pr.new/@supabase/mcp-server-supabase@48f2872
pnpm add https://pkg.pr.new/@supabase/mcp-utils@48f2872

commit: 48f2872

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

Labels

publish-preview Runs `publish-preview` workflow to publish preview packages via https://pkg.pr.new/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant