Skip to content

🤖 feat: create the Coder provider for a CoderAIProvider - #253

Merged
ThomasK33 merged 3 commits into
mainfrom
ai-provider-create
Oct 13, 2026
Merged

ThomasK33 merged 3 commits into
mainfrom
ai-provider-create

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Summary

The dormant CoderAIProvider reconciler now creates the Coder provider and applies the spec to it. It is still not registered, so the operator does not run it, and users see no change. The create follows the crash-safe rules that CoderTemplateTest uses: a marker before every create, a nonce that proves ownership, and a read of Coder before any second create.

Background

#154 adds AI providers, chat models and AI policies as controller CRDs. This is the fourth CoderAIProvider layer, after the API types (#235), the fake Coder routes (#237) and the reconciler skeleton (#245). Drift handling, key resends and kill switches come in the next layer. Deletion comes after that.

Create

When the object has no Coder ID and no create marker, the reconciler looks up the name:

  1. Another provider holds the name: NameConflict, and the controller never changes that provider (no adoption).
  2. The name is free: the reconciler writes status.createAttemptTime (the marker) and the key's change token (status.credential: Secret name, key, UID, resourceVersion) in one status update. A conflict on that update ends the reconcile before any request.
  3. It sends POST /api/v2/ai/providers with enabled: false, display_name: "coder-k8s:<object UID>" (the nonce), the type, the base URL and the key.

The answer decides the next step:

Answer Result
201 with a matching name, nonce and type Record status.providerID and the Coder key ID, clear the marker, then apply
No effect: 429, 401, 403, 406, a failed dial, a TLS error Clear the marker, retry after 30 s. Coder answers 403 from its authorization check before the insert commits.
409 Read the name. The nonce decides between "ours" and NameConflict
Other 4xx CoderRejected. No retry until the spec changes
Uncertain: timeout, reset, 5xx, a non-201 2xx, an HTML page, an answer for another name or without the expected key ConfirmingCreate. The marker stays

With a marker set, the next reconcile reads the name before it sends anything:

  • The nonce and type match: the provider is ours, and the reconcile stores its ID in status before the apply step runs. A provider with other than one key gives KeyChangedExternally.
  • Another provider: NameConflict, and the marker is cleared.
  • 404: the reconcile creates again, but only after it checks that the operator user is a site owner. Coder answers 403 to anyone else on these routes, but the check keeps rule 4 of the plan (a 404 proves absence only for a site owner) independent of that.

Apply

Once per generation, one PATCH sets display_name, icon and enabled to the spec values. The provider ID is already stored, so ownership does not depend on the nonce once the PATCH replaces it. The answer must show the requested display name (or the provider name for an empty one), icon and enabled flag. Then the reconcile sets status.appliedGeneration and Ready=True with reason Applied. A ready object needs no Coder call.

These cases do not count as applied:

  • The key Secret changed since the create (another change token): Pending, also when the provider was Ready before, because a Secret change does not change the generation. The next layer sends changed keys.
  • The answer shows another base URL: Pending. The next layer sends changed URLs.
  • 404: DeletedExternally, and the controller creates nothing new.
  • 401 or 403: CoderUnavailable, retried after 30 s, because no Kubernetes event follows a repaired operator token or owner role.

Other changes

  • The status message for a spec.baseURL that does not parse is now fixed text. The old message embedded the parse error, which repeats the URL, user info included.
  • New reconciler-level tests for an IPv6 host (match, and a near miss with another port).
  • A test for a control plane that renames its operator token Secret: the update event maps the old object too, so a provider whose key Secret had the old name is checked again and recovers.

Validation

