Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions devlog/_plan/260912_accounts/070_tun.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ MODIFY GUI `components/provider-workspace/types.ts`, `hooks/useProviderAccountPo
Field chain: private probe→transient cache→account results→authenticated API JSON→enum-normalized client/CLI→current/all-account quota text. Ranking/health/history do not consume it. Tests: each enum trigger, summary failure/fallback success, final-attempt precedence, stale bars, recovery, cross-account isolation, stale-config, late response, unknown wire code, and secret-free projection. Existing provider account quota fixtures supply transport injection; new files require both layout entries. Fix inventory's stale IPv6 proxy-only sentence and update every touched area owner. Local suites/build NOT RUN; hosted backend/GUI checks and rendered final-tip artifact. Authenticated TUN observation remains unmet until an authorized operator supplies exact SHA, proxy/TUN mode and sanitized successful refresh; no network/account changes here.

Reflection TUN-R01/R02 accepted. HTTP 300–399→redirect_blocked, 401/403→access_denied, 429→rate_limited, other non-2xx→upstream_error; success with unusable quota→response_unusable. Keep providerRedirectError cancellation and discard its message. Neither status establishes plan or reauth. fetchAntigravityQuota may reuse the private probe preserving null/rejection and success source; ProviderQuota/ProviderQuotaReport gain no diagnostic field, report-only views remain generic. getCachedProviderAccountQuota returns last-good quota only.

P revalidation on489af939: parent added explicit account readers and Combo quota evidence. Classification stays limited to Antigravity account probe/cache; provider report remains its existing report(...) projection with no inference authority or diagnosticfield. Current functions moved but contract unchanged. Active tun cursor honored after history A mismatch; history source implementation remains pending on its own branch. Local tests/build/typecheck/install still NOT RUN; authenticated field acceptance untouched.

A1 credential-currentness accepted. Reuse the existing private explicitQuotaIdentity hash recipe via a pure quotaCredentialIdentity(provider,id,capturedCredential,target) helper; existing explicit readers keep byte-identical hashes. Antigravity diagnostic capture uses its fixed canonical target and the credential whose access token matches the resolved probe token, captured before I/O. Preparation failure may use a pre-resolution identity only if it still matches. Record an epoch-bound private isQuotaFailureCurrent callback; recheck before cache publication, cached reads, result assembly and API projection. A changed/missing/unreadable identity omits the diagnostic only, preserving current last-good quota/unavailable semantics. No private digest or callback serialized. Add optional nonenumerable quotaFailureIsCurrent to the internal ProviderAccountQuota result instead of using isCurrent, whose existing API branch invalidates the entire quota row. Cache/result quotaFailure appears only while unavailable and current. Tests reauth during delayed probe and cached failure after same-id replacement; stale diagnosis is omitted, not attached to replacement credentials.

TUN-D source proposal labels refer to the read-only Faraday design outputs recorded in this task. The executable source of truth is this document's full type/flow contract, not an absent external file. No inference-wide authority is added.
9 changes: 9 additions & 0 deletions devlog/_plan/260912_accounts/071_tun_delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Safe Antigravity quota diagnostics

The existing canonical transport stays intact. A private probe result preserves exported null/rejection compatibility and classifies the final attempted endpoint. Account failures carry only an allowlisted code, bound to the probed credential/project and invalidated independently from last-good bars. API, CLI and both dashboard quota views consume it; unknown values and local management failures stay generic. All nine locale dictionaries updated.

Regression sources exercise status/typed transport categories, fallback recovery, old bars, same-id replacement during and after a probe, private-value serialization, API projection, GUI normalization/recovery and CLI code filtering. Local suites/build/typecheck/install: NOT RUN. Text whitespace checks only. Backend source reviewer Rawls found no material scoped defect; full consumer/security review and final hosted CI/render proof remain pending. Authenticated TUN field acceptance remains open for #3781; no live credential or network configuration was changed.

Resume binding verified: session01a093dc-e20d-79a1-8d46-231ced9ee05b, currentaccounts-tun branch489af939 baseline, actual FSMB. Host goal is blocked and has not been edited or described as active. Existing successful callback CI34673984380/fa4226a9ba is retained and not rerun. Other original PRs have changed heads and require current evidence; no merged PR is recreated.

Full source review found a diagnostic-specific merge regression gap. Added initial-roster-failure clearing, roster-only same-mode retention/mode-change clearing, and delayed failure/recovery after newer selection with surviving membership. No product behavior changed in this correction. Local suites NOT RUN.
7 changes: 7 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1038,3 +1038,10 @@ no quota bars rather than a fabricated one, and windows the plan does not report
absent instead of rendering as 0%.

A provider using a non-canonical `baseUrl` is never sent the key for this probe.


### Diagnosing an Antigravity quota refresh

The account quota view and `ocx account list google-antigravity --quota --refresh` distinguish access denial, rate limiting, blocked destinations or redirects, DNS/connection/timeouts, and unusable quota data. Last-known bars remain visible with their observation time when a refresh fails. Reauthentication retires diagnoses from the previous credential; a successful refresh clears the failure.

An access-denied result does not by itself prove an expired login or an ineligible plan. A blocked destination is a network-policy decision, not proof of a Fake-IP defect. Canonical Google quota destinations retain TLS verification and redirect/private-address restrictions. Authenticated TUN behavior must be checked in the affected environment; injected transport fixtures alone do not establish that field result.
7 changes: 7 additions & 0 deletions docs-site/src/content/docs/ko/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,3 +446,10 @@ opencodex를 로컬 OpenAI 호환 서버로 향하게 하세요 — 보통은
**Custom**을 선택하거나 `ocx init`에서 `custom`을 선택한 뒤 베이스 URL을 입력하세요. 모든 프로바이더 필드
(`headers`, `noReasoningModels`, `noVisionModels`, `models`, …)는
[설정 레퍼런스](/ko/reference/configuration/)를 참고하세요.


### Antigravity 쿼터 조회 실패 확인

계정 쿼터 화면과 `ocx account list google-antigravity --quota --refresh`는 접근 거부, 요청 한도, 목적지·리디렉션 차단, DNS·연결·시간 초과, 읽을 수 없는 응답을 구분합니다. 조회가 실패해도 마지막 관측 막대와 시각은 유지합니다. 재로그인하면 이전 자격 증명의 진단을 버리고, 조회에 성공하면 오류 표시를 지웁니다.

접근 거부만으로 로그인 만료나 플랜 사용 불가를 단정하지 않습니다. 목적지 차단도 Fake-IP 결함의 증거는 아닙니다. Google의 고정 쿼터 주소에는 TLS 인증서 확인과 리디렉션·사설 주소 제한이 유지됩니다. 인증된 TUN 환경의 동작은 해당 환경에서 별도로 확인해야 합니다.
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { parseQuotaFailureCode } from "../../../../src/providers/quota-types";
import { useT } from "../../i18n/shared";
import { accountQuotaFromReport } from "../../provider-workspace/report";
import { formatRelativeTime, relativeTimeLabelsFromT } from "../../provider-workspace/usage";
import { ProviderCapacityQuota } from "./ProviderCapacityQuota";
import type { AccountQuotaReading } from "./types";

/** The same reading states and credit/window renderer for current and all-account views. */
export default function ProviderAccountQuota({ quota: rawQuota, quotaMode, quotaUnavailable, quotaPending }: AccountQuotaReading) {
export default function ProviderAccountQuota({ quota: rawQuota, quotaMode, quotaUnavailable, quotaPending, quotaFailure }: AccountQuotaReading) {
const t = useT();
const quota = accountQuotaFromReport({ quota: rawQuota });
if (quotaMode === "unsupported") {
return <p className="muted" data-quota-state="unsupported">{t("pws.quotaUnsupported")}</p>;
}
const failure = quotaMode === "probe" && quotaUnavailable ? parseQuotaFailureCode(quotaFailure) : undefined;
const pending = quotaMode === "probe" && quotaPending === true;
const state = quotaUnavailable ? "unavailable" : pending ? "pending" : quota ? "ready" : quotaMode === "passive" ? "unobserved" : "unknown";
return <div data-quota-state={state}>
{quotaUnavailable && <p className="muted pwi-auth-acct-quota-stale">{t("pws.accountQuotaUnavailable")}</p>}
{quotaUnavailable && <p className="muted pwi-auth-acct-quota-stale">{t(failure ? `pws.quotaFailure.${failure}` : "pws.accountQuotaUnavailable")}</p>}
{quota || pending ? (
<ProviderCapacityQuota
report={{ quota, updatedAt: quota?.updatedAt, ...(quotaMode === "passive" ? { observed: true } : {}) }}
Expand Down
4 changes: 2 additions & 2 deletions gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ export default function ProviderAuthPanel({
</div>
<div className="pwi-auth-acct-quota">
<ProviderAccountQuota quotaMode={account.quotaMode} quota={account.quota}
quotaUnavailable={account.quotaUnavailable} quotaPending={account.quotaPending} />
quotaUnavailable={account.quotaUnavailable} quotaPending={account.quotaPending} quotaFailure={account.quotaFailure} />
</div>
</li>
);
Expand Down Expand Up @@ -657,7 +657,7 @@ export default function ProviderAuthPanel({
</div>
<div className="pwi-auth-acct-quota">
<ProviderAccountQuota quotaMode={entry.quotaMode} quota={entry.quota}
quotaUnavailable={entry.quotaUnavailable} quotaPending={entry.quotaPending} />
quotaUnavailable={entry.quotaUnavailable} quotaPending={entry.quotaPending} quotaFailure={entry.quotaFailure} />
</div>
</li>
))}
Expand Down
2 changes: 2 additions & 0 deletions gui/src/components/provider-workspace/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { QuotaFailureCode } from "../../../../src/providers/quota-types";
/**
* provider-workspace/types.ts — shared view-model types for the Providers
* workspace shell/rail/detail (WP080a). Data shapes only; no React.
Expand Down Expand Up @@ -46,6 +47,7 @@ export interface AccountQuotaReading {
quotaMode?: AccountQuotaMode;
quota?: AccountQuota | null;
quotaUnavailable?: boolean;
quotaFailure?: QuotaFailureCode;
/** Client-owned enrichment state, never inferred from missing quota data. */
quotaPending?: boolean;
}
Expand Down
13 changes: 9 additions & 4 deletions gui/src/hooks/useProviderAccountPools.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseQuotaFailureCode } from "../../../src/providers/quota-types";
import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
import type { AccountLoadState, AccountQuotaReading } from "../components/provider-workspace/types";
import { createBoundedFetch } from "../bounded-fetch";
Expand Down Expand Up @@ -36,7 +37,9 @@ function mergeRosterRows<T extends QuotaRow>(rows: T[], previous: T[]): T[] {
return mergeQuotaRows(rows, previous, false).map(row => supportsQuotaRead(row) ? {
...row,
quotaPending: prior.get(row.id)?.quotaPending ?? false,
quotaUnavailable: prior.get(row.id)?.quotaUnavailable ?? false,
quotaUnavailable: prior.get(row.id)?.quotaMode === row.quotaMode ? prior.get(row.id)?.quotaUnavailable ?? false : false,
quotaFailure: row.quotaMode === "probe" && prior.get(row.id)?.quotaMode === row.quotaMode && prior.get(row.id)?.quotaUnavailable
? parseQuotaFailureCode(prior.get(row.id)?.quotaFailure) : undefined,
} : row);
}

Expand All @@ -47,7 +50,7 @@ function mergeLateQuotaRows<T extends QuotaRow>(rows: T[], enriched: T[]): T[] {
const incoming = byId.get(row.id);
if (!incoming || incoming.quotaMode !== row.quotaMode) return row;
const quota = mergeQuotaRows([incoming], [row], true)[0];
return { ...row, quota: quota.quota, quotaPending: quota.quotaPending, quotaUnavailable: quota.quotaUnavailable };
return { ...row, quota: quota.quota, quotaPending: quota.quotaPending, quotaUnavailable: quota.quotaUnavailable, quotaFailure: quota.quotaFailure };
});
}

Expand All @@ -60,7 +63,7 @@ function mergeQuotaRows<T extends QuotaRow>(rows: T[], previous: T[], enriched:
const supported = supportsQuotaRead(row);
// Legacy/unknown mode must not acquire synthetic flags that would override
// a provider report or imply that a quota probe is supported.
if (!supported && row.quotaMode !== "unsupported") return { ...row, quotaMode: undefined, quotaPending: undefined };
if (!supported && row.quotaMode !== "unsupported") return { ...row, quotaMode: undefined, quotaPending: undefined, quotaFailure: undefined };
// Only surviving credential IDs can retain omitted data. Explicit null is an
// authoritative invalidation, including failed/expired credential readings.
const retain = supported && (!enriched || row.quotaUnavailable === true);
Expand All @@ -69,14 +72,16 @@ function mergeQuotaRows<T extends QuotaRow>(rows: T[], previous: T[], enriched:
quota: row.quotaMode === "unsupported" ? null : row.quota !== undefined ? row.quota : retain ? prior.get(row.id)?.quota : undefined,
quotaPending: !enriched && row.quotaMode === "probe",
quotaUnavailable: enriched ? row.quotaUnavailable === true : false,
quotaFailure: enriched && row.quotaMode === "probe" && row.quotaUnavailable === true
? parseQuotaFailureCode(row.quotaFailure) : undefined,
};
});
}

function unavailableQuotaRows<T extends QuotaRow>(rows: T[], attempted?: T[]): T[] {
const attemptedModes = attempted && new Map(attempted.map(row => [row.id, row.quotaMode]));
return rows.map(row => supportsQuotaRead(row) && (!attemptedModes || attemptedModes.get(row.id) === row.quotaMode)
? { ...row, quotaUnavailable: true, quotaPending: false }
? { ...row, quotaUnavailable: true, quotaPending: false, quotaFailure: undefined }
: row);
}

Expand Down
10 changes: 10 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2072,6 +2072,16 @@ export const de: Record<TKey, string> = {
"pws.rateLimits": "Limits",
"pws.quotaUnavailable": "Keine Kontingentdaten für diesen Provider.",
"pws.accountQuotaUnavailable": "Ratenlimit-Daten vorübergehend nicht verfügbar; falls vorhanden, werden zuletzt bekannte Werte angezeigt.",
"pws.quotaFailure.account_unavailable": "Kontodaten für die Kontingentprüfung sind nicht verfügbar.",
"pws.quotaFailure.access_denied": "Der Anbieter verweigert den Zugriff auf Kontingentdaten.",
"pws.quotaFailure.rate_limited": "Die Kontingentprüfung wurde durch ein Anfragelimit begrenzt.",
"pws.quotaFailure.upstream_error": "Der Anbieter konnte die Kontingentprüfung nicht abschließen.",
"pws.quotaFailure.redirect_blocked": "Eine Weiterleitung des Kontingentendpunkts wurde blockiert.",
"pws.quotaFailure.destination_blocked": "Die Netzwerkrichtlinie blockiert das Kontingentziel.",
"pws.quotaFailure.dns_failed": "Der Hostname des Kontingentservers konnte nicht aufgelöst werden.",
"pws.quotaFailure.timeout": "Die Kontingentanfrage hat das Zeitlimit überschritten.",
"pws.quotaFailure.transport_error": "Die Verbindung zum Kontingentserver ist fehlgeschlagen.",
"pws.quotaFailure.response_unusable": "Der Anbieter lieferte unbrauchbare Kontingentdaten.",
"pws.selected": "Ausgewählt",
"pws.copyModelId": "ID kopieren",
"pws.modelCopied": "Kopiert!",
Expand Down
10 changes: 10 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,16 @@ export const en = {
"pws.rateLimits": "Rate limits",
"pws.quotaUnavailable": "No quota data for this provider.",
"pws.accountQuotaUnavailable": "Rate-limit data temporarily unavailable; showing last known values when present.",
"pws.quotaFailure.account_unavailable": "Account details are unavailable for this quota check.",
"pws.quotaFailure.access_denied": "The provider denied access to quota data.",
"pws.quotaFailure.rate_limited": "The provider rate-limited the quota check.",
"pws.quotaFailure.upstream_error": "The provider could not complete the quota check.",
"pws.quotaFailure.redirect_blocked": "The quota endpoint returned a blocked redirect.",
"pws.quotaFailure.destination_blocked": "The quota destination was blocked by network policy.",
"pws.quotaFailure.dns_failed": "The quota hostname could not be resolved.",
"pws.quotaFailure.timeout": "The quota request timed out.",
"pws.quotaFailure.transport_error": "The quota connection failed.",
"pws.quotaFailure.response_unusable": "The provider returned unusable quota data.",
"pws.selected": "Selected",
"pws.copyModelId": "Copy ID",
"pws.modelCopied": "Copied!",
Expand Down
10 changes: 10 additions & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,16 @@ export const fr: Record<TKey, string> = {
"pws.rateLimits": "Limites de débit",
"pws.quotaUnavailable": "Aucune donnée de quota pour ce fournisseur.",
"pws.accountQuotaUnavailable": "Données de limite de débit temporairement indisponibles ; affichage des dernières valeurs connues, le cas échéant.",
"pws.quotaFailure.account_unavailable": "Les données du compte sont indisponibles pour cette vérification.",
"pws.quotaFailure.access_denied": "Le fournisseur a refusé l’accès aux données de quota.",
"pws.quotaFailure.rate_limited": "Le fournisseur a limité la fréquence des vérifications de quota.",
"pws.quotaFailure.upstream_error": "Le fournisseur n’a pas pu vérifier le quota.",
"pws.quotaFailure.redirect_blocked": "La redirection du service de quota a été bloquée.",
"pws.quotaFailure.destination_blocked": "La politique réseau a bloqué la destination du quota.",
"pws.quotaFailure.dns_failed": "Le nom du serveur de quota n’a pas pu être résolu.",
"pws.quotaFailure.timeout": "La requête de quota a expiré.",
"pws.quotaFailure.transport_error": "La connexion au service de quota a échoué.",
"pws.quotaFailure.response_unusable": "Les données de quota reçues sont inutilisables.",
"pws.selected": "Sélectionné",
"pws.copyModelId": "Copier l’ID",
"pws.modelCopied": "Copié !",
Expand Down
10 changes: 10 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,16 @@ export const ja: Record<TKey, string> = {
"pws.rateLimits": "レート制限",
"pws.quotaUnavailable": "このプロバイダーのクォータデータがありません。",
"pws.accountQuotaUnavailable": "レート制限データを一時的に取得できません。前回の値がある場合はそれを表示します。",
"pws.quotaFailure.account_unavailable": "割り当て確認に必要なアカウント情報を利用できません。",
"pws.quotaFailure.access_denied": "プロバイダーが割り当てデータへのアクセスを拒否しました。",
"pws.quotaFailure.rate_limited": "割り当て確認のレート制限に達しました。",
"pws.quotaFailure.upstream_error": "プロバイダーが割り当て確認を完了できませんでした。",
"pws.quotaFailure.redirect_blocked": "割り当てエンドポイントのリダイレクトをブロックしました。",
"pws.quotaFailure.destination_blocked": "ネットワークポリシーが割り当ての接続先をブロックしました。",
"pws.quotaFailure.dns_failed": "割り当てサーバーのホスト名を解決できませんでした。",
"pws.quotaFailure.timeout": "割り当てリクエストがタイムアウトしました。",
"pws.quotaFailure.transport_error": "割り当てサーバーへの接続に失敗しました。",
"pws.quotaFailure.response_unusable": "割り当てデータを読み取れませんでした。",
"pws.selected": "選択中",
"pws.copyModelId": "ID をコピー",
"pws.modelCopied": "コピーしました!",
Expand Down
Loading
Loading