Skip to content

fix(antigravity): fall back to agy usage report when OAuth quota endpoints return 403 - #3708

Closed
Chener wants to merge 4 commits into
steipete:mainfrom
Chener:fm/codexbar-ag-limits-pr-s1
Closed

Chener wants to merge 4 commits into
steipete:mainfrom
Chener:fm/codexbar-ag-limits-pr-s1

Conversation

@Chener

@Chener Chener commented Sep 17, 2026 •

Copy link
Copy Markdown

What Changed

  • AntigravityOAuthFetchStrategy now re-runs agy -p /usage in Auto mode when the account-scoped OAuth fetch identifies a selected Google account but returns no model quotas (for example, retrieveUserQuota answering 403), and only when the resolved local agy login email matches that account. The report is attributed to the selected account with the cli source label; a missing email, a mismatch, a missing binary, or a failed report keeps the OAuth identity-only result (Limits not available).
  • Added AntigravityCLIIdentityResolver, which resolves the local agy login email from the gemini/antigravity generic-password Keychain item, else from the first email found in ~/.gemini/antigravity-cli/settings.json, auth.json, or jetski_state.pbtxt.
  • Updated the Usage source settings subtitle, docs/antigravity.md, and CHANGELOG.md to describe the identity-match rule, and added AntigravityIdentityFallbackTests covering the matched-identity fallback and the unmatched-identity Limits not available path with a stubbed OAuth transport and a fake agy binary.

Risk Assessment

🚨 High: No new findings, but the code is unchanged since the previous round and the declined error-level defects remain in place (CodexBarTests target cannot compile because MockUsageFetcher conforms to a concrete struct and MockClaudeFetcher omits three protocol requirements; the unconditional import Security breaks the always-on Linux CLI build; the CLI fallback attributes an identity-free agy report to the selected account based on a guessed local file), so merging still requires the owner's explicit sign-off.

Testing

Baseline: swift build --build-tests on the unmodified branch fails with eight compile errors in the branch's new Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift (non-protocol conformance to the UsageFetcher struct, missing ClaudeUsageFetching requirements, unknown CLICommand and BrowserDetection.standard, non-Sendable static handler), so no test in the package can run on macOS and the branch's own two tests never execute; this was already declined in review and is reported only as observed state. To obtain behavioral signal I temporarily moved that file aside, added a transient Swift Testing harness that drives the real AntigravityOAuthFetchStrategy with URLSession.shared stubbed for cloudcode-pa.googleapis.com (loadCodeAssist 200, fetchAvailableModels 403, retrieveUserQuota 403) and a fake agy selected via ANTIGRAVITY_CLI_PATH (real agy never executed), ran it with a focused swift test --filter, then removed the harness and restored the branch file; the worktree is clean. All eight harness cases passed in-process: matching identity yields source "cli" with primary/secondary/four extra windows and the selected email; unprovable identity, foreign identity, failing or too-old agy, explicit oauth mode, auto mode without a selected account, and remote-quotas-present all keep the oauth result and never invoke agy. These harness runs are not live product validation and no scenario is marked pass on their basis. Two observations were recorded without findings because they map to declined review items: a selected account with no known email accepts any CLI identity and returns CLI quotas with a nil email (OBS-A), and on this machine's real agy install (settings.json and jetski_state.pbtxt present, no auth.json, no gemini/antigravity Keychain item) the resolver finds no email, so the fallback cannot fire here (OBS-B). No live end-to-end run or UI screenshot was possible: the remote host cannot be redirected in the real binary, and exercising it would need a real Google account whose quota endpoints return 403 plus a real agy login, which CLAUDE.md forbids without explicit request.

  • Live validation: ⚠️ inconclusive - 0 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Selected Antigravity account whose remote quota endpoints return 403, local agy identity matches the selected email: menu shows agy CLI quotas attributed to the selected account (source cli) ⏸️ untested no Not driven against the running CodexBar app or CLI. The only run was an in-process Swift Testing harness with the Google host stubbed on URLSession.shared and a fake agy binary, which does not establi…
Selected account, remote 403, no discoverable local agy identity: usage stays 'Limits not available' (oauth identity-only snapshot) and agy is never launched ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with stubbed remote responses and a fake agy, which does not establish a live result. A live run needs a real 403-a…
Adversarial: selected account A, local agy identity says account B, remote 403: no cross-account attribution, agy never launched ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with stubbed remote responses and a fake agy, which does not establish a live result. A live run needs two real Goo…
Adversarial: identity matches but agy fails on --version or is older than 1.1.11: fallback fails soft to the oauth snapshot without throwing ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with stubbed remote responses and a fake agy that fails or reports an old version, which does not establish a live…
Fallback gated to auto mode with a selected token account: explicit oauth mode, or auto mode with env-injected credentials but no selected account, never consults agy ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with stubbed remote responses, which does not establish a live result. A live run needs the real app configured in…
Non-regression: remote fetchAvailableModels returns quotas: normal oauth result with remote quotas, CLI fallback not consulted ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with a stubbed 200 fetchAvailableModels response, which does not establish a live result. A live run needs a real A…
Live: CodexBar menu bar app or codexbar usage --provider antigravity with a real selected Google account that receives 403 from the quota endpoints and a logged-in agy shows CLI quotas instead of 'L… ⏸️ untested no Requires a real Antigravity Google account whose fetchAvailableModels/retrieveUserQuota return 403, a real agy login for the same account, and a Keychain-backed token account; CLAUDE.md forbids live…
Evidence: Per-scenario output of the real OAuth strategy under the in-process harness (not live)
S1: source=cli strategy=antigravity.oauth primary=14% used secondary=11% used extraWindows=[Gemini 5-hour=5% used, Gemini weekly=14% used, Claude/GPT 5-hour=0% used, Claude/GPT weekly=11% used] identity.email=selected@example.com identity.loginMethod=cli | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 403", "/v1internal:retrieveUserQuota -> 403"] | agyCalls=["--version", "-p /usage --output-format json --print-timeout 90s"]
S2: source=oauth strategy=antigravity.oauth primary=nil secondary=nil extraWindows=[] identity.email=selected@example.com identity.loginMethod=Free | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 403", "/v1internal:retrieveUserQuota -> 403"] | agyCalls=[]
S3: source=oauth strategy=antigravity.oauth primary=nil secondary=nil extraWindows=[] identity.email=selected@example.com identity.loginMethod=Free | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 403", "/v1internal:retrieveUserQuota -> 403"] | agyCalls=[]
S4[failsVersion]: source=oauth strategy=antigravity.oauth primary=nil secondary=nil extraWindows=[] identity.email=selected@example.com identity.loginMethod=Free | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 403", "/v1internal:retrieveUserQuota -> 403"] | agyCalls=["--version"]
S4[oldVersion]: source=oauth strategy=antigravity.oauth primary=nil secondary=nil extraWindows=[] identity.email=selected@example.com identity.loginMethod=Free | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 403", "/v1internal:retrieveUserQuota -> 403"] | agyCalls=["--version"]
S5[mode=oauth selected=true]: source=oauth strategy=antigravity.oauth primary=nil secondary=nil extraWindows=[] identity.email=selected@example.com identity.loginMethod=Free | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 403", "/v1internal:retrieveUserQuota -> 403"] | agyCalls=[]
S5[mode=auto selected=false]: source=oauth strategy=antigravity.oauth primary=nil secondary=nil extraWindows=[] identity.email=selected@example.com identity.loginMethod=Free | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 403", "/v1internal:retrieveUserQuota -> 403"] | agyCalls=[]
S6: source=oauth strategy=antigravity.oauth primary=20% used secondary=50% used extraWindows=[] identity.email=selected@example.com identity.loginMethod=Free | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 200"] | agyCalls=[]
OBS-A[selected email unknown, cli config says other]: source=cli strategy=antigravity.oauth primary=14% used secondary=11% used extraWindows=[Gemini 5-hour=5% used, Gemini weekly=14% used, Claude/GPT 5-hour=0% used, Claude/GPT weekly=11% used] identity.email=nil identity.loginMethod=cli | remote=["/v1internal:loadCodeAssist -> 200", "/v1internal:fetchAvailableModels -> 403", "/v1internal:retrieveUserQuota -> 403"] | agyCalls=["--version", "-p /usage --output-format json --print-timeout 90s"]
OBS-B real-install: resolver(realHOME) found email=false; resolver(emptyHOME → Keychain only) found email=false; files: settings.json=present auth.json=absent jetski_state.pbtxt=present
Evidence: swift test transcript for the harness run
$ NM_EVIDENCE_LOG=... CODEXBAR_SUPPRESS_TEST_KEYCHAIN_ACCESS=1 swift test --filter AntigravityOAuthCLIQuotaFallbackProofTests
(branch test file AntigravityIdentityFallbackTests.swift temporarily set aside because it does not compile; product sources unchanged)

