docs/provider.md
Goal: adding a provider should feel like:
This doc describes the current provider architecture and the exact steps to add a new provider.
Sources/CodexBarCore: provider descriptors + fetch strategies + probes + parsing + shared utilities.Sources/CodexBar: UI/state + provider implementations (settings/login/menu hooks only).UsageProvider enum (used for persistence + widgets).ProviderDescriptor owns labels, URLs, default enablement, and fetch pipeline.ProviderFetchStrategy objects implement concrete fetch paths.Common building blocks already exist:
TTYCommandRunnerSubprocessRunnerBrowserCookieImporter (Safari/Chrome/Firefox adapters)OpenAIDashboardFetcher (WKWebView + JS)Provider behavior is descriptor-driven. Two flat first-party manifests form the closed bootstrap boundary:
ProviderManifest lists core descriptors and ProviderImplementationManifest lists app implementations. The registries
retain thread-safe register(_:) methods for future dynamic providers.
Runtime settings follow the same boundary. A provider owns its ProviderSettingsSectionKey and section payload in its
core folder, registers that key on its descriptor, and contributes the payload from its app implementation. The generic
ProviderSettingsSnapshot container performs the sole type-erased cast behind its constrained subscript; provider-local
accessors keep fetch strategies fully typed. Providers that share a payload type still declare a distinct key for each
ProviderInstanceID, while providers with no runtime settings receive an empty section from the descriptor default.
Credential and config behavior follows the descriptor boundary too. Providers with credentials register a Sendable
ProviderCredentialAdapter that owns config-to-environment projection, token resolution, token-account support,
diagnose classification, validation, and missing-credential messaging; a missing adapter means the provider has no
credential behavior. Typed settings-section registrations optionally expose cookie settings and a CLI credential
contribution, so the app, CLI, and plugin cookie broker consume the same provider-owned settings shape.
ProviderArchitectureGatekeeperTests is a drift tripwire against honest architecture mistakes by future contributors
and AI agents. Its scope is deliberately narrower than a Swift parser's: the lexical scanner detects dotted provider
case literals, including qualified, labeled, and multiline statements, and lowercase raw provider-ID string literals
in every single-statement position (including assignments, bare function arguments, dictionary keys and values, array
elements, and returns). It scans shipped Swift under Sources/** and WidgetExtension/**, with suppressions applied to
exact provider tokens rather than whole statements.
The following are out of scope by design:
/* ... */ comments are blanked before
scanning; comments spanning statement lines are treated as ending the scanned code for that line.Tests/**, where fixtures legitimately name providers, and non-Swift files, because this tripwire is scoped to
shipped Swift architecture.This is engineering scoping, not a claim of adversarial completeness: the gatekeeper is a lexical drift tripwire for honest mistakes. If in-the-wild drift is ever observed slipping past it, the concrete upgrade path is to replace the lexical policy scan with a SwiftSyntax-based implementation that can model expressions and dataflow.
Introduce a single descriptor per provider:
id (stable UsageProvider)--source modes + ordered strategy pipeline)usesAccountFallback for Codex auth.json)UI and settings should become descriptor-driven:
A provider declares a pipeline of strategies, in priority order. Each strategy:
kind (cli, web cookies, oauth, api token, local probe, web dashboard)UsageSnapshot (and optional credits/dashboard)--source or app settingsThe pipeline resolves to the best available strategy, and falls back on failure when allowed.
Each run returns a ProviderFetchOutcome with attempts + errors for debug UI and CLI --verbose.
Expose a narrow set of protocols/structs that provider implementations can use:
KeychainAPI: read-only, allowlisted service/account pairsBrowserCookieAPI: import cookies by domain list; returns cookie header + diagnosticsBrowserLocalStorageAPI: read origin-scoped key/value snapshots across browser profilesPTYAPI: run CLI interactions with timeouts + “send on substring” + stop rulesHTTPAPI: URLSession wrapper with domain allowlist + standard headers + tracingWebViewScrapeAPI: WKWebView lease + evaluateJavaScript + snapshot dumpingTokenCostAPI: Cost Usage local-log integration (Codex/Claude today; extend later)StatusAPI: status polling helpers (Statuspage + Workspace incidents)LoggerAPI: scoped logger + redaction helpersRule: providers do not talk to FileManager, Security, or “browser internals” directly unless they are the host API implementation.
Sources/CodexBarCore/Providers/<ProviderID>/
<ProviderID>Descriptor.swift (descriptor + strategy pipeline)<ProviderID>Strategies.swift (strategy implementations)<ProviderID>Probe.swift / <ProviderID>Fetcher.swift<ProviderID>Models.swift<ProviderID>Parser.swift (if text/HTML parsing)Sources/CodexBar/Providers/<ProviderID>/
<ProviderID>ProviderImplementation.swift (settings/login UI hooks only)import Foundation
public enum ExampleProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .example,
metadata: ProviderMetadata(
id: .example,
displayName: "Example",
sessionLabel: "Session",
weeklyLabel: "Weekly",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Example usage",
cliName: "example",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
dashboardURL: nil,
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .init(provider: .example),
iconResourceName: "ProviderIcon-example",
color: ProviderColor(red: 0.2, green: 0.6, blue: 0.8),
confettiPalette: [
ProviderColor(hex: 0x3399CC),
ProviderColor(hex: 0x66C2FF),
]),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Example cost summary is not supported." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .cli],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [ExampleFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "example",
versionDetector: nil))
}
}
struct ExampleFetchStrategy: ProviderFetchStrategy {
let id: String = "example.cli"
let kind: ProviderFetchKind = .cli
func isAvailable(_: ProviderFetchContext) async -> Bool { true }
func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult {
let usage = UsageSnapshot(
primary: .init(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date(),
identity: nil)
return self.makeResult(usage: usage, sourceLabel: "cli")
}
func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool { false }
}
Hosted relays and upstream aggregators need enough public evidence for maintainers and users to evaluate the trust boundary:
An integration can be restored when missing operator or authorization evidence becomes available.
Adding a first-party provider currently requires all of these registration points:
Sources/CodexBarCore/Providers/<Name>/ with the descriptor, fetch strategies, and core settings or
credential types.Sources/CodexBar/Providers/<Name>/ with the app implementation and any app settings contribution or UI.UsageProvider in
Sources/CodexBarCore/Providers/Providers.swift.Scripts/regenerate-provider-manifests.sh. Do not edit ProviderManifest.swift,
ProviderImplementationManifest.swift, or ProviderInstanceIDAliases.generated.swift directly. The generator also
refreshes docs/provider-ids.md, which is linked from docs/configuration.md.Sources/CodexBar/Resources/ProviderIcon-<id>.svg and reference it from the descriptor's branding.widgetSelectable: false, add the matching case and literal
caseDisplayRepresentations entry to the WidgetKit ProviderChoice AppEnum. AppIntents extracts this table
statically, so widget display representations cannot be derived at runtime. WidgetProviderChoiceTests keeps the
literal table synchronized with selectable descriptor metadata and display names.docs/providers.md, including authentication and data-source
guidance. Add a dedicated provider document when the integration needs more detail.If the provider has runtime settings, add its section key and payload beside the descriptor, pass the key as the
descriptor's settingsSection, and return a typed contribution from the app implementation. No central settings file
or builder switch changes are needed.
If the provider has credential behavior, define its credential adapter beside the descriptor. Register token-account metadata and config validation there, and register any cookie/settings projection through the descriptor's typed settings section; do not add provider cases to the generic config, diagnose, CLI, or plugin broker consumers.
Descriptor-owned metadata derives icon-style identity, log-category construction, display and compact labels, default
enablement, fetch/CLI metadata, config capabilities, menu-bar metric capabilities, and icon validation. Generated
manifests derive their order from UsageProvider; the provider architecture gatekeeper reports missing descriptor,
implementation, icon, settings-section, or widget registrations by provider ID. The WidgetKit case and display table
remain deliberate literal exceptions because AppIntents requires statically extractable declarations.
Current: checkboxes per provider.
Preferred direction: table/list rows (like a “sessions” table):
This keeps the pane scannable once we have >5 providers.