Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 81 additions & 12 deletions Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,16 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
authenticatedByAuthFile: Bool)

static func canImportBrowserCookies(runtime: ProviderRuntime, env: [String: String]) -> Bool {
runtime == .app || env["CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT"] == "1"
runtime == .app ||
ProviderInteractionContext.current == .userInitiated ||
env["CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT"] == "1"
}

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
#if os(macOS)
if CookieHeaderCache.load(provider: .grok) != nil {
return true
}
if Self.canImportBrowserCookies(runtime: context.runtime, env: context.env),
GrokCookieImporter.hasSession(browserDetection: context.browserDetection)
{
Expand Down Expand Up @@ -201,13 +206,30 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
let browserCredentials = try? credentialsResult.get()

#if os(macOS)
var cacheObservation = CookieHeaderCache.observeForConditionalMutation(provider: .grok)
var lastCookieError: Error?
if let cached = CookieHeaderCache.load(provider: .grok) {
do {
let snapshot = try await Self.fetchValidCookieHeader(
cached.cookieHeader,
credentials: browserCredentials)
return (snapshot, cached.sourceLabel, false)
} catch {
guard Self.isCookieAuthenticationFailure(error) else { throw error }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let stale team cookies reach browser fallback

When auth.json contains a non-expired team principal and the cached cookie has expired, the cookie-plus-bearer attempt can return teamUsageUnsupported while the subsequent cookie-only attempt returns 401. fetchValidCookieHeader prioritizes the saved team error, so this guard treats the cached attempt as non-authentication-related and exits before importing the newly signed-in browser session. Regular app refreshes then remain stuck on the stale cache and publish identity-only data until the user runs an explicit cookie refresh; preserve the team error only after fresh browser sessions have also been attempted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if CookieHeaderCache.clearIfCurrent(provider: .grok, expected: cached) {
cacheObservation = cacheObservation.afterOwnedClear()
}
lastCookieError = error
}
}

if Self.canImportBrowserCookies(runtime: context.runtime, env: context.env) {
var lastCookieError: Error?
do {
let sessions = try GrokCookieImporter.importSessions(browserDetection: context.browserDetection)
let (snapshot, sourceLabel) = try await Self.fetchFirstValidCookieSession(
sessions,
credentials: browserCredentials)
credentials: browserCredentials,
cacheObservation: cacheObservation)
return (snapshot, sourceLabel, false)
} catch {
lastCookieError = error
Expand Down Expand Up @@ -242,6 +264,7 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
static func fetchFirstValidCookieSession(
_ sessions: [GrokCookieImporter.SessionInfo],
credentials: GrokCredentials? = nil,
cacheObservation: CookieHeaderCache.ConditionalMutationObservation? = nil,
fetch: ((String, GrokCredentials?) async throws -> GrokWebBillingSnapshot)? = nil) async throws
-> (GrokWebBillingSnapshot, String)
{
Expand All @@ -253,16 +276,50 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
var lastError: Error?
var teamUsageUnsupportedError: Error?
for session in sessions {
for authCredentials in Self.cookieAuthAttempts(credentials: credentials) {
do {
let snapshot = try await fetchSnapshot(session.cookieHeader, authCredentials)
return (snapshot, session.sourceLabel)
} catch {
if case GrokWebBillingError.teamUsageUnsupported = error {
teamUsageUnsupportedError = error
}
lastError = error
do {
let snapshot = try await Self.fetchValidCookieHeader(
session.cookieHeader,
credentials: credentials,
fetch: fetchSnapshot)
if let cacheObservation {
CookieHeaderCache.storeIfObservationCurrent(
provider: .grok,
expected: cacheObservation,
cookieHeader: session.cookieHeader,
sourceLabel: session.sourceLabel)
}
return (snapshot, session.sourceLabel)
} catch {
if case GrokWebBillingError.teamUsageUnsupported = error {
teamUsageUnsupportedError = error
}
lastError = error
}
}
throw teamUsageUnsupportedError ?? lastError ?? GrokWebBillingError.missingCredentials
}

static func fetchValidCookieHeader(
_ cookieHeader: String,
credentials: GrokCredentials? = nil,
fetch: ((String, GrokCredentials?) async throws -> GrokWebBillingSnapshot)? = nil) async throws
-> GrokWebBillingSnapshot
{
let fetchSnapshot = fetch ?? { cookieHeader, credentials in
try await GrokWebBillingFetcher.fetch(
cookieHeader: cookieHeader,
credentials: credentials)
}
var lastError: Error?
var teamUsageUnsupportedError: Error?
for authCredentials in Self.cookieAuthAttempts(credentials: credentials) {
do {
return try await fetchSnapshot(cookieHeader, authCredentials)
} catch {
if case GrokWebBillingError.teamUsageUnsupported = error {
teamUsageUnsupportedError = error
}
lastError = error
}
}
throw teamUsageUnsupportedError ?? lastError ?? GrokWebBillingError.missingCredentials
Expand All @@ -272,6 +329,18 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
guard let credentials, !credentials.isExpired else { return [nil] }
return [credentials, nil]
}

static func isCookieAuthenticationFailure(_ error: Error) -> Bool {
guard let error = error as? GrokWebBillingError else { return false }
switch error {
case let .requestFailed(status, _):
return status == 401 || status == 403
case let .rpcFailed(status, message):
return GrokWebBillingError.isAuthenticationFailure(status: status, message: message)
case .missingCredentials, .emptyResponse, .invalidResponse, .teamUsageUnsupported, .parseFailed:
return false
}
}
#endif

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
Expand Down
76 changes: 75 additions & 1 deletion Tests/CodexBarTests/GrokWebBillingFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,86 @@ struct GrokWebBillingFetcherTests {
}

@Test
func `cli runtime does not import browser cookies unless explicitly enabled`() {
func `cli runtime imports browser cookies only when explicitly enabled`() {
#expect(GrokWebFetchStrategy.canImportBrowserCookies(runtime: .app, env: [:]))
#expect(!GrokWebFetchStrategy.canImportBrowserCookies(runtime: .cli, env: [:]))
#expect(GrokWebFetchStrategy.canImportBrowserCookies(
runtime: .cli,
env: ["CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT": "1"]))
let userInitiated = ProviderInteractionContext.$current.withValue(.userInitiated) {
GrokWebFetchStrategy.canImportBrowserCookies(runtime: .cli, env: [:])
}
#expect(userInitiated)
}

@Test
func `web strategy is available from a cached browser session`() async {
let service = "com.steipete.codexbar.tests.grok-availability.\(UUID().uuidString)"
await KeychainCacheStore.withServiceOverrideForTesting(service) {
await KeychainCacheStore.withImplicitTestStoreForTesting {
CookieHeaderCache.store(
provider: .grok,
cookieHeader: "sso=cached-session",
sourceLabel: "Chrome")
let grokHome = FileManager.default.temporaryDirectory
.appendingPathComponent(
"CodexBar-GrokCachedAvailability-\(UUID().uuidString)",
isDirectory: true)
let browserDetection = BrowserDetection(cacheTTL: 0)
let context = ProviderFetchContext(
runtime: .cli,
sourceMode: .web,
includeCredits: true,
webTimeout: 1,
webDebugDumpHTML: false,
verbose: false,
env: ["GROK_HOME": grokHome.path],
settings: nil,
fetcher: UsageFetcher(),
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection)

#expect(await GrokWebFetchStrategy().isAvailable(context))
}
}
}

@Test
func `validated browser session stages a cache replacement for explicit refresh`() async throws {
let service = "com.steipete.codexbar.tests.grok-refresh.\(UUID().uuidString)"
try await KeychainCacheStore.withServiceOverrideForTesting(service) {
try await KeychainCacheStore.withImplicitTestStoreForTesting {
let gate = try #require(CookieHeaderCache.beginRefreshReadSuppression(provider: .grok))
defer { CookieHeaderCache.endRefreshReadSuppression(gate) }
let observation = CookieHeaderCache.observeForConditionalMutation(provider: .grok)
let cookie = try #require(Self.cookie(name: "sso", value: "fresh-session"))
let sessions = [GrokCookieImporter.SessionInfo(cookies: [cookie], sourceLabel: "Chrome")]

let result = try await GrokWebFetchStrategy.fetchFirstValidCookieSession(
sessions,
cacheObservation: observation)
{ _, _ in
GrokWebBillingSnapshot(usedPercent: 7, resetsAt: nil)
}

#expect(result.0.usedPercent == 7)
#expect(CookieHeaderCache.load(provider: .grok)?.cookieHeader == "sso=fresh-session")
let commit = CookieHeaderCache.commitRefreshReadSuppression(gate)
#expect(commit == CookieRefreshCommitSummary(stagedCount: 1, committedCount: 1, failedCount: 0))
}
}
}

@Test
func `cached cookie eviction is limited to authentication failures`() {
#expect(GrokWebFetchStrategy.isCookieAuthenticationFailure(
GrokWebBillingError.requestFailed(401, "expired")))
#expect(GrokWebFetchStrategy.isCookieAuthenticationFailure(
GrokWebBillingError.rpcFailed(16, "unauthenticated")))
#expect(!GrokWebFetchStrategy.isCookieAuthenticationFailure(
GrokWebBillingError.requestFailed(503, "unavailable")))
#expect(!GrokWebFetchStrategy.isCookieAuthenticationFailure(
GrokWebBillingError.parseFailed))
}

