Uh oh!
There was an error while loading. Please reload this page.
fix(cli): resolve the backend env once per invocation, not once per reader (backend#2320) - #551
Conversation
…eader (backend#2320) The env/base-URL resolution family, not the one site #540 named. Each of the last recuts produced one more finding about a site that answered "which backend?" differently from its neighbour (cli#528 review, #542 review, now #540) — the failure mode is additive, so only the SECOND site is ever a bug. - telemetry: recordCommandOutcome took the resolved env as a parameter instead of calling telemetryEnv(signedInEnv()) a second time. The label and the sink (spool path + POST destination) now derive from one value, which is what the comment above RecordCommandOutcome already claimed. This is #540's finding. - sessionEnv is now the single config -> session-env resolution point, and it normalises (trim + lower-case) like api.ResolveEnv. Returning cfg.CurrentEnv verbatim made it the one env-resolving function whose output was not normalised: invisible where the value only reaches api.BaseURL (which lower-cases again), load-bearing where it is COMPARED, or where one consumer trims and another does not — api.BaseURL does not trim, so " dev " fell through to PROD. - `cluster doctor` built its API client from cfg.CurrentEnv raw and `auth status --check` compared it raw against an already-normalised target. Both go through sessionEnv now: a doctor probing prod with a dev token reports "session expired" for a session that is fine, and the installer, whose contract is --check's exit code, re-ran login against a working session. - internal/doctor.backendHost derives its host from api.BaseURL instead of restating the same three hosts in a second switch. Behaviour-identical (BaseURL already lower-cases); it removes the copy that drifts. - A guard test pins the closed set of sanctioned resolution sites, so the next one fails a check instead of a review. NOT changed: api.BaseURL's unknown/empty -> prod fail-open. It is shared with the installer's _backend_url and contradicted by client-runtime's controller.py, so it is a three-component decision tracked on backend#2171. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… previous edit Comment-only; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e stored string (backend#2320) The audit's last site: `auth status` printed cfg.CurrentEnv as its "backend" field while runAuthCheck — the machine-facing answer to the same question, 40 lines below in the same file — compares the resolved one. A status command that disagrees with the client is worse than no status command. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My own bug, and worth the comment it now carries. Past the session probe,
`cluster doctor` loads the real kubeconfig and calls the real doctor.Run, whose
checkBackendEgress probes backendHost("") — a live GET to
https://api.tracebloc.io/. On a developer machine with a real k3d cluster the
test therefore made a production request; the run time (~16s vs 0.01s stubbed)
is the tell. Stubbing loadClusterFn returns right after the session probe, which
is all this test needs: the env is decided before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Picks up #550 (the formatter gates walking the working tree instead of the repo), so make check here runs the gate CI will run. No file overlap with this branch's changes.
LukasWodka
left a comment
There was a problem hiding this comment.
Requesting changes on the guard test only — the production consolidation is right, and I checked it rather than taking the table on trust.
What I verified holds.sessionEnv is now the single config→session chain, and the three re-derivation sites really do route through it (internal/cli/doctor.go:118, auth.go:420, auth.go:467). signedInEnv's single production caller is RecordCommandOutcome, so changing it from "returns "" when not signed in" to "always resolved" breaks no consumer — I grepped every caller. The "Dev" case is a genuine latent fix, not just tidying: api.IsKnownEnv lower-cases but the oldsignedInEnv did not, so telemetryEnv("Dev") fell to the unknown arm and labelled the record prod while the client dialled dev — the exact mislabelling the comment above RecordCommandOutcome claims to prevent. backendHost deriving from api.BaseURL is sound (url.Parse over the closed set of https://… returns, host-only, prod on the unreachable error path), and the doctor.go sanctioning is doing real work: it has two raw needle hits, both in prose, so the comment-stripping is load-bearing for it passing at all.
What I'm holding on.TestNoNewEnvironmentResolutionSiteAppears is described as "the part that stops the next recut finding one more site," and it has three demonstrated fail-open holes — I armed the control first, then evaded it three ways against 4452b70:
| probe | expected | actual |
|---|---|---|
| new site, needle on its own line | 🔴 | 🔴 caught |
needle after // inside a string literal ("https://x/%s") | 🔴 | 🟢 missed |
a /* inside a string literal ("/*.json") hides every needle below it | 🔴 | 🟢 missed |
new site in a sibling package (internal/cluster) | 🔴 | 🟢 missed |
Details and the fix in the two inline comments. Neither is hypothetical: seven non-test files in internal/cli already carry a https:// literal, and the guard covers 1 of the 17 packages under internal/ while the description claims the repo. go/scanner closes the first two in ~10 lines and makes an unparseable file a finding rather than a silent pass; walking the module root closes the third and lets the allowlist carry the reasons the PR body already writes out for internal/api, internal/config and internal/doctor.
The third inline comment is non-blocking — a now-false docstring on signedInEnv.
Everything else here I'd own. Re-request me when the guard's own coverage is pinned and I'll re-run the evasion table.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit eb299b8. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
Four review findings, all the same class — a check that verifies the FORM of a
thing rather than the property the form exists to guarantee. Which is the class
this PR is about, so the guard having it was the worst possible place for it.
- Scan Go as Go. The hand-rolled comment stripper was fail-open on string
literals: the `//` inside an `https://…` literal started a "comment" that ate
the rest of the line, needle included, and a `/*` inside a literal like
`"/*.json"` swallowed every needle below it to the next `*/` or EOF. Seven
non-test files in internal/cli already carry an https:// literal, so this was
one future line away. go/scanner with mode 0 drops comments and knows literals,
so neither evasion exists. Literals are kept in the output — a needle inside a
string is then a loud false positive, which is the cheap direction.
- Walk the module root, not the test's own package dir. The guard covered 1 of
the 17 packages under internal/, i.e. it was blind exactly where the next site
is most likely to land: a new package written by someone who never reads
internal/cli. Keys are now repo-relative, and internal/api, internal/config and
internal/doctor join the allowlist with the reasons the PR body already gave.
- An inert allowlist entry now FAILS. Checking only that a sanctioned file exists
let my own change turn the telemetry.go entry into a licence: it matched no
needle any more, so it checked nothing while silently pre-approving the next raw
read in the very file whose double resolution this PR removes. The entry is gone
and the staleness assertion stops the next one going inert unnoticed.
- signedInEnv's docstring was false: it can no longer return "". Says so now,
including that `if signedInEnv() == ""` cannot fire — and telemetryEnv's empty
arm is marked production-unreachable-but-test-reachable rather than left to be
traced.
Also corrects an overclaim I made in the first draft of goCodeTokens' comment:
go/scanner is lexical, so the error covers unterminated literals and comments —
the faults that would desynchronise boundary tracking — not `func f( {`, which
scans clean. Stated precisely rather than left flattering.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>…both ways (backend#2320) Adopts Lukas's unified shape for the guard, which is better than what I pushed in f33a3d1 in two concrete ways: - `matchesAnyNeedle` is now THE matcher, called from the detection sweep AND the allowlist audit. Two copies of "does this file resolve an env?" is the same shape as the two copies of "which env?" this PR removes, and it would let detection and allowlisting drift apart exactly where nobody looks. - The allowlist audit is per ENTRY, not per suite, so staleness, the empty-reason check and the needles-went-stale backstop all fall out of one loop and the failure names the entry to delete. Renaming a needle now reports all five entries by name instead of a global counter hitting zero. Kept a narrow anchor the per-entry loop genuinely cannot see: an EMPTY allowlist makes that loop vacuous, so a needle rename plus an empty allowlist would pass in silence. It asserts both counts are non-zero. Eight reproductions, all red, all restored green — the control, both string-literal evasions, the sibling-package site, a re-inerted sanctioned entry, an empty reason, a needle rename, and a lexical fault elsewhere in the module. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LukasWodka
left a comment
There was a problem hiding this comment.
All four findings are addressed and I've verified each one against f33a3d17 rather than re-stamping — details on the three threads, which I've closed. Not approving yet only because Test is still pending; I'll approve on green.
The evasion table, re-run on the new head:
| probe | before | now |
|---|---|---|
| new site, needle on its own line | 🔴 caught | 🔴 caught |
needle after // inside a "https://…" literal | 🟢 missed | 🔴 caught |
/* inside a "/*.json" literal hiding every needle below | 🟢 missed | 🔴 caught |
new site in a sibling package (internal/cluster) | 🟢 missed | 🔴 caught |
| an inert allowlist entry (Bugbot's) | 🟢 missed | 🔴 caught |
Plus the check that matters more than any single probe: the allowlist is now the derived set, not a hand-maintained one that happens to agree. I tokenised every non-test .go in the module independently and counted needles in code only — five files have them, and they are exactly your five entries, while the three that grep hits in prose (internal/cli/doctor.go, telemetry.go, telemetry_installer_spool.go) are exactly the three correctly absent. So the guard, the allowlist and reality agree by construction rather than by coincidence.
Baseline green, go build ./... clean, go test ./internal/cli/ green — so broadening the needles to bare identifiers didn't import a backlog.
Two small things worth having on the record, neither a request:
The scanErrs > 0 fail-closed branch is unreachable in practice — an unterminated literal fails go build for the test binary first, in every package reachable from internal/cli. Your docstring is already careful about this (naming lexical faults and noting func f( { scans clean), so it's a correctly-scoped guarantee about the tokeniser rather than a check CI will ever exercise.
And the signedInEnv docstring went further than I asked: naming the trap outright — "Do NOT write if signedInEnv() == "" … it cannot fire" — is what actually stops the next reader, since the failure mode was trusting the old comment.
For the record on how this went: four independent holes in one guard, all in the same fail-open direction, and the production consolidation underneath never needed a change. That's the guard being the highest-leverage thing in the PR, which is why it drew the attention.
LukasWodka
left a comment
There was a problem hiding this comment.
Approving a670403c. Green gate clear: every check passes (quality / format skips — no shell or Python in this diff), mergeable, and zero unresolved threads including my own three, which I closed on the previous head after verifying each fix.
a670403c restructures the guard onto one needle set, one matcher and one allowlist, checked from both directions — the shape I sketched on the scope thread. I re-ran the whole table against it rather than assuming a restructure preserved the coverage:
| probe | result |
|---|---|
| baseline | 🟢 passes |
needle after // inside a "https://…" literal | 🔴 caught |
new site in a sibling package (internal/cluster) | 🔴 caught |
| an inert allowlist entry (Bugbot's) | 🔴 "is allowlisted … but resolves nothing any more" |
| a needle rename (typo all three) | 🔴 five per-entry failures + the anchor |
| an empty allowlist | 🔴 five detections + "the guard checked nothing: 0 sanctioned entries, 5 files matched" |
The needle-rename row is the one I most wanted to see, because the comment makes a specific claim — that the per-entry inert check subsumes the old global "needles went stale" counter, and does it more loudly. It does: five named entries to delete plus the anchor, instead of one counter reaching zero. Claim verified rather than accepted.
go build ./... clean, go test ./internal/cli/ green.
What the guard is now, stated plainly because it's the part worth reusing elsewhere in the org: one declaration of the needles, one matchesAnyNeedle called by both the detection sweep and the allowlist audit (so the two cannot drift), tokenisation by go/scanner so literals are literals, a module-wide walk, per-entry staleness that names the entry to delete, and an anchor for the genuinely-vacuous case. Every one of those exists because a specific hole was demonstrated, not because it sounded rigorous.
Thanks for taking four rounds on the guard without pushing back on any of them — and for the two things you added beyond the asks: the signedInEnv docstring naming the == "" trap outright, and the goCodeTokens comment scoping its guarantee to lexical faults instead of overclaiming that it catches malformed Go. The production consolidation underneath never needed a change; the guard drew the attention because it was the highest-leverage thing here.
LukasWodka
commented
Aug 22, 2026
/fr-pass |

Closes backend#2320. Fixes the unresolved Bugbot thread on the staging mirror (#540, "Telemetry env resolved twice") at the root rather than at the one site it named.
Why the whole family
Each of the last recuts produced one more finding about a site that answered "which backend am I talking to?" differently from its neighbour — cli#528 review (label via
ResolveEnv, client via the config), #542 review (delivercomputing its own spool path and draining another env's queue), now #540. The failure mode is additive: every new site looks locally correct, and only the second one is a bug. So this consolidates onto one resolution point per question instead of patching the named site.Every environment / base-URL resolution site in
cli(*)= changed here. Precedence is left-to-right.api.ResolveEnv(explicit)explicitarg,$CLIENT_ENV$CLIENT_ENV→prodprodapi.BaseURL(env)prodprod, silent — backend#2171, untouchedapi.IsKnownEnv(env)falsefalsecli.sessionEnv(cfg)(*)cfg.current_env, then$CLIENT_ENVcurrent_env→$CLIENT_ENV→prodprodBaseURLdecidescli.signedInEnv()(*)sessionEnvprod(via #4)cli.telemetryEnv(env)(*)prodprod(mirrors #2 deliberately)runLogin(--env)--env,$CLIENT_ENVIsKnownEnvrejectsprodrunAuthCheck(--env) (*)--env,$CLIENT_ENV, configprodauth statusdisplay (*)authedClient/logoutcluster doctorsession probe (*)pendingSink(env)/delivertelemetrySpoolPath/spoolEnvSlugunknownbucketunknownbucketdoctor.backendHost(*)CLIENT_ENVoff the jobs-manager Deploymentprodprodconfig.migrateV1envenv→prodprodSites 13 and 7 answer genuinely different questions (the cluster's configured env; the env a human named), so they stay separate by design — noted in comments so the next reader does not "consolidate" them.
What was wrong, and what changed
#540 — the label and the sink came from two config reads.
RecordCommandOutcomeresolved the env forpendingSink, thenrecordCommandOutcomecalledtelemetryEnv(signedInEnv())again for the emitter. Two reads that merely tend to agree, and disagree the moment aloginlands between them — exactly the "labelledstg, posted to prod" leak the comment aboveRecordCommandOutcomeclaims to prevent.recordCommandOutcomenow takes the resolved env, the same ruledeliver/pendingSinkalready follow.The
"Dev"case is a latent live bug, not tidying — the strongest single justification here, and worth stating plainly:api.IsKnownEnvlower-cases, but the oldsignedInEnvdid not. SotelemetryEnv("Dev")fell through to the unknown arm and labelled the record prod while the client dialled dev. That is the exact label-versus-destination split this telemetry work has been closing for three recuts, still open on a config one keystroke away from normal.sessionEnvreturnedcfg.CurrentEnvverbatim — the one env-resolving function in the CLI whose output was not normalised, whileResolveEnv,BaseURLandspoolEnvSlugall lower-case (and two of them trim). Invisible where the value only reachesBaseURL; load-bearing where it is compared, or where one consumer trims and another does not.api.BaseURLdoes not trim, so" dev "fell through to PROD.Three callers re-derived the session env instead of using
sessionEnv:cluster doctorbuilt its API client fromcfg.CurrentEnvraw → withcurrent_env: " dev "it probed prod with a dev token and reported "your session expired" for a session that is fine. The worst possible output from a diagnostic.auth status --checkcompared rawcfg.CurrentEnvagainst an already-normalised target → a"current_env": "Dev"config failed the probe for the very session it is signed in to, and the installer (whose contract is this exit code) would re-runloginagainst a working session.auth statusprintedcfg.CurrentEnvas its "backend" field whilerunAuthCheck— the machine-facing answer to the same question, 40 lines below in the same file — compared the resolved one. A status command that disagrees with the client is worse than no status command.doctor.backendHostrestatedapi.BaseURL's env→host table in a second switch. Now derived fromapi.BaseURL. Behaviour-identical —BaseURLalready lower-cases, soTrimSpaceis the only normalisation retained.A guard test (
TestNoNewEnvironmentResolutionSiteAppears) pins the closed set of files allowed to resolve an env from ambient state, with a reason per entry. It walks the module root, tokenises each file withgo/scanner(comments dropped, literals preserved), and audits the allowlist per entry — so a sanctioned file that stops resolving anything fails instead of quietly becoming a licence. One needle set, one matcher (matchesAnyNeedle), one allowlist, each exercised from both directions. This is the part that stops the next recut finding one more site.Explicitly NOT changed
api.BaseURL's unknown/empty → prod fail-open. It is shared with the installer's_backend_urland contradicted by client-runtime'scontroller.py(which refuses withSystemExit(1)), so it is a three-component decision tracked on backend#2171 — where I have added the evidence this audit produced. Site 6 (telemetryEnv) deliberately mirrors it and says so.Proof — each fix reverted, real red
Every fix was reverted individually against the new tests:
Precedence is covered explicitly:
TestSessionEnvFallsBackToClientEnvOnlyWhenUnsetpins that configcurrent_envbeats$CLIENT_ENVand that$CLIENT_ENVis consulted only whencurrent_envis absent — the offboard e2e fixture (test/integration/delete_e2e_test.go) writes"current_env": "prod", so a change letting$CLIENT_ENVwin would silently repoint that suite.TestAuthCheckStillRejectsARealEnvMismatchpins that normalising the comparison did not make it lenient about the mismatch it exists to catch.One fix has no red available and I am not pretending otherwise:
doctor.backendHostis behaviour-identical for every input (api.BaseURLalready lower-cases), so reverting it changes nothing observable. It is a de-duplication — the existingbackendHosttest table, whitespace case included, passes unchanged either way.What I ran
make check— green (vet, fullgo test ./...,fmt-check, file-budget, check-style, tool-pins)make lint— clean (errcheck, ineffassign, misspell, staticcheckall,-ST1005)make deadcode— clean. It reports a pre-existing stale-allowlist advisory (internal/doctor/doctor.go: Status.String); I confirmed it against a stashed pristinec1e918ftree, so it is not from this change and is not fixed here.go vet -tags integration ./test/...— the integration tree still compiles against the changed signature.Could not run: the k3d offboard e2e. It self-skips when a k3d cluster named
traceblocalready exists (delete_e2e_test.go:114, "refusing to delete a cluster this test didn't create") and this machine has one. Relying on CI for it — this is a skip, not a pass.One self-inflicted finding, recorded rather than quietly fixed
The first version of
TestClusterDoctorProbesTheSessionEnvstubbednewAPIClientbut notloadClusterFn. Past the session probe,cluster doctorloads the real kubeconfig and calls the realdoctor.Run, whosecheckBackendEgressprobesbackendHost("")— a liveGET https://api.tracebloc.io/. On a dev machine with a real k3d cluster (this one has one) that test therefore made a production request, unauthenticated but real. The tell is the run time: ~16s before, 0.01s onceloadClusterFnis stubbed.Fixed in
86e06c7and the reasoning is in the test as a comment, because the trap is not obvious from the call site —cluster doctorlooks like it stops when the session probe fails, and it does not. The other seven new tests are pure,httptest-backed, or file-only; none can reach the network.The guard took four rounds, and that is the story
Review found four independent holes in the guard — two string-literal evasions in my hand-rolled comment stripper (an
https://literal ate the rest of its line; a"/*.json"literal swallowed every needle below it), a scope hole (it walked 1 of the 17 packages underinternal/, i.e. it was blind exactly where the next site is most likely to land), and — the sharpest — an inert allowlist entry: my own consolidation stoppedtelemetry.gomatching any needle, so its entry checked nothing while silently pre-approving the next raw read in the one file whose double resolution this PR exists to remove.They are all one defect class, and it is the class this PR is about: a check that verifies the FORM of a thing rather than the property the form exists to guarantee. The allowlist checked existence when the property is still matches a needle. Worth naming, because it recurs — a gate that enforces enumeration but cannot tell what it enumerated; a thread count that reports
0on a PR the platform refuses to merge.As Lukas put it: four holes in one guard is not an argument against the guard, it is the guard being the highest-leverage thing in the PR. The fixes are in
f33a3d1anda670403; the production consolidation underneath was reviewed and taken as-is.The test-of-the-test — eight reproductions, all red
internal/cliinternal/cli/zz_probe.go resolves an environment from ambient state ("CurrentEnv")//inside"https://x/%s""/*.json"literal above the needleinternal/cluster(invisible before)internal/cluster/zz_probe.go … ("CLIENT_ENV")telemetry.go)… is allowlisted as a resolution site but resolves nothing any more — drop the entryresolutionSites["internal/cli/client.go"] is allowlisted with no reasonthe guard checked nothing: 5 sanctioned entries, 0 files matched/*)walking the module: …/zz_broken.go: 1 scan error(s) — cannot tell what this file readsEach was reverted and the guard is green with the tree clean.
One correction to my own claim, since it would otherwise read as stronger than it is:
go/scanneris lexical, so the error path covers unterminated literals and unterminated/*— the faults that would desynchronise boundary tracking — notfunc f( {, which scans clean and is reported as ordinary code. I checked; it passes. That is the right guarantee rather than a weak one (a file that tokenises correctly still yields correct needles, andgo buildis the check for whether it compiles), but the comment now says that instead of implying full syntax validation.Is that all of them?
Yes, and here is the falsifiable form of that claim. A repo-wide sweep of every non-test
.gofile with comments stripped (comments discuss these names constantly, so only code counts), forCurrentEnv,ResolveEnv(, andCLIENT_ENV, leaves exactly five files:internal/cli/telemetry.gohas dropped off that list entirely — post-change it names none of them in code, because it goes throughsessionEnv. Nothing outside Go derives a backend host either:grepfor_backend_url,CLIENT_ENV,dev-api,stg-api,api.tracebloc.ioacross*.sh/*.ps1/*.yml/*.yaml/*.tmplin this repo returns nothing (the cluster installer that owns_backend_urllives in another repo;internal/installeronly holds thehttps://tracebloc.io/i.shURL, which is env-independent by design).So the remaining reads are one primitive, one flag site, one session site, one storage layer, and one genuinely different question — no duplicates, and the guard test fails if a sixth appears.