import CodexBarCoreimport Foundation private struct CodexPreparedImportedAccount { let account: ManagedCodexAccount let homeURL: URL} struct CodexDisplacedLivePreservationExecutionResult: Equatable { let displacedLiveDisposition: CodexAccountPromotionResult.DisplacedLiveDisposition} @MainActorstruct CodexDisplacedLivePreservationExecutor { private let store: any ManagedCodexAccountStoring private let homeFactory: any ManagedCodexHomeProducing private let fileManager: FileManager init( store: any ManagedCodexAccountStoring, homeFactory: any ManagedCodexHomeProducing, fileManager: FileManager = .default) { self.store = store self.homeFactory = homeFactory self.fileManager = fileManager } func execute( plan: CodexDisplacedLivePreservationPlan, context: PreparedPromotionContext) throws -> CodexDisplacedLivePreservationExecutionResult { /* Safety contract: - This executor never swaps live auth. The caller must do that only after success. - Import cleanup is best-effort and leaves no orphaned managed home on failure. - Refresh/repair may copy auth before store commit, matching current behavior. */ switch plan { case .none: return CodexDisplacedLivePreservationExecutionResult(displacedLiveDisposition: .none) case let .reject(reason): throw self.error(for: reason) case .importNew: let importedAccount = try self.importDisplacedLiveAccount(from: context) return try self.commitImportedAccount(importedAccount) case let .refreshExisting(destination, _), let .repairExisting(destination, _): guard destination.persisted.id != context.target.persisted.id else { throw CodexAccountPromotionError.managedStoreCommitFailed } let refreshed = try self.refreshExistingManagedAccount(destination, from: context) return CodexDisplacedLivePreservationExecutionResult( displacedLiveDisposition: .alreadyManaged(managedAccountID: refreshed.id)) } } private func error(for reason: CodexDisplacedLivePreservationRejectReason) -> CodexAccountPromotionError { switch reason { case .liveUnreadable: .liveAccountUnreadable case .liveAPIKeyOnlyUnsupported: .liveAccountAPIKeyOnlyUnsupported case .liveIdentityMissingForPreservation: .liveAccountMissingIdentityForPreservation case .conflictingReadableManagedHome: .displacedLiveManagedAccountConflict } } private func importDisplacedLiveAccount( from context: PreparedPromotionContext) throws -> CodexPreparedImportedAccount { guard case let .readable(liveAuthMaterial) = context.live.homeState else { throw CodexAccountPromotionError.displacedLiveImportFailed } let importedHomeURL = self.homeFactory.makeHomeURL() let importedAccountID = Self.accountID(for: importedHomeURL) do { try self.fileManager.createDirectory(at: importedHomeURL, withIntermediateDirectories: true) try self.writeManagedAuthData(liveAuthMaterial.rawData, to: importedHomeURL) guard let liveAuthIdentity = context.live.authIdentity, let email = liveAuthIdentity.email, liveAuthIdentity.identity != .unresolved else { throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation } let now = Date().timeIntervalSince1970 return CodexPreparedImportedAccount( account: ManagedCodexAccount( id: importedAccountID, email: email, providerAccountID: liveAuthIdentity.providerAccountID, workspaceLabel: liveAuthIdentity.workspaceLabel, workspaceAccountID: liveAuthIdentity.workspaceAccountID, authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData), managedHomePath: importedHomeURL.path, createdAt: now, updatedAt: now, lastAuthenticatedAt: now), homeURL: importedHomeURL) } catch let error as CodexAccountPromotionError { try? self.removeManagedHomeIfSafe(importedHomeURL) throw error } catch { try? self.removeManagedHomeIfSafe(importedHomeURL) throw CodexAccountPromotionError.displacedLiveImportFailed } } private func commitImportedAccount(_ importedAccount: CodexPreparedImportedAccount) throws -> CodexDisplacedLivePreservationExecutionResult { do { let latestManagedAccounts = try self.store.loadAccounts() try self.store.storeAccounts(ManagedCodexAccountSet( version: latestManagedAccounts.version, accounts: latestManagedAccounts.accounts + [importedAccount.account])) return try self.resolveImportedAccountAfterCommit(importedAccount) } catch let error as CodexAccountPromotionError { try? self.removeManagedHomeIfSafe(importedAccount.homeURL) throw error } catch { try? self.removeManagedHomeIfSafe(importedAccount.homeURL) throw CodexAccountPromotionError.managedStoreCommitFailed } } private func resolveImportedAccountAfterCommit(_ importedAccount: CodexPreparedImportedAccount) throws -> CodexDisplacedLivePreservationExecutionResult { let persistedManagedAccounts = try self.store.loadAccounts() if persistedManagedAccounts.account(id: importedAccount.account.id) != nil { return CodexDisplacedLivePreservationExecutionResult( displacedLiveDisposition: .imported(managedAccountID: importedAccount.account.id)) } guard let existingManagedAccount = self.repairDestination( in: persistedManagedAccounts, for: importedAccount.account) else { throw CodexAccountPromotionError.managedStoreCommitFailed } let repairedManagedAccount = ManagedCodexAccount( id: existingManagedAccount.id, email: importedAccount.account.email, providerAccountID: importedAccount.account.providerAccountID, workspaceLabel: importedAccount.account.workspaceLabel, workspaceAccountID: importedAccount.account.workspaceAccountID, authFingerprint: importedAccount.account.authFingerprint, managedHomePath: importedAccount.homeURL.path, createdAt: existingManagedAccount.createdAt, updatedAt: importedAccount.account.updatedAt, lastAuthenticatedAt: importedAccount.account.lastAuthenticatedAt) try self.store.storeAccounts(ManagedCodexAccountSet( version: persistedManagedAccounts.version, accounts: persistedManagedAccounts.accounts.map { account in guard account.id == existingManagedAccount.id else { return account } return repairedManagedAccount })) if existingManagedAccount.managedHomePath != importedAccount.homeURL.path { try? self.removeManagedHomeIfSafe( URL(fileURLWithPath: existingManagedAccount.managedHomePath, isDirectory: true)) } return CodexDisplacedLivePreservationExecutionResult( displacedLiveDisposition: .alreadyManaged(managedAccountID: existingManagedAccount.id)) } private func repairDestination( in persistedManagedAccounts: ManagedCodexAccountSet, for importedAccount: ManagedCodexAccount) -> ManagedCodexAccount? { if let providerAccountID = importedAccount.providerAccountID { return persistedManagedAccounts.account( email: importedAccount.email, providerAccountID: providerAccountID) } let normalizedEmail = importedAccount.email .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() return persistedManagedAccounts.accounts.first { $0.email == normalizedEmail && $0.providerAccountID == nil } } private func refreshExistingManagedAccount( _ destination: PreparedStoredManagedAccount, from context: PreparedPromotionContext) throws -> ManagedCodexAccount { guard case let .readable(liveAuthMaterial) = context.live.homeState else { throw CodexAccountPromotionError.managedStoreCommitFailed } guard let liveAuthIdentity = context.live.authIdentity else { throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation } do { let latestManagedAccounts = try self.store.loadAccounts() guard let persistedManagedAccount = latestManagedAccounts.account(id: destination.persisted.id) else { throw CodexAccountPromotionError.managedStoreCommitFailed } let email = liveAuthIdentity.email ?? (liveAuthIdentity.providerAccountID != nil ? persistedManagedAccount.email : nil) guard let email, liveAuthIdentity.identity != .unresolved else { throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation } let now = Date().timeIntervalSince1970 let refreshedManagedAccount = ManagedCodexAccount( id: persistedManagedAccount.id, email: email, providerAccountID: liveAuthIdentity.providerAccountID ?? persistedManagedAccount.providerAccountID, workspaceLabel: liveAuthIdentity.workspaceLabel ?? persistedManagedAccount.workspaceLabel, workspaceAccountID: liveAuthIdentity.workspaceAccountID ?? persistedManagedAccount.workspaceAccountID, authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData), managedHomePath: persistedManagedAccount.managedHomePath, createdAt: persistedManagedAccount.createdAt, updatedAt: now, lastAuthenticatedAt: now) let refreshedHomeURL = URL(fileURLWithPath: persistedManagedAccount.managedHomePath, isDirectory: true) do { try self.homeFactory.validateManagedHomeForDeletion(refreshedHomeURL) } catch { throw CodexAccountPromotionError.displacedLiveImportFailed } try self.fileManager.createDirectory(at: refreshedHomeURL, withIntermediateDirectories: true) try self.writeManagedAuthData(liveAuthMaterial.rawData, to: refreshedHomeURL) try self.store.storeAccounts(ManagedCodexAccountSet( version: latestManagedAccounts.version, accounts: latestManagedAccounts.accounts.map { account in guard account.id == persistedManagedAccount.id else { return account } return refreshedManagedAccount })) return refreshedManagedAccount } catch let error as CodexAccountPromotionError { throw error } catch { throw CodexAccountPromotionError.managedStoreCommitFailed } } private func writeManagedAuthData(_ data: Data, to homeURL: URL) throws { let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL) try data.write(to: authFileURL, options: .atomic) try self.fileManager.setAttributes( [.posixPermissions: NSNumber(value: Int16(0o600))], ofItemAtPath: authFileURL.path) } private func removeManagedHomeIfSafe(_ homeURL: URL) throws { try self.homeFactory.validateManagedHomeForDeletion(homeURL) if self.fileManager.fileExists(atPath: homeURL.path) { try self.fileManager.removeItem(at: homeURL) } } private static func accountID(for homeURL: URL) -> UUID { UUID(uuidString: homeURL.lastPathComponent) ?? UUID() }}
import CodexBarCore
import Foundation
private struct CodexPreparedImportedAccount {
let account: ManagedCodexAccount
let homeURL: URL
}
struct CodexDisplacedLivePreservationExecutionResult: Equatable {
let displacedLiveDisposition: CodexAccountPromotionResult.DisplacedLiveDisposition
}
@MainActor
struct CodexDisplacedLivePreservationExecutor {
private let store: any ManagedCodexAccountStoring
private let homeFactory: any ManagedCodexHomeProducing
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
homeFactory: any ManagedCodexHomeProducing,
fileManager: FileManager = .default)
{
self.store = store
self.homeFactory = homeFactory
self.fileManager = fileManager
}
func execute(
plan: CodexDisplacedLivePreservationPlan,
context: PreparedPromotionContext) throws
-> CodexDisplacedLivePreservationExecutionResult
{
/*
Safety contract:
- This executor never swaps live auth. The caller must do that only after success.
- Import cleanup is best-effort and leaves no orphaned managed home on failure.
- Refresh/repair may copy auth before store commit, matching current behavior.
*/
switch plan {
case .none:
return CodexDisplacedLivePreservationExecutionResult(displacedLiveDisposition: .none)
case let .reject(reason):
throw self.error(for: reason)
case .importNew:
let importedAccount = try self.importDisplacedLiveAccount(from: context)
return try self.commitImportedAccount(importedAccount)
case let .refreshExisting(destination, _),
let .repairExisting(destination, _):
guard destination.persisted.id != context.target.persisted.id else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let refreshed = try self.refreshExistingManagedAccount(destination, from: context)
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .alreadyManaged(managedAccountID: refreshed.id))
}
}
private func error(for reason: CodexDisplacedLivePreservationRejectReason) -> CodexAccountPromotionError {
switch reason {
case .liveUnreadable:
.liveAccountUnreadable
case .liveAPIKeyOnlyUnsupported:
.liveAccountAPIKeyOnlyUnsupported
case .liveIdentityMissingForPreservation:
.liveAccountMissingIdentityForPreservation
case .conflictingReadableManagedHome:
.displacedLiveManagedAccountConflict
}
}
private func importDisplacedLiveAccount(
from context: PreparedPromotionContext) throws
-> CodexPreparedImportedAccount
{
guard case let .readable(liveAuthMaterial) = context.live.homeState else {
throw CodexAccountPromotionError.displacedLiveImportFailed
}
let importedHomeURL = self.homeFactory.makeHomeURL()
let importedAccountID = Self.accountID(for: importedHomeURL)
do {
try self.fileManager.createDirectory(at: importedHomeURL, withIntermediateDirectories: true)
try self.writeManagedAuthData(liveAuthMaterial.rawData, to: importedHomeURL)
guard let liveAuthIdentity = context.live.authIdentity,
let email = liveAuthIdentity.email,
liveAuthIdentity.identity != .unresolved
else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
let now = Date().timeIntervalSince1970
return CodexPreparedImportedAccount(
account: ManagedCodexAccount(
id: importedAccountID,
email: email,
providerAccountID: liveAuthIdentity.providerAccountID,
workspaceLabel: liveAuthIdentity.workspaceLabel,
workspaceAccountID: liveAuthIdentity.workspaceAccountID,
authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData),
managedHomePath: importedHomeURL.path,
createdAt: now,
updatedAt: now,
lastAuthenticatedAt: now),
homeURL: importedHomeURL)
} catch let error as CodexAccountPromotionError {
try? self.removeManagedHomeIfSafe(importedHomeURL)
throw error
} catch {
try? self.removeManagedHomeIfSafe(importedHomeURL)
throw CodexAccountPromotionError.displacedLiveImportFailed
}
}
private func commitImportedAccount(_ importedAccount: CodexPreparedImportedAccount) throws
-> CodexDisplacedLivePreservationExecutionResult
{
do {
let latestManagedAccounts = try self.store.loadAccounts()
try self.store.storeAccounts(ManagedCodexAccountSet(
version: latestManagedAccounts.version,
accounts: latestManagedAccounts.accounts + [importedAccount.account]))
return try self.resolveImportedAccountAfterCommit(importedAccount)
} catch let error as CodexAccountPromotionError {
try? self.removeManagedHomeIfSafe(importedAccount.homeURL)
throw error
} catch {
try? self.removeManagedHomeIfSafe(importedAccount.homeURL)
throw CodexAccountPromotionError.managedStoreCommitFailed
}
}
private func resolveImportedAccountAfterCommit(_ importedAccount: CodexPreparedImportedAccount) throws
-> CodexDisplacedLivePreservationExecutionResult
{
let persistedManagedAccounts = try self.store.loadAccounts()
if persistedManagedAccounts.account(id: importedAccount.account.id) != nil {
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .imported(managedAccountID: importedAccount.account.id))
}
guard let existingManagedAccount = self.repairDestination(
in: persistedManagedAccounts,
for: importedAccount.account)
else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let repairedManagedAccount = ManagedCodexAccount(
id: existingManagedAccount.id,
email: importedAccount.account.email,
providerAccountID: importedAccount.account.providerAccountID,
workspaceLabel: importedAccount.account.workspaceLabel,
workspaceAccountID: importedAccount.account.workspaceAccountID,
authFingerprint: importedAccount.account.authFingerprint,
managedHomePath: importedAccount.homeURL.path,
createdAt: existingManagedAccount.createdAt,
updatedAt: importedAccount.account.updatedAt,
lastAuthenticatedAt: importedAccount.account.lastAuthenticatedAt)
try self.store.storeAccounts(ManagedCodexAccountSet(
version: persistedManagedAccounts.version,
accounts: persistedManagedAccounts.accounts.map { account in
guard account.id == existingManagedAccount.id else { return account }
return repairedManagedAccount
}))
if existingManagedAccount.managedHomePath != importedAccount.homeURL.path {
try? self.removeManagedHomeIfSafe(
URL(fileURLWithPath: existingManagedAccount.managedHomePath, isDirectory: true))
}
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .alreadyManaged(managedAccountID: existingManagedAccount.id))
}
private func repairDestination(
in persistedManagedAccounts: ManagedCodexAccountSet,
for importedAccount: ManagedCodexAccount) -> ManagedCodexAccount?
{
if let providerAccountID = importedAccount.providerAccountID {
return persistedManagedAccounts.account(
email: importedAccount.email,
providerAccountID: providerAccountID)
}
let normalizedEmail = importedAccount.email
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return persistedManagedAccounts.accounts.first {
$0.email == normalizedEmail && $0.providerAccountID == nil
}
}
private func refreshExistingManagedAccount(
_ destination: PreparedStoredManagedAccount,
from context: PreparedPromotionContext) throws
-> ManagedCodexAccount
{
guard case let .readable(liveAuthMaterial) = context.live.homeState else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
guard let liveAuthIdentity = context.live.authIdentity else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
do {
let latestManagedAccounts = try self.store.loadAccounts()
guard let persistedManagedAccount = latestManagedAccounts.account(id: destination.persisted.id) else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let email = liveAuthIdentity.email
?? (liveAuthIdentity.providerAccountID != nil ? persistedManagedAccount.email : nil)
guard let email, liveAuthIdentity.identity != .unresolved else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
let now = Date().timeIntervalSince1970
let refreshedManagedAccount = ManagedCodexAccount(
id: persistedManagedAccount.id,
email: email,
providerAccountID: liveAuthIdentity.providerAccountID ?? persistedManagedAccount.providerAccountID,
workspaceLabel: liveAuthIdentity.workspaceLabel ?? persistedManagedAccount.workspaceLabel,
workspaceAccountID: liveAuthIdentity.workspaceAccountID ?? persistedManagedAccount.workspaceAccountID,
authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData),
managedHomePath: persistedManagedAccount.managedHomePath,
createdAt: persistedManagedAccount.createdAt,
updatedAt: now,
lastAuthenticatedAt: now)
let refreshedHomeURL = URL(fileURLWithPath: persistedManagedAccount.managedHomePath, isDirectory: true)
do {
try self.homeFactory.validateManagedHomeForDeletion(refreshedHomeURL)
} catch {
throw CodexAccountPromotionError.displacedLiveImportFailed
}
try self.fileManager.createDirectory(at: refreshedHomeURL, withIntermediateDirectories: true)
try self.writeManagedAuthData(liveAuthMaterial.rawData, to: refreshedHomeURL)
try self.store.storeAccounts(ManagedCodexAccountSet(
version: latestManagedAccounts.version,
accounts: latestManagedAccounts.accounts.map { account in
guard account.id == persistedManagedAccount.id else { return account }
return refreshedManagedAccount
}))
return refreshedManagedAccount
} catch let error as CodexAccountPromotionError {
throw error
} catch {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
}
private func writeManagedAuthData(_ data: Data, to homeURL: URL) throws {
let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL)
try data.write(to: authFileURL, options: .atomic)
try self.fileManager.setAttributes(
[.posixPermissions: NSNumber(value: Int16(0o600))],
ofItemAtPath: authFileURL.path)
}
private func removeManagedHomeIfSafe(_ homeURL: URL) throws {
try self.homeFactory.validateManagedHomeForDeletion(homeURL)
if self.fileManager.fileExists(atPath: homeURL.path) {
try self.fileManager.removeItem(at: homeURL)
}
}
private static func accountID(for homeURL: URL) -> UUID {
UUID(uuidString: homeURL.lastPathComponent) ?? UUID()
}
}