@topolo/cli 0.13.11 → 0.13.12

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.
package/dist/index.js CHANGED
@@ -8,6 +8,28 @@ import { fileURLToPath } from "url";
8
8
  import { homedir } from "os";
9
9
  function registerAgentPacksCommand(program2) {
10
10
  const packs = program2.command("agent-packs").description("Install and operate Topolo Agent Packs in supported local hosts").option("--store <path>", "Portable pack library (default ~/AgentPacks)");
11
+ packs.command("runtime-adoption").description("Plan or apply an owned private runtime configuration; never starts or authenticates a consumer").argument("<operation>", "plan or apply").requiredOption("--manifest <path>", "Closed owner-scoped manifest").option("--plan-digest <sha256>", "Exact reviewed plan digest required for apply").option("--confirm", "Confirm the configuration-only apply").allowExcessArguments(false).action((operation, options) => {
12
+ if (!["plan", "apply"].includes(operation) || !options.manifest.startsWith("/") || (operation === "apply" ? !/^[a-f0-9]{64}$/.test(options.planDigest ?? "") || !options.confirm : options.planDigest !== void 0 || options.confirm)) throw new Error("Exact runtime-adoption plan or confirmed apply required.");
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const adapter = [resolve(here, "../agent-packs/topolo-agent-pack"), resolve(here, "../../agent-packs/topolo-agent-pack")].find(existsSync);
15
+ if (!adapter) throw new Error("Installed bundled pack adapter unavailable.");
16
+ const result = spawnSync(
17
+ "python3",
18
+ [
19
+ adapter,
20
+ "--store",
21
+ packs.opts().store ?? resolve(homedir(), "AgentPacks"),
22
+ "runtime-adoption",
23
+ operation,
24
+ "--manifest",
25
+ options.manifest,
26
+ ...operation === "apply" ? ["--plan-digest", options.planDigest, "--confirm"] : []
27
+ ],
28
+ { stdio: "inherit", env: { PATH: process.env.PATH, HOME: homedir(), PYTHONDONTWRITEBYTECODE: "1" } }
29
+ );
30
+ if (result.error) throw new Error("Python 3.11 or later is required for runtime adoption.");
31
+ if (result.status !== 0) process.exitCode = result.status ?? 2;
32
+ });
11
33
  packs.command("smoke").description("Start or read one closed, bound native acceptance case").argument("<operation>", "start or read").argument("<identity>", "Owned role key for start, existing operation UUID for read").option("--case <id>", "Finite compiled acceptance case").option("--operation-id <id>", "Unique native operation UUID for start").option("--harness <name>", "codex or claude", "codex").allowExcessArguments(false).action(async (operation, identity, options) => {
12
34
  const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
13
35
  if (!["start", "read"].includes(operation) || !["codex", "claude"].includes(options.harness) || (operation === "start" ? !/^[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+$/.test(identity) || !["stg-context-memory-bytes", "stg-peer-ack", "prod-role-context-ack"].includes(options.case ?? "") || !uuid.test(options.operationId ?? "") : !uuid.test(identity) || options.case !== void 0 || options.operationId !== void 0))
@@ -209,7 +231,7 @@ import { platform as platform2 } from "os";
209
231
  import { createInterface } from "readline/promises";
210
232
  import { stdin as input } from "process";
211
233
 
212
- // node_modules/.pnpm/@topolo+sdk@0.23.21/node_modules/@topolo/sdk/dist/index.js
234
+ // node_modules/.pnpm/@topolo+sdk@0.23.22/node_modules/@topolo/sdk/dist/index.js
213
235
  function record(value) {
214
236
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
215
237
  }
@@ -288,6 +310,7 @@ var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
288
310
  "organization_enrollment_required",
289
311
  "permission_denied",
290
312
  "rate_limited",
313
+ "reauth_required",
291
314
  "request_failed",
292
315
  "request_timeout",
293
316
  "service_catalog_unavailable",
@@ -649,6 +672,22 @@ var TopoloHttpError = class extends TopoloSdkError {
649
672
  );
650
673
  }
651
674
  };
675
+ function topoloRetryAfterSeconds(value, now = Date.now()) {
676
+ if (!value) return void 0;
677
+ if (/^\d{1,10}$/.test(value)) return Number(value);
678
+ const at = Date.parse(value);
679
+ return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - now) / 1e3));
680
+ }
681
+ var TopoloRateLimitError = class extends TopoloHttpError {
682
+ retryable = true;
683
+ retryAfterSeconds;
684
+ constructor(service, path, body, message2, options = {}) {
685
+ super(service, path, 429, body, message2, options);
686
+ this.name = "TopoloRateLimitError";
687
+ if (this.code === "http_error") this.code = "rate_limited";
688
+ this.retryAfterSeconds = topoloRetryAfterSeconds(this.retryAfter);
689
+ }
690
+ };
652
691
  var ACTION_SECRET_PREFIX = "topolo-secret:";
653
692
  function isActionSecretReference(value) {
654
693
  return typeof value === "string" && value.startsWith(ACTION_SECRET_PREFIX);
@@ -1619,6 +1658,10 @@ function isRecord3(value) {
1619
1658
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1620
1659
  }
1621
1660
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
1661
+ var IDEMPOTENT_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
1662
+ var RATE_LIMIT_READ_RETRIES = 2;
1663
+ var RATE_LIMIT_READ_RETRY_MAX_DELAY_MS = 1e4;
1664
+ var RATE_LIMIT_READ_DEFAULT_DELAY_MS = 1e3;
1622
1665
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
1623
1666
  var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
1624
1667
  var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
@@ -1815,6 +1858,15 @@ var TopoloClient = class _TopoloClient {
1815
1858
  res = await fetchWithReadRetry();
1816
1859
  }
1817
1860
  }
