import CodexBarCoreimport Commanderimport Foundation extension CodexBarCLI { /// Window selected by the `guard` command: `session` maps to the primary /// rate window, `weekly` maps to the secondary rate window. enum GuardWindow: String { case session case weekly var payloadValue: String { self.rawValue } } /// Pure gating outcome. Kept free of I/O so it is unit-testable off-network. enum GuardDecision: String { case ok case blocked case unknown } enum GuardUnavailableReason: String, Sendable { case accountResolution = "account-resolution" case fetchFailed = "fetch-failed" case timeout case windowUnavailable = "window-unavailable" } enum GuardFetchOutcome: Sendable { case available(Double) case unavailable(GuardUnavailableReason) } struct GuardEvaluation: Sendable { let decision: GuardDecision let exitCode: Int32 let remainingPercent: Double? let unavailableReason: GuardUnavailableReason? } /// Command-specific stable status codes. `69` is sysexits `EX_UNAVAILABLE`. private enum GuardExitCode: Int32 { case safe = 0 case blocked = 1 case unavailable = 69 } /// Pure decision core for `codexbar guard`. /// /// - unavailable quota → `.unknown` (exit `0` when `failOpen`, else `69`). /// - remaining quota at or above the threshold → `.ok` (exit `0`). /// - otherwise → `.blocked` (exit `1`). static func evaluateGuard( outcome: GuardFetchOutcome, minimumRemainingPercent: Double, failOpen: Bool) -> GuardEvaluation { guard case let .available(remainingPercent) = outcome else { guard case let .unavailable(reason) = outcome else { preconditionFailure("Unhandled guard outcome") } return GuardEvaluation( decision: .unknown, exitCode: failOpen ? GuardExitCode.safe.rawValue : GuardExitCode.unavailable.rawValue, remainingPercent: nil, unavailableReason: reason) } if remainingPercent >= minimumRemainingPercent { return GuardEvaluation( decision: .ok, exitCode: GuardExitCode.safe.rawValue, remainingPercent: remainingPercent, unavailableReason: nil) } return GuardEvaluation( decision: .blocked, exitCode: GuardExitCode.blocked.rawValue, remainingPercent: remainingPercent, unavailableReason: nil) } /// Remaining headroom (`100 - usedPercent`) for a resolved rate window, or `nil` when the window /// is absent or a synthetic placeholder. A synthetic window is a lane the provider did not /// actually report (e.g. Claude with no live five-hour session), so it must not read as free /// headroom and let the gate pass on a phantom metric. static func guardRemainingHeadroom(for window: RateWindow?) -> Double? { guard let window, !window.isSyntheticPlaceholder else { return nil } return 100 - window.usedPercent } static func runGuard(_ values: ParsedValues) async { let output = CLIOutputPreferences.from(values: values) let json = values.flags.contains("json") let failOpen = values.flags.contains("failOpen") let verbose = values.flags.contains("verbose") guard let window = Self.decodeGuardWindow(from: values) else { Self.exitGuardArgumentError("--window must be session|weekly.", output: output) } let minimumRemainingPercent: Double switch Self.decodeGuardMinimumRemaining(from: values) { case let .success(value): minimumRemainingPercent = value case .failure: Self.exitGuardArgumentError( "--min-remaining must be a finite percent between 0 and 100.", output: output) } let timeout: TimeInterval switch Self.decodeGuardTimeout(from: values) { case let .success(value): timeout = value case .failure: Self.exitGuardArgumentError( "--timeout must be a finite number of seconds from 0 through 86400.", output: output) } let provider: UsageProvider switch Self.decodeGuardProvider(from: values) { case let .success(value): provider = value case let .failure(error): Self.exitGuardArgumentError(error.localizedDescription, output: output) } let config = Self.loadConfig(output: output) let outcome = await Self.runGuardFetch(timeout: timeout) { await ProviderInteractionContext.$current.withValue(.background) { await Self.guardFetchOutcome( provider: provider, window: window, config: config, verbose: verbose, webTimeout: timeout > 0 ? timeout : 60) } } if case .unavailable(.timeout) = outcome { TTYCommandRunner.terminateActiveProcessesForAppShutdown() } let evaluation = Self.evaluateGuard( outcome: outcome, minimumRemainingPercent: minimumRemainingPercent, failOpen: failOpen) Self.emitGuardResult( provider: provider, window: window, minimumRemainingPercent: minimumRemainingPercent, evaluation: evaluation, json: json, pretty: output.pretty) Self.platformExit(evaluation.exitCode) } // MARK: - Argument decoding private static func exitGuardArgumentError(_ message: String, output: CLIOutputPreferences) -> Never { self.exit(code: .usage, message: "Error: \(message)", output: output, kind: .args) } static func decodeGuardWindow(from values: ParsedValues) -> GuardWindow? { guard let raw = values.options["window"]?.last else { return .session } return GuardWindow(rawValue: raw.lowercased()) } static func guardProvider(rawOverride: String?) -> Result<UsageProvider, CLIArgumentError> { guard let rawOverride else { return .failure(CLIArgumentError("guard requires --provider <id>.")) } guard let selection = ProviderSelection(argument: rawOverride) else { return .failure(CLIArgumentError("unknown provider '\(rawOverride)'.")) } guard selection.asList.count == 1, let provider = selection.asList.first else { return .failure(CLIArgumentError("guard requires exactly one --provider.")) } return .success(provider) } private static func decodeGuardProvider(from values: ParsedValues) -> Result<UsageProvider, CLIArgumentError> { self.guardProvider(rawOverride: values.options["provider"]?.last) } static func decodeGuardMinimumRemaining(from values: ParsedValues) -> Result<Double, CLIArgumentError> { guard let raw = values.options["minRemaining"]?.last else { return .success(10) } guard let value = Double(raw), value.isFinite, value >= 0, value <= 100 else { return .failure(CLIArgumentError("--min-remaining must be a finite percent between 0 and 100.")) } return .success(value) } static func decodeGuardTimeout(from values: ParsedValues) -> Result<TimeInterval, CLIArgumentError> { self.guardTimeout(raw: values.options["timeout"]?.last) } static func guardTimeout(raw: String?) -> Result<TimeInterval, CLIArgumentError> { guard let raw else { return .success(60) } guard let value = TimeInterval(raw), value.isFinite, value >= 0, value <= 86400 else { return .failure(CLIArgumentError("--timeout must be a finite number of seconds from 0 through 86400.")) } return .success(value) } // MARK: - Fetch static func runGuardFetch( timeout: TimeInterval, operation: @escaping @Sendable () async -> GuardFetchOutcome) async -> GuardFetchOutcome { let sourceTask = Task<GuardFetchOutcome, Error> { await operation() } guard timeout > 0 else { return await (try? sourceTask.value) ?? .unavailable(.fetchFailed) } let join = BoundedTaskJoin(sourceTask: sourceTask) return switch await join.value(joinGrace: .seconds(timeout)) { case let .value(outcome): outcome case .failure: .unavailable(.fetchFailed) case .timedOut: .unavailable(.timeout) } } private static func guardFetchOutcome( provider: UsageProvider, window: GuardWindow, config: CodexBarConfig, verbose: Bool, webTimeout: TimeInterval) async -> GuardFetchOutcome { let tokenContext: TokenAccountCLIContext do { tokenContext = try TokenAccountCLIContext( selection: TokenAccountCLISelection(label: nil, index: nil, allAccounts: false), config: config, verbose: verbose) } catch { return .unavailable(.accountResolution) } // Resolve the configured token account the same way `usage` does, so token-only // providers (e.g. Claude, z.ai, OpenAI) fetch their quota instead of returning unknown. let account: ProviderTokenAccount? do { account = try tokenContext.resolvedAccounts(for: provider).first } catch { return .unavailable(.accountResolution) } let browserDetection = BrowserDetection() let fetcher = UsageFetcher() let claudeFetcher = ClaudeUsageFetcher(browserDetection: browserDetection) let env = tokenContext.environment( base: ProcessInfo.processInfo.environment, provider: provider, account: account) let settings = tokenContext.settingsSnapshot(for: provider, account: account) let baseSource = tokenContext.preferredSourceMode(for: provider) let effectiveSourceMode = tokenContext.effectiveSourceMode( base: baseSource, provider: provider, account: account) let fetchContext = ProviderFetchContext( runtime: .cli, sourceMode: effectiveSourceMode, includeCredits: false, webTimeout: webTimeout, webDebugDumpHTML: false, verbose: verbose, env: env, settings: settings, fetcher: tokenContext.fetcher(base: fetcher, provider: provider, env: env), claudeFetcher: claudeFetcher, browserDetection: browserDetection, // Guard is read-only: omit updater callbacks so refresh-dependent credentials fail unavailable. selectedTokenAccountID: account?.id) let outcome = await Self.fetchProviderUsage(provider: provider, context: fetchContext) if verbose { Self.printFetchAttempts(provider: provider, attempts: outcome.attempts) } switch outcome.result { case let .success(result): let usage = result.usage.scoped(to: provider) let rateWindow = window == .session ? usage.primary : usage.secondary guard let remaining = Self.guardRemainingHeadroom(for: rateWindow) else { return .unavailable(.windowUnavailable) } return .available(remaining) case .failure: return .unavailable(.fetchFailed) } } // MARK: - Output private struct GuardResultPayload: Encodable { let provider: String let window: String let remainingPercent: Double? let minimumRemainingPercent: Double let decision: String let exitCode: Int32 let unavailableReason: String? private enum CodingKeys: String, CodingKey { case provider case window case remainingPercent case minimumRemainingPercent case decision case exitCode case unavailableReason } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.provider, forKey: .provider) try container.encode(self.window, forKey: .window) try container.encode(self.minimumRemainingPercent, forKey: .minimumRemainingPercent) try container.encode(self.decision, forKey: .decision) try container.encode(self.exitCode, forKey: .exitCode) if let remainingPercent = self.remainingPercent { try container.encode(remainingPercent, forKey: .remainingPercent) } else { try container.encodeNil(forKey: .remainingPercent) } if let unavailableReason = self.unavailableReason { try container.encode(unavailableReason, forKey: .unavailableReason) } else { try container.encodeNil(forKey: .unavailableReason) } } } // swiftlint:disable:next function_parameter_count private static func emitGuardResult( provider: UsageProvider, window: GuardWindow, minimumRemainingPercent: Double, evaluation: GuardEvaluation, json: Bool, pretty: Bool) { if json { let payload = GuardResultPayload( provider: provider.rawValue, window: window.payloadValue, remainingPercent: evaluation.remainingPercent, minimumRemainingPercent: minimumRemainingPercent, decision: evaluation.decision.rawValue, exitCode: evaluation.exitCode, unavailableReason: evaluation.unavailableReason?.rawValue) Self.printJSON(payload, pretty: pretty) return } print(self.guardHumanLine( provider: provider, window: window, remainingPercent: evaluation.remainingPercent, minimumRemainingPercent: minimumRemainingPercent, decision: evaluation.decision, unavailableReason: evaluation.unavailableReason)) } static func guardHumanLine( provider: UsageProvider, window: GuardWindow, remainingPercent: Double?, minimumRemainingPercent: Double, decision: GuardDecision, unavailableReason: GuardUnavailableReason? = nil) -> String { let remainingText = remainingPercent .map { "\(Self.guardPercentString($0)) remaining" } ?? "unknown" let verdict = switch decision { case .ok: "OK" case .blocked: "BLOCKED" case .unknown: "UNKNOWN" } let reasonText = unavailableReason.map { "; \($0.rawValue)" } ?? "" return "\(provider.rawValue) \(window.payloadValue): \(remainingText) — " + "\(verdict) (minimum \(Self.guardPercentString(minimumRemainingPercent))\(reasonText))" } private static func guardPercentString(_ value: Double) -> String { let rounded = value.rounded() if abs(value - rounded) < 0.05 { return "\(Int(rounded))%" } return String(format: "%.1f%%", value) }}
import CodexBarCore
import Commander
import Foundation
extension CodexBarCLI {
/// Window selected by the `guard` command: `session` maps to the primary
/// rate window, `weekly` maps to the secondary rate window.
enum GuardWindow: String {
case session
case weekly
var payloadValue: String {
self.rawValue
}
}
/// Pure gating outcome. Kept free of I/O so it is unit-testable off-network.
enum GuardDecision: String {
case ok
case blocked
case unknown
}
enum GuardUnavailableReason: String, Sendable {
case accountResolution = "account-resolution"
case fetchFailed = "fetch-failed"
case timeout
case windowUnavailable = "window-unavailable"
}
enum GuardFetchOutcome: Sendable {
case available(Double)
case unavailable(GuardUnavailableReason)
}
struct GuardEvaluation: Sendable {
let decision: GuardDecision
let exitCode: Int32
let remainingPercent: Double?
let unavailableReason: GuardUnavailableReason?
}
/// Command-specific stable status codes. `69` is sysexits `EX_UNAVAILABLE`.
private enum GuardExitCode: Int32 {
case safe = 0
case blocked = 1
case unavailable = 69
}
/// Pure decision core for `codexbar guard`.
///
/// - unavailable quota → `.unknown` (exit `0` when `failOpen`, else `69`).
/// - remaining quota at or above the threshold → `.ok` (exit `0`).
/// - otherwise → `.blocked` (exit `1`).
static func evaluateGuard(
outcome: GuardFetchOutcome,
minimumRemainingPercent: Double,
failOpen: Bool) -> GuardEvaluation
{
guard case let .available(remainingPercent) = outcome else {
guard case let .unavailable(reason) = outcome else { preconditionFailure("Unhandled guard outcome") }
return GuardEvaluation(
decision: .unknown,
exitCode: failOpen ? GuardExitCode.safe.rawValue : GuardExitCode.unavailable.rawValue,
remainingPercent: nil,
unavailableReason: reason)
}
if remainingPercent >= minimumRemainingPercent {
return GuardEvaluation(
decision: .ok,
exitCode: GuardExitCode.safe.rawValue,
remainingPercent: remainingPercent,
unavailableReason: nil)
}
return GuardEvaluation(
decision: .blocked,
exitCode: GuardExitCode.blocked.rawValue,
remainingPercent: remainingPercent,
unavailableReason: nil)
}
/// Remaining headroom (`100 - usedPercent`) for a resolved rate window, or `nil` when the window
/// is absent or a synthetic placeholder. A synthetic window is a lane the provider did not
/// actually report (e.g. Claude with no live five-hour session), so it must not read as free
/// headroom and let the gate pass on a phantom metric.
static func guardRemainingHeadroom(for window: RateWindow?) -> Double? {
guard let window, !window.isSyntheticPlaceholder else { return nil }
return 100 - window.usedPercent
}
static func runGuard(_ values: ParsedValues) async {
let output = CLIOutputPreferences.from(values: values)
let json = values.flags.contains("json")
let failOpen = values.flags.contains("failOpen")
let verbose = values.flags.contains("verbose")
guard let window = Self.decodeGuardWindow(from: values) else {
Self.exitGuardArgumentError("--window must be session|weekly.", output: output)
}
let minimumRemainingPercent: Double
switch Self.decodeGuardMinimumRemaining(from: values) {
case let .success(value):
minimumRemainingPercent = value
case .failure:
Self.exitGuardArgumentError(
"--min-remaining must be a finite percent between 0 and 100.",
output: output)
}
let timeout: TimeInterval
switch Self.decodeGuardTimeout(from: values) {
case let .success(value):
timeout = value
case .failure:
Self.exitGuardArgumentError(
"--timeout must be a finite number of seconds from 0 through 86400.",
output: output)
}
let provider: UsageProvider
switch Self.decodeGuardProvider(from: values) {
case let .success(value):
provider = value
case let .failure(error):
Self.exitGuardArgumentError(error.localizedDescription, output: output)
}
let config = Self.loadConfig(output: output)
let outcome = await Self.runGuardFetch(timeout: timeout) {
await ProviderInteractionContext.$current.withValue(.background) {
await Self.guardFetchOutcome(
provider: provider,
window: window,
config: config,
verbose: verbose,
webTimeout: timeout > 0 ? timeout : 60)
}
}
if case .unavailable(.timeout) = outcome {
TTYCommandRunner.terminateActiveProcessesForAppShutdown()
}
let evaluation = Self.evaluateGuard(
outcome: outcome,
minimumRemainingPercent: minimumRemainingPercent,
failOpen: failOpen)
Self.emitGuardResult(
provider: provider,
window: window,
minimumRemainingPercent: minimumRemainingPercent,
evaluation: evaluation,
json: json,
pretty: output.pretty)
Self.platformExit(evaluation.exitCode)
}
// MARK: - Argument decoding
private static func exitGuardArgumentError(_ message: String, output: CLIOutputPreferences) -> Never {
self.exit(code: .usage, message: "Error: \(message)", output: output, kind: .args)
}
static func decodeGuardWindow(from values: ParsedValues) -> GuardWindow? {
guard let raw = values.options["window"]?.last else { return .session }
return GuardWindow(rawValue: raw.lowercased())
}
static func guardProvider(rawOverride: String?) -> Result<UsageProvider, CLIArgumentError> {
guard let rawOverride else {
return .failure(CLIArgumentError("guard requires --provider <id>."))
}
guard let selection = ProviderSelection(argument: rawOverride) else {
return .failure(CLIArgumentError("unknown provider '\(rawOverride)'."))
}
guard selection.asList.count == 1, let provider = selection.asList.first else {
return .failure(CLIArgumentError("guard requires exactly one --provider."))
}
return .success(provider)
}
private static func decodeGuardProvider(from values: ParsedValues) -> Result<UsageProvider, CLIArgumentError> {
self.guardProvider(rawOverride: values.options["provider"]?.last)
}
static func decodeGuardMinimumRemaining(from values: ParsedValues) -> Result<Double, CLIArgumentError> {
guard let raw = values.options["minRemaining"]?.last else { return .success(10) }
guard let value = Double(raw), value.isFinite, value >= 0, value <= 100 else {
return .failure(CLIArgumentError("--min-remaining must be a finite percent between 0 and 100."))
}
return .success(value)
}
static func decodeGuardTimeout(from values: ParsedValues) -> Result<TimeInterval, CLIArgumentError> {
self.guardTimeout(raw: values.options["timeout"]?.last)
}
static func guardTimeout(raw: String?) -> Result<TimeInterval, CLIArgumentError> {
guard let raw else { return .success(60) }
guard let value = TimeInterval(raw), value.isFinite, value >= 0, value <= 86400 else {
return .failure(CLIArgumentError("--timeout must be a finite number of seconds from 0 through 86400."))
}
return .success(value)
}
// MARK: - Fetch
static func runGuardFetch(
timeout: TimeInterval,
operation: @escaping @Sendable () async -> GuardFetchOutcome) async -> GuardFetchOutcome
{
let sourceTask = Task<GuardFetchOutcome, Error> {
await operation()
}
guard timeout > 0 else {
return await (try? sourceTask.value) ?? .unavailable(.fetchFailed)
}
let join = BoundedTaskJoin(sourceTask: sourceTask)
return switch await join.value(joinGrace: .seconds(timeout)) {
case let .value(outcome): outcome
case .failure: .unavailable(.fetchFailed)
case .timedOut: .unavailable(.timeout)
}
}
private static func guardFetchOutcome(
provider: UsageProvider,
window: GuardWindow,
config: CodexBarConfig,
verbose: Bool,
webTimeout: TimeInterval) async -> GuardFetchOutcome
{
let tokenContext: TokenAccountCLIContext
do {
tokenContext = try TokenAccountCLIContext(
selection: TokenAccountCLISelection(label: nil, index: nil, allAccounts: false),
config: config,
verbose: verbose)
} catch {
return .unavailable(.accountResolution)
}
// Resolve the configured token account the same way `usage` does, so token-only
// providers (e.g. Claude, z.ai, OpenAI) fetch their quota instead of returning unknown.
let account: ProviderTokenAccount?
do {
account = try tokenContext.resolvedAccounts(for: provider).first
} catch {
return .unavailable(.accountResolution)
}
let browserDetection = BrowserDetection()
let fetcher = UsageFetcher()
let claudeFetcher = ClaudeUsageFetcher(browserDetection: browserDetection)
let env = tokenContext.environment(
base: ProcessInfo.processInfo.environment,
provider: provider,
account: account)
let settings = tokenContext.settingsSnapshot(for: provider, account: account)
let baseSource = tokenContext.preferredSourceMode(for: provider)
let effectiveSourceMode = tokenContext.effectiveSourceMode(
base: baseSource,
provider: provider,
account: account)
let fetchContext = ProviderFetchContext(
runtime: .cli,
sourceMode: effectiveSourceMode,
includeCredits: false,
webTimeout: webTimeout,
webDebugDumpHTML: false,
verbose: verbose,
env: env,
settings: settings,
fetcher: tokenContext.fetcher(base: fetcher, provider: provider, env: env),
claudeFetcher: claudeFetcher,
browserDetection: browserDetection,
// Guard is read-only: omit updater callbacks so refresh-dependent credentials fail unavailable.
selectedTokenAccountID: account?.id)
let outcome = await Self.fetchProviderUsage(provider: provider, context: fetchContext)
if verbose {
Self.printFetchAttempts(provider: provider, attempts: outcome.attempts)
}
switch outcome.result {
case let .success(result):
let usage = result.usage.scoped(to: provider)
let rateWindow = window == .session ? usage.primary : usage.secondary
guard let remaining = Self.guardRemainingHeadroom(for: rateWindow) else {
return .unavailable(.windowUnavailable)
}
return .available(remaining)
case .failure:
return .unavailable(.fetchFailed)
}
}
// MARK: - Output
private struct GuardResultPayload: Encodable {
let provider: String
let window: String
let remainingPercent: Double?
let minimumRemainingPercent: Double
let decision: String
let exitCode: Int32
let unavailableReason: String?
private enum CodingKeys: String, CodingKey {
case provider
case window
case remainingPercent
case minimumRemainingPercent
case decision
case exitCode
case unavailableReason
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.provider, forKey: .provider)
try container.encode(self.window, forKey: .window)
try container.encode(self.minimumRemainingPercent, forKey: .minimumRemainingPercent)
try container.encode(self.decision, forKey: .decision)
try container.encode(self.exitCode, forKey: .exitCode)
if let remainingPercent = self.remainingPercent {
try container.encode(remainingPercent, forKey: .remainingPercent)
} else {
try container.encodeNil(forKey: .remainingPercent)
}
if let unavailableReason = self.unavailableReason {
try container.encode(unavailableReason, forKey: .unavailableReason)
} else {
try container.encodeNil(forKey: .unavailableReason)
}
}
}
// swiftlint:disable:next function_parameter_count
private static func emitGuardResult(
provider: UsageProvider,
window: GuardWindow,
minimumRemainingPercent: Double,
evaluation: GuardEvaluation,
json: Bool,
pretty: Bool)
{
if json {
let payload = GuardResultPayload(
provider: provider.rawValue,
window: window.payloadValue,
remainingPercent: evaluation.remainingPercent,
minimumRemainingPercent: minimumRemainingPercent,
decision: evaluation.decision.rawValue,
exitCode: evaluation.exitCode,
unavailableReason: evaluation.unavailableReason?.rawValue)
Self.printJSON(payload, pretty: pretty)
return
}
print(self.guardHumanLine(
provider: provider,
window: window,
remainingPercent: evaluation.remainingPercent,
minimumRemainingPercent: minimumRemainingPercent,
decision: evaluation.decision,
unavailableReason: evaluation.unavailableReason))
}
static func guardHumanLine(
provider: UsageProvider,
window: GuardWindow,
remainingPercent: Double?,
minimumRemainingPercent: Double,
decision: GuardDecision,
unavailableReason: GuardUnavailableReason? = nil) -> String
{
let remainingText = remainingPercent
.map { "\(Self.guardPercentString($0)) remaining" } ?? "unknown"
let verdict = switch decision {
case .ok: "OK"
case .blocked: "BLOCKED"
case .unknown: "UNKNOWN"
}
let reasonText = unavailableReason.map { "; \($0.rawValue)" } ?? ""
return "\(provider.rawValue) \(window.payloadValue): \(remainingText) — "
+ "\(verdict) (minimum \(Self.guardPercentString(minimumRemainingPercent))\(reasonText))"
}
private static func guardPercentString(_ value: Double) -> String {
let rounded = value.rounded()
if abs(value - rounded) < 0.05 {
return "\(Int(rounded))%"
}
return String(format: "%.1f%%", value)
}
}