-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(providers): close the Devin rekey host window and diagnose Antigravity quota blocks #4575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7a01f3f
99cdd9d
7d223ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # 260914 L5 — provider account lifecycle edges (#4503, #3781) | ||
|
|
||
| R1 라운드의 L5 레인. 계정 수명주기 경계에서 생긴 두 건을 한 PR로 닫는다. | ||
| 분기: `codex/260914-l5-provider-account-edges`, 타깃 `dev`. | ||
|
|
||
| ## 다루는 것 | ||
|
|
||
| - **#4503** Devin 프로바이더 병합 마이그레이션이 남긴 host-selection 창. | ||
| config 저장은 동기인데 credential rekey는 detached라, 그 사이(그리고 rekey가 | ||
| 실패하거나 collision으로 거부되면 그 프로세스 내내) EU/FedStart 테넌트가 US | ||
| 기본 호스트로 키를 보낸다. | ||
| - **#4503 부록** 같은 감사에서 함께 기록된 커버리지 공백. Pi-shape 이미지 파트의 | ||
| tool 경로가 합성으로만 덮여 있어, tool 분기 한정 회귀는 잡히지 않는다. | ||
| - **#3781** Antigravity 할당량 갱신 실패. canonical Fake-IP 처리 가설을 실제 | ||
| 소스에서 확인하고, 남은 구멍과 커버리지를 메운다. | ||
|
|
||
| ## 레인 경계 | ||
|
|
||
| 쓰기 가능: `src/oauth/devin.ts`, `src/providers/quota.ts`의 Antigravity 블록, | ||
| 그리고 위 서브시스템의 테스트. | ||
|
|
||
| 쓰면 안 되는 것: account pool 커널, `src/codex/routing.ts`, | ||
| `src/server/responses/*`, `src/codex/catalog/*`, `src/adapters/cursor/*`, `gui/`. | ||
| 같은 라운드의 다른 레인이 별도 워크트리에서 그 경로들을 소유한다. | ||
|
|
||
| ## 검증 방침 | ||
|
|
||
| 로컬 스위트/타입체크/설치는 레인 제약으로 **실행하지 않는다**. `node_modules`도 | ||
| 없다. 증거는 최종 head의 hosted CI 하나뿐이다. 그래서 구현은 타입체커 대신 | ||
| 기존 파일의 import 경로/타입 이름/strict null 처리를 그대로 맞추는 방식으로 간다. | ||
|
|
||
| ## 작업 단위 | ||
|
|
||
| - `010_wp1_account_lifecycle_edges.md` — 단일 work-phase. 여섯 개 서브에이전트에 | ||
| 서로 겹치지 않는 write scope를 배정해 병렬로 구현한다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # 010 — wp1: 계정 수명주기 경계 (#4503, #3781) | ||
|
|
||
| ## MODIFY: `src/oauth/devin.ts` — `resolveDevinApiServer` | ||
|
|
||
| 현재는 `getCredential(providerId)`를 **리터럴 슬롯 키**로 읽는다. 병합 | ||
| 마이그레이션(`runDevinProviderMergeStartupMigration`)은 `providers["devin"]`을 | ||
| 동기로 저장한 뒤 `void rekeyProviderCredentials("devin-cli","devin")`을 detached로 | ||
| 던진다. 그래서 config 행은 이미 `devin`인데 credential은 아직 `devin-cli` 슬롯에 | ||
| 있는 창이 생기고, rekey가 실패하거나 collision으로 거부되면 그 상태가 그 프로세스 | ||
| 동안 계속된다. 그 사이 `getCredential("devin")`은 undefined라 EU/FedStart 테넌트가 | ||
| configured baseUrl 또는 `DEVIN_DEFAULT_API_SERVER`(US)로 떨어진다. | ||
|
|
||
| 계약: | ||
|
|
||
| 1. 요청받은 providerId의 **리터럴 슬롯을 먼저** 본다. 아직 `devin-cli`로 남아 있는 | ||
| config 행은 자기 슬롯을 읽어야 하므로, 앞단에서 id를 정규화하면 오히려 틀린 | ||
| 슬롯을 읽는다. 기존 주석의 그 논거는 유지하고 확장한다. | ||
| 2. 리터럴 슬롯에 쓸 만한 `apiBaseUrl`이 없을 때만 `DEPRECATED_OAUTH_PROVIDER_ALIASES`가 | ||
| 묶어 둔 슬롯을 **양방향**으로 더 본다 (`devin` → `devin-cli`, `devin-cli` → `devin`). | ||
| 두 번째 문자열 리터럴을 박지 않고 alias 맵에서 유도해, 맵이 단일 출처로 남게 한다. | ||
| 3. 후보는 모두 `validateDevinApiBaseUrl`을 통과해야 한다. alias 슬롯을 리터럴보다 | ||
| 더 신뢰하지 않는다. | ||
| 4. 이후 순서는 그대로: configured baseUrl → `DEVIN_DEFAULT_API_SERVER`. | ||
| 5. 시그니처와 기존 호출부는 불변. | ||
|
|
||
| ## MODIFY: `src/providers/quota.ts` — Antigravity 블록만 | ||
|
|
||
| `probeAntigravityUsageQuota`의 summary 프로브는 바인딩도 본문도 없는 빈 catch로 받아서 | ||
| 분류된 진단을 통째로 버린다. summary가 outbound 정책(`destination_blocked`)이나 DNS(`dns_failed`)로 | ||
| 막히는 건 정확히 이 이슈가 말하는 Fake-IP 증상인데, fallback까지 실패하면 사용자에게는 | ||
| 더 두루뭉술한 쪽(`upstream_error`, `response_unusable`)만 보인다. | ||
|
|
||
| summary의 분류 결과를 기억해 두고, fallback도 unavailable로 끝났을 때 summary 쪽이 | ||
| 네트워크 정책 진단이고 fallback 쪽이 아니면 summary 진단을 택한다. 보존 조건: | ||
| fallback이 성공하면 첫 실패는 완전히 지워진다, summary의 즉시 반환 | ||
| (`redirect_blocked`/`access_denied`)은 그대로, `legacy` 채널의 모양과 | ||
| `rejects.toBe(error)` 동일성은 건드리지 않는다, 진단 값은 닫힌 | ||
| `QUOTA_FAILURE_CODES` 밖으로 나가지 않는다. | ||
|
|
||
| ## 회귀 테스트 | ||
|
|
||
| 새 테스트 파일은 만들지 않는다 (test-layout 게이트와 그 fixture를 건드리지 않기 위해). | ||
|
|
||
| - `tests/providers/devin-login.test.ts` — 마이그레이션 창에서 `devin` 요청이 | ||
| `devin-cli` 슬롯의 테넌트 호스트를 읽는지, 리터럴 슬롯 우선순위가 유지되는지, | ||
| 잘못된 alias `apiBaseUrl`이 신뢰받지 않는지. | ||
| - `tests/providers/devin-adapter.test.ts` — 같은 보장을 어댑터가 실제로 디스패치하는 | ||
| 호스트 수준에서. | ||
| - `tests/providers/provider-account-quota.test.ts` — 프록시 없이 Fake-IP DNS 응답이 | ||
| canonical 할당량 URL 두 개에 대해 허용되는지, 예외가 lookalike 호스트/다른 경로/쿼리 | ||
| 추가/다른 프로바이더 이름으로 넓어지지 않는지, 무관한 private·metadata 응답은 여전히 | ||
| 거부되고 안전한 `destination_blocked`로 보고되는지. | ||
| - `tests/responses/chat-completions-endpoint.test.ts` — #4503 부록. 직접 | ||
| `role:"tool"` 봉투에 실린 Pi-shape 이미지 파트 fixture. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2951,7 +2951,26 @@ function unavailableAntigravityQuota(failure: QuotaFailureCode): AntigravityQuot | |
| return { kind: "unavailable", failure, legacy: { kind: "null" } }; | ||
| } | ||
|
|
||
| /** Final attempt determines the safe diagnosis; a successful fallback clears the first failure. */ | ||
| /** | ||
| * Prefer a summary network-policy diagnosis over a vaguer fallback. A blocked | ||
| * destination is an actionable local-network fact, while "upstream_error" tells | ||
| * the operator to go look at Google. A successful models probe still clears | ||
| * the first failure completely. | ||
|
Comment on lines
+2954
to
+2958
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This changes shared OAuth and provider transport behavior without changing any AGENTS.md reference: src/AGENTS.md:L11-L11 Useful? React with 👍 / 👎. |
||
| */ | ||
| function antigravityUnavailableFailure( | ||
| summaryFailure: QuotaFailureCode | undefined, | ||
| fallbackFailure: QuotaFailureCode, | ||
| ): QuotaFailureCode { | ||
| if ( | ||
| (summaryFailure === "destination_blocked" || summaryFailure === "dns_failed") | ||
| && fallbackFailure !== "destination_blocked" | ||
| && fallbackFailure !== "dns_failed" | ||
| ) { | ||
| return summaryFailure; | ||
|
Comment on lines
+2964
to
+2969
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the summary lookup has a transient DNS/policy failure but the models fallback subsequently connects and returns a precise AGENTS.md reference: src/AGENTS.md:L11-L11 Useful? React with 👍 / 👎. |
||
| } | ||
| return fallbackFailure; | ||
| } | ||
|
|
||
| async function probeAntigravityUsageQuota(accessToken: string, projectId: string): Promise<AntigravityQuotaProbeResult> { | ||
| const fetchQuota = (url: string) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, { | ||
| headers: { | ||
|
|
@@ -2960,6 +2979,7 @@ async function probeAntigravityUsageQuota(accessToken: string, projectId: string | |
| }, | ||
| body: JSON.stringify({ project: projectId }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | ||
| }, antigravityOutboundDependencies); | ||
| let summaryFailure: QuotaFailureCode | undefined; | ||
| try { | ||
| const response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL); | ||
| if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_SUMMARY_URL)) return unavailableAntigravityQuota("redirect_blocked"); | ||
|
|
@@ -2968,19 +2988,30 @@ async function probeAntigravityUsageQuota(accessToken: string, projectId: string | |
| const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(response))); | ||
| if (quota) return { kind: "available", quota, source: "google-antigravity:retrieveUserQuotaSummary" }; | ||
| } | ||
| } catch { | ||
| } catch (error) { | ||
| // Existing behavior: summary transport/parse failure may recover through the models probe. | ||
| summaryFailure = quotaTransportFailure(error); | ||
| } | ||
| try { | ||
| const response = await fetchQuota(ANTIGRAVITY_QUOTA_MODELS_URL); | ||
| if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_MODELS_URL)) return unavailableAntigravityQuota("redirect_blocked"); | ||
| if (!response.ok) return unavailableAntigravityQuota(quotaHttpFailure(response.status)); | ||
| if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_MODELS_URL)) { | ||
| return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "redirect_blocked")); | ||
| } | ||
| if (!response.ok) { | ||
| return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, quotaHttpFailure(response.status))); | ||
| } | ||
| const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response))); | ||
| if (!customWindows.length) return unavailableAntigravityQuota("response_unusable"); | ||
| if (!customWindows.length) { | ||
| return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "response_unusable")); | ||
| } | ||
| return { kind: "available", quota: { customWindows, updatedAt: Date.now() }, source: "google-antigravity:fetchAvailableModels" }; | ||
| } catch (error) { | ||
| // The public compatibility wrapper still rejects this exact fallback error; it never enters a DTO. | ||
| return { kind: "unavailable", failure: quotaTransportFailure(error), legacy: { kind: "throw", error } }; | ||
| return { | ||
| kind: "unavailable", | ||
| failure: antigravityUnavailableFailure(summaryFailure, quotaTransportFailure(error)), | ||
| legacy: { kind: "throw", error }, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50376
🤖 get_repo_knowledge executed:
get_repo_knowledge lidge-jun/opencodex /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/conventions /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/learningsLength of output: 9475
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50375
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 19722
Define precedence for specific fallback failures.
The rule selects the summary diagnosis whenever the fallback is not a network-policy failure. This also matches any other fallback classification. If the fallback can return
redirect_blockedoraccess_denied, the rule could replace a more-specific error withdestination_blockedordns_failed. Line 36 only preserves summary immediate returns; it does not define fallback precedence.Narrow the override to generic fallback codes such as
upstream_errorandresponse_unusable, or add an explicit precedence table and tests for every classified fallback code.🤖 Prompt for AI Agents