🤖 fix: make workspace resourceVersion a fingerprint of the returned representation - #115
Conversation
…epresentation CoderWorkspace.metadata.resourceVersion was Coder's Workspace.UpdatedAt in nanoseconds. On Coder 2.37.2 builds, rename, TTL and autostart changes did not advance that timestamp, so the exposed value did not identify those same-object changes and an old token still matched in UPDATE and DELETE precondition checks (#109). Compute the token from what this API returns instead: the converter builds the CoderWorkspace with resourceVersion unset, serializes it and assigns the full hex SHA-256. Every emitted metadata, spec and status field takes part, including lastUsedAt and autoShutdown, so an exposed change alters the token while a backend-only timestamp change with an identical projection keeps it. The token is opaque and not monotonic: an identical representation returns the same token again. UPDATE and DELETE keep comparing against the freshly fetched snapshot (not an atomic compare-and-swap), and uid still protects recreation identity. The three storage paths that patched the response with the build's UpdatedAt are removed; the LatestBuild replacement and mutation order stay. Tests reproduce the symptom with a mock that keeps updated_at frozen: converter tokens track each exposed field family (rename, template and version, build id and status, TTL, autostart, lastUsedAt, autoShutdown, identity and namespace metadata) and stay stable for identical or backend-only changes; the storage flow covers GET stability, UPDATE with a changed token, the local Modified watch event, LIST presence, stale UPDATE and stale DELETE returning 409 without a build transition, a live uid+token delete, and out-of-band TTL, autostart, build status and rename changes. The how-to documents the contract and compatibility limits (tokens from earlier releases no longer match; template resourceVersion and watch behavior are unchanged). Part of #109 _Generated with `mux` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$254.50`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=254.50 --> Change-Id: I2682de8b16153df39c3456b63f680893c7ad52ce Signed-off-by: Thomas Kosiewski <tk@coder.com> --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh`_
Correct the watch bullet added with the representation-fingerprint resourceVersion: state the working request (supply the current opaque token, omit sendInitialEvents and resourceVersionMatch; the token is ignored once the options are accepted and is not a replay cursor) and record the observed rejections under WatchList defaulting (omitted or "0" resourceVersion -> 400 because initial events are unsupported; resourceVersionMatch rejected; sendInitialEvents=false without a matching option rejected upstream with 422). Documentation only; no product, watch, or test changes. Broader HTTP watch option coverage is tracked in #112. _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh`_ Change-Id: I0d310b02c8c7c709a7719a2e43e06d0a2ee99a13 Signed-off-by: Thomas Kosiewski <tk@coder.com> Integrated the doc-only correction without changing the validated tree. Signed-off-by: Thomas Kosiewski <tk@coder.com> --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh`_
Extend the stopped-workspace rename case of hack/e2e-workspace-lifecycle.sh: capture the genuine pre-rename object and token; after the out-of-band rename require the same UID, a changed canonical name and a changed token; require 409 for a stale UPDATE (current object under the new name, old token) and a stale DELETE (new name, live UID, old token); then require the backend latest build ID, the backend build count and the object to be unchanged. A new case requires the renamed object in LIST with the same token as GET. The receipt records both tokens and the new cases. Offline tests model the old timestamp behavior (rename keeps the token) and negatives for accepted stale UPDATE/DELETE, rejected-but-mutating stale requests, a missing LIST item and a LIST/GET token mismatch; each stops before the wrong-UID/live deletes and the recreate. _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_ Change-Id: I325f475ec99aaeab3597e5b80fc1616eb50f30a8 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. |
|
@codex security review |
#109 runtime evidence on
|
🛡️ 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
Make
CoderWorkspace.metadata.resourceVersionan opaque fingerprint of the representation this API returns: the full SHA-256 of the converted object, serialized withresourceVersionunset. On Coder 2.37.2, builds, renames, TTL and autostart changes do not advance the workspaceupdated_atthat the old token came from. So a stale token still matched, and UPDATE and DELETE preconditions did not detect those changes.Part of #109. The required Kind E2E now proves stale-token rejection on a real backend, using the maintained workspace driver added in #114.
Background
Workspace.UpdatedAtin nanoseconds. Coder 2.37.2 leaves that timestamp unchanged across builds, rename, TTL and autostart. Earlier runtime evidence showed the unchanged timestamp and the stale token still being accepted.resourceVersionas opaque: clients may compare it only for equality. This server does not implement watch resumption orresourceVersionMatch, so a non-monotonic token is permitted.resourceVersionis unchanged (template metadata updates already change it).Implementation
internal/aggregated/convert/workspace.go: build theCoderWorkspacewithresourceVersionunset, serialize it, and assign the hex SHA-256. Every emitted metadata/spec/status field takes part, includingstatus.lastUsedAtandstatus.autoShutdown. A serialization error is an impossible state and panics with anassertion failed:message.internal/aggregated/storage/workspace.go: remove the three places that patched the response with the build'sUpdatedAt. GET, LIST, mutation responses and local watch events now share one conversion.preconditions.resourceVersionis supplied) compare against the freshly fetched object and return409 Conflictbefore any Coder mutation. This is a snapshot check, not an atomic compare-and-swap;uidstill guards recreation identity.updated_atcovers GET stability, UPDATE with a changed token, the localModifiedevent, LIST presence, stale UPDATE and stale DELETE returning 409 without a build, live delete, and out-of-band TTL, autostart, build status and rename.docs/how-to/deploy-aggregated-apiserver.md: documents the contract, compatibility limits and watch options (current opaque token; omitsendInitialEventsandresourceVersionMatch).hack/e2e-workspace-lifecycle.sh: in the stopped-workspace rename case, capture the genuine pre-rename token. After the out-of-band rename, require:Validation
make test,make build,make verify-vendor,make lint,make docs-check(strict),make test-scripts,bash -n,shellcheck -xand actionlint pass on00065225.Risks
409after upgrade and must re-read. Tokens are not monotonic and can repeat if the whole representation returns to an earlier value.lastUsedAtand build status are part of the token, so activity between a read and a write can produce a409without a user edit. That is intended; re-read and retry.📋 Implementation Plan
Workspace representation versions (#109)
Goal and scope
Make CoderWorkspace resourceVersion detect changes in the representation returned by this API, even when Coder does not advance Workspace.UpdatedAt. Keep UID identity checks and the existing non-atomic backend mutation boundary. One cohesive PR, target <=500 changed lines including tests/docs. No template-version change, persistent counter, watch redesign, installer change, or import-readiness work.
Contract
Implementation and quality gates
Hands-on dogfooding
Use one new owned KIND cluster, namespace, image tag, KUBECONFIG, cache/temp and evidence root. Build/load the exact committed source and run Coder v2.37.2. Prepare all scripts and an output/resource ownership audit before launch. Never use a shared deployment or change global settings.
Stop conditions
Stop and report a concrete blocker on unexplained source/output mutation, setup mismatch, missing native receipts, contract conflict, or inability to demonstrate status-inclusive tokens through real backend operations. Keep implementation small; do not add persistence, polling, field exclusions or a new subsystem to force a green test. Publication, required reviews, final independent recommendation, merge queue and post-merge verification remain separate gates owned by the maintainer desk.
#109 PLAN addendum — runtime validation through the maintained E2E (the accepted PLAN.md contract is unchanged)
Why the sequencing changes
The required Kind E2E now runs a maintained CoderWorkspace lifecycle driver on the exact PR head for aggregated-API pull requests (#113, landed f8dab5b). That replaces the ad hoc local harness for runtime evidence. Earlier local packets (v5–v8, Stage-1, supplement) stay FAIL and are not relabeled; none of their scripts are reused.
Integration
fix/workspace-rv-e2efrom main f8dab5b. Cherry-pick d4737060 and 67067993 unchanged; they touch no 🤖 ci: cover CoderWorkspace lifecycle in the Kind E2E job #113 files. Keep 67067993 and all frozen packets intact.New runtime assertions (maintained driver only, small extension)
Reuse the stopped-workspace rename scenario:
Existing watch/update, fresh-token delete, recreate and prior-UID checks continue.
Offline qualification before publication
Stubbed-tool tests model the old timestamp behavior (token unchanged by rename) and require the driver to fail on it. Negatives: stale preconditions ignored (request succeeds), token equality where it must differ, renamed item missing from LIST, and any failure stopping later mutations.
Runtime and review bounds
One PR-triggered E2E run on the final commit is authorized after offline gates pass. Classify any failure before any further attempt; no automatic rerun. Six assessments total including automatic reviews on open; one final independent assessment reserved. Merge-queue E2E and post-merge checks still required.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:xhigh• Cost:$965.57