Skip to content

Converge Fizzy's first-party OAuth clients from one registry - #3252

Merged
jeremy merged 3 commits into
mainfrom
first-party-oauth-clients
Oct 28, 2026
Merged

jeremy merged 3 commits into
mainfrom
first-party-oauth-clients

Conversation

@jeremy

@jeremy jeremy commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Adds Oauth::FirstPartyClients, a registry of the OAuth clients Fizzy ships with, modelled on bc3's. Before this, each client was made its own way: a migration and a first-use fallback for the personal access issuer, and oauth:provision_mcp_client for each MCP server. Each wrote its own subset of attributes.

What it does

  • Definitions:
    • fizzy-personal-access, the personal access token issuer.
    • The hosted MCP servers, by the resource each fronts: fizzy-mcp-server for https://mcp.fizzy.do/mcp, and fizzy-mcp-server-beta1 for https://beta1-mcp.fizzy-beta.com/mcp.
  • A total BASELINE. Each definition merges over every policy column oauth_clients has, so a converge resets drift in anything a definition doesn't name. A test pins the baseline to the table's columns, so a new column must be added to it.
  • One transaction. Every client converges, or none does.
  • No secret is ever minted or rotated. A confidential client's live secret survives a converge. A new confidential client is created with no secret at all, so any client without one, never minted or since cleared, is named in the task output on every run until oauth:mint_client_secret gives it one. A public row loses any stray secret.
  • No client is turned back on. Fizzy has no disabled flag. A hosted MCP client is turned off by clearing its secret, which no converge writes back, or by dropping its resource from FIZZY_OAUTH_MCP_RESOURCES, which takes the client out of the converge altogether.
  • No repointing. An MCP client is refused rather than moved when it fronts a resource that's still configured, as before, or a hosted resource. A hosted resource is configured only on its own destination, so its row isn't safe to move from any of them.
  • One entry point for operators and seeds. bin/rails oauth:provision_first_party_clients runs it, and so do the development seeds. Oauth::Client.personal_access_issuer takes its attributes from the registry, so the issuer made on first use matches what a converge writes.

Design choices

  • Hosted MCP clients are scoped by FIZZY_OAUTH_MCP_RESOURCES. The resource URLs are literals in the registry, but each one converges only where FIZZY_OAUTH_MCP_RESOURCES names its resource. That's the only place its client can exchange tokens (Oauth::Client#delegation_client?).
    • Production and the betas share a database. Under this scoping, production's converge never writes beta1's row, and beta1's never writes production's.
    • Development and self-hosted installs serve neither resource, so they get neither client.
    • The alternative was to converge every literal everywhere, as bc3's registry does. That creates beta rows from a production console, and production's row on every install.
  • oauth:provision_mcp_client stays, as a thin wrapper. It's still the way to provision an MCP server the registry doesn't host, such as a self-hosted install's own. The registry can't name those, and retiring the task would leave them no way in. It now converges through the same converge! and BASELINE as everything else, so its rows reset drift too. A hosted resource and its client can't be provisioned apart, which closes a hole: a shared database could otherwise have hosted rows repointed from a destination where their resource isn't configured. Oauth::Client.provision_mcp_client! moves into the registry as provision_mcp_server!.
  • oauth:mint_client_secret is unchanged.
  • Staging is not in the registry. deploy.staging.yml names a staging MCP resource but no exchange client, and staging sets no FIZZY_OAUTH_MCP_RESOURCES. It can be added when staging is lit.

Tests

test/models/oauth/first_party_clients_test.rb and a task test. Each guarantee was checked by breaking it and watching its test fail:

Break Test that fails
touch every row on converge converging again changes nothing
merge without BASELINE, or without part of it drift in any baseline attribute is reset
let BASELINE's nil client_secret_digest through on confidential rows a live secret survives a converge
mint a secret for a confidential row that has none a client turned off by clearing its secret stays off
converge every hosted definition regardless of configuration a production converge creates no beta client; a client turned off by dropping its resource stays off; an install that serves no hosted MCP resource gets no MCP client
drop the transaction a converge that fails partway writes nothing

Oauth::FirstPartyClients, modelled on bc3's, names the clients Fizzy ships
with: the personal access token issuer, and the MCP servers 37signals hosts,
by the resource each fronts (production's fizzy-mcp-server, beta1's
fizzy-mcp-server-beta1). Each definition merges over a BASELINE of every
policy column oauth_clients has, so a converge resets drift in any attribute
a definition doesn't name. The batch runs in one transaction.

A converge never mints or rotates a secret: a confidential client's comes
only from oauth:mint_client_secret, and one without a secret is reported for
it. Hosted MCP clients converge only where FIZZY_OAUTH_MCP_RESOURCES names
their resource, the one place each can exchange tokens. Production and the
betas share a database, so production's converge never writes a beta's row,
and a client turned off by dropping its resource is left alone.

