fix(onboarding): scope the mirror-probe cache to the auth identity · Entire
fix(onboarding): scope the mirror-probe cache to the auth identity
c698a05·
peyton-alt·yesterday·5 files·+57 added/-10 removed
The probe cache keyed entries by owner/repo alone, but probeRepoMirrored consults the active context's core — so after 'entire auth use' the mirror rung could serve the previous identity's cached answer (mirrored or missing) for the rest of the 15-minute TTL. Cache keys now carry an identity scope: the active context name, or a digest of ENTIRE_TOKEN for env-token sessions.
Trail-Finding: 019f6b46-0118-7dea-b2bb-4eb947d47fc5
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Sessions
01KXNPA9ADJATTHRGKVXC4P44XView transcript
[?
Fix Onboarding History and Scan Cache IssuesClaude Code·Fable 5·9 steps](/content/gh/entireio/cli/session/4c8ea8be-3447-426e-b8cb-07f2c8a04b71#timeline-01KXNPA9ADJATTHRGKVXC4P44X/index.html)
Changes
5
cmd/entire/cli
Monboarding_mirror_cache.go+25/-3
Monboarding_rungs.go+4/-4
Monboarding_rungs_test.go+25
Mrepo_mirror.go+1/-1
Mrepo_mirror_test.go+2/-2
1
2
3
4
5
6
7
8
9
10
11
12
13
9 unmodified lines
23
24
25
22
23
24
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package cli
import (
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"time"
"github.com/entireio/cli/cmd/entire/cli/auth"
"github.com/entireio/cli/internal/entireclient/userdirs"
)
9 unmodified lines
// back online.
const mirrorProbeFailureTTL = 5 * time.Minute
// mirrorProbeCache is a best-effort per-user cache of mirror-probe results
// keyed by "owner/repo", on the shared jsonFileCache shell. The ground truth
// stays the control plane.
// mirrorProbeCache is a best-effort per-user cache of mirror-probe results,
// keyed by mirrorProbeKey (auth identity + "owner/repo"), on the shared
// jsonFileCache shell. The ground truth stays the control plane.
type mirrorProbeCache struct {
path string
ttl time.Duration
failureTTL time.Duration
}
// mirrorProbeKey scopes a probe-cache entry to the auth identity the probe
// runs under. probeRepoMirrored consults the *active context's* core, so the
// answer is identity-dependent: after `entire auth use`, a result cached
// under the previous context could show the wrong identity's mirror state for
// the rest of the TTL. ENTIRE_TOKEN sessions are scoped by a token digest — a
// changed token is a changed identity, and parsing the aud claim would cost
// more than it buys.
func mirrorProbeKey(slug string) string {
if tok := os.Getenv(auth.EnvTokenVar); tok != "" {
sum := sha256.Sum256([]byte(tok))
return "env-" + hex.EncodeToString(sum[:4]) + "|" + slug
}
if _, current, err := auth.Contexts(); err == nil && current != "" {
return "ctx-" + current + "|" + slug
}
return "ctx-none|" + slug
}
func defaultMirrorProbeCache() mirrorProbeCache {
return mirrorProbeCache{
path: filepath.Join(userdirs.Cache(), "onboarding_mirror.json"),
Mcmd/entire/cli/onboarding_mirror_cache.go+25/-3
185 unmodified lines
186
187
188
189
189
190
191
192
192
193
194
195
10 unmodified lines
206
207
208
209
209
210
211
212
212
213
214
215
185 unmodified lines
// terminal doesn't hang on every invocation. Owner/repo arrive lowercased
// from the mirror rung.
func probeRepoMirrored(ctx context.Context, owner, repo string) (mirrorProbeResult, error) {
slug := owner + "/" + repo
key := mirrorProbeKey(owner + "/" + repo)
cache := defaultMirrorProbeCache()
now := time.Now()
if probe, unreachable, ok := cache.get(slug, now); ok {
if probe, unreachable, ok := cache.get(key, now); ok {
if unreachable {
return mirrorProbeResult{}, errors.New("control plane unreachable (cached)")
}
10 unmodified lines
}
probe, err := probeMirrorAcross(ctx, clients, owner, repo)
if err != nil {
cache.putUnreachable(slug, now)
cache.putUnreachable(key, now)
return mirrorProbeResult{}, err
}
cache.put(slug, probe, now)
cache.put(key, probe, now)
return probe, nil
}
Mcmd/entire/cli/onboarding_rungs.go+4/-4
757 unmodified lines
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
757 unmodified lines
}
}
// The probe consults the active context's core, so a cached answer is only
// valid for the identity that produced it: `entire auth use` must change the
// cache key, or the mirror rung serves the previous org's mirror state for
// the rest of the TTL. Not parallel: mutates ENTIRE_TOKEN and the config dir.
func TestMirrorProbeKey_ScopedByAuthIdentity(t *testing.T) {
t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir()) // no contexts configured
base := mirrorProbeKey(testOwnerAcme + "/" + testRepoAPI)
if !strings.HasPrefix(base, "ctx-none|") {
t.Errorf("key with no login = %q, want ctx-none scope", base)
}
t.Setenv("ENTIRE_TOKEN", "token-a")
keyA := mirrorProbeKey(testOwnerAcme + "/" + testRepoAPI)
t.Setenv("ENTIRE_TOKEN", "token-b")
keyB := mirrorProbeKey(testOwnerAcme + "/" + testRepoAPI)
if keyA == base || keyA == keyB {
t.Errorf("env-token keys must differ per identity: base=%q a=%q b=%q", base, keyA, keyB)
}
if !strings.HasSuffix(keyA, "|"+testOwnerAcme+"/"+testRepoAPI) {
t.Errorf("key %q should end with the repo slug", keyA)
}
}
func TestImportScanCache_HitRequiresMatchingFingerprint(t *testing.T) {
t.Parallel()
cache := importScanCache{path: filepath.Join(t.TempDir(), "imports.json")}
Mcmd/entire/cli/onboarding_rungs_test.go+25
463 unmodified lines
464
465
466
467
467
468
469
470
463 unmodified lines
// flag, plus the GetMirror read for existing empty-upstream placements).
func healMirrorProbeCache(owner, repo string) {
slugOwner, slugRepo := githubSlug(owner, repo)
defaultMirrorProbeCache().put(slugOwner+"/"+slugRepo, mirrorProbeResult{Mirrored: true}, time.Now())
defaultMirrorProbeCache().put(mirrorProbeKey(slugOwner+"/"+slugRepo), mirrorProbeResult{Mirrored: true}, time.Now())
}
// reportOneShotMirror renders the human output for `repo mirror create
Mcmd/entire/cli/repo_mirror.go+1/-1
274 unmodified lines
275
276
277
278
278
279
280
281
1 unmodified line
283
284
285
286
286
287
288
289
274 unmodified lines
outcome, err := createAndAwaitMirror(ctx, c, "sus", "r", "c", false, time.Second, nil, nil)
require.ErrorIs(t, err, errMirrorSuspended)
require.Equal(t, coreapi.MirrorStatusSuspended, outcome.status)
_, _, ok := defaultMirrorProbeCache().get("sus/r", time.Now())
_, _, ok := defaultMirrorProbeCache().get(mirrorProbeKey("sus/r"), time.Now())
require.False(t, ok, "suspended empty placement must not be written through to the probe cache")
})
1 unmodified line
c := serve(t, coreapi.MirrorStatusReady)
_, err := createAndAwaitMirror(ctx, c, "ok", "r", "c", false, time.Second, nil, nil)
require.NoError(t, err)
probe, unreachable, ok := defaultMirrorProbeCache().get("ok/r", time.Now())
probe, unreachable, ok := defaultMirrorProbeCache().get(mirrorProbeKey("ok/r"), time.Now())
require.True(t, ok, "serving placement should be cached")
require.False(t, unreachable)
require.True(t, probe.Mirrored)
Mcmd/entire/cli/repo_mirror_test.go+2/-2