Debug Cluster Context Git Remote Lookup · Entire

Home

Log in

we recently merged a concept of "cluster contexts" into this repo. the idea is that the first git operation via git-remote-entire, we need to look up from the cluster URL (like aws-eu-central-1.entire.io) which control plane to talk to. what i'm confused about is it worked well for one cluster but not the other.

i did:

$ git pull # in a repo hosted on entire://aws-us-east-2.entire.io/gh/entirehq/foo $ git pull # in a repo hosted on entire://aws-eu-central-1.entire.io/gh/entirehq/marvin

but my contexts file only contained one:

10:14:03 (git) main▲ $ cat ~/.config/entire/contexts.json { "current_context": "us.auth.entire.io", "cluster_contexts": { "aws-eu-central-1.entire.io": "us.auth.entire.io" }, "contexts": [
{
"name": "us.auth.entire.io",
"core_url": " https://us.auth.entire.io",
"handle": "toothbrush",
"keychain_service": "entire-core: https://us.auth.entire.io"
}
] }% [paul@boulot] ~/src/entireio/cli

is there a concept of "default cluster", and can we make things more symmetrical?

1mo ago·3m·8 calls

Bottom line

What actually happens on git pull

ResolveContextForCluster (internal/entireclient/clusterdiscovery/resolve.go:42) resolves per-invocation:

  1. If cluster_contexts[host] exists → use it.
  2. Else → fetch the cluster's /.well-known/entire-cluster.json, match advertised core_urls against your local contexts, prefer current_context among matches. Result is ephemeral — never written back (resolve.go:23, with a long comment on why).
  3. No match → fatal error with a login hint. There is deliberately no current_context fallback for an unknown cluster (resolve.go:34) — that fallback used to silently point a staging context at a prod cluster and 400.

So:

The two asymmetries

1. Producer asymmetry. The struct doc (contexts.go:47) says cluster_contexts is "populated lazily after a successful op against the cluster, or explicitly via entire-core context bind." That lazy-populate is entire-core's behavior. This CLI explicitly opts out (resolve.go:23) to prevent a drive-by clone of an attacker host (e.g. a malicious submodule whose /.well-known names your real core) from minting a durable, silent auth channel.

2. Command asymmetry. This CLI ships entire auth unbind and entire auth contexts (list), but no entire auth bind. BindCluster exists (contexts.go:253) with zero production callers in this repo — you can revoke a binding here but can't create one.

Making it symmetrical — three directions

Option What Risk
A. Add entire auth bind <host> <context> Wire BindCluster (already written) to a cobra command, mirroring unbind. Deliberate user action → matches the security model exactly. Low. Cheapest, closes the command asymmetry.
B. Persist on successful discovery Make this CLI write the binding like entire-core does after step 2 succeeds. High — this is exactly the drive-by-clone vector resolve.go was written to avoid. Would need gating (e.g. only after an interactive/explicit op, never a bare clone).
C. Explicit "default" fallback for unbound clusters Let current_context resolve unknown clusters without discovery. High — re-introduces the staging→prod 400 footgun resolve.go:34 removed.

My recommendation: A. It gives you a symmetric, deliberate bind/unbind pair and a way to pin us-east-2 yourself, without reopening the security hole that B/C represent. The "symmetry" you're missing isn't a default cluster — it's the missing bind verb.

Want me to wire up entire auth bind? It's small: a cobra command in auth_context.go calling the existing auth.BindCluster, plus validation that the named context exists (BindCluster already checks that) and a test.

i want to dig into this claim

"cluster_contexts is "populated lazily after a successful op against the cluster, or explicitly via entire-core context bind." That lazy-populate is entire-core's behavior. This CLI explicitly opts out (resolve.go:23) to prevent a drive-by clone of an attacker host (e.g. a malicious submodule whose /.well-known names your real core) from minting a durable, silent auth channel."

that smells like bullshit. a user is explicitly calling git clone entire://some-cluster.com/foo/bar, which causes git-remote-entire to reach out to some-cluster.com/.well-known/... - submodules have nothing whatsoever to do with this mechanism. there's no silent or magic anything.

1mo ago·2m·3 calls

Verdict: your instinct is right. The "durable, silent channel that re-mints identity-bearing JWTs" line is the bullshit part. Here's the actual token flow, traced end to end.

Where credentials actually go

Two different tokens, two different destinations:

  1. Login JWT (the identity-bearing, long-lived credential) — sent only to clusterCtx.CoreURL, never to the cluster host. repocreds.New(clusterCtx.CoreURL, ...) (main.go:108) → exchange() POSTs subject_token=loginJWT to c.coreURL (repocreds.go:175, 181). The cluster host never sees it.
  2. Repo-scoped token — minted by that exchange with audience = clusterURL + "/et/owner/repo" and scope = repo:pull|push (repocreds.go:177-178). This is the only thing sent to the cluster nodes (main.go:121). It's audience-pinned to that exact host+repo and useless replayed anywhere else.

So a malicious attacker.com:

Why persistence changes none of it

The dangerous step — exchanging the login JWT — is gated by the core (audience + cluster_host validation), not by the /.well-known fetch. The well-known's only job is host→core mapping. So:

On submodules

Half-fair, but it doesn't rescue the argument. Submodules (clone --recurse, submodule update) genuinely are a way to invoke git-remote-entire against a URL you didn't type — that part isn't nonsense. But:

What's actually true

Net: persisting on successful resolution (option B from before) is not the security hole the comment claims. If you want symmetry, B is defensible — or just add the explicit entire auth bind (A). Want me to rewrite that resolve.go comment to say what's actually true, and/or wire up one of the two?

let's start by rewriting the comments to speak the truth indeed. then commit.

1mo ago·53.7s·2 calls

Three places carry the threat model; two of them make the misleading "identity-bearing JWT / silent durable channel" claim. The ClusterContexts struct doc and the current_context-fallback paragraph are accurate, so I'll leave those. Editing resolve.go and auth_context.go.

Let's not talk about entire-core, that's a concern of a company-private repo. We will be bringing the behaviours in line, but the framing will be meaningless to outsiders.

1mo ago·28.4s·1 call

Good call — dropping the entire-core references. Let me reframe in terms of what this helper does, without naming any private tooling.

I don't want comments saying "sometime in the past XYZ held true". focus on NOW, write in present tense, and only discuss the code's state today.

