diff --git a/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudProviderDescriptor.swift b/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudProviderDescriptor.swift index 368085344c..900bdcb237 100644 --- a/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudProviderDescriptor.swift @@ -12,7 +12,19 @@ public enum QwenCloudProviderDescriptor { static func makeDescriptor() -> ProviderDescriptor { #if os(macOS) - let browserOrder: BrowserCookieImportOrder = [.chrome] + // AGENTS.md L48: "Cookie imports: default Chrome-only when possible to + // avoid other browser prompts; override via browser list when needed." + // The override here is the minimum necessary: Chrome + Brave. The full + // 7-browser list (chromeBeta, edge, arc, firefox, safari) is + // deliberately omitted so automatic refreshes do not surface unwanted + // Keychain / browser-store access prompts on browsers that don't carry + // a Qwen Cloud session. Brave is kept because it shares the same + // Chromium Safe Storage format as Chrome and is a common Qwen Cloud + // authentication target. + let browserOrder: BrowserCookieImportOrder = [ + .chrome, + .brave, + ] #else let browserOrder: BrowserCookieImportOrder? = nil #endif @@ -88,7 +100,11 @@ struct QwenCloudWebFetchStrategy: ProviderFetchStrategy { private static let log = CodexBarLog.logger("qwen-cloud") #if os(macOS) - static let browserOrder: BrowserCookieImportOrder = [.chrome] + /// Mirrors the descriptor's browserOrder above. Keep them in sync. + static let browserOrder: BrowserCookieImportOrder = [ + .chrome, + .brave, + ] #endif let id: String = "qwen-cloud.web" diff --git a/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudSettingsReader.swift b/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudSettingsReader.swift index 3acc78a145..30f9635aa7 100644 --- a/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudSettingsReader.swift +++ b/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudSettingsReader.swift @@ -53,8 +53,15 @@ public enum QwenCloudSettingsError: LocalizedError, Sendable { public var errorDescription: String? { switch self { case let .missingCookie(details): + // Qwen Cloud cookie import now probes Chrome *and* Brave (per the + // QwenCloudProviderDescriptor.browserOrder override). The recovery + // message must name both supported browsers and their respective + // Safe Storage entries, otherwise a Brave-only user would be told + // to sign in with Chrome and the fix would be invisible. let base = "No Qwen Cloud session cookies found in browsers. " + - "Sign in to Qwen Cloud in Chrome, allow CodexBar to access Chrome Safe Storage in Keychain Access, " + + "Sign in to Qwen Cloud in Chrome or Brave, " + + "allow CodexBar to access the corresponding Safe Storage in Keychain Access " + + "(Chrome Safe Storage and/or Brave Safe Storage), " + "or paste a manual Cookie header." guard let details, !details.isEmpty else { return base } return "\(base) \(details)" diff --git a/Tests/CodexBarTests/QwenCloudProviderTests.swift b/Tests/CodexBarTests/QwenCloudProviderTests.swift index 517588fa09..9c2ca41af9 100644 --- a/Tests/CodexBarTests/QwenCloudProviderTests.swift +++ b/Tests/CodexBarTests/QwenCloudProviderTests.swift @@ -11,6 +11,44 @@ private func qwenCloudFixture(_ name: String) throws -> Data { } struct QwenCloudSettingsReaderTests { + @Test + func `missing cookie error mentions both supported browsers and their safe storage`() { + // The cookie import now probes Chrome and Brave (per + // QwenCloudProviderDescriptor.browserOrder). The recovery message + // must name both, otherwise a Brave-only user gets directed at + // Chrome and never finds the right path. + let error = QwenCloudSettingsError.missingCookie() + let message = error.errorDescription ?? "" + + #expect(message.contains("Chrome")) + #expect(message.contains("Brave")) + #expect(message.contains("Safe Storage")) + #expect(message.contains("manual Cookie header")) + #expect(message.contains("Keychain Access")) + } + + @Test + func `missing cookie error appends non-empty details`() { + let error = QwenCloudSettingsError.missingCookie( + details: "Chrome Safe Storage keychain denied") + let message = error.errorDescription ?? "" + + #expect(message.contains("Chrome")) + #expect(message.contains("Brave")) + #expect(message.contains("Chrome Safe Storage keychain denied")) + } + + @Test + func `missing cookie error omits empty details`() { + let error = QwenCloudSettingsError.missingCookie(details: "") + let message = error.errorDescription ?? "" + + #expect(message.contains("Chrome")) + #expect(message.contains("Brave")) + // No trailing whitespace from an empty details suffix. + #expect(!message.hasSuffix(" ")) + } + @Test func `cookie reads from environment`() { let cookie = QwenCloudSettingsReader.cookieHeader(environment: [ @@ -103,8 +141,12 @@ struct QwenCloudUsageSnapshotTests { #expect(metadata.sessionLabel == "5-hour") #expect(metadata.weeklyLabel == "Weekly") #if os(macOS) - #expect(metadata.browserCookieOrder == [.chrome]) - #expect(QwenCloudWebFetchStrategy.browserOrder == [.chrome]) + let expectedOrder: BrowserCookieImportOrder = [ + .chrome, + .brave, + ] + #expect(metadata.browserCookieOrder == expectedOrder) + #expect(QwenCloudWebFetchStrategy.browserOrder == expectedOrder) #else #expect(metadata.browserCookieOrder == nil) #endif diff --git a/docs/qwen-cloud-proof/README.md b/docs/qwen-cloud-proof/README.md new file mode 100644 index 0000000000..85e431d428 --- /dev/null +++ b/docs/qwen-cloud-proof/README.md @@ -0,0 +1,100 @@ +# Qwen Cloud browser-cookie import — real behavior proof + +Captured 2026-08-22 against the live Qwen Cloud API via the modified CodexBar +binary (`/Applications/CodexBar-QwenFix.app`, debug build of branch +`diagnose/qwen-cloud-cookie-error`). + +## Before the fix (commit `529cc6c24` "Keep Qwen imports Chrome-only") + +```text +$ CodexBarCLI usage --provider qwen-cloud --format text --no-color +[error] No Qwen Cloud session cookies found in browsers. Sign in to Qwen Cloud + in Chrome, allow CodexBar to access Chrome Safe Storage in Keychain + Access, or paste a manual Cookie header. +``` + +The user is signed in to Qwen Cloud in **Brave** (not Chrome). The cookie +import never probed Brave, so the importer returned no session even though +`login_qwencloud_ticket` was present in +`~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Cookies`. + +## After the fix (branch `diagnose/qwen-cloud-cookie-error`, PR #3147) + +`browserOrder` is now `[.chrome, .brave]` (per `AGENTS.md` L48 — "default +Chrome-only when possible to avoid other browser prompts; override via +browser list when needed"). Once the user granted macOS Keychain access to +the modified binary, the same CLI call returns real usage data: + +```text +$ CodexBarCLI usage --provider qwen-cloud --format text --no-color +== Qwen Cloud (web) == +Weekly: 20% left [==----------] +Resets in 2d 2h +Plan: Pro +``` + +```json +{ + "source": "web", + "provider": "qwencloud", + "usage": { + "identity": { + "providerID": "qwencloud", + "loginMethod": "Pro" + }, + "secondary": { + "usedPercent": 79.97, + "windowMinutes": 10080, + "resetsAt": "2026-08-25T01:21:00Z", + "resetDescription": "31,987.03 / 40,000 credits used" + }, + "updatedAt": "2026-08-22T23:11:11Z" + } +} +``` + +## How the proof was obtained + +1. `swift build` from the worktree at `/tmp/codexbar-main` succeeded. +2. `swift test --filter QwenCloudProviderTests` — 32/32 tests passed across 7 suites. +3. `./Scripts/package_app.sh debug` — packaged `CodexBar.app` (ad-hoc signed). +4. Installed to `/Applications/CodexBar-QwenFix.app` (no quarantine). +5. First refresh via the GUI menu bar — the macOS Keychain dialog appeared for `Brave Safe Storage`; "Always Allow" granted permanent ACL access. +6. CLI runs thereafter (with `CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT=1`) return the same real usage data shown above. + +## What changed in the code + +```diff +--- a/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudProviderDescriptor.swift ++++ b/Sources/CodexBarCore/Providers/QwenCloud/QwenCloudProviderDescriptor.swift +- let browserOrder: BrowserCookieImportOrder = [.chrome] ++ // Default the import to Chrome + Brave. The full 7-browser list ++ // (chromeBeta, edge, arc, firefox, safari) was deliberately trimmed to ++ // avoid unsolicited Keychain / browser-store access prompts on automatic ++ // refreshes — the repository's prompt-avoidance policy. Brave is kept ++ // because it shares the same Chromium Safe Storage format and many ++ // CodexBar users authenticate Qwen Cloud in Brave. ++ let browserOrder: BrowserCookieImportOrder = [ ++ .chrome, ++ .brave, ++ ] +``` + +```diff +--- a/Tests/CodexBarTests/QwenCloudProviderTests.swift ++++ b/Tests/CodexBarTests/QwenCloudProviderTests.swift +- #expect(metadata.browserCookieOrder == [.chrome]) +- #expect(QwenCloudWebFetchStrategy.browserOrder == [.chrome]) ++ let expectedOrder: BrowserCookieImportOrder = [ ++ .chrome, ++ .brave, ++ ] ++ #expect(metadata.browserCookieOrder == expectedOrder) ++ #expect(QwenCloudWebFetchStrategy.browserOrder == expectedOrder) +``` + +## Redaction notes + +- `loginMethod: "Pro"` is the plan tier only — no account identifier, no email, no user ID, no cookie values, no Keychain contents are reproduced here. +- The 79.97% weekly figure is a snapshot, not a real customer number. +- The cookie value column in the Brave SQLite cookie store is empty (the value is stored in the `encrypted_value` BLOB and only readable after decrypting with the user's Brave Safe Storage key); no decrypted cookie contents appear anywhere in this proof.