Auth Refactor: Eliminate Static Fallbacks

take a look at this issue. assess how much of it still is relevant and then tell me how we're going to tackle it.
take a look at this issue. assess how much of it still is relevant and then tell me how we're going to tackle it. https://linear.app/entirehq/issue/COR-395/reposcopedtoken-bypasses-cluster-discovery-entire-repo-mirror-remove
Bottom line: Item 1 (the real bug) is 100% still live — repo_token.go even has a comment promising this card will fix it. Item 2 is already done — resolveAuthHostToken no longer exists. The fix is to make RepoScopedToken use the exact same stack git-remote-entire already uses: clusterdiscovery.ResolveContextForCluster + NewRefreshingLoginProvider + repocreds.
Relevance assessment
- Item 1 — fully relevant, unchanged.
RepoScopedToken(cmd/entire/cli/auth/repo_token.go:84-96) still readsLookupCurrentToken()and exchanges at a hard-codedapi.AuthBaseURL(). Its doc comment explicitly says "COR-395 reworks this." Callers are still exactly the two the issue names: mirror-create suspension probe (repo_mirror.go:153) andwaitForMirrorClone(repo_mirror_probe.go:182). - Item 2 — already done. Zero references to
resolveAuthHostTokenin code or tests; only a stale mention inrepo_token.go's comment. Nothing to do beyond deleting that comment line. - COR-393 not landed (~10
api.AuthBaseURL()sites remain), so this card sequences fine standalone and removes one of those sites as a side effect. - One issue assumption to discard: the "fix shape" suggests routing through
NewRefreshingResourceProvider— that doesn't fit. It derives audience from the resource origin with no repo slug orrepo:<action>scope. The "repo-scoped sibling" it hypothesizes already exists:internal/entireclient/repocreds, which is whatgit-remote-entireuses. The old wire-form-parity concern dissolves entirely — repocreds is the wire form we're matching, byte-for-byte (audience-only, noresourceparam).
Plan
- Rework the mint path — replace
RepoScopedToken's body (or replace the function with a smallrepoCredsForCluster(ctx, clusterHost)constructor) with thegit-remote-entirerecipe fromcmd/git-remote-entire/main.go:248-264:clusterdiscovery.ResolveContextForCluster(...)→ picks the eligible context for the cluster's discovered core, with the login-hint / ambiguous-context errors for free (multi-core acceptance criteria handled).auth.NewRefreshingLoginProvider(clusterCtx, ...)→ refresh-aware login JWT, closing the documented "stale login JWT" gap too.repocreds.New(clusterCtx.CoreURL, clusterBaseURL, loginProvider, httpClient).Token(ctx, repoSlug, action).
- Preserve the suspended-mirror sentinel —
explainSuspendedMirrorkeys onErrRepoTargetUnknown, currently mapped fromsts.ExchangeError{Code: "invalid_target"}. repocreds surfaces*httputil.OAuthErrorinstead, so re-map the sentinel from that error'sinvalid_targetcode. - Update the two callers — in
waitForMirrorClone, hold therepocreds.Cacheacross the loop and callInvalidate+Tokenon 401 (replaces the hand-rolledminReauthIntervalre-mint floor, or keep the floor as belt-and-braces). - Cleanup — drop the dead
resolveAuthHostTokencomment, therepoExchangeTransportForTestseam if repocreds'httpClientinjection covers it, and thests/api.AuthBaseURLimports fromrepo_token.go. - Tests — port the wire-form assertion test to the new path; add the multi-core scenario (active context on core A, cluster advertising core B → B's context wins; no eligible context → login hint, never a silent exchange at A); regression-run the existing mirror create/probe tests.
One thing to decide at implementation time: where the new function lives. repocreds is in internal/entireclient, which the cli package can import fine — but auth currently owns RepoScopedToken. Cleanest is probably keeping a thin wrapper in auth so the callers barely change. Want me to start on it?
yes, start the fix. whenever your diff touches comments, make sure they're succinct, parsimonious, and updated for correctness.
Committed 821be3e98cf — RepoScopedToken now resolves the cluster's trusted core via /.well-known discovery instead of exchanging at the hard-coded api.AuthBaseURL(); lint and all 6222 unit tests pass, integration left to CI.
What changed:
auth/repo_token.go(rewritten): resolves the login context withclusterdiscovery.ResolveContextForCluster(active-if-eligible → sole → explicit-choice error, login hint when none), then mints throughNewRefreshingLoginProvider+repocreds— the exact stack git-remote-entire uses. Signature changed to takeclusterHost(both callers were prependinghttps://anyway). Bonus fixes for free: expired login JWTs now silently re-mint from the refresh token, and the wire form is byte-identical to git-remote-entire (client_idvia Basic auth, audience-only, noresource).httputil.OAuthErrorgainsCode: parsed best-effort from the RFC 6749errorbody field, so the suspended-mirror sentinelErrRepoTargetUnknownsurvives the move fromsts.ExchangeErrorto the repocreds error chain.explainSuspendedMirrorkeeps working unchanged.- Callers (
repo_mirror.go,repo_mirror_probe.go): one-line signature updates; the wait-loop's 401-driven re-mint logic is untouched. - Tests: wire-form test now asserts the exchange hits the resolved context's core and that
client_idtravels as Basic auth; invalid_target sentinel test ported; new test that discovery errors surface verbatim (never a wrong-core exchange); newOAuthError.Codeparse test. Test seams follow thedata_api.gopattern (sharedresolveContextFunctype, sandboxedENTIRE_CONFIG_DIR). - Item 2 of the issue (
resolveAuthHostToken) was already gone — the rewrite removed the last stale comment mentioning it.
One behavior change worth noting in the PR: repo mirror now errors with a login hint when no local context matches the cluster's core (previously it would silently exchange the active context's token and fail server-side) — that's the acceptance criterion. Not pushed, per your rules.
/simplify → 4 cleanup agents in parallel → apply the fixes
You are improving the quality of the changed code, not hunting for bugs. Review
it for reuse, simplification, efficiency, and altitude issues, then fix what you
find. Do not look for correctness bugs — that is what /code-review is for.
Phase 0 — Gather the diff
Run git diff @{upstream}...HEAD (or git diff main...HEAD / git diff HEAD~1
if there's no upstream) to get the unified diff under review. If there are
uncommitted changes, or the range diff is empty, also run git diff HEAD and
include the working-tree changes in scope — the review often runs before the
commit. If a PR number, branch name, or file path was passed as an argument,
review that target instead. Treat this diff as the review scope.
Phase 1 — Review (4 cleanup agents in parallel)
Launch 4 independent review agents via the Agent tool, all in a
single message so they run concurrently. Pass each agent the diff and one of
the four angles below. Each returns its findings with file, line, a
one-line summary, and the concrete cost (what is duplicated, wasted, or
harder to maintain).
Reuse
Flag new code that re-implements something the codebase already has — Grep shared/utility modules and files adjacent to the change, and name the existing helper to call instead.
Simplification
Flag unnecessary complexity the diff adds: redundant or derivable state, copy-paste with slight variation, deep nesting, dead code left behind. Name the simpler form that does the same job.
Efficiency
Flag wasted work the diff introduces: redundant computation or repeated I/O, independent operations run sequentially, blocking work added to startup or hot paths. Also flag long-lived objects built from closures or captured environments — they keep the entire enclosing scope alive for the object's lifetime (a memory leak when that scope holds large values); prefer a class/struct that copies only the fields it needs. Name the cheaper alternative.
Altitude
Check that each change is implemented at the right depth, not as a fragile bandaid. Special cases layered on shared infrastructure are a sign the fix isn't deep enough — prefer generalizing the underlying mechanism over adding special cases.
Phase 2 — Apply the fixes
Wait for all four agents to complete, dedup findings that point at the same line or mechanism, and fix each remaining one directly. Skip any finding whose fix would change intended behavior, require changes well outside the reviewed diff, or that you judge to be a false positive — note the skip rather than arguing with it. Finish with a brief summary of what was fixed and what was skipped (or confirm the code was already clean).
Committed e771cac8f08 — two cleanups applied, two findings skipped as not worth it, altitude review came back fully clean.
Fixed:
- Removed the test-only production setter
setResolveContextForClusterForTestfromrepo_token.go— in-package tests now swap the seam var directly via astubResolveContextForClustertest helper, matching thestubResolveContextForAPIidiom indata_api_test.go(reuse finding). - Deduped the sandbox setup (
ENTIRE_CONFIG_DIR+ file token backend) that appeared twice inrepo_token_test.gointo onesandboxRepoTokenStoreshelper (simplification finding).
Skipped:
- Per-call
MigrateLegacyLoginContext+http.Clientallocation (efficiency finding): the wait loop re-mints at most every 30s (minReauthInterval), so worst case is ~60 keyring/file reads over a 30-minute wait — negligible, and hoisting it would push migration responsibility onto callers, breaking symmetry withResolveDataAPITokenwhich migrates per-call the same way. - The altitude agent confirmed the layer choices (Code parse in
httputil, sentinel mapping inrepo_token.go, separate API/cluster seams sharing one type) are all at the right depth — no changes.
Build, auth tests, fmt, and lint all pass after the cleanup.
Run an adversarial Codex review through the shared plugin runtime. Position it as a challenge review that questions the chosen implementation, design choices, tradeoffs, and assumptions. It is not just a stricter pass over implementation defects.
Raw slash-command arguments: ``
Core constraint:
- This command is review-only.
- Do not fix issues, apply patches, or suggest that you are about to make changes.
- Your only job is to run the review and return Codex's output verbatim to the user.
- Keep the framing focused on whether the current approach is the right one, what assumptions it depends on, and where the design could fail under real-world conditions.
Execution mode rules:
- If the raw arguments include
--wait, do not ask. Run in the foreground. - If the raw arguments include
--background, do not ask. Run in a Claude background task. - Otherwise, estimate the review size before asking:
- For working-tree review, start with
git status --short --untracked-files=all. - For working-tree review, also inspect both
git diff --shortstat --cachedandgit diff --shortstat. - For base-branch review, use
git diff --shortstat <base>...HEAD. - Treat untracked files or directories as reviewable work for auto or working-tree review even when
git diff --shortstatis empty. - Only conclude there is nothing to review when the relevant scope is actually empty.
- Recommend waiting only when the scoped review is clearly tiny, roughly 1-2 files total and no sign of a broader directory-sized change.
- In every other case, including unclear size, recommend background.
- When in doubt, run the review instead of declaring that there is nothing to review.
- For working-tree review, start with
- Then use
AskUserQuestionexactly once with two options, putting the recommended option first and suffixing its label with(Recommended):Wait for resultsRun in background
Argument handling:
- Preserve the user's arguments exactly.
- Do not strip
--waitor--backgroundyourself. - Do not weaken the adversarial framing or rewrite the user's focus text.
- The companion script parses
--waitand--background, but Claude Code'sBash(..., run_in_background: true)is what actually detaches the run. /codex:adversarial-reviewuses the same review target selection as/codex:review.- It supports working-tree review, branch review, and
--base <ref>. - It does not support
--scope stagedor--scope unstaged. - Unlike
/codex:review, it can still take extra focus text after the flags.
Foreground flow:
- Run:
- Return the command stdout verbatim, exactly as-is.
- Do not paraphrase, summarize, or add commentary before or after it.
- Do not fix any issues mentioned in the review output.
Background flow:
- Launch the review with
Bashin the background:
- Do not call
BashOutputor wait for completion in this turn. - After launching the command, tell the user: "Codex adversarial review started in the background. Check
/codex:statusfor progress."
Here is Codex's adversarial review output, verbatim:
Codex Adversarial Review
Target: branch diff against main Verdict: needs-attention
No-ship: the new repo-token path makes repo mirror create waits exceed the caller's timeout budget and adds a repeated cluster-discovery dependency that can abort long clone waits after auth already succeeded once.
Findings:
- [high]
--wait-timeoutno longer bounds the initial authorization phase (cmd/entire/cli/repo_mirror_probe.go:181-193)waitForMirrorClonestill mints the first repo token before it appliescontext.WithTimeout. Before this change that was a single exchange. Nowauth.RepoScopedTokenperforms extra network work first: cluster discovery, possible login refresh, then the repo-token exchange. The result is that a user who asks for a small--wait-timeoutcan still block for tens of seconds before the timeout even starts, and the command can appear hung in the auth phase instead of respecting the requested clone-wait budget. Recommendation: Apply the timeout context before the firstmintToken()call, or pass a dedicated bounded context intoRepoScopedTokenso discovery/refresh/exchange are all covered by the user's wait timeout. Add a test with a very small wait timeout and a slow auth path. - [medium] Token re-mint during clone wait now re-runs cluster discovery on every 401 (cmd/entire/cli/auth/repo_token.go:70-94)
RepoScopedTokendoes a fresh context resolution on every call and explicitly does not cache anything.waitForMirrorClonecalls it again whenever the probe token expires. That means a long-running clone wait is now coupled to repeated/.well-known/entire-cluster.jsonresolution and context selection, even after one token has already been minted successfully. A transient discovery outage, stale/missing cache, or config/cache-dir issue later in the wait can now abort the clone wait even though the mirror and the original auth path were already working. Recommendation: Split one-time cluster/context resolution from per-token exchange. Resolve the cluster context once before entering the wait loop and reuse a login provider / repo-cred cache for re-mints, instead of re-running discovery on every token refresh.
Next steps:
- Move the wait timeout to cover the first token mint and add regression coverage for slow discovery/refresh.
- Refactor the mirror-wait auth path so reauthorization reuses a previously resolved cluster context instead of re-running discovery each time.
Both Codex findings are fixed in separate commits — plus one extra commit repairing an unintended edit I found along the way.
2ebd0d8ef59(high finding):waitForMirrorClonenow arms the--wait-timeoutdeadline before the authorization phase, so cluster discovery + login refresh + first exchange all consume the user's wait budget instead of running unbounded before it. Regression testTestWaitForMirrorClone_TimeoutBoundsAuthorizationpins a hung auth path against a 50ms timeout.f2098377390(medium finding): newauth.RepoTokenSourcesplits one-time resolution from per-token exchange — discovery and context selection run once at construction;Token/Invalidatesit on arepocredscache so 401-driven re-mints only re-exchange (refreshing the login JWT if needed). The wait loop holds one source and invalidates before re-minting;RepoScopedTokenremains as the one-shot wrapper for the single suspension probe. TestTestRepoTokenSource_ReMintSkipsDiscoveryasserts discovery runs exactly once across an initial mint + invalidate + re-mint.30318e52975(incidental): while running lint I discovered the earlier/simplifycommit had swept in a stray edit toexplain.go(viagit add -A) that deleted a//nolint:exhaustivedirective and broke lint. I never made that edit intentionally — restored the file to main's version in its own commit.
Format, lint, and all 6224 unit tests pass. Nothing pushed.
Run an adversarial Codex review through the shared plugin runtime. Position it as a challenge review that questions the chosen implementation, design choices, tradeoffs, and assumptions. It is not just a stricter pass over implementation defects.
Raw slash-command arguments: ``
Core constraint:
- This command is review-only.
- Do not fix issues, apply patches, or suggest that you are about to make changes.
- Your only job is to run the review and return Codex's output verbatim to the user.
- Keep the framing focused on whether the current approach is the right one, what assumptions it depends on, and where the design could fail under real-world conditions.
Execution mode rules:
- If the raw arguments include
--wait, do not ask. Run in the foreground. - If the raw arguments include
--background, do not ask. Run in a Claude background task. - Otherwise, estimate the review size before asking:
- For working-tree review, start with
git status --short --untracked-files=all. - For working-tree review, also inspect both
git diff --shortstat --cachedandgit diff --shortstat. - For base-branch review, use
git diff --shortstat <base>...HEAD. - Treat untracked files or directories as reviewable work for auto or working-tree review even when
git diff --shortstatis empty. - Only conclude there is nothing to review when the relevant scope is actually empty.
- Recommend waiting only when the scoped review is clearly tiny, roughly 1-2 files total and no sign of a broader directory-sized change.
- In every other case, including unclear size, recommend background.
- When in doubt, run the review instead of declaring that there is nothing to review.
- For working-tree review, start with
- Then use
AskUserQuestionexactly once with two options, putting the recommended option first and suffixing its label with(Recommended):Wait for resultsRun in background
Argument handling:
- Preserve the user's arguments exactly.
- Do not strip
--waitor--backgroundyourself. - Do not weaken the adversarial framing or rewrite the user's focus text.
- The companion script parses
--waitand--background, but Claude Code'sBash(..., run_in_background: true)is what actually detaches the run. /codex:adversarial-reviewuses the same review target selection as/codex:review.- It supports working-tree review, branch review, and
--base <ref>. - It does not support
--scope stagedor--scope unstaged. - Unlike
/codex:review, it can still take extra focus text after the flags.
Foreground flow:
- Run:
- Return the command stdout verbatim, exactly as-is.
- Do not paraphrase, summarize, or add commentary before or after it.
- Do not fix any issues mentioned in the review output.
Background flow:
- Launch the review with
Bashin the background:
- Do not call
BashOutputor wait for completion in this turn. - After launching the command, tell the user: "Codex adversarial review started in the background. Check
/codex:statusfor progress."
Here is Codex's adversarial review output, verbatim:
Codex Adversarial Review
Target: branch diff against main Verdict: needs-attention
No-ship: this auth rewrite introduces new hard failures on paths that previously worked, especially around repo mirror probing during discovery or local-state degradation.
Findings:
- [high] Repo-scoped token minting now hard-depends on cluster discovery with no fallback (cmd/entire/cli/auth/repo_token.go:80-83)
NewRepoTokenSourcenow always resolves the cluster throughResolveContextForClusterand immediately returns any discovery error. That makesrepo mirror create's suspension probe and clone-wait auth fail on a cold cache whenever/.well-known/entire-cluster.jsonis briefly unavailable, even though the old implementation could still exchange the existing login token directly. The data-API path in this same package explicitly falls back on discovery failure to avoid exactly this regression; this path does not. Impact: a transient discovery outage becomes a user-visible auth outage for mirror creation/readiness checks. Recommendation: Add a fallback for discovery-unavailable/unreachable cases that preserves the pre-change behavior, and cover it with a test that mints successfully when discovery fails but the login token + exchange path still work. - [medium] Corrupt or unreadable
contexts.jsonnow blocks repo token exchange instead of degrading to the legacy token store (cmd/entire/cli/auth/repo_token.go:76-83) This constructor now routes all repo token minting throughResolveContextForCluster, which loadscontexts.jsonand bubbles any load error back to the caller. Before this change, the repo-token path read credentials throughLookupCurrentToken;CurrentContextToken()intentionally swallowedcontexts.Loadfailures andContextStorefell back to the legacy keyring entry. The new behavior means a malformed or permission-brokencontexts.jsoncan break mirror probing entirely even when a valid legacy login token is still present. Impact: local state corruption turns into a hard auth outage with no recovery path short of manual file repair. Recommendation: Treat local context-load failures like the existingContextStoredoes: fall back to the legacy token path, or at minimum special-caseload contextsfailures so a usable legacy login still works.
Next steps:
- Add regression tests for
RepoScopedTokenwhen cluster discovery is unavailable but static exchange would succeed. - Add regression tests for malformed/unreadable
contexts.jsonwith a valid legacy token present.
Codex's findings are dismissed as intended behavior — no code changes. The branch stands as committed: the COR-395 discovery rework, the two adversarial-review fixes (timeout ordering, RepoTokenSource split), and the explain.go restore. I've also saved the sunset decision to memory so future auth work doesn't reintroduce legacy-store fallbacks or discovery bypasses.
Ready for you to push whenever you like — CI will cover the integration suite.
I think we already have https://linear.app/entirehq/issue/COR-393/sunset-entire-auth-base-url-discovery-only-auth-resolution-entire, is that the right place to save these pointers?
Yes — COR-393 is the right place. It already owns everything these pointers touch: the §4 env-read audit explicitly lists repo_token.go, the "Key decisions" section records the no-fallback and strand-legacy-users calls, and the MigrateLegacyLoginContext removal is in scope there.
I drafted a comment but the harness blocked posting it without your explicit go-ahead (a question isn't consent to publish). Here's what I'd post on COR-393:
Pointers from the COR-395 rework (branch
woolly-herding-scroll), relevant to the audit list here:
repo_token.gono longer readsapi.AuthBaseURL()—RepoScopedToken/NewRepoTokenSourcenow resolve viaclusterdiscovery.ResolveContextForCluster+NewRefreshingLoginProvider+repocreds. One site off the §4 audit list.NewRepoTokenSourcecallsMigrateLegacyLoginContext()(best-effort bridge, same asResolveDataAPITokenand git-remote-entire). When this card removes the migration machinery, that call site goes too.- Two adversarial-review findings against the COR-395 rework were dismissed on the strength of this card's decisions — record them as confirmed-intentional so they don't resurface:
- "repo-token minting hard-depends on cluster discovery, no static fallback": intentional. Discovery has disk-cache + stale-on-failure; a cold-cache outage that fails the probe would equally fail the subsequent
git clone. The data-API static fallback exists only for deployments predating the well-known endpoint, which never applied to clusters.- "corrupt
contexts.jsonno longer degrades to the legacy keyring token": intentional. contexts.json is the source of truth; failing loudly beats silently authenticating with a stale legacy identity.
Say the word and I'll post it.
Posted to COR-393. The comment records the repo_token.go audit-site progress, the MigrateLegacyLoginContext call site that goes away with that card, and the two confirmed-intentional review dismissals so they don't resurface when the sunset work lands.
alright, don't create a PR yet, but let's draft a description for a PR and we'll panelbeat it.
Here's a draft, written to your "less is more" preference:
Title: repo mirror: mint repo-scoped tokens via cluster discovery (COR-395)
Closes COR-395.
entire repo mirror create (suspension probe + clone wait) minted repo-scoped tokens by exchanging the active context's login JWT at the hard-coded api.AuthBaseURL() core — on multi-core that ships the token to the wrong core's STS. Now it resolves the cluster's trusted core from /.well-known/entire-cluster.json and picks the matching login context, the same path (and wire form) git-remote-entire already uses.
User impact
- Multi-core: active context on core A, mirror on a cluster fronted by core B → B's context is used. No eligible login → actionable "log in to one of: …" error instead of a wrong-core STS failure.
- An expired login JWT is now silently refreshed from the stored refresh token mid-wait (previously a hard failure).
--wait-timeoutnow bounds the authorization phase too, not just the probe loop.- Long clone waits resolve discovery/context once up front; 401 re-mints only re-exchange, so a discovery hiccup mid-wait can't abort an authorized wait.
Intentional, per the COR-393 sunset direction (raised by adversarial review, dismissed):
- No static fallback when discovery is unreachable on a cold cache — a probe that "succeeded" anyway would just defer the same failure to
git clone. - A corrupt
contexts.jsonerrors loudly instead of degrading to the legacy keyring token.
Plumbing: httputil.OAuthError gains a parsed RFC 6749 Code so the suspended-mirror sentinel survives the new exchange path. COR-395's second item (resolveAuthHostToken removal) had already landed; the last stale comment reference is gone.
🤖 Generated with Claude Code
Things you might want to panelbeat: whether to mention the RepoTokenSource API by name (left out — internal), and whether the COR-393 paragraph is worth two bullets or one line pointing at the Linear comment.
the two bullets about "dismissed issues" reads a bit AI-sloppy. Let's be more concrete about what machinery we are still leaving in place, to be removed in a followup PR.
Reworked draft:
Title: repo mirror: mint repo-scoped tokens via cluster discovery (COR-395)
Closes COR-395.
entire repo mirror create (suspension probe + clone wait) minted repo-scoped tokens by exchanging the active context's login JWT at the hard-coded api.AuthBaseURL() core — on multi-core that ships the token to the wrong core's STS. Now it resolves the cluster's trusted core from /.well-known/entire-cluster.json and picks the matching login context, the same path (and wire form) git-remote-entire already uses.
User impact
- Multi-core: active context on core A, mirror on a cluster fronted by core B → B's context is used. No eligible login → actionable "log in to one of: …" error instead of a wrong-core STS failure.
- An expired login JWT is now silently refreshed from the stored refresh token mid-wait (previously a hard failure).
--wait-timeoutnow bounds the authorization phase too, not just the probe loop.- Long clone waits resolve discovery/context once up front; 401 re-mints only re-exchange, so a discovery hiccup mid-wait can't abort an authorized wait.
New hard requirements (no fallback, consistent with COR-393): minting needs cluster discovery reachable on a cold cache, and a readable contexts.json. The old path could fall back to exchanging the legacy keyring token at the default core — that fallback is gone here and not coming back.
Transitional machinery left in place, to be removed in the COR-393 follow-up:
- the
MigrateLegacyLoginContext()best-effort bridge inNewRepoTokenSource(goes when the migration machinery is deleted), - the remaining
api.AuthBaseURL()env-read sites elsewhere in the auth package — this PR only takesrepo_token.gooff that list.
Plumbing: httputil.OAuthError gains a parsed RFC 6749 Code so the suspended-mirror sentinel survives the new exchange path. COR-395's second item (resolveAuthHostToken removal) had already landed; the last stale comment reference is gone.
🤖 Generated with Claude Code
The "dismissed issues" framing is gone; in its place are the concrete invariants (what no longer works, deliberately) and the exact leftovers COR-393 will sweep up.
PR opened: https://github.com/entireio/cli/pull/1402 — branch pushed to origin, description as panelbeaten. CI will run the integration suite from here.
if we wait very long for mirror creation on the server side, say, 1h in a pathological case, will we refresh tokens for the status requests during that time?
Yes — both token layers now self-refresh for the whole wait; the only hard stop is a dead refresh token.
The chain during a 1h wait (assuming --wait-timeout 1h, default is 30m):
- Repo-scoped token (TTL ~minutes): the probe loop keeps using it until the data plane returns 401, then
Invalidate+Tokenre-exchanges, floored at one re-mint per 30s (minReauthInterval). In between, a 401'd tick just retries next tick — nothing is lost. This worked before the PR too. - Login JWT (the exchange's subject token, TTL ~1h): this is the part that's new. Each re-mint calls the source's
NewRefreshingLoginProvider, which checks the stored expiry and, when stale, re-mints from the persisted refresh token via the tokenmanager — serialized across processes with a file lock, rotated refresh token persisted refresh-slot-first. Pre-PR, the loop read the stored access token verbatim, so a login JWT expiring mid-wait was a hardre-authorize clone probefailure. Now it's invisible. - No network on the happy path: the refresh check is local (stored expiry); it only dials the core when the JWT is actually stale, so the ~120 re-mint opportunities in an hour cost nothing extra.
The one way a pathological wait still dies on auth: the refresh grant itself is rejected (revoked/expired refresh-token family) — the provider then surfaces "login session for X expired; run entire login" and the wait aborts. That's correct behavior, not a gap: there's no credential left to continue with.
One caveat to be aware of: cluster discovery and context selection happen once at source construction, so if the cluster's trusted-core set were rotated mid-wait (genuinely pathological), the loop keeps exchanging at the originally resolved core until restarted. I'd call that acceptable for a 1h window.
apart from the nitpicks to the comment wording which i am happy to apply, do any of the review comments such as https://github.com/entireio/cli/pull/1402#discussion_r3388735450 raise points we should address?
No — all three are wording-only; nothing behavioral to address. The full set is the three Copilot inline comments (plus its overview); there are no human reviews yet.
On the one you linked (r3388735450), the only quasi-substantive angle is its parenthetical "or broaden the check if non-200 2xx responses should be accepted" — and the right resolution is the comment fix, not the code change. RFC 6749 defines token-endpoint success as 200 with a JSON body; a 201/204 from /oauth/token would be non-conformant, and accepting one would just push the failure into the body-decode path with a worse error message. Strict-200 is correct as-is. (For the record, the "non-2xx" wording in oauth.go predates this PR — I carried it over when adding the Code sentence; the matching wording in the new test comment is mine.)
The other two are the same class: "non-2xx" → "non-200" in the test comment, and "slice of auth.RepoTokenSource" reading as a Go slice — "subset" is the better word there, fair nit.
Since you're happy applying the wording fixes yourself, I'll leave them to you — or say the word and I'll push the three one-liners.
Pushed f11b13864bb — all three Copilot nits addressed: "non-2xx" → "non-200" in the OAuthError doc and the test comment (with a one-line RFC 6749 rationale for why strict-200 is the correct check, settling the "or broaden it?" parenthetical), and "slice of" → "subset of" on the repoTokenSource interface. Lint and build clean.
wait did we really fix https://github.com/entireio/cli/pull/1402#discussion_r3388735450
Yes, genuinely fixed. The comment targeted the OAuthError doc in oauth.go (the "non-2xx" claim vs. the != http.StatusOK implementation), and commit f11b13864bb changed exactly that line — it now reads "responds with a non-200 status" — plus the second occurrence in the PostOAuthToken doc, where I also added the rationale ("RFC 6749 defines token-endpoint success as 200 only") that settles Copilot's "or broaden the check" parenthetical in favor of keeping strict-200.
The grep above shows the only remaining "non-2xx" wording is in cmd/entire/cli/api/client.go — a different, pre-existing helper (CheckResponse), and there the wording is accurate: it really does accept the whole 2xx range (>= 200 && < 300), so there's nothing to fix and it's outside this PR anyway.
hey i think this ticket https://linear.app/entirehq/issue/COR-316/entire-cli-adopt-the-refresh-tier describes what we're working on?
Yes — COR-316 is the umbrella for this work, and as far as I can tell it's essentially complete once PR #1402 merges. Its two bullets shipped in earlier slices, and our PR closed the last gap:
- "Request
offline_accessat login; persist the refresh token" — done: login requestsScope: "cli offline_access"(auth/client.go:72) and the refresh token is persisted in its own keyring slot (RefreshServicehandling instore.go/refresh.go). - "Wire the
refresh_tokengrant via auth-go's tokenmanager" — done by COR-389 / PR #1377 for control-plane and data-API paths (NewRefreshingLoginProvider/NewRefreshingResourceProviderovertokenmanager), plus git-remote-entire. - The one remaining path that still read the stored access token verbatim and hard-failed at JWT expiry was
RepoScopedToken— exactly what PR #1402 / COR-395 moved onto the refresh tier.
So the relationship is: COR-316 = "adopt the refresh tier everywhere", COR-395 = the last hold-out path, #1402 = the closing PR. I notice the ticket just flipped to In Progress (presumably you) — once #1402 merges, I'd say COR-316 can close alongside COR-395.
Want me to attach PR #1402 to COR-316 and leave a one-line comment noting it's the final refresh-tier adoption piece? (Not doing it unprompted — same external-publish rule as before.)
yes i'd like a point by point breakdown of what work was covered in which PRs, as a comment on that ticket.
Posted to COR-316. The breakdown maps each ticket bullet to its PR, verified against git history rather than memory:
- #1337 (Jun 3) —
offline_accessat login, refresh-token persistence (refresh-slot-first ordering, logout scrub), andNewRefreshingLoginProviderfor git-remote-entire. - #1367 (COR-389) — control-plane commands onto the per-context refreshing resolver.
- #1377 (COR-389) — data-API commands onto
ResolveDataAPIToken+NewRefreshingResourceProviderwith/.well-known/entire-api.jsondiscovery. - #1402 (COR-395, open) — repo-scoped tokens, the last verbatim-read path.
The comment also notes the ticket can close with COR-395 once #1402 merges, and points residual cleanup (TokenForResource fallbacks, ENTIRE_AUTH_BASE_URL) at COR-393.
ah but wait, i see there are two other tickets marked as blocking COR-316, namely: COR-330 and COR-318. Take a look and tell me if that's perhaps also already been worked on.
Short answer: no — neither has been done, and honestly neither looks like a true blocker of COR-316's code work.
- COR-330 (security review sign-off): this is a process gate, not code — a structured review of the three-tier session architecture (cookie attributes, CSRF/Origin,
fid-liveness, rotation + reuse detection, fail-closed introspection, rate limiting). Nothing we've shipped covers it, and I wouldn't count the Codex adversarial passes on #1402 as it — they reviewed one PR's diff, not the architecture. Still genuinely open. - COR-318 (retire KV sessions /
ent_tokens / CSRF): lives in the entire.io front-end repo, not this one, and is explicitly "cleanup after the browser is fully on bearer" — blocked by COR-315. From the CLI side, the adjacent prep is done (the CLI never touches the legacyent_token endpoint and uses the entire-core device flow, so deletingroutes/cli-auth.tswon't break it), but the actual deletions are front-end work I can't verify from here and almost certainly untouched while COR-315 is open.
One thing worth a second look on your end: the blocking direction reads backwards to me. COR-316's implementation is complete (per the breakdown comment — it never needed either ticket), and COR-318 is downstream cleanup that logically can't gate CLI refresh-tier adoption. What these two actually gate is the go-live cutover — COR-330 even says "gate on the browser + CLI cutover". If COR-316 is being held open as a proxy for "cutover done", the relations make sense; if it's meant to track the CLI code, I'd drop both blockers and let COR-316 close with COR-395/#1402.
ok, sounds fine, i'll think about that later. let's make a new spinoff branch, and think about how to tackle https://linear.app/entirehq/issue/COR-393/sunset-entire-auth-base-url-discovery-only-auth-resolution-entire. i expect a few design decisions and back and forth before we start work.
I have the full inventory. Here's the lay of the land and the design decisions I think we need to settle before writing code.
Current state (stacked on #1402)
The remaining ENTIRE_AUTH_BASE_URL surface groups into five clusters:
| Cluster | Sites | Fate per ticket |
|---|---|---|
| Login destination | auth/client.go:59 (device flow issuer) | → entire login --server |
| Status/logout fallback | auth.go:56,158, logout.go:72-93 (resolveStatusTarget's fallbackBaseURL + legacy keyring probe) | → "run entire login" |
| Control-plane fallback | control_plane.go:63-70 (no-context → static target + TokenForResource) | → error |
| Legacy token plumbing | store.go:157 (LookupCurrentToken), contexts.go:164 (MigrateLegacyLoginContext) | → delete |
| Provider/host detection | provider.go via IsSplitHost(), api_client.go:27 validation, exchange.go:95 singleton | → constants / delete |
Good news: TokenForResource has exactly three callers left (control-plane fallback, data-API discovery-unavailable fallback, data-API unparseable-host fallback), and LookupCurrentToken's only production callers are inside the legacy fallback chain itself. The whole tower comes down together.
Design decisions for you
1. Cold-turkey on the env var, or one deprecation release? The ticket says "no longer read anywhere." Cold removal is cleaner, and the var is undocumented/internal-only — but anyone with it exported gets silent behavior change (login goes to the default core instead of their custom one). Middle ground: keep one release where a set ENTIRE_AUTH_BASE_URL is detected and hard-errors with "use entire login --server". My recommendation: the hard-error transition — it's ~5 lines, catches internal devs' muscle memory, and still satisfies "never silently used."
2. auth status with no login at all: error or informational? The ticket says control-plane commands error. But status is diagnostic — my recommendation: print "Not logged in. Run entire login." and exit 0 for status, while logout no-ops successfully and org/repo/project/grant hard-error non-zero. Erroring on "tell me my status" feels wrong.
3. How dead is v1? effectiveProviderVersion still resolves to v1 when auth and data origins coincide, plus the ENTIRE_AUTH_PROVIDER_VERSION override. "All-in on contexts" implies deleting the v1 provider, the env var, IsSplitHost, and collapsing CurrentProvider() to constants (entire-cli, the STS/token paths). Question: is there any deployment — dev loopback cores included — still serving the v1 (non-OIDC) surface that the CLI must keep talking to? If not, I delete the whole routing table.
4. PR shape. Options: (a) one PR stacked on #1402 (it's all one demolition, but touches ~15 files + big test churn), or (b) two PRs: first login --server + env-var hard-error (additive, low risk), then the fallback/TokenForResource/migration demolition. My recommendation: (b) — the first is shippable immediately, the second is where review attention belongs.
5. Small but user-visible: with MigrateLegacyLoginContext gone, pre-contexts logins (and entiredb CLI logins predating contexts.json) hit the "no eligible context" path. The ticket accepts stranding them with a login hint — confirming that includes git-remote-entire's cold-boot migration call too (same machinery, same deletion).
Where do you want to push back first?
-
yes i like that - if os.LookupEnv returns ok, we bleat.
-
yes, auth status informational. i like the recommendations.
-
i think v1 is dead as a doornail and unsupported - slated for complete removal. i just want to understand completely what we're talking about, because there's a lot of jargon there.
-
okay let's do (b).
-
i'm happy to strand old sessions. but when you say "stranding them with a login hint" but presumably this path gives user a friendly "please entire login" message?
All locked in: env-var bleat (1), informational auth status (2), PR split (4). Here's the jargon decoded for 3, and the answer to 5.
3. What "v1/v2 provider" actually means
A Provider is nothing more than four strings: an OAuth client id plus three URL paths on the login server — where to start a device login, where to poll/refresh tokens, where to do token exchange (auth/provider.go:24-48).
- v1 is the original world where entire.io was simultaneously the website, the data API, and the login server. Homegrown endpoint paths (
/oauth/device/code), and no token-exchange path at all — when auth and data live on the same host, the login token is accepted directly, so nothing ever needed exchanging. - v2 is the dedicated login servers (us.auth.entire.io, …) running a standard OIDC stack: standard paths (
/device_authorization,/oauth/token), with/oauth/tokendoubling as the RFC 8693 exchange endpoint.
The "heuristic" (IsSplitHost): the CLI guesses which world it's in by comparing the auth origin to the data origin — different hosts → assume v2; same host → assume v1; ENTIRE_AUTH_PROVIDER_VERSION forces it. Since the default config has always been split-host, everyone has been on v2 for a while; v1 only triggers if someone deliberately points both env vars at one host. Loopback dev cores run entire-core's OIDC surface, so they're v2 too.
"Complete removal" therefore means: delete the v1 entry, the ENTIRE_AUTH_PROVIDER_VERSION env var, IsSplitHost(), the routing table, the process-wide sync.Once singleton and its SetProviderForTest escape hatch — CurrentProvider() collapses to a package-level constant. A satisfying amount of machinery for four strings.
5. Yes — the friendly message already exists, with one wrinkle
A stranded legacy session has no context, so it lands in clusterdiscovery.renderLoginHint, which today prints:
The wrinkle: notice the hint currently ignores the core list (renderLoginHint(subject string, _ []string)) — that's deliberate, from your recent commit d471460e0ca "squash away info about different login servers until ready". This card is what makes it ready: once --server exists, PR 2 un-squashes the hint into the ticket's desired UX ("accepts logins from: … entire login --server <url>"), keeping the plain entire login form for the zero-contexts case per the acceptance note. Control-plane/status (no discovery involved) get the plain "run entire login" wording.
PR 1 scope, then
entire login --server <url> (default = new DefaultAuthBaseURL constant, validate https-or-loopback), plus the bleat: os.LookupEnv(ENTIRE_AUTH_BASE_URL) set → hard error naming entire login --server. Side effect worth knowing: once the bleat exists, every remaining api.AuthBaseURL() call can only ever return the default — which is exactly what makes PR 2's mechanical collapse safe. Tests that t.Setenv the var to exercise commands will need updating in PR 1, not PR 2.
Want me to start on PR 1?
ok cool
-
yes all-in on v2, v1 is dead and gone, remove all traces from cli
-
nope okay, for now we will NOT embellish that login hint any further - we haven't officially launched multi-region auth, so for now users will only interact with the default login server. (and --server flag should be sure to default to https://us.auth.entire.io )
Committed 6e40fcaa432. On your question: no — the Go standard library exports no constants for URL schemes. net/url treats URL.Scheme as a plain string with no named values, and net/http only exports method constants (http.MethodGet etc.); internally the stdlib itself compares against bare "http"/"https" literals. So when goconst complained about the third "https" occurrence in the cli package, there was nothing in stdlib to reach for — I added the two tiny package consts, which is also exactly what api/base_url.go already does for the same reason (its schemeHTTP/schemeHTTPS are unexported, so the cli package can't reuse them without promoting them to exported API, which felt like more ceremony than two one-line consts deserve).
If you'd rather export them from api and use one shared pair, that's a 5-minute change — but duplicating two string constants across packages seemed the smaller evil.
State of PR 1: --server flag with strict origin validation, auth.NewClient(server, …), and the env-var bleat in main.go are in, with tests (TestParseLoginServer, TestRejectRemovedAuthEnv). Lint and all 6241 unit tests pass. Next when you're ready: push + open the PR, then start the PR 2 demolition (fallback removal, TokenForResource, v1 provider, migration machinery).
yep ok, push and let's discuss the PR body before opening it. it should be terse and to the point, focusing on the user visible impact and answering "ok, what now" and "why"
Pushed. Here's a terse draft — three sections, exactly the "what / what now / why" shape:
Title: login: add --server, retire ENTIRE_AUTH_BASE_URL (COR-393, part 1)
Part 1 of COR-393. Stacked on #1402.
What changes
ENTIRE_AUTH_BASE_URLis retired: if it's set (even empty), every built-inentirecommand exits immediately with an error naming the replacement.entire login --server <url>is the replacement (defaulthttps://us.auth.entire.io). The value must be a bare http(s) origin; userinfo/path/query are rejected, not silently dropped.
If you have the env var exported (dev/staging setups): unset it and run entire login --server <that-url> once. Every other command then follows the login context — no per-command override needed or possible.
Why
Auth resolution is context- and discovery-based now (COR-389, #1402): commands follow your login, not a process-global env override. Leaving the var readable-but-ignored would silently send credentials somewhere the operator doesn't expect, so it fails loudly instead. With the gate in place, the remaining internal AuthBaseURL() reads can only ever observe the default — which is what makes the follow-up demolition PR (static fallbacks, v1 provider, legacy-token migration) a mechanical change.
🤖 Generated with Claude Code
Two open questions for the panelbeat:
- The PR will need
--base woolly-herding-scroll(it stacks on #1402) — or we wait for #1402 to merge and rebase onto main. Preference? - Worth mentioning that plugins and
git-remote-entireare exempt from the bleat (separate binaries), or is that noise?
we'll figure out how plugins deal with auth later. git-remote-entire however, should use contexts, and disregard ENTIRE_AUTH_BASE_URL. does it?
Mostly yes — the auth path proper ignores it, but two vestigial reads still reach the git-remote-entire process until PR 2:
What ignores it (the path that matters): credential resolution is fully context-based — ResolveContextForCluster picks the context from the cluster's /.well-known advertisement, and the exchange targets clusterCtx.CoreURL from contexts.json. ENTIRE_AUTH_BASE_URL plays no role in which core gets dialed or which identity is used. The ENTIRE_TOKEN CI path likewise derives its core from the token's aud claim. No direct api.AuthBaseURL() call exists anywhere in cmd/git-remote-entire.
The two vestiges (indirect, via the shared auth package):
MigrateLegacyLoginContext()— its legacy keyring probe is keyed byapi.AuthBaseURL(). A set var only changes which legacy slot gets bridged; worst case a pre-contexts login doesn't migrate. Harmless.CurrentProvider()insidenewContextTokenManager— the v1/v2 auto-detect readsIsSplitHost(), which comparesAuthBaseURL()againstBaseURL(). This is the real one: someone who setsENTIRE_AUTH_BASE_URLto the same origin as the data URL flips the helper to v1 — empty STS path, wrong endpoints — and refresh/exchange break in confusing ways.
Both vestiges are exactly what PR 2 deletes (migration machinery gone, provider collapses to constants), at which point git-remote-entire genuinely disregards the var rather than "disregards it unless you set it to something pathological."
So my suggestion: no bleat in git-remote-entire (failing clones over an env var is harsh, and "disregard" is the stated goal) — PR 2 achieves true disregard by deletion. The PR body's exemption note can say exactly that in one line: "git-remote-entire resolves auth via contexts and never honored the var for core selection; its two indirect reads (legacy migration key, provider detection) are deleted in part 2."
Want me to fold that line in and open the PR with --base woolly-herding-scroll?
responding to the questions:
-
yes open the PR with --base woolly-herding-scroll
-
nope, don't mention any of that.
PR opened: https://github.com/entireio/cli/pull/1404, based on woolly-herding-scroll so it shows only the COR-393 part-1 diff; it'll retarget cleanly once #1402 merges. Body as agreed — no plugin/git-remote-entire exemption notes.
Ready to start the part-2 demolition (fallback removal, TokenForResource + singleton, v1 provider, migration machinery) whenever you are.
The COR-393 demolition is complete — 7 commits on paul/cor-393-demolish-auth-fallbacks, net −1,600 lines, all unit + login-integration tests green. Not pushed.
The commits, in order:
5c76d6227— control plane: no active context →ErrNotLoggedInerror with the login hint, instead of the staticAuthBaseURL+TokenForResourcefallback.90959fec6—auth status/logoutcontext-only: legacy keyring probe gone; no login → informational "Not logged in." (exit 0) and a no-op logout, per your decisions.d4b0c1e25— data API discovery-only: both static fallbacks become errors naming the host;TokenForResource, the singleton manager, and theSetManagerForTest/DiscoveryUnavailableForTestseams deleted.5bea7212d— provider collapse: v1, the routing table,ENTIRE_AUTH_PROVIDER_VERSION,IsSplitHost, and thesync.Oncesingleton reduce to four OIDC constants.5dfea65dd— legacy store deleted wholesale: login single-writes contexts.json (fatally),MigrateLegacyLoginContext/LookupCurrentToken/ContextStoregone, plus the entireauthfilestorebuild-tag apparatus — the keychain-protection job transferred to the tag-freeENTIRE_TOKEN_STORE=file.f8476faa3—api.AuthBaseURL()deleted; the env var name survives only inside the PR-1 bleat.613b91aa7— README/dev-docs/comments aligned (local-dev flow is nowentire login --server http://localhost:8787 --insecure-http-auth).
Two things worth knowing from the trenches:
- The integration login tests were silently coupled to v1: with auth/data origins pinned to the same fake server, the old heuristic picked the v1 provider, so the fixtures served
/oauth/device/codeand an opaque token. They now serve/device_authorizationand a JWT with properiss/handleclaims — which the contexts-only model genuinely requires. Worth a glance in review since it changes what the tests prove. - Acceptance checklist from the ticket: env var unread ✓ (gate-only),
--server✓ (PR 1), no-context errors with hints ✓,TokenForResourcegone ✓, never-logged-in and wrong-core tests ✓ (control-plane, status, data-API). The login-hint embellishment stayed squashed per your call.
Suggest CI runs the full integration suite on the PR. Want me to draft the part-2 PR body next?
Draft PR opened: https://github.com/entireio/cli/pull/1410, stacked on #1404 (part 1) so it shows only the demolition diff. The stack is now: #1402 (COR-395) ← #1404 (login --server + bleat) ← #1410 (demolition, closes COR-393).
CI will exercise the full integration suite; when #1402 and #1404 merge, this one retargets and can come out of draft.
Run an adversarial Codex review through the shared plugin runtime. Position it as a challenge review that questions the chosen implementation, design choices, tradeoffs, and assumptions. It is not just a stricter pass over implementation defects.
Raw slash-command arguments: ``
Core constraint:
- This command is review-only.
- Do not fix issues, apply patches, or suggest that you are about to make changes.
- Your only job is to run the review and return Codex's output verbatim to the user.
- Keep the framing focused on whether the current approach is the right one, what assumptions it depends on, and where the design could fail under real-world conditions.
Execution mode rules:
- If the raw arguments include
--wait, do not ask. Run in the foreground. - If the raw arguments include
--background, do not ask. Run in a Claude background task. - Otherwise, estimate the review size before asking:
- For working-tree review, start with
git status --short --untracked-files=all. - For working-tree review, also inspect both
git diff --shortstat --cachedandgit diff --shortstat. - For base-branch review, use
git diff --shortstat <base>...HEAD. - Treat untracked files or directories as reviewable work for auto or working-tree review even when
git diff --shortstatis empty. - Only conclude there is nothing to review when the relevant scope is actually empty.
- Recommend waiting only when the scoped review is clearly tiny, roughly 1-2 files total and no sign of a broader directory-sized change.
- In every other case, including unclear size, recommend background.
- When in doubt, run the review instead of declaring that there is nothing to review.
- For working-tree review, start with
- Then use
AskUserQuestionexactly once with two options, putting the recommended option first and suffixing its label with(Recommended):Wait for resultsRun in background
Argument handling:
- Preserve the user's arguments exactly.
- Do not strip
--waitor--backgroundyourself. - Do not weaken the adversarial framing or rewrite the user's focus text.
- The companion script parses
--waitand--background, but Claude Code'sBash(..., run_in_background: true)is what actually detaches the run. /codex:adversarial-reviewuses the same review target selection as/codex:review.- It supports working-tree review, branch review, and
--base <ref>. - It does not support
--scope stagedor--scope unstaged. - Unlike
/codex:review, it can still take extra focus text after the flags.
Foreground flow:
- Run:
- Return the command stdout verbatim, exactly as-is.
- Do not paraphrase, summarize, or add commentary before or after it.
- Do not fix any issues mentioned in the review output.
Background flow:
- Launch the review with
Bashin the background:
- Do not call
BashOutputor wait for completion in this turn. - After launching the command, tell the user: "Codex adversarial review started in the background. Check
/codex:statusfor progress."
Here is Codex's adversarial review output, verbatim:
Codex Adversarial Review
Target: branch diff against main Verdict: needs-attention
No-ship: the auth fallback removal strands existing installs, breaks compatible-but-not-identical servers, and introduces at least one concrete login regression.
Findings:
- [high]
entire loginnow rejects tokens the command explicitly claims to support (cmd/entire/cli/login.go:165-173)runLoginnow hard-fails ifRecordLoginContextcannot derive claims from the access token. That directly contradicts the code immediately above it:validateReceivedTokentreats opaque or non-JWT access tokens as valid. In main, login still succeeded because the legacy store write was authoritative and the context write was best-effort; on this branch, any server that returns an opaque token or a JWT withoutiss/handle/subwill fail login outright. Recommendation: Do not make context recording the only persistence path unless you can build the context from trusted metadata other than JWT claims. Restore a fallback save path, or fetch the missing issuer/subject from the server and add an end-to-end test with an opaque access token. - [high] Upgrading from a legacy-only login now hard-logs users out with no migration path (cmd/git-remote-entire/main.go:237-245)
The clone/push helper now resolves credentials only from
contexts.json. On main this path first calledMigrateLegacyLoginContext(), so users authenticated with older CLIs could keep working after upgrade. This branch removes that bridge, while other auth paths also removed their legacy fallback. Result: an existing installation that only has the old keyring entry will suddenly fail as 'not logged in' until the user re-authenticates, and the old secret is no longer cleaned up by normal logout flows. Recommendation: Keep a one-time legacy-to-context migration on read/startup until you have a proven upgrade boundary, or add an explicit migration command that runs before any auth path drops the old store. - [high] Data-API auth now hard-fails on missing
/.well-knowndiscovery, breaking mixed-version rollouts (cmd/entire/cli/auth/data_api.go:56-78)ResolveDataAPITokennow treats discovery as mandatory and returns an error as soon asErrDiscoveryUnavailableis hit. Main preserved the previous static exchange path for exactly this case. That means any reachable data host that has not rolled out/.well-known/entire-api.jsonyet, is temporarily serving 404/503 there, or is an older/self-hosted deployment becomes unusable even when the user still has a valid login and the old path would have authenticated successfully. Recommendation: Do not remove the static fallback until server capability/version is enforced. Either preserve the old path behind a compatibility check, or fail earlier with an explicit minimum-server-version gate. - [medium] Logout can report success while leaving refresh tokens on the machine (cmd/entire/cli/auth/context_store.go:13-67)
RemoveCurrentContext/RemoveContextdelete the context first, then calldeleteContextKeychain, which swallows all token-store deletion errors. On main, local token deletion failure was surfaced to the caller; here a locked or failing keychain still yields a successful logout. That leaves the long-lived refresh token persisted locally even though the CLI printsLogged out., which weakens logout semantics and can permit later token minting by any process that can read the same keychain slot. Recommendation: Make credential deletion part of the success contract for logout. At minimum, surface deletion failures loudly; preferably fail the logout or defer removing the context until both access and refresh slots are actually removed.
Next steps:
- Restore or replace the legacy-login migration before removing the old auth store paths.
- Reintroduce a compatibility strategy for data hosts that do not yet advertise discovery.
- Fix login persistence so opaque/non-claim-bearing tokens do not hard-fail.
- Make logout report local credential-deletion failures instead of silently ignoring them.
-
for the first item, it sounds like the docs and/or comments are wrong - we are intentionally moving away from legacy access tokens.
-
hard-fail on missing /.well-known is understood and by design. all live resource providers do already provide this mechanism.
-
for logout, yes, perhaps we should make that more failsafe.
Anything else you think is worth actioning from this review?
Pushed a99b64010ed to the draft PR. Here's the disposition of the review and my answer on what else is worth actioning:
Actioned:
- Finding 4 (logout failsafe):
RemoveContext/RemoveCurrentContextnow delete credentials first — refresh slot before access slot, so a partial failure strands at worst the short-lived token — and any deletion failure aborts the logout with the context entry intact. The failure mode inverts to the benign direction: a failed logout says so and retries cleanly, instead of printing "Logged out." over a live refresh token. Added aUseFailingDeleteBackendForTestingseam and a regression test pinning that the entry and refresh token survive an aborted logout. - Finding 1 (opaque tokens): comments corrected, behavior kept —
validateReceivedToken's doc no longer claims opaque-token logins work; it now says plainly thatRecordLoginContextis the sole persistence path, requiresiss/handleclaims, and that legacy opaque-token servers are intentionally unsupported. Test comments aligned.
Not actioned, and why:
- Finding 3 (discovery hard-fail): by design, as you said — and it's the second time we've recorded this decision (COR-393 ticket + the COR-395 review). The error names the host and the missing well-known, which is the agreed UX.
- Finding 2 (legacy stranding): accepted in the ticket ("we can strand such users"). The one residual Codex adds — the old legacy keyring secret is never scrubbed — I'd consciously skip: that slot only ever held an access token (pre-
offline_accesslogins had no refresh token), it expires server-side in hours, and nothing reads it anymore. Scrubbing it would mean re-importing a slice of the store machinery we just deleted, to clean up a dead secret. The one thing I would do instead: when this ships, give the changelog a line like "pre-0.x logins must runentire loginonce after upgrading" — that's the actual user-facing consequence, and it costs a sentence at release time.
Nothing else in the review meets the bar for action. Lint and all 6,197 unit tests pass; the draft PR is current.
sounds good. have a look at all the comments that are touched by this PR, and those it's stacked on, to make sure they reflect the CURRENT truth? don't leave "transition" comments (e.g., previously we X, now we Y) but just describe the behaviour and intent today.
Comment audit done — 7 files corrected in abbcccf085b, pushed. I swept every production comment in the 50 files the stack touches, two grep passes (transition phrasing + references to deleted machinery), then read each hit in context.
Fixed (was describing a world that no longer exists):
RecordLoginContextstill documented the dual-write ("legacy keyring entry is still written by the caller… during the transition", "login still succeeds on the legacy entry") and a "read-time migration" caller foractivate=false. Now: it's the CLI's only credential write, andactivateis described by its semantics alone.git-remote-entirepackage doc still promised "a pre-contexts.json login is migrated at read-time so existing users don't have to re-authenticate." Deleted.newContextTokenManagerjustified itself against "the bug the singleton manager — pinned to AuthBaseURL — has", both deleted; now states the invariant positively (credentials never travel to a host the context doesn't belong to).coreapi.providerSourceexplained its login hint via "the static fallback path returns a bare sentinel" — that path is gone; the hint is now described as a backstop for any untailoredErrNotLoggedIn.NewRefreshingLoginProvider"strict superset of the old read-only provider… behaves exactly as before" → present contract.NormalizeOriginURLended with "see AuthBaseURL", a dangling pointer to the deleted function; the keyring-slot rationale is now inlined. Also "Nothing reads its value anymore" → "Nothing reads its value",defaultContextTokenTTL's opaque-bearer example (opaque tokens can't reach that code),secondsUntil's "historically", and login's "legacy… no longer supported" → "opaque-token-only servers are not supported".
Deliberately kept:
DeviceAuthStart/DeviceAuthPoll"preserves the historical type name" — that is the current intent of the alias.repo_mirror.go's "default for now — once multi-cluster selection lands" — a live forward-looking design note, not stack residue.- The probe's redirect comment explaining why
CheckRedirectfollows 307s — a footgun explainer that stays relevant. RejectRemovedAuthEnvsaying the var "is retired in favour ofentire login --server" — the retirement is the function's present purpose.
Lint and all 6,197 unit tests green; draft PR #1410 is current.
There are a few review comments on https://github.com/entireio/cli/pull/1410 - let's consider them in turn.
Two inline comments from Copilot, both on code this stack added. Here's each with my take:
1. login.go:172 — opaque tokens fail late with a cryptic error. Copilot's point: validateReceivedToken still waves opaque/non-JWT tokens through, so they only die later in RecordLoginContext as save login: parse login token claims: …. Since we've committed to contexts-only (claim-bearing JWTs required), the validation step should enforce that directly. I agree — this is the behavioral completion of what we only re-commented yesterday. Concretely: validateReceivedToken rejects any token whose claims don't parse, and additionally requires non-empty iss and handle/sub — with errors that say what the server must issue (e.g. login server returned a token without parseable JWT claims; entire-cli requires claim-bearing JWTs). The three *OpaqueTokenAccepted unit tests flip to rejection tests, and RecordLoginContext's own checks remain as the backstop for its other callers. Failure becomes immediate, and the message matches the model instead of looking like an internal parse bug.
2. data_api.go:78 — "(… unreachable)" is too narrow. The ErrDiscoveryUnavailable sentinel covers 404, 503, malformed, and unreachable, but the message singles out "unreachable" — so a host that's perfectly reachable but returns 404 gets told it's unreachable. Agree, trivial fix: something like does not advertise its trusted login servers (/.well-known/entire-api.json missing or unreachable) — the wrapped error already carries the precise cause for anyone who reads the chain.
My recommendation is to apply both: #2 is a one-liner, #1 is a small behavior change (~30 lines incl. tests) squarely in the direction you've already ruled. Want me to go ahead with both, or just #2?
Both review comments addressed, committed separately, pushed.
efc75432894— discovery error wording: "(/.well-known/entire-api.json missing or unreachable)", matching everything the sentinel covers; the wrapped error chain still carries the precise cause.f8b0a281122— claim-free tokens now die atvalidateReceivedTokenwith errors that name the requirement (parseable JWT claims/no iss claim/no handle or sub claim) instead of surfacing later assave login: parse login token claims.subalone satisfies the identity claim (matchingRecordLoginContext's fallback), and the old*OpaqueTokenAcceptedtests flipped into a rejection table plus three new claim-requirement tests.
Lint, all 6,201 unit tests, and a forced rerun of the login integration tests (their fake server already issues claim-bearing JWTs) are green. Draft PR #1410 is current.
❯ okay, push and respond to the review comments - only one sentence each, with link to commits as apporprotate.
Both replies posted (branch was already pushed — nothing new to push):
- Reply on the login-validation comment → points at
f8b0a281122. - Reply on the error-wording comment → points at
efc75432894.
whoops, let's address https://github.com/entireio/cli/pull/1410#discussion_r3392588804
The fix is committed locally as 4f6a2693aab, but the push needs you — Secretive is refusing the SSH signing operation, which usually means it's waiting for (or was denied) physical approval on your end. Run git push from a terminal where you can approve the key prompt, and I'll post the one-sentence reply to the review comment once it's up.
What the commit does, for the record: context removal (selection → keyring delete → entry delete) now runs inside a single locked contexts.Modify, closing the read/write gap Copilot flagged — a concurrent auth use can no longer retarget "current" mid-logout. A failed credential delete aborts the callback, which discards the entry change (per Modify's contract), preserving the retry-friendly failure mode the failsafe test pins. Lint and all 6,201 unit tests pass.
Pushed 4f6a2693aab and replied to the review comment. Draft PR #1410 is fully current: all three Copilot findings addressed, lint and 6,201 unit tests green.
let's rebase paul/cor-393-demolish-auth-fallbacks on its base, paul/cor-393-sunset-entire-auth-base-url
Rebase complete — paul/cor-393-demolish-auth-fallbacks now sits on the updated base, all checks green. It needs a git push --force-with-lease when you're ready (history rewritten); say the word and I'll run it.
What the rebase involved:
- The base gained 3 commits addressing #1404 review feedback, two of which solved the same problems my demolition had solved independently (the broken login integration tests, login.go legacy-entry handling). Conflicts in 11 files across 4 of my 12 commits.
- Resolution principle: the demolition's end state wins wherever the base's change was interim scaffolding it deletes (control-plane fallback comments, legacy dual-write logic,
TokenForResourcedocs). For the integration tests I kept the base's version (fakeLoginJWT, dedicated config-dir sandbox, the new contexts.json assertion) and dropped my parallelloginTestJWThelper plus the now-deadENTIRE_TEST_AUTH_STORE_FILEline. - One thing my conflict resolution initially clobbered: the base's
u.Redacted()redaction inparseLoginServererrors (so a password-bearing--serverURL can't leak into CI logs). I caught it in the post-rebase audit by diffing each resolved file against the base — restored as2af3c53dfe6. The base's other two improvements (the present-but-empty env-gate test skip, the smoke-script updates) survived intact, verified explicitly.
Lint, all 6,201 unit tests, and a forced rerun of the login integration tests pass on the rebased branch.
Pushed (force-with-lease) — 2af3c53dfe6 is now the tip of paul/cor-393-demolish-auth-fallbacks. Draft PR #1410 now shows only the demolition delta against the updated part-1 branch, with the redaction restore as the final commit.
alright let's rebase paul/cor-393-demolish-auth-fallbacks on paul/cor-393-sunset-entire-auth-base-url
Blocked on SSH again — Secretive is refusing the signing operation for git fetch, same as before. I need the remote state to rebase against (the base presumably moved again). Approve the key in Secretive (or run ! git fetch origin here so the prompt lands on your side), and I'll proceed with the rebase.
Rebased onto the new base — 14 commits, all checks green, ready for git push --force-with-lease on your word.
This rebase was more interesting than the last one: the base had been rebased onto a main that now contains the merged #1402 and the new loopback browser login flow (#1366/#1403), which reworks exactly the files the demolition touches. Notable resolutions:
- Provider collapse absorbed the browser flow: the new flow added
AuthorizePathto the provider struct; my constants gainedoauthAuthorizePath = "/authorize"and both thedeviceflowandauthcodeclients now wire from constants. v1 stays deleted — the browser flow only ever used v2 paths. runLoginbecamepersistLogin(shared by device + browser flows) upstream; the legacy-write deletion and claims validation re-applied cleanly inside the new structure. With the dual-write's warn-and-continue gone,persistLogin'serrWparameter became dead — dropped in the final commit (b8b0d594a03, lint caught it).- Integration harness: upstream made
startLoginProcessvariadic for--deviceflag tests; kept that shape, merged the env-sandbox comments, legacy store var stays gone. - Lost-content audit (the step that caught the redaction last time): diffed all four conflict files against the base — every remaining delta is intentional demolition content; redaction, browser flow, and the variadic harness all intact.
Lint, 6,228 unit tests, and a forced rerun of the login integration tests pass.
ok, everything else is merged, let's rebase https://github.com/entireio/cli/pull/1410 on origin/main
Done — PR #1410 is rebased onto origin/main and pushed (6c240a0259b); GitHub had already retargeted the base to main when #1404 merged.
The interesting wrinkle this round: main gained a userdirs refactor that deleted contexts.DefaultConfigDir() in favor of userdirs.Config() (which also auto-isolates under go test). Three of my commits conflicted on exactly the legacy functions they delete or rewrite — resolved by taking the deletion side and substituting userdirs.Config() into the surviving logout code (RemoveContext/removeContextLocked). A post-rebase sweep confirms zero DefaultConfigDir references remain anywhere.
Final state: 14 commits, −1,859 lines net vs main. Lint, all 6,264 unit tests, and the login integration tests pass. The PR can come out of draft whenever you're ready.
It's the authfilestore build-tag removal, fallout from deleting the legacy keyring store (commit "auth: delete the legacy keyring store and pre-contexts migration").
The tag existed for exactly one purpose: the legacy auth.Store's file backend (store_filebackend.go) was only compiled under -tags=authfilestore, so test-spawned entire binaries could honor ENTIRE_TEST_AUTH_STORE_FILE and keep the legacy store off the developer's real OS keychain. mise's test:integration, build:e2e, and test:ci tasks passed the tag to produce those test builds.
With the legacy store deleted, the tag gates zero files — keeping -tags=authfilestore would build identical binaries while implying a test-only build variant that no longer exists. The keychain-protection job it served didn't go away, it moved: the surviving store (internal/entireclient/tokenstore) honors ENTIRE_TOKEN_STORE=file + ENTIRE_TOKEN_STORE_PATH unconditionally, no special build needed, and the e2e/integration harnesses set those env vars (the e2e harness already did; the integration login tests gained them in the same commit). The build:e2e description was updated to say exactly that.
Side benefit: test binaries are now bit-identical to production builds, so integration/e2e tests exercise the real thing rather than a tagged variant.
oh cool so can we remove the build:e2e target and simply call the regular build target in our Github workflow?
oh cool so can we remove the build:e2e target and simply call the regular build target in our Github workflow?
Yes — done, with one correction to the premise: build:e2e was never in the GitHub workflows. The e2e workflows run go build directly and pass E2E_ENTIRE_BIN; the only callers were the mise e2e drivers (test:e2e, canary, roger-roger). And it turns out a plain build file task (mise-tasks/build) already existed, building the identical two binaries — post-tag-removal, build:e2e was a byte-for-byte duplicate of it.
So commit 0043e04b7bd deletes the build:e2e TOML task and points the three e2e drivers at mise run build. Verified end-to-end: mise run build produces both binaries, lint passes, and a single-test canary run (which exercises the full canary → _default → mise run build chain) exits clean. Nothing in .github/ needed changing.
One commit ahead of the pushed branch — say the word and I'll push.