Repository navigation
Conversation
Console runs HA behind a load balancer, so consecutive CLI requests can land on different nodes. Each node authorizes from its own permission-graph cache, which it refreshes asynchronously. A chained apply -- create a group, then create a child of that group -- could therefore fail authorization on the second call simply because the node that served it had not yet seen the group the previous call created. Console now stamps every response with X-Cdk-Graph-Version, a monotonically increasing int64 identifying its permission-graph state. A client that sends back the highest version it has seen tells the server "do not authorize me against a cache older than this"; the server blocks briefly until its cache has caught up. That restores causality for a CLI run without any sticky routing from the load balancer. Hook this into the shared resty client in Make rather than per command, so every Console API call participates, including ones added later: - OnAfterResponse records the version, keeping the maximum seen - OnBeforeRequest echoes it once we have a non-zero value Tracking is in-process only -- a fresh invocation starts from zero and sends no header until the first response carries one. There is no state file and nothing persisted to the environment, so the header cannot outlive the run that observed it or leak between unrelated invocations. apply runs resources concurrently against one shared client when --max-parallel is set, so the counter is an atomic int64 updated through a compare-and-swap loop; a plain load/store would drop versions under a race. Missing and unparsable headers are both ignored silently -- an older Console sends no header at all, and this must never be the reason a request fails. The Gateway client is deliberately untouched: Gateway does not serve the Console permission graph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
emupton
force-pushed
the
feat/graph-version-echo
branch
from
November 2, 2026 09:22
e67082a to
bf9d19f
Compare
emupton
marked this pull request as ready for review
November 2, 2026 11:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pairs with conduktor/console-plus#5920 (stale-while-revalidate permission graph) and its stacked header PR.
Why
Console's permission-graph cache is moving to stale-while-revalidate: after a permission write, a Console node that didn't process the write serves the previous graph for the duration of one background rebuild (seconds). A CLI chaining dependent applies — create a group, then create a child entity the new grant authorizes — can race that window when a load balancer routes consecutive requests to different Console nodes: the second call 403s, a retry succeeds.
Console closes this for clients that opt in: every API response carries
X-Cdk-Graph-Version(a monotonic int64), and a request echoing a version makes the handling node catch its cache up before authorizing.What changes
The shared resty client now:
X-Cdk-Graph-Versionfrom every response and keeps the max seen (atomic —apply --max-parallelruns goroutines against one client);In-process only: a fresh invocation starts clean and sends no header until the first response carries one. Missing or unparsable headers are ignored; older Console versions are unaffected. Hooked at the client level so every call site participates, including future ones.
What this deliberately does NOT do
There is no retry-on-403 here. The echo works by blocking, not failing: a request carrying a version makes the Console node wait for its cache to catch up before authorizing, so a version mismatch surfaces as a slightly slower response, never as a 403 — within the covered flow, retry logic would be dead code.
A 403 from graph staleness therefore remains possible only where no version has been seen yet:
conduktorinvocations in a CI script: the second process starts clean and sends no header on its first call.These windows are seconds wide (one graph rebuild) and self-heal; scripts that chain separate invocations need their own retry for now. If that proves flaky in practice, the natural follow-up is a single bounded retry on 403 in the shared client — not persistence of the version to disk, which brings file-locking and parallel-CI problems.
Validation
go build ./..., fullgo test ./..., plus unit tests covering: capture, echo on next request, no regression on a lower version, no header when none seen, unparsable values ignored, concurrent-use safety.🤖 Generated with Claude Code