Auth Refactor: Eliminate Static Fallbacks

Claude Code·Fable 5.[1m]·toothbrush·3mo ago·45hr 37min·22 Checkpoints·96 file changes·+1125/-2668·4.5M tokens

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.

3mo ago

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

3mo ago·2m

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 reads LookupCurrentToken() and exchanges at a hard-coded api.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) and waitForMirrorClone (repo_mirror_probe.go:182).
  • Item 2 — already done. Zero references to resolveAuthHostToken in code or tests; only a stale mention in repo_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 or repo:<action> scope. The "repo-scoped sibling" it hypothesizes already exists: internal/entireclient/repocreds, which is what git-remote-entire uses. The old wire-form-parity concern dissolves entirely — repocreds is the wire form we're matching, byte-for-byte (audience-only, no resource param).

Plan

  1. Rework the mint path — replace RepoScopedToken's body (or replace the function with a small repoCredsForCluster(ctx, clusterHost) constructor) with the git-remote-entire recipe from cmd/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).
  2. Preserve the suspended-mirror sentinelexplainSuspendedMirror keys on ErrRepoTargetUnknown, currently mapped from sts.ExchangeError{Code: "invalid_target"}. repocreds surfaces *httputil.OAuthError instead, so re-map the sentinel from that error's invalid_target code.
  3. Update the two callers — in waitForMirrorClone, hold the repocreds.Cache across the loop and call Invalidate + Token on 401 (replaces the hand-rolled minReauthInterval re-mint floor, or keep the floor as belt-and-braces).
  4. Cleanup — drop the dead resolveAuthHostToken comment, the repoExchangeTransportForTest seam if repocreds' httpClient injection covers it, and the sts/api.AuthBaseURL imports from repo_token.go.
  5. 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.

3mo ago·10m

All 6222 unit tests pass. Running integration + canary (the rest of the pre-commit checklist):

CI will run integration.

3mo ago·52.6s