@Test
Expand Down
5 changes: 3 additions & 2 deletions docs/grok.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ browser session when the CLI surface does not expose billing.
browser session, then retries that session with cookies only.
- CodexBar imports Chrome only by default to avoid unrelated browser
Keychain prompts.
- CLI/test runtime does not import browser cookies unless
`CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT=1` is set.
- Ordinary CLI/test runtime does not import browser cookies unless
`CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT=1` is set. An explicit
`codexbar cookie refresh --provider grok` also opts in for that refresh.
- `~/.grok/auth.json` is still used for identity and as a last best-effort
bearer-only probe after browser sessions fail. Expired tokens are not sent.
- Parses the returned protobuf enough to recover used percent and
Expand Down
3 changes: 2 additions & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,8 @@ scan fails, while provider/account configuration changes replace obsolete result
- `grok agent stdio` (ACP) JSON-RPC `x.ai/billing` method; requires `grok login` (SuperGrok OAuth/OIDC).
- Reads cached credentials from `~/.grok/auth.json` for identity (email, team).
- Falls back to grok.com's billing gRPC-web endpoint via Chrome session cookies when the CLI does not expose billing.
- CLI/test runs do not import browser cookies unless `CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT=1` is set.
- Ordinary CLI/test runs do not import browser cookies unless `CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT=1` is set;
`codexbar cookie refresh --provider grok` opts in for its explicit refresh.
- Local fallback aggregates `~/.grok/sessions/**/signals.json` token counts when the RPC is unavailable.
- Status: link only to `https://status.x.ai` (no auto-polling yet).
- Details: `docs/grok.md`.
Expand Down