From 772b1e06dd61d4251cab259a2c9319e931fd049b Mon Sep 17 00:00:00 2001 From: icey-zhang Date: Thu, 16 Apr 2026 11:14:57 +0800 Subject: [PATCH] Fix Antigravity probe failures caused by URLSession delegate signature mismatch and port/token handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The URLSessionDelegate TLS challenge methods used `@Sendable` completionHandler but Swift 6.2 SDK requires `@MainActor @Sendable`, causing the delegate to never be called. This made all HTTPS connections to Antigravity's self-signed localhost server fail silently, falling back to the HTTP extension port which uses a different CSRF token and doesn't serve the quota API — resulting in misleading "session expired" (403) or "HTTP 404" errors. Fixes: - Switch delegate methods to async variants to match the current SDK protocol signature - Extract `--extension_server_csrf_token` from process args and use it for HTTP extension port fallback - Rank port probe results (success > httpError > unreachable) so a 403 on the extension port doesn't shadow a working API port Co-Authored-By: Claude Opus 4.6 --- .../Antigravity/AntigravityStatusProbe.swift | 97 +++++++++++++++---- .../AntigravityStatusProbeTests.swift | 30 ++++++ 2 files changed, 109 insertions(+), 18 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift index cec06f366..680b6d785 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift @@ -267,12 +267,14 @@ public struct AntigravityStatusProbe: Sendable { let ports = try await Self.listeningPorts(pid: processInfo.pid, timeout: self.timeout) let connectPort = try await Self.findWorkingPort( ports: ports, + extensionPort: processInfo.extensionPort, csrfToken: processInfo.csrfToken, timeout: self.timeout) let context = RequestContext( httpsPort: connectPort, httpPort: processInfo.extensionPort, csrfToken: processInfo.csrfToken, + extensionCsrfToken: processInfo.extensionCsrfToken, timeout: self.timeout) do { @@ -297,6 +299,7 @@ public struct AntigravityStatusProbe: Sendable { let ports = try await Self.listeningPorts(pid: processInfo.pid, timeout: self.timeout) let connectPort = try await Self.findWorkingPort( ports: ports, + extensionPort: processInfo.extensionPort, csrfToken: processInfo.csrfToken, timeout: self.timeout) let response = try await Self.makeRequest( @@ -307,6 +310,7 @@ public struct AntigravityStatusProbe: Sendable { httpsPort: connectPort, httpPort: processInfo.extensionPort, csrfToken: processInfo.csrfToken, + extensionCsrfToken: processInfo.extensionCsrfToken, timeout: self.timeout)) return try Self.parsePlanInfoSummary(response) } @@ -405,6 +409,7 @@ public struct AntigravityStatusProbe: Sendable { let pid: Int let extensionPort: Int? let csrfToken: String + let extensionCsrfToken: String? let commandLine: String } @@ -428,7 +433,8 @@ public struct AntigravityStatusProbe: Sendable { sawAntigravity = true guard let token = Self.extractFlag("--csrf_token", from: match.command) else { continue } let port = Self.extractPort("--extension_server_port", from: match.command) - return ProcessInfoResult(pid: match.pid, extensionPort: port, csrfToken: token, commandLine: match.command) + let extToken = Self.extractFlag("--extension_server_csrf_token", from: match.command) + return ProcessInfoResult(pid: match.pid, extensionPort: port, csrfToken: token, extensionCsrfToken: extToken, commandLine: match.command) } if sawAntigravity { @@ -507,22 +513,66 @@ public struct AntigravityStatusProbe: Sendable { return ports.sorted() } + enum PortProbeResult { + case success + case httpError + case unreachable + } + private static func findWorkingPort( ports: [Int], + extensionPort: Int?, csrfToken: String, timeout: TimeInterval) async throws -> Int { + var httpErrorPort: Int? for port in ports { - let ok = await Self.testPortConnectivity(port: port, csrfToken: csrfToken, timeout: timeout) - if ok { return port } + let result = await Self.testPortConnectivity(port: port, csrfToken: csrfToken, timeout: timeout) + switch result { + case .success: + return port + case .httpError: + if httpErrorPort == nil { + httpErrorPort = port + } + case .unreachable: + break + } + } + if let httpErrorPort { + self.log.debug("Port probe fell back to HTTP-reachable candidate", metadata: [ + "port": "\(httpErrorPort)", + ]) + return httpErrorPort + } + if let fallback = self.fallbackProbePort(ports: ports, extensionPort: extensionPort) { + self.log.debug("Port probe fell back to best-effort candidate", metadata: [ + "port": "\(fallback)", + ]) + return fallback } throw AntigravityStatusProbeError.portDetectionFailed("no working API port found") } + static func fallbackProbePort(ports: [Int], extensionPort: Int?) -> Int? { + if let nonExtension = ports.first(where: { $0 != extensionPort }) { + return nonExtension + } + if let extensionPort { + return extensionPort + } + return ports.first + } + + static func isReachableProbeError(_ error: Error) -> Bool { + guard case let AntigravityStatusProbeError.apiError(message) = error else { return false } + return message.hasPrefix("HTTP ") + } + private static func testPortConnectivity( port: Int, csrfToken: String, - timeout: TimeInterval) async -> Bool + timeout: TimeInterval) async -> PortProbeResult { do { _ = try await self.makeRequest( @@ -533,14 +583,22 @@ public struct AntigravityStatusProbe: Sendable { httpsPort: port, httpPort: nil, csrfToken: csrfToken, + extensionCsrfToken: nil, timeout: timeout)) - return true + return .success } catch { + if self.isReachableProbeError(error) { + self.log.debug("Port probe received HTTP response; treating port as reachable", metadata: [ + "port": "\(port)", + "error": error.localizedDescription, + ]) + return .httpError + } self.log.debug("Port probe failed", metadata: [ "port": "\(port)", "error": error.localizedDescription, ]) - return false + return .unreachable } } @@ -555,6 +613,7 @@ public struct AntigravityStatusProbe: Sendable { let httpsPort: Int let httpPort: Int? let csrfToken: String + let extensionCsrfToken: String? let timeout: TimeInterval } @@ -598,12 +657,18 @@ public struct AntigravityStatusProbe: Sendable { payload: payload, context: context) } catch { - guard let httpPort = context.httpPort, httpPort != context.httpsPort else { throw error } + guard let httpPort = context.httpPort else { throw error } + let fallbackContext = RequestContext( + httpsPort: httpPort, + httpPort: nil, + csrfToken: context.extensionCsrfToken ?? context.csrfToken, + extensionCsrfToken: nil, + timeout: context.timeout) return try await Self.sendRequest( scheme: "http", port: httpPort, payload: payload, - context: context) + context: fallbackContext) } } @@ -693,11 +758,9 @@ private final class LocalhostSessionDelegate: NSObject { extension LocalhostSessionDelegate: URLSessionDelegate { func urlSession( _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) - { - let result = self.challengeResult(challenge) - completionHandler(result.disposition, result.credential) + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + self.challengeResult(challenge) } } @@ -705,11 +768,9 @@ extension LocalhostSessionDelegate: URLSessionTaskDelegate { func urlSession( _ session: URLSession, task: URLSessionTask, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) - { - let result = self.challengeResult(challenge) - completionHandler(result.disposition, result.credential) + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + self.challengeResult(challenge) } private func challengeResult(_ challenge: URLAuthenticationChallenge) -> ( diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift index 33c1bb7e5..eb5614ac9 100644 --- a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift @@ -322,4 +322,34 @@ struct AntigravityStatusProbeTests { #expect(usage.accountEmail(for: .antigravity) == "test@example.com") #expect(usage.loginMethod(for: .antigravity) == "Pro") } + + @Test + func `http probe errors still count as reachable`() { + #expect( + AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("HTTP 403: Forbidden"))) + #expect( + AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("HTTP 404: Not Found"))) + #expect( + !AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("Invalid response"))) + #expect(!AntigravityStatusProbe.isReachableProbeError(AntigravityStatusProbeError.notRunning)) + } + + @Test + func `fallback probe port prefers non extension candidate`() { + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [51170, 61775], + extensionPort: 61775) == 51170) + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [61775], + extensionPort: 61775) == 61775) + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [51170, 61775], + extensionPort: nil) == 51170) + } }