dsh-agy 0.2.6 → 0.2.8

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.
@@ -4,6 +4,7 @@ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID }
4
4
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import lockfile from "proper-lockfile";
7
+ import { Document, isMap, isScalar, parseDocument } from "yaml";
7
8
  //#region src/types.ts
8
9
  /**
9
10
  * Host-independent authentication error. The adapter maps `kind` to the DSH
@@ -102,7 +103,7 @@ const AGY_DEFAULT_REDIRECT_URI = "http://localhost:51121/oauth-callback";
102
103
  * Antigravity API endpoints. The daily runtime host (no .sandbox suffix) is the
103
104
  * live endpoint for consumer OAuth accounts — cloudcode-pa.googleapis.com
104
105
  * answers RESOURCE_EXHAUSTED for them (verified by live probe), while the
105
- * daily host answers 200. Order matters: first reachable non-429/403 wins.
106
+ * daily host answers 200. Order matters: first reachable non-429/403/503 wins.
106
107
  */
107
108
  const AGY_ENDPOINT_DAILY = "https://daily-cloudcode-pa.googleapis.com";
108
109
  const AGY_ENDPOINT_PROD = "https://cloudcode-pa.googleapis.com";
@@ -115,13 +116,24 @@ const AGY_ENDPOINT_FALLBACKS = [
115
116
  AGY_ENDPOINT_DAILY_SANDBOX,
116
117
  AGY_ENDPOINT_AUTOPUSH
117
118
  ];
118
- /** Statuses that mean "this endpoint is not usable for this account"; skip to the next. */
119
- const AGY_ENDPOINT_SKIP_STATUSES = /* @__PURE__ */ new Set([429, 403]);
120
119
  /**
121
- * Try each runtime endpoint in order, skipping unusable ones (429/403/network).
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.
125
+ */
126
+ const AGY_ENDPOINT_SKIP_STATUSES = /* @__PURE__ */ new Set([
127
+ 429,
128
+ 403,
129
+ 503
130
+ ]);
131
+ /**
132
+ * Try each runtime endpoint in order, skipping unusable ones (429/403/503/network).
122
133
  * Returns the first other response (2xx or a real error like 400/401); when
123
134
  * every endpoint is unusable, returns the last skipped response so the caller's
124
- * classifier can still produce a meaningful error.
135
+ * classifier can still produce a meaningful error (a returned 503 becomes a
136
+ * retryable SERVER failure rather than being swallowed here).
125
137
  */