1861
+ for (let retry = 0; res.status === 429 && IDEMPOTENT_READ_METHODS.has(method) && retry < RATE_LIMIT_READ_RETRIES; retry += 1) {
1862
+ const header = canonicalTopoloRetryAfter(res.headers.get("Retry-After"));
1863
+ const seconds = topoloRetryAfterSeconds(header);
1864
+ const delayMs = header === void 0 ? RATE_LIMIT_READ_DEFAULT_DELAY_MS * (retry + 1) : (seconds ?? Infinity) * 1e3;
1865
+ if (delayMs > RATE_LIMIT_READ_RETRY_MAX_DELAY_MS || opts.signal?.aborted) break;
1866
+ await res.body?.cancel().catch(() => void 0);
1867
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs));
1868
+ res = await fetchWithReadRetry();
1869
+ }
1818
1870
  } catch (err) {
1819
1871
  this.emit({
1820
1872
  phase: "error",
@@ -1880,6 +1932,15 @@ var TopoloClient = class _TopoloClient {
1880
1932
  { ...errorContext, code: extractTopoloServiceErrorCode(parsed) ?? "permission_denied" }
1881
1933
  );
1882
1934
  }
1935
+ if (res.status === 429) {
1936
+ throw new TopoloRateLimitError(
1937
+ resolvedService.serviceKey,
1938
+ opts.path,
1939
+ parsed,
1940
+ message2,
1941
+ errorContext
1942
+ );
1943
+ }
1883
1944
  throw new TopoloHttpError(
1884
1945
  resolvedService.serviceKey,
1885
1946
  opts.path,
@@ -3825,8 +3886,8 @@ async function pollUntilApproved(params) {
3825
3886
  throw new Error("Device code expired. Run `topolo auth login` again.");
3826
3887
  }
3827
3888
 
3828
- // node_modules/.pnpm/@topolo+sdk@0.23.21/node_modules/@topolo/sdk/dist/node/credential-store.js
3829
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
3889
+ // node_modules/.pnpm/@topolo+sdk@0.23.22/node_modules/@topolo/sdk/dist/node/credential-store.js
3890
+ import { mkdir as mkdir2, readFile as readFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
3830
3891
  import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
3831
3892
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
3832
3893
  import { constants, fstatSync, readSync } from "fs";
@@ -3882,6 +3943,7 @@ var PUBLIC_ERROR_CODES2 = /* @__PURE__ */ new Set([
3882
3943
  "organization_enrollment_required",
3883
3944
  "permission_denied",
3884
3945
  "rate_limited",
3946
+ "reauth_required",
3885
3947
  "request_failed",
3886
3948
  "request_timeout",
3887
3949
  "service_catalog_unavailable",
@@ -4141,6 +4203,30 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
4141
4203
  );
4142
4204
  }
4143
4205
  };
4206
+ function topoloRetryAfterSeconds2(value, now = Date.now()) {
4207
+ if (!value) return void 0;
4208
+ if (/^\d{1,10}$/.test(value)) return Number(value);
4209
+ const at = Date.parse(value);
4210
+ return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - now) / 1e3));
4211
+ }
4212
+ var TopoloRateLimitError2 = class extends TopoloHttpError2 {
4213
+ retryable = true;
4214
+ retryAfterSeconds;
4215
+ constructor(service, path, body, message2, options = {}) {
4216
+ super(service, path, 429, body, message2, options);
4217
+ this.name = "TopoloRateLimitError";
4218
+ if (this.code === "http_error") this.code = "rate_limited";
4219
+ this.retryAfterSeconds = topoloRetryAfterSeconds2(this.retryAfter);
4220
+ }
4221
+ };
4222
+ var TopoloReauthRequiredError = class extends TopoloAuthError2 {
4223
+ reauthorization;
4224
+ constructor(message2, reauthorization, options = {}) {
4225
+ super(message2, "reauth_required", { status: 401, service: "auth", path: "/api/developer-oauth/token", ...options });
4226
+ this.name = "TopoloReauthRequiredError";
4227
+ this.reauthorization = Object.freeze({ ...reauthorization });
4228
+ }
4229
+ };
4144
4230
  var APP_API_URLS2 = {
4145
4231
  auth: "https://auth.topolo.app",
4146
4232
  developers: "https://developers.topolo.app"
@@ -4589,7 +4675,9 @@ async function readConfig() {
4589
4675
  async function writeConfig(config) {
4590
4676
  const path = configPath();
4591
4677
  await mkdir2(dirname2(path), { recursive: true });
4592
- await writeFile2(path, JSON.stringify(config, null, 2) + "\n", "utf8");
4678
+ await withConfigWriteLock(async (_assertOwned, persist) => {
4679
+ await persist((current) => protectStoredGrants(config, current), { allowCreate: true });
4680
+ });
4593
4681
  if (platform() !== "win32") {
4594
4682
  try {
4595
4683
  await chmod2(path, 384);
@@ -4597,6 +4685,142 @@ async function writeConfig(config) {
4597
4685
  }
4598
4686
  }
4599
4687
  }
4688
+ var DEFAULT_SLOT = "@default";
4689
+ var RETIRED_MAX_ENTRIES = 512;
4690
+ var RETIRED_RETENTION_SECONDS = 30 * 24 * 3600;
4691
+ var TERMINAL_RENEWAL_CODES = /* @__PURE__ */ new Set(["invalid_grant", "expired_token"]);
4692
+ function refreshTokenFingerprint(refreshToken) {
4693
+ return createHash2("sha256").update("topolo-refresh-token:v1:" + refreshToken).digest("hex").slice(0, 32);
4694
+ }
4695
+ function clone(value) {
4696
+ return value === void 0 ? value : JSON.parse(JSON.stringify(value));
4697
+ }
4698
+ function nowSeconds() {
4699
+ return Math.floor(Date.now() / 1e3);
4700
+ }
4701
+ function retiredEntry(config, refreshToken) {
4702
+ if (!refreshToken) return void 0;
4703
+ const entry = config.retiredRefreshTokens?.[refreshTokenFingerprint(refreshToken)];
4704
+ return entry && typeof entry === "object" ? entry : void 0;
4705
+ }
4706
+ function retire(config, refreshToken, reason) {
4707
+ const now = nowSeconds();
4708
+ const entries = Object.entries({ ...config.retiredRefreshTokens ?? {}, [refreshTokenFingerprint(refreshToken)]: { reason, at: now } }).filter(([key, value]) => /^[a-f0-9]{32}$/.test(key) && value && Number.isFinite(value.at) && value.at > now - RETIRED_RETENTION_SECONDS).sort(([, a], [, b]) => b.at - a.at).slice(0, RETIRED_MAX_ENTRIES);
4709
+ config.retiredRefreshTokens = Object.fromEntries(entries);
4710
+ }
4711
+ function environmentSlots(config, environment) {
4712
+ const slots = [];
4713
+ const profile = config.profiles?.[environment];
4714
+ if (environment === "production" && (config.credential || !profile)) {
4715
+ slots.push({ holder: config, key: profile?.activeContext ?? DEFAULT_SLOT, path: "credential" });
4716
+ }
4717
+ if (profile) {
4718
+ slots.push({ holder: profile, key: profile.activeContext ?? DEFAULT_SLOT, path: `profiles.${environment}.credential` });
4719
+ for (const [key, context] of Object.entries(profile.contexts ?? {})) {
4720
+ if (context) slots.push({ holder: context, key, path: `profiles.${environment}.contexts.${key}.credential` });
4721
+ }
4722
+ }
4723
+ return slots;
4724
+ }
4725
+ function allSlots(config) {
4726
+ const environments = /* @__PURE__ */ new Set(["production", ...Object.keys(config.profiles ?? {})]);
4727
+ return [...environments].flatMap((environment) => environmentSlots(config, environment));
4728
+ }
4729
+ function slotRefreshToken(slot) {
4730
+ const credential = slot.holder.credential;
4731
+ return credential?.kind === "access_token" ? credential.refreshToken : void 0;
4732
+ }
4733
+ function replaceGrant(config, environment, refreshToken, replacement) {
4734
+ const touched = /* @__PURE__ */ new Set();
4735
+ for (const slot of environmentSlots(config, environment)) {
4736
+ if (slotRefreshToken(slot) !== refreshToken) continue;
4737
+ if (replacement) slot.holder.credential = clone(replacement);
4738
+ else delete slot.holder.credential;
4739
+ touched.add(slot.key);
4740
+ }
4741
+ return touched;
4742
+ }
4743
+ function usableCredential(config, credential) {
4744
+ if (!credential) return false;
4745
+ if (credential.kind !== "access_token" || !credential.refreshToken) return true;
4746
+ return !retiredEntry(config, credential.refreshToken);
4747
+ }
4748
+ function slotCredential(profile, key) {
4749
+ if (key === DEFAULT_SLOT) return profile.credential;
4750
+ return profile.contexts?.[key]?.credential ?? (profile.activeContext === key ? profile.credential : void 0);
4751
+ }
4752
+ function protectStoredGrants(next, current) {
4753
+ const result = clone(next) ?? {};
4754
+ const retired = current.retiredRefreshTokens;
4755
+ const currentSlots = new Map(allSlots(current).map((slot) => [slot.path, slot.holder.credential]));
4756
+ for (const slot of allSlots(result)) {
4757
+ const token = slotRefreshToken(slot);
4758
+ if (!token || !retired?.[refreshTokenFingerprint(token)]) continue;
4759
+ const stored = currentSlots.get(slot.path);
4760
+ if (stored) slot.holder.credential = clone(stored);
4761
+ else delete slot.holder.credential;
4762
+ }
4763
+ if (retired) result.retiredRefreshTokens = clone(retired);
4764
+ else delete result.retiredRefreshTokens;
4765
+ for (const environment of /* @__PURE__ */ new Set([...Object.keys(result.profiles ?? {}), ...Object.keys(current.profiles ?? {})])) {
4766
+ const stored = current.profiles?.[environment]?.reauthorization;
4767
+ const profile = result.profiles?.[environment];
4768
+ if (!profile) continue;
4769
+ const kept = Object.fromEntries(Object.entries(stored ?? {}).filter(([key]) => !usableCredential(result, slotCredential(profile, key))));
4770
+ if (Object.keys(kept).length) profile.reauthorization = clone(kept);
4771
+ else delete profile.reauthorization;
4772
+ }
4773
+ return result;
4774
+ }
4775
+ function setReauthorization(config, environment, key, update) {
4776
+ const profiles = config.profiles ??= {};
4777
+ const profile = profiles[environment] ??= {};
4778
+ const map = { ...profile.reauthorization ?? {} };
4779
+ const value = update(map[key]);
4780
+ if (value) map[key] = value;
4781
+ else delete map[key];
4782
+ if (Object.keys(map).length) profile.reauthorization = map;
4783
+ else delete profile.reauthorization;
4784
+ }
4785
+ function loginCommand(environment, key) {
4786
+ const base = environment === "production" ? "topolo auth login" : `topolo --env ${environment} auth login`;
4787
+ if (!key.startsWith("org:")) return base;
4788
+ const use = environment === "production" ? "topolo context use" : `topolo --env ${environment} context use`;
4789
+ return `${base} && ${use} ${key.slice(4)}`;
4790
+ }
4791
+ function contextLabel(key) {
4792
+ return key === DEFAULT_SLOT ? "default" : key;
4793
+ }
4794
+ function reauthRequired(environment, key, entry, state) {
4795
+ const pending = entry?.pending;
4796
+ const command = loginCommand(environment, key);
4797
+ const reason = entry?.reason ?? "invalid_grant";
4798
+ const head = `The saved Topolo sign-in for ${environment} context ${contextLabel(key)} has ended (${reason}) and is not renewed again.`;
4799
+ let action;
4800
+ if (state === "awaiting_approval" && pending) {
4801
+ action = ` Approve the new sign-in: open ${pending.verificationUriComplete ?? pending.verificationUri} and confirm code ${pending.userCode} (expires ${new Date(pending.expiresAt * 1e3).toISOString()}). The next call resumes once approved.`;
4802
+ } else if (state === "organization_mismatch") {
4803
+ action = ` The last approval was for ${entry?.mismatch?.approvedContext ?? "another context"}, not ${contextLabel(key)}; approve again with that organization selected.`;
4804
+ } else if (state === "denied") {
4805
+ action = " The last approval was denied.";
4806
+ } else {
4807
+ action = "";
4808
+ }
4809
+ return new TopoloReauthRequiredError(`${head}${action} Or sign in again: ${command} (context ${contextLabel(key)}).`, {
4810
+ environment,
4811
+ context: contextLabel(key),
4812
+ reason,
4813
+ state,
4814
+ ...state === "awaiting_approval" && pending ? {
4815
+ verificationUri: pending.verificationUri,
4816
+ ...pending.verificationUriComplete ? { verificationUriComplete: pending.verificationUriComplete } : {},
4817
+ userCode: pending.userCode,
4818
+ expiresAt: new Date(pending.expiresAt * 1e3).toISOString(),
4819
+ pollIntervalSeconds: pending.interval
4820
+ } : {},
4821
+ loginCommand: command
4822
+ });
4823
+ }
4600
4824
  function normalizeEnvironment(value) {
4601
4825
  const normalized = (value ?? "production").trim().toLowerCase();
4602
4826
  if (normalized === "" || normalized === "production" || normalized === "prod") {
@@ -4694,7 +4918,19 @@ async function resolveCredential(environment = selectedEnvironment()) {
4694
4918
  }
4695
4919
  var REFRESH_SKEW_SECONDS = 60;
4696
4920
  var CONFIG_LOCK_RETRY_MS = 25;
4697
- var CONFIG_LOCK_TIMEOUT_MS = 5e3;
4921
+ var CONFIG_LOCK_DEFAULT_TIMEOUT_MS = 15e3;
4922
+ var AUTH_EXCHANGE_TIMEOUT_MS = 1e4;
4923
+ function configLockTimeoutMs() {
4924
+ const value = Number(process.env.TOPOLO_CONFIG_LOCK_TIMEOUT_MS);
4925
+ return Number.isFinite(value) && value >= 1e3 && value <= 12e4 ? value : CONFIG_LOCK_DEFAULT_TIMEOUT_MS;
4926
+ }
4927
+ function boundedOAuth(serviceUrls) {
4928
+ const fetchImpl = (input2, init) => globalThis.fetch(input2, {
4929
+ ...init,
4930
+ signal: init?.signal ?? AbortSignal.timeout(AUTH_EXCHANGE_TIMEOUT_MS)
4931
+ });
4932
+ return new TopoloOAuth2({ serviceUrls, fetch: fetchImpl });
4933
+ }
4698
4934
  var CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS = 2e3;
4699
4935
  var ConfigOwnershipError = class extends TopoloSdkError2 {
4700
4936
  constructor() {
@@ -4822,6 +5058,7 @@ function reclaimDeadOwnerLock(lockPath, markerName, marker) {
4822
5058
  return reclaimed;
4823
5059
  }
4824
5060
  var RELEASE_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
5061
+ var PERSIST_ATTEMPTS = 3;
4825
5062
  async function withConfigWriteLock(operation) {
4826
5063
  const lockPath = `${configPath()}.lock`;
4827
5064
  await mkdir2(dirname2(lockPath), { recursive: true });
@@ -4842,7 +5079,7 @@ async function withConfigWriteLock(operation) {
4842
5079
  } catch (error) {
4843
5080
  if (error.code !== "EEXIST") throw error;
4844
5081
  if (reclaimDeadOwnerLock(lockPath, markerName, marker)) break;
4845
- if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
5082
+ if (performance.now() - startedAt >= configLockTimeoutMs()) {
4846
5083
  throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
4847
5084
  }
4848
5085
  await new Promise((resolve22) => setTimeout(resolve22, CONFIG_LOCK_RETRY_MS));
@@ -4881,31 +5118,54 @@ async function withConfigWriteLock(operation) {
4881
5118
  throw new ConfigOwnershipError();
4882
5119
  }
4883
5120
  };
4884
- const persist = async (update) => {
5121
+ const persist = async (update, options = {}) => {
4885
5122
  const path = configPath();
4886
5123
  await mkdir2(dirname2(path), { recursive: true });
4887
- const temporary = join3(dirname2(path), ".credential-write-" + randomUUID2());
4888
- let temporaryOwned = false;
4889
- try {
4890
- assertOwnedSync();
4891
- const entry = lstatSync(path);
5124
+ const snapshot = () => {
5125
+ let entry;
5126
+ try {
5127
+ entry = lstatSync(path);
5128
+ } catch (error) {
5129
+ if (options.allowCreate && error.code === "ENOENT") return null;
5130
+ throw new ConfigOwnershipError();
5131
+ }
4892
5132
  if (!entry.isFile() || entry.isSymbolicLink()) throw new ConfigOwnershipError();
4893
- const before = readFileSync2(path, "utf8");
4894
- const updated = update(JSON.parse(before));
4895
- const fd = openSync(temporary, "wx", 384);
4896
- temporaryOwned = true;
5133
+ return readFileSync2(path, "utf8");
5134
+ };
5135
+ for (let attempt = 1; ; attempt += 1) {
5136
+ const temporary = join3(dirname2(path), ".credential-write-" + randomUUID2());
5137
+ let temporaryOwned = false;
4897
5138
  try {
4898
- writeFileSync(fd, JSON.stringify(updated, null, 2) + "\n");
4899
- fsyncSync(fd);
5139
+ assertOwnedSync();
5140
+ const before = snapshot();
5141
+ let current = {};
5142
+ if (before !== null) {
5143
+ try {
5144
+ current = JSON.parse(before);
5145
+ } catch (error) {
5146
+ if (!options.allowCreate) throw error;
5147
+ }
5148
+ }
5149
+ const updated = update(current);
5150
+ const fd = openSync(temporary, "wx", 384);
5151
+ temporaryOwned = true;
5152
+ try {
5153
+ writeFileSync(fd, JSON.stringify(updated, null, 2) + "\n");
5154
+ fsyncSync(fd);
5155
+ } finally {
5156
+ closeSync(fd);
5157
+ }
5158
+ assertOwnedSync();
5159
+ if (snapshot() !== before) {
5160
+ if (attempt < PERSIST_ATTEMPTS) continue;
5161
+ throw new ConfigOwnershipError();
5162
+ }
5163
+ renameSync(temporary, path);
5164
+ temporaryOwned = false;
5165
+ return;
4900
5166
  } finally {
4901
- closeSync(fd);
5167
+ if (temporaryOwned) unlinkSync(temporary);
4902
5168
  }
4903
- assertOwnedSync();
4904
- if (readFileSync2(path, "utf8") !== before) throw new ConfigOwnershipError();
4905
- renameSync(temporary, path);
4906
- temporaryOwned = false;
4907
- } finally {
4908
- if (temporaryOwned) unlinkSync(temporary);
4909
5169
  }
4910
5170
  };
4911
5171
  const release = () => {
@@ -4966,6 +5226,17 @@ async function withConfigWriteLock(operation) {
4966
5226
  function renewalFailure(error) {
4967
5227
  const path = "/api/developer-oauth/token";
4968
5228
  if (error instanceof ConfigOwnershipError) return new ConfigOwnershipError();
5229
+ if (error instanceof TopoloReauthRequiredError) return error;
5230
+ if ((error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) && error.status === 429) {
5231
+ const message2 = "Topolo sign-in renewal is temporarily rate limited by Auth. Retry after the Retry-After interval; the saved sign-in has been retained.";
5232
+ return new TopoloRateLimitError2(
5233
+ "auth",
5234
+ path,
5235
+ { code: "rate_limited", retryable: true, message: message2 },
5236
+ message2,
5237
+ { requestId: error.requestId, retryAfter: error.retryAfter }
5238
+ );
5239
+ }
4969
5240
  if (error instanceof TopoloAuthError2 && error.code === "invalid_grant") {
4970
5241
  return new TopoloAuthError2("The saved Topolo sign-in can no longer be renewed. Sign in again.", "invalid_grant", {
4971
5242
  status: error.status,
@@ -4990,120 +5261,266 @@ function renewalFailure(error) {
4990
5261
  "Topolo sign-in renewal could not reach Auth. Retry the request; the saved sign-in has been retained."
4991
5262
  );
4992
5263
  }
4993
- async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
5264
+ function isTerminalRefusal(error) {
5265
+ return error instanceof TopoloAuthError2 && TERMINAL_RENEWAL_CODES.has(error.code) && (error.status === 400 || error.status === 401);
5266
+ }
5267
+ function resolveTarget(profile, target) {
5268
+ if (target.kind === "active") {
5269
+ return {
5270
+ key: profile.activeContext ?? DEFAULT_SLOT,
5271
+ credential: profile.credential,
5272
+ oauthClientId: profile.oauthClientId,
5273
+ context: profile.activeContext ? profile.contexts?.[profile.activeContext] : void 0
5274
+ };
5275
+ }
5276
+ const context = profile.contexts?.[target.key];
5277
+ return {
5278
+ key: target.key,
5279
+ credential: context?.credential,
5280
+ oauthClientId: context?.oauthClientId ?? profile.oauthClientId,
5281
+ context
5282
+ };
5283
+ }
5284
+ function envCredentialConfigured(environment) {
4994
5285
  const prefix = envVarPrefix(environment);
4995
- if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
4996
- return resolveCredential(environment);
5286
+ return Boolean(process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN);
5287
+ }
5288
+ function authServiceUrls(profile, environment, purpose) {
5289
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
5290
+ if (environment === "local" && !serviceUrls?.auth) {
5291
+ throw new TopoloSdkError2("environment_service_url_required", `Local Topolo ${purpose} requires an explicit local Auth service origin.`);
4997
5292
  }
5293
+ return serviceUrls;
5294
+ }
5295
+ var renewalsInFlight = /* @__PURE__ */ new Map();
5296
+ function singleFlight(key, run) {
5297
+ const existing = renewalsInFlight.get(key);
5298
+ if (existing) return existing;
5299
+ const started = run().finally(() => {
5300
+ renewalsInFlight.delete(key);
5301
+ });
5302
+ renewalsInFlight.set(key, started);
5303
+ return started;
5304
+ }
5305
+ async function renewTarget(environment, target, options) {
4998
5306
  const observedProfile = profileForEnvironment(await readConfig(), environment);
4999
- const observed = observedProfile.credential;
5000
- if (!observed || observed.kind !== "access_token" || !observed.refreshToken || !observedProfile.oauthClientId || !options.force && observed.expiresAt && observed.expiresAt - REFRESH_SKEW_SECONDS > Math.floor(Date.now() / 1e3)) return observed ?? null;
5001
- return withConfigWriteLock(async (_assertOwned, persist) => {
5002
- const config = await readConfig();
5003
- const profile = profileForEnvironment(config, environment);
5004
- const cred = profile.credential;
5005
- if (!cred || cred.kind !== "access_token") return cred ?? null;
5006
- if (!cred.refreshToken || !profile.oauthClientId) return cred;
5007
- const now = Math.floor(Date.now() / 1e3);
5008
- if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken && cred.expiresAt !== void 0 && cred.expiresAt > now) {
5009
- return cred;
5010
- }
5011
- if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
5012
- const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
5013
- if (environment === "local" && !serviceUrls?.auth) {
5014
- throw new TopoloSdkError2(
5015
- "environment_service_url_required",
5016
- "Local Topolo credential refresh requires an explicit local Auth service origin."
5017
- );
5018
- }
5019
- try {
5020
- const oauth = new TopoloOAuth2({
5021
- serviceUrls
5022
- });
5023
- const tokens = await oauth.refreshToken({
5024
- clientId: profile.oauthClientId,
5025
- refreshToken: cred.refreshToken
5026
- });
5307
+ const observedTarget = resolveTarget(observedProfile, target);
5308
+ const observed = observedTarget.credential;
5309
+ if (!observed) {
5310
+ return observedProfile.reauthorization?.[observedTarget.key] ? continueReauthorization(environment, observedTarget.key) : null;
5311
+ }
5312
+ if (observed.kind !== "access_token" || !observed.refreshToken || !observedTarget.oauthClientId || !options.force && observed.expiresAt && observed.expiresAt - REFRESH_SKEW_SECONDS > nowSeconds()) return observed;
5313
+ const flightKey = JSON.stringify([environment, target.kind === "active" ? "@active" : target.key]);
5314
+ const run = async () => {
5315
+ const outcome = await withConfigWriteLock(async (_assertOwned, persist) => {
5316
+ const config = await readConfig();
5317
+ const profile = profileForEnvironment(config, environment);
5318
+ const resolved = resolveTarget(profile, target);
5319
+ const credential = resolved.credential;
5320
+ if (!credential) return profile.reauthorization?.[resolved.key] ? { reauthorize: resolved.key } : { credential: null };
5321
+ if (credential.kind !== "access_token" || !credential.refreshToken || !resolved.oauthClientId) return { credential };
5322
+ const now = nowSeconds();
5323
+ if (options.force && observed.kind === "access_token" && observed.accessToken !== credential.accessToken && credential.expiresAt !== void 0 && credential.expiresAt > now) return { credential };
5324
+ if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return { credential };
5325
+ const sent = credential.refreshToken;
5326
+ const endGrant = async (reason) => {
5327
+ await persist((current) => {
5328
+ const next = clone(current);
5329
+ const touched = replaceGrant(next, environment, sent, null);
5330
+ touched.add(resolved.key);
5331
+ retire(next, sent, reason);
5332
+ for (const key of touched) {
5333
+ setReauthorization(next, environment, key, (existing) => ({ ...existing, reason, since: existing?.since ?? nowSeconds() }));
5334
+ }
5335
+ return next;
5336
+ });
5337
+ return { reauthorize: resolved.key };
5338
+ };
5339
+ const retired = retiredEntry(config, sent);
5340
+ if (retired) {
5341
+ return endGrant(retired.reason === "rotated" ? "rotated_grant_lost" : retired.reason);
5342
+ }
5343
+ const serviceUrls = authServiceUrls(profile, environment, target.kind === "active" ? "credential refresh" : "context refresh");
5344
+ let tokens;
5345
+ try {
5346
+ tokens = await boundedOAuth(serviceUrls).refreshToken({ clientId: resolved.oauthClientId, refreshToken: sent });
5347
+ } catch (error) {
5348
+ if (isTerminalRefusal(error)) return endGrant(error.code);
5349
+ throw renewalFailure(error);
5350
+ }
5027
5351
  const refreshed = {
5028
5352
  kind: "access_token",
5029
5353
  accessToken: tokens.access_token,
5030
- ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
5031
- expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
5354
+ refreshToken: tokens.refresh_token ?? sent,
5355
+ expiresAt: nowSeconds() + tokens.expires_in
5032
5356
  };
5033
- await persist((current) => {
5034
- const latest = profileForEnvironment(current, environment);
5035
- const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
5036
- if (!same(latest.credential, cred) || latest.oauthClientId !== profile.oauthClientId || latest.activeContext !== profile.activeContext)
5037
- throw new ConfigOwnershipError();
5038
- const activeContext = latest.activeContext;
5039
- const activeEntry = activeContext ? latest.contexts?.[activeContext] : void 0;
5040
- const previousEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
5041
- if (!same(activeEntry?.credential, previousEntry?.credential)) throw new ConfigOwnershipError();
5042
- return activeContext && activeEntry ? writeProfileConfig(current, environment, {
5043
- credential: refreshed,
5044
- contexts: { ...latest.contexts, [activeContext]: {
5045
- ...activeEntry,
5046
- credential: refreshed,
5047
- oauthClientId: activeEntry.oauthClientId ?? latest.oauthClientId
5048
- } }
5049
- }) : environment === "production" && !current.profiles?.production ? { ...current, credential: refreshed } : writeProfileConfig(current, environment, { credential: refreshed });
5050
- });
5051
- return refreshed;
5052
- } catch (error) {
5053
- throw renewalFailure(error);
5054
- }
5055
- });
5357
+ try {
5358
+ await persist((current) => {
5359
+ const next = clone(current);
5360
+ replaceGrant(next, environment, sent, refreshed);
5361
+ if (tokens.refresh_token && tokens.refresh_token !== sent) retire(next, sent, "rotated");
5362
+ return next;
5363
+ });
5364
+ } catch (error) {
5365
+ throw renewalFailure(error);
5366
+ }
5367
+ return { credential: refreshed };
5368
+ });
5369
+ if ("reauthorize" in outcome) return continueReauthorization(environment, outcome.reauthorize);
5370
+ return outcome.credential;
5371
+ };
5372
+ return options.force ? run() : singleFlight(flightKey, run);
5373
+ }
5374
+ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
5375
+ if (envCredentialConfigured(environment)) return resolveCredential(environment);
5376
+ return renewTarget(environment, { kind: "active" }, options);
5056
5377
  }
5057
5378
  async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
5058
- const observedProfile = profileForEnvironment(await readConfig(), environment);
5059
- const observedContext = observedProfile.contexts?.[key];
5060
- const observed = observedContext?.credential;
5061
- if (!observed || observed.kind !== "access_token" || !observed.refreshToken || !(observedContext.oauthClientId ?? observedProfile.oauthClientId) || !options.force && observed.expiresAt && observed.expiresAt - REFRESH_SKEW_SECONDS > Math.floor(Date.now() / 1e3)) return observed ?? null;
5379
+ return renewTarget(environment, { kind: "context", key }, options);
5380
+ }
5381
+ function decodeTokenOrganization(accessToken) {
5382
+ try {
5383
+ const payload = accessToken.split(".")[1];
5384
+ if (!payload) return { known: false, organizationId: null };
5385
+ const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
5386
+ const org = claims.orgId ?? claims.organizationId ?? claims.org_id ?? null;
5387
+ return { known: true, organizationId: typeof org === "string" && org ? org : null };
5388
+ } catch {
5389
+ return { known: false, organizationId: null };
5390
+ }
5391
+ }
5392
+ async function continueReauthorization(environment, key) {
5062
5393
  return withConfigWriteLock(async (_assertOwned, persist) => {
5063
5394
  const config = await readConfig();
5064
5395
  const profile = profileForEnvironment(config, environment);
5065
- const context = profile.contexts?.[key];
5066
- const credential = context?.credential;
5067
- if (!credential || credential.kind !== "access_token") return credential ?? null;
5068
- const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
5069
- if (!credential.refreshToken || !oauthClientId) return credential;
5070
- const now = Math.floor(Date.now() / 1e3);
5071
- if (options.force && observed.accessToken !== credential.accessToken && credential.expiresAt !== void 0 && credential.expiresAt > now) return credential;
5072
- if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
5073
- const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
5074
- if (environment === "local" && !serviceUrls?.auth) {
5075
- throw new TopoloSdkError2(
5076
- "environment_service_url_required",
5077
- "Local Topolo context refresh requires an explicit local Auth service origin."
5078
- );
5396
+ const credential = slotCredential(profile, key);
5397
+ if (credential && usableCredential(config, credential)) return credential;
5398
+ const entry = profile.reauthorization?.[key];
5399
+ if (!entry) return credential ?? null;
5400
+ const clientId = (key === DEFAULT_SLOT ? void 0 : profile.contexts?.[key]?.oauthClientId) ?? profile.oauthClientId;
5401
+ if (!clientId) throw reauthRequired(environment, key, entry, "unavailable");
5402
+ const serviceUrls = authServiceUrls(profile, environment, "sign-in renewal");
5403
+ const oauth = boundedOAuth(serviceUrls);
5404
+ const now = nowSeconds();
5405
+ let state = clone(entry);
5406
+ const save = async (update) => {
5407
+ await persist((current) => {
5408
+ const next = clone(current);
5409
+ setReauthorization(next, environment, key, update);
5410
+ return next;
5411
+ });
5412
+ };
5413
+ let pending = state.pending && state.pending.clientId === clientId && state.pending.expiresAt > now ? state.pending : void 0;
5414
+ if (pending && !pending.denied && pending.nextPollAt <= now) {
5415
+ let tokens;
5416
+ try {
5417
+ tokens = await oauth.pollDeviceToken({ clientId, deviceCode: pending.deviceCode });
5418
+ } catch (error) {
5419
+ const code = error instanceof TopoloAuthError2 ? error.code : void 0;
5420
+ if (code === "authorization_pending") {
5421
+ pending = { ...pending, nextPollAt: now + pending.interval };
5422
+ } else if (code === "slow_down") {
5423
+ const interval = pending.interval + 5;
5424
+ const retryAfter = Number(error.retryAfter);
5425
+ pending = { ...pending, interval, nextPollAt: now + Math.max(interval, Number.isFinite(retryAfter) ? retryAfter : 0) };
5426
+ } else if (code === "access_denied") {
5427
+ pending = { ...pending, denied: true };
5428
+ } else if (code === "expired_token" || code === "invalid_grant") {
5429
+ pending = void 0;
5430
+ } else {
5431
+ const retryAfter = Number(error.retryAfter);
5432
+ pending = { ...pending, nextPollAt: now + Math.max(pending.interval * 2, Number.isFinite(retryAfter) ? retryAfter : 0) };
5433
+ }
5434
+ state = { ...state, pending };
5435
+ if (!pending) delete state.pending;
5436
+ await save(() => state);
5437
+ if (pending) throw reauthRequired(environment, key, state, pending.denied ? "denied" : "awaiting_approval");
5438
+ }
5439
+ if (tokens) {
5440
+ const approved = {
5441
+ kind: "access_token",
5442
+ accessToken: tokens.access_token,
5443
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
5444
+ expiresAt: nowSeconds() + tokens.expires_in
5445
+ };
5446
+ const organization = decodeTokenOrganization(tokens.access_token);
5447
+ const approvedKey = organization.known ? organization.organizationId ? `org:${organization.organizationId}` : "personal" : null;
5448
+ const matches = key === DEFAULT_SLOT || approvedKey === key;
5449
+ await persist((current) => {
5450
+ const next = clone(current);
5451
+ const nextProfile = (next.profiles ??= {})[environment] ??= {};
5452
+ const legacyProduction = environment === "production" && !current.profiles?.[environment]?.contexts && key === DEFAULT_SLOT && Boolean(current.credential);
5453
+ if (matches) {
5454
+ if (key === DEFAULT_SLOT) {
5455
+ if (legacyProduction) next.credential = approved;
5456
+ else nextProfile.credential = approved;
5457
+ } else {
5458
+ const existing = nextProfile.contexts?.[key];
5459
+ nextProfile.contexts = { ...nextProfile.contexts ?? {}, [key]: {
5460
+ ...existing ?? (key.startsWith("org:") ? { contextType: "organization", organizationId: key.slice(4) } : { contextType: "personal" }),
5461
+ credential: approved,
5462
+ oauthClientId: clientId
5463
+ } };
5464
+ if (nextProfile.activeContext === key) nextProfile.credential = approved;
5465
+ }
5466
+ setReauthorization(next, environment, key, () => void 0);
5467
+ } else {
5468
+ if (approvedKey && !(approvedKey in (nextProfile.reauthorization ?? {}))) {
5469
+ const existing = nextProfile.contexts?.[approvedKey];
5470
+ nextProfile.contexts = { ...nextProfile.contexts ?? {}, [approvedKey]: {
5471
+ ...existing ?? (approvedKey.startsWith("org:") ? { contextType: "organization", organizationId: approvedKey.slice(4) } : { contextType: "personal" }),
5472
+ credential: approved,
5473
+ oauthClientId: clientId
5474
+ } };
5475
+ if (nextProfile.activeContext === approvedKey) nextProfile.credential = approved;
5476
+ }
5477
+ setReauthorization(next, environment, key, (existing) => {
5478
+ const updated = {
5479
+ ...existing ?? state,
5480
+ mismatch: { approvedContext: approvedKey ?? "an unverifiable context", at: nowSeconds() }
5481
+ };
5482
+ delete updated.pending;
5483
+ return updated;
5484
+ });
5485
+ }
5486
+ return next;
5487
+ });
5488
+ if (matches) return approved;
5489
+ const latest = profileForEnvironment(await readConfig(), environment).reauthorization?.[key];
5490
+ throw reauthRequired(environment, key, latest ?? state, "organization_mismatch");
5491
+ }
5492
+ }
5493
+ if (pending) {
5494
+ throw reauthRequired(environment, key, state, pending.denied ? "denied" : "awaiting_approval");
5495
+ }
5496
+ if (state.startRetryAt && state.startRetryAt > now) {
5497
+ throw reauthRequired(environment, key, state, state.mismatch ? "organization_mismatch" : "unavailable");
5079
5498
  }
5080
5499
  try {
5081
- const oauth = new TopoloOAuth2({ serviceUrls });
5082
- const tokens = await oauth.refreshToken({
5083
- clientId: oauthClientId,
5084
- refreshToken: credential.refreshToken
5085
- });
5086
- const refreshed = {
5087
- kind: "access_token",
5088
- accessToken: tokens.access_token,
5089
- ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
5090
- expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
5500
+ const started = await oauth.requestDeviceCode({ clientId });
5501
+ const interval = Math.max(1, Math.floor(Number(started.interval) || 5));
5502
+ const startedPending = {
5503
+ clientId,
5504
+ deviceCode: started.device_code,
5505
+ userCode: started.user_code,
5506
+ verificationUri: started.verification_uri,
5507
+ ...started.verification_uri_complete ? { verificationUriComplete: started.verification_uri_complete } : {},
5508
+ startedAt: now,
5509
+ expiresAt: now + Math.max(1, Math.floor(Number(started.expires_in) || 600)),
5510
+ interval,
5511
+ nextPollAt: now + interval
5091
5512
  };
5092
- await persist((current) => {
5093
- const latest = profileForEnvironment(current, environment);
5094
- const latestContext = latest.contexts?.[key];
5095
- if (!latestContext || JSON.stringify(latestContext.credential) !== JSON.stringify(credential) || (latestContext.oauthClientId ?? latest.oauthClientId) !== oauthClientId || latestContext.contextType !== context.contextType || latestContext.organizationId !== context.organizationId)
5096
- throw new ConfigOwnershipError();
5097
- if (latest.activeContext === key && JSON.stringify(latest.credential) !== JSON.stringify(profile.activeContext === key ? profile.credential : credential))
5098
- throw new ConfigOwnershipError();
5099
- return writeProfileConfig(current, environment, {
5100
- ...latest.activeContext === key ? { credential: refreshed } : {},
5101
- contexts: { ...latest.contexts, [key]: { ...latestContext, credential: refreshed, oauthClientId } }
5102
- });
5103
- });
5104
- return refreshed;
5513
+ state = { ...state, pending: startedPending };
5514
+ delete state.startRetryAt;
5515
+ await save(() => state);
5516
+ throw reauthRequired(environment, key, state, "awaiting_approval");
5105
5517
  } catch (error) {
5106
- throw renewalFailure(error);
5518
+ if (error instanceof TopoloReauthRequiredError || error instanceof ConfigOwnershipError) throw error;
5519
+ const retryAfter = Number(error.retryAfter);
5520
+ state = { ...state, startRetryAt: now + Math.max(30, Number.isFinite(retryAfter) ? retryAfter : 0) };
5521
+ delete state.pending;
5522
+ await save(() => state);
5523
+ throw reauthRequired(environment, key, state, "unavailable");
5107
5524
  }
5108
5525
  });
5109
5526
  }
@@ -7079,7 +7496,7 @@ function message(error) {
7079
7496
 
7080
7497
  // src/commands/agent-config.ts
7081
7498
  import { existsSync as existsSync5 } from "fs";
7082
- import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as writeFile3 } from "fs/promises";
7499
+ import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as writeFile2 } from "fs/promises";
7083
7500
  import { randomUUID as randomUUID3 } from "crypto";
7084
7501
  import { dirname as dirname5 } from "path";
7085
7502
  async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
@@ -7315,7 +7732,7 @@ async function atomicWrite(path, contents, before) {
7315
7732
  const created = await handle.stat();
7316
7733
  temporaryIdentity = [created.dev, created.ino].join(":");
7317
7734
  await assertSnapshot(path, destination);
7318
- await writeFile3(handle, contents, { encoding: "utf8" });
7735
+ await writeFile2(handle, contents, { encoding: "utf8" });
7319
7736
  await handle.close();
7320
7737
  handleClosed = true;
7321
7738
  await assertSnapshot(path, destination);