742 lines
28 KiB
Swift
742 lines
28 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
|
|
extension SettingsStore {
|
|
private var codexPersistedActiveSource: CodexActiveSource {
|
|
self.providerConfig(for: .codex)?.codexActiveSource ?? .liveSystem
|
|
}
|
|
|
|
private enum ManagedCodexAccountStoreState {
|
|
case none
|
|
case selected(ManagedCodexAccount)
|
|
case unreadable
|
|
}
|
|
|
|
private static func failClosedManagedCodexHomePath(fileManager: FileManager = .default) -> String {
|
|
ManagedCodexHomeFactory.defaultRootURL(fileManager: fileManager)
|
|
.appendingPathComponent("managed-store-unreadable", isDirectory: true)
|
|
.path
|
|
}
|
|
|
|
private static func normalizedCodexProfileHomePaths(_ paths: [String]?) -> [String] {
|
|
var seen: Set<String> = []
|
|
var result: [String] = []
|
|
for path in (paths ?? []).compactMap({ CodexHomeScope.normalizedHomePath($0) }) {
|
|
guard seen.insert(path).inserted else { continue }
|
|
result.append(path)
|
|
}
|
|
return result
|
|
}
|
|
|
|
private func loadManagedCodexAccounts() throws -> ManagedCodexAccountSet {
|
|
#if DEBUG
|
|
if CodexManagedRemoteHomeTestingOverride.isUnreadable(for: self) {
|
|
throw CodexManagedRemoteHomeTestingOverrideError.unreadableManagedStore
|
|
}
|
|
if let override = CodexManagedRemoteHomeTestingOverride.account(for: self) {
|
|
return ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [override])
|
|
}
|
|
let store = if let storeURL = CodexManagedRemoteHomeTestingOverride.managedStoreURL(for: self) {
|
|
FileManagedCodexAccountStore(fileURL: storeURL)
|
|
} else {
|
|
FileManagedCodexAccountStore()
|
|
}
|
|
#else
|
|
let store = FileManagedCodexAccountStore()
|
|
#endif
|
|
|
|
return try store.loadAccounts()
|
|
}
|
|
|
|
private func managedCodexAccountStoreState(
|
|
activeSource: CodexActiveSource? = nil) -> ManagedCodexAccountStoreState
|
|
{
|
|
let source = activeSource ?? self.codexResolvedActiveSource
|
|
guard case let .managedAccount(id) = source else {
|
|
return .none
|
|
}
|
|
do {
|
|
let accounts = try self.loadManagedCodexAccounts()
|
|
guard let account = accounts.account(id: id)
|
|
else {
|
|
return .none
|
|
}
|
|
return .selected(account)
|
|
} catch {
|
|
return .unreadable
|
|
}
|
|
}
|
|
|
|
var activeManagedCodexAccount: ManagedCodexAccount? {
|
|
guard case let .selected(account) = self.managedCodexAccountStoreState() else {
|
|
return nil
|
|
}
|
|
return account
|
|
}
|
|
|
|
var activeManagedCodexRemoteHomePath: String? {
|
|
self.managedCodexRemoteHomePath(forActiveSource: self.codexResolvedActiveSource)
|
|
}
|
|
|
|
func liveSystemCodexHomePath(forActiveSource source: CodexActiveSource) -> String? {
|
|
guard source == .liveSystem else {
|
|
return nil
|
|
}
|
|
let path = self.codexAccountReconciliationSnapshot(activeSourceOverride: source)
|
|
.liveSystemAccount?.codexHomePath.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard let path, !path.isEmpty else {
|
|
return nil
|
|
}
|
|
return path
|
|
}
|
|
|
|
var codexProfileHomePaths: [String] {
|
|
Self.normalizedCodexProfileHomePaths(
|
|
self.configSnapshot.providerConfig(for: .codex)?.codexProfileHomePaths)
|
|
}
|
|
|
|
func profileCodexHomePath(forActiveSource source: CodexActiveSource) -> String? {
|
|
guard case let .profileHome(path) = source else {
|
|
return nil
|
|
}
|
|
guard let normalizedPath = CodexHomeScope.normalizedHomePath(path),
|
|
self.codexProfileHomePaths.contains(normalizedPath)
|
|
else {
|
|
return nil
|
|
}
|
|
return normalizedPath
|
|
}
|
|
|
|
func managedCodexRemoteHomePath(forActiveSource source: CodexActiveSource) -> String? {
|
|
guard case let .managedAccount(id) = source else {
|
|
return nil
|
|
}
|
|
|
|
#if DEBUG
|
|
if let override = CodexManagedRemoteHomeTestingOverride.homePath(for: self) {
|
|
return override
|
|
}
|
|
#endif
|
|
|
|
do {
|
|
let accounts = try self.loadManagedCodexAccounts()
|
|
// A selected managed source must never fall back to ambient ~/.codex.
|
|
return accounts.account(id: id)?.managedHomePath ?? Self.failClosedManagedCodexHomePath()
|
|
} catch {
|
|
return Self.failClosedManagedCodexHomePath()
|
|
}
|
|
}
|
|
|
|
var activeManagedCodexCookieCacheScope: CookieHeaderCache.Scope? {
|
|
switch self.managedCodexAccountStoreState() {
|
|
case let .selected(account):
|
|
.managedAccount(account.id)
|
|
case .unreadable:
|
|
.managedStoreUnreadable
|
|
case .none:
|
|
nil
|
|
}
|
|
}
|
|
|
|
var hasUnreadableManagedCodexAccountStore: Bool {
|
|
self.codexAccountReconciliationSnapshot.hasUnreadableAddedAccountStore
|
|
}
|
|
|
|
var codexUsageDataSource: CodexUsageDataSource {
|
|
get {
|
|
let source = self.configSnapshot.providerConfig(for: .codex)?.source
|
|
return Self.codexUsageDataSource(from: source)
|
|
}
|
|
set {
|
|
let source: ProviderSourceMode? = switch newValue {
|
|
case .auto: .auto
|
|
case .oauth: .oauth
|
|
case .cli: .cli
|
|
}
|
|
self.updateProviderConfig(provider: .codex) { entry in
|
|
entry.source = source
|
|
}
|
|
self.logProviderModeChange(provider: .codex, field: "usageSource", value: newValue.rawValue)
|
|
}
|
|
}
|
|
|
|
var codexActiveSource: CodexActiveSource {
|
|
get {
|
|
self.codexPersistedActiveSource
|
|
}
|
|
set {
|
|
self.invalidateCodexAccountReconciliationSnapshotCache()
|
|
self.cachedCodexAccountMenuProjection = nil
|
|
self.updateProviderConfig(provider: .codex) { entry in
|
|
entry.codexActiveSource = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
var codexResolvedActiveSource: CodexActiveSource {
|
|
self.codexResolvedActiveSourceState.resolvedSource
|
|
}
|
|
|
|
var codexResolvedActiveSourceState: CodexResolvedActiveSource {
|
|
CodexActiveSourceResolver.resolve(from: self.codexAccountReconciliationSnapshot)
|
|
}
|
|
|
|
@discardableResult
|
|
func persistResolvedCodexActiveSourceCorrectionIfNeeded() -> Bool {
|
|
let resolution = self.codexResolvedActiveSourceState
|
|
guard resolution.requiresPersistenceCorrection else { return false }
|
|
self.codexActiveSource = resolution.resolvedSource
|
|
return true
|
|
}
|
|
|
|
@discardableResult
|
|
func refreshCodexAccountReconciliationAfterManagedAccountsDidChange() -> Bool {
|
|
self.invalidateCodexAccountReconciliationSnapshotCache()
|
|
self.cachedCodexAccountMenuProjection = nil
|
|
return self.persistResolvedCodexActiveSourceCorrectionIfNeeded()
|
|
}
|
|
|
|
var codexCookieHeader: String {
|
|
get { self.configSnapshot.providerConfig(for: .codex)?.sanitizedCookieHeader ?? "" }
|
|
set {
|
|
// This is intentionally provider-scoped today. A per-managed-account manual cookie override would need
|
|
// its own storage and UI semantics so editing one account's header does not silently rewrite another's.
|
|
self.updateProviderConfig(provider: .codex) { entry in
|
|
entry.cookieHeader = self.normalizedConfigValue(newValue)
|
|
}
|
|
self.logSecretUpdate(provider: .codex, field: "cookieHeader", value: newValue)
|
|
}
|
|
}
|
|
|
|
var codexCookieSource: ProviderCookieSource {
|
|
get {
|
|
let resolved = self.resolvedCookieSource(provider: .codex, fallback: .auto)
|
|
return self.openAIWebAccessEnabled ? resolved : .off
|
|
}
|
|
set {
|
|
self.updateProviderConfig(provider: .codex) { entry in
|
|
entry.cookieSource = newValue
|
|
}
|
|
self.logProviderModeChange(provider: .codex, field: "cookieSource", value: newValue.rawValue)
|
|
self.openAIWebAccessEnabled = newValue.isEnabled
|
|
}
|
|
}
|
|
|
|
func ensureCodexCookieLoaded() {}
|
|
}
|
|
|
|
extension SettingsStore {
|
|
private static var codexAccountReconciliationSnapshotCacheInterval: TimeInterval {
|
|
#if DEBUG
|
|
if let codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting {
|
|
return codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting
|
|
}
|
|
#endif
|
|
return self.isRunningTests ? 0 : self.productionCodexAccountReconciliationSnapshotCacheInterval
|
|
}
|
|
|
|
func invalidateCodexAccountReconciliationSnapshotCache() {
|
|
self.cachedCodexAccountReconciliationSnapshot = nil
|
|
self.codexAccountReconciliationGeneration &+= 1
|
|
}
|
|
|
|
var codexAccountReconciliationSnapshot: CodexAccountReconciliationSnapshot {
|
|
self.codexAccountReconciliationSnapshot(activeSourceOverride: nil)
|
|
}
|
|
|
|
func codexAccountReconciliationSnapshot(
|
|
activeSourceOverride: CodexActiveSource?) -> CodexAccountReconciliationSnapshot
|
|
{
|
|
let activeSource = activeSourceOverride ?? self.codexPersistedActiveSource
|
|
let cacheInterval = Self.codexAccountReconciliationSnapshotCacheInterval
|
|
let now = Date()
|
|
if cacheInterval > 0,
|
|
let cached = self.cachedCodexAccountReconciliationSnapshot,
|
|
cached.activeSource == activeSource,
|
|
now.timeIntervalSince(cached.loadedAt) < cacheInterval
|
|
{
|
|
return cached.snapshot
|
|
}
|
|
|
|
let snapshot = self.codexAccountSnapshotLoader(activeSource: activeSource)()
|
|
let loadedAt = Date()
|
|
if cacheInterval > 0 {
|
|
self.cachedCodexAccountReconciliationSnapshot = CachedCodexAccountReconciliationSnapshot(
|
|
activeSource: activeSource,
|
|
loadedAt: loadedAt,
|
|
snapshot: snapshot)
|
|
}
|
|
if activeSource == self.codexPersistedActiveSource {
|
|
self.cachedCodexAccountMenuProjection = CachedCodexAccountMenuProjection(
|
|
activeSource: activeSource,
|
|
loadedAt: loadedAt,
|
|
projection: CodexVisibleAccountProjection.make(from: snapshot))
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
/// Menu rendering must stay side-effect free: no `auth.json` reads, JWT parsing, or fingerprint hashing.
|
|
var codexVisibleAccountProjectionForMenuDisplay: CodexVisibleAccountProjection? {
|
|
let activeSource = self.codexPersistedActiveSource
|
|
guard let cached = self.cachedCodexAccountMenuProjection,
|
|
cached.activeSource == activeSource
|
|
else {
|
|
return nil
|
|
}
|
|
return cached.projection
|
|
}
|
|
|
|
var codexAccountMenuProjectionNeedsRevalidation: Bool {
|
|
let activeSource = self.codexPersistedActiveSource
|
|
guard let cached = self.cachedCodexAccountMenuProjection,
|
|
cached.activeSource == activeSource
|
|
else {
|
|
return true
|
|
}
|
|
return Date().timeIntervalSince(cached.loadedAt) >= Self.codexAccountReconciliationSnapshotCacheInterval
|
|
}
|
|
|
|
func revalidateCodexAccountMenuProjection() async -> CodexAccountMenuProjectionRevalidationResult {
|
|
guard self.codexAccountMenuProjectionNeedsRevalidation else { return .skipped }
|
|
|
|
let activeSource = self.codexPersistedActiveSource
|
|
let generation = self.codexAccountReconciliationGeneration
|
|
let loader = self.codexAccountSnapshotLoader(activeSource: activeSource)
|
|
let snapshot = await Self.loadCodexAccountSnapshot(loader)
|
|
|
|
guard generation == self.codexAccountReconciliationGeneration,
|
|
activeSource == self.codexPersistedActiveSource
|
|
else {
|
|
return .discarded
|
|
}
|
|
|
|
let now = Date()
|
|
let projection = CodexVisibleAccountProjection.make(from: snapshot)
|
|
let previousProjection = self.cachedCodexAccountMenuProjection.flatMap { cached in
|
|
cached.activeSource == activeSource ? cached.projection : nil
|
|
}
|
|
self.cachedCodexAccountMenuProjection = CachedCodexAccountMenuProjection(
|
|
activeSource: activeSource,
|
|
loadedAt: now,
|
|
projection: projection)
|
|
if Self.codexAccountReconciliationSnapshotCacheInterval > 0 {
|
|
self.cachedCodexAccountReconciliationSnapshot = CachedCodexAccountReconciliationSnapshot(
|
|
activeSource: activeSource,
|
|
loadedAt: now,
|
|
snapshot: snapshot)
|
|
}
|
|
return previousProjection == projection ? .unchanged : .updated
|
|
}
|
|
|
|
@concurrent
|
|
private nonisolated static func loadCodexAccountSnapshot(
|
|
_ loader: @escaping @Sendable () -> CodexAccountReconciliationSnapshot)
|
|
async -> CodexAccountReconciliationSnapshot
|
|
{
|
|
loader()
|
|
}
|
|
|
|
var codexVisibleAccountProjection: CodexVisibleAccountProjection {
|
|
CodexVisibleAccountProjection.make(from: self.codexAccountReconciliationSnapshot)
|
|
}
|
|
|
|
var codexVisibleAccounts: [CodexVisibleAccount] {
|
|
self.codexVisibleAccountProjection.visibleAccounts
|
|
}
|
|
|
|
@discardableResult
|
|
func selectCodexVisibleAccount(id: String) -> Bool {
|
|
guard let source = self.codexSource(forVisibleAccountID: id) else { return false }
|
|
self.invalidateCodexAccountReconciliationSnapshotCache()
|
|
self.codexActiveSource = source
|
|
return true
|
|
}
|
|
|
|
func selectDisplayedCodexVisibleAccount(_ account: CodexVisibleAccount) {
|
|
// The row already carries the exact source it represented. Re-resolving its ID would synchronously
|
|
// reload auth state from the menu click callback and can also fail after a stale snapshot is rendered.
|
|
self.codexActiveSource = account.selectionSource
|
|
}
|
|
|
|
func selectAuthenticatedManagedCodexAccount(_ account: ManagedCodexAccount) {
|
|
if let visibleAccountID = self.codexVisibleAccountProjection.visibleAccounts
|
|
.first(where: { $0.storedAccountID == account.id })?
|
|
.id,
|
|
self.selectCodexVisibleAccount(id: visibleAccountID)
|
|
{
|
|
return
|
|
}
|
|
|
|
self.invalidateCodexAccountReconciliationSnapshotCache()
|
|
self.codexActiveSource = .managedAccount(id: account.id)
|
|
_ = self.persistResolvedCodexActiveSourceCorrectionIfNeeded()
|
|
}
|
|
|
|
func codexSource(forVisibleAccountID id: String) -> CodexActiveSource? {
|
|
self.codexVisibleAccountProjection.source(forVisibleAccountID: id)
|
|
}
|
|
|
|
private func codexAccountSnapshotLoader(
|
|
activeSource: CodexActiveSource) -> @Sendable () -> CodexAccountReconciliationSnapshot
|
|
{
|
|
#if DEBUG
|
|
if let loader = self._test_codexAccountSnapshotLoader {
|
|
return { loader(activeSource) }
|
|
}
|
|
#endif
|
|
let reconciler = self.codexAccountReconciler(activeSource: activeSource)
|
|
return { reconciler.loadSnapshot() }
|
|
}
|
|
|
|
private func codexAccountReconciler(activeSource: CodexActiveSource) -> DefaultCodexAccountReconciler {
|
|
let baseEnvironment = self.codexReconciliationEnvironment()
|
|
#if DEBUG
|
|
let liveSystemAccountOverride = CodexManagedRemoteHomeTestingOverride.liveSystemAccount(for: self)
|
|
let reconciliationEnvironmentOverride = CodexManagedRemoteHomeTestingOverride
|
|
.reconciliationEnvironment(for: self)
|
|
let managedAccountOverride = CodexManagedRemoteHomeTestingOverride.account(for: self)
|
|
let managedStoreURLOverride = CodexManagedRemoteHomeTestingOverride.managedStoreURL(for: self)
|
|
let unreadableStoreOverride = CodexManagedRemoteHomeTestingOverride.isUnreadable(for: self)
|
|
guard CodexManagedRemoteHomeTestingOverride.hasAnyOverride(for: self) else {
|
|
return DefaultCodexAccountReconciler(
|
|
activeSource: activeSource,
|
|
baseEnvironment: baseEnvironment,
|
|
profileHomePaths: self.codexProfileHomePaths,
|
|
managedEnvironmentBuilder: { environment, account in
|
|
CodexHomeScope.scopedEnvironment(base: environment, codexHome: account.managedHomePath)
|
|
})
|
|
}
|
|
|
|
let storeLoader: @Sendable () throws -> ManagedCodexAccountSet
|
|
if unreadableStoreOverride {
|
|
storeLoader = { throw CodexManagedRemoteHomeTestingOverrideError.unreadableManagedStore }
|
|
} else if let managedAccountOverride {
|
|
let accounts = ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [managedAccountOverride])
|
|
storeLoader = { accounts }
|
|
} else if let managedStoreURLOverride {
|
|
let store = FileManagedCodexAccountStore(fileURL: managedStoreURLOverride)
|
|
storeLoader = { try store.loadAccounts() }
|
|
} else {
|
|
let accounts = ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [])
|
|
storeLoader = { accounts }
|
|
}
|
|
|
|
return DefaultCodexAccountReconciler(
|
|
storeLoader: storeLoader,
|
|
systemObserver: CodexManagedRemoteHomeTestingSystemObserver(
|
|
overrideAccount: liveSystemAccountOverride,
|
|
usesInjectedEnvironment: reconciliationEnvironmentOverride != nil),
|
|
activeSource: activeSource,
|
|
baseEnvironment: baseEnvironment,
|
|
profileHomePaths: self.codexProfileHomePaths,
|
|
managedEnvironmentBuilder: { environment, account in
|
|
CodexHomeScope.scopedEnvironment(base: environment, codexHome: account.managedHomePath)
|
|
})
|
|
#else
|
|
return DefaultCodexAccountReconciler(
|
|
activeSource: activeSource,
|
|
baseEnvironment: baseEnvironment,
|
|
profileHomePaths: self.codexProfileHomePaths,
|
|
managedEnvironmentBuilder: { environment, account in
|
|
CodexHomeScope.scopedEnvironment(base: environment, codexHome: account.managedHomePath)
|
|
})
|
|
#endif
|
|
}
|
|
|
|
private func codexReconciliationEnvironment() -> [String: String] {
|
|
#if DEBUG
|
|
if let override = CodexManagedRemoteHomeTestingOverride.reconciliationEnvironment(for: self) {
|
|
return override
|
|
}
|
|
#endif
|
|
return ProcessInfo.processInfo.environment
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
private enum CodexManagedRemoteHomeTestingOverride {
|
|
private struct Override {
|
|
var account: ManagedCodexAccount?
|
|
var homePath: String?
|
|
var unreadableStore: Bool = false
|
|
var managedStoreURL: URL?
|
|
var liveSystemAccount: ObservedSystemCodexAccount?
|
|
var reconciliationEnvironment: [String: String]?
|
|
|
|
var isEmpty: Bool {
|
|
self.account == nil && self.homePath == nil && self.unreadableStore == false && self
|
|
.managedStoreURL == nil && self.liveSystemAccount == nil && self
|
|
.reconciliationEnvironment == nil
|
|
}
|
|
}
|
|
|
|
private final class Entry {
|
|
weak var settings: SettingsStore?
|
|
var overrideValue: Override
|
|
|
|
init(settings: SettingsStore, overrideValue: Override) {
|
|
self.settings = settings
|
|
self.overrideValue = overrideValue
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private static var values: [ObjectIdentifier: Entry] = [:]
|
|
|
|
@MainActor
|
|
private static func entry(for settings: SettingsStore) -> Entry? {
|
|
let key = ObjectIdentifier(settings)
|
|
guard let entry = self.values[key] else { return nil }
|
|
guard let storedSettings = entry.settings, storedSettings === settings else {
|
|
self.values.removeValue(forKey: key)
|
|
return nil
|
|
}
|
|
return entry
|
|
}
|
|
|
|
@MainActor
|
|
static func account(for settings: SettingsStore) -> ManagedCodexAccount? {
|
|
self.entry(for: settings)?.overrideValue.account
|
|
}
|
|
|
|
@MainActor
|
|
static func setAccount(_ account: ManagedCodexAccount?, for settings: SettingsStore) {
|
|
let key = ObjectIdentifier(settings)
|
|
var override = self.entry(for: settings)?.overrideValue ?? Override()
|
|
override.account = account
|
|
if override.isEmpty {
|
|
self.values.removeValue(forKey: key)
|
|
} else {
|
|
self.values[key] = Entry(settings: settings, overrideValue: override)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
static func homePath(for settings: SettingsStore) -> String? {
|
|
self.entry(for: settings)?.overrideValue.homePath
|
|
}
|
|
|
|
@MainActor
|
|
static func setHomePath(_ value: String?, for settings: SettingsStore) {
|
|
let key = ObjectIdentifier(settings)
|
|
var override = self.entry(for: settings)?.overrideValue ?? Override()
|
|
override.homePath = value
|
|
if override.isEmpty {
|
|
self.values.removeValue(forKey: key)
|
|
} else {
|
|
self.values[key] = Entry(settings: settings, overrideValue: override)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
static func isUnreadable(for settings: SettingsStore) -> Bool {
|
|
self.entry(for: settings)?.overrideValue.unreadableStore == true
|
|
}
|
|
|
|
@MainActor
|
|
static func setUnreadable(_ value: Bool, for settings: SettingsStore) {
|
|
let key = ObjectIdentifier(settings)
|
|
var override = self.entry(for: settings)?.overrideValue ?? Override()
|
|
override.unreadableStore = value
|
|
if override.isEmpty {
|
|
self.values.removeValue(forKey: key)
|
|
} else {
|
|
self.values[key] = Entry(settings: settings, overrideValue: override)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
static func liveSystemAccount(for settings: SettingsStore) -> ObservedSystemCodexAccount? {
|
|
self.entry(for: settings)?.overrideValue.liveSystemAccount
|
|
}
|
|
|
|
@MainActor
|
|
static func managedStoreURL(for settings: SettingsStore) -> URL? {
|
|
self.entry(for: settings)?.overrideValue.managedStoreURL
|
|
}
|
|
|
|
@MainActor
|
|
static func setManagedStoreURL(_ value: URL?, for settings: SettingsStore) {
|
|
let key = ObjectIdentifier(settings)
|
|
var override = self.entry(for: settings)?.overrideValue ?? Override()
|
|
override.managedStoreURL = value
|
|
if override.isEmpty {
|
|
self.values.removeValue(forKey: key)
|
|
} else {
|
|
self.values[key] = Entry(settings: settings, overrideValue: override)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
static func setLiveSystemAccount(_ account: ObservedSystemCodexAccount?, for settings: SettingsStore) {
|
|
let key = ObjectIdentifier(settings)
|
|
var override = self.entry(for: settings)?.overrideValue ?? Override()
|
|
override.liveSystemAccount = account
|
|
if override.isEmpty {
|
|
self.values.removeValue(forKey: key)
|
|
} else {
|
|
self.values[key] = Entry(settings: settings, overrideValue: override)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
static func reconciliationEnvironment(for settings: SettingsStore) -> [String: String]? {
|
|
self.entry(for: settings)?.overrideValue.reconciliationEnvironment
|
|
}
|
|
|
|
@MainActor
|
|
static func setReconciliationEnvironment(_ environment: [String: String]?, for settings: SettingsStore) {
|
|
let key = ObjectIdentifier(settings)
|
|
var override = self.entry(for: settings)?.overrideValue ?? Override()
|
|
override.reconciliationEnvironment = environment
|
|
if override.isEmpty {
|
|
self.values.removeValue(forKey: key)
|
|
} else {
|
|
self.values[key] = Entry(settings: settings, overrideValue: override)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
static func hasAnyOverride(for settings: SettingsStore) -> Bool {
|
|
self.entry(for: settings)?.overrideValue.isEmpty == false
|
|
}
|
|
}
|
|
|
|
private enum CodexManagedRemoteHomeTestingOverrideError: Error {
|
|
case unreadableManagedStore
|
|
}
|
|
|
|
private struct CodexManagedRemoteHomeTestingSystemObserver: CodexSystemAccountObserving {
|
|
let overrideAccount: ObservedSystemCodexAccount?
|
|
let usesInjectedEnvironment: Bool
|
|
|
|
func loadSystemAccount(environment: [String: String]) throws -> ObservedSystemCodexAccount? {
|
|
if let overrideAccount {
|
|
return overrideAccount
|
|
}
|
|
guard self.usesInjectedEnvironment else {
|
|
return nil
|
|
}
|
|
return try DefaultCodexSystemAccountObserver().loadSystemAccount(environment: environment)
|
|
}
|
|
}
|
|
|
|
extension SettingsStore {
|
|
private func invalidateCodexAccountReconciliationCachesForTesting() {
|
|
self.invalidateCodexAccountReconciliationSnapshotCache()
|
|
self.cachedCodexAccountMenuProjection = nil
|
|
}
|
|
|
|
var _test_activeManagedCodexRemoteHomePath: String? {
|
|
get { CodexManagedRemoteHomeTestingOverride.homePath(for: self) }
|
|
set {
|
|
self.invalidateCodexAccountReconciliationCachesForTesting()
|
|
CodexManagedRemoteHomeTestingOverride.setHomePath(newValue, for: self)
|
|
}
|
|
}
|
|
|
|
var _test_activeManagedCodexAccount: ManagedCodexAccount? {
|
|
get { CodexManagedRemoteHomeTestingOverride.account(for: self) }
|
|
set {
|
|
self.invalidateCodexAccountReconciliationCachesForTesting()
|
|
CodexManagedRemoteHomeTestingOverride.setAccount(newValue, for: self)
|
|
}
|
|
}
|
|
|
|
var _test_unreadableManagedCodexAccountStore: Bool {
|
|
get { CodexManagedRemoteHomeTestingOverride.isUnreadable(for: self) }
|
|
set {
|
|
self.invalidateCodexAccountReconciliationCachesForTesting()
|
|
CodexManagedRemoteHomeTestingOverride.setUnreadable(newValue, for: self)
|
|
}
|
|
}
|
|
|
|
var _test_managedCodexAccountStoreURL: URL? {
|
|
get { CodexManagedRemoteHomeTestingOverride.managedStoreURL(for: self) }
|
|
set {
|
|
self.invalidateCodexAccountReconciliationCachesForTesting()
|
|
CodexManagedRemoteHomeTestingOverride.setManagedStoreURL(newValue, for: self)
|
|
}
|
|
}
|
|
|
|
var _test_liveSystemCodexAccount: ObservedSystemCodexAccount? {
|
|
get { CodexManagedRemoteHomeTestingOverride.liveSystemAccount(for: self) }
|
|
set {
|
|
self.invalidateCodexAccountReconciliationCachesForTesting()
|
|
CodexManagedRemoteHomeTestingOverride.setLiveSystemAccount(newValue, for: self)
|
|
}
|
|
}
|
|
|
|
var _test_codexReconciliationEnvironment: [String: String]? {
|
|
get { CodexManagedRemoteHomeTestingOverride.reconciliationEnvironment(for: self) }
|
|
set {
|
|
self.invalidateCodexAccountReconciliationCachesForTesting()
|
|
CodexManagedRemoteHomeTestingOverride.setReconciliationEnvironment(newValue, for: self)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
extension SettingsStore {
|
|
func codexSettingsSnapshot(
|
|
tokenOverride: TokenAccountOverride?,
|
|
activeSourceOverride: CodexActiveSource? = nil) -> ProviderSettingsSnapshot.CodexProviderSettings
|
|
{
|
|
let reconciliationSnapshot = self.codexAccountReconciliationSnapshot(
|
|
activeSourceOverride: activeSourceOverride)
|
|
let resolvedActiveSource = CodexActiveSourceResolver.resolve(from: reconciliationSnapshot)
|
|
return CodexProviderSettingsBuilder.make(input: CodexProviderSettingsBuilderInput(
|
|
usageDataSource: self.codexUsageDataSource,
|
|
cookieSource: self.codexSnapshotCookieSource(tokenOverride: tokenOverride),
|
|
manualCookieHeader: self.codexSnapshotCookieHeader(tokenOverride: tokenOverride),
|
|
reconciliationSnapshot: reconciliationSnapshot,
|
|
resolvedActiveSource: resolvedActiveSource))
|
|
}
|
|
|
|
private static func codexUsageDataSource(from source: ProviderSourceMode?) -> CodexUsageDataSource {
|
|
guard let source else { return .auto }
|
|
switch source {
|
|
case .auto, .web, .api:
|
|
return .auto
|
|
case .cli:
|
|
return .cli
|
|
case .oauth:
|
|
return .oauth
|
|
}
|
|
}
|
|
|
|
private func codexSnapshotCookieHeader(tokenOverride: TokenAccountOverride?) -> String {
|
|
let fallback = self.codexCookieHeader
|
|
guard let support = TokenAccountSupportCatalog.support(for: .codex),
|
|
case .cookieHeader = support.injection
|
|
else {
|
|
return fallback
|
|
}
|
|
guard let account = ProviderTokenAccountSelection.selectedAccount(
|
|
provider: .codex,
|
|
settings: self,
|
|
override: tokenOverride)
|
|
else {
|
|
return fallback
|
|
}
|
|
return TokenAccountSupportCatalog.normalizedCookieHeader(account.token, support: support)
|
|
}
|
|
|
|
private func codexSnapshotCookieSource(tokenOverride: TokenAccountOverride?) -> ProviderCookieSource {
|
|
let fallback = self.codexCookieSource
|
|
guard let support = TokenAccountSupportCatalog.support(for: .codex),
|
|
support.requiresManualCookieSource
|
|
else {
|
|
return fallback
|
|
}
|
|
if self.tokenAccounts(for: .codex).isEmpty { return fallback }
|
|
return .manual
|
|
}
|
|
}
|