import CodexBarCoreimport Darwinimport Foundation @MainActorprotocol CodexAccountReconciliationSnapshotLoading { func loadSnapshot() -> CodexAccountReconciliationSnapshot} protocol CodexAuthMaterialReading: Sendable { func readAuthData(homeURL: URL) throws -> Data?} protocol CodexLiveAuthSwapping: Sendable { func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws} @MainActorprotocol CodexActiveSourceWriting { func writeCodexActiveSource(_ source: CodexActiveSource)} @MainActorprotocol CodexAccountScopedRefreshing { func refreshCodexAccountScopedState(allowDisabled: Bool) async} @MainActorstruct SettingsStoreCodexAccountReconciliationSnapshotLoader: CodexAccountReconciliationSnapshotLoading { private let settingsStore: SettingsStore init(settingsStore: SettingsStore) { self.settingsStore = settingsStore } func loadSnapshot() -> CodexAccountReconciliationSnapshot { self.settingsStore.codexAccountReconciliationSnapshot }} struct DefaultCodexAuthMaterialReader: CodexAuthMaterialReading { func readAuthData(homeURL: URL) throws -> Data? { let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL) guard FileManager.default.fileExists(atPath: authFileURL.path) else { return nil } return try Data(contentsOf: authFileURL) }} struct DefaultCodexLiveAuthSwapper: CodexLiveAuthSwapping { func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws { try FileManager.default.createDirectory(at: liveHomeURL, withIntermediateDirectories: true) let liveAuthURL = CodexAccountPromotionService.authFileURL(for: liveHomeURL) let stagedAuthURL = liveHomeURL.appendingPathComponent( "auth.json.codexbar-staged-\(UUID().uuidString)", isDirectory: false) do { try data.write(to: stagedAuthURL) try FileManager.default.setAttributes( [.posixPermissions: NSNumber(value: Int16(0o600))], ofItemAtPath: stagedAuthURL.path) try self.renameItem(at: stagedAuthURL, to: liveAuthURL) } catch { try? FileManager.default.removeItem(at: stagedAuthURL) throw error } } private func renameItem(at sourceURL: URL, to destinationURL: URL) throws { let sourcePath = sourceURL.path let destinationPath = destinationURL.path let result = sourcePath.withCString { sourceFS in destinationPath.withCString { destinationFS in rename(sourceFS, destinationFS) } } guard result == 0 else { throw NSError( domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSFilePathErrorKey: destinationPath]) } }} @MainActorstruct SettingsStoreCodexActiveSourceWriter: CodexActiveSourceWriting { private let settingsStore: SettingsStore init(settingsStore: SettingsStore) { self.settingsStore = settingsStore } func writeCodexActiveSource(_ source: CodexActiveSource) { self.settingsStore.codexActiveSource = source }} @MainActorstruct UsageStoreCodexAccountScopedRefresher: CodexAccountScopedRefreshing { private let usageStore: UsageStore init(usageStore: UsageStore) { self.usageStore = usageStore } func refreshCodexAccountScopedState(allowDisabled: Bool) async { await self.usageStore.refreshCodexAccountScopedState(allowDisabled: allowDisabled) }} struct CodexAccountPromotionResult: Equatable { enum Outcome: Equatable { case promoted case convergedNoOp } enum DisplacedLiveDisposition: Equatable { case none case alreadyManaged(managedAccountID: UUID) case imported(managedAccountID: UUID) } let targetManagedAccountID: UUID let outcome: Outcome let displacedLiveDisposition: DisplacedLiveDisposition let didMutateLiveAuth: Bool let resultingActiveSource: CodexActiveSource} enum CodexAccountPromotionError: Error, Equatable { case targetManagedAccountNotFound case targetManagedAccountAuthMissing case targetManagedAccountAuthUnreadable case liveAccountUnreadable case liveAccountMissingIdentityForPreservation case liveAccountAPIKeyOnlyUnsupported case displacedLiveManagedAccountConflict case displacedLiveImportFailed case managedStoreCommitFailed case liveAuthSwapFailed} @MainActorfinal class CodexAccountPromotionService { private let store: any ManagedCodexAccountStoring private let homeFactory: any ManagedCodexHomeProducing private let identityReader: any ManagedCodexIdentityReading private let workspaceResolver: any ManagedCodexWorkspaceResolving private let snapshotLoader: any CodexAccountReconciliationSnapshotLoading private let authMaterialReader: any CodexAuthMaterialReading private let liveAuthSwapper: any CodexLiveAuthSwapping private let activeSourceWriter: any CodexActiveSourceWriting private let accountScopedRefresher: any CodexAccountScopedRefreshing private let baseEnvironment: [String: String] private let fileManager: FileManager init( store: any ManagedCodexAccountStoring, homeFactory: any ManagedCodexHomeProducing, identityReader: any ManagedCodexIdentityReading, workspaceResolver: any ManagedCodexWorkspaceResolving = DefaultManagedCodexWorkspaceResolver(), snapshotLoader: any CodexAccountReconciliationSnapshotLoading, authMaterialReader: any CodexAuthMaterialReading, liveAuthSwapper: any CodexLiveAuthSwapping, activeSourceWriter: any CodexActiveSourceWriting, accountScopedRefresher: any CodexAccountScopedRefreshing, baseEnvironment: [String: String] = ProcessInfo.processInfo.environment, fileManager: FileManager = .default) { self.store = store self.homeFactory = homeFactory self.identityReader = identityReader self.workspaceResolver = workspaceResolver self.snapshotLoader = snapshotLoader self.authMaterialReader = authMaterialReader self.liveAuthSwapper = liveAuthSwapper self.activeSourceWriter = activeSourceWriter self.accountScopedRefresher = accountScopedRefresher self.baseEnvironment = baseEnvironment self.fileManager = fileManager } convenience init( settingsStore: SettingsStore, usageStore: UsageStore, baseEnvironment: [String: String] = ProcessInfo.processInfo.environment, fileManager: FileManager = .default) { self.init( store: FileManagedCodexAccountStore(fileManager: fileManager), homeFactory: ManagedCodexHomeFactory(fileManager: fileManager), identityReader: DefaultManagedCodexIdentityReader(), workspaceResolver: DefaultManagedCodexWorkspaceResolver(), snapshotLoader: SettingsStoreCodexAccountReconciliationSnapshotLoader(settingsStore: settingsStore), authMaterialReader: DefaultCodexAuthMaterialReader(), liveAuthSwapper: DefaultCodexLiveAuthSwapper(), activeSourceWriter: SettingsStoreCodexActiveSourceWriter(settingsStore: settingsStore), accountScopedRefresher: UsageStoreCodexAccountScopedRefresher(usageStore: usageStore), baseEnvironment: baseEnvironment, fileManager: fileManager) } func promoteManagedAccount(id: UUID) async throws -> CodexAccountPromotionResult { let contextBuilder = PreparedPromotionContextBuilder( store: self.store, workspaceResolver: self.workspaceResolver, snapshotLoader: self.snapshotLoader, authMaterialReader: self.authMaterialReader, baseEnvironment: self.baseEnvironment, fileManager: self.fileManager) let context = try await contextBuilder.build(targetID: id) if let resultingActiveSource = self.convergedActiveSource(for: context) { self.activeSourceWriter.writeCodexActiveSource(resultingActiveSource) await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true) return CodexAccountPromotionResult( targetManagedAccountID: id, outcome: .convergedNoOp, displacedLiveDisposition: .none, didMutateLiveAuth: false, resultingActiveSource: resultingActiveSource) } let targetAuthMaterial = try self.requiredTargetAuthMaterial(from: context.target) let preservationPlan = CodexDisplacedLivePreservationPlanner().makePlan(context: context) let executionResult = try CodexDisplacedLivePreservationExecutor( store: self.store, homeFactory: self.homeFactory, fileManager: self.fileManager) .execute(plan: preservationPlan, context: context) do { try self.liveAuthSwapper.swapLiveAuthData(targetAuthMaterial.rawData, liveHomeURL: context.live.homeURL) } catch { throw CodexAccountPromotionError.liveAuthSwapFailed } self.activeSourceWriter.writeCodexActiveSource(.liveSystem) await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true) return CodexAccountPromotionResult( targetManagedAccountID: id, outcome: .promoted, displacedLiveDisposition: executionResult.displacedLiveDisposition, didMutateLiveAuth: true, resultingActiveSource: .liveSystem) } nonisolated static func authFileURL(for homeURL: URL) -> URL { homeURL.appendingPathComponent("auth.json", isDirectory: false) } private func convergedActiveSource(for context: PreparedPromotionContext) -> CodexActiveSource? { if let liveAuthIdentity = context.live.authIdentity { let targetIdentity = context.target.authIdentity ?? context.target.persistedIdentity guard CodexIdentityMatcher.matches(targetIdentity.identity, liveAuthIdentity.identity) else { return nil } if liveAuthIdentity.email != nil { return .liveSystem } if liveAuthIdentity.providerAccountID != nil { return .managedAccount(id: context.target.persisted.id) } return nil } guard let liveSystemAccount = context.snapshot.liveSystemAccount else { return nil } guard CodexIdentityMatcher.matches( context.snapshot.runtimeIdentity(for: context.target.persisted), context.snapshot.runtimeIdentity(for: liveSystemAccount)) else { return nil } return .liveSystem } private func requiredTargetAuthMaterial(from target: PreparedStoredManagedAccount) throws -> PreparedAuthMaterial { switch target.homeState { case let .readable(authMaterial): guard authMaterial.authIdentity.email != nil else { throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable } return authMaterial case .missing: throw CodexAccountPromotionError.targetManagedAccountAuthMissing case .unreadable: throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable } }}
import CodexBarCore
import Darwin
import Foundation
@MainActor
protocol CodexAccountReconciliationSnapshotLoading {
func loadSnapshot() -> CodexAccountReconciliationSnapshot
}
protocol CodexAuthMaterialReading: Sendable {
func readAuthData(homeURL: URL) throws -> Data?
}
protocol CodexLiveAuthSwapping: Sendable {
func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws
}
@MainActor
protocol CodexActiveSourceWriting {
func writeCodexActiveSource(_ source: CodexActiveSource)
}
@MainActor
protocol CodexAccountScopedRefreshing {
func refreshCodexAccountScopedState(allowDisabled: Bool) async
}
@MainActor
struct SettingsStoreCodexAccountReconciliationSnapshotLoader: CodexAccountReconciliationSnapshotLoading {
private let settingsStore: SettingsStore
init(settingsStore: SettingsStore) {
self.settingsStore = settingsStore
}
func loadSnapshot() -> CodexAccountReconciliationSnapshot {
self.settingsStore.codexAccountReconciliationSnapshot
}
}
struct DefaultCodexAuthMaterialReader: CodexAuthMaterialReading {
func readAuthData(homeURL: URL) throws -> Data? {
let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL)
guard FileManager.default.fileExists(atPath: authFileURL.path) else {
return nil
}
return try Data(contentsOf: authFileURL)
}
}
struct DefaultCodexLiveAuthSwapper: CodexLiveAuthSwapping {
func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws {
try FileManager.default.createDirectory(at: liveHomeURL, withIntermediateDirectories: true)
let liveAuthURL = CodexAccountPromotionService.authFileURL(for: liveHomeURL)
let stagedAuthURL = liveHomeURL.appendingPathComponent(
"auth.json.codexbar-staged-\(UUID().uuidString)",
isDirectory: false)
do {
try data.write(to: stagedAuthURL)
try FileManager.default.setAttributes(
[.posixPermissions: NSNumber(value: Int16(0o600))],
ofItemAtPath: stagedAuthURL.path)
try self.renameItem(at: stagedAuthURL, to: liveAuthURL)
} catch {
try? FileManager.default.removeItem(at: stagedAuthURL)
throw error
}
}
private func renameItem(at sourceURL: URL, to destinationURL: URL) throws {
let sourcePath = sourceURL.path
let destinationPath = destinationURL.path
let result = sourcePath.withCString { sourceFS in
destinationPath.withCString { destinationFS in
rename(sourceFS, destinationFS)
}
}
guard result == 0 else {
throw NSError(
domain: NSPOSIXErrorDomain,
code: Int(errno),
userInfo: [NSFilePathErrorKey: destinationPath])
}
}
}
@MainActor
struct SettingsStoreCodexActiveSourceWriter: CodexActiveSourceWriting {
private let settingsStore: SettingsStore
init(settingsStore: SettingsStore) {
self.settingsStore = settingsStore
}
func writeCodexActiveSource(_ source: CodexActiveSource) {
self.settingsStore.codexActiveSource = source
}
}
@MainActor
struct UsageStoreCodexAccountScopedRefresher: CodexAccountScopedRefreshing {
private let usageStore: UsageStore
init(usageStore: UsageStore) {
self.usageStore = usageStore
}
func refreshCodexAccountScopedState(allowDisabled: Bool) async {
await self.usageStore.refreshCodexAccountScopedState(allowDisabled: allowDisabled)
}
}
struct CodexAccountPromotionResult: Equatable {
enum Outcome: Equatable {
case promoted
case convergedNoOp
}
enum DisplacedLiveDisposition: Equatable {
case none
case alreadyManaged(managedAccountID: UUID)
case imported(managedAccountID: UUID)
}
let targetManagedAccountID: UUID
let outcome: Outcome
let displacedLiveDisposition: DisplacedLiveDisposition
let didMutateLiveAuth: Bool
let resultingActiveSource: CodexActiveSource
}
enum CodexAccountPromotionError: Error, Equatable {
case targetManagedAccountNotFound
case targetManagedAccountAuthMissing
case targetManagedAccountAuthUnreadable
case liveAccountUnreadable
case liveAccountMissingIdentityForPreservation
case liveAccountAPIKeyOnlyUnsupported
case displacedLiveManagedAccountConflict
case displacedLiveImportFailed
case managedStoreCommitFailed
case liveAuthSwapFailed
}
@MainActor
final class CodexAccountPromotionService {
private let store: any ManagedCodexAccountStoring
private let homeFactory: any ManagedCodexHomeProducing
private let identityReader: any ManagedCodexIdentityReading
private let workspaceResolver: any ManagedCodexWorkspaceResolving
private let snapshotLoader: any CodexAccountReconciliationSnapshotLoading
private let authMaterialReader: any CodexAuthMaterialReading
private let liveAuthSwapper: any CodexLiveAuthSwapping
private let activeSourceWriter: any CodexActiveSourceWriting
private let accountScopedRefresher: any CodexAccountScopedRefreshing
private let baseEnvironment: [String: String]
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
homeFactory: any ManagedCodexHomeProducing,
identityReader: any ManagedCodexIdentityReading,
workspaceResolver: any ManagedCodexWorkspaceResolving = DefaultManagedCodexWorkspaceResolver(),
snapshotLoader: any CodexAccountReconciliationSnapshotLoading,
authMaterialReader: any CodexAuthMaterialReading,
liveAuthSwapper: any CodexLiveAuthSwapping,
activeSourceWriter: any CodexActiveSourceWriting,
accountScopedRefresher: any CodexAccountScopedRefreshing,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default)
{
self.store = store
self.homeFactory = homeFactory
self.identityReader = identityReader
self.workspaceResolver = workspaceResolver
self.snapshotLoader = snapshotLoader
self.authMaterialReader = authMaterialReader
self.liveAuthSwapper = liveAuthSwapper
self.activeSourceWriter = activeSourceWriter
self.accountScopedRefresher = accountScopedRefresher
self.baseEnvironment = baseEnvironment
self.fileManager = fileManager
}
convenience init(
settingsStore: SettingsStore,
usageStore: UsageStore,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default)
{
self.init(
store: FileManagedCodexAccountStore(fileManager: fileManager),
homeFactory: ManagedCodexHomeFactory(fileManager: fileManager),
identityReader: DefaultManagedCodexIdentityReader(),
workspaceResolver: DefaultManagedCodexWorkspaceResolver(),
snapshotLoader: SettingsStoreCodexAccountReconciliationSnapshotLoader(settingsStore: settingsStore),
authMaterialReader: DefaultCodexAuthMaterialReader(),
liveAuthSwapper: DefaultCodexLiveAuthSwapper(),
activeSourceWriter: SettingsStoreCodexActiveSourceWriter(settingsStore: settingsStore),
accountScopedRefresher: UsageStoreCodexAccountScopedRefresher(usageStore: usageStore),
baseEnvironment: baseEnvironment,
fileManager: fileManager)
}
func promoteManagedAccount(id: UUID) async throws -> CodexAccountPromotionResult {
let contextBuilder = PreparedPromotionContextBuilder(
store: self.store,
workspaceResolver: self.workspaceResolver,
snapshotLoader: self.snapshotLoader,
authMaterialReader: self.authMaterialReader,
baseEnvironment: self.baseEnvironment,
fileManager: self.fileManager)
let context = try await contextBuilder.build(targetID: id)
if let resultingActiveSource = self.convergedActiveSource(for: context) {
self.activeSourceWriter.writeCodexActiveSource(resultingActiveSource)
await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true)
return CodexAccountPromotionResult(
targetManagedAccountID: id,
outcome: .convergedNoOp,
displacedLiveDisposition: .none,
didMutateLiveAuth: false,
resultingActiveSource: resultingActiveSource)
}
let targetAuthMaterial = try self.requiredTargetAuthMaterial(from: context.target)
let preservationPlan = CodexDisplacedLivePreservationPlanner().makePlan(context: context)
let executionResult = try CodexDisplacedLivePreservationExecutor(
store: self.store,
homeFactory: self.homeFactory,
fileManager: self.fileManager)
.execute(plan: preservationPlan, context: context)
do {
try self.liveAuthSwapper.swapLiveAuthData(targetAuthMaterial.rawData, liveHomeURL: context.live.homeURL)
} catch {
throw CodexAccountPromotionError.liveAuthSwapFailed
}
self.activeSourceWriter.writeCodexActiveSource(.liveSystem)
await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true)
return CodexAccountPromotionResult(
targetManagedAccountID: id,
outcome: .promoted,
displacedLiveDisposition: executionResult.displacedLiveDisposition,
didMutateLiveAuth: true,
resultingActiveSource: .liveSystem)
}
nonisolated static func authFileURL(for homeURL: URL) -> URL {
homeURL.appendingPathComponent("auth.json", isDirectory: false)
}
private func convergedActiveSource(for context: PreparedPromotionContext) -> CodexActiveSource? {
if let liveAuthIdentity = context.live.authIdentity {
let targetIdentity = context.target.authIdentity ?? context.target.persistedIdentity
guard CodexIdentityMatcher.matches(targetIdentity.identity, liveAuthIdentity.identity) else {
return nil
}
if liveAuthIdentity.email != nil {
return .liveSystem
}
if liveAuthIdentity.providerAccountID != nil {
return .managedAccount(id: context.target.persisted.id)
}
return nil
}
guard let liveSystemAccount = context.snapshot.liveSystemAccount else {
return nil
}
guard CodexIdentityMatcher.matches(
context.snapshot.runtimeIdentity(for: context.target.persisted),
context.snapshot.runtimeIdentity(for: liveSystemAccount))
else {
return nil
}
return .liveSystem
}
private func requiredTargetAuthMaterial(from target: PreparedStoredManagedAccount) throws -> PreparedAuthMaterial {
switch target.homeState {
case let .readable(authMaterial):
guard authMaterial.authIdentity.email != nil else {
throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable
}
return authMaterial
case .missing:
throw CodexAccountPromotionError.targetManagedAccountAuthMissing
case .unreadable:
throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable
}
}
}