import Foundation public struct CodexResolvedActiveSource: Equatable, Sendable { public let persistedSource: CodexActiveSource public let resolvedSource: CodexActiveSource public init(persistedSource: CodexActiveSource, resolvedSource: CodexActiveSource) { self.persistedSource = persistedSource self.resolvedSource = resolvedSource } public var requiresPersistenceCorrection: Bool { self.persistedSource != self.resolvedSource } } public enum CodexActiveSourceResolver { public static func resolve(from snapshot: CodexAccountReconciliationSnapshot) -> CodexResolvedActiveSource { let persistedSource = snapshot.activeSource let resolvedSource: CodexActiveSource = switch persistedSource { case .liveSystem: .liveSystem case let .managedAccount(id): if let activeStoredAccount = snapshot.activeStoredAccount { self.resolvedSource(for: activeStoredAccount, snapshot: snapshot) } else { snapshot.liveSystemAccount != nil ? .liveSystem : .managedAccount(id: id) } case let .profileHome(path): if let normalizedPath = snapshot.configuredProfileHomePath(path: path) { self.resolvedProfileSource(path: normalizedPath, snapshot: snapshot) } else { .liveSystem } } return CodexResolvedActiveSource( persistedSource: persistedSource, resolvedSource: resolvedSource) } private static func resolvedProfileSource( path: String, snapshot: CodexAccountReconciliationSnapshot) -> CodexActiveSource { if let livePath = snapshot.liveSystemAccount.flatMap({ CodexHomeScope.normalizedHomePath($0.codexHomePath) }), livePath == path { return .liveSystem } if let storedAccount = snapshot.storedAccounts.first(where: { CodexHomeScope.normalizedHomePath($0.managedHomePath) == path }) { return self.resolvedSource(for: storedAccount, snapshot: snapshot) } return .profileHome(path: path) } private static func resolvedSource( for storedAccount: ManagedCodexAccount, snapshot: CodexAccountReconciliationSnapshot) -> CodexActiveSource { self.matchesLiveSystemAccount( storedAccount: storedAccount, snapshot: snapshot, liveSystemAccount: snapshot.liveSystemAccount) ? .liveSystem : .managedAccount(id: storedAccount.id) } private static func matchesLiveSystemAccount( storedAccount: ManagedCodexAccount, snapshot: CodexAccountReconciliationSnapshot, liveSystemAccount: ObservedSystemCodexAccount?) -> Bool { guard let liveSystemAccount else { return false } if let storedFingerprint = storedAccount.authFingerprint, let liveFingerprint = liveSystemAccount.authFingerprint, storedFingerprint == liveFingerprint { return true } return CodexIdentityMatcher.matches( snapshot.runtimeIdentity(for: storedAccount), lhsEmail: snapshot.runtimeEmail(for: storedAccount), snapshot.runtimeIdentity(for: liveSystemAccount), rhsEmail: liveSystemAccount.email) } } public struct CodexAccountReconciliationSnapshot: Equatable, Sendable { public let storedAccounts: [ManagedCodexAccount] public let activeStoredAccount: ManagedCodexAccount? public let liveSystemAccount: ObservedSystemCodexAccount? public let profileHomeAccounts: [ObservedSystemCodexAccount] public let profileHomePaths: [String] public let matchingStoredAccountForLiveSystemAccount: ManagedCodexAccount? public let activeSource: CodexActiveSource public let hasUnreadableAddedAccountStore: Bool public let storedAccountRuntimeIdentities: [UUID: CodexIdentity] public let storedAccountRuntimeEmails: [UUID: String] public init( storedAccounts: [ManagedCodexAccount], activeStoredAccount: ManagedCodexAccount?, liveSystemAccount: ObservedSystemCodexAccount?, profileHomeAccounts: [ObservedSystemCodexAccount] = [], profileHomePaths: [String]? = nil, matchingStoredAccountForLiveSystemAccount: ManagedCodexAccount?, activeSource: CodexActiveSource, hasUnreadableAddedAccountStore: Bool, storedAccountRuntimeIdentities: [UUID: CodexIdentity] = [:], storedAccountRuntimeEmails: [UUID: String] = [:]) { self.storedAccounts = storedAccounts self.activeStoredAccount = activeStoredAccount self.liveSystemAccount = liveSystemAccount self.profileHomeAccounts = Self.uniqueProfileHomeAccounts(profileHomeAccounts) self.profileHomePaths = Self.uniqueNormalizedPaths( profileHomePaths ?? profileHomeAccounts.map(\.codexHomePath)) self.matchingStoredAccountForLiveSystemAccount = matchingStoredAccountForLiveSystemAccount self.activeSource = activeSource self.hasUnreadableAddedAccountStore = hasUnreadableAddedAccountStore self.storedAccountRuntimeIdentities = storedAccountRuntimeIdentities self.storedAccountRuntimeEmails = storedAccountRuntimeEmails } public static func == (lhs: CodexAccountReconciliationSnapshot, rhs: CodexAccountReconciliationSnapshot) -> Bool { lhs.storedAccounts.map(AccountIdentity.init) == rhs.storedAccounts.map(AccountIdentity.init) && lhs.activeStoredAccount.map(AccountIdentity.init) == rhs.activeStoredAccount.map(AccountIdentity.init) && lhs.liveSystemAccount == rhs.liveSystemAccount && lhs.profileHomeAccounts == rhs.profileHomeAccounts && lhs.profileHomePaths == rhs.profileHomePaths && lhs.matchingStoredAccountForLiveSystemAccount.map(AccountIdentity.init) == rhs.matchingStoredAccountForLiveSystemAccount.map(AccountIdentity.init) && lhs.activeSource == rhs.activeSource && lhs.hasUnreadableAddedAccountStore == rhs.hasUnreadableAddedAccountStore && lhs.storedAccountRuntimeIdentities == rhs.storedAccountRuntimeIdentities && lhs.storedAccountRuntimeEmails == rhs.storedAccountRuntimeEmails } public func runtimeIdentity(for storedAccount: ManagedCodexAccount) -> CodexIdentity { self.storedAccountRuntimeIdentities[storedAccount.id] ?? CodexIdentityResolver.resolve(accountId: nil, email: storedAccount.email) } public func runtimeEmail(for storedAccount: ManagedCodexAccount) -> String { self.storedAccountRuntimeEmails[storedAccount.id] ?? Self.normalizeEmail(storedAccount.email) } public func runtimeIdentity(for liveSystemAccount: ObservedSystemCodexAccount) -> CodexIdentity { CodexIdentityMatcher.normalized( liveSystemAccount.identity, fallbackEmail: liveSystemAccount.email) } public func profileHomeAccount(path: String) -> ObservedSystemCodexAccount? { guard let normalizedPath = CodexHomeScope.normalizedHomePath(path) else { return nil } return self.profileHomeAccounts.first { CodexHomeScope.normalizedHomePath($0.codexHomePath) == normalizedPath } } public func configuredProfileHomePath(path: String) -> String? { guard let normalizedPath = CodexHomeScope.normalizedHomePath(path), self.profileHomePaths.contains(normalizedPath) else { return nil } return normalizedPath } private static func normalizeEmail(_ email: String) -> String { email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } private static func uniqueProfileHomeAccounts( _ accounts: [ObservedSystemCodexAccount]) -> [ObservedSystemCodexAccount] { var seenPaths: Set = [] var result: [ObservedSystemCodexAccount] = [] for account in accounts { let path = CodexHomeScope.normalizedHomePath(account.codexHomePath) ?? account.codexHomePath guard seenPaths.insert(path).inserted else { continue } result.append(account) } return result } private static func uniqueNormalizedPaths(_ paths: [String]) -> [String] { var seen: Set = [] return paths.compactMap { path in guard let normalizedPath = CodexHomeScope.normalizedHomePath(path), seen.insert(normalizedPath).inserted else { return nil } return normalizedPath } } } public struct DefaultCodexAccountReconciler: Sendable { public let storeLoader: @Sendable () throws -> ManagedCodexAccountSet public let systemObserver: any CodexSystemAccountObserving public let activeSource: CodexActiveSource public let baseEnvironment: [String: String] public let profileHomePaths: [String] public let managedEnvironmentBuilder: @Sendable ([String: String], ManagedCodexAccount) -> [String: String] public init( storeLoader: @escaping @Sendable () throws -> ManagedCodexAccountSet = { try FileManagedCodexAccountStore().loadAccounts() }, systemObserver: any CodexSystemAccountObserving = DefaultCodexSystemAccountObserver(), activeSource: CodexActiveSource = .liveSystem, baseEnvironment: [String: String], profileHomePaths: [String] = [], managedEnvironmentBuilder: @escaping @Sendable ([String: String], ManagedCodexAccount) -> [String: String] = { baseEnvironment, account in CodexHomeScope.scopedEnvironment(base: baseEnvironment, codexHome: account.managedHomePath) }) { self.storeLoader = storeLoader self.systemObserver = systemObserver self.activeSource = activeSource self.baseEnvironment = baseEnvironment self.profileHomePaths = Self.uniqueNormalizedPaths(profileHomePaths) self.managedEnvironmentBuilder = managedEnvironmentBuilder } public func loadSnapshot() -> CodexAccountReconciliationSnapshot { let liveSystemAccount = self.loadLiveSystemAccount() let profileHomeAccounts = self.loadProfileHomeAccounts(liveSystemAccount: liveSystemAccount) do { let accounts = try self.storeLoader() let runtimeAccounts = Dictionary(uniqueKeysWithValues: accounts.accounts.map { account in let runtimeAccount = self.loadRuntimeAccount(for: account) return (account.id, runtimeAccount) }) let activeStoredAccount: ManagedCodexAccount? = switch self.activeSource { case let .managedAccount(id): accounts.account(id: id) case .liveSystem, .profileHome: nil } let matchingStoredAccountForLiveSystemAccount = liveSystemAccount.flatMap { liveAccount in if let liveFingerprint = liveAccount.authFingerprint, let exactFingerprintMatch = accounts.accounts.first(where: { $0.authFingerprint == liveFingerprint }) { return exactFingerprintMatch } return accounts.accounts.first { account in guard let runtimeAccount = runtimeAccounts[account.id] else { return false } return CodexIdentityMatcher.matches( runtimeAccount.identity, lhsEmail: runtimeAccount.email, self.runtimeIdentity(for: liveAccount), rhsEmail: liveAccount.email) } } return CodexAccountReconciliationSnapshot( storedAccounts: accounts.accounts, activeStoredAccount: activeStoredAccount, liveSystemAccount: liveSystemAccount, profileHomeAccounts: self.profileHomeAccounts( profileHomeAccounts, excludingManagedAccounts: accounts.accounts), profileHomePaths: self.profileHomePaths, matchingStoredAccountForLiveSystemAccount: matchingStoredAccountForLiveSystemAccount, activeSource: self.activeSource, hasUnreadableAddedAccountStore: false, storedAccountRuntimeIdentities: runtimeAccounts.mapValues(\.identity), storedAccountRuntimeEmails: runtimeAccounts.mapValues(\.email)) } catch { return CodexAccountReconciliationSnapshot( storedAccounts: [], activeStoredAccount: nil, liveSystemAccount: liveSystemAccount, profileHomeAccounts: profileHomeAccounts, profileHomePaths: self.profileHomePaths, matchingStoredAccountForLiveSystemAccount: nil, activeSource: self.activeSource, hasUnreadableAddedAccountStore: true) } } private func loadProfileHomeAccounts( liveSystemAccount: ObservedSystemCodexAccount?) -> [ObservedSystemCodexAccount] { let livePath = liveSystemAccount.flatMap { CodexHomeScope.normalizedHomePath($0.codexHomePath) } return self.profileHomePaths.compactMap { path in guard path != livePath else { return nil } return self.loadProfileHomeAccount(homePath: path) } } private func loadProfileHomeAccount(homePath: String) -> ObservedSystemCodexAccount? { let environment = CodexHomeScope.scopedEnvironment(base: self.baseEnvironment, codexHome: homePath) let account = UsageFetcher(environment: environment).loadAuthBackedCodexAccount() guard let rawEmail = account.email?.trimmingCharacters(in: .whitespacesAndNewlines), !rawEmail.isEmpty else { return nil } let providerAccountID: String? = switch account.identity { case let .providerAccount(id): ManagedCodexAccount.normalizeProviderAccountID(id) case .emailOnly, .unresolved: nil } return ObservedSystemCodexAccount( email: rawEmail.lowercased(), workspaceAccountID: providerAccountID, authFingerprint: CodexAuthFingerprint.fingerprint(homePath: homePath), codexHomePath: homePath, observedAt: Date(), identity: account.identity) } private func loadLiveSystemAccount() -> ObservedSystemCodexAccount? { do { guard let account = try self.systemObserver.loadSystemAccount(environment: self.baseEnvironment) else { return nil } let normalizedEmail = Self.normalizeEmail(account.email) guard !normalizedEmail.isEmpty else { return nil } return ObservedSystemCodexAccount( email: normalizedEmail, workspaceLabel: account.workspaceLabel, workspaceAccountID: account.workspaceAccountID, authFingerprint: account.authFingerprint, codexHomePath: account.codexHomePath, observedAt: account.observedAt, identity: self.runtimeIdentity(for: account)) } catch { return nil } } private static func normalizeEmail(_ email: String) -> String { email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } private func loadRuntimeAccount(for account: ManagedCodexAccount) -> RuntimeManagedCodexAccount { let scopedEnvironment = self.managedEnvironmentBuilder(self.baseEnvironment, account) let authBackedAccount = UsageFetcher(environment: scopedEnvironment).loadAuthBackedCodexAccount() let email = Self.normalizeEmail(authBackedAccount.email ?? account.email) let identity = CodexIdentityMatcher.normalized(authBackedAccount.identity, fallbackEmail: email) return RuntimeManagedCodexAccount( email: email, identity: identity) } private func profileHomeAccounts( _ profileHomeAccounts: [ObservedSystemCodexAccount], excludingManagedAccounts managedAccounts: [ManagedCodexAccount]) -> [ObservedSystemCodexAccount] { let managedPaths = Set(managedAccounts.compactMap { CodexHomeScope.normalizedHomePath($0.managedHomePath) }) return profileHomeAccounts.filter { account in guard let path = CodexHomeScope.normalizedHomePath(account.codexHomePath) else { return false } return !managedPaths.contains(path) } } private func runtimeIdentity(for liveSystemAccount: ObservedSystemCodexAccount) -> CodexIdentity { CodexIdentityMatcher.normalized( liveSystemAccount.identity, fallbackEmail: liveSystemAccount.email) } private static func uniqueNormalizedPaths(_ paths: [String]) -> [String] { var seen: Set = [] var result: [String] = [] for path in paths.compactMap({ CodexHomeScope.normalizedHomePath($0) }) { guard seen.insert(path).inserted else { continue } result.append(path) } return result } } public enum CodexIdentityMatcher { public static func matches(_ lhs: CodexIdentity, _ rhs: CodexIdentity) -> Bool { switch (lhs, rhs) { case let (.providerAccount(leftID), .providerAccount(rightID)): leftID == rightID case let (.emailOnly(leftEmail), .emailOnly(rightEmail)): leftEmail == rightEmail default: false } } public static func matches( _ lhs: CodexIdentity, lhsEmail: String?, _ rhs: CodexIdentity, rhsEmail: String?) -> Bool { guard self.matches(lhs, rhs) else { return false } guard case .providerAccount = lhs, case .providerAccount = rhs else { return true } guard let normalizedLeftEmail = CodexIdentityResolver.normalizeEmail(lhsEmail), let normalizedRightEmail = CodexIdentityResolver.normalizeEmail(rhsEmail) else { return true } return normalizedLeftEmail == normalizedRightEmail } public static func normalized(_ identity: CodexIdentity, fallbackEmail: String) -> CodexIdentity { switch identity { case .providerAccount: identity case let .emailOnly(normalizedEmail): CodexIdentityResolver.resolve(accountId: nil, email: normalizedEmail) case .unresolved: CodexIdentityResolver.resolve(accountId: nil, email: fallbackEmail) } } public static func selectionKey(for identity: CodexIdentity, fallbackEmail: String) -> String { switch self.normalized(identity, fallbackEmail: fallbackEmail) { case let .providerAccount(id): "provider:\(id)" case let .emailOnly(normalizedEmail): "email:\(normalizedEmail)" case .unresolved: "unresolved:\(fallbackEmail)" } } } private struct RuntimeManagedCodexAccount { let email: String let identity: CodexIdentity } private struct AccountIdentity: Equatable { let id: UUID let email: String let providerAccountID: String? let workspaceLabel: String? let workspaceAccountID: String? let managedHomePath: String let createdAt: TimeInterval let updatedAt: TimeInterval let lastAuthenticatedAt: TimeInterval? let authFingerprint: String? init(_ account: ManagedCodexAccount) { self.id = account.id self.email = account.email self.providerAccountID = account.providerAccountID self.workspaceLabel = account.workspaceLabel self.workspaceAccountID = account.workspaceAccountID self.managedHomePath = account.managedHomePath self.createdAt = account.createdAt self.updatedAt = account.updatedAt self.lastAuthenticatedAt = account.lastAuthenticatedAt self.authFingerprint = account.authFingerprint } }