From 15d0ca9db54963c130b8f9d075b68b5625f0112d Mon Sep 17 00:00:00 2001 From: taibaran Date: Sat, 16 May 2026 10:45:34 +0300 Subject: [PATCH 1/4] fix(grok): treat expired credentials as missing in fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the Codex P1 review on #965 (`cbd30a4e`). Grok session tokens expire after ~7 days. The auth-error suppression in `GrokStatusProbe.fetch` previously only checked `credentials == nil`, so an `auth.json` whose `expires_at` was already in the past still counted as "renderable" and masked the RPC auth failure. The probe would return a successful snapshot carrying stale identity and no `grok login` hint while billing silently 401s — users had to inspect logs to figure out they were logged out. Treat expired records the same as missing credentials when deciding whether to swallow the RPC error, and drop them from the rendered snapshot so the UI doesn't surface a stale email/team for an inactive session. Adds a unit test covering past/future/missing `expires_at`. --- .../Providers/Grok/GrokStatusProbe.swift | 14 +++++-- Tests/CodexBarTests/GrokAuthTests.swift | 39 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift index 12708379d..ca7715e7f 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift @@ -100,18 +100,24 @@ public struct GrokStatusProbe: Sendable { let localSummary = GrokLocalSessionScanner.summarize(env: env) let cliVersion = Self.detectVersion(env: env) + // Grok session tokens expire after ~7 days. An expired record on disk + // must be treated like a missing credential when deciding whether to + // mask the RPC auth error: otherwise we render a snapshot with stale + // identity and no `grok login` hint while billing silently 401s. + let activeCredentials = credentials.flatMap { $0.isExpired ? nil : $0 } + // `localSummary` is *not* currently projected into a visible RateWindow or // identity field, so a stale `~/.grok/sessions/` directory must not // suppress the auth-required hint. Only swallow the RPC error when we - // actually have something rendrable for the user — credentials or a - // billing response. - if billing == nil, credentials == nil { + // actually have something rendrable for the user — fresh credentials + // or a billing response. + if billing == nil, activeCredentials == nil { throw rpcError ?? GrokRPCError.notAuthenticated } return GrokUsageSnapshot( billing: billing, - credentials: credentials, + credentials: activeCredentials, localSummary: localSummary, cliVersion: cliVersion, updatedAt: Date()) diff --git a/Tests/CodexBarTests/GrokAuthTests.swift b/Tests/CodexBarTests/GrokAuthTests.swift index c9d23a1d5..32978ada3 100644 --- a/Tests/CodexBarTests/GrokAuthTests.swift +++ b/Tests/CodexBarTests/GrokAuthTests.swift @@ -71,6 +71,45 @@ struct GrokAuthTests { } } + @Test + func `isExpired reflects past expires_at`() throws { + // Past expiry + let pastJson = #""" + { + "https://auth.x.ai::client": { + "key": "stale-token", + "expires_at": "2020-01-01T00:00:00Z" + } + } + """# + let past = try GrokCredentialsStore.parse(data: Data(pastJson.utf8)) + #expect(past.isExpired == true) + + // Future expiry + let futureJson = #""" + { + "https://auth.x.ai::client": { + "key": "fresh-token", + "expires_at": "2099-01-01T00:00:00Z" + } + } + """# + let future = try GrokCredentialsStore.parse(data: Data(futureJson.utf8)) + #expect(future.isExpired == false) + + // Missing expires_at — treated as non-expired so we never spuriously lock + // out clients whose auth.json shape predates this field. + let noExpiryJson = #""" + { + "https://auth.x.ai::client": { + "key": "ageless-token" + } + } + """# + let noExpiry = try GrokCredentialsStore.parse(data: Data(noExpiryJson.utf8)) + #expect(noExpiry.isExpired == false) + } + @Test func `falls back to legacy when OIDC entry has no key`() throws { // A stale/partial OIDC record must not shadow a healthy legacy session. From 6458552520243e0bd8e2d8e017be5f996852f7f9 Mon Sep 17 00:00:00 2001 From: taibaran Date: Sat, 16 May 2026 11:21:29 +0300 Subject: [PATCH 2/4] test(grok): include grok in SettingsStoreTests provider-order fixture CI on #976 surfaced a hardcoded provider-order list in SettingsStoreTests.swift:1161 that wasn't updated when grok was added in #965. The Grok-only test filter I ran locally before #965 skipped this file, so the breakage only became visible on the next post-merge CI run. Append .grok to the expected ordering so the suite passes. All 2565 tests pass under Xcode 26.5 (51 in CodexBarTests + plenty more). --- Tests/CodexBarTests/SettingsStoreTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/CodexBarTests/SettingsStoreTests.swift b/Tests/CodexBarTests/SettingsStoreTests.swift index f1551aad5..a6bd97f58 100644 --- a/Tests/CodexBarTests/SettingsStoreTests.swift +++ b/Tests/CodexBarTests/SettingsStoreTests.swift @@ -1200,6 +1200,7 @@ struct SettingsStoreTests { .commandcode, .stepfun, .bedrock, + .grok, ]) // Move one provider; ensure it's persisted across instances. From 8c6eb097e268614a14d3e00591a3a078b5f6c30e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 16 May 2026 12:21:14 +0100 Subject: [PATCH 3/4] fix: preserve Grok identity after refreshed billing --- .../Providers/Grok/GrokStatusProbe.swift | 14 ++++++++++++-- Tests/CodexBarTests/GrokAuthTests.swift | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift index ca7715e7f..32f831557 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift @@ -109,7 +109,7 @@ public struct GrokStatusProbe: Sendable { // `localSummary` is *not* currently projected into a visible RateWindow or // identity field, so a stale `~/.grok/sessions/` directory must not // suppress the auth-required hint. Only swallow the RPC error when we - // actually have something rendrable for the user — fresh credentials + // actually have something renderable for the user — fresh credentials // or a billing response. if billing == nil, activeCredentials == nil { throw rpcError ?? GrokRPCError.notAuthenticated @@ -117,9 +117,19 @@ public struct GrokStatusProbe: Sendable { return GrokUsageSnapshot( billing: billing, - credentials: activeCredentials, + credentials: Self.credentialsForSnapshot(credentials: credentials, billing: billing), localSummary: localSummary, cliVersion: cliVersion, updatedAt: Date()) } + + static func credentialsForSnapshot( + credentials: GrokCredentials?, + billing: GrokBillingResponse?) -> GrokCredentials? + { + // If billing succeeded, the CLI accepted/refreshed auth and the local + // identity is still useful even when the persisted expires_at is stale. + if billing != nil { return credentials } + return credentials.flatMap { $0.isExpired ? nil : $0 } + } } diff --git a/Tests/CodexBarTests/GrokAuthTests.swift b/Tests/CodexBarTests/GrokAuthTests.swift index 32978ada3..f7fbef592 100644 --- a/Tests/CodexBarTests/GrokAuthTests.swift +++ b/Tests/CodexBarTests/GrokAuthTests.swift @@ -110,6 +110,25 @@ struct GrokAuthTests { #expect(noExpiry.isExpired == false) } + @Test + func `expired credentials are preserved when billing succeeds`() throws { + let pastJson = #""" + { + "https://auth.x.ai::client": { + "key": "stale-token", + "email": "grok@example.com", + "team_id": "team_123", + "expires_at": "2020-01-01T00:00:00Z" + } + } + """# + let expired = try GrokCredentialsStore.parse(data: Data(pastJson.utf8)) + let billing = try JSONDecoder().decode(GrokBillingResponse.self, from: Data(#"{}"#.utf8)) + + #expect(GrokStatusProbe.credentialsForSnapshot(credentials: expired, billing: nil) == nil) + #expect(GrokStatusProbe.credentialsForSnapshot(credentials: expired, billing: billing)?.email == "grok@example.com") + } + @Test func `falls back to legacy when OIDC entry has no key`() throws { // A stale/partial OIDC record must not shadow a healthy legacy session. From cc194a793c73d1410f5f20362637bd468061427d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 16 May 2026 12:24:28 +0100 Subject: [PATCH 4/4] style: format Grok auth regression --- Tests/CodexBarTests/GrokAuthTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/CodexBarTests/GrokAuthTests.swift b/Tests/CodexBarTests/GrokAuthTests.swift index f7fbef592..367b9140a 100644 --- a/Tests/CodexBarTests/GrokAuthTests.swift +++ b/Tests/CodexBarTests/GrokAuthTests.swift @@ -126,7 +126,8 @@ struct GrokAuthTests { let billing = try JSONDecoder().decode(GrokBillingResponse.self, from: Data(#"{}"#.utf8)) #expect(GrokStatusProbe.credentialsForSnapshot(credentials: expired, billing: nil) == nil) - #expect(GrokStatusProbe.credentialsForSnapshot(credentials: expired, billing: billing)?.email == "grok@example.com") + #expect(GrokStatusProbe.credentialsForSnapshot(credentials: expired, billing: billing)? + .email == "grok@example.com") } @Test