bin/rails oauth:provision_first_party_clients runs it, and so do the
development seeds. The personal access issuer made on first use takes its
attributes from the registry. oauth:provision_mcp_client stays for an MCP
server the registry doesn't host, such as a self-hosted one, and converges
its client over the same baseline; a hosted resource and its client can't be
provisioned apart.
Copilot AI balanced review requested due to automatic review settings October 28, 2026 01:50
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T02:34:50.060128Z 8dcb0a3 Manual request
🔒 Security Review ✅ Completed 2026-10-28T02:32:57.928779Z 8dcb0a3 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d973ed3be2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/models/oauth/first_party_clients.rb Outdated
Comment thread app/models/oauth/first_party_clients.rb Outdated
Comment thread app/models/oauth/first_party_clients.rb

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Provisioning can repoint an existing production OAuth client and break token exchanges, while repeated runs can incorrectly report an unminted client as ready for authentication.

2 open issues
What changed in this PR

Centralizes Fizzy’s first-party OAuth client provisioning in one registry, shared by operator tasks, development seeds, and personal-access issuer creation.

Changes:

  • Adds transactional convergence with a complete policy baseline and environment-scoped hosted MCP definitions.
  • Routes existing provisioning through the registry and reports clients awaiting secrets.
  • Adds coverage for drift correction, secret preservation, resource isolation, and rollback.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
test/​tasks/​oauth_provision_first_party_clients_task_test.rb Tests provisioning output and secret guidance.
test/​models/​oauth/​token_delegation_test.rb Uses the registry’s MCP provisioning API.
test/​models/​oauth/​first_party_clients_test.rb Tests registry convergence and safeguards.
test/​integration/​oauth_token_exchange_test.rb Provisions exchange clients through the registry.
saas/​config/​deploy.beta.yml Updates a comment to reference registry-managed beta clients.
lib/​tasks/​oauth.rake Adds registry provisioning and adapts the existing MCP task.
db/​seeds.rb Provisions first-party clients during development seeding.
app/​models/​oauth/​first_party_clients.rb Defines client policies and transactional convergence.
app/​models/​oauth/​client.rb Sources issuer attributes from the registry and removes the old provisioning method.

Review effort: Balanced

Comment thread app/models/oauth/first_party_clients.rb Outdated
Comment thread app/models/oauth/first_party_clients.rb Outdated
… and let non-MCP rows shed a stray resource

A hosted row's resource is configured only on its own destination, and
production and the betas share a database, so the repointing guard now
refuses to move a hosted row from anywhere, including when
oauth:provision_mcp_client defaults to fizzy-mcp-server.

The guard applies only to MCP definitions, so a personal access row that
drifted onto a configured resource converges back to none.

A new registry client keeps no throwaway secret, so awaiting_secret? reports
it on every converge until oauth:mint_client_secret gives it one.
@jeremy

jeremy commented Oct 28, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 51baece700

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 51baece700

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Explicitly passing the legacy default CLIENT_ID now raises for self-hosted resources, breaking existing provisioning commands even though omitting it still provisions the same ID.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Allow explicit legacy default client ID for self-hosted resources

app/​models/​oauth/​first_party_clients.rb:97

For a self-hosted resource, explicitly passing CLIENT_ID=fizzy-mcp-server now raises here, while omitting CLIENT_ID still provisions that exact ID. This breaks existing provisioning commands that explicitly name the previous default, even when updating their own unchanged self-hosted row. Allow the legacy default ID consistently; the fronting_for_good? check in converge! already prevents it from taking over an existing hosted row.

2 resolved since last review

Review effort: Balanced

converge! already keeps fizzy-mcp-server off production's row wherever that
row exists, so the hosted-pair check needs to hold only for the betas' ids.
@jeremy

jeremy commented Oct 28, 2026

Copy link
Copy Markdown
Member Author

Fixed the finding in Copilot's review on 51baece, the one titled "Allow explicit legacy default client ID for self-hosted resources", in 8dcb0a3. CLIENT_ID=fizzy-mcp-server is accepted again for any resource, the same as leaving it out. The hosted-pair check now applies only to the betas' client ids, and converge! still refuses to move production's row wherever it exists. Test: "a self-hosted MCP server may name the default client_id, as it may omit it".

All five earlier review threads are resolved: four were fixed in 51baece. The fifth, race-safe creation, I declined on merit, and the reasoning is in #3252 (comment).

@codex review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approved

No active review issues were identified for the change centralizing first-party OAuth client policies and provisioning.

No open issues 🎉

Review effort: Balanced

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 8dcb0a3b73

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 8dcb0a3b73

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jeremy
jeremy merged commit 263f52e into main Oct 28, 2026
14 checks passed
@jeremy
jeremy deleted the first-party-oauth-clients branch October 28, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants