Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
16 changes: 0 additions & 16 deletions Sources/CodexBar/ManagedCodexAccountService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,6 @@ final class ManagedCodexAccountService {
else {
throw ManagedCodexAccountServiceError.workspaceSelectionCancelled
}
try self.persistSelectedWorkspaceID(selected.workspaceAccountID, homePath: homePath)
return selected
}

Expand All @@ -378,21 +377,6 @@ final class ManagedCodexAccountService {
providerAccountID: providerAccountID)
}

private func persistSelectedWorkspaceID(_ workspaceID: String, homePath: String) throws {
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
let credentials = try CodexOAuthCredentialsStore.load(env: env)
try CodexOAuthCredentialsStore.save(
CodexOAuthCredentials(
accessToken: credentials.accessToken,
refreshToken: credentials.refreshToken,
idToken: credentials.idToken,
accountId: workspaceID,
lastRefresh: credentials.lastRefresh),
env: env)
}

private func reconciledExistingAccount(
authenticatedEmail: String,
providerAccountID: String?,
Expand Down
16 changes: 16 additions & 0 deletions Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct CodexProviderImplementation: ProviderImplementation {
_ = settings.codexUsageDataSource
_ = settings.codexCookieSource
_ = settings.codexCookieHeader
_ = settings.codexExternalOAuthSourcesAllowed
}

@MainActor
Expand Down Expand Up @@ -132,6 +133,21 @@ struct CodexProviderImplementation: ProviderImplementation {
onChange: nil,
onAppDidBecomeActive: nil,
onAppearWhenEnabled: nil),
ProviderSettingsToggleDescriptor(
id: "codex-external-oauth-sources",
title: "External Codex OAuth sources",
subtitle: [
"Explicitly allow read-only fallback to legacy Codex and OpenCode OAuth files.",
"CodexBar never refreshes or writes those external credentials.",
"Off by default because this shares another app's OAuth session with Codex usage requests.",
].joined(separator: " "),
binding: context.boolBinding(\.codexExternalOAuthSourcesAllowed),
statusText: nil,
actions: [],
isVisible: nil,
onChange: nil,
onAppDidBecomeActive: nil,
onAppearWhenEnabled: nil),
ProviderSettingsToggleDescriptor(
id: "codex-openai-web-battery-saver",
title: "OpenAI web battery saver",
Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBar/Providers/Codex/CodexSettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,8 @@ extension SettingsStore {
cookieSource: self.codexSnapshotCookieSource(tokenOverride: tokenOverride),
manualCookieHeader: self.codexSnapshotCookieHeader(tokenOverride: tokenOverride),
reconciliationSnapshot: reconciliationSnapshot,
resolvedActiveSource: resolvedActiveSource))
resolvedActiveSource: resolvedActiveSource,
allowExternalOAuthSources: self.codexExternalOAuthSourcesAllowed))
}

private static func codexUsageDataSource(from source: ProviderSourceMode?) -> CodexUsageDataSource {
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,15 @@ extension SettingsStore {
}
}

var codexExternalOAuthSourcesAllowed: Bool {
get { self.defaultsState.codexExternalOAuthSourcesAllowed }
set {
self.defaultsState.codexExternalOAuthSourcesAllowed = newValue
self.userDefaults.set(newValue, forKey: "codexExternalOAuthSourcesAllowed")
self.noteBackgroundWorkSettingsChanged()
}
}

