Connections: the platform owns OAuth sign-ins, the flow, and the audit page - #292
Merged
Conversation
czpython
force-pushed
the
connections
branch
2 times, most recently
from
August 20, 2026 18:23
87964f5 to
b0e492f
Compare
…t page
A connection is one signed-in provider account — a mailbox, a handle, a
workspace — stored on the platform's oauth_connections row, owned by the
druks account that consented, N per provider. The engine rotates its
refresh token on mint: fresh read under the refresh lock, rotated token
committed on its own session before the cache fills, once, as private
methods on the row.
Extensions declare their use of a service with Acme.with_scopes(...); the
consent asks for the union of every installed declaration, and workflow
code reads the sign-ins through the handle — list_for_account(account_id),
then mint_access_token() on each connection.
Platform routes run the whole flow: /api/oauth/{name}/connect creates a
connection for the session account or reconsents one via ?connection=<id>;
one global callback serves every service; connections are revoked one by
one, session-identity only. Settings grows the Connections audit page and
the service detail lists that service's sign-ins.
McpOauthGrant is gone: its client half becomes McpClientRegistration
pointing at the server row, its grants become connections under MCP's
one-per-(server, account) policy, and the migration moves the rows,
re-encrypting each secret under its new table.column AAD.
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.
An extension's OAuth surface is a declaration and a loop:
The grant model, its migration, the rotation verbs with their transaction
topology, and the hand-rolled connect/callback routes all leave the author
surface.
Connections are platform rows
A connection is one signed-in provider account — a mailbox, a handle, a
workspace.
oauth_connectionsholds one row per sign-in, N per provider,owned by the druks account that completed the consent: the encrypted
refresh token, the granted scopes, the consent time. The engine rotates the
refresh token on mint — fresh read under the refresh lock, rotated token
committed on its own session before the cache fills — once, as private
methods on the row, instead of copied into every consumer.
McpOauthGrantis gone. MCP registers a fresh dynamic client on everyconnect, so each connection keeps its own client: that half becomes
McpClientRegistration, pointing at the server row, with the RFC 8707resource read live from the server's url. MCP keeps its
one-per-(server, account) policy over the shared table. The migration moves
the rows and re-encrypts each secret under its new
table.columnAAD.Declarations feed the consent
Scopes belong to the consumer.
Acme.with_scopes(...)on the Extensionclass declares the app's use of a service; the consent asks for the union
of every installed declaration, derived by walking the loaded extension
classes — no registry to drift. The handle reads the connections that grant
it:
list_for_account(account_id),get(connection_id).The platform runs the flow
GET /api/oauth/{name}/connectbegins consent with the scope union andstores a new connection for the session account;
?connection=<id>reconsents an existing one — widened scopes replace itstokens and the stale cached access token is evicted. One global
GET /api/oauth/callbackserves every service: connect state is keyed bythe state value alone and the provider rides the stash.
DELETE /api/oauth/connections/{id}revokes one connection. Consent andrevocation are session-identity actions, never an agent PAT's. Replacing a
service's client credentials deletes its connections — a new client can
never refresh the old client's tokens.
The surfaces
Settings grows a Connections page — everything the signed-in user has
authenticated to, across services, revoked per connection. The service
detail card lists that service's sign-ins beside the pasted client
credentials, with reconnect offered when an installed extension declares a
scope a connection lacks. App UIs own the connect button and the domain
rendering of their sign-ins.
Notes
id). Deploys are stop-then-start, so the cost is one cache miss per
connection after the deploy; stale locks expire by TTL.
it pinned as a redirect_uri.
not by a seeded-data test — seeding the pre-split schema mid-chain needs
a harness the one-time move does not justify. Flagging rather than
hiding it.