126
138
  async function fetchAgyFirstOk(urlPath, init, fetchImpl = proxiedFetch) {
127
139
  let lastSkipped = null;
@@ -1081,7 +1093,7 @@ var AgySessionManager = class AgySessionManager {
1081
1093
  try {
1082
1094
  const auth = await this.accessTokenFor(account);
1083
1095
  if (!auth) return null;
1084
- const { fetchAvailableModels } = await import("./models-BTMqI4Md.mjs").then((n) => n.r);
1096
+ const { fetchAvailableModels } = await import("./models-F-1cWO0f.mjs").then((n) => n.r);
1085
1097
  const quotas = ingestFamilyQuotas(await fetchAvailableModels(auth.access, account.projectId, (input, init) => {
1086
1098
  const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
1087
1099
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
@@ -1232,7 +1244,7 @@ var AgySessionManager = class AgySessionManager {
1232
1244
  }
1233
1245
  const key = this.accountKey(picked.account);
1234
1246
  if (!picked.account.projectId && !this.projectRetryFailed.has(key)) try {
1235
- const { loadCodeAssist } = await import("./exchange-DNrUFqeJ.mjs").then((n) => n.n);
1247
+ const { loadCodeAssist } = await import("./exchange-Bdql4kf3.mjs").then((n) => n.n);
1236
1248
  const { projectId } = await loadCodeAssist(auth.access);
1237
1249
  if (projectId) {
1238
1250
  await this.store.mutate((s) => {
@@ -1327,7 +1339,7 @@ var AgySessionManager = class AgySessionManager {
1327
1339
  ok: false,
1328
1340
  error: "No agy account configured — run `dsh-agy login` first."
1329
1341
  };
1330
- const { toAgyRequestBody } = await import("./translate-CfeRpHDe.mjs").then((n) => n.n);
1342
+ const { toAgyRequestBody } = await import("./translate-CE9tNXiV.mjs").then((n) => n.n);
1331
1343
  const { fetchAgyFirstOk } = await Promise.resolve().then(() => constants_exports);
1332
1344
  const { parseAgySse } = await import("./parse-Bs4rPkHA.mjs").then((n) => n.n);
1333
1345
  const body = toAgyRequestBody({
@@ -1517,34 +1529,86 @@ function resolveDshHome() {
1517
1529
  function homedir() {
1518
1530
  return process.env.HOME ?? process.env.USERPROFILE ?? ".";
1519
1531
  }
1532
+ /** A credentials reference name must be a POSIX-style identifier. */
1533
+ const REF_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
1534
+ /** The document's non-reference sections. */
1535
+ const SECTION_KEYS = ["version", "records"];
1536
+ /** Every top-level key DSH's credentials provider admits; anything else is rejected. */
1537
+ const DOCUMENT_TOP_LEVEL = /* @__PURE__ */ new Set([...SECTION_KEYS, "refs"]);
1538
+ /**
1539
+ * Describe one YAML parse failure without quoting the source, which holds
1540
+ * secrets. The parser's own message embeds the offending line.
1541
+ */
1542
+ function describeYamlError(error) {
1543
+ const at = error.linePos?.[0];
1544
+ return `${error.code ?? "parse-error"}${at ? ` at line ${at.line}, column ${at.col}` : ""}`;
1545
+ }
1546
+ /**
1547
+ * Parse the credentials document and locate its `refs` section.
1548
+ *
1549
+ * The document is owned by DSH's credentials provider, which serializes it with
1550
+ * the `yaml` package. Values are folded across physical lines (a long
1551
+ * single-quoted scalar folds at whitespace) and `records` payloads nest
1552
+ * arbitrary keys, so this MUST be a real parse rather than a line scan: a
1553
+ * line-based reader cannot represent folding, and it lets a `records` payload
1554
+ * shadow a real top-level reference.
1555
+ *
1556
+ * Deliberately permissive: it validates only the shape it consumes, so the read
1557
+ * path keeps working on a document written by an older build. What may be
1558
+ * rewritten is enforced separately, in the writer.
1559
+ */
1560
+ function parseCredentialsDocument(text, file, context) {
1561
+ const document = parseDocument(text, {
1562
+ prettyErrors: true,
1563
+ uniqueKeys: true
1564
+ });
1565
+ if (document.errors.length > 0) throw new Error(`${context}: cannot parse ${file}: ` + document.errors.map(describeYamlError).join("; "));
1566
+ const root = document.contents;
1567
+ if (!isMap(root)) throw new Error(`${context}: ${file} must be a mapping`);
1568
+ const refs = root.get("refs");
1569
+ if (refs !== void 0 && refs !== null && !isMap(refs)) throw new Error(`${context}: "refs" in ${file} must be a mapping`);
1570
+ return {
1571
+ document,
1572
+ root,
1573
+ refs: isMap(refs) ? refs : void 0,
1574
+ flat: !root.has("version")
1575
+ };
1576
+ }
1520
1577
  /**
1521
- * Minimal reader for the DSH credentials document: a flat YAML mapping of
1522
- * `KEY: "value"` / `KEY: value` lines. Values are unquoted with JSON-style
1523
- * escaping for double-quoted scalars; anything more complex fails loudly.
1578
+ * Read the references from the DSH credentials document.
1579
+ *
1580
+ * Only `refs` holds references, and a `records` payload must never be able to
1581
+ * shadow one. Two compatibility cases matter:
1582
+ *
1583
+ * - A pre-release "flat" document keeps its references at the top level; the
1584
+ * provider still reads that layout, so we do too.
1585
+ * - Version 0.2.7 of this plugin appended the master key as a *top-level* key.
1586
+ * The provider rejects such a document, but the key still decrypts an
1587
+ * existing account store, so it is read as a fallback. Dropping it would
1588
+ * silently mint a new master key and strand those accounts.
1589
+ *
1590
+ * Non-string and empty values are skipped, mirroring the provider.
1524
1591
  */
1525
1592
  function readCredentialsDocument(file) {
1526
- if (!existsSync(file)) return /* @__PURE__ */ new Map();
1527
1593
  const entries = /* @__PURE__ */ new Map();
1594
+ if (!existsSync(file)) return entries;
1528
1595
  const text = readFileSync(file, "utf8");
1529
- for (const line of text.split(/\r?\n/)) {
1530
- const trimmed = line.trim();
1531
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("---")) continue;
1532
- const match = /^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/.exec(trimmed);
1533
- if (!match) continue;
1534
- const [, key = "", rawValue = ""] = match;
1535
- let value;
1536
- if (rawValue.startsWith("\"")) try {
1537
- value = JSON.parse(rawValue);
1538
- } catch {
1539
- throw new Error(`readCredentialsDocument: invalid quoted value for "${key}" in ${file}`);
1540
- }
1541
- else if (rawValue.startsWith("'")) {
1542
- if (!rawValue.endsWith("'") || rawValue.length < 2) throw new Error(`readCredentialsDocument: unterminated single-quoted value for "${key}" in ${file}`);
1543
- value = rawValue.slice(1, -1).replace(/''/g, "'");
1544
- } else value = rawValue;
1545
- if (value.length === 0) continue;
1596
+ if (text.trim().length === 0) return entries;
1597
+ const { root, refs, flat } = parseCredentialsDocument(text, file, "readCredentialsDocument");
1598
+ const section = flat ? root : refs;
1599
+ if (section) for (const item of section.items) {
1600
+ if (!isScalar(item.key) || !isScalar(item.value)) continue;
1601
+ const key = item.key.value;
1602
+ const value = item.value.value;
1603
+ if (typeof key !== "string" || !REF_NAME.test(key)) continue;
1604
+ if (flat && SECTION_KEYS.includes(key)) continue;
1605
+ if (typeof value !== "string" || value.length === 0) continue;
1546
1606
  entries.set(key, value);
1547
1607
  }
1608
+ if (!flat && !entries.has("AGY_MASTER_KEY")) {
1609
+ const legacy = root.get(MASTER_KEY_REF);
1610
+ if (typeof legacy === "string" && legacy.length > 0) entries.set(MASTER_KEY_REF, legacy);
1611
+ }
1548
1612
  return entries;
1549
1613
  }
1550
1614
  /**
@@ -1568,21 +1632,83 @@ function loadMasterKey(dshHome) {
1568
1632
  assertOwnerOnly(file);
1569
1633
  return readCredentialsDocument(file).get(MASTER_KEY_REF);
1570
1634
  }
1635
+ /** Top-level keys DSH's credentials provider admits; anything else is rejected. */
1636
+ /**
1637
+ * Render the version-1 layout for a pre-release "flat" document — a non-empty
1638
+ * top-level mapping of reference names to non-empty string scalars, with no
1639
+ * `version` key and no document directives. The original lines are nested
1640
+ * verbatim under `refs:`, so comments, blank lines, and each value's spelling
1641
+ * survive byte for byte. Returns undefined for anything the recognizer
1642
+ * declines, which the caller must then refuse to rewrite.
1643
+ *
1644
+ * Nesting whole lines is safe for block scalars: an explicit indent indicator
1645
+ * (`|2`) is absolute and the implied one is detected relative to the key, so
1646
+ * shifting every line by the same two spaces preserves the parsed value.
1647
+ */
1648
+ function renderFlatLayoutMigration(text) {
1649
+ for (const line of text.split("\n")) if (/^(%|---|\.\.\.)/.test(line)) return void 0;
1650
+ const document = parseDocument(text, {
1651
+ prettyErrors: true,
1652
+ uniqueKeys: true
1653
+ });
1654
+ if (document.errors.length > 0) return void 0;
1655
+ const flat = document.contents;
1656
+ if (!isMap(flat) || flat.items.length === 0) return void 0;
1657
+ for (const pair of flat.items) {
1658
+ if (!isScalar(pair.key) || typeof pair.key.value !== "string") return void 0;
1659
+ if (DOCUMENT_TOP_LEVEL.has(pair.key.value) || !REF_NAME.test(pair.key.value)) return void 0;
1660
+ if (!isScalar(pair.value) || typeof pair.value.value !== "string") return void 0;
1661
+ if (pair.value.value.length === 0) return void 0;
1662
+ }
1663
+ return `version: 1\nrefs:\n${text.split("\n").map((line) => line.length === 0 ? line : ` ${line}`).join("\n")}${text.endsWith("\n") ? "" : "\n"}`;
1664
+ }
1665
+ /**
1666
+ * Render the credentials document with {@link MASTER_KEY_REF} added under `refs`.
1667
+ *
1668
+ * The key MUST be nested under `refs`: DSH's credentials provider rejects any
1669
+ * other top-level key, and a document it rejects makes *every* credential in
1670
+ * the file unreadable, not just ours. Editing the parsed document keeps the
1671
+ * comments and formatting of every untouched entry; a document this build
1672
+ * cannot prove it understands is never rewritten.
1673
+ */
1674
+ function renderWithMasterKey(existingText, masterKey, file) {
1675
+ const fresh = () => new Document({
1676
+ version: 1,
1677
+ refs: { [MASTER_KEY_REF]: masterKey }
1678
+ }).toString();
1679
+ if (existingText.trim().length === 0) return fresh();
1680
+ const { document, root, refs, flat } = parseCredentialsDocument(existingText, file, "persistMasterKey");
1681
+ if (flat) {
1682
+ const migrated = renderFlatLayoutMigration(existingText);
1683
+ if (migrated === void 0) throw new Error(`persistMasterKey: ${file} is not a credentials document this build can prove it understands; refusing to rewrite it`);
1684
+ const next = parseDocument(migrated);
1685
+ next.setIn(["refs", MASTER_KEY_REF], masterKey);
1686
+ return next.toString();
1687
+ }
1688
+ for (const key of root.items) {
1689
+ if (!isScalar(key.key) || typeof key.key.value !== "string") continue;
1690
+ if (!DOCUMENT_TOP_LEVEL.has(key.key.value)) throw new Error(`persistMasterKey: ${file} has unknown top-level key "${key.key.value}"; refusing to rewrite it`);
1691
+ }
1692
+ const version = root.get("version");
1693
+ if (version !== 1) throw new Error(`persistMasterKey: ${file} declares version ${JSON.stringify(version)}; refusing to rewrite it`);
1694
+ if (refs) document.setIn(["refs", MASTER_KEY_REF], masterKey);
1695
+ else document.set("refs", { [MASTER_KEY_REF]: masterKey });
1696
+ return document.toString();
1697
+ }
1571
1698
  /**
1572
1699
  * Generate and persist a fresh master key (0600) in the DSH credentials document.
1573
1700
  *
1574
- * Append-only + atomic rename: the DSH credentials service owns this file and
1575
- * preserves comments and the formatting of untouched entries (and may hold
1576
- * YAML constructs this minimal reader cannot parse). Rewriting the whole file
1577
- * from our parsed view would silently drop those; appending keeps them.
1701
+ * The key is added under `refs` by editing the parsed document, then written
1702
+ * with an atomic tmp+rename, so every other credential — and the comments and
1703
+ * formatting DSH services rely on — survives. Never rewrite the whole file from
1704
+ * a rebuilt view: that would silently drop entries this build does not model.
1578
1705
  */
1579
1706
  function persistMasterKey(dshHome, masterKey) {
1580
1707
  const file = join(dshHome, ".credentials.yaml");
1581
1708
  mkdirSync(dirname(file), { recursive: true });
1582
1709
  const existingText = existsSync(file) ? readFileSync(file, "utf8") : "";
1583
1710
  if (existingText.length > 0 && readCredentialsDocument(file).has("AGY_MASTER_KEY")) throw new Error(`persistMasterKey: ${MASTER_KEY_REF} already exists in ${file}`);
1584
- const entry = `${MASTER_KEY_REF}: ${JSON.stringify(masterKey)}`;
1585
- const next = existingText.length > 0 && !existingText.endsWith("\n") ? `${existingText}\n${entry}\n` : `${existingText}${entry}\n`;
1711
+ const next = renderWithMasterKey(existingText, masterKey, file);
1586
1712
  const tmp = `${file}.tmp-masterkey`;
1587
1713
  writeFileSync(tmp, next, { mode: 384 });
1588
1714
  renameSync(tmp, file);
@@ -1821,4 +1947,4 @@ var JsonAccountStore = class {
1821
1947
  //#endregion
1822
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 };
1823
1949
 
1824
- //# sourceMappingURL=accounts-CjOuS29o.mjs.map
1950
+ //# sourceMappingURL=accounts-Um8N9I56.mjs.map