From d450c4f96667283fb05df6d752b1bf565b182f98 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 12 Dec 2025 23:24:20 +0900 Subject: [PATCH] fix(antigravity-auth): address Oracle feedback - custom credentials, logging, docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix custom credentials to actually work in OAuth/refresh flows - oauth.ts: Add clientId/clientSecret parameters to buildAuthURL(), exchangeCode() - token.ts: Add clientId/clientSecret parameters to refreshAccessToken() - fetch.ts: Pass credentials to oauth/token functions - plugin.ts: Use closure cache for credentials, pass to all flows - Unify console.* logging policy with ANTIGRAVITY_DEBUG guards - constants.ts: Document logging policy - tools.ts: Guard console.warn with ANTIGRAVITY_DEBUG - plugin.ts: Guard 4 console.error with ANTIGRAVITY_DEBUG - Add explicit init.body type handling - fetch.ts: Check body type, pass-through non-string bodies - fetch.ts: Document body type assumption - Document SSE buffering behavior - response.ts: Add warning that current implementation buffers - response.ts: Add TODO for future ReadableStream enhancement πŸ€– GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- ai-todolist.md | 2 +- src/auth/antigravity/constants.ts | 14 +++++++ src/auth/antigravity/fetch.ts | 64 +++++++++++++++++-------------- src/auth/antigravity/oauth.ts | 26 +++++++++---- src/auth/antigravity/plugin.ts | 61 ++++++++++++++++------------- src/auth/antigravity/response.ts | 27 +++++++++---- src/auth/antigravity/token.ts | 10 +++-- src/auth/antigravity/tools.ts | 12 +++--- 8 files changed, 137 insertions(+), 79 deletions(-) diff --git a/ai-todolist.md b/ai-todolist.md index 82fbf41..2e2f1ea 100644 --- a/ai-todolist.md +++ b/ai-todolist.md @@ -8,7 +8,7 @@ ## ν˜„μž¬ μ§„ν–‰ 쀑인 μž‘μ—… -**λͺ¨λ“  μž‘μ—… μ™„λ£Œ** - βœ… Phase 1-4 μ™„λ£Œ (14 tasks) +**λͺ¨λ“  μž‘μ—… μ™„λ£Œ** - βœ… Phase 1-4 μ™„λ£Œ (14 tasks) + Oracle λ¬Έμ„œν™” 이슈 μˆ˜μ • --- diff --git a/src/auth/antigravity/constants.ts b/src/auth/antigravity/constants.ts index af9410f..6961549 100644 --- a/src/auth/antigravity/constants.ts +++ b/src/auth/antigravity/constants.ts @@ -1,6 +1,20 @@ /** * Antigravity OAuth configuration constants. * Values sourced from cliproxyapi/sdk/auth/antigravity.go + * + * ## Logging Policy + * + * All console logging in antigravity modules follows a consistent policy: + * + * - **Debug logs**: Guard with `if (process.env.ANTIGRAVITY_DEBUG === "1")` + * - Includes: info messages, warnings, non-fatal errors + * - Enable debugging: `ANTIGRAVITY_DEBUG=1 opencode` + * + * - **Fatal errors**: None currently. All errors are handled by returning + * appropriate error responses to OpenCode's auth system. + * + * This policy ensures production silence while enabling verbose debugging + * when needed for troubleshooting OAuth flows. */ // OAuth 2.0 Client Credentials diff --git a/src/auth/antigravity/fetch.ts b/src/auth/antigravity/fetch.ts index c810895..6a12298 100644 --- a/src/auth/antigravity/fetch.ts +++ b/src/auth/antigravity/fetch.ts @@ -8,6 +8,12 @@ * - Applies response transformation (including thinking extraction) * - Implements endpoint fallback (daily β†’ autopush β†’ prod) * + * **Body Type Assumption:** + * This interceptor assumes `init.body` is a JSON string (OpenAI format). + * Non-string bodies (ReadableStream, Blob, FormData, URLSearchParams, etc.) + * are passed through unchanged to the original fetch to avoid breaking + * other requests that may not be OpenAI-format API calls. + * * Debug logging available via ANTIGRAVITY_DEBUG=1 environment variable. */ @@ -59,9 +65,6 @@ function isRetryableError(status: number): boolean { return false } -/** - * Attempt fetch with a single endpoint - */ async function attemptFetch( endpoint: string, url: string, @@ -69,43 +72,42 @@ async function attemptFetch( accessToken: string, projectId: string, modelName?: string -): Promise { +): Promise { debugLog(`Trying endpoint: ${endpoint}`) try { - // Parse request body if present - let body: Record = {} - if (init.body) { + const rawBody = init.body + + if (rawBody !== undefined && typeof rawBody !== "string") { + debugLog(`Non-string body detected (${typeof rawBody}), signaling pass-through`) + return "pass-through" + } + + let parsedBody: Record = {} + if (rawBody) { try { - body = - typeof init.body === "string" - ? (JSON.parse(init.body) as Record) - : (init.body as unknown as Record) + parsedBody = JSON.parse(rawBody) as Record } catch { - // If body parsing fails, use empty object - body = {} + parsedBody = {} } } - // Apply tool normalization if tools present - if (body.tools && Array.isArray(body.tools)) { - const normalizedTools = normalizeToolsForGemini(body.tools as OpenAITool[]) + if (parsedBody.tools && Array.isArray(parsedBody.tools)) { + const normalizedTools = normalizeToolsForGemini(parsedBody.tools as OpenAITool[]) if (normalizedTools) { - body.tools = normalizedTools + parsedBody.tools = normalizedTools } } - // Transform request const transformed = transformRequest( url, - body, + parsedBody, accessToken, projectId, modelName, endpoint ) - // Make the request const response = await fetch(transformed.url, { method: init.method || "POST", headers: transformed.headers, @@ -113,7 +115,6 @@ async function attemptFetch( signal: init.signal, }) - // Check for retryable errors if (!response.ok && isRetryableError(response.status)) { debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`) return null @@ -121,7 +122,6 @@ async function attemptFetch( return response } catch (error) { - // Network error - try next endpoint debugLog( `Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next` ) @@ -179,6 +179,8 @@ async function transformResponseWithThinking( * @param getAuth - Async function to retrieve current auth state * @param client - Auth client for saving updated tokens * @param providerId - Provider identifier (e.g., "google") + * @param clientId - Optional custom client ID for token refresh (defaults to ANTIGRAVITY_CLIENT_ID) + * @param clientSecret - Optional custom client secret for token refresh (defaults to ANTIGRAVITY_CLIENT_SECRET) * @returns Custom fetch function compatible with standard fetch signature * * @example @@ -186,7 +188,9 @@ async function transformResponseWithThinking( * const customFetch = createAntigravityFetch( * () => auth(), * client, - * "google" + * "google", + * "custom-client-id", + * "custom-client-secret" * ) * * // Use like standard fetch @@ -199,7 +203,9 @@ async function transformResponseWithThinking( export function createAntigravityFetch( getAuth: () => Promise, client: AuthClient, - providerId: string + providerId: string, + clientId?: string, + clientSecret?: string ): (url: string, init?: RequestInit) => Promise { // Cache for current token state let cachedTokens: AntigravityTokens | null = null @@ -237,7 +243,7 @@ export function createAntigravityFetch( debugLog("Token expired, refreshing...") try { - const newTokens = await refreshAccessToken(refreshParts.refreshToken) + const newTokens = await refreshAccessToken(refreshParts.refreshToken, clientId, clientSecret) // Update cached tokens cachedTokens = { @@ -297,7 +303,6 @@ export function createAntigravityFetch( } } - // Try each endpoint in fallback order const maxEndpoints = Math.min(ANTIGRAVITY_ENDPOINT_FALLBACKS.length, 3) for (let i = 0; i < maxEndpoints; i++) { @@ -312,10 +317,13 @@ export function createAntigravityFetch( modelName ) + if (response === "pass-through") { + debugLog("Non-string body detected, passing through to original fetch") + return fetch(url, init) + } + if (response) { debugLog(`Success with endpoint: ${endpoint}`) - - // Transform response (with thinking extraction if applicable) return transformResponseWithThinking(response, modelName || "") } } diff --git a/src/auth/antigravity/oauth.ts b/src/auth/antigravity/oauth.ts index 913a49d..74b542e 100644 --- a/src/auth/antigravity/oauth.ts +++ b/src/auth/antigravity/oauth.ts @@ -121,10 +121,12 @@ export function decodeState(encoded: string): OAuthState { * Build the OAuth authorization URL with PKCE. * * @param projectId - Optional GCP project ID to include in state + * @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID) * @returns Authorization result with URL and verifier */ export async function buildAuthURL( - projectId?: string + projectId?: string, + clientId: string = ANTIGRAVITY_CLIENT_ID ): Promise { const pkce = await generatePKCEPair() @@ -134,7 +136,7 @@ export async function buildAuthURL( } const url = new URL(GOOGLE_AUTH_URL) - url.searchParams.set("client_id", ANTIGRAVITY_CLIENT_ID) + url.searchParams.set("client_id", clientId) url.searchParams.set("redirect_uri", ANTIGRAVITY_REDIRECT_URI) url.searchParams.set("response_type", "code") url.searchParams.set("scope", ANTIGRAVITY_SCOPES.join(" ")) @@ -155,15 +157,19 @@ export async function buildAuthURL( * * @param code - Authorization code from OAuth callback * @param verifier - PKCE verifier from initial auth request + * @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID) + * @param clientSecret - Optional custom client secret (defaults to ANTIGRAVITY_CLIENT_SECRET) * @returns Token exchange result with access and refresh tokens */ export async function exchangeCode( code: string, - verifier: string + verifier: string, + clientId: string = ANTIGRAVITY_CLIENT_ID, + clientSecret: string = ANTIGRAVITY_CLIENT_SECRET ): Promise { const params = new URLSearchParams({ - client_id: ANTIGRAVITY_CLIENT_ID, - client_secret: ANTIGRAVITY_CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, code, grant_type: "authorization_code", redirect_uri: ANTIGRAVITY_REDIRECT_URI, @@ -317,18 +323,22 @@ export function startCallbackServer( * * @param projectId - Optional GCP project ID * @param openBrowser - Function to open URL in browser + * @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID) + * @param clientSecret - Optional custom client secret (defaults to ANTIGRAVITY_CLIENT_SECRET) * @returns Object with tokens and user info */ export async function performOAuthFlow( projectId?: string, - openBrowser?: (url: string) => Promise + openBrowser?: (url: string) => Promise, + clientId: string = ANTIGRAVITY_CLIENT_ID, + clientSecret: string = ANTIGRAVITY_CLIENT_SECRET ): Promise<{ tokens: AntigravityTokenExchangeResult userInfo: AntigravityUserInfo verifier: string }> { // Build auth URL first to get the verifier - const auth = await buildAuthURL(projectId) + const auth = await buildAuthURL(projectId, clientId) // Start callback server const callbackPromise = startCallbackServer() @@ -356,7 +366,7 @@ export async function performOAuthFlow( } // Exchange code for tokens - const tokens = await exchangeCode(callback.code, auth.verifier) + const tokens = await exchangeCode(callback.code, auth.verifier, clientId, clientSecret) // Fetch user info const userInfo = await fetchUserInfo(tokens.access_token) diff --git a/src/auth/antigravity/plugin.ts b/src/auth/antigravity/plugin.ts index 9863d95..c51012b 100644 --- a/src/auth/antigravity/plugin.ts +++ b/src/auth/antigravity/plugin.ts @@ -74,6 +74,12 @@ function isOAuthAuth( export async function createGoogleAntigravityAuthPlugin({ client, }: PluginInput): Promise<{ auth: AuthHook }> { + // Cache for custom credentials from provider.options + // These are populated by loader() and used by authorize() + // Falls back to defaults if loader hasn't been called yet + let cachedClientId: string = ANTIGRAVITY_CLIENT_ID + let cachedClientSecret: string = ANTIGRAVITY_CLIENT_SECRET + const authHook: AuthHook = { /** * Provider identifier - must be "google" as Antigravity is @@ -100,18 +106,16 @@ export async function createGoogleAntigravityAuthPlugin({ return {} } - // Read credentials from provider.options (opencode.json) - // Fall back to default credentials if not configured - const clientId = + cachedClientId = (provider.options?.clientId as string) || ANTIGRAVITY_CLIENT_ID - const clientSecret = + cachedClientSecret = (provider.options?.clientSecret as string) || ANTIGRAVITY_CLIENT_SECRET // Log if using custom credentials (for debugging) if ( process.env.ANTIGRAVITY_DEBUG === "1" && - (clientId !== ANTIGRAVITY_CLIENT_ID || - clientSecret !== ANTIGRAVITY_CLIENT_SECRET) + (cachedClientId !== ANTIGRAVITY_CLIENT_ID || + cachedClientSecret !== ANTIGRAVITY_CLIENT_SECRET) ) { console.log( "[antigravity-plugin] Using custom credentials from provider.options" @@ -153,13 +157,12 @@ export async function createGoogleAntigravityAuthPlugin({ return {} } - // Create the Antigravity fetch interceptor - // Note: The fetch interceptor uses constants for token refresh internally - // Custom credentials in provider.options are for future extensibility const antigravityFetch = createAntigravityFetch( getAuth, authClient, - GOOGLE_PROVIDER_ID + GOOGLE_PROVIDER_ID, + cachedClientId, + cachedClientSecret ) return { @@ -185,8 +188,7 @@ export async function createGoogleAntigravityAuthPlugin({ * @returns Authorization result with URL and callback */ authorize: async (): Promise => { - // Build OAuth URL with PKCE - const { url, verifier } = await buildAuthURL() + const { url, verifier } = await buildAuthURL(undefined, cachedClientId) // Start local callback server to receive OAuth callback const callbackPromise = startCallbackServer() @@ -208,28 +210,33 @@ export async function createGoogleAntigravityAuthPlugin({ // Check for errors if (result.error) { - console.error( - `[antigravity-plugin] OAuth error: ${result.error}` - ) + if (process.env.ANTIGRAVITY_DEBUG === "1") { + console.error( + `[antigravity-plugin] OAuth error: ${result.error}` + ) + } return { type: "failed" as const } } if (!result.code) { - console.error( - "[antigravity-plugin] No authorization code received" - ) + if (process.env.ANTIGRAVITY_DEBUG === "1") { + console.error( + "[antigravity-plugin] No authorization code received" + ) + } return { type: "failed" as const } } // Verify state and extract original verifier const state = decodeState(result.state) if (state.verifier !== verifier) { - console.error("[antigravity-plugin] PKCE verifier mismatch") + if (process.env.ANTIGRAVITY_DEBUG === "1") { + console.error("[antigravity-plugin] PKCE verifier mismatch") + } return { type: "failed" as const } } - // Exchange authorization code for tokens - const tokens = await exchangeCode(result.code, verifier) + const tokens = await exchangeCode(result.code, verifier, cachedClientId, cachedClientSecret) // Fetch user info (optional, for logging) try { @@ -263,11 +270,13 @@ export async function createGoogleAntigravityAuthPlugin({ expires: Date.now() + tokens.expires_in * 1000, } } catch (error) { - console.error( - `[antigravity-plugin] OAuth flow failed: ${ - error instanceof Error ? error.message : "Unknown error" - }` - ) + if (process.env.ANTIGRAVITY_DEBUG === "1") { + console.error( + `[antigravity-plugin] OAuth flow failed: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ) + } return { type: "failed" as const } } }, diff --git a/src/auth/antigravity/response.ts b/src/auth/antigravity/response.ts index 53f702d..60aedeb 100644 --- a/src/auth/antigravity/response.ts +++ b/src/auth/antigravity/response.ts @@ -4,7 +4,7 @@ * * Key responsibilities: * - Non-streaming response transformation - * - SSE streaming response transformation (preserving stream) + * - SSE streaming response transformation (buffered - see transformStreamingResponse) * - Error response handling with retry-after extraction * - Usage metadata extraction from x-antigravity-* headers */ @@ -340,19 +340,29 @@ export function transformStreamingPayload(payload: string): string { } /** - * Transform a streaming SSE response + * Transforms a streaming SSE response from Antigravity to OpenAI format. * - * For streaming responses: - * - Preserves the SSE format for downstream consumers + * **⚠️ CURRENT IMPLEMENTATION: BUFFERING** + * This implementation reads the entire stream into memory before transforming. + * While functional, it does not preserve true streaming characteristics: + * - Blocks until entire response is received + * - Consumes memory proportional to response size + * - Increases Time-To-First-Byte (TTFB) + * + * **TODO: Future Enhancement** + * Implement true streaming using ReadableStream transformation: + * - Parse SSE chunks incrementally + * - Transform and yield chunks as they arrive + * - Reduce memory footprint and TTFB + * + * For streaming responses (current buffered approach): * - Unwraps the `response` field from each SSE event + * - Returns transformed SSE text as new Response * - Extracts usage metadata from headers * - * Note: This reads the entire stream and returns a new Response. - * The stream is preserved as SSE text, not blocked. - * * Note: Does NOT handle thinking block extraction (Task 10) * - * @param response - Fetch Response object with SSE body + * @param response - The SSE response from Antigravity API * @returns TransformResult with transformed response and metadata */ export async function transformStreamingResponse(response: Response): Promise { @@ -425,6 +435,7 @@ export async function transformStreamingResponse(response: Response): Promise { const params = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: ANTIGRAVITY_CLIENT_ID, - client_secret: ANTIGRAVITY_CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, }) const response = await fetch(GOOGLE_TOKEN_URL, { diff --git a/src/auth/antigravity/tools.ts b/src/auth/antigravity/tools.ts index 4470433..f70c91f 100644 --- a/src/auth/antigravity/tools.ts +++ b/src/auth/antigravity/tools.ts @@ -117,11 +117,13 @@ export function normalizeToolsForGemini( functionDeclarations.push(declaration) } else { - // Log warning for unsupported tool types - console.warn( - `[antigravity-tools] Unsupported tool type: "${tool.type}". ` + - `Only "function" type tools are supported for Gemini. Tool will be skipped.` - ) + // Log warning for unsupported tool types (debug only) + if (process.env.ANTIGRAVITY_DEBUG === "1") { + console.warn( + `[antigravity-tools] Unsupported tool type: "${tool.type}". ` + + `Only "function" type tools are supported for Gemini. Tool will be skipped.` + ) + } } }