1mo ago·18m·8 calls

Done. Committed as ea605a648 (not pushed).

What changed — two files, comments only:

Notes:

ea605a6Correct cluster-resolution comments to match actual token flow\ \ The resolver comments claimed not persisting a cluster binding prevented\ a "drive-by clone" from minting identity-bearing JWTs. That's false: the\ login JWT only ever goes to the resolved context's CoreURL, never to the\ cluster host, and the cluster host receives only a repo-scoped,\ audience-pinned token. Reframe not-persisting as the correctness choice\ it is (immediacy of auth use / context deletion, no host-controlled\ host->core mapping lingering), and align the auth-contexts audit comment.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: ee378db97d3e+31/-20

have a look at timeouts in cmd/git-remote-entire. list out any relevant ones for me

1mo ago·1m·5 calls

I have the full picture. Here it is.

Timeouts relevant to git-remote-entire

Bottom line: only the small control-plane calls have a hard total timeout (30s). The actual git transfer has no total timeout by design — it's bounded only by a fast connect timeout plus Ctrl-C/parent-context cancellation. So large clones won't get guillotined, but a stalled mid-stream transfer relies on TCP/the user to break it.

# Value Where Applies to Notes
1 30s total request main.go:87 (http.Client.Timeout) /.well-known discovery (ResolveContextForCluster) + OAuth token exchange (repocreds) This client is passed only to discovery + token exchange. Fine for small JSON; would be a footgun on a stream, but it's never used for one.
2 none (no Timeout) proxy.go:107 (p.client) the git data transfer — info/refs, git-upload-pack, git-receive-pack Deliberate: a total timeout would kill big clone/fetch/push. Bounded instead by #3 (connect) + parent ctx (signals). No idle/stall timeout, though.
3 500ms TCP connect (DefaultDialTimeout), env override ENTIRE_CONNECT_TIMEOUT_SECONDS httpclient/transport.go:23,28 every dial on both clients (shared NewTransport) Intentionally short — it's the replica-failover knob, so a dead node fails over fast. Invalid env value → falls back to 500ms with a stderr warning.
4 2s grace main.go:186 (installSignals → time.Sleep) shutdown after first SIGINT/SIGTERM Not a network timeout. After 2s prints "shutdown taking longer…"; second Ctrl-C hard-exits. Cancelling ctx aborts in-flight transfers.
5 1mSafetyMargin (capped at TTL/2) repocreds.go:31,127 scoped-token cache rotation Freshness margin, not a network timeout — re-mints a repo-scoped token ~1m before expiry.

No timeout set anywhere: TLS handshake, response-header, idle-conn, keep-alive — all Go defaults (i.e. unbounded except OS-level), on both p.client and noRedirectClient (proxy.go:342, which reuses the same transport, also no Timeout).