Committed 821be3e98cfRepoScopedToken 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 with clusterdiscovery.ResolveContextForCluster (active-if-eligible → sole → explicit-choice error, login hint when none), then mints through NewRefreshingLoginProvider + repocreds — the exact stack git-remote-entire uses. Signature changed to take clusterHost (both callers were prepending https:// 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_id via Basic auth, audience-only, no resource).
  • httputil.OAuthError gains Code: parsed best-effort from the RFC 6749 error body field, so the suspended-mirror sentinel ErrRepoTargetUnknown survives the move from sts.ExchangeError to the repocreds error chain. explainSuspendedMirror keeps 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_id travels as Basic auth; invalid_target sentinel test ported; new test that discovery errors surface verbatim (never a wrong-core exchange); new OAuthError.Code parse test. Test seams follow the data_api.go pattern (shared resolveContextFunc type, sandboxed ENTIRE_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).

3mo ago·4m

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 setResolveContextForClusterForTest from repo_token.go — in-package tests now swap the seam var directly via a stubResolveContextForCluster test helper, matching the stubResolveContextForAPI idiom in data_api_test.go (reuse finding).
  • Deduped the sandbox setup (ENTIRE_CONFIG_DIR + file token backend) that appeared twice in repo_token_test.go into one sandboxRepoTokenStores helper (simplification finding).

Skipped:

  • Per-call MigrateLegacyLoginContext + http.Client allocation (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 with ResolveDataAPIToken which migrates per-call the same way.
  • The altitude agent confirmed the layer choices (Code parse in httputil, sentinel mapping in repo_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 --cached and git 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 --shortstat is 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.
  • Then use AskUserQuestion exactly once with two options, putting the recommended option first and suffixing its label with (Recommended):
    • Wait for results
    • Run in background

Argument handling:

  • Preserve the user's arguments exactly.
  • Do not strip --wait or --background yourself.
  • Do not weaken the adversarial framing or rewrite the user's focus text.
  • The companion script parses --wait and --background, but Claude Code's Bash(..., run_in_background: true) is what actually detaches the run.
  • /codex:adversarial-review uses the same review target selection as /codex:review.
  • It supports working-tree review, branch review, and --base <ref>.
  • It does not support --scope staged or --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 Bash in the background:
  • Do not call BashOutput or wait for completion in this turn.
  • After launching the command, tell the user: "Codex adversarial review started in the background. Check /codex:status for progress."
3mo ago·7m

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-timeout no longer bounds the initial authorization phase (cmd/entire/cli/repo_mirror_probe.go:181-193) waitForMirrorClone still mints the first repo token before it applies context.WithTimeout. Before this change that was a single exchange. Now auth.RepoScopedToken performs 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-timeout can 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 first mintToken() call, or pass a dedicated bounded context into RepoScopedToken so 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) RepoScopedToken does a fresh context resolution on every call and explicitly does not cache anything. waitForMirrorClone calls it again whenever the probe token expires. That means a long-running clone wait is now coupled to repeated /.well-known/entire-cluster.json resolution 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.

sounds good. fix them both in separate commits.

3mo ago·8m

Both Codex findings are fixed in separate commits — plus one extra commit repairing an unintended edit I found along the way.

  • 2ebd0d8ef59 (high finding): waitForMirrorClone now arms the --wait-timeout deadline 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 test TestWaitForMirrorClone_TimeoutBoundsAuthorization pins a hung auth path against a 50ms timeout.
  • f2098377390 (medium finding): new auth.RepoTokenSource splits one-time resolution from per-token exchange — discovery and context selection run once at construction; Token/Invalidate sit on a repocreds cache 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; RepoScopedToken remains as the one-shot wrapper for the single suspension probe. Test TestRepoTokenSource_ReMintSkipsDiscovery asserts discovery runs exactly once across an initial mint + invalidate + re-mint.
  • 30318e52975 (incidental): while running lint I discovered the earlier /simplify commit had swept in a stray edit to explain.go (via git add -A) that deleted a //nolint:exhaustive directive 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 --cached and git 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 --shortstat is 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.
  • Then use AskUserQuestion exactly once with two options, putting the recommended option first and suffixing its label with (Recommended):
    • Wait for results
    • Run in background

Argument handling:

  • Preserve the user's arguments exactly.
  • Do not strip --wait or --background yourself.
  • Do not weaken the adversarial framing or rewrite the user's focus text.
  • The companion script parses --wait and --background, but Claude Code's Bash(..., run_in_background: true) is what actually detaches the run.
  • /codex:adversarial-review uses the same review target selection as /codex:review.
  • It supports working-tree review, branch review, and --base <ref>.
  • It does not support --scope staged or --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 Bash in the background:
  • Do not call BashOutput or wait for completion in this turn.
  • After launching the command, tell the user: "Codex adversarial review started in the background. Check /codex:status for progress."
3mo ago·6m

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) NewRepoTokenSource now always resolves the cluster through ResolveContextForCluster and immediately returns any discovery error. That makes repo mirror create's suspension probe and clone-wait auth fail on a cold cache whenever /.well-known/entire-cluster.json is 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.json now 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 through ResolveContextForCluster, which loads contexts.json and bubbles any load error back to the caller. Before this change, the repo-token path read credentials through LookupCurrentToken; CurrentContextToken() intentionally swallowed contexts.Load failures and ContextStore fell back to the legacy keyring entry. The new behavior means a malformed or permission-broken contexts.json can 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 existing ContextStore does: fall back to the legacy token path, or at minimum special-case load contexts failures so a usable legacy login still works.

Next steps:

  • Add regression tests for RepoScopedToken when cluster discovery is unavailable but static exchange would succeed.
  • Add regression tests for malformed/unreadable contexts.json with a valid legacy token present.

I thikn this is all fine because we're about to sunset the legacy tokens.

3mo ago·43.8s

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?

3mo ago·54.7s

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.go no longer reads api.AuthBaseURL()RepoScopedToken/NewRepoTokenSource now resolve via clusterdiscovery.ResolveContextForCluster + NewRefreshingLoginProvider + repocreds. One site off the §4 audit list.
  • NewRepoTokenSource calls MigrateLegacyLoginContext() (best-effort bridge, same as ResolveDataAPIToken and 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.json no 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.

yep post the comment.

3mo ago·35.5s

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.

3mo ago·28.6s

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-timeout now 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.json errors 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.

3mo ago·21.5s

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-timeout now 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 in NewRepoTokenSource (goes when the migration machinery is deleted),
  • the remaining api.AuthBaseURL() env-read sites elsewhere in the auth package — this PR only takes repo_token.go off 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.

ok open the PR.

3mo ago·2m

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?

3mo ago·34.3s

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 + Token re-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 hard re-authorize clone probe failure. 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?

3mo ago·46.9s

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.

go ahead and push the wording fixes

3mo ago·2m

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.

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?

3mo ago·45.8s

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_access at login; persist the refresh token" — done: login requests Scope: "cli offline_access" (auth/client.go:72) and the refresh token is persisted in its own keyring slot (RefreshService handling in store.go/refresh.go).
  • "Wire the refresh_token grant via auth-go's tokenmanager" — done by COR-389 / PR #1377 for control-plane and data-API paths (NewRefreshingLoginProvider/NewRefreshingResourceProvider over tokenmanager), 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.

3mo ago·2m

Posted to COR-316. The breakdown maps each ticket bullet to its PR, verified against git history rather than memory:

  • #1337 (Jun 3) — offline_access at login, refresh-token persistence (refresh-slot-first ordering, logout scrub), and NewRefreshingLoginProvider for git-remote-entire.
  • #1367 (COR-389) — control-plane commands onto the per-context refreshing resolver.
  • #1377 (COR-389) — data-API commands onto ResolveDataAPIToken + NewRefreshingResourceProvider with /.well-known/entire-api.json discovery.
  • #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.

3mo ago·1m

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 legacy ent_ token endpoint and uses the entire-core device flow, so deleting routes/cli-auth.ts won'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.

3mo ago·2m

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:

ClusterSitesFate per ticket
Login destinationauth/client.go:59 (device flow issuer)entire login --server
Status/logout fallbackauth.go:56,158, logout.go:72-93 (resolveStatusTarget's fallbackBaseURL + legacy keyring probe)→ "run entire login"
Control-plane fallbackcontrol_plane.go:63-70 (no-context → static target + TokenForResource)→ error
Legacy token plumbingstore.go:157 (LookupCurrentToken), contexts.go:164 (MigrateLegacyLoginContext)→ delete
Provider/host detectionprovider.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?

  1. yes i like that - if os.LookupEnv returns ok, we bleat.

  2. yes, auth status informational. i like the recommendations.

  3. 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.

  4. okay let's do (b).

  5. 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?

3mo ago·1m

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/token doubling 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

  1. yes all-in on v2, v1 is dead and gone, remove all traces from cli

  2. 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 )

3mo ago·10m

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"

3mo ago·59.2s

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_URL is retired: if it's set (even empty), every built-in entire command exits immediately with an error naming the replacement.
  • entire login --server <url> is the replacement (default https://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:

  1. The PR will need --base woolly-herding-scroll (it stacks on #1402) — or we wait for #1402 to merge and rebase onto main. Preference?
  2. Worth mentioning that plugins and git-remote-entire are 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?

3mo ago·41.8s

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):

  1. MigrateLegacyLoginContext() — its legacy keyring probe is keyed by api.AuthBaseURL(). A set var only changes which legacy slot gets bridged; worst case a pre-contexts login doesn't migrate. Harmless.
  2. CurrentProvider() inside newContextTokenManager — the v1/v2 auto-detect reads IsSplitHost(), which compares AuthBaseURL() against BaseURL(). This is the real one: someone who sets ENTIRE_AUTH_BASE_URL to 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:

  1. yes open the PR with --base woolly-herding-scroll

  2. nope, don't mention any of that.

3mo ago·33.3s

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.

yep, cool, let's spinoff another branch and start the demolition.

3mo ago·46m

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:

  1. 5c76d6227 — control plane: no active context → ErrNotLoggedIn error with the login hint, instead of the static AuthBaseURL + TokenForResource fallback.
  2. 90959fec6auth status/logout context-only: legacy keyring probe gone; no login → informational "Not logged in." (exit 0) and a no-op logout, per your decisions.
  3. d4b0c1e25 — data API discovery-only: both static fallbacks become errors naming the host; TokenForResource, the singleton manager, and the SetManagerForTest/DiscoveryUnavailableForTest seams deleted.
  4. 5bea7212d — provider collapse: v1, the routing table, ENTIRE_AUTH_PROVIDER_VERSION, IsSplitHost, and the sync.Once singleton reduce to four OIDC constants.
  5. 5dfea65dd — legacy store deleted wholesale: login single-writes contexts.json (fatally), MigrateLegacyLoginContext/LookupCurrentToken/ContextStore gone, plus the entire authfilestore build-tag apparatus — the keychain-protection job transferred to the tag-free ENTIRE_TOKEN_STORE=file.
  6. f8476faa3api.AuthBaseURL() deleted; the env var name survives only inside the PR-1 bleat.
  7. 613b91aa7 — README/dev-docs/comments aligned (local-dev flow is now entire 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/code and an opaque token. They now serve /device_authorization and a JWT with proper iss/handle claims — 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 ✓, TokenForResource gone ✓, 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?

yep push and create a DRAFT PR/

3mo ago·1m

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 --cached and git 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 --shortstat is 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.
  • Then use AskUserQuestion exactly once with two options, putting the recommended option first and suffixing its label with (Recommended):
    • Wait for results
    • Run in background

Argument handling:

  • Preserve the user's arguments exactly.
  • Do not strip --wait or --background yourself.
  • Do not weaken the adversarial framing or rewrite the user's focus text.
  • The companion script parses --wait and --background, but Claude Code's Bash(..., run_in_background: true) is what actually detaches the run.
  • /codex:adversarial-review uses the same review target selection as /codex:review.
  • It supports working-tree review, branch review, and --base <ref>.
  • It does not support --scope staged or --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 Bash in the background:
  • Do not call BashOutput or wait for completion in this turn.
  • After launching the command, tell the user: "Codex adversarial review started in the background. Check /codex:status for progress."
3mo ago·8m

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 login now rejects tokens the command explicitly claims to support (cmd/entire/cli/login.go:165-173) runLogin now hard-fails if RecordLoginContext cannot derive claims from the access token. That directly contradicts the code immediately above it: validateReceivedToken treats 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 without iss/handle/sub will 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 called MigrateLegacyLoginContext(), 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-known discovery, breaking mixed-version rollouts (cmd/entire/cli/auth/data_api.go:56-78) ResolveDataAPIToken now treats discovery as mandatory and returns an error as soon as ErrDiscoveryUnavailable is 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.json yet, 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/RemoveContext delete the context first, then call deleteContextKeychain, 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 prints Logged 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?

3mo ago·6m

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/RemoveCurrentContext now 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 a UseFailingDeleteBackendForTesting seam 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 that RecordLoginContext is the sole persistence path, requires iss/handle claims, 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_access logins 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 run entire login once 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.

3mo ago·5m

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):

  • RecordLoginContext still 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 for activate=false. Now: it's the CLI's only credential write, and activate is described by its semantics alone.
  • git-remote-entire package doc still promised "a pre-contexts.json login is migrated at read-time so existing users don't have to re-authenticate." Deleted.
  • newContextTokenManager justified 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.providerSource explained 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 untailored ErrNotLoggedIn.
  • NewRefreshingLoginProvider "strict superset of the old read-only provider… behaves exactly as before" → present contract.
  • NormalizeOriginURL ended 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 CheckRedirect follows 307s — a footgun explainer that stays relevant.
  • RejectRemovedAuthEnv saying the var "is retired in favour of entire 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.

3mo ago·58.7s

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?

ok, fix and commit separately.

3mo ago·4m

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 at validateReceivedToken with errors that name the requirement (parseable JWT claims / no iss claim / no handle or sub claim) instead of surfacing later as save login: parse login token claims. sub alone satisfies the identity claim (matching RecordLoginContext's fallback), and the old *OpaqueTokenAccepted tests 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.

3mo ago·36.6s

Both replies posted (branch was already pushed — nothing new to push):

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.

push

3mo ago·43.2s

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

3mo ago·8m

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, TokenForResource docs). For the integration tests I kept the base's version (fakeLoginJWT, dedicated config-dir sandbox, the new contexts.json assertion) and dropped my parallel loginTestJWT helper plus the now-dead ENTIRE_TEST_AUTH_STORE_FILE line.
  • One thing my conflict resolution initially clobbered: the base's u.Redacted() redaction in parseLoginServer errors (so a password-bearing --server URL can't leak into CI logs). I caught it in the post-rebase audit by diffing each resolved file against the base — restored as 2af3c53dfe6. 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.

great, push

3mo ago·43.2s

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

3mo ago·2m

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.

try agin

3mo ago·9m

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 AuthorizePath to the provider struct; my constants gained oauthAuthorizePath = "/authorize" and both the deviceflow and authcode clients now wire from constants. v1 stays deleted — the browser flow only ever used v2 paths.
  • runLogin became persistLogin (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's errW parameter became dead — dropped in the final commit (b8b0d594a03, lint caught it).
  • Integration harness: upstream made startLoginProcess variadic for --device flag 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.

3mo ago
3mo ago

ok, everything else is merged, let's rebase https://github.com/entireio/cli/pull/1410 on origin/main

3mo ago·7m

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.

why did we adjust mise.toml?

3mo ago·32.1s

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?

3mo ago

oh cool so can we remove the build:e2e target and simply call the regular build target in our Github workflow?

3mo ago·4m

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.