internal/controller/coderaiprovider_create_test.go (envtest plus the fake Coder from #237). Every test uses a new reconciler for each reconcile, as a restart would.

  • Create: the POST sends the provider disabled with the nonce and the key, and the status records the IDs and the change token. A failed PATCH is retried, and the next reconcile applies the spec. A ready object then sends no request and writes no status.
  • Outcome classes (plan test S5): a lost answer after commit, a reset before commit, HTML 200 after commit, HTML 502 before commit, 500 after commit, an answer for another name, and 429. Each ends with exactly one provider that this object owns, and with the expected number of creates: 1 when the first create committed, otherwise 2.
  • A create that hangs until the reconcile's context ends, before and after commit: the marker is already stored, and the next reconcile reads first.
  • An apply PATCH that commits while the reconcile's context ends: the provider ID is already stored, so the next reconcile still owns the provider and reports Ready.
  • Plan test S4: a provider created by someone else between the lookup and the create gives NameConflict, zero PATCH and DELETE requests, and no second create.
  • A 400 is not retried until the spec changes.
  • A non-owner operator user: a 404 does not trigger a second create.
  • Apply guards: a changed key Secret (before and after Ready), a changed base URL, an update answer without the requested enabled flag, a provider deleted in Coder before the apply, and a 401 and a 403 on the apply, which recover on the next reconcile.
  • A 403 on the create clears the marker and is retried.

Mutation check: I broke the create flow in 18 ways (ready shortcut before the key check, 401 or 403 on the apply stalled, 403 on the create rejected, ID not stored before the apply, applied fields not checked, a create answer without the key treated as a dashboard change, uncertain answer clears the marker, no nonce check, provider created enabled, no site-owner check, rejected create retried, 429 keeps the marker, wrong answer recorded, key change ignored, applied without the URL check, 404 on apply retried, PATCH on every reconcile, 409 recorded as ours). Each change made a test fail.

Local gates on the pushed tree: make verify-vendor, make test, make test-integration, make build, make lint, GOFLAGS=-mod=vendor go tool govulncheck ./..., go test -race ./internal/controller/, and regeneration with no diff.

Review round 1

The automatic Codex review on b2778fba left three findings. All are fixed in d514eb4e (rebased from 9cff0b88):

  1. P1 "Persist ownership before replacing the nonce": the reconcile stores the provider ID before the apply PATCH, with a crash test at that boundary.
  2. P2 "Verify applied fields before reporting Ready": the update answer must show the requested display name, icon and enabled flag.
  3. P2 "Confirm malformed key lists instead of stalling": a create answer without the expected key is uncertain and keeps the marker, so a read confirms it.

Review round 2

The requested Codex review on 9cff0b88 left two P2 findings. Both are fixed in 877bfede:

  1. "Check the credential token before returning Ready": the change-token check now runs before the ready shortcut, with a rotation-after-Ready test.
  2. "Retry authentication and authorization failures": a 401 or 403 on the apply is now CoderUnavailable with a 30 s retry. A 403 on the create is a create without effect. Tests cover both.

The security review on 9cff0b88 found no issues. The branch is rebased onto main at 116cefe8.

Review round 3

The requested Codex reviews on 877bfede: security found no issues. The normal review left two valid P2 findings. Under the review-round cap (from round 4, blockers only), I deferred both to A5, with the independent advisor's agreement. Neither is a blocker: the controller is not registered, nothing leaks, and no data is lost.

  1. "Treat AI-provider 406 responses as uncertain": Coder never answers 406 on these routes. A proxy that replaces a committed create answer with a 406, or with any other 4xx, would leave the object stuck in an assertion error with a disabled provider in Coder.
  2. "Do not enable before applying the new base URL": a provider whose first apply failed can be enabled at its old base URL when spec.baseURL changes. The old URL already passed the destination check of the current Secret.

Both are in the #154 plan's A5 row, and the A7 activation gate requires them.

Tracked for later layers

Size

256 production lines added and 36 removed. 423 test lines added and 4 removed. No generated files change. The plan's A4 row targets about 510 lines. Most of the extra lines are the outcome-class tests.

Refs #154


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $23.36

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 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-13T16:51:39.313838Z 877bfed Manual request
🔒 Security Review ✅ Completed 2026-10-13T16:51:06.797003Z 877bfed 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: b2778fba3e

ℹ️ 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 internal/controller/coderaiprovider_controller.go
Comment thread internal/controller/coderaiprovider_create.go
Comment thread internal/controller/coderaiprovider_create.go
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@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: 9cff0b8897

ℹ️ 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 internal/controller/coderaiprovider_create.go Outdated
Comment thread internal/controller/coderaiprovider_create.go
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

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

Reviewed commit: 9cff0b8897

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.

The CoderAIProvider reconciler (still not registered) now creates the
Coder provider when the name is free: it writes the create marker and the
key's change token first, then POSTs the provider disabled with the nonce
display name coder-k8s:<UID>. An uncertain answer keeps the marker, and
the next reconcile reads Coder before it sends anything again. The nonce
and the type prove ownership. Then one PATCH per generation sets the
display name, icon and enabled flag. The status message for a bad
baseURL no longer repeats the URL.

Refs #154

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I504b0bb461fb75ecba41154e9d34e01e642af890
Store the provider ID before the apply PATCH replaces the nonce display
name, so a crash or a failed status write after the PATCH cannot turn the
provider into a NameConflict. Check the applied display name, icon and
enabled flag in the update answer before reporting Ready. Treat a create
answer without the expected key as uncertain, so a read confirms it.

Refs #154

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: Ie28921f3eaf3a24af2a6d7b4cf47211b0652758f
A rotated key Secret does not change the generation, so check the change
token before the ready shortcut. Retry a 401 or 403 on the apply PATCH,
and treat a 403 on the create as a create without effect, because no
Kubernetes event follows a repaired operator token or owner role.

Refs #154

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I0eedef2da992751b7089adbc82b42b90572f97f4
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

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

Reviewed commit: 877bfedef1

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 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: 877bfedef1

ℹ️ 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 internal/controller/coderaiprovider_create.go
Comment thread internal/controller/coderaiprovider_create.go
@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 5036e32 Oct 13, 2026
10 checks passed
@ThomasK33
ThomasK33 deleted the ai-provider-create branch October 13, 2026 18:07
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.

1 participant