343 lines · 13.3 KB · Swift
RawDownload
#if os(macOS)import AppKitimport Foundation public struct CodexWebDashboardStrategy: ProviderFetchStrategy {    public let id: String = "codex.web.dashboard"    public let kind: ProviderFetchKind = .webDashboard     public init() {}     public func isAvailable(_ context: ProviderFetchContext) async -> Bool {        context.sourceMode.usesWeb &&            !Self.managedAccountStoreIsUnreadable(context) &&            !Self.managedAccountTargetIsUnavailable(context) &&            (!Self.profileAccountTargetIsUnavailable(context) || context.sourceMode == .web)    }     public func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {        guard !Self.managedAccountStoreIsUnreadable(context) else {            // A fail-closed placeholder CODEX_HOME does not identify a target account. If the managed store            // itself is unreadable, web import must not fall back to "any signed-in browser account".            throw OpenAIDashboardFetcher.FetchError.loginRequired        }        guard !Self.managedAccountTargetIsUnavailable(context) else {            // If the selected managed account no longer exists in a readable store, web import must not            // fall back to "any signed-in browser account" for that stale selection.            throw OpenAIDashboardFetcher.FetchError.loginRequired        }        guard !Self.profileAccountTargetIsUnavailable(context) else {            // A profile without an auth-backed email cannot safely target browser cookie import.            throw OpenAIDashboardFetcher.FetchError.loginRequired        }         let options = OpenAIWebOptions(            deadline: OpenAIDashboardFetcher.deadline(startingAt: Date(), timeout: context.webTimeout),            debugDumpHTML: context.webDebugDumpHTML,            verbose: context.verbose)         // Ensure AppKit is initialized before using WebKit in a CLI.        await MainActor.run {            _ = NSApplication.shared        }         let result: OpenAIWebCodexResult        do {            result = try await Self.fetchOpenAIWebCodex(                context: context,                options: options,                browserDetection: context.browserDetection)        } catch let error as URLError where error.code == .timedOut {            throw OpenAIWebCodexError.timedOut(seconds: context.webTimeout)        }        return self.makeResult(            usage: result.usage,            credits: result.credits,            dashboard: result.dashboard,            sourceLabel: "openai-web")    }     public func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {        _ = error        return context.sourceMode == .auto    }     private static func managedAccountStoreIsUnreadable(_ context: ProviderFetchContext) -> Bool {        context.settings?.codex?.managedAccountStoreUnreadable == true    }     private static func managedAccountTargetIsUnavailable(_ context: ProviderFetchContext) -> Bool {        context.settings?.codex?.managedAccountTargetUnavailable == true    }     private static func profileAccountTargetIsUnavailable(_ context: ProviderFetchContext) -> Bool {        context.settings?.codex?.profileAccountTargetUnavailable == true    }} struct OpenAIWebCodexResult {    let usage: UsageSnapshot    let credits: CreditsSnapshot?    let dashboard: OpenAIDashboardSnapshot} enum OpenAIWebCodexError: LocalizedError, Equatable {    case missingUsage    case policyRejected(CodexDashboardAuthorityDecision)    case timedOut(seconds: TimeInterval)     var errorDescription: String? {        switch self {        case .missingUsage:            return "OpenAI web dashboard did not include usage limits."        case let .timedOut(seconds):            return "OpenAI web dashboard fetch timed out after \(seconds.formatted()) seconds."        case let .policyRejected(decision):            switch decision.reason {            case let .wrongEmail(expected, actual):                var details: [String] = []                if let expected {                    details.append("expected \(expected)")                }                if let actual {                    details.append("got \(actual)")                }                if details.isEmpty {                    return "OpenAI web dashboard belonged to the wrong account."                }                return "OpenAI web dashboard belonged to the wrong account (\(details.joined(separator: ", ")))."            case .unresolvedWithoutTrustedEvidence:                return "Active Codex identity is unresolved and no trusted auth-backed continuity exists."            case .providerAccountMissingScopedEmail:                return "Active Codex provider account is missing its scoped email, " +                    "so dashboard ownership cannot be proven."            case .providerAccountLacksExactOwnershipProof:                return "OpenAI web dashboard could not be proven to belong to the active provider account."            case .missingDashboardSignedInEmail:                return "OpenAI web dashboard did not expose a signed-in email."            case let .sameEmailAmbiguity(email):                return "OpenAI web dashboard email \(email) is ambiguous across multiple known owners."            default:                return "OpenAI web dashboard was rejected by Codex dashboard authority."            }        }    }} private struct OpenAIWebOptions {    let deadline: Date    let debugDumpHTML: Bool    let verbose: Bool} @MainActorprivate final class WebLogBuffer {    private var lines: [String] = []    private let maxCount: Int    private let verbose: Bool    private let logger = CodexBarLog.logger(LogCategories.openAIWeb)     init(maxCount: Int = 300, verbose: Bool) {        self.maxCount = maxCount        self.verbose = verbose    }     func append(_ line: String) {        self.lines.append(line)        if self.lines.count > self.maxCount {            self.lines.removeFirst(self.lines.count - self.maxCount)        }        if self.verbose {            self.logger.verbose(line)        }    }     func snapshot() -> [String] {        self.lines    }} extension CodexWebDashboardStrategy {    @MainActor    fileprivate static func fetchOpenAIWebCodex(        context: ProviderFetchContext,        options: OpenAIWebOptions,        browserDetection: BrowserDetection) async throws -> OpenAIWebCodexResult    {        let logger = WebLogBuffer(verbose: options.verbose)        let log: @MainActor (String) -> Void = { line in            logger.append(line)        }        do {            let result = try await Self.fetchOpenAIWebDashboard(                context: context,                options: options,                browserDetection: browserDetection,                preferCachedCookieHeader: true,                logger: log)            return try Self.makeAuthorizedDashboardResult(                dashboard: result.dashboard,                context: context,                routingTargetEmail: result.routingTargetEmail)        } catch {            guard Self.shouldRetryWithFreshBrowserImport(after: error) else {                throw error            }            _ = try OpenAIDashboardBrowserCookieImporter.remainingTimeout(until: options.deadline)            log("Retrying OpenAI web dashboard with a fresh browser cookie import.")            let result = try await Self.fetchOpenAIWebDashboard(                context: context,                options: options,                browserDetection: browserDetection,                preferCachedCookieHeader: false,                logger: log)            return try Self.makeAuthorizedDashboardResult(                dashboard: result.dashboard,                context: context,                routingTargetEmail: result.routingTargetEmail)        }    }     nonisolated static func shouldRetryWithFreshBrowserImport(after error: Error) -> Bool {        if error is OpenAIWebCodexError {            return error as? OpenAIWebCodexError == .missingUsage        }        if case OpenAIDashboardFetcher.FetchError.noDashboardData = error {            return true        }        return false    }     @MainActor    static func makeAuthorizedDashboardResultForTesting(        dashboard: OpenAIDashboardSnapshot,        context: ProviderFetchContext,        routingTargetEmail: String?)        throws -> OpenAIWebCodexResult    {        try self.makeAuthorizedDashboardResult(            dashboard: dashboard,            context: context,            routingTargetEmail: routingTargetEmail)    }     @MainActor    private static func makeAuthorizedDashboardResult(        dashboard: OpenAIDashboardSnapshot,        context: ProviderFetchContext,        routingTargetEmail: String?) throws -> OpenAIWebCodexResult    {        let input = CodexCLIDashboardAuthorityContext.makeLiveWebInput(            dashboard: dashboard,            context: context,            routingTargetEmail: routingTargetEmail)        let decision = CodexDashboardAuthority.evaluate(input)         switch decision.disposition {        case .attach:            let attachedAccountEmail = CodexCLIDashboardAuthorityContext.attachmentEmail(from: input)            let credits = dashboard.toCreditsSnapshot()            let usage = dashboard.toUsageSnapshot(provider: .codex, accountEmail: attachedAccountEmail)                ?? Self.makeCreditsOnlyUsageSnapshot(                    dashboard: dashboard,                    attachedAccountEmail: attachedAccountEmail,                    credits: credits)            guard let usage else {                throw OpenAIWebCodexError.missingUsage            }            if let attachedAccountEmail {                OpenAIDashboardCacheStore.save(OpenAIDashboardCache(                    accountEmail: attachedAccountEmail,                    snapshot: dashboard))            }            return OpenAIWebCodexResult(usage: usage, credits: credits, dashboard: dashboard)        case .displayOnly:            if decision.cleanup.contains(.dashboardCache) {                OpenAIDashboardCacheStore.clear()            }            throw CodexDashboardPolicyError.displayOnly(decision)        case .failClosed:            if decision.cleanup.contains(.dashboardCache) {                OpenAIDashboardCacheStore.clear()            }            throw OpenAIWebCodexError.policyRejected(decision)        }    }     private static func makeCreditsOnlyUsageSnapshot(        dashboard: OpenAIDashboardSnapshot,        attachedAccountEmail: String?,        credits: CreditsSnapshot?) -> UsageSnapshot?    {        guard credits != nil else { return nil }        return UsageSnapshot(            primary: nil,            secondary: nil,            tertiary: nil,            updatedAt: dashboard.updatedAt,            identity: ProviderIdentitySnapshot(                providerID: .codex,                accountEmail: attachedAccountEmail ?? dashboard.signedInEmail,                accountOrganization: nil,                loginMethod: dashboard.accountPlan))    }     private struct OpenAIWebDashboardFetchResult {        let dashboard: OpenAIDashboardSnapshot        let routingTargetEmail: String?    }     @MainActor    private static func fetchOpenAIWebDashboard(        context: ProviderFetchContext,        options: OpenAIWebOptions,        browserDetection: BrowserDetection,        preferCachedCookieHeader: Bool,        logger: @MainActor @escaping (String) -> Void) async throws -> OpenAIWebDashboardFetchResult    {        let auth = context.fetcher.loadAuthBackedCodexAccount()        let routingTargetEmail = auth.email?.trimmingCharacters(in: .whitespacesAndNewlines)        let allowAnyAccount = routingTargetEmail == nil         let importResult = try await OpenAIDashboardBrowserCookieImporter(browserDetection: browserDetection)            .importBestCookies(                intoAccountEmail: routingTargetEmail,                allowAnyAccount: allowAnyAccount,                preferCachedCookieHeader: preferCachedCookieHeader,                cacheScope: context.settings?.codex?.openAIWebCacheScope,                deadline: options.deadline,                logger: logger)        let effectiveEmail = routingTargetEmail ?? importResult.signedInEmail?            .trimmingCharacters(in: .whitespacesAndNewlines)         let dashboard = try await OpenAIDashboardFetcher().loadLatestDashboard(            accountEmail: effectiveEmail,            cacheScope: context.settings?.codex?.openAIWebCacheScope,            logger: logger,            debugDumpHTML: options.debugDumpHTML,            timeout: OpenAIDashboardBrowserCookieImporter.remainingTimeout(until: options.deadline))        return OpenAIWebDashboardFetchResult(            dashboard: dashboard,            routingTargetEmail: routingTargetEmail)    }}#elsepublic struct CodexWebDashboardStrategy: ProviderFetchStrategy {    public let id: String = "codex.web.dashboard"    public let kind: ProviderFetchKind = .webDashboard     public init() {}     public func isAvailable(_: ProviderFetchContext) async -> Bool {        false    }     public func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult {        throw ProviderFetchError.noAvailableStrategy(.codex)    }     public func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {        false    }}#endif