var openAIWebAccessEnabled: Bool {
get { self.defaultsState.openAIWebAccessEnabled }
set {
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,11 @@ extension SettingsStore {
if Self.isRunningTests, codexSparkUsageVisibleDefault == nil {
userDefaults.set(true, forKey: "codexSparkUsageVisible")
}
let codexExternalOAuthSourcesAllowed = userDefaults.object(
forKey: "codexExternalOAuthSourcesAllowed") as? Bool ?? false
if Self.isRunningTests, userDefaults.object(forKey: "codexExternalOAuthSourcesAllowed") == nil {
userDefaults.set(false, forKey: "codexExternalOAuthSourcesAllowed")
}
let openAIWebAccessDefault = userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool
let openAIWebAccessEnabled = openAIWebAccessDefault ?? false
if Self.isRunningTests, openAIWebAccessDefault == nil {
Expand Down Expand Up @@ -616,6 +621,7 @@ extension SettingsStore {
showOptionalCreditsAndExtraUsage: showOptionalCreditsAndExtraUsage,
claudeDailyRoutinesUsageVisible: claudeDailyRoutinesUsageVisible,
codexSparkUsageVisible: codexSparkUsageVisible,
codexExternalOAuthSourcesAllowed: codexExternalOAuthSourcesAllowed,
openAIWebAccessEnabled: openAIWebAccessEnabled,
openAIWebBatterySaverEnabled: openAIWebBatterySaverEnabled,
backgroundWorkLowPowerModeEnabled: backgroundWorkLowPowerModeEnabled,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ struct SettingsDefaultsState {
var showOptionalCreditsAndExtraUsage: Bool
var claudeDailyRoutinesUsageVisible: Bool
var codexSparkUsageVisible: Bool
var codexExternalOAuthSourcesAllowed: Bool
var openAIWebAccessEnabled: Bool
var openAIWebBatterySaverEnabled: Bool
var backgroundWorkLowPowerModeEnabled: Bool
Expand Down
29 changes: 24 additions & 5 deletions Sources/CodexBar/UsageStore+CodexResetCredits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ extension UsageStore {
typealias CodexResetCreditsFetcher = @Sendable ([String: String]) async throws
-> CodexRateLimitResetCreditsSnapshot?

func codexResetCreditsFetcher() -> CodexResetCreditsFetcher {
func codexResetCreditsFetcher(workspaceAccountID: String? = nil) -> CodexResetCreditsFetcher {
if let override = self._test_codexResetCreditsFetcherOverride {
return override
}
return { env in
try await Self.fetchCodexResetCredits(env: env)
try await Self.fetchCodexResetCredits(
env: env,
workspaceAccountID: workspaceAccountID)
}
}

Expand All @@ -35,6 +37,14 @@ extension UsageStore {
if result.usage.codexResetCredits != nil {
return outcome
}
if result.codexResetCreditsAttempted {
// OAuth already tried the winning in-memory credential snapshot. Never reload
// auth.json here: a concurrent CLI login could attach another account's credits.
if requiresResetCreditRescue {
return outcome.replacingResult(with: .failure(UsageError.noRateLimitsFound))
}
return outcome
}

do {
try Task.checkCancellation()
Expand Down Expand Up @@ -68,13 +78,15 @@ extension UsageStore {
}

nonisolated static func fetchCodexResetCredits(
env: [String: String]) async throws -> CodexRateLimitResetCreditsSnapshot?
env: [String: String],
workspaceAccountID: String? = nil) async throws -> CodexRateLimitResetCreditsSnapshot?
{
try Task.checkCancellation()
let credentials = try CodexOAuthCredentialsStore.loadOAuthTokens(env: env)
return try await Self.fetchCodexResetCredits(
credentials: credentials,
env: env,
workspaceAccountID: workspaceAccountID,
request: { accessToken, accountId, requestEnvironment in
try await CodexOAuthUsageFetcher.fetchRateLimitResetCredits(
accessToken: accessToken,
Expand All @@ -86,23 +98,29 @@ extension UsageStore {
private nonisolated static func fetchCodexResetCredits(
credentials: CodexOAuthCredentials,
env: [String: String],
workspaceAccountID: String? = nil,
request: @escaping @Sendable (String, String?, [String: String]) async throws
-> CodexRateLimitResetCreditsSnapshot?) async throws -> CodexRateLimitResetCreditsSnapshot?
{
try Task.checkCancellation()
// Supplemental inventory is strictly read-only. The main OAuth usage strategy owns token refreshes;
// CLI/web winners with stale credentials simply skip this best-effort GET.
guard !credentials.needsRefresh else { return nil }
return try await request(credentials.accessToken, credentials.accountId, env)
return try await request(credentials.accessToken, workspaceAccountID ?? credentials.accountId, env)
}

nonisolated static func _fetchCodexResetCreditsForTesting(
credentials: CodexOAuthCredentials,
env: [String: String] = [:],
workspaceAccountID: String? = nil,
request: @escaping @Sendable (String, String?, [String: String]) async throws
-> CodexRateLimitResetCreditsSnapshot?) async throws -> CodexRateLimitResetCreditsSnapshot?
{
try await self.fetchCodexResetCredits(credentials: credentials, env: env, request: request)
try await self.fetchCodexResetCredits(
credentials: credentials,
env: env,
workspaceAccountID: workspaceAccountID,
request: request)
}
}

Expand All @@ -117,6 +135,7 @@ extension ProviderFetchOutcome {
sourceLabel: result.sourceLabel,
strategyID: result.strategyID,
strategyKind: result.strategyKind,
codexResetCreditsAttempted: result.codexResetCreditsAttempted,
diagnostic: result.diagnostic,
claudeOAuthKeychainPersistentRefHash: result.claudeOAuthKeychainPersistentRefHash,
claudeOAuthHistoryOwnerIdentifier: result.claudeOAuthHistoryOwnerIdentifier,
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/UsageStore+Refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ extension UsageStore {
: nil
let priorTokenAccountSnapshot = self.tokenAccountSnapshot(provider: provider, account: tokenAccount)
let descriptor = spec.descriptor
let codexResetCreditsFetcher = self.codexResetCreditsFetcher()
let codexResetCreditsFetcher = self.codexResetCreditsFetcher(workspaceAccountID: fetchContext.codexWorkspaceID)
let previousCodexSnapshot = codexPreparation?.previousSnapshot
let codexMissingWindowBackfillSnapshot = codexPreparation?.missingWindowBackfillSnapshot
let fetchOutcome: @Sendable () async -> ProviderFetchOutcome = {
Expand Down
9 changes: 5 additions & 4 deletions Sources/CodexBar/UsageStore+TokenAccounts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ private struct CodexAccountFetchRequest {
let limitResetOwnerKey: CodexLimitResetOwnerKey?
let descriptor: ProviderDescriptor
let context: ProviderFetchContext
let resetCreditsFetcher: UsageStore.CodexResetCreditsFetcher
}

private struct CodexManagedVisibleAccountRuntimeState {
Expand Down Expand Up @@ -740,7 +741,7 @@ extension UsageStore {
return await Self.attachingCodexResetCreditsIfNeeded(
to: outcome,
env: context.env,
fetcher: self.codexResetCreditsFetcher())
fetcher: self.codexResetCreditsFetcher(workspaceAccountID: context.codexWorkspaceID))
}

private func fetchTokenAccountOutcomes(
Expand Down Expand Up @@ -817,7 +818,6 @@ extension UsageStore {
priorSnapshots: [CodexAccountUsageSnapshot],
activeVisibleAccountID: String?) async
-> [CodexAccountFetchResult] {
let resetCreditsFetcher = self.codexResetCreditsFetcher()
let requests: [CodexAccountFetchRequest] = accounts.enumerated().map { index, account in
let descriptor = self.providerSpecs[.codex]?.descriptor ?? ProviderDescriptorRegistry
.descriptor(for: .codex)
Expand Down Expand Up @@ -845,7 +845,8 @@ extension UsageStore {
missingWindowBackfillSnapshot: missingWindowBackfillSnapshot,
limitResetOwnerKey: limitResetOwnerKey,
descriptor: descriptor,
context: context)
context: context,
resetCreditsFetcher: self.codexResetCreditsFetcher(workspaceAccountID: context.codexWorkspaceID))
}

return await withTaskGroup(
Expand All @@ -859,7 +860,7 @@ extension UsageStore {
return await Self.attachingCodexResetCreditsIfNeeded(
to: baseOutcome,
env: request.context.env,
fetcher: resetCreditsFetcher)
fetcher: request.resetCreditsFetcher)
}
let initialOutcome = await fetchOutcome()
let outcome: ProviderFetchOutcome? = if Self.codexUsageOutcomeMatchesVisibleAccount(
Expand Down
14 changes: 4 additions & 10 deletions Sources/CodexBarCore/ManagedCodexAccountStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ public struct FileManagedCodexAccountStore: ManagedCodexAccountStoring, @uncheck
if !self.fileManager.fileExists(atPath: directory.path) {
try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}
try data.write(to: self.fileURL, options: [.atomic])
try self.applySecurePermissionsIfNeeded()
// Managed account metadata contains account identities and private Codex home paths. Use
// the same staged 0600 writer as auth.json so a newly-created file is never briefly
// readable under a permissive umask.
try CredentialFileWriter.writePrivate(data, to: self.fileURL)
}

public func ensureFileExists() throws -> URL {
Expand All @@ -59,14 +61,6 @@ public struct FileManagedCodexAccountStore: ManagedCodexAccountStoring, @uncheck
return self.fileURL
}

private func applySecurePermissionsIfNeeded() throws {
#if os(macOS)
try self.fileManager.setAttributes([
.posixPermissions: NSNumber(value: Int16(0o600)),
], ofItemAtPath: self.fileURL.path)
#endif
}

private static func emptyAccountSet() -> ManagedCodexAccountSet {
ManagedCodexAccountSet(version: self.currentVersion, accounts: [])
}
Expand Down
Loading
Loading