✔ Test "S1 remote 403 + CLI identity matches selected account -> CLI quotas attributed" passed after 1.022 seconds.
✔ Test "S2 remote 403 + no discoverable CLI identity -> oauth Limits-not-available, agy never run" passed after 0.004 seconds.
✔ Test "S3 remote 403 + CLI identity is a different account -> no cross-account attribution" passed after 0.002 seconds.
✔ Test "S4 remote 403 + matching identity but agy cannot report -> fails soft to oauth snapshot" with 2 test cases passed after 1.088 seconds.
✔ Test "S5 fallback gated to auto mode with a selected account" with 2 test cases passed after 0.012 seconds.
✔ Test "S6 remote quotas present -> normal oauth result, CLI fallback never consulted" passed after 0.009 seconds.
✔ Test "OBS-A selected account without a known email + any CLI identity (observation only)" passed after 3.184 seconds.
✔ Test "OBS-B identity resolver against this machine's real agy install (observation only)" passed after 0.003 seconds.
✔ Suite AntigravityOAuthCLIQuotaFallbackProofTests passed after 5.329 seconds.
✔ Test run with 8 tests in 1 suite passed after 5.329 seconds.
EXIT=0
Evidence: Compile failure of the branch's own test file (blocks macOS test bundle)

Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:226:8: error: inheritance from non-protocol type 'UsageFetcher' Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:231:8: error: type 'MockClaudeFetcher' does not conform to protocol 'ClaudeUsageFetching' Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:228:28: error: cannot find type 'CLICommand' in scope Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:130:48: error: type 'BrowserDetection' has no member 'standard' Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:7:16: error: static property 'handler' is not concurrency-safe because it is nonisolated global shared mutable state error: emit-module command failed with exit code 1

