Skip to content

🤖 fix: provider and MCP creates adopt only by a secret nonce from the cleanup record - #429

Merged
ThomasK33 merged 14 commits into
mainfrom
ai-mcp-secret-nonce
Oct 31, 2026
Merged

ThomasK33 merged 14 commits into
mainfrom
ai-mcp-secret-nonce

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

The operator no longer adopts a Coder AI provider or MCP server config because its display name is coder-k8s:<object UID>. Anyone who can read the Kubernetes object sees that UID. A create now sends coder-k8s: and a random secret (crypto/rand.Text), and only the cleanup record holds it. After a lost create answer, the operator adopts only a resource whose display name is the recorded nonce. Once it knows the Coder ID, that nonce adopts no other resource.

A CoderAIProvider from before cleanup records now gets its record before its delete, as a CoderMCPServer already did. That backfill gets half of the bounded delete attempt, so a hanging Coder call still leaves time for the delete and the release.

Fixes #426
Fixes #422

Background

#423 kept create-path adoption by the temporary display name, because it is the only recovery for a lost create answer (#253). #426 tracks the gap: a Coder user who creates a resource with that display name can get it adopted, overwritten and later deleted by ID.

#422 status after this PR. Items 1, 4 and 5 were fixed in #423. Item 2 is a documented limit, and this PR adds the docs sentence. Item 3 is fixed for providers and documented for both kinds (see Risks). Item 6 is the recorded decision that the control plane hold never ends early.

Implementation

  • cleanup_record.go:
    • nextCreateNonce keeps the recorded random nonce while the record has no Coder ID, so a late commit is still recognized. After the ID is known, Coder has shown the nonce, so the next create gets a new one.
    • keptCreateNonce gives an object without a record (from before records) the old coder-k8s:<UID> nonce, because that is what its create sent.
    • adoptionNonce returns the nonce that may prove a found resource. It returns nothing when the record holds another Coder ID. It also returns nothing for an old UID nonce unless the object still has its create marker.
  • Provider and MCP create paths take the nonce from the record write, send it, and check it in every recovery path: the read, the create answer, and the read after a 4xx.
  • The MCP live delete and both sweeps use the recorded nonce. The sweep assertion accepts the old UID form or the random form.
  • CoderAIProviderReconciler.deleteStep backfills a missing record. The MCP delete no longer re-reads the record after its backfill: that read used a context that a hanging Coder call had used up, which turned the bounded pass into a reconcile error loop.
  • Docs: nonce rules, the migration rule, read access to records, the item 3 limit, and the item 2 sentence.

Validation

Each test commit comes before its fix. On main (03eca051) with the new tests, these fail:

  • TestCoderAIProviderUIDDisplayNameNotAdopted and TestCoderMCPServerUIDDisplayNameNotAdopted: a person's resource with coder-k8s:<UID> was adopted.
  • Test*LostCreateAnswerRecoversByNonce, TestCoderAIProviderNonceReuse and TestCoderMCPServerNewNonceAfterKnownID: the nonce was the UID. The recovery itself passes on both.
  • Test*ShownNonceNotAdopted: a person replaced a known resource with one that carried its nonce.
  • Test*OldNonceAdoptedUntilNextCreate and Test*OldNonceNotAdoptedWithoutPendingCreate: the migration rules.
  • TestCoderAIProviderRecordBackfillOnDelete, and TestCoderMCPServerRecordBackfillOnDeleteHangs (fails on main: reconcile error from an expired context).

TestCoderAIProviderRecordBackfillOnDeleteHangs passes on main. It guards the new provider backfill.

Local gates on f18b14f6: make verify-vendor, make build, make lint, make test, make test-integration, nix develop -c make docs-check, and markdownlint-cli2 and cspell on the two changed docs. Two independent pre-PR reviewers (claude-opus-5-5, gpt-6.1-sol) checked the branch. Their findings on known-ID replay, open-ended old UID nonces and the bounded backfill are fixed here.

Risks

  • Residual window, kept by design: after a create commits but its answer is lost, Coder shows the nonce to users who can read the resource until the operator learns the ID, normally at the next read about 5 seconds later. Such a user can delete the resource and create a replacement with that display name. Such a user can delete the resource anyway, and Coder offers no owner or idempotency key that could tell the two apart.
  • The nonce is a secret in a ConfigMap. The install gives ConfigMap access in the operator namespace only to the operator's ServiceAccount, and the docs Roles grant only coder.com resources. A view, edit or admin binding in that namespace, or a ClusterRoleBinding of them, exposes it. The docs say so. A Secret store would need new RBAC, so it is not in this PR.
  • Migration: a create from an older version that is still in flight across the upgrade and commits after the new version's next create now stalls with a false NameConflict or SlugConflict. Nothing is adopted or deleted wrongly. The sweep reports it with ManualCleanupNeeded.
  • Cleanup follow-ups after #419: display-name delete proofs, legacy backfill, reason wording #422 item 3, MCP and provider: a record needs Coder's deployment ID. When the deployment ID read or the record write fails, and the delete also fails, in the one bounded attempt, the finalizer still goes without a record. Holding it longer would block a namespace deletion (Hold a deleted CoderControlPlane while CoderMCPServer objects still use it #382). The operator logs the ID with ControlPlaneGone, and the docs say to delete the resource by hand.
  • No change to API types, RBAC or manifests.

Generated with mux • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

…426)

Change-Id: I09458d1f910f09b9016af22eef8325af09f82e5d
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…cord (#426)

The temporary display name of a create was coder-k8s:<object UID>, which anyone
who can read the object sees. A create now sends coder-k8s: and a random secret
that only the cleanup record holds, and the create path adopts only a resource
with the recorded nonce. A record of an older version keeps its UID nonce until
the next create.

Change-Id: I24cef38b9d832dd866e3289057c18ebd53bb629b
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…te (#422)

Change-Id: I4deab98eed66e407e8d36895642cbdfa5b38c2eb
Signed-off-by: Thomas Kosiewski <tk@coder.com>
#422)

Change-Id: I17a9040ca9554da0c0d17151eb083563f5d2df58
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ord limits (#426, #422)

Change-Id: If36e326cd9f555815f7b8a5742617426c06d37fb
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I8d0394a03baa8bb1bd1764ce252a2c42713f8a64
Signed-off-by: Thomas Kosiewski <tk@coder.com>
#426)

Change-Id: I6cfb631af524abd36a9b552558341491f53b328e
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…leases (#422)

Change-Id: I4a95915b3b9b432eaa756e284975405a14cbc111
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…able to release (#422)

Change-Id: Id24ae05ac3be7f5ec092d17d3097e49c409598ae
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I44fd62a03699678a24c555ccb5b79d6608363ee3
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…eaves time for the delete (#426, #422)

Change-Id: Ia4f49570e3917aa36257d7a031f052bb6380b87c
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ackfill leaves time for the delete (#426, #422)

Change-Id: I291c169de2a99b325ecdd6353c24820b3509caba
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ords (#426)

Change-Id: I1e511651edd2d12e5e0cc67e50a7a2d43b285028
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I735cf368639faf217a6219f073aef7e884d8900f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 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-31T00:04:02.775575Z f18b14f PR opened
🔒 Security Review ✅ Completed 2026-10-31T00:08:04.300701Z f18b14f 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.

@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: f18b14f64f

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.

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 5581f95 Oct 31, 2026
20 of 23 checks passed
@ThomasK33
ThomasK33 deleted the ai-mcp-secret-nonce branch October 31, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant