Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/pr-proof/muse-web-team-quota.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Muse Code browser-team quota - production-path proof

Date: 2026-09-26 08:48 AWST (UTC+8), macOS arm64, real Muse Code Everyday Usage account.
Redactions: device token, llama_dev_sess cookie value, email, team ID (<team-id>). Nothing else edited.

## Setup (no CodexBar Keychain read)
- CLI device token copied once (user-approved `security` prompt) into a temporary
`auth.json` (mode 0600), selected through MUSE_AUTH_PATH.
- Firefox `llama_dev_sess` cookie pasted as Manual in a temporary config selected
through CODEXBAR_CONFIG. The "default" run uses a config without cookie settings.
- Both temporary files deleted right after the run.
- State at run time: 5-hour window idle, so POST /muse-code/key omits `subs_usage`.
The session sees one team.
- Command: `codexbar usage --provider muse --format json` (fields summarised).

## Brew 0.66.0 baseline
source=oauth 5h=none weekly=none
[Muse Code subscription] Plan=Muse Code Everyday Usage, Quota=Not included in this login response

## Branch, default settings (cookie source Off)
source=oauth 5h=none weekly=none
[Muse Code subscription] Plan=Muse Code Everyday Usage, Quota=Not included in this login response
-> no browser read.

## Branch, cookie set, no team selected
source=oauth 5h=none weekly=none
[Muse Code subscription] Plan=..., Quota=Not included in this login response
[Browser teams] Status=Choose a browser team ID in Muse Code settings, My Team=<team-id>
-> teams listed, no quota requested.

## Branch, cookie set, team ID not visible to the session ("123")
source=oauth 5h=none weekly=none
[Browser teams] Status=The selected browser team is not visible to this session, My Team=<team-id>
-> no quota requested.

## Branch, cookie set, real team selected
source=oauth+web dataConfidence=estimated
5h=0% (no reset: window idle) weekly=22.58% resets 2026-09-28T00:00:00Z
[Muse Code subscription] Plan=Muse Code Everyday Usage
[Browser team quota (dev.meta.ai)] Team=My Team, 5 hours=0%, Weekly=23%

Cross-check (same cookie, direct GET /api/portal/teams/<team-id>/subscription-quota, 07:29 AWST):
tier "Muse Code Everyday Usage" (equals login subs_tier_name), window_weighted_used 0,
weekly_weighted_used 13550839000 / 60000000000 = 22.58%.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 0.67.1 — Unreleased

### Fixed

- Muse Code: optionally show the explicitly selected dev.meta.ai browser team’s quota when the login omits quotas, with cookies Off by default and team choices in settings (#4011). Fixes #4002. Thanks @enieuwy!
### Added

- Menu bar: add opt-in, bounded startup diagnostics for status-item creation and Control Center hosting investigations (#3377).
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ extension UsageMenuCardView.Model {
return [L("Quota estimated from local usage history")] + subscriptionNotes
}

// Provider-specific by design: Muse browser-team quotas come from a user-selected dev.meta.ai team.
if input.provider == .muse, input.snapshot?.dataConfidence == .estimated {
return [L("Quota from the selected dev.meta.ai browser team")] + subscriptionNotes
}

if let notes = self.apiProviderUsageNotes(input: input) {
return notes + subscriptionNotes
}
Expand Down
71 changes: 68 additions & 3 deletions Sources/CodexBar/Providers/Muse/MuseProviderImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,80 @@ struct MuseProviderImplementation: ProviderImplementation {
}

@MainActor
func observeSettings(_: SettingsStore) {}
func observeSettings(_ settings: SettingsStore) {
_ = settings.museWebTeamID
}

/// The shared cookie snapshot defaults to Automatic; Muse reads a browser session only after an explicit choice.
@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
ProviderSettingsSnapshotContribution(
MuseProviderSettings(
cookieSource: context.settings.museCookieSource,
manualCookieHeader: context.settings.museCookieHeader,
webTeamID: context.settings.museWebTeamID.isEmpty ? nil : context.settings.museWebTeamID),
for: MuseProviderSettingsKey.self)
}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
MuseCredentials.hasLogin(environment: context.environment)
}

/// The dev.meta.ai session only fills quotas that the Muse login response leaves out.
@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let rows = context.store.snapshot(for: .muse)?.details.first { $0.title == "Browser teams" }?.rows ?? []
var options = [ProviderSettingsPickerOption(id: "", title: "Choose a team…")]
for row in rows where !row.value.isEmpty && row.value.allSatisfy(\.isNumber) {
guard !options.contains(where: { $0.id == row.value }) else { continue }
options.append(.init(id: row.value, title: "\(row.label) (\(row.value))"))
}
let selected = context.settings.museWebTeamID
if !selected.isEmpty, !options.contains(where: { $0.id == selected }) {
options.append(.init(id: selected, title: "\(selected) (unavailable)"))
}
return [
ProviderCookieSourceUI.picker(
id: "muse-cookie-source",
context: context,
source: \.museCookieSource,
allowsOff: true,
subtitles: {
.init(
auto: L("Automatically imports browser cookies."),
manual: L("Paste a Cookie header or cURL capture from %@.", "dev.meta.ai"),
off: L("%@ cookies are disabled.", "Muse Code"))
}),
ProviderSettingsPickerDescriptor(
id: "muse-web-team-id",
title: "Browser team",
subtitle: "Refresh Muse Code to load teams when the login omits quotas. "
+ "Choose the web team's quota to display, then refresh.",
binding: context.binding(\.museWebTeamID),
options: options,
isVisible: { context.settings.museCookieSource != .off },
onChange: nil),
]
}

@MainActor
func settingsFields(context _: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[]
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "muse-cookie",
title: "",
subtitle: "",
kind: .secure,
placeholder: "Cookie: llama_dev_sess=...",
binding: context.binding(\.museCookieHeader),
actions: [
ProviderSettingsActionDescriptor.openURL(
id: "muse-open-usage",
title: "Open dev.meta.ai",
url: URL(string: "https://dev.meta.ai/usage")),
],
isVisible: { context.settings.museCookieSource == .manual }),
]
}
}
23 changes: 23 additions & 0 deletions Sources/CodexBar/Providers/Muse/MuseSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var museCookieHeader: String {
get { self[providerConfig: .muse, field: .cookieHeader] }
set { self[providerConfig: .muse, field: .cookieHeader] = newValue }
}

var museWebTeamID: String {
get { self[providerConfig: .muse, field: .workspace] }
set { self[providerConfig: .muse, field: .workspace] = newValue }
}

var museCookieSource: ProviderCookieSource {
// Browser sessions are opt-in; a pasted header without an explicit source means Manual, as in the CLI.
get {
let header = self.providerConfig(for: .muse)?.sanitizedCookieHeader
return self.resolvedCookieSource(provider: .muse, fallback: header == nil ? .off : .manual)
}
set { self.setCookieSource(newValue, provider: .muse) }
}
}
2 changes: 2 additions & 0 deletions Sources/CodexBar/SettingsStore+MenuObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ extension SettingsStore {
_ = self.augmentCookieSource
_ = self.ampCookieSource
_ = self.t3ChatCookieSource
_ = self.museCookieSource
_ = self.zoomMateCookieSource
_ = self.ollamaCookieSource
_ = self.mergeIcons
Expand All @@ -124,6 +125,7 @@ extension SettingsStore {
_ = self.augmentCookieHeader
_ = self.ampCookieHeader
_ = self.t3ChatCookieHeader
_ = self.museCookieHeader
_ = self.zoomMateCookieHeader
_ = self.ollamaCookieHeader
_ = self.copilotAPIToken
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/UsageStore+Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ extension UsageStore {
"augmentCookieSource": self.settings.augmentCookieSource.rawValue,
"ampCookieSource": self.settings.ampCookieSource.rawValue,
"t3ChatCookieSource": self.settings.t3ChatCookieSource.rawValue,
"museCookieSource": self.settings.museCookieSource.rawValue,
"ollamaCookieSource": self.settings.ollamaCookieSource.rawValue,
"openAIWebAccess": self.settings.openAIWebAccessEnabled ? "1" : "0",
"openAIWebBatterySaver": self.settings.openAIWebBatterySaverEnabled ? "1" : "0",
Expand Down
45 changes: 42 additions & 3 deletions Sources/CodexBarCore/Providers/Muse/MuseProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,34 @@ public enum MuseProviderDescriptor {
"Muse Code login not found. Run `muse login`, then refresh CodexBar."
})

/// Chrome needs a no-UI Safe Storage grant and Firefox needs none; Safari's store can require Full Disk Access.
private static var browserCookieOrder: BrowserCookieImportOrder? {
#if os(macOS)
[.chrome, .firefox]
#else
nil
#endif
}

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .muse,
settingsSection: .init(
MuseProviderSettingsKey.self,
cookieSettings: { settings in
.init(cookieSource: settings.cookieSource, manualCookieHeader: settings.manualCookieHeader)
},
credentialSettings: { context in
// Browser sessions are opt-in for Muse: without an explicit source or pasted header, stay Off.
let header = context.config?.sanitizedCookieHeader
return MuseProviderSettings(
cookieSource: context.config?.cookieSource ?? (header == nil ? .off : .manual),
manualCookieHeader: header,
webTeamID: context.config?.sanitizedWorkspaceID)
}),
credentials: self.credentials,
// `workspaceID` holds the user-selected dev.meta.ai team for the browser-team quota.
config: ProviderConfigCapabilities(workspaceIDValidationOrder: 8),
metadata: ProviderMetadata(
id: .muse,
displayName: "Muse Code",
Expand All @@ -32,6 +56,7 @@ public enum MuseProviderDescriptor {
cliName: "muse",
defaultEnabled: false,
widgetSelectable: false,
browserCookieOrder: self.browserCookieOrder,
dashboardURL: "https://dev.meta.ai",
subscriptionDashboardURL: "https://dev.meta.ai",
statusPageURL: nil),
Expand Down Expand Up @@ -75,9 +100,23 @@ struct MuseOAuthFetchStrategy: ProviderFetchStrategy {

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let token = try MuseCredentials.accessToken(environment: context.env)
let runtime = try ProviderPluginRuntime(bundledPlugin: "muse")
let snapshot = try await runtime.fetchUsage(secrets: ["MUSE_DEVICE_TOKEN": token])
return self.makeResult(usage: snapshot, sourceLabel: "oauth")
// The key request (15 s) and the bounded dev.meta.ai fallback (5 × 8 s) fit one 60 s deadline.
let runtime = try ProviderPluginRuntime(bundledPlugin: "muse", timeout: 60)
let cookies = ProviderPluginCookieBroker(
provider: .muse, domains: runtime.manifest.cookieDomains, context: context)
// Reading the browser session is opt-in: an unconfigured Muse provider keeps its CLI-token-only behavior.
let settings = context.settings?[MuseProviderSettingsKey.self]
let cookieSource = settings?.cookieSource ?? .off
let result = try await runtime.fetchResult(
settings: settings?.webTeamID.map { ["MUSE_WEB_TEAM_ID": $0] } ?? [:],
secrets: ["MUSE_DEVICE_TOKEN": token],
sourceMode: context.sourceMode,
cookieSource: cookieSource,
cookieInvalidator: { cookies.rejectCookie(domain: $0) },
cookieSessionResolver: { try cookies.nextSession(domain: $0, cachedOnly: $1) },
cookieSessionInvalidator: { cookies.rejectCookie(domain: $0, id: $1) },
cookieResolver: { _, domain in try cookies.cookieHeader(domain: domain) })
return self.makeResult(usage: result.usage, sourceLabel: result.sourceLabel ?? "oauth")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
Expand Down
29 changes: 29 additions & 0 deletions Sources/CodexBarCore/Providers/Muse/MuseProviderSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

public struct MuseProviderSettings: ProviderCookieSettings {
public let cookieSource: ProviderCookieSource
public let manualCookieHeader: String?
/// The dev.meta.ai team whose quota fills omitted login quotas. Nil means no team was chosen.
public let webTeamID: String?

public init(cookieSource: ProviderCookieSource, manualCookieHeader: String?) {
self.init(cookieSource: cookieSource, manualCookieHeader: manualCookieHeader, webTeamID: nil)
}

public init(cookieSource: ProviderCookieSource, manualCookieHeader: String?, webTeamID: String?) {
self.cookieSource = cookieSource
self.manualCookieHeader = manualCookieHeader
self.webTeamID = webTeamID
}
}

public enum MuseProviderSettingsKey: ProviderSettingsSectionKey {
public static let providerID = ProviderInstanceID.muse
public typealias Section = MuseProviderSettings
}

extension ProviderSettingsSnapshot {
public static func make(muse: MuseProviderSettings?) -> Self {
self.make(muse, for: MuseProviderSettingsKey.self)
}
}
Loading
Loading