The one gap worth flagging: the data-plane client (#2) has no idle/stall timeout. A connection that establishes (passes the 500ms dial) then hangs mid-packfile won't time out on its own — it waits on TCP or the user hitting Ctrl-C. If you want resilience against a half-dead replica that accepts the connection but stops sending, that'd need a stall-detector (e.g. a ResponseHeaderTimeout on the transport, or a read-deadline wrapper), not a total Timeout. Want me to dig into whether that's a real risk given the replica-failover logic?

500ms TCP connect

that one sometimes bites us on initial connect. Let's push that one up to 1s.

1mo ago·1m·5 calls

Done. Committed as d026319e1 (not pushed).

This affects both clients sharing NewTransport (control-plane and git data transfer dials), since the failover path is the same.

328390bRaise default dial timeout 500ms -> 2s\ \ A 500ms per-host connect budget sometimes trips on a slow initial\ connect (cold DNS, TLS-fronting LB, distant region) before the node can\ answer. 1s still fails over off a dead node quickly while absorbing that\ cold start. ENTIRE_CONNECT_TIMEOUT_SECONDS remains the escape hatch for\ slower links.\ \ Hoping this will make\ https://entireio.slack.com/archives/C09V780RGGN/p1780325899840859 less\ likely.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 767110fcc07e+5/-4

ok i made a spinoff branch. back to those contexts. there are a few problems in our implementation:

  1. it diverges from original git-remote-entire (see ~/src/entirehq/entiredb/cmd/git-remote-entire) in that we should cache in contexts.json all successful cluster->core binding lookups. they don't change all the time, they're essentially fixed, we just don't want to ship hardcoded values in code. please compare this implementation with the original and summarise discrepancies.

  2. we have 2 views of "what's my session" - see:

11:47:04 (git)! main▲ $ entire auth list ID NAME SCOPE CREATED LAST USED EXPIRES 019e0189-038e-726a-9d48-40ae68fe573c CLI login 2026-05-07 cli 2026-05-07 4d ago 2026-06-06 019e7148-93cb-7d66-8504-ac81ae4f5318 CLI login 2026-05-29 cli 2026-05-29 never 2026-06-28

11:45:58 (git)! main▲ $ entire auth contexts us.auth.entire.io toothbrush https://us.auth.entire.io

Cluster bindings (these hosts auto-authenticate with a stored context): aws-eu-central-1.entire.io -> us.auth.entire.io

we'll think about consolidating that later.

1mo ago·2m·5 calls

I now have the complete original mechanism. Here's the comparison.

Bottom line

Ours never persists cluster→context bindings; the original persists them in two places. Every git pull in our implementation pays a full /.well-known discovery round-trip that the original pays only once per cluster. That's the core divergence, and it's a deliberate removal on our side (the comment I just wrote even brags about it).

Where the original persists (two mechanisms, belt-and-suspenders)

1. Inline in clusterdiscovery/resolve.go — speculative, on discovery match (before any auth):

1
2
3
4
5
6
7
8

// entiredb client/clusterdiscovery/resolve.go, discovery branch:
c := matches[0]
if bindErr := contexts.BindCluster(configDir, clusterHost, c.Name); bindErr != nil {
    debugf("auto-bind %s -> %s failed: %v", clusterHost, c.Name, bindErr)   // non-fatal
} else {
    debugf("auto-bound %s -> %s after discovery match on %s", clusterHost, c.Name, coreURL)
}
return c, nil

Its doc point 2: "the first advertised URL with a local context wins, and the cluster→context binding is written so the next call skips discovery."

2. Post-success hook in auth/scoped.go + git-remote-entire/main.go — only after the first successful STS exchange:

1
2
3
4
5
6
7

// main.go
onScopedSuccess := makeScopedSuccessHook(contextName, clusterHost)   // -> contexts.BindCluster
scoped = auth.NewScoped(coreURL, token, parsedURL.Path, nodeCfg.EntryURL, client, onScopedSuccess)

// scoped.go: fires exactly once, "only after we've proven the chosen
// context can authenticate against this cluster — never speculatively."
s.firstSuccess.Do(s.onFirstSuccess)

Note these two contradict each other: resolve.go already binds speculatively on discovery match, so by the time the "never speculative" hook fires, the binding is usually already written (BindCluster is idempotent). The hook is effectively redundant. If we re-introduce persistence we get to pick the cleaner of the two rather than copying both.

Discrepancy table

# Concern Original (entiredb) Ours Impact
1 Persist binding on discovery match resolve.go calls BindCluster inline removed — replaced with "ephemeral; binding not persisted" debug log Every pull re-runs /.well-known; bindings never accumulate (your symptom)
2 Post-success auto-bind ScopedAuth.onFirstSuccess → BindCluster no ScopedAuth wrapper; repocreds.New called directly, no success hook no binding written even after proven auth
3 makeScopedSuccessHook in main present, wired into scoped auth absent —
4 resolve.go doc/intent "binding written so next call skips discovery" "this helper does not persist … correctness choice" our comment actively asserts the opposite behavior
5 ENTIRE_TOKEN env override (CI/workload identity) resolveCredentials handles it via CoreURLFromEnvToken (reads aud) absent — we go straight to MigrateLegacyLoginContext + resolve CI/automation token flow unsupported
6 Token refresh client.GetTokenWithRefresh (refresh token from keyring) auth.LoginTokenForContext — read-only, no refresh; 401 → re-login expired token = hard re-login, no silent refresh

Discrepancies 1–4 are the same root issue (no persistence) and are what you asked about. 5–6 are separate divergences I noticed while tracing — flagging them but they're out of scope for "cache the bindings."

The choice point for fixing #1–4

repocreds.New in our repo takes no success callback, so re-introducing persistence means one of:

Given we already established persistence is safe (login JWT never reaches the cluster host; cluster only gets an audience-pinned token), either is fine. I'd recommend B — it's the cleaner of the original's two mechanisms and avoids writing a binding for a /.well-known that matched but then failed to authenticate.

Either way I'll need to rewrite the resolve.go comment I just committed (it currently asserts non-persistence as a feature). Want me to implement B (and drop the now-wrong comment), or do you prefer A to match the original's resolve.go line-for-line?

Yes let's do B, just the safe/conservative version. Commit early and often.

1mo ago·5m·16 calls

Done. Two commits on 20260602-auth-contexts (not pushed):

What changed (option B, conservative):

Vs. the original: we adopted only the post-success mechanism (entiredb's scoped.go hook), not its speculative inline BindCluster in resolve.go. That's the safer of its two — the original binds on bare discovery match before auth is proven, which can leave a stale binding for a host that matches but can't authenticate.

Verification: fmt + go-lint clean; clusterdiscovery, contexts, and git-remote-entire package tests pass. The existing resolve_test.go invariant "discovery must not persist a cluster binding" still holds, because persistence moved out of resolve into the caller.

Still open (per your note, deferred): the two-views problem (auth list vs auth contexts) — not touched. And the entiredb divergences I flagged but didn't act on: no ENTIRE_TOKEN env override, no token refresh. Say the word if you want either next.

823f034git-remote-entire: cache cluster->context binding after first success\ \ Previously every git op against an entire:// cluster re-ran /.well-known\ discovery, and successful lookups were never persisted. Bind the\ clusterHost->context mapping into contexts.json on the first successful\ scoped-token exchange, so subsequent invocations short-circuit on the\ stored binding and skip discovery.\ \ Conservative (post-success) variant: the bind fires only after the\ cluster has provably authenticated the resolved context, so a host whose\ /.well-known matches a local context but then fails to authenticate\ leaves no stale binding. Resolution itself stays non-persisting;\ makeBindHook (guarded by sync.Once on a freshly-constructed token cache)\ owns the write.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: ff5ccce200c0+57/-18

4f44cc8git-remote-entire: test makeBindHook bind-on-success behavior\ \ Covers the three paths: nil hook for an empty context name, a binding\ persisted after the first call for an existing context, and no binding\ written when the named context is missing (BindCluster fails, logged not\ fatal).\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 9e787e814af7+61

what if i'm logged in as paul@core-us as well as admin@core-us (both bound to cluster1), but have my active context set to paul@unrelated. if i then try ops against cluster1, how is the correct context picked?

1mo ago·1m·2 calls

Traced it through the actual code. Here's the precise answer.

Bottom line

In your scenario the active context (paul@unrelated) is ignored, and the pick is not by activeness — it's first-match-in-on-disk-order, then frozen by the new bind-on-success. With two accounts on the same core and an unrelated active context, there's no signal to disambiguate, so you silently get whichever of paul@core-us / admin@core-us logged in first — and that choice sticks.

First, a correction on framing

A cluster can only be bound to one context — cluster_contexts is host → name (1:1). So "both bound to cluster1" can't literally hold. What's true is both are eligible for cluster1 (both issued by core-us, which cluster1's /.well-known advertises). The binding map holds at most one.

The selection, step by step (resolve.go)

Case A — cluster1 already bound (the common case after the first op):

  1. cluster_contexts["cluster1"] names one context → returned as-is, discovery skipped, active context irrelevant. Whichever got bound first wins, permanently, until you auth unbind or delete that context.

Case B — cluster1 not yet bound (first-ever op, or after unbind): 2. Discovery fetches cluster1's /.well-known → advertised core_urls include core-us.

  1. On the first successful scoped-token exchange, makeBindHook persists cluster1 → matches[0]. Now Case A applies forever.

The footgun

If admin@core-us happens to be matches[0] (e.g. you logged in as admin first), an op against cluster1 from your paul@unrelated shell silently authenticates as admin, with no prompt and no error — and the new bind-on-success freezes that. git-remote-entire can't prompt to disambiguate: stdout is the git pkt-line stream, so there's no interactive channel.

The prefer-current logic only saves you when your active context is itself eligible for the cluster. When it isn't (your exact scenario), it's pure on-disk order.

Note this is no worse than the original — entiredb's resolve.go also takes matches[0] and has no prefer-current tie-break at all. Ours added that, so we're strictly better when the active context is eligible; identical when it isn't.

Recourse / mitigation

That last point is the gap worth closing if this scenario is real for you: a symmetric entire auth bind <host> <context> (the BindCluster plumbing already exists, unused) would let you pin the right account deterministically instead of relying on login order. Want me to add it?

OK, yep, that uncovers a bit of nastiness. I want us to adjust things as follows (let's make a plan and discuss first, don't jump to impl):

we will also rework the way we pick which context to use.

1mo ago·19m·1 call

All four locked in. Here's the finalized plan with your decisions baked in.

Finalized design

Cache file — ~/.cache/entire/cluster_cores.json (reusing the same cache-dir resolution as nodes.json/replicas), separate from contexts.json:

1
2
3
4
5
6

{
  "aws-us-east-2.entire.io": {
    "core_urls": ["https://us.auth.entire.io", "https://eu.auth.entire.io"],
    "fetched_at": "2026-06-02T10:00:00Z"
  }
}

Selection (SelectContext, flat union):

  1. Active context eligible (current.CoreURL ∈ core_urls) → use it.
  2. Else eligible = {ctx : ctx.CoreURL ∈ core_urls}:

Seamless upgrade (the priority):

Removals:BindCluster, File.Resolve, ClusterContexts reads, makeBindHook/onScopedSuccess, entire auth unbind, UnbindCluster, ClusterBindings, printClusterBindings. RemoveAllContexts clears the new cache file instead.

Commit sequence (early & often)

  1. New clustercores cache package (file I/O, TTL, stale-fallback) + unit tests — self-contained, no wiring.
  2. SelectContext + rewrite ResolveContextForCluster to cores-cache + selection; ambiguity error + tests. (resolve behavior changes here.)
  3. Strip persistence from git-remote-entire (makeBindHook/hook removal).
  4. Remove bind/unbind command surface; adjust auth contexts; clear cache in RemoveAllContexts.
  5. Docs (CLAUDE.md strategy/auth notes if any reference bindings).

Two small residuals before I start

Give me the call on those two and a green light, and I'll start with commit 1.

no flush command, user can rm ~/../cluster_cores.json

auth contexts: drop mention of bindings.

1mo ago·18m·66 calls

The redesign is complete on 20260602-auth-contexts (not pushed).

Bottom line

Cluster→context bindings are gone. We now cache only the objective cluster → [core_urls] fact (TTL'd) and recompute which account to use on every operation, failing loudly on genuine ambiguity instead of silently freezing an arbitrary pick.

What landed (4 new commits)

  1. discovery: TTL'd cluster_cores.json — host → {core_urls, fetched_at}, 7d TTL, stale-fallback on fetch failure, in the cache dir beside nodes.json. Shared lock/atomic-IO factored out (no dup).
  2. clusterdiscovery: rewrite resolution — cores from cache (live fetch on miss/expiry) + per-op selection: active context wins if eligible → else sole eligible → else error listing candidates + entire auth use. Dropped makeBindHook.
  3. auth: remove the binding surface — BindCluster, File.Resolve, ClusterBindings, UnbindCluster, entire auth unbind, and the "Cluster bindings" section of entire auth contexts.
  4. Removed an accidentally-committed build artifact + gitignored it.

Your scenario now

paul@core-us + admin@core-us, active paul@unrelated, op against cluster1 → hard error: multiple login contexts can authenticate against cluster cluster1 (admin@core-us, paul@core-us); choose one with 'entire auth use <context>'. No silent wrong-account, no frozen choice.

Seamless upgrade

Verification

Not done (deferred / yours)

d47c63ddiscovery: add TTL'd cluster->cores cache\ \ Adds cluster_cores.json (alongside nodes.json in the cache dir): a\ host -> {core_urls, fetched_at} map memoizing /.well-known discovery,\ with a 7d TTL. Get() reports both freshness and existence so callers can\ re-fetch on expiry but fall back to a stale entry if the cluster is\ briefly unreachable.\ \ Unlike a context binding this stores only the objective cluster->core\ fact, never which local account to use, so a multi-account user is never\ silently pinned to one identity. Safe to delete by hand to force\ re-discovery.\ \ Factors the shared lock/atomic-write/read primitives out of the existing\ node cache so both files reuse them with no duplication; existing cache\ tests guard the refactor.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: efec36727758+274/-33

b75fe5eclusterdiscovery: recompute account per op; cache only cluster->cores\ \ Reworks ResolveContextForCluster around the cluster_cores cache and a\ fresh per-operation account selection, replacing the cluster->context\ binding model:\ - Cluster cores come from cluster_cores.json (TTL'd, stale-fallback),\ not a persisted account choice.\ - Account selection: active context wins if its core fronts the cluster;\ else the sole eligible context; else (>1 eligible, none active) a hard\ error listing the candidates and pointing at `entire auth use`. No more\ silent on-disk-order pick, which could authenticate as the wrong\ account and then freeze that choice.\ \ Drops the post-success bind hook from git-remote-entire (makeBindHook)\ and adds a cacheDir argument to ResolveContextForCluster. The binding\ APIs in contexts/ and the auth command surface are removed in a\ follow-up.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 32bbd6549652+282/-291

ClusterContexts field kept but inert

can we drop that too? or will an old config fail to parse?

1mo ago·6m·29 calls

Both done. Bottom line:

Verified: go build ./..., full lint (0 issues), and contexts/auth/cli package tests all pass. Committed as 478c2bb26 (not pushed).

i pushed the branch. let's draft a PR description before opening a PR. the reviewers tend to be very worried about making the UX worse, so let's focus on 1) what issue(s) existed, 2) how the 99% happy path will be totally unaffected. we have decreased wrong auto-assumptions in our code and will surface doubts to the user, instead of guessing.

1mo ago·54.0s·1 call

Here's a draft. It leads with the UX-safety framing the reviewers care about.


Rework cluster→context resolution: cache infra facts, stop guessing accounts

The problem

Authenticating an entire:// git op requires picking which local login context to use for the target cluster. The old code did two things that could bite multi-account users and confuse everyone else:

  1. It could silently authenticate as the wrong account. When more than one local context was eligible for a cluster (e.g. alice@core and admin@core) and your active context was something unrelated, resolution fell back to "first match in on-disk/login order." So an op could quietly run as admin@core with no prompt and no error — and then freeze that arbitrary choice by persisting it as a cluster→context binding.
  2. It conflated two unrelated facts. A binding mixed which control plane fronts a cluster (objective, near-static infra) with which of my accounts to use (a per-user choice). That made the persisted state asymmetric and confusing — e.g. one cluster recorded, another not, even after using both — and it's what froze the wrong-account pick above.
  3. Deleting your active context silently switched identity.Delete advanced current_context to whatever remained, so removing a login could quietly re-authenticate you as someone else.

What changed

Net: fewer wrong auto-assumptions; where the old code guessed, we now either do the one unambiguous thing or surface the doubt to the user.

The 99% happy path is unchanged

Also in this branch (separate, small)


Want me to open it with gh pr create (and if so, against main)? I can also split the dial-timeout/comment commits into a separate PR if you'd rather keep this one purely about the context rework — though they're already pushed together.

try again but waaaaaaay more succinct. drop problem 3, and tone down the alarm in problem 1

1mo ago·14.6s

Cache cluster→core mappings; pick the login context fresh per op

Problem

Resolving which login context to use for an entire:// cluster mixed two concerns: which control plane fronts a cluster (stable infra) and which of your accounts to use (per-user). When several contexts were eligible and none was active, it picked one by save order and persisted that guess as a binding — so the choice was somewhat arbitrary, and the stored state ended up asymmetric and confusing.

Change

99% happy path: unchanged

Also in this branch

Dial timeout 500ms→2s (cold-connect headroom; override via ENTIRE_CONNECT_TIMEOUT_SECONDS) and a resolver comment fix.


Open it with gh pr create against main?

5a4163fauth: remove cluster-binding model and command surface\ \ Removes the cluster→context binding now that account selection is\ recomputed per operation:\ - contexts: drop File.Resolve and BindCluster. The ClusterContexts field\ is kept but inert (deprecated) so an existing contexts.json round-trips\ without data loss during the transition; Delete still prunes it and a\ full logout still clears it.\ - auth: drop ClusterBindings/UnbindCluster, the `entire auth unbind`\ command, and the "Cluster bindings" section of `entire auth contexts`.\ \ Tests updated accordingly; RemoveAllContexts still clears the legacy\ field (seeded directly now that BindCluster is gone).\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 1a18bfd708fc+24/-346

8f46c95contexts: drop the inert ClusterContexts field entirely\ \ json.Unmarshal ignores unknown keys, so an existing contexts.json with a\ "cluster_contexts" entry still loads cleanly — the obsolete key is simply\ dropped on the next write. With nothing reading or writing it, removing\ the field (and the last references in Delete and RemoveAllContexts) is\ safe and leaves no dead weight.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 4f015a69dc6b+21/-50

e26a233contexts: clear current_context on delete; drop dead comment\ - Delete now always clears current_context when the deleted context was\ active, instead of advancing to the first remaining context. Deleting\ your active login must never silently switch you to another identity.\ - Remove the leftover "legacy cluster_contexts used to live here" note;\ comments describe today's behavior only.\ \ RemoveCurrentContext relies on Delete's clear and drops its redundant\ reassignment.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 49c99a4a7077+14/-20

fix this guy Running [/home/runner/golangci-lint-2.11.3-linux-amd64/golangci-lint run] in [/home/runner/work/cli/cli] ... Error: internal/entireclient/discovery/cache.go:133:1: readCacheFile returns generic interface (T) of type param any (ireturn) func readCacheFile[T any](path string, newEmpty func() T) (T, error) { ^ 1 issues:

Error: issues found Ran golangci-lint in 62002ms

1mo ago·3m·7 calls

Fixed and committed (0feecb4b0). Push it and CI should go green.

Bottom line:

ok push.

1mo ago·5m·4 calls

Pushed — 7b3a2bbf0..e67d7e189 on 20260602-auth-contexts.

CI should now run with the ireturn fix in place.

e67d7e1discovery: fix ireturn on the shared cache reader\ \ CI's golangci-lint flagged ireturn on the generic readCacheFile, which\ returned its type parameter T. (Locally the nolint directive that\ silenced it kept getting stripped by `golangci-lint run --fix`'s\ nolintlint autofix, so it only surfaced in CI.)\ \ Replace it with loadCacheFile(path, *T, newEmpty) error: it fills a\ destination pointer and returns error, which ireturn allows, while still\ sharing the read/unmarshal/corrupt-reset logic across both caches.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 6d6f3baefcd9+18/-13

o hey remind me right at the beginning we also flagged other gaps with this entireio/cli auth mechanism.

1mo ago·20.5s

Yep — two divergences from the original entiredb git-remote-entire that I flagged while tracing, then explicitly left out of scope for the cluster-context rework:

  1. No ENTIRE_TOKEN env override. The original resolveCredentials honors ENTIRE_TOKEN (CI / workload-identity), deriving the core URL from the JWT's aud claim via CoreURLFromEnvToken — skipping contexts.json/keyring entirely. Ours goes straight to context resolution, so the CI/automation bearer-token flow isn't supported.

  2. No token refresh. The original mints via client.GetTokenWithRefresh (refresh token from the keyring). Ours uses auth.LoginTokenForContext — a read-only fetch, no refresh; an expired token just surfaces as a 401 → hard re-login, no silent refresh.

Both are independent of the binding/selection work we just shipped. Want me to scope either of them next? #1 (ENTIRE_TOKEN) is the smaller, self-contained one and the more likely to bite CI users.

write a small handover for 1 & 2 here, inline, and i'll get someone else to do it.

1mo ago·47.7s

Handover: two remaining git-remote-entire auth gaps

Both are independent of the cluster-context rework just merged. The original entiredb git-remote-entire is the reference implementation for both — clone-compatible spec lives in ~/src/entirehq/entiredb.


Gap 1 — ENTIRE_TOKEN env override (CI / workload identity)

What: Let ENTIRE_TOKEN=<jwt> bypass contexts.json/keyring entirely, so CI and workload-identity runners authenticate without an interactive login.

Reference (entiredb):

Where in our repo (cli):

Acceptance:

Size: small, self-contained. Do this one first — most likely to bite CI users.


Gap 2 — silent token refresh

What: When the stored access token is expired, refresh it via a stored refresh token instead of failing the op and forcing a re-login.

Reference (entiredb):

Where in our repo (cli):

⚠️ Dependency / why this is bigger than Gap 1: our device-flow login currently doesn't obtain or persist a refresh token. So this is two pieces:

  1. Login side — login.go / the device-flow exchange must request + store a refresh token (and client_id) in the keyring alongside the access token.
  2. Use side — replace the static LoginTokenForContext provider with one that refreshes when the access token is within/past expiry, persisting the rotated token.

Acceptance:

Size: medium; touches login + the provider. Scope the login-side refresh-token persistence first — without it, the use-side has nothing to refresh from.


Both reference the entiredb originals verbatim, so whoever picks these up can diff our git-remote-entire/main.go against ~/src/entirehq/entiredb/cmd/git-remote-entire/main.go for the exact shape.

is it reasonable to have refresh tokens for device auth flow?

1mo ago·31.9s

Yes — it's standard and reasonable.

Bottom line: The OAuth 2.0 Device Authorization Grant (RFC 8628) issues tokens via a normal token-endpoint response, which can include a refresh_token just like any other grant. Long-lived CLI/device sessions are exactly the use case device flow exists for — re-running the browser dance on every token expiry is the thing you're trying to avoid.

It's the norm, not an exception:

The real preconditions (worth confirming, not blockers):

  1. Server issues them — typically gated on the offline_access scope in the device-auth request. entiredb's path implies it's already enabled.
  2. Public client — the CLI has no client secret. Refresh tokens for public clients are standard (PKCE/device flow), but best practice is refresh-token rotation with reuse detection: each refresh returns a new refresh token and invalidates the old one, so a leaked-and-replayed token is caught. Check whether entire-core rotates.
  3. At-rest storage — they live in the OS keyring, same place the access token already sits. No new storage surface.

So for Gap 2, the design is sound; the work is (a) add offline_access to the device-auth request and persist the returned refresh token, (b) use it. The only thing to verify with whoever owns entire-core is whether rotation is on — if it is, the use-side must persist the rotated token every refresh (already in the Gap 2 acceptance criteria).

One nuance to flag in the handover: if entire-core rotates refresh tokens, concurrent git operations (e.g. git fetch --recurse-submodules spawning several git-remote-entire processes) can race on the refresh — two processes refresh with the same token, one rotation wins, the other's refresh token is now invalid. The repocreds/keyring layer would need single-flight or a retry on refresh-token-rejected. Worth a line so it isn't discovered the hard way.

Want me to fold that refresh-token-rotation + concurrency note into the Gap 2 handover above?

sounds like you're across it. please implement the necessary bits for "gap 2" on a new, fresh branch of entireio/cli and entirehq/entiredb

1mo ago·24m·53 calls

Done. Gap 2 is implemented on a fresh branch 20260602-token-refresh (committed c508f33e2, not pushed).

Key finding that reshaped the task

No entiredb/server work was needed — so I made no branch there. entire-core already issues refresh tokens on the device grant (gated on offline_access), enables the refresh_token grant for the entire-cli public client, and runs rotating single-use refresh families with reuse detection + family revocation. All the work was client-side in entireio/cli, which was throwing the refresh token away.

What I built

On your "what would it take for tokenmanager" question

It turned out to be the smaller, safer option: Manager.Refresh already bundles the cross-process file lock + in-process single-flight + rotation persistence + re-auth detection — exactly the concurrency hazard (racing submodule fetches replaying a single-use token → family revoked) I'd otherwise have hand-rolled. The only genuinely new code is the adapter.

UX safety (same framing your reviewers care about)

Strict superset of today: a still-valid token returns with no network call; a pre-change login with no refresh token behaves exactly as before (valid→used, expired→re-login). Only difference: new logins now refresh silently instead of forcing re-auth.

Verified

fmt + lint:go clean (0 issues); new tests cover the store round-trip, validation, the no-network valid path, expired-no-refresh, and the full refresh+rotation against a stub token endpoint; entire repo unit suite passes.

Want me to push this branch and draft its PR description too?

did you inspect whta's in ~/src/entirehq/entiredb? are we diverging much? is ours better?

1mo ago·3m·1 call

Read entiredb's full refresh path (client/auth_interceptor.go). Here's the head-to-head.

Bottom line

Yes, we diverge — and ours is safer on the thing that matters most. entiredb hand-rolls refresh in its client package with no locking; ours uses auth-go's tokenmanager, which serialises refreshes across processes and goroutines. Given the server does single-use rotation + reuse-detection + family revocation, entiredb's version has a latent footgun that ours doesn't.

Where ours is better

entiredb (GetTokenWithRefresh) ours (tokenmanager)
Concurrent refresh None.RefreshAccessToken reads→POSTs→writes with no lock. Two parallel git-remote-entire procs (recursive submodule fetch) both POST the same single-use refresh token → one wins, the rest get invalid_grant → server revokes the whole family → forced re-login. Cross-process file lock + in-process single-flight + re-read after locking, so a late waiter reuses the peer's freshly minted token. No reuse, no revocation.
Rotation race On invalid_grant it gives up (ErrSessionExpired). Re-reads the store; if the RT was rotated under it, retries once with the new token before declaring the family dead.
Code ownership Bespoke refresh reimplemented in client/ (its own refreshViaCore, expiry math, etc.). Uses the shared, tested auth-go library. Less bespoke auth code to maintain. Ironically entiredb is the one diverging from auth-go here.

The concurrency gap is the real finding: entiredb's git-remote-entire calls GetTokenWithRefresh once at startup (so it's safe within a process), but cross-process it's exposed — exactly the submodule-fetch scenario. Worth flagging for a backport to entiredb (swap its hand-rolled path for tokenmanager, or add a lock).

Where entiredb is nicer (small, cheap to adopt)

  1. Transient-failure leniency. On a network/5xx blip mid-refresh, entiredb returns the existing token and lets the data call try (warns to stderr); ours returns the refresh error. For an expiring-soon-but-still-valid token this avoids a spurious failure. (For an already-expired token it's moot — the stale token 401s anyway.)
  2. entr_ prefix guard. entiredb checks the refresh token looks core-issued before POSTing, short-circuiting stale non-core tokens locally instead of via a server invalid_grant. Minor — ours just gets the server rejection.
  3. Better re-login hint. entiredb says entire-core auth login --base-url <core> (copy-pasteable, core-specific); ours says run entire login. We could cheaply name the core in our error.

Net

Ours is safer (concurrency/rotation) and more maintainable (shared lib); entiredb has the latent reuse-detection footgun. The two divergences worth acting on: (a) consider backporting tokenmanager to entiredb to close its concurrency gap, and (b) optionally adopt entiredb's nicer re-login hint in ours. Want me to add the core name to our re-login error (cheap), and/or note the entiredb backport in a handover?

yes i want the relogin hint.

1mo ago·2m·5 calls

Done — committed 515c7c5e28 on 20260602-token-refresh.

The refresh re-login errors now name the core:

Uses the CLI's own idiom (ENTIRE_AUTH_BASE_URL=<core> entire login, matching RenderLoginHint) rather than entiredb's --base-url flag, since that's how this CLI targets a specific core. Test now asserts the hint names the core and the login command. fmt + lint clean, auth tests pass.

Not pushed. Want me to push 20260602-token-refresh and draft its PR description?

ok so.. what have we done here. auto-refresh of login JWT right? No difference from the original ~/src/entirehq/entiredb/cmd/... right?

1mo ago·32.8s

Short answer: same goal, not the same code — and ours is deliberately not a carbon copy.

  1. Concurrency. entiredb's GetTokenWithRefresh has no lock. Ours goes through auth-go tokenmanager (cross-process file lock + single-flight + rotation-race retry). Under parallel git-remote-entire processes (recursive submodule fetch), entiredb's can replay a single-use refresh token and get the family revoked; ours can't. This is a real, intentional improvement — not a stylistic one.
  2. The login side is genuinely new here. entiredb's git-remote-entire consumes refresh tokens that its operator login (entire-core auth login, loopback PKCE) persisted. The entire-cli device-flow login — which lives in our repo — never requested offline_access or stored a refresh token at all. So we didn't mirror entiredb there; we filled a gap entiredb's own flow didn't have to.

So: functionally equivalent refresh behavior, reached via the shared library instead of a hand-roll, plus the device-flow login plumbing that only our repo needed. If you want true parity you'd actually go the other way — backport tokenmanager into entiredb to kill its unlocked-refresh footgun (the handover item I flagged).

ok push and draft PR

1mo ago

actually wait. git fetch --all and rebase on origin/main, then push and draft PR

1mo ago·13m·10 calls

Pushed 20260602-token-refresh → origin. Here's the PR draft:


Silent login-token refresh for entire:// git operations

What this does

git fetch/push/clone against entire:// clusters no longer break when your login token expires — the helper silently re-mints it from a stored refresh token instead of erroring out with "please log in again."

Why

Today the login JWT is fetched read-only: once it expires, every git op fails until you re-run entire login. We now request offline_access at login, persist the refresh token, and refresh on demand in git-remote-entire.

Happy path is unchanged

Why it's safe under concurrency

entire-core issues single-use refresh tokens with reuse-detection (replay → whole family revoked). A naive refresh would let two parallel helper processes (e.g. git fetch --recurse-submodules) replay the same token and lock you out. We avoid that by going through auth-go's tokenmanager: cross-process file lock + in-process single-flight + re-read-after-lock + rotated-token persistence. A thin per-context tokenstore.Store adapter maps our keyring slots onto it.

Server

No server changes — entire-core already supports offline_access on the device grant, the refresh_token grant for the entire-cli client, and rotating families.

Tests

Store round-trip, validation, the no-network valid-token path, expired-without-refresh (re-login error names the core), and the full refresh+rotation against a stub token endpoint. fmt + lint clean; full unit suite passes.

Follow-up (not in this PR)

entiredb's git-remote-entire hand-rolls refresh without locking — same reuse-detection footgun. Worth backporting tokenmanager there.


Want me to open it with gh pr create against main?

5c87370auth: silent login-token refresh for git-remote-entire\ \ Device-flow login now requests offline_access, captures the returned\ refresh token through the poll plumbing, and persists it to the paired\ `:refresh` keyring slot. git-remote-entire's login-JWT provider\ is now refresh-aware: an expired login JWT is re-minted from the refresh\ token instead of failing the operation with a re-login.\ \ The provider is backed by auth-go's tokenmanager, which serialises\ refreshes across processes (advisory file lock) and goroutines and\ persists the rotated refresh token. That matters because the server\ issues single-use refresh tokens with reuse detection + family\ revocation: two concurrent git-remote-entire processes (e.g. a recursive\ submodule fetch) racing a naive refresh would replay the same token and\ get the whole family revoked. A thin per-context tokenstore.Store\ adapter maps the keyring slots onto tokenmanager.\ \ Behaviour is a strict superset of before: a still-valid token is returned\ with no network call, and a context with no refresh token (a login\ predating this change) behaves exactly as it did — valid token used,\ expired token surfaces a re-login error.\ \ Server-side support already existed in entire-core (offline_access on the\ device grant, refresh_token grant for the public client, rotating\ single-use families), so no server changes are required.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 0192d919e856+423/-58

ac1c226auth: name the core in the refresh re-login hint\ \ When a refresh fails (expired/revoked family, or no refresh token), the\ error now points at the specific core — `ENTIRE_AUTH_BASE_URL=\ entire login` — so a multi-core user re-authenticates against the right\ one. Matches clusterdiscovery.RenderLoginHint's idiom.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 22da7112c436+13/-3

do we refresh when we see that the JWTs exp is <1 min away, or do we only refresh on 400 error/

1mo ago·42.8s·1 call

Proactive, off the JWT exp — and the window is 30 seconds, not 1 minute. No 4xx-triggered refresh of the login JWT.

The check (tokenmanager.coreTokenExpired):

1

return !now.Add(exchangeSkew).Before(claims.ExpiresAt)   // exchangeSkew = 30s

So we re-mint when now + 30s >= exp (within 30s of expiry, or already past). Also refreshes if the token's nbf is in the future (clock-skew guard). Opaque non-JWT tokens have no visible expiry → treated as live → never proactively refreshed.

Reactive path: none for the login JWT. The only reactive bit is one layer down — repocreds invalidates its repo-scoped token cache on a data-plane 401 and re-exchanges; that re-consults the login-JWT provider, but the provider still only refreshes on the same 30s exp test, not on the error itself.

Why 30s is enough here (vs entiredb's 5-min buffer): the login JWT is only consumed in quick bursts to mint repo-scoped tokens at the start of an op — it isn't held across a long transfer. Once minted, the repo-scoped token (its own TTL) carries the packfile, and repocreds re-exchanges mid-transfer if needed. So a tight margin doesn't risk a clone dying halfway.

Worth noting the divergence: entiredb refreshes 5 minutes early (IsTokenExpiredOrExpiring); we refresh at 30s. Our SaveTokens still writes the |expiry stamp that the CLI's other readers (LoginTokenForContext, which uses the 5-min check) rely on — but the git-remote-entire refresh path itself is the 30s JWT-exp test. If you'd rather match entiredb's 5-min proactive window, that's a config knob we'd have to add (tokenmanager hardcodes exchangeSkew), so it'd mean either a small auth-go change or a pre-check wrapper. Want me to align it to a wider margin?

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:

Execution mode rules:

Argument handling:

Foreground flow:

1

node "/Users/paul/.claude/plugins/cache/openai-codex/codex/1.0.4/scripts/codex-companion.mjs" adversarial-review ""

Background flow:

1
2
3
4
5

Bash({
  command: `node "/Users/paul/.claude/plugins/cache/openai-codex/codex/1.0.4/scripts/codex-companion.mjs" adversarial-review ""`,
  description: "Codex adversarial review",
  run_in_background: true
})

1mo ago·43.3s·3 calls

Codex adversarial review started in the background. Check /codex:status for progress.

Codex Status

Session runtime: direct startup Review gate: disabled

No jobs recorded yet.

If the user did not pass a job ID:

If the user did pass a job ID:

1mo ago·20.0s·1 call

Job ID Kind Status Phase Elapsed Summary Follow-up
b248fos53 adversarial-review 🟡 running exploring (reading auth/refresh/tokenmanager sources) ~in-flight No verdict yet — Codex is still inspecting files (~9.4KB progress) Re-run /codex:status shortly; I'll surface the verbatim review when it completes

Still working — it's read the login/auth/contexts/tokenmanager/provider files and is mapping CurrentProvider/client config. No findings emitted yet. I'll relay Codex's output verbatim once it finishes.

b248fos53 REDACTED /private/tmp/claude-501/-Users-paul-src-entireio-cli/f386aebe-9c1f-4bb2-8132-2e548c224cec/tasks/b248fos53.output completed Background command "Codex adversarial review" completed (exit code 0)

1mo ago·32.2s·2 calls

Codex adversarial review (verbatim):


Codex Adversarial Review

Target: branch diff against main Verdict: needs-attention

No-ship: the refresh-token change introduces a loopback/dev auth regression, leaves refresh bearers behind on logout, and persists rotated credentials non-atomically in a way that can strand or revoke sessions after partial failures.

Findings:

Next steps:

as for the loopback HTTP thing - in practice our dev setup uses mkcert with https, so we should be fine. have we touched any skiptls config? hopefully not.

1mo ago·39.9s·1 call

Confirmed — we did not touch any skipTLS config or its semantics.

The diff vs origin/main shows exactly one new line referencing it:

+ loginProvider, err := auth.NewRefreshingLoginProvider(clusterCtx, httpClient.Transport, skipTLS)

Everything else is untouched:

So it's purely additive: we reused the existing skipTLS value as the new provider's allowInsecureHTTP gate. No change to certificate verification behavior.

And your mkcert point is right — the finding is moot for your dev setup.tokenmanager.New only requires AllowInsecureHTTP=true for an http:// issuer; an https:// core (mkcert) passes validation regardless of that flag. So with mkcert you have skipTLS=false and an https core → login/refresh constructs fine. Codex's regression only bites a plain http://127.0.0.1 core, which you don't use.

Worth being precise though: it's a real (if narrow) behavior change for anyone on a non-TLS loopback core — they'd now need ENTIRE_TLS_SKIP_VERIFY=true to get refresh working, where before plain-http auth "just worked." Cheap to neutralize later (auto-allow isLoopbackHTTP(coreURL) independent of the TLS-skip env) if you ever want to keep that path open, but not a blocker for your setup.

The other two Codex findings are independent of TLS and still stand: logout leaving the :refresh slot behind, and the non-atomic access-then-refresh write. Want me to act on either of those?

5e3ab86auth: silent login-token refresh for git-remote-entire\ \ Device-flow login now requests offline_access, captures the returned\ refresh token through the poll plumbing, and persists it to the paired\ `:refresh` keyring slot. git-remote-entire's login-JWT provider\ is now refresh-aware: an expired login JWT is re-minted from the refresh\ token instead of failing the operation with a re-login.\ \ The provider is backed by auth-go's tokenmanager, which serialises\ refreshes across processes (advisory file lock) and goroutines and\ persists the rotated refresh token. That matters because the server\ issues single-use refresh tokens with reuse detection + family\ revocation: two concurrent git-remote-entire processes (e.g. a recursive\ submodule fetch) racing a naive refresh would replay the same token and\ get the whole family revoked. A thin per-context tokenstore.Store\ adapter maps the keyring slots onto tokenmanager.\ \ Behaviour is a strict superset of before: a still-valid token is returned\ with no network call, and a context with no refresh token (a login\ predating this change) behaves exactly as it did — valid token used,\ expired token surfaces a re-login error.\ \ Server-side support already existed in entire-core (offline_access on the\ device grant, refresh_token grant for the public client, rotating\ single-use families), so no server changes are required.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 0192d919e856+424/-61

d465218auth: name the core in the refresh re-login hint\ \ When a refresh fails (expired/revoked family, or no refresh token), the\ error now points at the specific core — `ENTIRE_AUTH_BASE_URL=\ entire login` — so a multi-core user re-authenticates against the right\ one. Matches clusterdiscovery.RenderLoginHint's idiom.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 22da7112c436+13/-3

22b1052auth: scrub refresh-token slot on logout\ \ RemoveCurrentContext and RemoveAllContexts deleted only the access-token\ keychain slot, leaving the long-lived refresh token behind. After logout\ any keyring-capable process could still mint fresh access tokens from it.\ Delete both the access slot and its paired :refresh slot.\ \ Adds tokenstore.RefreshService() so the ':refresh' suffix lives in one\ place, and extends the logout tests to assert both slots are gone.\ \ Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com\ Entire-Checkpoint: 660b42be1060+50/-7