Repository navigation
🤖 fix: keep recorded provider IDs and add cleanup records for MCP servers - #419
Conversation
Change-Id: I12b6b4f617c2057de880b3800b8cc9cf08502174 Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: If109ee2e3ac425d7c6462a06f67997a5f3fcd96e Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Ia2e08b94b5c2e258d8eec9e0078f9442b641a6ff Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I312bf9f47741f74373c895cc0640d237a621a60f Signed-off-by: Thomas Kosiewski <tk@coder.com>
…cases (#353) Change-Id: I7090bb024a2cc861f5b30e1bee9e6cfe448e9030 Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: If70ce36eb7a6c8eab57523150a32ff7c9f8064bd Signed-off-by: Thomas Kosiewski <tk@coder.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Change-Id: I2607ba5c0198a39eb8f95ee3e1d841fa0470ff86 Signed-off-by: Thomas Kosiewski <tk@coder.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ece19e1fc9
ℹ️ 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".
…fter a deployment switch (#353) Change-Id: I08581faa886e871b8928e4b763b5657bad43653a Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4c543839b9
ℹ️ 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".
|
@codex review The finding on |
|
@codex security review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Change-Id: I03330d46559a94b2fe57346dcd17875fc9f5d698 Signed-off-by: Thomas Kosiewski <tk@coder.com>
…le a config can be its own (#353) Change-Id: I8ad87b4d6a2ccdb9a4d21cc6287b928d99d613ce Signed-off-by: Thomas Kosiewski <tk@coder.com>
…manual cleanup while a config can be its own (#353) Change-Id: I62170dd522143a8ea677de9a62e78ee6d00ecfda Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
@codex review |
|
@codex security review |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6474666aa5
ℹ️ 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".
…onfigs, and an organization stall after a deployment switch (#353) Change-Id: I31648be19fc5faaf5555f56916188df5c819b43b Signed-off-by: Thomas Kosiewski <tk@coder.com>
… and report a deployment switch before an organization stall (#353) Change-Id: I409d537e4e4ee56806d712721808ab8c9ea9d324 Signed-off-by: Thomas Kosiewski <tk@coder.com>
|
@codex review |
|
@codex security review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 22aa163eca
ℹ️ 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".
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Summary
Goal: a namespace deletion must not leave Coder resources behind that no one can find again. This PR closes the last open items of #353. It does two things:
status.providerIDwas empty but the cleanup record held the provider ID (the state after a lost status write), the controller rewrote the record without the ID. A later delete then removed the record and released the finalizer without a DELETE, and the provider stayed in Coder. Codex found it on 🤖 fix: finish the control plane hold and cleanup record follow-ups of #353 #410 (🤖 fix: finish the control plane hold and cleanup record follow-ups of #353 #410 (comment)). The record now keeps its ID unless a new create follows.CoderMCPServer. MCP server configs now get the same record as providers and keys. A config that a namespace deletion leaves in Coder is deleted by a later sweep through a control plane of the same Coder deployment, when the record holds the config ID. A record without a config ID never deletes anything. While a config in the organization can still be the object's, the record stays and the operator reports it for manual cleanup.Every other open item is closed by a decision. The reasons are in the Decisions section.
Fixes #353
Refs #156
Background
#353 tracks what a namespace or control plane deletion can leave in Coder. The main parts shipped in v0.1.5. #410 closed 6 follow-ups and left 7 open. This table covers each open item on
mainat 93d8854:main#362 items 2, 7 and 9 and #384 items 1, 2, 4 and 5 were done in #362 and #410.
Implementation
Provider fix (
coderaiprovider_record.go):writeAIProviderRecordkeeps the previous record's provider ID when the status has none and no create follows. A new create still drops the ID. The controller creates only after Coder answers 404 for the provider name, and Coder cannot rename a provider, so the old ID is gone at that point.MCP server records follow the provider design and the invariants of option B′ (#353 maintainer decision comment):
createMCPServerwrites the record before the create marker and the create request. The record holds the deployment ID from/api/v2/buildinfo(read in the same reconcile), the temporary display name (nonce), the pinned organization ID, the slug, and the config ID once known. It holds no credential. A failed record write or deployment ID read sends no create.CoderDeploymentChangedon a mismatch, before a slug conflict in another deployment can clear the marker.check()writes the record of a config from before records existed. It runs before the opt-in, organization and credential checks, so an object that a kill switch disables gets its record too. The kill switches never wait on the new buildinfo read.codermcpserver_apply.go) first checks the record's deployment. A control plane that now reaches another deployment stalls the object withCoderDeploymentChanged, keeps the config ID and creates nothing. Before this check, an applied object reportedDeletedExternallyin that case (Codex review on this PR).codermcpserver_delete.go) reads the record once. It checks that the control plane still reaches the recorded deployment, uses the recorded config ID when the status lost it, and removes the record only after Coder confirms the delete. Every release without a delete keeps the record. With only a create marker, the finalizer behavior ofmainstays. The record also stays when another config holds the slug, or when a config with the nonce has another slug: a person can have renamed the config of a lost create answer.cleanup_sweep.go) gets an MCP kind. It deletes the recorded config ID. A record without a config ID deletes nothing, the same rule that 🤖 fix: revoke a CoderProvisioner key created at an intermediate control plane endpoint #413 adopted for provisioner keys. A person can edit the slug and the display name of any config in Coder (UpdateMCPServerConfigRequesthas both), so neither proves that the operator created a config. The sweep lists the recorded organization as a site owner. When it holds no config with the recorded slug and none with the nonce as display name, the record goes. Otherwise the record stays: the sweep sets the gaugecoder_k8s_cleanup_records_unmatched, writes aManualCleanupNeededWarning Event on the ConfigMap that names the config, logs it, and checks again every 10 minutes. A name or display-name mismatch is never proof of absence. The sweep also watchesCoderMCPServerdeletions. The sweep kinds now return one of three outcomes (retry, gone, kept) instead of agoneflag.APIReaderandOperatorNamespace, and asserts both at setup.mcpServerWithSlugtakes a slug instead of the object, so the sweep can use it.The how-to
manage-mcp-servers.mddocuments the records, theCoderDeploymentChangedreason, the sweep, and the manual cleanup of a record without a config ID.Two choices in the fix round go beyond keeping a record when a config has the slug. Both only keep more records and delete nothing more:
Codex review round on 6474666 (three P2 findings, all fixed in 22aa163):
deleteStep(no status ID, no create marker) no longer removes a record without a config ID.recordMCPServerclears the marker onSlugConflictalso when a person renamed the config of a lost create answer. The finalizer still goes at once, and the sweep decides.deleteStepbackfills the record of a config from before records existed, after it has Coder access and before the delete, so a bounded attempt that fails still leaves a record.OrganizationNotFound,OrganizationChanged),check()reportsCoderDeploymentChangedfirst if the record names another deployment. The buildinfo read runs only in that failing branch.Decisions
These items stay without code. Each decision states what users see and why.
operatorAccessReady: falsedoes not prove that no dependent cleanup is in flight: a cleanup that read a valid token before the status flip can still call Coder.TestControlPlaneHoldsForCleanupInFlightcovers that case, and 🤖 fix: finish the control plane hold and cleanup record follow-ups of #353 #410 rejected two early releases for this reason. The cost is a delay of at most 10 minutes. A dependent that then releases without a delete keeps its cleanup record, so a sweep can still delete its resource.TestControlPlaneHoldsWithoutCoderDeployment). The cost is the same bounded delay, and records cover what stays.time.Now()and the real clock return the same time. The hold end lives in shared functions (controlPlaneHolds,controlPlaneHoldEnd,controlPlaneHoldRequeue).TestControlPlaneHoldEnddrives them with a fake clock through the key path, andTestAIProviderDeleteUnderHoldEndsAtHoldEndcovers the provider pass at the hold end. A provider clock field would have no test that needs it.CoderControlPlanewhose Coder answers the recorded deployment ID and then a 404. That person already runs a Coder image of their choice behind the operator's access. The sweep sends that control plane's own token only to that server, and it cannot delete anything in the real Coder. The result is the v0.1.4 behavior: a resource stays in the real Coder without a record. A fix needs a trust anchor that survives a namespace recreation. The normal recovery also recreates the namespace, so a control plane UID check would block it, and the Namespace deletion leaves AI Gateway keys and AI providers in Coder #353 design rules out URL matching.Validation
TestCoderAIProviderKeepsRecordedIDWithoutStatusIDlanded as its own commit (e0cf501) before the fix. Onmainthe record ID became empty. With the fix, the record keeps the ID and the delete sends DELETE for it.TestCoderMCPServerCleanupRecord,TestCoderMCPServerNoCreateWithoutRecord,TestCoderMCPServerRecordKeptWithoutDelete,TestCoderMCPServerPendingCreateAfterDeploymentSwitch,TestCoderMCPServerKeepsRecordedIDWithoutStatusID,TestCoderMCPServerRecordBackfill,TestCoderMCPServerRecordBackfillOnDisable,TestCoderMCPServerRecordAfterLostCreateAnswer,TestCoderMCPServerRecreateDropsGoneID,TestCoderMCPServerDeploymentSwitchKeepsRecord,TestCoderMCPServerDeleteRecordEdgeCases,TestCoderMCPServerSetupNeedsRecordDependencies.TestCleanupSweepMCPServer(settle window,NoMatchingDeploymentEvent and gauge, sweep through a new control plane of the same deployment, swept counter) andTestCleanupSweepMCPServerLivingObject(living object untouched).TestCleanupSweepMCPServerWithoutConfigID: a record without a config ID never sends a DELETE. The record goes after a create that never committed. It stays with aManualCleanupNeededEvent for a person's config with the slug, for a person's config that copies the temporary display name, and for the config of a lost create answer after a person edits its display name or its slug.TestCleanupSweepMCPServerManualCleanup: the kept record counts in the gauge, and after a person deletes the config, the next sweep removes the record and the gauge drops to 0.TestCoderMCPServerDeletePendingCreatenow also asserts which delete outcomes keep the record.a person's config with the temporary display namesends 1 DELETE (expected 0), andlost create answer, display name editedremoves the record (RequeueAfter0s, expected 10m). The slug-edit, person's-slug and live-path cases fail too.create never committedpasses there and on the fix. All pass on 6474666.TestCoderMCPServerDeleteKeepsRecordAfterSlugConflict,TestCoderMCPServerRecordBackfillOnDelete, andTestCoderMCPServerDeploymentSwitchKeepsRecord/organization not in the new one.mainwith only the two new reconciler fields added, so that they compile. All fail for behavior reasons (no record, no MCP sweep kind, no setup check), except two that guard new code and pass onmain:TestCoderMCPServerDeleteRecordEdgeCases/foreign ConfigMapandTestCoderMCPServerRecreateDropsGoneID(mainhas no record to drop).appliedsubtest ofTestCoderMCPServerDeploymentSwitchKeepsRecordthen seesDeletedExternally). Removing the provider fix fails its test too.pendingCreateincodermcpserver_delete_test.gonow clears the record's config ID, because a real pending create has none.mcpRequestsleaves out buildinfo reads, which are not MCP requests.mainat 968c155 (🤖 fix: map uncertain aggregated write answers to 504 and list unconfigured namespaces as empty #400):make verify-vendor,make test,make build,make lint,make test-integration,make docs-check(throughnix develop), plusmarkdownlint-cli2andcspellon the docs.Risks
ConfirmingCreate, every 5 seconds), each reconcile reads buildinfo once more and rewrites an unchanged record. Every MCP reconcile reads the record live once or twice. Providers do the same.SlugConflictfor its own config, because only the record proves ownership. Its delete still uses the recorded ID. Providers behave the same withNameConflict. A follow-up can accept the recorded ID as proof.ManualCleanupNeededEvent and the gauge report it, and the how-to has the steps. This is the cost of the rule that 🤖 fix: revoke a CoderProvisioner key created at an intermediate control plane endpoint #413 adopted for provisioner keys.CoderRejected) now leaves its record to the sweep instead of removing it at delete time. The sweep removes it after the settle window when no config has the slug or the nonce. Without a ready control plane of that deployment in the namespace, the record stays withNoMatchingDeployment, as any record does.releaseWithoutDeletelogs the slug and the config ID and counts the release, as onmain.main, not fixed here: theCoderAIProvidersweep without a recorded ID still deletes a provider whose name, display name and type match the record, and the MCP live delete path still deletes a config whose slug and display name match the nonce. The independent verifier says these rely on the same assumption that a person cannot copy the display name. The maintainer will file a follow-up.CoderAIProviderwhose control plane now reaches another deployment still reportsDeletedExternally, notCoderDeploymentChanged, as before this PR. Its record and delete path already handle the switch, so nothing leaks. The MCP check above can be ported toproviderGone.CoderMCPServerstatus and printer code. This PR changes no status write or printer column, and the branch mergesmainat 2054f14 without conflicts.Generated with
mux• Model:anthropic:claude-opus-5-5• Thinking:xhigh