$ swift build --build-tests   (branch f99c5db97, macOS, worktree unchanged)

Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:128:22: error: cannot convert value of type 'MockUsageFetcher' to expected argument type 'UsageFetcher'
Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:130:48: error: type 'BrowserDetection' has no member 'standard'
Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:211:22: error: cannot convert value of type 'MockUsageFetcher' to expected argument type 'UsageFetcher'
Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:213:48: error: type 'BrowserDetection' has no member 'standard'
Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:226:8: error: inheritance from non-protocol type 'UsageFetcher'
Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:228:28: error: cannot find type 'CLICommand' in scope
Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:231:8: error: type 'MockClaudeFetcher' does not conform to protocol 'ClaudeUsageFetching'
Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:7:16: error: static property 'handler' is not concurrency-safe because it is nonisolated global shared mutable state [#MutableGlobalVariable]

error: emit-module command failed with exit code 1 (use -v to see invocation)
exit status: 1
Evidence: Transient harness source (not committed; kept for reproducibility)
import Foundation
import Testing
@testable import CodexBarCore

// Transient validation harness (no-mistakes test phase). Drives the real
// `AntigravityOAuthFetchStrategy` with a stubbed Google API on URLSession.shared
// and a fake `agy` binary selected through ANTIGRAVITY_CLI_PATH, so the real
// local `agy` is never executed.

private final class AntigravityFallbackProofURLProtocol: URLProtocol {
    private static let _handlerBox = LockIsolated<(@Sendable (URLRequest) throws -> (HTTPURLResponse, Data))?>(nil)
    static var handler: (@Sendable (URLRequest) throws -> (HTTPURLResponse, Data))? {
        get { Self._handlerBox.value }
        set { Self._handlerBox.setValue(newValue) }
    }

    override static func canInit(with request: URLRequest) -> Bool {
        let host = request.url?.host ?? ""
        return host == "cloudcode-pa.googleapis.com" || host == "oauth2.googleapis.com"
    }

    override static func canonicalRequest(for request: URLRequest) -> URLRequest {
        request
    }

    override func startLoading() {
        guard let handler = Self.handler else {
            self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
            return
        }
        do {
            let (response, data) = try handler(self.request)
            self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            self.client?.urlProtocol(self, didLoad: data)
            self.client?.urlProtocolDidFinishLoading(self)
        } catch {
            self.client?.urlProtocol(self, didFailWithError: error)
        }
    }

    override func stopLoading() {}
}

@Suite(.serialized)
struct AntigravityOAuthCLIQuotaFallbackProofTests {
    enum RemoteMode: String {
        case quotaEndpoints403
        case quotasAvailable
    }

    enum FakeAgy: String {
        case validReport
        case failsVersion
        case oldVersion
    }

    struct Fixture {
        let home: URL
        let agy: URL
        let callsLog: URL
        let requests: LockIsolated<[String]>

        var agyCalls: [String] {
            (try? String(contentsOf: self.callsLog, encoding: .utf8))?
                .split(separator: "\n").map(String.init) ?? []
        }

        func cleanup() {
            try? FileManager.default.removeItem(at: self.home)
        }
    }

    static let selectedEmail = "selected@example.com"
    static let otherEmail = "someone-else@example.com"

    // MARK: - Scenarios

    @Test
    func `S1 remote 403 + CLI identity matches selected account -> CLI quotas attributed`() async throws {
        let fx = try Self.makeFixture(remote: .quotaEndpoints403, agy: .validReport, cliConfigEmail: Self.selectedEmail)
        defer { fx.cleanup() }
        let result = try await Self.run(fx, selectedEmail: Self.selectedEmail, sourceMode: .auto, selected: true)
        Self.log("S1", fx, result)

        #expect(result.sourceLabel == "cli")
        #expect(result.usage.primary != nil)
        #expect(result.usage.secondary != nil)
        #expect(result.usage.identity?.accountEmail == Self.selectedEmail)
        #expect(result.usage.identity?.loginMethod == "cli")
        #expect(fx.agyCalls.contains { $0.hasPrefix("-p /usage") })
    }

    @Test
    func `S2 remote 403 + no discoverable CLI identity -> oauth Limits-not-available, agy never run`() async throws {
        let fx = try Self.makeFixture(remote: .quotaEndpoints403, agy: .validReport, cliConfigEmail: nil)
        defer { fx.cleanup() }
        let result = try await Self.run(fx, selectedEmail: Self.selectedEmail, sourceMode: .auto, selected: true)
        Self.log("S2", fx, result)

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.primary == nil)
        #expect(result.usage.identity?.accountEmail == Self.selectedEmail)
        #expect(result.usage.identity?.loginMethod != "cli")
        #expect(fx.agyCalls.isEmpty)
    }

    @Test
    func `S3 remote 403 + CLI identity is a different account -> no cross-account attribution`() async throws {
        let fx = try Self.makeFixture(remote: .quotaEndpoints403, agy: .validReport, cliConfigEmail: Self.otherEmail)
        defer { fx.cleanup() }
        let result = try await Self.run(fx, selectedEmail: Self.selectedEmail, sourceMode: .auto, selected: true)
        Self.log("S3", fx, result)

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.primary == nil)
        #expect(result.usage.identity?.accountEmail == Self.selectedEmail)
        #expect(fx.agyCalls.isEmpty)
    }

    @Test(arguments: [FakeAgy.failsVersion, FakeAgy.oldVersion])
    func `S4 remote 403 + matching identity but agy cannot report -> fails soft to oauth snapshot`(
        agy: FakeAgy) async throws
    {
        let fx = try Self.makeFixture(remote: .quotaEndpoints403, agy: agy, cliConfigEmail: Self.selectedEmail)
        defer { fx.cleanup() }
        let result = try await Self.run(fx, selectedEmail: Self.selectedEmail, sourceMode: .auto, selected: true)
        Self.log("S4[\(agy.rawValue)]", fx, result)

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.primary == nil)
        #expect(result.usage.identity?.accountEmail == Self.selectedEmail)
        #expect(fx.agyCalls.contains { $0.hasPrefix("--version") })
        #expect(!fx.agyCalls.contains { $0.hasPrefix("-p /usage") } || agy == .validReport)
    }

    @Test(arguments: [(ProviderSourceMode.oauth, true), (ProviderSourceMode.auto, false)])
    func `S5 fallback gated to auto mode with a selected account`(
        sourceMode: ProviderSourceMode, selected: Bool) async throws
    {
        let fx = try Self.makeFixture(remote: .quotaEndpoints403, agy: .validReport, cliConfigEmail: Self.selectedEmail)
        defer { fx.cleanup() }
        let result = try await Self.run(fx, selectedEmail: Self.selectedEmail, sourceMode: sourceMode, selected: selected)
        Self.log("S5[mode=\(sourceMode.rawValue) selected=\(selected)]", fx, result)

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.primary == nil)
        #expect(fx.agyCalls.isEmpty)
    }

    @Test
    func `S6 remote quotas present -> normal oauth result, CLI fallback never consulted`() async throws {
        let fx = try Self.makeFixture(remote: .quotasAvailable, agy: .validReport, cliConfigEmail: Self.selectedEmail)
        defer { fx.cleanup() }
        let result = try await Self.run(fx, selectedEmail: Self.selectedEmail, sourceMode: .auto, selected: true)
        Self.log("S6", fx, result)

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.primary != nil)
        #expect(result.usage.identity?.accountEmail == Self.selectedEmail)
        #expect(result.usage.identity?.loginMethod != "cli")
        #expect(fx.agyCalls.isEmpty)
    }

    @Test
    func `OBS-A selected account without a known email + any CLI identity (observation only)`() async throws {
        let fx = try Self.makeFixture(remote: .quotaEndpoints403, agy: .validReport, cliConfigEmail: Self.otherEmail)
        defer { fx.cleanup() }
        let result = try await Self.run(fx, selectedEmail: nil, sourceMode: .auto, selected: true)
        Self.log("OBS-A[selected email unknown, cli config says other]", fx, result)
    }

    @Test
    func `OBS-B identity resolver against this machine's real agy install (observation only)`() {
        let realEnv = ProcessInfo.processInfo.environment
        let realFound = AntigravityCLIIdentityResolver.resolveCLIEmail(env: realEnv) != nil
        let emptyHome = FileManager.default.temporaryDirectory
            .appendingPathComponent("nm-empty-home-" + UUID().uuidString, isDirectory: true)
        try? FileManager.default.createDirectory(at: emptyHome, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: emptyHome) }
        let keychainOnlyFound = AntigravityCLIIdentityResolver.resolveCLIEmail(env: ["HOME": emptyHome.path]) != nil
        let home = realEnv["HOME"] ?? NSHomeDirectory()
        let dir = home + "/.gemini/antigravity-cli/"
        let present = ["settings.json", "auth.json", "jetski_state.pbtxt"].map { name 

... [955 bytes truncated] ...

hIntermediateDirectories: true)
            let settings = "{\"accountEmail\": \"\(cliConfigEmail)\", \"theme\": \"dark\"}\n"
            try settings.write(to: configDir.appendingPathComponent("settings.json"), atomically: true, encoding: .utf8)
        }

        let callsLog = home.appendingPathComponent("agy-calls.log")
        let report = home.appendingPathComponent("agy-report.json")
        try Self.reportJSON.write(to: report, atomically: true, encoding: .utf8)
        let agyPath = home.appendingPathComponent("agy")
        let version: String = switch agy {
        case .validReport: "1.2.5"
        case .failsVersion: ""
        case .oldVersion: "1.0.0"
        }
        let versionLine = agy == .failsVersion ? "exit 1" : "echo \"\(version)\"; exit 0"
        let script = """
        #!/bin/sh
        printf '%s\\n' "$*" >> "\(callsLog.path)"
        if [ "$1" = "--version" ]; then \(versionLine); fi
        if [ "$1" = "-p" ] && [ "$2" = "/usage" ]; then cat "\(report.path)"; exit 0; fi
        exit 1
        """
        try script.write(to: agyPath, atomically: true, encoding: .utf8)
        try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: agyPath.path)

        let requests = LockIsolated<[String]>([])
        AntigravityFallbackProofURLProtocol.handler = { request in
            guard let url = request.url else { throw URLError(.badURL) }
            func reply(_ status: Int, _ body: Data) -> (HTTPURLResponse, Data) {
                requests.setValue(requests.value + ["\(url.path) -> \(status)"])
                return GeminiAPITestHelpers.response(url: url.absoluteString, status: status, body: body)
            }
            switch url.path {
            case "/v1internal:loadCodeAssist":
                return reply(200, GeminiAPITestHelpers.jsonData([
                    "currentTier": ["id": "free-tier", "name": "free"],
                    "cloudaicompanionProject": "managed-project-123",
                ]))
            case "/v1internal:fetchAvailableModels":
                switch remote {
                case .quotaEndpoints403:
                    return reply(403, Data(#"{"error":{"code":403,"status":"PERMISSION_DENIED"}}"#.utf8))
                case .quotasAvailable:
                    return reply(200, GeminiAPITestHelpers.jsonData([
                        "models": [
                            "gemini-3-pro-low": [
                                "displayName": "Gemini 3 Pro Low",
                                "quotaInfo": ["remainingFraction": 0.8, "resetTime": "2026-09-18T00:00:00Z"],
                            ],
                            "claude-sonnet-4": [
                                "displayName": "Claude Sonnet 4",
                                "quotaInfo": ["remainingFraction": 0.5, "resetTime": "2026-09-18T00:00:00Z"],
                            ],
                        ],
                    ]))
                }
            case "/v1internal:retrieveUserQuota":
                return reply(403, Data(#"{"error":{"code":403,"status":"PERMISSION_DENIED"}}"#.utf8))
            default:
                return reply(404, Data())
            }
        }
        return Fixture(home: home, agy: agyPath, callsLog: callsLog, requests: requests)
    }

    static func run(
        _ fx: Fixture,
        selectedEmail: String?,
        sourceMode: ProviderSourceMode,
        selected: Bool) async throws -> ProviderFetchResult
    {
        let registered = URLProtocol.registerClass(AntigravityFallbackProofURLProtocol.self)
        defer {
            if registered { URLProtocol.unregisterClass(AntigravityFallbackProofURLProtocol.self) }
            AntigravityFallbackProofURLProtocol.handler = nil
        }
        let credentials = AntigravityOAuthCredentials(
            accessToken: "selected-access-token",
            refreshToken: nil,
            expiryDate: Date().addingTimeInterval(3600),
            idToken: nil,
            email: selectedEmail,
            projectID: "managed-project-123")
        var env: [String: String] = [
            "HOME": fx.home.path,
            "PATH": "/usr/bin:/bin",
            "ANTIGRAVITY_CLI_PATH": fx.agy.path,
            "CODEXBAR_SUPPRESS_TEST_KEYCHAIN_ACCESS": "1",
        ]
        env[AntigravityOAuthCredentialsStore.environmentCredentialsKey] =
            try AntigravityOAuthCredentialsStore.tokenAccountValue(for: credentials)
        let context = ProviderFetchContext(
            runtime: .app,
            sourceMode: sourceMode,
            includeCredits: false,
            webTimeout: 5,
            webDebugDumpHTML: false,
            verbose: false,
            env: env,
            settings: nil,
            fetcher: UsageFetcher(environment: env),
            claudeFetcher: StubClaudeFetcher(),
            browserDetection: BrowserDetection(cacheTTL: 0),
            selectedTokenAccountID: selected ? UUID() : nil)
        return try await AntigravityOAuthFetchStrategy().fetch(context)
    }

    static func log(_ name: String, _ fx: Fixture, _ result: ProviderFetchResult) {
        let usage = result.usage
        func pct(_ window: RateWindow?) -> String {
            window.map { String(format: "%.0f%% used", $0.usedPercent) } ?? "nil"
        }
        let extras = (usage.extraRateWindows ?? []).map { "\($0.title)=\(pct($0.window))" }
        let line = "\(name): source=\(result.sourceLabel) strategy=\(result.strategyID) "
            + "primary=\(pct(usage.primary)) secondary=\(pct(usage.secondary)) "
            + "extraWindows=[\(extras.joined(separator: ", "))] "
            + "identity.email=\(usage.identity?.accountEmail ?? "nil") "
            + "identity.loginMethod=\(usage.identity?.loginMethod ?? "nil") "
            + "| remote=\(fx.requests.value) | agyCalls=\(fx.agyCalls)"
        Self.emit(line)
    }

    static func emit(_ line: String) {
        print("[nm-proof] " + line)
        guard let path = ProcessInfo.processInfo.environment["NM_EVIDENCE_LOG"] else { return }
        let url = URL(fileURLWithPath: path)
        let data = Data((line + "\n").utf8)
        if let handle = try? FileHandle(forWritingTo: url) {
            handle.seekToEndOfFile()
            handle.write(data)
            try? handle.close()
        } else {
            try? data.write(to: url)
        }
    }

    private struct StubClaudeFetcher: ClaudeUsageFetching {
        func loadLatestUsage(model _: String) async throws -> ClaudeUsageSnapshot {
            throw ClaudeUsageError.parseFailed("stub")
        }

        func debugRawProbe(model _: String) async -> String { "stub" }
        func detectVersion() -> String? { nil }
    }

    static let reportJSON = """
    {
      "conversation_id": "",
      "status": "SUCCESS",
      "response": "Synthetic quota report",
      "duration_seconds": 0,
      "num_turns": 0,
      "usage": {"input_tokens": 0, "output_tokens": 0, "thinking_tokens": 0, "cache_read_tokens": 0, "total_tokens": 0},
      "command": {
        "name": "usage",
        "data": {
          "description": "Models share limits",
          "groups": [
            {
              "name": "Gemini Models",
              "description": "Gemini models",
              "buckets": [
                {"id": "gemini-weekly", "name": "Weekly Limit Remaining", "window": "weekly",
                 "remaining_fraction": 0.86, "reset_time": "2026-09-20T18:40:27Z"},
                {"id": "gemini-5h", "name": "Five Hour Limit Remaining", "window": "5h",
                 "remaining_fraction": 0.95, "reset_time": "2026-09-17T23:48:04Z"}
              ]
            },
            {
              "name": "Claude and GPT models",
              "description": "3p models",
              "buckets": [
                {"id": "3p-weekly", "name": "Weekly Limit Remaining", "window": "weekly",
                 "remaining_fraction": 0.89, "reset_time": "2026-09-21T02:38:46Z"},
                {"id": "3p-5h", "name": "Five Hour Limit Remaining", "window": "5h",
                 "remaining_fraction": 1.0, "reset_time": "2026-09-17T22:29:19Z"}
              ]
            }
          ]
        }
      }
    }
    """
}
- Outcome: ⚠️ 1 warning across 1 run (23m49s)

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🚨 **Review** - high risk

✅ No issues found.

⚠️ **Test** - 1 warning
  • ⚠️ live validation verdict: inconclusive (0 of 7 scenarios were driven live against the product); untested: Selected Antigravity account whose remote quota endpoints return 403, local agy identity matches the selected email: menu shows agy CLI quotas attributed to the selected account (source cli), Selected account, remote 403, no discoverable local agy identity: usage stays 'Limits not available' (oauth identity-only snapshot) and agy is never launched, Adversarial: selected account A, local agy identity says account B, remote 403: no cross-account attribution, agy never launched, Adversarial: identity matches but agy fails on --version or is older than 1.1.11: fallback fails soft to the oauth snapshot without throwing, Fallback gated to auto mode with a selected token account: explicit oauth mode, or auto mode with env-injected credentials but no selected account, never consults agy, Non-regression: remote fetchAvailableModels returns quotas: normal oauth result with remote quotas, CLI fallback not consulted, Live: CodexBar menu bar app or codexbar usage --provider antigravity with a real selected Google account that receives 403 from the quota endpoints and a logged-in agy shows CLI quotas instead of 'Limits not available'
  • Live validation: ⚠️ inconclusive - 0 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Selected Antigravity account whose remote quota endpoints return 403, local agy identity matches the selected email: menu shows agy CLI quotas attributed to the selected account (source cli) ⏸️ untested no Not driven against the running CodexBar app or CLI. The only run was an in-process Swift Testing harness with the Google host stubbed on URLSession.shared and a fake agy binary, which does not establi…
Selected account, remote 403, no discoverable local agy identity: usage stays 'Limits not available' (oauth identity-only snapshot) and agy is never launched ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with stubbed remote responses and a fake agy, which does not establish a live result. A live run needs a real 403-a…
Adversarial: selected account A, local agy identity says account B, remote 403: no cross-account attribution, agy never launched ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with stubbed remote responses and a fake agy, which does not establish a live result. A live run needs two real Goo…
Adversarial: identity matches but agy fails on --version or is older than 1.1.11: fallback fails soft to the oauth snapshot without throwing ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with stubbed remote responses and a fake agy that fails or reports an old version, which does not establish a live…
Fallback gated to auto mode with a selected token account: explicit oauth mode, or auto mode with env-injected credentials but no selected account, never consults agy ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with stubbed remote responses, which does not establish a live result. A live run needs the real app configured in…
Non-regression: remote fetchAvailableModels returns quotas: normal oauth result with remote quotas, CLI fallback not consulted ⏸️ untested no Not driven against the running product. Only observed through the in-process harness with a stubbed 200 fetchAvailableModels response, which does not establish a live result. A live run needs a real A…
Live: CodexBar menu bar app or codexbar usage --provider antigravity with a real selected Google account that receives 403 from the quota endpoints and a logged-in agy shows CLI quotas instead of 'L… ⏸️ untested no Requires a real Antigravity Google account whose fetchAvailableModels/retrieveUserQuota return 403, a real agy login for the same account, and a Keychain-backed token account; CLAUDE.md forbids live…
  • swift build --build-tests on the unmodified branch (fails in AntigravityIdentityFallbackTests.swift; evidence codexbartests-build-failure.txt)
  • NM_EVIDENCE_LOG=... CODEXBAR_SUPPRESS_TEST_KEYCHAIN_ACCESS=1 swift test --filter AntigravityOAuthCLIQuotaFallbackProofTests with the branch's broken test file temporarily set aside (8 in-process harness tests passed; harness source preserved in evidence dir, removed from worktree; not live product validation)
  • Harness S1 (in-process, remote stubbed, fake agy): remote 403 on fetchAvailableModels and retrieveUserQuota + settings.json email == selected email + fake agy report → source=cli, quotas populated, identity.email=selected, agy invoked with --version then -p /usage --output-format json --print-timeout 90s
  • Harness S2 (in-process): remote 403 + no CLI config email → source=oauth, primary=nil, agy never invoked
  • Harness S3 (in-process): remote 403 + settings.json email of a different account → source=oauth, primary=nil, agy never invoked
  • Harness S4 (in-process; failsVersion, oldVersion 1.0.0): matching identity but agy cannot report → source=oauth, primary=nil, only --version attempted
  • Harness S5 (in-process; sourceMode=oauth with selected account; sourceMode=auto without selected account) → source=oauth, agy never invoked
  • Harness S6 (in-process): fetchAvailableModels 200 with quotas → source=oauth with remote quotas, agy never invoked
  • Observation OBS-A: selected credentials without email + foreign CLI email → fallback runs, identity.email=nil, loginMethod=cli (matches declined review item)
  • Observation OBS-B: AntigravityCLIIdentityResolver against this machine's real HOME and against an empty HOME (Keychain only) → no email found; settings.json and jetski_state.pbtxt present, auth.json absent
  • Read-only check that /opt/homebrew/bin/agy exists and that real ~/.gemini/antigravity-cli/settings.json and jetski_state.pbtxt contain no email pattern (counts only, no values read into evidence)
  • git status --short and git diff --stat after cleanup: clean
⚠️ **Document** - 1 info
  • ⚠️ Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift:53 - The Settings picker subtitle beside Usage source still says "Auto skips agy reports without account identity for selected or injected Google accounts." After this change, Auto with a selected token account does consult the agy -p /usage report once the account-scoped OAuth fetch returns no model quotas and the local agy login email matches the selected account. docs/antigravity.md explicitly points users to this subtitle for the explanation, so the UI copy now understates the behavior. It is user-facing Swift string content rather than a documentation file, so the documentation phase did not edit it. Suggested wording: "Auto uses agy reports for a selected or injected Google account only when the local agy login matches it; otherwise it skips identity-free reports." Keep the second sentence about Local API / agy CLI unchanged.

🔧 Fix applied.
1 info still open:

  • ℹ️ Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift:332 - Judgment call for awareness: applying the user-selected Settings subtitle fix required updating the verbatim string expectation in antigravity usage source picker clarifies local ide and agy, because that test asserts the exact subtitle copy. Only the expected literal changed; no test logic or behavior was altered. No further action needed unless the outer executor prefers the copy change and test mirror to land as separate commits.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@clawsweeper

clawsweeper Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Sep 17, 2026
@clawsweeper

clawsweeper Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed September 17, 2026, 8:02 AM ET / 12:02 UTC (Revision 7).

ClawSweeper review

What this changes

Adds automatic Antigravity quota recovery through agy when OAuth returns no quotas, using locally discovered email metadata to associate the report with the selected account, with tests and updated guidance.

Merge readiness

⛔ Blocked before merge - 15 items remain

Keep open under the repository’s conservative review policy. The latest test-fixture repairs address two earlier findings, but all four previously reported production blockers remain. The selected-account recovery is distinct from the shipped explicit CLI option.

Priority: P2
Reviewed head: 59f9b006dce38aabac684a07f3cb4fbedfe45eb7
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🧂 unranked krab (1/6) Useful recovery work and repaired fixtures remain blocked by four concrete production defects and mocked-only proof.
Proof confidence 🦪 silver shellfish (2/6) Needs real behavior proof before merge: Authority-chain proof required: the hash-matched body exercises the OAuth strategy with mocked Google responses and fake agy, not the real credential-to-report boundary. Provide after-fix recovery and rejection of unknown, different, or switched CLI ownership before fallback I/O or attribution, including disabled-Keychain behavior; redacted terminal traces or runtime logs are acceptable. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🧂 unranked krab (1/6) Security review found an item that needs attention.

Verification

Check Result Evidence
Real behavior Needs proof Needs real behavior proof before merge: Authority-chain proof required: the hash-matched body exercises the OAuth strategy with mocked Google responses and fake agy, not the real credential-to-report boundary. Provide after-fix recovery and rejection of unknown, different, or switched CLI ownership before fallback I/O or attribution, including disabled-Keychain behavior; redacted terminal traces or runtime logs are acceptable. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 9 items Applicable repository policy: Read the complete root AGENTS.md. No nested AGENTS.md or maintainer-note files were found under the inspected source, test, documentation, and agent directories. Applied provider-isolation and non-interactive Keychain guidance; no builds, tests, or live credential probes were run during this read-only review.
Introduced patch and current-main boundary: The verified merge-base-to-head delta changes seven files. Current main returns the OAuth identity-only snapshot when quotas are empty; the branch adds the local-email lookup and report attribution. The existing print-report implementation removes CodexBar’s injected OAuth credentials from the child environment, so agy authenticates independently.
Account ownership is not established: The resolver accepts the first email in arbitrary Keychain metadata or whole configuration files. The new caller uses matches(), which accepts an absent expected email, and then attaches the selected email to an identity-free report. No credential-bound ownership check connects that metadata to the account used by the subprocess.
Findings 4 actionable findings [P1] [P1] Guard Security.framework code on supported platforms
[P1] [P1] Require a known selected email before accepting CLI usage
[P1] [P1] Bind fallback identity to the credentials used by agy
Security Needs attention Unverified cross-account attribution: An absent selected email or stale local email metadata can authorize an independently authenticated CLI report to populate the selected account’s usage.
Keychain opt-out and test isolation bypass: Direct SecItemCopyMatching skips the repository’s access gates and non-interactive query safeguards.

How this fits together

CodexBar’s Antigravity provider combines selected Google-account credentials and local application or CLI usage sources. Its source-selection rules determine which account’s quotas appear in the menu bar.

flowchart TD
  A[Selected Google account] --> B[OAuth quota request]
  B --> C{Quotas available?}
  C -->|Yes| F[Account usage display]
  C -->|No| D[Local email lookup]
  D --> E[agy usage report]
  E --> F
Loading

Decision needed

Question Recommendation
Should selected-account Auto recovery wait for a supported agy credential-to-report ownership contract? Preserve the current boundary: Keep explicit Local API / agy CLI recovery available and defer automatic selected-account attribution until ownership is verifiable.

Why: Shipped behavior intentionally rejects identity-free reports, and the proposed metadata heuristic cannot establish ownership; choosing a supported integration contract requires owner input.

Before merge

  • Add real behavior proof - Needs real behavior proof before merge: Authority-chain proof required: the hash-matched body exercises the OAuth strategy with mocked Google responses and fake agy, not the real credential-to-report boundary. Provide after-fix recovery and rejection of unknown, different, or switched CLI ownership before fallback I/O or attribution, including disabled-Keychain behavior; redacted terminal traces or runtime logs are acceptable. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • [P1] Guard Security.framework code on supported platforms (P1) - This new file belongs to CodexBarCore, which Package.swift builds for the Linux CLI. Linux has no Security module, so the unconditional import prevents the entire CLI from compiling. Guard the import and Keychain implementation with the repository’s macOS conditional and provide a supported non-macOS path.
  • [P1] Require a known selected email before accepting CLI usage (P1) - When selected credentials lack a resolvable email, expected is nil and matches() returns true for any discovered CLI email. The fallback consequently accepts another account’s report despite the selected-account requirement; supplied OBS-A output confirms this path. Require a nonempty selected identity before invoking the fallback.
  • [P1] Bind fallback identity to the credentials used by agy (P1) - The first email anywhere in settings or metadata is not proof of the CLI’s active account. If those files retain account A while agy authenticates as B, this resolver can return A and the caller labels B’s identity-free quotas as A. The subprocess deliberately uses its own credentials, and no subsequent ownership check repairs the mismatch. Use identity tied to the same authenticated CLI session or preserve the current rejection.
  • [P1] Respect Keychain access gates and suppress interaction (P1) - Every eligible fallback calls SecItemCopyMatching directly, bypassing KeychainSecurity’s disabled-access and test-suppression checks and omitting KeychainNoUIQuery. Users who disable Keychain access still reach the system Keychain, and the new tests reach the real store even with a synthetic HOME. Route reads through the existing guarded, non-interactive helpers and inject a test identity source.
  • Resolve security concern: Unverified cross-account attribution - An absent selected email or stale local email metadata can authorize an independently authenticated CLI report to populate the selected account’s usage.
  • Resolve security concern: Keychain opt-out and test isolation bypass - Direct SecItemCopyMatching skips the repository’s access gates and non-interactive query safeguards.
  • Resolve merge risk (P1) - Existing Auto configurations could silently display another local account’s quotas; fresh-install, upgrade, and account-switch evidence does not establish safe attribution.
  • Resolve merge risk (P1) - The new resolver bypasses disabled-Keychain and test-isolation controls, including when tests supply a synthetic HOME.
  • Resolve merge risk (P1) - The unconditional Security import prevents supported Linux CLI builds.
  • Complete next step (P2) - Resolve the four production findings and obtain owner agreement on the supported account-binding contract before enabling automatic selected-account fallback.
  • Improve patch quality - Fix the platform guard and route identity reads through non-interactive, test-isolated Keychain helpers.
  • Improve patch quality - Replace guessed email matching with supported credential-bound ownership, including rejection of unknown or switched accounts.
  • Improve patch quality - Add redacted after-fix runtime evidence, preferably screenshots or recordings where useful, with diagnostic traces proving ownership and fresh/upgrade behavior; update the PR body to trigger re-review, or ask a maintainer to comment @clawsweeper re-review.
  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.

Findings

  • [P1] [P1] Guard Security.framework code on supported platforms — Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:2
  • [P1] [P1] Require a known selected email before accepting CLI usage — Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift:888-890
  • [P1] [P1] Bind fallback identity to the credentials used by agy — Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:54-59
  • [high] Unverified cross-account attribution — Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift:890
  • [medium] Keychain opt-out and test isolation bypass — Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:27
Agent review details

Security

Needs attention: Account attribution and Keychain access cross existing safety boundaries without adequate authorization checks.

Review metrics

Metric Value Why it matters
Production and test growth production +132 net lines; tests +237 net lines The growth supports the stated automatic fallback and fixtures, but its ownership assumption remains unresolved.

Merge-risk options

Maintainer options:

  1. Retain explicit recovery pending verified ownership (recommended)
    Pause the automatic attribution change while retaining the shipped local-source option and establishing an authoritative account-binding contract.
  2. Repair and prove the account boundary
    Guard platform-specific code, restore Keychain controls, reject unknown ownership, and demonstrate credential-bound attribution through account changes.

Technical review

Best possible solution:

Preserve explicit local-source recovery until a supported, credential-bound ownership check can safely associate each agy report with the selected account.

Do we have a high-confidence way to reproduce the issue?

Yes for the introduced defects: source establishes the Linux import failure, unknown-email acceptance, and direct Keychain access; supplied mocked output corroborates unknown-email acceptance. The linked reporter’s exact runtime failure remains unverified, and this review executed no tests.

Is this the best way to solve the issue?

No. Automatic recovery is useful, but arbitrary email metadata cannot safely authenticate an identity-free report; the existing explicit local-source option is the supported alternative.

Full review comments:

  • [P1] [P1] Guard Security.framework code on supported platforms — Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:2
    This new file belongs to CodexBarCore, which Package.swift builds for the Linux CLI. Linux has no Security module, so the unconditional import prevents the entire CLI from compiling. Guard the import and Keychain implementation with the repository’s macOS conditional and provide a supported non-macOS path.
    Confidence: 1
  • [P1] [P1] Require a known selected email before accepting CLI usage — Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift:888-890
    When selected credentials lack a resolvable email, expected is nil and matches() returns true for any discovered CLI email. The fallback consequently accepts another account’s report despite the selected-account requirement; supplied OBS-A output confirms this path. Require a nonempty selected identity before invoking the fallback.
    Confidence: 1
  • [P1] [P1] Bind fallback identity to the credentials used by agy — Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:54-59
    The first email anywhere in settings or metadata is not proof of the CLI’s active account. If those files retain account A while agy authenticates as B, this resolver can return A and the caller labels B’s identity-free quotas as A. The subprocess deliberately uses its own credentials, and no subsequent ownership check repairs the mismatch. Use identity tied to the same authenticated CLI session or preserve the current rejection.
    Confidence: 0.99
  • [P1] [P1] Respect Keychain access gates and suppress interaction — Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:26-27
    Every eligible fallback calls SecItemCopyMatching directly, bypassing KeychainSecurity’s disabled-access and test-suppression checks and omitting KeychainNoUIQuery. Users who disable Keychain access still reach the system Keychain, and the new tests reach the real store even with a synthetic HOME. Route reads through the existing guarded, non-interactive helpers and inject a test identity source.
    Confidence: 1

Overall correctness: patch is incorrect
Overall confidence: 0.99

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against b6e65a83dc47.

Labels

Label justifications:

  • P2: This is a bounded provider recovery improvement with an existing in-app alternative.
  • merge-risk: 🚨 compatibility: The new Core file breaks supported Linux builds and changes existing Auto account-selection behavior.
  • merge-risk: 🚨 security-boundary: The fallback trusts unbound local identity metadata and bypasses established Keychain access controls.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: Authority-chain proof required: the hash-matched body exercises the OAuth strategy with mocked Google responses and fake agy, not the real credential-to-report boundary. Provide after-fix recovery and rejection of unknown, different, or switched CLI ownership before fallback I/O or attribution, including disabled-Keychain behavior; redacted terminal traces or runtime logs are acceptable. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

Security concerns:

  • [high] Unverified cross-account attribution — Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift:890
    An absent selected email or stale local email metadata can authorize an independently authenticated CLI report to populate the selected account’s usage.
    Confidence: 0.99
  • [medium] Keychain opt-out and test isolation bypass — Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:27
    Direct SecItemCopyMatching skips the repository’s access gates and non-interactive query safeguards.
    Confidence: 1

What I checked:

  • Applicable repository policy: Read the complete root AGENTS.md. No nested AGENTS.md or maintainer-note files were found under the inspected source, test, documentation, and agent directories. Applied provider-isolation and non-interactive Keychain guidance; no builds, tests, or live credential probes were run during this read-only review. (AGENTS.md:1, 59f9b006dce3)
  • Introduced patch and current-main boundary: The verified merge-base-to-head delta changes seven files. Current main returns the OAuth identity-only snapshot when quotas are empty; the branch adds the local-email lookup and report attribution. The existing print-report implementation removes CodexBar’s injected OAuth credentials from the child environment, so agy authenticates independently. (Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift:887, 59f9b006dce3)
  • Account ownership is not established: The resolver accepts the first email in arbitrary Keychain metadata or whole configuration files. The new caller uses matches(), which accepts an absent expected email, and then attaches the selected email to an identity-free report. No credential-bound ownership check connects that metadata to the account used by the subprocess. (Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:54, 59f9b006dce3)
  • Platform and Keychain defects: The added Core file imports Security unconditionally and calls SecItemCopyMatching directly. Package.swift includes CodexBarCore in Linux CLI builds. Existing KeychainSecurity.copyMatching enforces disabled-access and test-safety gates; KeychainNoUIQuery supplies non-interactive query settings. The new resolver bypasses both. (Sources/CodexBarCore/Providers/Antigravity/AntigravityCLIIdentityResolver.swift:27, 59f9b006dce3)
  • Re-review continuity: The current test file uses the concrete UsageFetcher initializer, implements ClaudeUsageFetching requirements, protects its handler with LockIsolated, and stubs the actual quota endpoints. Head commit metadata reports three fallback cases and 54 settings tests passing. The body’s older test-compilation failure is therefore historical, not a current finding; the four production findings from the preceding review remain visible. (Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift:153, 59f9b006dce3)
  • Captured proof inspected: Fetched body SHA-256 matches the captured source identity 468342c8092dd24ca787d5c5c62c91b4723c78394a841c16bdaa0ca4057ffd65. Inspected the supplied excerpts and expanded evidence: the OAuth strategy was exercised with URLProtocol stubs and a fake agy, with zero scenarios driven live. OBS-A reports acceptance of foreign CLI metadata when the selected email is absent; OBS-B reports no discoverable email on the inspected real installation. The downloaded image shows an unrelated historical Claude parsing error and does not prove this fallback. (59f9b006dce3)

Likely related people:

  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • sobczi: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (6 earlier review cycles)
  • reviewed 2026-09-17T08:45:22.827Z sha f99c5db :: needs real behavior proof before merge. :: [P1] [P1] Guard Security.framework code on supported platforms | [P1] [P1] Require a known selected email before accepting CLI usage | [P1] [P1] Bind fallback identity to the credentials used by agy | [P1] [P1] Respect Keychain access gates and suppress interaction | [P1] [P1] Construct test dependencies using the actual repository APIs | [P2] [P2] Make the fixtures reach the selected-account fallback
  • reviewed 2026-09-17T09:57:52.650Z sha f99c5db :: needs real behavior proof before merge. :: [P1] [P1] Guard Security.framework code on supported platforms | [P1] [P1] Require a known selected email before accepting CLI usage | [P1] [P1] Bind fallback identity to the credentials used by agy | [P1] [P1] Respect Keychain access gates and suppress interaction | [P1] [P1] Construct test dependencies using the repository APIs | [P2] [P2] Make the fixtures reach the selected-account fallback
  • reviewed 2026-09-17T10:09:30.146Z sha 968af4b :: needs real behavior proof before merge. :: [P1] [P1] Guard Security.framework code on supported platforms | [P1] [P1] Require a known selected email before accepting CLI usage | [P1] [P1] Bind fallback identity to the credentials used by agy | [P1] [P1] Respect Keychain access gates and suppress interaction | [P1] [P1] Construct test dependencies using the repository APIs | [P2] [P2] Make the fixtures reach the selected-account fallback
  • reviewed 2026-09-17T10:31:15.729Z sha f99c5db :: needs real behavior proof before merge. :: [P1] [P1] Guard Security.framework code on supported platforms | [P1] [P1] Require a known selected email before accepting CLI usage | [P1] [P1] Bind fallback identity to the credentials used by agy | [P1] [P1] Respect Keychain access gates and suppress interaction | [P1] [P1] Construct test dependencies using the repository APIs | [P2] [P2] Make the fixtures reach the selected-account fallback
  • reviewed 2026-09-17T11:24:38.391Z sha 1dffd87 :: needs real behavior proof before merge. :: [P1] [P1] Guard Security.framework code on supported platforms | [P1] [P1] Require a known selected email before accepting CLI usage | [P1] [P1] Bind fallback identity to the credentials used by agy | [P1] [P1] Respect Keychain access gates and suppress interaction | [P1] [P1] Construct test dependencies using the repository APIs | [P2] [P2] Make the fixtures reach the selected-account fallback
  • reviewed 2026-09-17T11:51:43.180Z sha 59f9b00 :: needs real behavior proof before merge. :: [P1] [P1] Guard Security.framework code on supported platforms | [P1] [P1] Require a known selected email before accepting CLI usage | [P1] [P1] Bind fallback identity to the credentials used by agy | [P1] [P1] Respect Keychain access gates and suppress interaction

@Chener
Chener marked this pull request as draft September 17, 2026 09:53
@clawsweeper clawsweeper Bot added the merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. label Sep 17, 2026
@Chener
Chener force-pushed the fm/codexbar-ag-limits-pr-s1 branch from 968af4b to f99c5db Compare September 17, 2026 10:26
@Chener Chener changed the title Fix: Antigravity CLI quota fallback for 403 API responses fix(antigravity): fall back to agy usage report when OAuth quota endpoints return 403 Sep 17, 2026
…fork-approval gate: zero jobs, conclusion action_required, identical for all 5 runs across 3 head commits of this cross-repository PR from Chener/CodexBar. A maintainer must approve the workflow; no code can change that. However, running the CI steps locally exposed a real code-caused break hidden behind the gate: the CodexBarTests target did not compile because Tests/CodexBarTests/AntigravityIdentityFallbackTests.swift used a non-concurrency-safe static handler, conformed a mock to the UsageFetcher struct as if it were a protocol, referenced a nonexistent CLICommand type, omitted all ClaudeUsageFetching requirements, and called nonexistent BrowserDetection.standard. Both macOS test shards would have failed once approved. Fix: rewrote only that test file, keeping the same two scenarios; it now builds the context like sibling suites, stubs the real cloudcode-pa.googleapis.com endpoints (loadCodeAssist 200, both quota endpoints 403), drives a fake agy honoring --version and -p /usage with a real report fixture, and asserts observable outputs (sourceLabel, primary usedPercent, identity, whether agy was launched). Regression property confirmed: with the base descriptor the matching-login test fails on all assertions; with the fallback it passes. Verified: swift build --build-tests passes; AntigravityIdentityFallbackTests 3 cases pass; ProviderSettingsDescriptorTests 54 pass; swiftformat --lint clean; swiftlint --strict 0 violations. Notes: this overlaps the declined review finding about these tests, but was required for a compiling test target and is confined to the test file. Separately, AntigravityCLIIdentityResolver.swift imports Security unconditionally (all other Core files guard with #if os(macOS)); the always-on build-linux-cli job will fail once approved. That was a declined finding and was left untouched; could not prove locally (no Linux SDK installed)
@Chener
Chener marked this pull request as ready for review September 17, 2026 11:59
@steipete

Copy link
Copy Markdown
Owner

Thanks @Chener for identifying the selected-account OAuth-to-CLI fallback gap. The fetched head has changed the older concrete-fetcher mock issue. The implementation still cannot establish the required account match: it accepts the first email anywhere in settings or Keychain metadata, and an unknown selected email passes the helper. It also imports Security unconditionally and bypasses no-UI Keychain handling. I recommend closing this implementation while keeping #3662 open. The fallback design should use a verified active CLI identity field and retain the OAuth result when identity is missing or conflicting.

@steipete steipete closed this Sep 21, 2026
@Chener

Chener commented Sep 21, 2026

Copy link
Copy Markdown
Author

Thank you for the careful review. I agree that this implementation does not establish the required account identity boundary and should remain closed. Let us keep #3662 open for a design based on verified active CLI identity, preserving the OAuth result when identity is missing or conflicting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants