dsh-agy 0.2.9 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +1 -0
  2. package/lib/{accounts-Um8N9I56.mjs → accounts-hsgwm7J9.mjs} +260 -233
  3. package/lib/accounts-hsgwm7J9.mjs.map +1 -0
  4. package/lib/auth-DPPVFwio.mjs +32 -0
  5. package/lib/auth-DPPVFwio.mjs.map +1 -0
  6. package/lib/cli/index.d.mts.map +1 -1
  7. package/lib/cli/index.mjs +8 -7
  8. package/lib/cli/index.mjs.map +1 -1
  9. package/lib/constants-BFKYjOmh.mjs +173 -0
  10. package/lib/constants-BFKYjOmh.mjs.map +1 -0
  11. package/lib/{exchange-Bdql4kf3.mjs → exchange-dQm2mjJ5.mjs} +15 -14
  12. package/lib/exchange-dQm2mjJ5.mjs.map +1 -0
  13. package/lib/identity-DsftHTJC.mjs +35 -0
  14. package/lib/identity-DsftHTJC.mjs.map +1 -0
  15. package/lib/{import-BkqCMX7m.mjs → import-D7R0GpRr.mjs} +14 -12
  16. package/lib/import-D7R0GpRr.mjs.map +1 -0
  17. package/lib/index.mjs +2 -2
  18. package/lib/{models-F-1cWO0f.mjs → models-IfOGIOS9.mjs} +3 -3
  19. package/lib/{models-F-1cWO0f.mjs.map → models-IfOGIOS9.mjs.map} +1 -1
  20. package/lib/{plugin-common-Di4n-1eR.mjs → plugin-common-Dk772zkm.mjs} +16 -129
  21. package/lib/plugin-common-Dk772zkm.mjs.map +1 -0
  22. package/lib/{proxy-DQPIwUov.mjs → proxy-DLvX8yxv.mjs} +63 -11
  23. package/lib/proxy-DLvX8yxv.mjs.map +1 -0
  24. package/lib/{translate-CE9tNXiV.mjs → translate-D0JHjwC-.mjs} +2 -2
  25. package/lib/{translate-CE9tNXiV.mjs.map → translate-D0JHjwC-.mjs.map} +1 -1
  26. package/lib/web/plugin.mjs +9 -9
  27. package/lib/web/plugin.mjs.map +1 -1
  28. package/package.json +3 -2
  29. package/lib/accounts-Um8N9I56.mjs.map +0 -1
  30. package/lib/exchange-Bdql4kf3.mjs.map +0 -1
  31. package/lib/import-BkqCMX7m.mjs.map +0 -1
  32. package/lib/plugin-common-Di4n-1eR.mjs.map +0 -1
  33. package/lib/proxy-DQPIwUov.mjs.map +0 -1
package/README.md CHANGED
@@ -230,5 +230,6 @@ pnpm run record:fixtures # re-record real-API fixtures (needs a real accou
230
230
  pnpm run e2e # real-account end-to-end (needs AGY_REFRESH_TOKEN)
231
231
  pnpm run debug:request # endpoint/header bisection probe
232
232
  pnpm run verify:tools # live two-turn tool-signature check
233
+ pnpm run verify:proxy-routing # live per-account proxy routing + fail-closed check
233
234
  npm pack --dry-run # verify the publishable artifact
234
235
  ```
@@ -1,195 +1,233 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { i as proxiedFetch, n as isProxyUnreachableError } from "./proxy-DQPIwUov.mjs";
1
+ import { a as AGY_VERSION_FALLBACK, g as isProxyRouted, h as AgyPoolBlockedError, m as AgyAuthError, p as resolveAgyClientCredentials, s as OAUTH_TOKEN_URL, t as AGY_CLIENT_ID } from "./constants-BFKYjOmh.mjs";
2
+ import { a as proxiedFetch, r as isProxyUnreachableError, t as accountFetch } from "./proxy-DLvX8yxv.mjs";
3
+ import { t as deriveAntigravitySessionId } from "./identity-DsftHTJC.mjs";
4
+ import { i as parseRefreshParts, n as calculateTokenExpiry, r as formatRefreshParts, t as accessTokenExpired } from "./auth-DPPVFwio.mjs";
3
5
  import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
4
6
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
5
7
  import { dirname, join, resolve } from "node:path";
6
8
  import lockfile from "proper-lockfile";
7
9
  import { Document, isMap, isScalar, parseDocument } from "yaml";
8
- //#region src/types.ts
10
+ //#region src/runtime/classify.ts
9
11
  /**
10
- * Host-independent authentication error. The adapter maps `kind` to the DSH
11
- * error protocol without coupling the session or CLI layers to dsh-llm.
12
+ * Upstream failure classification: HTTP status + headers + body → FailureKind.
13
+ * Conservative by design: only auth-failure may revoke an account; everything
14
+ * else cools, rotates, or retries.
12
15
  */
13
- var AgyAuthError = class extends Error {
14
- kind;
15
- constructor(kind, message, options) {
16
- super(message, options);
17
- this.name = "AgyAuthError";
18
- this.kind = kind;
19
- }
20
- };
21
- /**
22
- * Enabled accounts exist, but every candidate is temporarily blocked. Kept
23
- * independent of dsh-llm so CLI and web entry points do not gain a host import.
24
- */
25
- var AgyPoolBlockedError = class extends Error {
26
- kind;
27
- blockedUntil;
28
- constructor(kind, blockedUntil) {
29
- super(kind === "quota-exhausted" ? "All agy accounts have exhausted quota for the requested model family." : "All agy accounts are temporarily blocked for the requested model family.");
30
- this.name = "AgyPoolBlockedError";
31
- this.kind = kind;
32
- this.blockedUntil = blockedUntil;
16
+ const QUOTA_EXHAUSTED_KEYWORDS = [
17
+ "quota_exhausted",
18
+ "quota exhausted",
19
+ "quota reached",
20
+ "enable overages",
21
+ "individual quota"
22
+ ];
23
+ /** Classify a 429 body into the four upstream categories. */
24
+ function classifyRateLimit(bodyText, retryAfterMs) {
25
+ const text = (bodyText ?? "").toLowerCase();
26
+ if (QUOTA_EXHAUSTED_KEYWORDS.some((keyword) => text.includes(keyword))) return "quota_exhausted";
27
+ if (retryAfterMs !== void 0 && retryAfterMs < 3e3) return "soft_rate_limit";
28
+ if (retryAfterMs !== void 0) return "rate_limited";
29
+ return text.includes("quota") || text.includes("resource_exhausted") ? "quota_exhausted" : "unknown";
30
+ }
31
+ const RATE_LIMIT_RESET_FIELDS = [
32
+ "resetTime",
33
+ "reset_time",
34
+ "resetAt",
35
+ "quotaResetTime"
36
+ ];
37
+ function extractResetTime(bodyText) {
38
+ if (!bodyText) return void 0;
39
+ try {
40
+ const data = JSON.parse(bodyText);
41
+ for (const field of RATE_LIMIT_RESET_FIELDS) {
42
+ const value = data[field];
43
+ if (typeof value === "string" && value) return value;
44
+ if (typeof value === "number" && Number.isFinite(value)) return value > 0xe8d4a51000 ? new Date(value).toISOString() : new Date(Date.now() + value * 1e3).toISOString();
45
+ }
46
+ const quotaInfo = data.quotaInfo;
47
+ if (quotaInfo && typeof quotaInfo.resetTime === "string") return quotaInfo.resetTime;
48
+ } catch {}
49
+ }
50
+ function parseRetryAfter(header) {
51
+ if (!header) return void 0;
52
+ const seconds = Number(header);
53
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
54
+ const date = Date.parse(header);
55
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
56
+ }
57
+ /** Classify a completed HTTP response (non-2xx). */
58
+ function classifyHttpError(status, headers, bodyText) {
59
+ const retryAfterMs = parseRetryAfter(headers.get("retry-after"));
60
+ const resetTime = extractResetTime(bodyText);
61
+ if (status === 429) return {
62
+ kind: "rate-limit",
63
+ rateLimitCategory: classifyRateLimit(bodyText, retryAfterMs),
64
+ status,
65
+ retryAfterMs,
66
+ resetTime,
67
+ message: bodyText ? bodyText.slice(0, 200) : void 0
68
+ };
69
+ if (status === 401) return {
70
+ kind: "auth-failure",
71
+ status,
72
+ message: bodyText ? bodyText.slice(0, 200) : void 0
73
+ };
74
+ if (status === 403) {
75
+ const category = classifyRateLimit(bodyText, void 0);
76
+ if (category === "quota_exhausted") return {
77
+ kind: "rate-limit",
78
+ rateLimitCategory: category,
79
+ status,
80
+ message: bodyText ? bodyText.slice(0, 200) : void 0
81
+ };
82
+ return {
83
+ kind: "auth-failure",
84
+ status,
85
+ message: bodyText ? bodyText.slice(0, 200) : void 0
86
+ };
33
87
  }
34
- };
35
- //#endregion
36
- //#region src/oauth/constants.ts
37
- /**
38
- * Antigravity (agy) OAuth and API constants.
39
- *
40
- * The client id/secret below are the public Google consumer-OAuth credentials
41
- * shipped inside the Antigravity desktop product and its `agy` CLI; they are
42
- * embedded in many public tools (see NOTICE.md). They are not secrets owned by
43
- * this project.
44
- */
45
- var constants_exports = /* @__PURE__ */ __exportAll({
46
- AGY_CLIENT_ID: () => AGY_CLIENT_ID,
47
- AGY_CLIENT_SECRET: () => AGY_CLIENT_SECRET,
48
- AGY_DEFAULT_REDIRECT_URI: () => AGY_DEFAULT_REDIRECT_URI,
49
- AGY_ENDPOINT_AUTOPUSH: () => AGY_ENDPOINT_AUTOPUSH,
50
- AGY_ENDPOINT_DAILY: () => AGY_ENDPOINT_DAILY,
51
- AGY_ENDPOINT_DAILY_SANDBOX: () => AGY_ENDPOINT_DAILY_SANDBOX,
52
- AGY_ENDPOINT_FALLBACKS: () => AGY_ENDPOINT_FALLBACKS,
53
- AGY_ENDPOINT_PROD: () => AGY_ENDPOINT_PROD,
54
- AGY_ENDPOINT_SKIP_STATUSES: () => AGY_ENDPOINT_SKIP_STATUSES,
55
- AGY_SCOPES: () => AGY_SCOPES,
56
- AGY_VERSION_FALLBACK: () => AGY_VERSION_FALLBACK,
57
- OAUTH_AUTHORIZE_URL: () => OAUTH_AUTHORIZE_URL,
58
- OAUTH_TOKEN_URL: () => OAUTH_TOKEN_URL,
59
- OAUTH_USERINFO_URL: () => OAUTH_USERINFO_URL,
60
- fetchAgyFirstOk: () => fetchAgyFirstOk,
61
- getAgyBootstrapClientMetadata: () => getAgyBootstrapClientMetadata,
62
- getAgyBootstrapUserAgent: () => getAgyBootstrapUserAgent,
63
- resolveAgyClientCredentials: () => resolveAgyClientCredentials
64
- });
65
- const AGY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
66
- const AGY_CLIENT_SECRET = "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";
67
- /**
68
- * Effective OAuth client credentials: AGY_CLIENT_ID / AGY_CLIENT_SECRET env
69
- * overrides win when set (BYO OAuth app escape hatch, mirrors pi-antigravity);
70
- * otherwise the embedded public Antigravity credentials are used.
71
- */
72
- function resolveAgyClientCredentials(overrideClientId) {
73
- if (overrideClientId) {
74
- if (overrideClientId === "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com") return {
75
- clientId: AGY_CLIENT_ID,
76
- clientSecret: AGY_CLIENT_SECRET
88
+ if (status === 404) return {
89
+ kind: "transient",
90
+ status,
91
+ message: bodyText ? bodyText.slice(0, 200) : void 0
92
+ };
93
+ if (status >= 500) return {
94
+ kind: "transient",
95
+ status,
96
+ retryAfterMs,
97
+ message: bodyText ? bodyText.slice(0, 200) : void 0
98
+ };
99
+ if (status === 400) {
100
+ const text = (bodyText ?? "").toLowerCase();
101
+ if (text.includes("context") && (text.includes("overflow") || text.includes("too long") || text.includes("exceeded")) || text.includes("model") && (text.includes("not found") || text.includes("unavailable") || text.includes("not supported"))) return {
102
+ kind: "transient",
103
+ status,
104
+ message: bodyText ? bodyText.slice(0, 200) : void 0
77
105
  };
78
106
  return {
79
- clientId: overrideClientId,
80
- clientSecret: process.env.AGY_CLIENT_SECRET || "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"
107
+ kind: "request-error",
108
+ status,
109
+ message: bodyText ? bodyText.slice(0, 200) : void 0
81
110
  };
82
111
  }
83
112
  return {
84
- clientId: process.env.AGY_CLIENT_ID || "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
85
- clientSecret: process.env.AGY_CLIENT_SECRET || "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"
113
+ kind: "transient",
114
+ status,
115
+ message: bodyText ? bodyText.slice(0, 200) : void 0
86
116
  };
87
117
  }
88
- /** Required scopes. `openid` must NOT be added: it routes Google into the hanging
89
- * `firstparty/nativeapp` consent for this client (verified by OmniRoute). */
90
- const AGY_SCOPES = [
91
- "https://www.googleapis.com/auth/cloud-platform",
92
- "https://www.googleapis.com/auth/userinfo.email",
93
- "https://www.googleapis.com/auth/userinfo.profile",
94
- "https://www.googleapis.com/auth/cclog",
95
- "https://www.googleapis.com/auth/experimentsandconfigs"
96
- ];
97
- const OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
98
- const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
99
- const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
100
- /** Default loopback callback used by the standalone CLI listener (fixed port, like opencode). */
101
- const AGY_DEFAULT_REDIRECT_URI = "http://localhost:51121/oauth-callback";
118
+ /** Codes that carry a socket/syscall `code` worth surfacing in a failure message. */
119
+ const TRANSPORT_CAUSE_CODES = /* @__PURE__ */ new Set([
120
+ "ECONNREFUSED",
121
+ "ECONNRESET",
122
+ "ETIMEDOUT",
123
+ "ENETUNREACH",
124
+ "EHOSTUNREACH",
125
+ "EPIPE",
126
+ "ENOTFOUND",
127
+ "EAI_AGAIN",
128
+ "CERT_HAS_EXPIRED",
129
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
130
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
131
+ "ERR_TLS_CERT_ALTNAME_INVALID",
132
+ "UND_ERR_CONNECT_TIMEOUT",
133
+ "UND_ERR_SOCKET",
134
+ "UND_ERR_HEADERS_TIMEOUT",
135
+ "UND_ERR_BODY_TIMEOUT",
136
+ "UND_ERR_ABORTED"
137
+ ]);
102
138
  /**
103
- * Antigravity API endpoints. The daily runtime host (no .sandbox suffix) is the
104
- * live endpoint for consumer OAuth accounts — cloudcode-pa.googleapis.com
105
- * answers RESOURCE_EXHAUSTED for them (verified by live probe), while the
106
- * daily host answers 200. Order matters: first reachable non-429/403/503 wins.
139
+ * Strip proxy credentials from URL-like text so `user:pass` never reaches logs
140
+ * or the GUI. Splits at the LAST `@` of the authority, which is the real
141
+ * userinfo delimiter (RFC 3986: a host cannot contain a raw `@`), so a password
142
+ * that itself contains `@` is redacted whole rather than truncated at the first
143
+ * one — the failure mode of the character-class regex this replaced.
107
144
  */
108
- const AGY_ENDPOINT_DAILY = "https://daily-cloudcode-pa.googleapis.com";
109
- const AGY_ENDPOINT_PROD = "https://cloudcode-pa.googleapis.com";
110
- const AGY_ENDPOINT_DAILY_SANDBOX = "https://daily-cloudcode-pa.sandbox.googleapis.com";
111
- const AGY_ENDPOINT_AUTOPUSH = "https://autopush-cloudcode-pa.sandbox.googleapis.com";
112
- /** Runtime/bootstrap endpoint fallback order (daily first, mirroring OmniRoute). */
113
- const AGY_ENDPOINT_FALLBACKS = [
114
- AGY_ENDPOINT_DAILY,
115
- AGY_ENDPOINT_PROD,
116
- AGY_ENDPOINT_DAILY_SANDBOX,
117
- AGY_ENDPOINT_AUTOPUSH
118
- ];
145
+ function redactCredentials(text) {
146
+ return text.replace(/([a-z][a-z0-9+.-]*:\/\/)([^\s/?#]+)/gi, (match, scheme, authority) => {
147
+ const at = authority.lastIndexOf("@");
148
+ if (at === -1) return match;
149
+ return `${scheme}<REDACTED>@${authority.slice(at + 1)}`;
150
+ });
151
+ }
119
152
  /**
120
- * Statuses that mean "this endpoint is not usable for this attempt"; skip to the
121
- * next one in the chain. 429/403 = rate/quota/entitlement wall, 503 = capacity
122
- * rejection (e.g. "No capacity available for model ..."). A 503 skipped here
123
- * still reaches the caller when every endpoint fails, where the classifier marks
124
- * it transient and the adapter surfaces it as a retryable SERVER error.
153
+ * Placeholder messages undici/Node emit while the real reason sits deeper in
154
+ * the cause chain; they must not shadow a usable code further down.
125
155
  */
126
- const AGY_ENDPOINT_SKIP_STATUSES = /* @__PURE__ */ new Set([
127
- 429,
128
- 403,
129
- 503
156
+ const GENERIC_CAUSE_MESSAGES = /* @__PURE__ */ new Set([
157
+ "fetch failed",
158
+ "terminated",
159
+ "other side closed",
160
+ "aborted"
130
161
  ]);
131
162
  /**
132
- * Try each runtime endpoint in order, skipping unusable ones (429/403/503/network).
133
- * Returns the first other response (2xx or a real error like 400/401); when
134
- * every endpoint is unusable, returns the last skipped response so the caller's
135
- * classifier can still produce a meaningful error (a returned 503 becomes a
136
- * retryable SERVER failure rather than being swallowed here).
163
+ * Walk the error cause chain collecting the actionable `code` and message.
164
+ * Node/undici reports `TypeError: fetch failed` and stores the real reason in
165
+ * `error.cause` (verified against live failures): without this, DNS, TLS,
166
+ * timeout, reset, and proxy failures are indistinguishable in DSH session
167
+ * events and the GUI.
137
168
  */
138
- async function fetchAgyFirstOk(urlPath, init, fetchImpl = proxiedFetch) {
139
- let lastSkipped = null;
140
- for (const baseEndpoint of AGY_ENDPOINT_FALLBACKS) try {
141
- const response = await fetchImpl(`${baseEndpoint}${urlPath}`, init);
142
- if (!AGY_ENDPOINT_SKIP_STATUSES.has(response.status)) return response;
143
- lastSkipped = response;
144
- } catch (error) {
145
- const { isProxyUnreachableError } = await import("./proxy-DQPIwUov.mjs").then((n) => n.o);
146
- if (isProxyUnreachableError(error)) throw error;
169
+ function describeCause(error) {
170
+ const seen = /* @__PURE__ */ new Set();
171
+ let current = error?.cause ?? error;
172
+ let fallbackMessage;
173
+ for (let depth = 0; current && depth < 6 && !seen.has(current); depth++) {
174
+ seen.add(current);
175
+ if (typeof current === "object") {
176
+ const code = current.code;
177
+ const message = current.message;
178
+ const usableMessage = typeof message === "string" && message.length > 0 ? message : void 0;
179
+ if (typeof code === "string" && TRANSPORT_CAUSE_CODES.has(code)) return {
180
+ code,
181
+ message: usableMessage
182
+ };
183
+ if (usableMessage && !GENERIC_CAUSE_MESSAGES.has(usableMessage) && !fallbackMessage) fallbackMessage = usableMessage;
184
+ const errors = current.errors;
185
+ if (Array.isArray(errors)) for (const nested of errors) {
186
+ const described = describeCause({ cause: nested });
187
+ if (described.code) return described;
188
+ if (described.message && !fallbackMessage) fallbackMessage = described.message;
189
+ }
190
+ }
191
+ current = current?.cause;
147
192
  }
148
- if (lastSkipped) return lastSkipped;
149
- throw new Error("all agy endpoints failed");
150
- }
151
- /** Default Antigravity client version used in User-Agent strings; overridden by the
152
- * runtime version fetcher (see runtime/fingerprint.ts). */
153
- const AGY_VERSION_FALLBACK = "1.18.3";
154
- /** Electron-style UA used for bootstrap calls (loadCodeAssist/onboardUser). */
155
- function getAgyBootstrapUserAgent(version = AGY_VERSION_FALLBACK) {
156
- return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${version} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`;
193
+ return fallbackMessage ? { message: fallbackMessage } : {};
157
194
  }
158
- /** Client-Metadata payload for bootstrap calls — ideType only (backend enum
159
- * validation rejects freely-added platform/pluginType; AGENTS.md invariant). */
160
- function getAgyBootstrapClientMetadata() {
161
- return "{\"ideType\":\"ANTIGRAVITY\"}";
162
- }
163
- //#endregion
164
- //#region src/runtime/identity.ts
165
195
  /**
166
- * Antigravity request/session identity generation (mirrors the active
167
- * OmniRoute implementation; the archived reference stopped maintaining these).
196
+ * Human-readable transport failure: `fetch failed (UND_ERR_SOCKET: other side
197
+ * closed)`. The sanitized cause code/message is what makes a transient socket
198
+ * error distinguishable from a DNS, TLS, or proxy failure in session records.
168
199
  */
169
- /** One request id per upstream call: `agent/<epoch>/<8 hex>`. */
170
- function generateAntigravityRequestId() {
171
- return `agent/${Date.now()}/${randomBytes(4).toString("hex")}`;
172
- }
173
- const FNV_OFFSET_I64 = -3750763044362895579n;
174
- const FNV_PRIME_I64 = 1099511628211n;
175
- /** 64-bit FNV-1a hash of a string (stable across processes). */
176
- function fnv1a64(input) {
177
- let hash = FNV_OFFSET_I64;
178
- for (let i = 0; i < input.length; i++) {
179
- hash ^= BigInt(input.charCodeAt(i));
180
- hash = BigInt.asIntN(64, hash * FNV_PRIME_I64);
181
- }
182
- return hash;
200
+ function describeFetchError(error) {
201
+ const base = error instanceof Error ? error.message : String(error);
202
+ const { code, message } = describeCause(error);
203
+ if (!code && !message) return redactCredentials(base);
204
+ const detail = code && message ? message.includes(code) ? message : `${code}: ${message}` : code ?? message;
205
+ if (detail === base) return redactCredentials(base);
206
+ return redactCredentials(`${base} (${detail})`);
183
207
  }
184
208
  /**
185
- * Stable per-account session id: same account always derives the same id, so
186
- * multi-turn context caching keys consistently; unknown accounts get a fresh
187
- * random id.
209
+ * Classify a fetch-level failure (DNS, refused, timeout, abort).
210
+ * @param routing - how the failed request was routed; fail-closed applies only
211
+ * when it carried an explicit per-account proxy (see {@link AccountRouting}).
188
212
  */
189
- function deriveAntigravitySessionId(accountKey) {
190
- if (!accountKey || accountKey.trim().length === 0) return null;
191
- const hash = fnv1a64(accountKey.trim());
192
- return `-${((hash < 0n ? -hash : hash) % 9000000000000000000n).toString()}`;
213
+ function classifyFetchError(error, routing) {
214
+ const message = describeFetchError(error);
215
+ if (error instanceof DOMException && error.name === "AbortError") return {
216
+ kind: "network-error",
217
+ message
218
+ };
219
+ if (error?.errorCode === "proxy_unreachable" || error?.code === "PROXY_UNREACHABLE") return {
220
+ kind: "proxy-unreachable",
221
+ message
222
+ };
223
+ if (isProxyRouted(routing) && isProxyUnreachableError(error)) return {
224
+ kind: "proxy-unreachable",
225
+ message
226
+ };
227
+ return {
228
+ kind: "network-error",
229
+ message
230
+ };
193
231
  }
194
232
  //#endregion
195
233
  //#region src/runtime/version.ts
@@ -306,35 +344,6 @@ function peekCachedAntigravityVersion() {
306
344
  const ideState = { inFlight: null };
307
345
  const cliState = { inFlight: null };
308
346
  //#endregion
309
- //#region src/oauth/auth.ts
310
- const ACCESS_TOKEN_EXPIRY_BUFFER_MS = 6e4;
311
- /** Split the packed refresh string into its constituent refresh token and project ids. */
312
- function parseRefreshParts(refresh) {
313
- const [refreshToken = "", projectId = "", managedProjectId = ""] = (refresh ?? "").split("|");
314
- return {
315
- refreshToken: refreshToken || void 0,
316
- projectId: projectId || void 0,
317
- managedProjectId: managedProjectId || void 0
318
- };
319
- }
320
- /** Serialize refresh token parts into the stored string format. */
321
- function formatRefreshParts(parts) {
322
- const projectSegment = parts.projectId ?? "";
323
- const base = `${parts.refreshToken ?? ""}|${projectSegment}`;
324
- return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
325
- }
326
- /** Whether the access token is expired or missing, with a buffer for clock skew. */
327
- function accessTokenExpired(auth) {
328
- if (!auth.access || typeof auth.expires !== "number") return true;
329
- return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;
330
- }
331
- /** Absolute expiry timestamp from a duration; malformed durations expire immediately. */
332
- function calculateTokenExpiry(requestTimeMs, expiresInSeconds) {
333
- const seconds = typeof expiresInSeconds === "number" ? expiresInSeconds : 3600;
334
- if (Number.isNaN(seconds) || seconds <= 0) return requestTimeMs;
335
- return requestTimeMs + seconds * 1e3;
336
- }
337
- //#endregion
338
347
  //#region src/oauth/refresh.ts
339
348
  var AgyTokenRefreshError = class extends Error {
340
349
  code;
@@ -1033,8 +1042,9 @@ var AgySessionManager = class AgySessionManager {
1033
1042
  expires: cached.expires,
1034
1043
  refresh: account.refresh
1035
1044
  };
1036
- if (isProxyUnreachableError(result.error)) throw new AgyAuthError("transport", "proxy_unreachable", { cause: result.error });
1037
- throw new AgyAuthError(result.error.status === 429 ? "rate-limit" : result.error.status === 0 || result.error.status === 408 || result.error.status >= 500 ? "transport" : "invalid-credential", result.error.message, { cause: result.error });
1045
+ if (account.proxy && isProxyUnreachableError(result.error)) throw new AgyAuthError("transport", "proxy_unreachable", { cause: result.error });
1046
+ const kind = result.error.status === 429 ? "rate-limit" : result.error.status === 0 || result.error.status === 408 || result.error.status >= 500 ? "transport" : "invalid-credential";
1047
+ throw new AgyAuthError(kind, describeFetchError(result.error), { cause: result.error });
1038
1048
  }
1039
1049
  if (result.type === "revoked") {
1040
1050
  this.tokenCache.delete(key);
@@ -1093,15 +1103,12 @@ var AgySessionManager = class AgySessionManager {
1093
1103
  try {
1094
1104
  const auth = await this.accessTokenFor(account);
1095
1105
  if (!auth) return null;
1096
- const { fetchAvailableModels } = await import("./models-F-1cWO0f.mjs").then((n) => n.r);
1106
+ const { fetchAvailableModels } = await import("./models-IfOGIOS9.mjs").then((n) => n.r);
1107
+ const routed = accountFetch({ proxyUrl: account.proxy });
1097
1108
  const quotas = ingestFamilyQuotas(await fetchAvailableModels(auth.access, account.projectId, (input, init) => {
1098
1109
  const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
1099
1110
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
1100
- if (account.proxy) return proxiedFetch(input, {
1101
- ...init,
1102
- signal
1103
- }, { proxyUrl: account.proxy });
1104
- return fetch(input, {
1111
+ return routed(input, {
1105
1112
  ...init,
1106
1113
  signal
1107
1114
  });
@@ -1205,6 +1212,15 @@ var AgySessionManager = class AgySessionManager {
1205
1212
  let storage = await this.store.load();
1206
1213
  const maxAttempts = storage.accounts.filter((account) => account.enabled !== false).length;
1207
1214
  let proxyUnreachableCount = 0;
1215
+ /**
1216
+ * Last transport failure seen on a *proxyless* account. Such an account is
1217
+ * skipped (another enabled account may be healthy) without writing a
1218
+ * cooldown: a cooldown would surface as AgyPoolBlockedError — i.e. RATE_LIMIT
1219
+ * for a plain network error — and would block a solo pool outright. It is
1220
+ * remembered so the request still reports its real cause rather than
1221
+ * degrading into "no account configured" when every account fails this way.
1222
+ */
1223
+ let lastTransportError;
1208
1224
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1209
1225
  if (storage.accounts.filter((account) => account.enabled !== false).length > 1) await this.refreshQuotaCache(storage);
1210
1226
  const picked = await this.pickAccount(storage, model);
@@ -1213,29 +1229,14 @@ var AgySessionManager = class AgySessionManager {
1213
1229
  try {
1214
1230
  auth = await this.accessTokenFor(picked.account);
1215
1231
  } catch (error) {
1216
- if (error instanceof AgyAuthError && error.kind === "transport" && isProxyUnreachableError(error)) {
1232
+ if (Boolean(picked.account.proxy) && isProxyUnreachableError(error)) {
1217
1233
  proxyUnreachableCount++;
1218
- this.lastUsed = null;
1219
- const deadIndex = storage.accounts.findIndex((a) => this.accountKey(a) === this.accountKey(picked.account));
1220
- if (deadIndex !== -1) {
1221
- const next = pickNextAccountIndex(storage.accounts, deadIndex, Date.now());
1222
- if (next !== storage.activeIndex) {
1223
- storage.activeIndex = next;
1224
- await this.store.mutate((s) => {
1225
- s.activeIndex = next;
1226
- }).catch(() => {});
1227
- }
1228
- }
1229
- storage = await this.store.load();
1230
- continue;
1231
- }
1232
- if (isProxyUnreachableError(error)) {
1233
- proxyUnreachableCount++;
1234
- this.lastUsed = null;
1235
- storage = await this.store.load();
1234
+ storage = await this.skipAccount(storage, picked.account);
1236
1235
  continue;
1237
1236
  }
1238
- throw error;
1237
+ lastTransportError = error;
1238
+ storage = await this.skipAccount(storage, picked.account);
1239
+ continue;
1239
1240
  }
1240
1241
  if (!auth) {
1241
1242
  this.lastUsed = null;
@@ -1244,8 +1245,8 @@ var AgySessionManager = class AgySessionManager {
1244
1245
  }
1245
1246
  const key = this.accountKey(picked.account);
1246
1247
  if (!picked.account.projectId && !this.projectRetryFailed.has(key)) try {
1247
- const { loadCodeAssist } = await import("./exchange-Bdql4kf3.mjs").then((n) => n.n);
1248
- const { projectId } = await loadCodeAssist(auth.access);
1248
+ const { loadCodeAssist } = await import("./exchange-dQm2mjJ5.mjs").then((n) => n.n);
1249
+ const { projectId } = await loadCodeAssist(auth.access, { proxyUrl: picked.account.proxy });
1249
1250
  if (projectId) {
1250
1251
  await this.store.mutate((s) => {
1251
1252
  const account = s.accounts.find((candidate) => this.accountKey(candidate) === key);
@@ -1276,6 +1277,27 @@ var AgySessionManager = class AgySessionManager {
1276
1277
  };
1277
1278
  }
1278
1279
  if (proxyUnreachableCount === maxAttempts && maxAttempts > 0) throw new AgyAuthError("transport", "proxy_unreachable");
1280
+ if (lastTransportError !== void 0) throw lastTransportError;
1281
+ }
1282
+ /**
1283
+ * Drop the session pin and move the pool cursor off an account that just
1284
+ * failed, so the next `pickAccount` in this same request tries another one.
1285
+ * State on the account itself is deliberately left untouched (no cooldown).
1286
+ */
1287
+ async skipAccount(storage, account) {
1288
+ this.lastUsed = null;
1289
+ const key = this.accountKey(account);
1290
+ const deadIndex = storage.accounts.findIndex((candidate) => this.accountKey(candidate) === key);
1291
+ if (deadIndex !== -1) {
1292
+ const next = pickNextAccountIndex(storage.accounts, deadIndex, Date.now());
1293
+ if (next !== storage.activeIndex) {
1294
+ storage.activeIndex = next;
1295
+ await this.store.mutate((s) => {
1296
+ s.activeIndex = next;
1297
+ }).catch(() => {});
1298
+ }
1299
+ }
1300
+ return this.store.load();
1279
1301
  }
1280
1302
  /** Adapter hook: apply rotation decisions and fingerprint regeneration. */
1281
1303
  async reportFailure(kind, session, info) {
@@ -1339,8 +1361,8 @@ var AgySessionManager = class AgySessionManager {
1339
1361
  ok: false,
1340
1362
  error: "No agy account configured — run `dsh-agy login` first."
1341
1363
  };
1342
- const { toAgyRequestBody } = await import("./translate-CE9tNXiV.mjs").then((n) => n.n);
1343
- const { fetchAgyFirstOk } = await Promise.resolve().then(() => constants_exports);
1364
+ const { toAgyRequestBody } = await import("./translate-D0JHjwC-.mjs").then((n) => n.n);
1365
+ const { fetchAgyFirstOk } = await import("./constants-BFKYjOmh.mjs").then((n) => n.l);
1344
1366
  const { parseAgySse } = await import("./parse-Bs4rPkHA.mjs").then((n) => n.n);
1345
1367
  const body = toAgyRequestBody({
1346
1368
  provider: "agy",
@@ -1358,16 +1380,21 @@ var AgySessionManager = class AgySessionManager {
1358
1380
  projectId: session.account.projectId,
1359
1381
  sessionId: deriveAntigravitySessionId(session.account.email) ?? void 0
1360
1382
  });
1383
+ const headers = {
1384
+ authorization: `Bearer ${session.auth.access}`,
1385
+ "content-type": "application/json",
1386
+ accept: "text/event-stream",
1387
+ ...session.impersonation
1388
+ };
1389
+ const routing = {
1390
+ proxyUrl: session.account.proxy,
1391
+ streaming: true
1392
+ };
1361
1393
  const response = await fetchAgyFirstOk("/v1internal:streamGenerateContent?alt=sse", {
1362
1394
  method: "POST",
1363
- headers: {
1364
- authorization: `Bearer ${session.auth.access}`,
1365
- "content-type": "application/json",
1366
- accept: "text/event-stream",
1367
- ...session.impersonation
1368
- },
1395
+ headers,
1369
1396
  body: JSON.stringify(body)
1370
- });
1397
+ }, accountFetch(routing), routing);
1371
1398
  if (!response.ok) {
1372
1399
  const text = await response.text().catch(() => "");
1373
1400
  return {
@@ -1945,6 +1972,6 @@ var JsonAccountStore = class {
1945
1972
  }
1946
1973
  };
1947
1974
  //#endregion
1948
- export { AgyPoolBlockedError as A, OAUTH_TOKEN_URL as C, getAgyBootstrapUserAgent as D, getAgyBootstrapClientMetadata as E, resolveAgyClientCredentials as O, OAUTH_AUTHORIZE_URL as S, fetchAgyFirstOk as T, deriveAntigravitySessionId as _, deriveKey as a, AGY_ENDPOINT_FALLBACKS as b, resolveDshHome as c, isAgyDisabled as d, generateFingerprint as f, resolveAntigravityVersionBounded as g, resolveAntigravityVersion as h, createAesGcmCodec as i, AgyAuthError as k, resolveMasterKeyCodec as l, calculateTokenExpiry as m, maskProxyUrl as n, loadMasterKey as o, recordFingerprintVersion as p, MASTER_KEY_REF as r, persistMasterKey as s, JsonAccountStore as t, AgySessionManager as u, generateAntigravityRequestId as v, OAUTH_USERINFO_URL as w, AGY_SCOPES as x, AGY_DEFAULT_REDIRECT_URI as y };
1975
+ export { classifyHttpError as _, deriveKey as a, resolveDshHome as c, isAgyDisabled as d, generateFingerprint as f, classifyFetchError as g, resolveAntigravityVersionBounded as h, createAesGcmCodec as i, resolveMasterKeyCodec as l, resolveAntigravityVersion as m, maskProxyUrl as n, loadMasterKey as o, recordFingerprintVersion as p, MASTER_KEY_REF as r, persistMasterKey as s, JsonAccountStore as t, AgySessionManager as u, describeFetchError as v };
1949
1976
 
1950
- //# sourceMappingURL=accounts-Um8N9I56.mjs.map
1977
+ //# sourceMappingURL=accounts-hsgwm7J9.mjs.map