@n1creator/openacp-cli 2026.712.8 → 2026.712.9

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,111 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // src/core/security/network-redaction.ts
12
+ function sanitizeProxyDebugNamespaces(value) {
13
+ if (!value) return value;
14
+ const entries = value.split(",").map((item) => item.trim()).filter(Boolean);
15
+ const safeEntries = entries.filter((item) => item.startsWith("-") || !UNSAFE_PROXY_DEBUG.test(item));
16
+ if (safeEntries.some((item) => !item.startsWith("-") && item.includes("*"))) {
17
+ for (const exclusion of PROXY_DEBUG_EXCLUSIONS) if (!safeEntries.includes(exclusion)) safeEntries.push(exclusion);
18
+ }
19
+ const safe = safeEntries.join(",");
20
+ return safe || void 0;
21
+ }
22
+ function redactNetworkSecrets(input2) {
23
+ return input2.replace(/(https?:\/\/api\.telegram\.org\/bot)[^/?#\s]+/gi, "$1<redacted>").replace(/(\/bot)[0-9]+:[A-Za-z0-9_-]+/g, "$1<redacted>").replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1<redacted>@").replace(new RegExp(`([?&](?:${SENSITIVE_QUERY_KEYS})=)[^&#\\s]*`, "gi"), "$1<redacted>").replace(/\b(authorization|proxy-authorization)\s*[:=]\s*(?:Bearer|Basic)\s+[^\s,;}"']+/gi, "$1: <redacted>").replace(/\b(x-api-key|api-key|cookie|set-cookie)\s*[:=]\s*[^\r\n,;}]+/gi, "$1: <redacted>");
24
+ }
25
+ function normalizedKey(key) {
26
+ return key.toLowerCase().replace(/[^a-z0-9]/g, "");
27
+ }
28
+ function isSensitiveObjectKey(key) {
29
+ const normalized = normalizedKey(key);
30
+ return SENSITIVE_OBJECT_KEYS.has(normalized) || /(?:token|secret|password|passwd|apikey|authorization|cookie)$/.test(normalized);
31
+ }
32
+ function sanitizeNetworkLogValue(value, seen = /* @__PURE__ */ new WeakMap()) {
33
+ if (typeof value === "string") return redactNetworkSecrets(value);
34
+ if (value instanceof Error) {
35
+ const sanitized = new Error(redactNetworkSecrets(value.message));
36
+ sanitized.name = value.name;
37
+ if (value.stack) sanitized.stack = redactNetworkSecrets(value.stack);
38
+ for (const [key, nested] of Object.entries(value)) {
39
+ ;
40
+ sanitized[key] = isSensitiveObjectKey(key) ? "<redacted>" : sanitizeNetworkLogValue(nested, seen);
41
+ }
42
+ return sanitized;
43
+ }
44
+ if (!value || typeof value !== "object") return value;
45
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof Date) return value;
46
+ if (seen.has(value)) return seen.get(value);
47
+ if (Array.isArray(value)) {
48
+ const result2 = [];
49
+ seen.set(value, result2);
50
+ for (const item of value) result2.push(sanitizeNetworkLogValue(item, seen));
51
+ return result2;
52
+ }
53
+ const result = {};
54
+ seen.set(value, result);
55
+ for (const [key, nested] of Object.entries(value)) {
56
+ result[key] = isSensitiveObjectKey(key) ? "<redacted>" : sanitizeNetworkLogValue(nested, seen);
57
+ }
58
+ return result;
59
+ }
60
+ var SENSITIVE_QUERY_KEYS, UNSAFE_PROXY_DEBUG, PROXY_DEBUG_EXCLUSIONS, SENSITIVE_OBJECT_KEYS;
61
+ var init_network_redaction = __esm({
62
+ "src/core/security/network-redaction.ts"() {
63
+ "use strict";
64
+ SENSITIVE_QUERY_KEYS = [
65
+ "access_token",
66
+ "access-token",
67
+ "token",
68
+ "api_key",
69
+ "api-key",
70
+ "apikey",
71
+ "key",
72
+ "secret",
73
+ "password",
74
+ "passwd",
75
+ "auth",
76
+ "authorization",
77
+ "signature",
78
+ "sig",
79
+ "bot_token",
80
+ "bot-token"
81
+ ].join("|");
82
+ UNSAFE_PROXY_DEBUG = /^\*?(?:proxy-agent|proxy|https-proxy-agent|http-proxy-agent|socks-proxy-agent)(?::|\*|$)/i;
83
+ PROXY_DEBUG_EXCLUSIONS = [
84
+ "-proxy",
85
+ "-proxy:*",
86
+ "-proxy-agent",
87
+ "-proxy-agent:*",
88
+ "-https-proxy-agent",
89
+ "-https-proxy-agent:*",
90
+ "-http-proxy-agent",
91
+ "-http-proxy-agent:*",
92
+ "-socks-proxy-agent",
93
+ "-socks-proxy-agent:*"
94
+ ];
95
+ SENSITIVE_OBJECT_KEYS = /* @__PURE__ */ new Set([
96
+ "authorization",
97
+ "proxyauthorization",
98
+ "xapikey",
99
+ "apikey",
100
+ "token",
101
+ "xgoogapikey",
102
+ "xauthtoken",
103
+ "xtelegrambottoken",
104
+ "authentication",
105
+ "accesstoken",
106
+ "password",
107
+ "passwd",
108
+ "secret",
109
+ "bottoken",
110
+ "cookie",
111
+ "setcookie"
112
+ ]);
113
+ }
114
+ });
115
+
11
116
  // src/core/utils/log.ts
12
117
  var log_exports = {};
13
118
  __export(log_exports, {
@@ -124,7 +229,7 @@ function initLogger(config) {
124
229
  ]
125
230
  });
126
231
  currentTransport = transports;
127
- rootLogger = pino({ level: config.level }, transports);
232
+ rootLogger = pino({ level: config.level, hooks: networkSecurityHooks }, transports);
128
233
  initialized = true;
129
234
  Object.assign(log, wrapVariadic(rootLogger));
130
235
  return rootLogger;
@@ -135,7 +240,7 @@ function setLogLevel(level) {
135
240
  function createChildLogger(context) {
136
241
  return new Proxy({}, {
137
242
  get(_target, prop, receiver) {
138
- const child = rootLogger.child(context);
243
+ const child = rootLogger.child(sanitizeNetworkLogValue(context));
139
244
  const value = Reflect.get(child, prop, receiver);
140
245
  return typeof value === "function" ? value.bind(child) : value;
141
246
  }
@@ -149,7 +254,7 @@ function createSessionLogger(sessionId, parentLogger) {
149
254
  try {
150
255
  const sessionLogPath = path.join(sessionLogDir, `${sessionId}.log`);
151
256
  const dest = pino.destination(sessionLogPath);
152
- const sessionFileLogger = pino({ level: parentLogger.level }, dest).child({ sessionId });
257
+ const sessionFileLogger = pino({ level: parentLogger.level, hooks: networkSecurityHooks }, dest).child({ sessionId });
153
258
  const combinedChild = parentLogger.child({ sessionId });
154
259
  const originalInfo = combinedChild.info.bind(combinedChild);
155
260
  const originalWarn = combinedChild.warn.bind(combinedChild);
@@ -183,16 +288,34 @@ function createSessionLogger(sessionId, parentLogger) {
183
288
  return parentLogger.child({ sessionId });
184
289
  }
185
290
  }
186
- function closeSessionLogger(logger) {
291
+ async function closeSessionLogger(logger) {
187
292
  const dest = logger.__sessionDest;
188
- if (dest && typeof dest.destroy === "function") {
189
- dest.destroy();
190
- }
293
+ if (!dest) return;
294
+ await new Promise((resolve7) => {
295
+ let settled = false;
296
+ const done = () => {
297
+ if (!settled) {
298
+ settled = true;
299
+ resolve7();
300
+ }
301
+ };
302
+ const timeout = setTimeout(done, 3e3);
303
+ dest.once?.("close", () => {
304
+ clearTimeout(timeout);
305
+ done();
306
+ });
307
+ if (typeof dest.end === "function") dest.end();
308
+ else {
309
+ dest.destroy?.();
310
+ clearTimeout(timeout);
311
+ done();
312
+ }
313
+ });
191
314
  }
192
315
  async function shutdownLogger() {
193
316
  if (!initialized) return;
194
317
  const transport = currentTransport;
195
- rootLogger = pino({ level: "debug" });
318
+ rootLogger = pino({ level: "debug", hooks: networkSecurityHooks });
196
319
  Object.assign(log, wrapVariadic(rootLogger));
197
320
  currentTransport = void 0;
198
321
  logDir = void 0;
@@ -229,12 +352,19 @@ async function cleanupOldSessionLogs(retentionDays) {
229
352
  } catch {
230
353
  }
231
354
  }
232
- var rootLogger, initialized, logDir, currentTransport, log, muteCount, savedLevel;
355
+ var networkSecurityHooks, rootLogger, initialized, logDir, currentTransport, log, muteCount, savedLevel;
233
356
  var init_log = __esm({
234
357
  "src/core/utils/log.ts"() {
235
358
  "use strict";
359
+ init_network_redaction();
360
+ networkSecurityHooks = {
361
+ logMethod(inputArgs, method) {
362
+ method.apply(this, inputArgs.map((arg) => sanitizeNetworkLogValue(arg)));
363
+ }
364
+ };
236
365
  rootLogger = pino({
237
366
  level: "debug",
367
+ hooks: networkSecurityHooks,
238
368
  transport: { target: "pino-pretty", options: { colorize: true, translateTime: "SYS:standard", destination: 2 } }
239
369
  });
240
370
  initialized = false;
@@ -328,22 +458,22 @@ __export(config_registry_exports, {
328
458
  resolveOptions: () => resolveOptions,
329
459
  setFieldValueAsync: () => setFieldValueAsync
330
460
  });
331
- function getFieldDef(path36) {
332
- return CONFIG_REGISTRY.find((f) => f.path === path36);
461
+ function getFieldDef(path37) {
462
+ return CONFIG_REGISTRY.find((f) => f.path === path37);
333
463
  }
334
464
  function getSafeFields() {
335
465
  return CONFIG_REGISTRY.filter((f) => f.scope === "safe");
336
466
  }
337
- function isHotReloadable(path36) {
338
- const def = getFieldDef(path36);
467
+ function isHotReloadable(path37) {
468
+ const def = getFieldDef(path37);
339
469
  return def?.hotReload ?? false;
340
470
  }
341
471
  function resolveOptions(def, config) {
342
472
  if (!def.options) return void 0;
343
473
  return typeof def.options === "function" ? def.options(config) : def.options;
344
474
  }
345
- function getConfigValue(config, path36) {
346
- const parts = path36.split(".");
475
+ function getConfigValue(config, path37) {
476
+ const parts = path37.split(".");
347
477
  let current = config;
348
478
  for (const part of parts) {
349
479
  if (current && typeof current === "object" && part in current) {
@@ -1376,7 +1506,7 @@ function stripPythonPackageVersion(pkg) {
1376
1506
  const at = pkg.indexOf("@");
1377
1507
  return at === -1 ? pkg : pkg.slice(0, at);
1378
1508
  }
1379
- async function installAgent(agent, store, progress, agentsDir) {
1509
+ async function installAgent(agent, store, progress, agentsDir, scopedFetch = globalThis.fetch) {
1380
1510
  const agentKey = getAgentAlias(agent.id);
1381
1511
  await progress?.onStart(agent.id, agent.name);
1382
1512
  const dist = resolveDistribution(agent);
@@ -1396,7 +1526,7 @@ Install it with: pip install uv`;
1396
1526
  let binaryPath;
1397
1527
  if (dist.type === "binary") {
1398
1528
  try {
1399
- binaryPath = await downloadAndExtract(agent.id, dist.archive, progress, agentsDir);
1529
+ binaryPath = await downloadAndExtract(agent.id, dist.archive, progress, agentsDir, scopedFetch);
1400
1530
  } catch (err) {
1401
1531
  const msg = `Failed to download ${agent.name}. Please try again or install manually.`;
1402
1532
  await progress?.onError(msg);
@@ -1412,12 +1542,12 @@ Install it with: pip install uv`;
1412
1542
  await progress?.onSuccess(agent.name);
1413
1543
  return { ok: true, agentKey, setupSteps: setupSteps.length > 0 ? setupSteps : void 0 };
1414
1544
  }
1415
- async function downloadAndExtract(agentId, archiveUrl, progress, agentsDir) {
1545
+ async function downloadAndExtract(agentId, archiveUrl, progress, agentsDir, scopedFetch = globalThis.fetch) {
1416
1546
  const destDir = path10.join(agentsDir ?? DEFAULT_AGENTS_DIR, agentId);
1417
1547
  fs11.mkdirSync(destDir, { recursive: true });
1418
1548
  await progress?.onStep("Downloading...");
1419
1549
  log11.info({ agentId, url: archiveUrl }, "Downloading agent binary");
1420
- const response = await fetch(archiveUrl);
1550
+ const response = await scopedFetch(archiveUrl);
1421
1551
  if (!response.ok) {
1422
1552
  throw new Error(`Download failed: ${response.status} ${response.statusText}`);
1423
1553
  }
@@ -1437,20 +1567,39 @@ async function readResponseWithProgress(response, contentLength, progress) {
1437
1567
  const arrayBuffer = await response.arrayBuffer();
1438
1568
  return Buffer.from(arrayBuffer);
1439
1569
  }
1440
- const reader = response.body.getReader();
1441
1570
  const chunks = [];
1442
1571
  let received = 0;
1443
- while (true) {
1444
- const { done, value } = await reader.read();
1445
- if (done) break;
1572
+ const consume = async (value) => {
1446
1573
  chunks.push(value);
1447
1574
  received += value.length;
1448
- if (received > MAX_DOWNLOAD_SIZE) {
1449
- throw new Error(`Download exceeds size limit of ${MAX_DOWNLOAD_SIZE} bytes`);
1575
+ if (received > MAX_DOWNLOAD_SIZE) throw new Error(`Download exceeds size limit of ${MAX_DOWNLOAD_SIZE} bytes`);
1576
+ if (contentLength > 0) await progress?.onDownloadProgress(Math.min(100, Math.round(received / contentLength * 100)));
1577
+ };
1578
+ const body = response.body;
1579
+ if (typeof body.getReader === "function") {
1580
+ const reader = body.getReader();
1581
+ try {
1582
+ while (true) {
1583
+ const { done, value } = await reader.read();
1584
+ if (done) break;
1585
+ await consume(value);
1586
+ }
1587
+ } catch (error) {
1588
+ await reader.cancel?.(error).catch(() => {
1589
+ });
1590
+ throw error;
1591
+ } finally {
1592
+ reader.releaseLock?.();
1450
1593
  }
1451
- if (contentLength > 0) {
1452
- await progress?.onDownloadProgress(Math.round(received / contentLength * 100));
1594
+ } else if (body[Symbol.asyncIterator]) {
1595
+ try {
1596
+ for await (const value of body) await consume(new Uint8Array(value));
1597
+ } catch (error) {
1598
+ body.destroy?.(error instanceof Error ? error : void 0);
1599
+ throw error;
1453
1600
  }
1601
+ } else {
1602
+ return Buffer.from(await response.arrayBuffer());
1454
1603
  }
1455
1604
  return Buffer.concat(chunks);
1456
1605
  }
@@ -1551,12 +1700,16 @@ var init_groq = __esm({
1551
1700
  "use strict";
1552
1701
  GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
1553
1702
  GroqSTT = class {
1554
- constructor(apiKey, defaultModel = "whisper-large-v3-turbo") {
1703
+ constructor(apiKey, defaultModel = "whisper-large-v3-turbo", scopedFetch = globalThis.fetch, getScopedFetch) {
1555
1704
  this.apiKey = apiKey;
1556
1705
  this.defaultModel = defaultModel;
1706
+ this.scopedFetch = scopedFetch;
1707
+ this.getScopedFetch = getScopedFetch;
1557
1708
  }
1558
1709
  apiKey;
1559
1710
  defaultModel;
1711
+ scopedFetch;
1712
+ getScopedFetch;
1560
1713
  name = "groq";
1561
1714
  /**
1562
1715
  * Transcribes audio using the Groq Whisper API.
@@ -1573,7 +1726,7 @@ var init_groq = __esm({
1573
1726
  if (options?.language) {
1574
1727
  form.append("language", options.language);
1575
1728
  }
1576
- const resp = await fetch(GROQ_API_URL, {
1729
+ const resp = await (this.getScopedFetch?.() ?? this.scopedFetch)(GROQ_API_URL, {
1577
1730
  method: "POST",
1578
1731
  headers: { Authorization: `Bearer ${this.apiKey}` },
1579
1732
  body: form
@@ -1693,8 +1846,1065 @@ var init_native_stt = __esm({
1693
1846
  }
1694
1847
  });
1695
1848
 
1849
+ // src/core/network/proxy-types.ts
1850
+ var PROXY_PROTOCOLS;
1851
+ var init_proxy_types = __esm({
1852
+ "src/core/network/proxy-types.ts"() {
1853
+ "use strict";
1854
+ PROXY_PROTOCOLS = ["http", "https", "socks5", "socks5h"];
1855
+ }
1856
+ });
1857
+
1858
+ // src/core/network/proxy-store.ts
1859
+ import fs15 from "fs";
1860
+ import path15 from "path";
1861
+ import net from "net";
1862
+ function atomicWrite(file, value, mode) {
1863
+ fs15.mkdirSync(path15.dirname(file), { recursive: true, mode: 448 });
1864
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
1865
+ fs15.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
1866
+ `, { mode, flag: "wx" });
1867
+ fs15.chmodSync(tmp, mode);
1868
+ const fd = fs15.openSync(tmp, "r");
1869
+ fs15.fsyncSync(fd);
1870
+ fs15.closeSync(fd);
1871
+ fs15.renameSync(tmp, file);
1872
+ try {
1873
+ const dir = fs15.openSync(path15.dirname(file), "r");
1874
+ fs15.fsyncSync(dir);
1875
+ fs15.closeSync(dir);
1876
+ } catch {
1877
+ }
1878
+ }
1879
+ function isRoute(value) {
1880
+ return value === "direct" || value === "inherit" || typeof value === "string" && /^profile:[a-z0-9][a-z0-9._-]{0,63}$/i.test(value);
1881
+ }
1882
+ function isProxyScope(value) {
1883
+ return typeof value === "string" && /^[a-z0-9][a-z0-9_-]*(?:\.[a-z0-9][a-z0-9_-]*)+$/i.test(value) && value.length <= 160;
1884
+ }
1885
+ function canonicalStoredHost(value) {
1886
+ if (typeof value !== "string" || !value || /[\u0000-\u001f\u007f\s/@?#]/.test(value)) throw new Error("invalid profile host");
1887
+ if (value.startsWith("[") && value.endsWith("]") && net.isIP(value.slice(1, -1)) === 6) return value.slice(1, -1).toLowerCase();
1888
+ if (net.isIP(value) === 4 || net.isIP(value) === 6) return value.toLowerCase();
1889
+ const host = value.toLowerCase().replace(/\.$/, "");
1890
+ if (host.length > 253 || !host.split(".").every((label) => /^(?!-)[a-z0-9-]{1,63}(?<!-)$/.test(label))) throw new Error("invalid profile host");
1891
+ return host;
1892
+ }
1893
+ function validateConfig(value) {
1894
+ if (!value || typeof value !== "object") throw new Error("root must be an object");
1895
+ const v = value;
1896
+ if (v.version === 1) {
1897
+ v.version = PROXY_STORE_VERSION;
1898
+ v.revision = 0;
1899
+ v.persistedScopes = Object.keys(v.routing?.routes ?? {});
1900
+ }
1901
+ if (v.version !== PROXY_STORE_VERSION) throw new Error(`unsupported version ${String(v.version)}`);
1902
+ if (!Number.isSafeInteger(v.revision) || v.revision < 0) throw new Error("revision must be a non-negative integer");
1903
+ if (!Array.isArray(v.profiles) || !v.routing || typeof v.routing !== "object" || !isRoute(v.routing.global) || !v.routing.routes || typeof v.routing.routes !== "object" || Array.isArray(v.routing.routes)) throw new Error("invalid profiles/routing shape");
1904
+ const ids = /* @__PURE__ */ new Set();
1905
+ for (const p of v.profiles) {
1906
+ if (!p || typeof p !== "object" || !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(p.id) || ids.has(p.id) || !["http", "https", "socks5", "socks5h"].includes(p.protocol) || !Number.isInteger(p.port) || p.port < 1 || p.port > 65535) throw new Error("invalid profile");
1907
+ ids.add(p.id);
1908
+ p.host = canonicalStoredHost(p.host);
1909
+ if (typeof p.name !== "string" || !p.name.trim() || p.name.length > 100 || typeof p.failClosed !== "boolean" || typeof p.hasCredentials !== "boolean") throw new Error("invalid profile fields");
1910
+ if (!Array.isArray(p.noProxy) || p.noProxy.length > 256 || p.noProxy.some((item) => typeof item !== "string" || !item.trim() || /[\u0000-\u001f\u007f\s]/.test(item))) throw new Error("invalid noProxy");
1911
+ }
1912
+ for (const [scope, route] of Object.entries(v.routing.routes)) {
1913
+ if (!isProxyScope(scope) || !isRoute(route)) throw new Error(`invalid route for ${scope}`);
1914
+ }
1915
+ for (const route of [v.routing.global, ...Object.values(v.routing.routes)]) {
1916
+ if (route.startsWith("profile:") && !ids.has(route.slice(8))) throw new Error(`route references missing profile ${route.slice(8)}`);
1917
+ }
1918
+ if (!Array.isArray(v.persistedScopes) || v.persistedScopes.some((s) => !isProxyScope(s)) || new Set(v.persistedScopes).size !== v.persistedScopes.length) throw new Error("invalid persisted scopes");
1919
+ return structuredClone(v);
1920
+ }
1921
+ function validateSecrets(value) {
1922
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("secrets root must be an object");
1923
+ for (const [id, record] of Object.entries(value)) {
1924
+ if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(id)) throw new Error("invalid secret profile id");
1925
+ if (!record || typeof record !== "object") throw new Error("invalid secret record");
1926
+ const r = record;
1927
+ if (r.username !== void 0 && (typeof r.username !== "string" || r.username.length > 4096 || /[\r\n\u0000]/.test(r.username))) throw new Error("invalid secret username");
1928
+ if (r.password !== void 0 && (typeof r.password !== "string" || r.password.length > 4096 || /[\r\n\u0000]/.test(r.password))) throw new Error("invalid secret password");
1929
+ }
1930
+ return structuredClone(value);
1931
+ }
1932
+ function validateCredentialConsistency(config, secrets) {
1933
+ const profiles = new Map(config.profiles.map((profile) => [profile.id, profile]));
1934
+ for (const id of Object.keys(secrets)) {
1935
+ if (!profiles.has(id)) throw new Error(`credentials reference missing profile ${id}`);
1936
+ }
1937
+ for (const profile of config.profiles) {
1938
+ const secret = secrets[profile.id];
1939
+ const present = Boolean(secret?.username || secret?.password);
1940
+ if (profile.hasCredentials !== present) throw new Error(`hasCredentials mismatch for profile ${profile.id}`);
1941
+ }
1942
+ }
1943
+ var PROXY_STORE_VERSION, DEFAULT_CONFIG2, ProxyStoreError, ProxyStoreCorruptError, ProxyRevisionConflictError, ProxyStore;
1944
+ var init_proxy_store = __esm({
1945
+ "src/core/network/proxy-store.ts"() {
1946
+ "use strict";
1947
+ PROXY_STORE_VERSION = 2;
1948
+ DEFAULT_CONFIG2 = {
1949
+ version: PROXY_STORE_VERSION,
1950
+ revision: 0,
1951
+ profiles: [],
1952
+ routing: { global: "inherit", routes: {} },
1953
+ persistedScopes: []
1954
+ };
1955
+ ProxyStoreError = class extends Error {
1956
+ constructor(code, message, details) {
1957
+ super(message);
1958
+ this.code = code;
1959
+ this.details = details;
1960
+ this.name = "ProxyStoreError";
1961
+ }
1962
+ code;
1963
+ details;
1964
+ };
1965
+ ProxyStoreCorruptError = class extends ProxyStoreError {
1966
+ constructor(file, reason, quarantine, lkgAvailable = false) {
1967
+ super("PROXY_STORE_CORRUPT", `Proxy policy store is invalid (${path15.basename(file)}): ${reason}`, { file, quarantine, lkgAvailable });
1968
+ this.name = "ProxyStoreCorruptError";
1969
+ }
1970
+ };
1971
+ ProxyRevisionConflictError = class extends ProxyStoreError {
1972
+ constructor(expected, actual) {
1973
+ super("PROXY_REVISION_CONFLICT", `Proxy policy changed concurrently (expected revision ${expected}, current ${actual})`, { expected, actual });
1974
+ this.name = "ProxyRevisionConflictError";
1975
+ }
1976
+ };
1977
+ ProxyStore = class {
1978
+ configPath;
1979
+ secretsPath;
1980
+ journalPath;
1981
+ lkgPath;
1982
+ lockPath;
1983
+ constructor(instanceRoot) {
1984
+ this.configPath = path15.join(instanceRoot, "proxy.json");
1985
+ this.secretsPath = path15.join(instanceRoot, "proxy-secrets.json");
1986
+ this.journalPath = path15.join(instanceRoot, "proxy-transaction.json");
1987
+ this.lkgPath = path15.join(instanceRoot, "proxy-lkg.json");
1988
+ this.lockPath = path15.join(instanceRoot, "proxy.lock");
1989
+ const lock = this.acquireLock();
1990
+ try {
1991
+ this.cleanupOrphans(instanceRoot);
1992
+ this.recoverJournal();
1993
+ } finally {
1994
+ this.releaseLock(lock);
1995
+ }
1996
+ }
1997
+ cleanupOrphans(root) {
1998
+ try {
1999
+ const names = fs15.readdirSync(root);
2000
+ for (const name of names) if (/^proxy.*\.tmp$/.test(name)) fs15.rmSync(path15.join(root, name), { force: true });
2001
+ const quarantines = names.filter((n) => n.includes(".corrupt.")).sort().reverse();
2002
+ for (const name of quarantines.slice(3)) fs15.rmSync(path15.join(root, name), { force: true });
2003
+ } catch {
2004
+ }
2005
+ }
2006
+ quarantine(file) {
2007
+ try {
2008
+ const target = `${file}.corrupt.${Date.now()}`;
2009
+ fs15.copyFileSync(file, target, fs15.constants.COPYFILE_EXCL);
2010
+ fs15.chmodSync(target, 384);
2011
+ return target;
2012
+ } catch {
2013
+ return void 0;
2014
+ }
2015
+ }
2016
+ readConfigFile(file = this.configPath) {
2017
+ try {
2018
+ return validateConfig(JSON.parse(fs15.readFileSync(file, "utf8")));
2019
+ } catch (error) {
2020
+ if (error.code === "ENOENT" && file === this.configPath) return structuredClone(DEFAULT_CONFIG2);
2021
+ const q = file === this.configPath ? this.quarantine(file) : void 0;
2022
+ throw new ProxyStoreCorruptError(file, error.message, q, fs15.existsSync(this.lkgPath));
2023
+ }
2024
+ }
2025
+ readSecretsFile() {
2026
+ try {
2027
+ const stat = fs15.statSync(this.secretsPath);
2028
+ if ((stat.mode & 63) !== 0) throw new Error("secret file mode must be 0600");
2029
+ return validateSecrets(JSON.parse(fs15.readFileSync(this.secretsPath, "utf8")));
2030
+ } catch (error) {
2031
+ if (error.code === "ENOENT") return {};
2032
+ const q = this.quarantine(this.secretsPath);
2033
+ throw new ProxyStoreCorruptError(this.secretsPath, error.message, q, fs15.existsSync(this.lkgPath));
2034
+ }
2035
+ }
2036
+ recoverJournal() {
2037
+ if (!fs15.existsSync(this.journalPath)) return;
2038
+ try {
2039
+ const tx = JSON.parse(fs15.readFileSync(this.journalPath, "utf8"));
2040
+ if (tx.version !== 1) throw new Error("unsupported transaction version");
2041
+ const config = validateConfig(tx.config);
2042
+ const secrets = validateSecrets(tx.secrets);
2043
+ validateCredentialConsistency(config, secrets);
2044
+ atomicWrite(this.configPath, config, 384);
2045
+ atomicWrite(this.secretsPath, secrets, 384);
2046
+ atomicWrite(this.lkgPath, tx, 384);
2047
+ fs15.unlinkSync(this.journalPath);
2048
+ } catch (error) {
2049
+ throw new ProxyStoreCorruptError(this.journalPath, error.message, this.quarantine(this.journalPath), fs15.existsSync(this.lkgPath));
2050
+ }
2051
+ }
2052
+ load() {
2053
+ const config = this.readConfigFile();
2054
+ try {
2055
+ validateCredentialConsistency(config, this.readSecretsFile());
2056
+ } catch (error) {
2057
+ if (error instanceof ProxyStoreCorruptError) throw error;
2058
+ throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath), fs15.existsSync(this.lkgPath));
2059
+ }
2060
+ return config;
2061
+ }
2062
+ getSecrets() {
2063
+ const config = this.readConfigFile();
2064
+ const secrets = this.readSecretsFile();
2065
+ try {
2066
+ validateCredentialConsistency(config, secrets);
2067
+ } catch (error) {
2068
+ throw new ProxyStoreCorruptError(this.secretsPath, error.message, this.quarantine(this.secretsPath), fs15.existsSync(this.lkgPath));
2069
+ }
2070
+ return secrets;
2071
+ }
2072
+ getSecret(id) {
2073
+ return this.getSecrets()[id];
2074
+ }
2075
+ commit(config, secrets, expectedRevision) {
2076
+ const lock = this.acquireLock();
2077
+ try {
2078
+ this.recoverJournal();
2079
+ const actual = this.load().revision;
2080
+ if (actual !== expectedRevision) throw new ProxyRevisionConflictError(expectedRevision, actual);
2081
+ const next = validateConfig({ ...config, version: PROXY_STORE_VERSION, revision: actual + 1 });
2082
+ const cleanSecrets = validateSecrets(secrets);
2083
+ validateCredentialConsistency(next, cleanSecrets);
2084
+ const tx = { version: 1, config: next, secrets: cleanSecrets };
2085
+ atomicWrite(this.journalPath, tx, 384);
2086
+ atomicWrite(this.configPath, next, 384);
2087
+ atomicWrite(this.secretsPath, cleanSecrets, 384);
2088
+ atomicWrite(this.lkgPath, tx, 384);
2089
+ fs15.unlinkSync(this.journalPath);
2090
+ return next;
2091
+ } finally {
2092
+ this.releaseLock(lock);
2093
+ }
2094
+ }
2095
+ releaseLock(fd) {
2096
+ try {
2097
+ fs15.closeSync(fd);
2098
+ } catch {
2099
+ }
2100
+ try {
2101
+ fs15.unlinkSync(this.lockPath);
2102
+ } catch {
2103
+ }
2104
+ }
2105
+ acquireLock() {
2106
+ fs15.mkdirSync(path15.dirname(this.lockPath), { recursive: true, mode: 448 });
2107
+ for (let attempt = 0; attempt < 200; attempt++) {
2108
+ try {
2109
+ const fd = fs15.openSync(this.lockPath, "wx", 384);
2110
+ fs15.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }));
2111
+ fs15.fsyncSync(fd);
2112
+ return fd;
2113
+ } catch (error) {
2114
+ if (error.code !== "EEXIST") throw error;
2115
+ if (this.removeStaleLock()) continue;
2116
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
2117
+ }
2118
+ }
2119
+ throw new ProxyStoreError("PROXY_STORE_BUSY", "Proxy policy store is locked by another process");
2120
+ }
2121
+ removeStaleLock() {
2122
+ try {
2123
+ const lock = JSON.parse(fs15.readFileSync(this.lockPath, "utf8"));
2124
+ const old = !Number.isFinite(lock.createdAt) || Date.now() - Number(lock.createdAt) > 3e4;
2125
+ let dead = false;
2126
+ if (Number.isInteger(lock.pid)) {
2127
+ try {
2128
+ process.kill(lock.pid, 0);
2129
+ } catch (error) {
2130
+ dead = error.code === "ESRCH";
2131
+ }
2132
+ }
2133
+ if (dead || old && !Number.isInteger(lock.pid)) {
2134
+ fs15.unlinkSync(this.lockPath);
2135
+ return true;
2136
+ }
2137
+ } catch {
2138
+ try {
2139
+ if (Date.now() - fs15.statSync(this.lockPath).mtimeMs > 3e4) {
2140
+ fs15.unlinkSync(this.lockPath);
2141
+ return true;
2142
+ }
2143
+ } catch {
2144
+ }
2145
+ }
2146
+ return false;
2147
+ }
2148
+ loadLastKnownGood() {
2149
+ try {
2150
+ return validateConfig(JSON.parse(fs15.readFileSync(this.lkgPath, "utf8")).config);
2151
+ } catch {
2152
+ return void 0;
2153
+ }
2154
+ }
2155
+ };
2156
+ }
2157
+ });
2158
+
2159
+ // src/core/network/proxy-service.ts
2160
+ import { ProxyAgent } from "proxy-agent";
2161
+ import nodeFetch from "node-fetch";
2162
+ import fs16 from "fs";
2163
+ import net2 from "net";
2164
+ import { Readable } from "stream";
2165
+ import debug from "debug";
2166
+ function validateId(value, label) {
2167
+ if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(value)) throw new ProxyValidationError(`Invalid ${label}: ${value}`);
2168
+ }
2169
+ function canonicalHost(input2) {
2170
+ if (!input2 || /[\u0000-\u001f\u007f\s/@?#]/.test(input2)) throw new ProxyValidationError("Proxy host must be a DNS name, IPv4, or bracketed IPv6 without URL components");
2171
+ if (input2.includes(":")) {
2172
+ if (!(input2.startsWith("[") && input2.endsWith("]")) || net2.isIP(input2.slice(1, -1)) !== 6) throw new ProxyValidationError("IPv6 proxy hosts must be bracketed and must not include a port");
2173
+ return input2.slice(1, -1).toLowerCase();
2174
+ }
2175
+ if (net2.isIP(input2) === 4) return input2;
2176
+ const host = input2.toLowerCase().replace(/\.$/, "");
2177
+ if (host.length > 253 || !host.split(".").every((label) => /^(?!-)[a-z0-9-]{1,63}(?<!-)$/.test(label))) throw new ProxyValidationError("Invalid proxy DNS host");
2178
+ return host;
2179
+ }
2180
+ function categoryDefault(scope) {
2181
+ const dot = scope.indexOf(".");
2182
+ return dot > 0 ? `${scope.slice(0, dot)}.default` : void 0;
2183
+ }
2184
+ function routeProfileId(route) {
2185
+ return route.startsWith("profile:") ? route.slice("profile:".length) : void 0;
2186
+ }
2187
+ function proxyUrl(profile, secret) {
2188
+ const host = net2.isIP(profile.host) === 6 ? `[${profile.host}]` : profile.host;
2189
+ const url = new URL(`${profile.protocol}://${host}:${profile.port}`);
2190
+ const username = secret?.username;
2191
+ if (username) url.username = username;
2192
+ if (secret?.password) url.password = secret.password;
2193
+ return url.toString();
2194
+ }
2195
+ function shouldBypassProxy(target, noProxy) {
2196
+ const url = new URL(target);
2197
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
2198
+ const hostAndPort = `${net2.isIP(host) === 6 ? `[${host}]` : host}:${url.port || (url.protocol === "https:" ? "443" : "80")}`;
2199
+ return noProxy.some((raw) => {
2200
+ const pattern = raw.trim().toLowerCase();
2201
+ if (!pattern) return false;
2202
+ if (pattern === "*") return true;
2203
+ if (pattern.replace(/^\[|\]$/g, "") === host) return true;
2204
+ if (pattern.startsWith("[") && pattern.includes("]:") || pattern.includes(":") && net2.isIP(pattern) !== 6) return hostAndPort === pattern;
2205
+ const normalized = pattern.startsWith("*.") ? pattern.slice(1) : pattern;
2206
+ return host === normalized.replace(/^\./, "") || normalized.startsWith(".") && host.endsWith(normalized);
2207
+ });
2208
+ }
2209
+ function safeError(error) {
2210
+ const message = error instanceof Error ? error.message : "Proxy request failed";
2211
+ return new Error(redactNetworkSecrets(message));
2212
+ }
2213
+ function normalizeResponse(response) {
2214
+ if (response instanceof Response) return response;
2215
+ const body = response.body ? Readable.toWeb(response.body) : null;
2216
+ return copyResponse(response, body);
2217
+ }
2218
+ async function fetchThroughAgent(input2, init, agent) {
2219
+ if (typeof ReadableStream !== "undefined" && init?.body instanceof ReadableStream) {
2220
+ throw new ProxyValidationError("Web ReadableStream request bodies are not supported by scoped fetch; use string, URLSearchParams, Blob, or FormData");
2221
+ }
2222
+ if (typeof Request !== "undefined" && input2 instanceof Request) {
2223
+ const inherited = {
2224
+ method: input2.method,
2225
+ headers: Object.fromEntries(input2.headers.entries()),
2226
+ redirect: input2.redirect,
2227
+ signal: input2.signal
2228
+ };
2229
+ if (!init?.body && input2.method !== "GET" && input2.method !== "HEAD") inherited.body = Buffer.from(await input2.clone().arrayBuffer());
2230
+ return nodeFetch(input2.url, { ...inherited, ...init, agent });
2231
+ }
2232
+ return nodeFetch(input2, { ...init, agent });
2233
+ }
2234
+ function copyResponse(response, body) {
2235
+ const noBody = response.status === 101 || response.status === 204 || response.status === 205 || response.status === 304;
2236
+ const copy = new Response(noBody ? null : body, { status: response.status, statusText: response.statusText, headers: response.headers });
2237
+ for (const key of ["url", "redirected", "type"]) {
2238
+ try {
2239
+ Object.defineProperty(copy, key, { value: response[key], configurable: true });
2240
+ } catch {
2241
+ }
2242
+ }
2243
+ return copy;
2244
+ }
2245
+ var enabledDebug, PROXY_ENV_KEYS, ProxyValidationError, ProxyUnknownScopeError, ProxyProfileInUseError, ProxyRouteTestError, ProxyProfileTestError, ProxyService;
2246
+ var init_proxy_service = __esm({
2247
+ "src/core/network/proxy-service.ts"() {
2248
+ "use strict";
2249
+ init_proxy_types();
2250
+ init_proxy_store();
2251
+ init_proxy_store();
2252
+ init_network_redaction();
2253
+ enabledDebug = debug.disable();
2254
+ debug.enable(sanitizeProxyDebugNamespaces(enabledDebug) ?? "");
2255
+ PROXY_ENV_KEYS = [
2256
+ "HTTP_PROXY",
2257
+ "HTTPS_PROXY",
2258
+ "ALL_PROXY",
2259
+ "NO_PROXY",
2260
+ "http_proxy",
2261
+ "https_proxy",
2262
+ "all_proxy",
2263
+ "no_proxy",
2264
+ "NODE_USE_ENV_PROXY"
2265
+ ];
2266
+ ProxyValidationError = class extends Error {
2267
+ constructor(message, code = "PROXY_VALIDATION_ERROR") {
2268
+ super(message);
2269
+ this.code = code;
2270
+ this.name = "ProxyValidationError";
2271
+ }
2272
+ code;
2273
+ };
2274
+ ProxyUnknownScopeError = class extends ProxyValidationError {
2275
+ constructor(scope) {
2276
+ super(`Proxy scope "${scope}" is not registered`, "PROXY_UNKNOWN_SCOPE");
2277
+ }
2278
+ };
2279
+ ProxyProfileInUseError = class extends ProxyValidationError {
2280
+ constructor(id) {
2281
+ super(`Proxy profile "${id}" is still used by routing`, "PROXY_PROFILE_IN_USE");
2282
+ }
2283
+ };
2284
+ ProxyRouteTestError = class extends Error {
2285
+ code = "PROXY_ROUTE_TEST_FAILED";
2286
+ constructor(scope, cause) {
2287
+ const reason = safeError(cause).message;
2288
+ super(`Proxy route test failed for ${scope}; route was not changed: ${reason}`);
2289
+ this.name = "ProxyRouteTestError";
2290
+ }
2291
+ };
2292
+ ProxyProfileTestError = class extends Error {
2293
+ code = "PROXY_PROFILE_TEST_FAILED";
2294
+ constructor(profileId, cause) {
2295
+ const reason = safeError(cause).message;
2296
+ super(`Proxy profile test failed for ${profileId}; profile was not changed: ${reason}`);
2297
+ this.name = "ProxyProfileTestError";
2298
+ }
2299
+ };
2300
+ ProxyService = class {
2301
+ constructor(instanceRoot, retiredLeaseTimeoutMs = 5 * 6e4, allowedNodeEnvironmentFlags = process.allowedNodeEnvironmentFlags) {
2302
+ this.retiredLeaseTimeoutMs = retiredLeaseTimeoutMs;
2303
+ this.allowedNodeEnvironmentFlags = allowedNodeEnvironmentFlags;
2304
+ this.store = new ProxyStore(instanceRoot);
2305
+ try {
2306
+ const config = this.store.load();
2307
+ for (const scope of [...config.persistedScopes, ...Object.keys(config.routing.routes)]) this.scopes.add(scope);
2308
+ } catch {
2309
+ }
2310
+ }
2311
+ retiredLeaseTimeoutMs;
2312
+ allowedNodeEnvironmentFlags;
2313
+ store;
2314
+ scopes = /* @__PURE__ */ new Set([
2315
+ "channels.default",
2316
+ "channels.telegram",
2317
+ "agents.default",
2318
+ "agents.codex",
2319
+ "agents.cursor",
2320
+ "services.default",
2321
+ "services.npmUpdate",
2322
+ "services.agentRegistry",
2323
+ "services.pluginInstaller",
2324
+ "services.speechDownloads",
2325
+ "plugins.default"
2326
+ ]);
2327
+ testers = /* @__PURE__ */ new Map();
2328
+ listeners = /* @__PURE__ */ new Set();
2329
+ transports = /* @__PURE__ */ new Map();
2330
+ facades = /* @__PURE__ */ new Map();
2331
+ mutationQueue = Promise.resolve();
2332
+ policyGeneration = 0;
2333
+ maxTransports = 128;
2334
+ serialize(operation) {
2335
+ const next = this.mutationQueue.then(operation, operation);
2336
+ this.mutationQueue = next.then(() => void 0, () => void 0);
2337
+ return next;
2338
+ }
2339
+ getPolicyGeneration() {
2340
+ return this.policyGeneration;
2341
+ }
2342
+ invalidatePolicyBeforeCommit(config, affectedProfile, changedScope) {
2343
+ const agentAffected = changedScope === "global" || changedScope === "agents.default" || Boolean(changedScope?.startsWith("agents.")) || Boolean(affectedProfile && Object.entries(config.routing.routes).some(([scope, route]) => scope.startsWith("agents.") && route === `profile:${affectedProfile}`)) || Boolean(affectedProfile && config.routing.global === `profile:${affectedProfile}`);
2344
+ if (agentAffected) this.policyGeneration++;
2345
+ return agentAffected;
2346
+ }
2347
+ registerScope(scope) {
2348
+ if (!isProxyScope(scope)) throw new ProxyValidationError(`Invalid proxy scope: ${scope}`);
2349
+ this.scopes.add(scope);
2350
+ this.persistScope(scope);
2351
+ return () => this.scopes.delete(scope);
2352
+ }
2353
+ registerRouteTester(scope, tester) {
2354
+ this.registerScope(scope);
2355
+ this.testers.set(scope, tester);
2356
+ return () => this.testers.delete(scope);
2357
+ }
2358
+ /**
2359
+ * Scope discovery is part of the durable policy schema, not only an in-memory
2360
+ * UI concern. Persisting registrations lets an operator configure a plugin or
2361
+ * agent while it is temporarily disabled and keeps the category list stable
2362
+ * across daemon restarts.
2363
+ */
2364
+ persistScope(scope) {
2365
+ const config = this.store.load();
2366
+ if (config.persistedScopes.includes(scope)) return;
2367
+ config.persistedScopes = [...config.persistedScopes, scope].sort();
2368
+ this.store.commit(config, this.store.getSecrets(), config.revision);
2369
+ }
2370
+ onRouteChanged(listener) {
2371
+ this.listeners.add(listener);
2372
+ return () => this.listeners.delete(listener);
2373
+ }
2374
+ listProfiles() {
2375
+ return this.store.load().profiles.map((profile) => ({ ...profile }));
2376
+ }
2377
+ getProfile(id) {
2378
+ return this.listProfiles().find((profile) => profile.id === id);
2379
+ }
2380
+ saveProfile(input2) {
2381
+ const config = this.store.load();
2382
+ const secrets = this.store.getSecrets();
2383
+ const { profile, nextSecrets } = this.buildProfile(input2, config, secrets);
2384
+ const affectedScopes = this.scopesUsingProfile(config, input2.id);
2385
+ this.invalidatePolicyBeforeCommit(config, input2.id);
2386
+ config.profiles = [...config.profiles.filter((p) => p.id !== profile.id), profile];
2387
+ config.persistedScopes = [.../* @__PURE__ */ new Set([...config.persistedScopes, ...Object.keys(config.routing.routes)])];
2388
+ this.store.commit(config, nextSecrets, config.revision);
2389
+ this.retireScopes(affectedScopes);
2390
+ return profile;
2391
+ }
2392
+ buildProfile(input2, config, secrets) {
2393
+ validateId(input2.id, "profile id");
2394
+ if (!PROXY_PROTOCOLS.includes(input2.protocol)) throw new ProxyValidationError(`Unsupported proxy protocol: ${input2.protocol}`);
2395
+ if (!Number.isInteger(input2.port) || input2.port < 1 || input2.port > 65535) throw new ProxyValidationError("Proxy port must be between 1 and 65535");
2396
+ const host = canonicalHost(input2.host);
2397
+ const existing = config.profiles.find((p) => p.id === input2.id);
2398
+ const nextSecrets = structuredClone(secrets);
2399
+ if (input2.username !== void 0 || input2.password !== void 0) nextSecrets[input2.id] = { username: input2.username ?? nextSecrets[input2.id]?.username, password: input2.password ?? nextSecrets[input2.id]?.password };
2400
+ const secret = nextSecrets[input2.id];
2401
+ return { profile: {
2402
+ id: input2.id,
2403
+ name: input2.name ?? existing?.name ?? input2.id,
2404
+ protocol: input2.protocol,
2405
+ host,
2406
+ port: input2.port,
2407
+ noProxy: input2.noProxy ?? existing?.noProxy ?? ["localhost", "127.0.0.1", "::1"],
2408
+ failClosed: input2.failClosed ?? existing?.failClosed ?? true,
2409
+ hasCredentials: Boolean(secret?.username || secret?.password)
2410
+ }, nextSecrets };
2411
+ }
2412
+ async saveProfileSafely(input2, expectedRevision) {
2413
+ return this.serialize(async () => {
2414
+ const config = this.store.load();
2415
+ const secrets = this.store.getSecrets();
2416
+ if (expectedRevision !== void 0 && config.revision !== expectedRevision) throw new ProxyRevisionConflictError(expectedRevision, config.revision);
2417
+ const { profile: candidate, nextSecrets } = this.buildProfile(input2, config, secrets);
2418
+ const affectedScopes = this.scopesUsingProfile(config, input2.id);
2419
+ const candidateSecret = nextSecrets[input2.id];
2420
+ for (const [scope, tester] of this.testers) {
2421
+ if (this.resolve(scope).profile?.id === input2.id) {
2422
+ const fetcher = this.createProfileFetch(candidate, candidateSecret);
2423
+ try {
2424
+ await tester(fetcher);
2425
+ } catch (error) {
2426
+ throw new ProxyProfileTestError(input2.id, error);
2427
+ } finally {
2428
+ fetcher.destroy?.();
2429
+ }
2430
+ }
2431
+ }
2432
+ this.invalidatePolicyBeforeCommit(config, input2.id);
2433
+ config.profiles = [...config.profiles.filter((p) => p.id !== candidate.id), candidate];
2434
+ const saved = this.store.commit(config, nextSecrets, config.revision).profiles.find((p) => p.id === candidate.id);
2435
+ this.retireScopes(affectedScopes);
2436
+ return saved;
2437
+ });
2438
+ }
2439
+ /** Import a conventional proxy env file without ever returning or logging its credential URL. */
2440
+ importEnvFile(id, envFile, name) {
2441
+ return this.saveProfile(this.parseEnvFile(id, envFile, name));
2442
+ }
2443
+ parseEnvFile(id, envFile, name) {
2444
+ const stat = fs16.statSync(envFile);
2445
+ if (!stat.isFile()) throw new Error("Proxy env path is not a file");
2446
+ if ((stat.mode & 63) !== 0) throw new Error("Proxy env file must have mode 0600");
2447
+ const values = {};
2448
+ for (const rawLine of fs16.readFileSync(envFile, "utf8").split(/\r?\n/)) {
2449
+ const line = rawLine.trim();
2450
+ if (!line || line.startsWith("#")) continue;
2451
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
2452
+ if (!match) continue;
2453
+ let value = match[2].trim();
2454
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2455
+ values[match[1]] = value;
2456
+ }
2457
+ const httpUrl = values.HTTP_PROXY ?? values.http_proxy;
2458
+ const httpsUrl = values.HTTPS_PROXY ?? values.https_proxy;
2459
+ if (httpUrl && httpsUrl && httpUrl !== httpsUrl) throw new ProxyValidationError("HTTP_PROXY and HTTPS_PROXY differ; use one canonical proxy URL or create separate profiles");
2460
+ const rawUrl = httpsUrl ?? httpUrl ?? values.ALL_PROXY ?? values.all_proxy;
2461
+ if (!rawUrl) throw new Error("No proxy URL found in env file");
2462
+ let url;
2463
+ try {
2464
+ url = new URL(rawUrl);
2465
+ } catch {
2466
+ throw new ProxyValidationError("Proxy env contains an invalid URL");
2467
+ }
2468
+ const protocol = url.protocol.slice(0, -1);
2469
+ if (!PROXY_PROTOCOLS.includes(protocol)) throw new ProxyValidationError(`Unsupported proxy protocol: ${protocol}`);
2470
+ if (url.pathname && url.pathname !== "/" || url.search || url.hash) throw new ProxyValidationError("Proxy URL must not contain path, query, or fragment");
2471
+ const noProxy = values.NO_PROXY ?? values.no_proxy;
2472
+ return {
2473
+ id,
2474
+ name,
2475
+ protocol,
2476
+ // WHATWG URL keeps IPv6 hostnames bracketed. Preserve that canonical
2477
+ // input form here; buildProfile performs the single host normalization.
2478
+ host: url.hostname,
2479
+ port: Number(url.port || (protocol.startsWith("socks") ? 1080 : protocol === "https" ? 443 : 80)),
2480
+ username: decodeURIComponent(url.username),
2481
+ password: decodeURIComponent(url.password),
2482
+ noProxy: noProxy ? noProxy.split(",").map((item) => item.trim()).filter(Boolean) : void 0
2483
+ };
2484
+ }
2485
+ async importEnvFileSafely(id, envFile, name, expectedRevision) {
2486
+ return this.saveProfileSafely(this.parseEnvFile(id, envFile, name), expectedRevision);
2487
+ }
2488
+ async deleteProfile(id, expectedRevision) {
2489
+ return this.serialize(async () => {
2490
+ const config = this.store.load();
2491
+ const secrets = this.store.getSecrets();
2492
+ if (expectedRevision !== void 0 && config.revision !== expectedRevision) throw new ProxyRevisionConflictError(expectedRevision, config.revision);
2493
+ if (!config.profiles.some((p) => p.id === id)) throw new ProxyValidationError(`Proxy profile "${id}" does not exist`);
2494
+ if (Object.values(config.routing.routes).includes(`profile:${id}`) || config.routing.global === `profile:${id}`) throw new ProxyProfileInUseError(id);
2495
+ config.profiles = config.profiles.filter((p) => p.id !== id);
2496
+ delete secrets[id];
2497
+ this.store.commit(config, secrets, config.revision);
2498
+ });
2499
+ }
2500
+ resolve(scope, routeOverride) {
2501
+ const config = this.store.load();
2502
+ return this.resolveFromConfig(scope, config, routeOverride);
2503
+ }
2504
+ resolveFromConfig(scope, config, routeOverride) {
2505
+ const exact = routeOverride ?? config.routing.routes[scope];
2506
+ const category = categoryDefault(scope);
2507
+ const route = exact ?? (category ? config.routing.routes[category] : void 0) ?? config.routing.global;
2508
+ const resolvedFrom = routeOverride ? "candidate" : exact ? scope : category && config.routing.routes[category] ? category : "global";
2509
+ const id = routeProfileId(route);
2510
+ const profile = id ? config.profiles.find((item) => item.id === id) : void 0;
2511
+ if (id && !profile) throw new Error(`Proxy profile "${id}" does not exist`);
2512
+ return { scope, route, resolvedFrom, profile };
2513
+ }
2514
+ async setRoute(scope, route, expectedRevision) {
2515
+ return this.serialize(async () => {
2516
+ if (scope !== "global" && !this.getKnownScopes().includes(scope)) throw new ProxyUnknownScopeError(scope);
2517
+ this.validateRoute(route);
2518
+ const config = this.store.load();
2519
+ if (expectedRevision !== void 0 && config.revision !== expectedRevision) throw new ProxyRevisionConflictError(expectedRevision, config.revision);
2520
+ const candidate = structuredClone(config);
2521
+ if (scope === "global") candidate.routing.global = route;
2522
+ else candidate.routing.routes[scope] = route;
2523
+ const affectedScopes = this.changedResolutionScopes(config, candidate);
2524
+ try {
2525
+ await this.testCandidateRoutes(config, candidate);
2526
+ } catch (error) {
2527
+ throw new ProxyRouteTestError(scope, error);
2528
+ }
2529
+ const affectsAgents = this.invalidatePolicyBeforeCommit(config, void 0, scope);
2530
+ candidate.persistedScopes = [.../* @__PURE__ */ new Set([...candidate.persistedScopes, ...scope === "global" ? [] : [scope]])];
2531
+ this.store.commit(candidate, this.store.getSecrets(), config.revision);
2532
+ this.retireScopes(affectedScopes);
2533
+ for (const listener of this.listeners) await listener(scope, route);
2534
+ return {
2535
+ scope,
2536
+ route,
2537
+ warmPoolInvalidated: affectsAgents,
2538
+ activeAgentProcessesUnaffected: affectsAgents
2539
+ };
2540
+ });
2541
+ }
2542
+ async clearRoute(scope, expectedRevision) {
2543
+ return this.serialize(async () => {
2544
+ const config = this.store.load();
2545
+ if (expectedRevision !== void 0 && config.revision !== expectedRevision) throw new ProxyRevisionConflictError(expectedRevision, config.revision);
2546
+ const candidate = structuredClone(config);
2547
+ if (scope === "global") candidate.routing.global = "inherit";
2548
+ else {
2549
+ if (!this.getKnownScopes().includes(scope)) throw new ProxyUnknownScopeError(scope);
2550
+ delete candidate.routing.routes[scope];
2551
+ }
2552
+ const affectedScopes = this.changedResolutionScopes(config, candidate);
2553
+ try {
2554
+ await this.testCandidateRoutes(config, candidate);
2555
+ } catch (error) {
2556
+ throw new ProxyRouteTestError(scope, error);
2557
+ }
2558
+ this.invalidatePolicyBeforeCommit(config, void 0, scope);
2559
+ this.store.commit(candidate, this.store.getSecrets(), config.revision);
2560
+ this.retireScopes(affectedScopes);
2561
+ for (const listener of this.listeners) await listener(scope, this.resolve(scope).route);
2562
+ });
2563
+ }
2564
+ getKnownScopes() {
2565
+ const config = this.store.load();
2566
+ return [.../* @__PURE__ */ new Set([...this.scopes, ...config.persistedScopes, ...Object.keys(config.routing.routes)])].sort();
2567
+ }
2568
+ createFetch(scope, routeOverride) {
2569
+ if (routeOverride) return this.createTransport(scope, routeOverride);
2570
+ if (!this.getKnownScopes().includes(scope)) throw new ProxyUnknownScopeError(scope);
2571
+ const existing = this.facades.get(scope);
2572
+ if (existing) return existing;
2573
+ const facade = (async (input2, init) => {
2574
+ const entry = this.acquireTransport(scope);
2575
+ try {
2576
+ const response = await entry.fetcher(input2, init);
2577
+ return this.releaseWithResponse(response, entry);
2578
+ } catch (error) {
2579
+ this.releaseTransport(entry);
2580
+ throw error;
2581
+ }
2582
+ });
2583
+ this.facades.set(scope, facade);
2584
+ return facade;
2585
+ }
2586
+ createTransport(scope, routeOverride) {
2587
+ const resolution = this.resolve(scope, routeOverride);
2588
+ const cacheKey = JSON.stringify({
2589
+ route: resolution.route,
2590
+ profile: resolution.profile,
2591
+ inherited: resolution.route === "inherit" ? [process.env.HTTP_PROXY, process.env.HTTPS_PROXY, process.env.ALL_PROXY, process.env.NO_PROXY] : void 0
2592
+ });
2593
+ if (!routeOverride) {
2594
+ const cached = this.transports.get(scope);
2595
+ if (cached?.key === cacheKey) return cached.fetcher;
2596
+ }
2597
+ if (resolution.route === "direct") {
2598
+ const fetcher2 = this.createDirectFetch();
2599
+ if (!routeOverride) this.cacheTransport(scope, cacheKey, fetcher2);
2600
+ return fetcher2;
2601
+ }
2602
+ let agent;
2603
+ if (resolution.route === "inherit") {
2604
+ const hasInheritedProxy = process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? process.env.ALL_PROXY ?? process.env.all_proxy;
2605
+ if (!hasInheritedProxy) {
2606
+ const fetcher2 = this.createDirectFetch();
2607
+ if (!routeOverride) this.cacheTransport(scope, cacheKey, fetcher2);
2608
+ return fetcher2;
2609
+ }
2610
+ agent = new ProxyAgent();
2611
+ } else if (resolution.profile) {
2612
+ const fetcher2 = this.createProfileFetch(resolution.profile, this.store.getSecret(resolution.profile.id));
2613
+ if (!routeOverride) this.cacheTransport(scope, cacheKey, fetcher2);
2614
+ return fetcher2;
2615
+ } else {
2616
+ return globalThis.fetch.bind(globalThis);
2617
+ }
2618
+ const fetcher = (async (input2, init) => {
2619
+ try {
2620
+ const response = await fetchThroughAgent(input2, init, agent);
2621
+ return normalizeResponse(response);
2622
+ } catch (error) {
2623
+ if (resolution.profile?.failClosed !== false) throw safeError(error);
2624
+ return globalThis.fetch(input2, init);
2625
+ }
2626
+ });
2627
+ fetcher.destroy = () => agent.destroy?.();
2628
+ if (!routeOverride) this.cacheTransport(scope, cacheKey, fetcher);
2629
+ return fetcher;
2630
+ }
2631
+ cacheTransport(scope, key, fetcher) {
2632
+ const old = this.transports.get(scope);
2633
+ if (old && old.fetcher !== fetcher) this.retireTransport(old);
2634
+ this.transports.set(scope, { key, fetcher, active: 0, retired: false, destroyed: false, leases: /* @__PURE__ */ new Set() });
2635
+ while (this.transports.size > this.maxTransports) {
2636
+ const oldest = this.transports.keys().next().value;
2637
+ if (!oldest) break;
2638
+ const entry = this.transports.get(oldest);
2639
+ if (entry) this.retireTransport(entry);
2640
+ this.transports.delete(oldest);
2641
+ }
2642
+ }
2643
+ buildAgentEnv(agentName, inherited) {
2644
+ return this.buildChildEnv(`agents.${agentName}`, inherited);
2645
+ }
2646
+ buildChildEnv(scope, inherited) {
2647
+ const resolution = this.resolve(scope);
2648
+ const env = { ...inherited };
2649
+ if (env.DEBUG) {
2650
+ const safeDebug = sanitizeProxyDebugNamespaces(env.DEBUG);
2651
+ if (safeDebug) env.DEBUG = safeDebug;
2652
+ else delete env.DEBUG;
2653
+ }
2654
+ if (resolution.route === "inherit") return env;
2655
+ for (const key of PROXY_ENV_KEYS) delete env[key];
2656
+ if (env.NODE_OPTIONS) {
2657
+ env.NODE_OPTIONS = env.NODE_OPTIONS.split(/\s+/).filter((flag) => flag !== "--use-env-proxy").join(" ");
2658
+ if (!env.NODE_OPTIONS) delete env.NODE_OPTIONS;
2659
+ }
2660
+ if (resolution.route === "direct") {
2661
+ env.NODE_USE_ENV_PROXY = "0";
2662
+ return env;
2663
+ }
2664
+ const profile = resolution.profile;
2665
+ const url = proxyUrl(profile, this.store.getSecret(profile.id));
2666
+ const noProxy = profile.noProxy.join(",");
2667
+ if (profile.protocol === "http" || profile.protocol === "https") {
2668
+ env.HTTP_PROXY = env.http_proxy = url;
2669
+ env.HTTPS_PROXY = env.https_proxy = url;
2670
+ env.NODE_USE_ENV_PROXY = "1";
2671
+ if (this.allowedNodeEnvironmentFlags.has("--use-env-proxy")) {
2672
+ env.NODE_OPTIONS = [env.NODE_OPTIONS, "--use-env-proxy"].filter(Boolean).join(" ");
2673
+ }
2674
+ } else {
2675
+ env.ALL_PROXY = env.all_proxy = url;
2676
+ }
2677
+ env.NO_PROXY = env.no_proxy = noProxy;
2678
+ return env;
2679
+ }
2680
+ async test(scope, targetUrl = "https://api.ipify.org?format=json") {
2681
+ if (!this.getKnownScopes().includes(scope)) throw new ProxyUnknownScopeError(scope);
2682
+ let response;
2683
+ try {
2684
+ response = await this.createFetch(scope)(targetUrl, { signal: AbortSignal.timeout(1e4) });
2685
+ return { ok: response.ok, status: response.status };
2686
+ } catch (error) {
2687
+ return { ok: false, error: error instanceof Error ? redactNetworkSecrets(error.message) : "Proxy test failed" };
2688
+ } finally {
2689
+ try {
2690
+ await response?.body?.cancel();
2691
+ } catch {
2692
+ }
2693
+ }
2694
+ }
2695
+ async testProfile(id, targetUrl) {
2696
+ if (!this.getProfile(id)) return { ok: false, error: `Proxy profile "${id}" does not exist` };
2697
+ let fetcher;
2698
+ let response;
2699
+ try {
2700
+ fetcher = this.createTransport("services.proxyTest", `profile:${id}`);
2701
+ response = await fetcher(targetUrl ?? "https://api.ipify.org?format=json", {
2702
+ signal: AbortSignal.timeout(1e4)
2703
+ });
2704
+ return { ok: response.ok, status: response.status };
2705
+ } catch (error) {
2706
+ return { ok: false, error: error instanceof Error ? redactNetworkSecrets(error.message) : "Proxy test failed" };
2707
+ } finally {
2708
+ try {
2709
+ await response?.body?.cancel();
2710
+ } catch {
2711
+ }
2712
+ ;
2713
+ fetcher?.destroy?.();
2714
+ }
2715
+ }
2716
+ status() {
2717
+ const config = this.store.load();
2718
+ const scopes = this.getKnownScopes();
2719
+ const daemonVariables = PROXY_ENV_KEYS.filter((key) => {
2720
+ const value = process.env[key];
2721
+ return !["NO_PROXY", "no_proxy", "NODE_USE_ENV_PROXY"].includes(key) && value !== void 0 && value !== "";
2722
+ });
2723
+ return {
2724
+ revision: config.revision,
2725
+ profiles: this.listProfiles(),
2726
+ routing: config.routing,
2727
+ scopes,
2728
+ diagnostics: scopes.map((scope) => {
2729
+ const resolution = this.resolve(scope);
2730
+ const child = scope.startsWith("agents.") || scope.startsWith("services.");
2731
+ const socks = child && Boolean(resolution.profile?.protocol.startsWith("socks"));
2732
+ return {
2733
+ scope,
2734
+ route: resolution.route,
2735
+ resolvedFrom: resolution.resolvedFrom,
2736
+ childProcessSupport: child ? socks ? "best-effort-socks-env" : "native-env" : "not-applicable",
2737
+ warning: socks ? "SOCKS support depends on the child process honoring ALL_PROXY; npm and arbitrary ACP clients may require HTTP/HTTPS profiles." : void 0
2738
+ };
2739
+ }),
2740
+ environment: {
2741
+ daemonWideProxyActive: daemonVariables.length > 0,
2742
+ compatibilityMode: daemonVariables.length > 0,
2743
+ variables: daemonVariables,
2744
+ message: daemonVariables.length > 0 ? "Daemon-wide proxy environment is active. Scoped direct transports remain isolated, but category isolation is in compatibility mode until legacy wrapper/drop-in proxy injection is removed." : "No daemon-wide proxy environment detected; native scoped routing is authoritative."
2745
+ }
2746
+ };
2747
+ }
2748
+ validateRoute(route) {
2749
+ if (route === "direct" || route === "inherit") return;
2750
+ const id = routeProfileId(route);
2751
+ if (!id || !this.getProfile(id)) throw new ProxyValidationError(`Proxy profile "${id ?? route}" does not exist`);
2752
+ }
2753
+ async testCandidateRoutes(current, candidate) {
2754
+ for (const [scope, tester] of this.testers) {
2755
+ const before = this.resolveFromConfig(scope, current);
2756
+ const after = this.resolveFromConfig(scope, candidate);
2757
+ if (before.route !== after.route || before.profile?.id !== after.profile?.id) {
2758
+ const fetcher = this.createTransport(scope, after.route);
2759
+ try {
2760
+ await tester(fetcher);
2761
+ } finally {
2762
+ fetcher.destroy?.();
2763
+ }
2764
+ }
2765
+ }
2766
+ }
2767
+ createProfileFetch(profile, secret) {
2768
+ const url = proxyUrl(profile, secret);
2769
+ const agent = new ProxyAgent({
2770
+ getProxyForUrl: (target) => shouldBypassProxy(target, profile.noProxy) ? "" : url
2771
+ });
2772
+ const directFallback = profile.failClosed === false ? this.createDirectFetch() : void 0;
2773
+ const fetcher = (async (input2, init) => {
2774
+ try {
2775
+ const response = await fetchThroughAgent(input2, init, agent);
2776
+ return normalizeResponse(response);
2777
+ } catch (error) {
2778
+ if (profile.failClosed !== false) throw safeError(error);
2779
+ return directFallback(input2, init);
2780
+ }
2781
+ });
2782
+ fetcher.destroy = () => {
2783
+ agent.destroy?.();
2784
+ directFallback?.destroy?.();
2785
+ };
2786
+ return fetcher;
2787
+ }
2788
+ /** Direct transport that ignores daemon-wide env proxy flags without global mutation. */
2789
+ createDirectFetch() {
2790
+ const agent = new ProxyAgent({ getProxyForUrl: () => "" });
2791
+ const fetcher = (async (input2, init) => {
2792
+ const response = await fetchThroughAgent(input2, init, agent);
2793
+ return normalizeResponse(response);
2794
+ });
2795
+ fetcher.destroy = () => agent.destroy?.();
2796
+ return fetcher;
2797
+ }
2798
+ acquireTransport(scope) {
2799
+ this.createTransport(scope);
2800
+ const entry = this.transports.get(scope);
2801
+ entry.active++;
2802
+ return entry;
2803
+ }
2804
+ releaseTransport(entry) {
2805
+ entry.active = Math.max(0, entry.active - 1);
2806
+ if (entry.retired && entry.active === 0) this.destroyTransport(entry);
2807
+ }
2808
+ retireTransport(entry) {
2809
+ entry.retired = true;
2810
+ if (entry.active === 0) {
2811
+ this.destroyTransport(entry);
2812
+ return;
2813
+ }
2814
+ if (!entry.retirementTimer) {
2815
+ entry.retirementTimer = setTimeout(() => {
2816
+ const reason = new Error("Retired proxy transport response exceeded its maximum lease");
2817
+ for (const abort of [...entry.leases]) abort(reason);
2818
+ this.destroyTransport(entry);
2819
+ }, this.retiredLeaseTimeoutMs);
2820
+ entry.retirementTimer.unref?.();
2821
+ }
2822
+ }
2823
+ destroyTransport(entry) {
2824
+ if (entry.destroyed) return;
2825
+ entry.destroyed = true;
2826
+ if (entry.retirementTimer) clearTimeout(entry.retirementTimer);
2827
+ entry.fetcher.destroy?.();
2828
+ }
2829
+ retireScopes(scopes) {
2830
+ for (const scope of scopes) {
2831
+ const entry = this.transports.get(scope);
2832
+ if (!entry) continue;
2833
+ this.transports.delete(scope);
2834
+ this.retireTransport(entry);
2835
+ }
2836
+ }
2837
+ scopesUsingProfile(config, profileId) {
2838
+ return this.allScopes(config).filter((scope) => this.resolveFromConfig(scope, config).profile?.id === profileId);
2839
+ }
2840
+ changedResolutionScopes(before, after) {
2841
+ return this.allScopes(before, after).filter((scope) => {
2842
+ const a = this.resolveFromConfig(scope, before);
2843
+ const b = this.resolveFromConfig(scope, after);
2844
+ return JSON.stringify([a.route, a.profile]) !== JSON.stringify([b.route, b.profile]);
2845
+ });
2846
+ }
2847
+ allScopes(...configs) {
2848
+ return [.../* @__PURE__ */ new Set([...this.scopes, ...configs.flatMap((config) => [...config.persistedScopes, ...Object.keys(config.routing.routes)])])];
2849
+ }
2850
+ releaseWithResponse(response, entry) {
2851
+ const release = () => this.releaseTransport(entry);
2852
+ if (!response.body) {
2853
+ release();
2854
+ return response;
2855
+ }
2856
+ const reader = response.body.getReader();
2857
+ let released = false;
2858
+ let bodyController;
2859
+ const done = () => {
2860
+ if (!released) {
2861
+ released = true;
2862
+ entry.leases.delete(abort);
2863
+ release();
2864
+ }
2865
+ };
2866
+ const abort = (reason) => {
2867
+ if (released) return;
2868
+ try {
2869
+ bodyController?.error(reason);
2870
+ } catch {
2871
+ }
2872
+ void reader.cancel(reason).then(done, done);
2873
+ };
2874
+ entry.leases.add(abort);
2875
+ if (entry.destroyed) abort(new Error("Retired proxy transport response exceeded its maximum lease"));
2876
+ const body = new ReadableStream({
2877
+ start(controller) {
2878
+ bodyController = controller;
2879
+ },
2880
+ async pull(controller) {
2881
+ try {
2882
+ const chunk = await reader.read();
2883
+ if (chunk.done) {
2884
+ done();
2885
+ controller.close();
2886
+ } else controller.enqueue(chunk.value);
2887
+ } catch (error) {
2888
+ done();
2889
+ controller.error(error);
2890
+ }
2891
+ },
2892
+ async cancel(reason) {
2893
+ try {
2894
+ await reader.cancel(reason);
2895
+ } finally {
2896
+ done();
2897
+ }
2898
+ }
2899
+ });
2900
+ return copyResponse(response, body);
2901
+ }
2902
+ };
2903
+ }
2904
+ });
2905
+
1696
2906
  // src/core/doctor/checks/config.ts
1697
- import * as fs15 from "fs";
2907
+ import * as fs17 from "fs";
1698
2908
  var configCheck;
1699
2909
  var init_config2 = __esm({
1700
2910
  "src/core/doctor/checks/config.ts"() {
@@ -1706,14 +2916,14 @@ var init_config2 = __esm({
1706
2916
  order: 1,
1707
2917
  async run(ctx) {
1708
2918
  const results = [];
1709
- if (!fs15.existsSync(ctx.configPath)) {
2919
+ if (!fs17.existsSync(ctx.configPath)) {
1710
2920
  results.push({ status: "fail", message: "Config file not found" });
1711
2921
  return results;
1712
2922
  }
1713
2923
  results.push({ status: "pass", message: "Config file exists" });
1714
2924
  let raw;
1715
2925
  try {
1716
- raw = JSON.parse(fs15.readFileSync(ctx.configPath, "utf-8"));
2926
+ raw = JSON.parse(fs17.readFileSync(ctx.configPath, "utf-8"));
1717
2927
  } catch (err) {
1718
2928
  results.push({
1719
2929
  status: "fail",
@@ -1732,7 +2942,7 @@ var init_config2 = __esm({
1732
2942
  fixRisk: "safe",
1733
2943
  fix: async () => {
1734
2944
  applyMigrations(raw);
1735
- fs15.writeFileSync(ctx.configPath, JSON.stringify(raw, null, 2));
2945
+ fs17.writeFileSync(ctx.configPath, JSON.stringify(raw, null, 2));
1736
2946
  return { success: true, message: "applied migrations" };
1737
2947
  }
1738
2948
  });
@@ -1756,8 +2966,8 @@ var init_config2 = __esm({
1756
2966
 
1757
2967
  // src/core/doctor/checks/agents.ts
1758
2968
  import { execFileSync as execFileSync3 } from "child_process";
1759
- import * as fs16 from "fs";
1760
- import * as path16 from "path";
2969
+ import * as fs18 from "fs";
2970
+ import * as path17 from "path";
1761
2971
  function commandExists2(cmd) {
1762
2972
  try {
1763
2973
  execFileSync3("which", [cmd], { stdio: "pipe" });
@@ -1766,9 +2976,9 @@ function commandExists2(cmd) {
1766
2976
  }
1767
2977
  let dir = process.cwd();
1768
2978
  while (true) {
1769
- const binPath = path16.join(dir, "node_modules", ".bin", cmd);
1770
- if (fs16.existsSync(binPath)) return true;
1771
- const parent = path16.dirname(dir);
2979
+ const binPath = path17.join(dir, "node_modules", ".bin", cmd);
2980
+ if (fs18.existsSync(binPath)) return true;
2981
+ const parent = path17.dirname(dir);
1772
2982
  if (parent === dir) break;
1773
2983
  dir = parent;
1774
2984
  }
@@ -1790,9 +3000,9 @@ var init_agents = __esm({
1790
3000
  const defaultAgent = ctx.config.defaultAgent;
1791
3001
  let agents = {};
1792
3002
  try {
1793
- const agentsPath = path16.join(ctx.dataDir, "agents.json");
1794
- if (fs16.existsSync(agentsPath)) {
1795
- const data = JSON.parse(fs16.readFileSync(agentsPath, "utf-8"));
3003
+ const agentsPath = path17.join(ctx.dataDir, "agents.json");
3004
+ if (fs18.existsSync(agentsPath)) {
3005
+ const data = JSON.parse(fs18.readFileSync(agentsPath, "utf-8"));
1796
3006
  agents = data.installed ?? {};
1797
3007
  }
1798
3008
  } catch {
@@ -1830,8 +3040,8 @@ var settings_manager_exports = {};
1830
3040
  __export(settings_manager_exports, {
1831
3041
  SettingsManager: () => SettingsManager
1832
3042
  });
1833
- import fs17 from "fs";
1834
- import path17 from "path";
3043
+ import fs19 from "fs";
3044
+ import path18 from "path";
1835
3045
  var SettingsManager, SettingsAPIImpl;
1836
3046
  var init_settings_manager = __esm({
1837
3047
  "src/core/plugin/settings-manager.ts"() {
@@ -1854,7 +3064,7 @@ var init_settings_manager = __esm({
1854
3064
  async loadSettings(pluginName) {
1855
3065
  const settingsPath = this.getSettingsPath(pluginName);
1856
3066
  try {
1857
- const content = fs17.readFileSync(settingsPath, "utf-8");
3067
+ const content = fs19.readFileSync(settingsPath, "utf-8");
1858
3068
  return JSON.parse(content);
1859
3069
  } catch {
1860
3070
  return {};
@@ -1874,7 +3084,7 @@ var init_settings_manager = __esm({
1874
3084
  }
1875
3085
  /** Resolve the absolute path to a plugin's settings.json file. */
1876
3086
  getSettingsPath(pluginName) {
1877
- return path17.join(this.basePath, pluginName, "settings.json");
3087
+ return path18.join(this.basePath, pluginName, "settings.json");
1878
3088
  }
1879
3089
  async getPluginSettings(pluginName) {
1880
3090
  return this.loadSettings(pluginName);
@@ -1895,7 +3105,7 @@ var init_settings_manager = __esm({
1895
3105
  readFile() {
1896
3106
  if (this.cache !== null) return this.cache;
1897
3107
  try {
1898
- const content = fs17.readFileSync(this.settingsPath, "utf-8");
3108
+ const content = fs19.readFileSync(this.settingsPath, "utf-8");
1899
3109
  this.cache = JSON.parse(content);
1900
3110
  return this.cache;
1901
3111
  } catch {
@@ -1904,9 +3114,9 @@ var init_settings_manager = __esm({
1904
3114
  }
1905
3115
  }
1906
3116
  writeFile(data) {
1907
- const dir = path17.dirname(this.settingsPath);
1908
- fs17.mkdirSync(dir, { recursive: true });
1909
- fs17.writeFileSync(this.settingsPath, JSON.stringify(data, null, 2));
3117
+ const dir = path18.dirname(this.settingsPath);
3118
+ fs19.mkdirSync(dir, { recursive: true });
3119
+ fs19.writeFileSync(this.settingsPath, JSON.stringify(data, null, 2));
1910
3120
  this.cache = data;
1911
3121
  }
1912
3122
  async get(key) {
@@ -1941,11 +3151,12 @@ var init_settings_manager = __esm({
1941
3151
  });
1942
3152
 
1943
3153
  // src/core/doctor/checks/telegram.ts
1944
- import * as path18 from "path";
3154
+ import * as path19 from "path";
1945
3155
  var BOT_TOKEN_REGEX, telegramCheck;
1946
3156
  var init_telegram = __esm({
1947
3157
  "src/core/doctor/checks/telegram.ts"() {
1948
3158
  "use strict";
3159
+ init_network_redaction();
1949
3160
  BOT_TOKEN_REGEX = /^\d+:[A-Za-z0-9_-]{35,}$/;
1950
3161
  telegramCheck = {
1951
3162
  name: "Telegram",
@@ -1957,7 +3168,7 @@ var init_telegram = __esm({
1957
3168
  return results;
1958
3169
  }
1959
3170
  const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
1960
- const sm = new SettingsManager2(path18.join(ctx.pluginsDir, "data"));
3171
+ const sm = new SettingsManager2(path19.join(ctx.pluginsDir, "data"));
1961
3172
  const ps = await sm.loadSettings("@openacp/telegram");
1962
3173
  const botToken = ps.botToken;
1963
3174
  const chatId = ps.chatId;
@@ -1970,9 +3181,17 @@ var init_telegram = __esm({
1970
3181
  return results;
1971
3182
  }
1972
3183
  results.push({ status: "pass", message: "Bot token format valid" });
3184
+ let telegramFetch;
3185
+ try {
3186
+ telegramFetch = ctx.fetchForScope("channels.telegram");
3187
+ } catch (err) {
3188
+ const message = err instanceof Error ? err.message : String(err);
3189
+ results.push({ status: "fail", message: `Cannot initialize Telegram transport: ${redactNetworkSecrets(message)}` });
3190
+ return results;
3191
+ }
1973
3192
  let botId;
1974
3193
  try {
1975
- const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
3194
+ const res = await telegramFetch(`https://api.telegram.org/bot${botToken}/getMe`);
1976
3195
  const data = await res.json();
1977
3196
  if (data.ok && data.result) {
1978
3197
  botId = data.result.id;
@@ -1982,7 +3201,8 @@ var init_telegram = __esm({
1982
3201
  return results;
1983
3202
  }
1984
3203
  } catch (err) {
1985
- results.push({ status: "fail", message: `Cannot reach Telegram API: ${err instanceof Error ? err.message : String(err)}` });
3204
+ const message = err instanceof Error ? err.message : String(err);
3205
+ results.push({ status: "fail", message: `Cannot reach Telegram API: ${redactNetworkSecrets(message)}` });
1986
3206
  return results;
1987
3207
  }
1988
3208
  if (!chatId || chatId === 0) {
@@ -1990,7 +3210,7 @@ var init_telegram = __esm({
1990
3210
  return results;
1991
3211
  }
1992
3212
  try {
1993
- const res = await fetch(`https://api.telegram.org/bot${botToken}/getChat`, {
3213
+ const res = await telegramFetch(`https://api.telegram.org/bot${botToken}/getChat`, {
1994
3214
  method: "POST",
1995
3215
  headers: { "Content-Type": "application/json" },
1996
3216
  body: JSON.stringify({ chat_id: chatId })
@@ -2010,11 +3230,12 @@ var init_telegram = __esm({
2010
3230
  results.push({ status: "pass", message: `Chat is supergroup with topics ("${data.result.title}")` });
2011
3231
  }
2012
3232
  } catch (err) {
2013
- results.push({ status: "fail", message: `Cannot validate chat: ${err instanceof Error ? err.message : String(err)}` });
3233
+ const message = err instanceof Error ? err.message : String(err);
3234
+ results.push({ status: "fail", message: `Cannot validate chat: ${redactNetworkSecrets(message)}` });
2014
3235
  return results;
2015
3236
  }
2016
3237
  try {
2017
- const res = await fetch(`https://api.telegram.org/bot${botToken}/getChatMember`, {
3238
+ const res = await telegramFetch(`https://api.telegram.org/bot${botToken}/getChatMember`, {
2018
3239
  method: "POST",
2019
3240
  headers: { "Content-Type": "application/json" },
2020
3241
  body: JSON.stringify({ chat_id: chatId, user_id: botId })
@@ -2031,7 +3252,8 @@ var init_telegram = __esm({
2031
3252
  });
2032
3253
  }
2033
3254
  } catch (err) {
2034
- results.push({ status: "fail", message: `Admin check failed: ${err instanceof Error ? err.message : String(err)}` });
3255
+ const message = err instanceof Error ? err.message : String(err);
3256
+ results.push({ status: "fail", message: `Admin check failed: ${redactNetworkSecrets(message)}` });
2035
3257
  }
2036
3258
  return results;
2037
3259
  }
@@ -2040,7 +3262,7 @@ var init_telegram = __esm({
2040
3262
  });
2041
3263
 
2042
3264
  // src/core/doctor/checks/storage.ts
2043
- import * as fs18 from "fs";
3265
+ import * as fs20 from "fs";
2044
3266
  var storageCheck;
2045
3267
  var init_storage = __esm({
2046
3268
  "src/core/doctor/checks/storage.ts"() {
@@ -2050,28 +3272,28 @@ var init_storage = __esm({
2050
3272
  order: 4,
2051
3273
  async run(ctx) {
2052
3274
  const results = [];
2053
- if (!fs18.existsSync(ctx.dataDir)) {
3275
+ if (!fs20.existsSync(ctx.dataDir)) {
2054
3276
  results.push({
2055
3277
  status: "fail",
2056
3278
  message: "Data directory ~/.openacp does not exist",
2057
3279
  fixable: true,
2058
3280
  fixRisk: "safe",
2059
3281
  fix: async () => {
2060
- fs18.mkdirSync(ctx.dataDir, { recursive: true });
3282
+ fs20.mkdirSync(ctx.dataDir, { recursive: true });
2061
3283
  return { success: true, message: "created directory" };
2062
3284
  }
2063
3285
  });
2064
3286
  } else {
2065
3287
  try {
2066
- fs18.accessSync(ctx.dataDir, fs18.constants.W_OK);
3288
+ fs20.accessSync(ctx.dataDir, fs20.constants.W_OK);
2067
3289
  results.push({ status: "pass", message: "Data directory exists and writable" });
2068
3290
  } catch {
2069
3291
  results.push({ status: "fail", message: "Data directory not writable" });
2070
3292
  }
2071
3293
  }
2072
- if (fs18.existsSync(ctx.sessionsPath)) {
3294
+ if (fs20.existsSync(ctx.sessionsPath)) {
2073
3295
  try {
2074
- const content = fs18.readFileSync(ctx.sessionsPath, "utf-8");
3296
+ const content = fs20.readFileSync(ctx.sessionsPath, "utf-8");
2075
3297
  const data = JSON.parse(content);
2076
3298
  if (typeof data === "object" && data !== null && "sessions" in data) {
2077
3299
  results.push({ status: "pass", message: "Sessions file valid" });
@@ -2082,7 +3304,7 @@ var init_storage = __esm({
2082
3304
  fixable: true,
2083
3305
  fixRisk: "risky",
2084
3306
  fix: async () => {
2085
- fs18.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
3307
+ fs20.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
2086
3308
  return { success: true, message: "reset sessions file" };
2087
3309
  }
2088
3310
  });
@@ -2094,7 +3316,7 @@ var init_storage = __esm({
2094
3316
  fixable: true,
2095
3317
  fixRisk: "risky",
2096
3318
  fix: async () => {
2097
- fs18.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
3319
+ fs20.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
2098
3320
  return { success: true, message: "reset sessions file" };
2099
3321
  }
2100
3322
  });
@@ -2102,20 +3324,20 @@ var init_storage = __esm({
2102
3324
  } else {
2103
3325
  results.push({ status: "pass", message: "Sessions file not present yet (created on first session)" });
2104
3326
  }
2105
- if (!fs18.existsSync(ctx.logsDir)) {
3327
+ if (!fs20.existsSync(ctx.logsDir)) {
2106
3328
  results.push({
2107
3329
  status: "warn",
2108
3330
  message: "Log directory does not exist",
2109
3331
  fixable: true,
2110
3332
  fixRisk: "safe",
2111
3333
  fix: async () => {
2112
- fs18.mkdirSync(ctx.logsDir, { recursive: true });
3334
+ fs20.mkdirSync(ctx.logsDir, { recursive: true });
2113
3335
  return { success: true, message: "created log directory" };
2114
3336
  }
2115
3337
  });
2116
3338
  } else {
2117
3339
  try {
2118
- fs18.accessSync(ctx.logsDir, fs18.constants.W_OK);
3340
+ fs20.accessSync(ctx.logsDir, fs20.constants.W_OK);
2119
3341
  results.push({ status: "pass", message: "Log directory exists and writable" });
2120
3342
  } catch {
2121
3343
  results.push({ status: "fail", message: "Log directory not writable" });
@@ -2128,8 +3350,8 @@ var init_storage = __esm({
2128
3350
  });
2129
3351
 
2130
3352
  // src/core/doctor/checks/workspace.ts
2131
- import * as fs19 from "fs";
2132
- import * as path19 from "path";
3353
+ import * as fs21 from "fs";
3354
+ import * as path20 from "path";
2133
3355
  var workspaceCheck;
2134
3356
  var init_workspace = __esm({
2135
3357
  "src/core/doctor/checks/workspace.ts"() {
@@ -2139,21 +3361,21 @@ var init_workspace = __esm({
2139
3361
  order: 5,
2140
3362
  async run(ctx) {
2141
3363
  const results = [];
2142
- const workspace = path19.dirname(ctx.dataDir);
2143
- if (!fs19.existsSync(workspace)) {
3364
+ const workspace = path20.dirname(ctx.dataDir);
3365
+ if (!fs21.existsSync(workspace)) {
2144
3366
  results.push({
2145
3367
  status: "warn",
2146
3368
  message: `Workspace directory does not exist: ${workspace}`,
2147
3369
  fixable: true,
2148
3370
  fixRisk: "safe",
2149
3371
  fix: async () => {
2150
- fs19.mkdirSync(workspace, { recursive: true });
3372
+ fs21.mkdirSync(workspace, { recursive: true });
2151
3373
  return { success: true, message: "created directory" };
2152
3374
  }
2153
3375
  });
2154
3376
  } else {
2155
3377
  try {
2156
- fs19.accessSync(workspace, fs19.constants.W_OK);
3378
+ fs21.accessSync(workspace, fs21.constants.W_OK);
2157
3379
  results.push({ status: "pass", message: `Workspace directory exists: ${workspace}` });
2158
3380
  } catch {
2159
3381
  results.push({ status: "fail", message: `Workspace directory not writable: ${workspace}` });
@@ -2166,8 +3388,8 @@ var init_workspace = __esm({
2166
3388
  });
2167
3389
 
2168
3390
  // src/core/doctor/checks/plugins.ts
2169
- import * as fs20 from "fs";
2170
- import * as path20 from "path";
3391
+ import * as fs22 from "fs";
3392
+ import * as path21 from "path";
2171
3393
  var pluginsCheck;
2172
3394
  var init_plugins = __esm({
2173
3395
  "src/core/doctor/checks/plugins.ts"() {
@@ -2177,16 +3399,16 @@ var init_plugins = __esm({
2177
3399
  order: 6,
2178
3400
  async run(ctx) {
2179
3401
  const results = [];
2180
- if (!fs20.existsSync(ctx.pluginsDir)) {
3402
+ if (!fs22.existsSync(ctx.pluginsDir)) {
2181
3403
  results.push({
2182
3404
  status: "warn",
2183
3405
  message: "Plugins directory does not exist",
2184
3406
  fixable: true,
2185
3407
  fixRisk: "safe",
2186
3408
  fix: async () => {
2187
- fs20.mkdirSync(ctx.pluginsDir, { recursive: true });
2188
- fs20.writeFileSync(
2189
- path20.join(ctx.pluginsDir, "package.json"),
3409
+ fs22.mkdirSync(ctx.pluginsDir, { recursive: true });
3410
+ fs22.writeFileSync(
3411
+ path21.join(ctx.pluginsDir, "package.json"),
2190
3412
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
2191
3413
  );
2192
3414
  return { success: true, message: "initialized plugins directory" };
@@ -2195,15 +3417,15 @@ var init_plugins = __esm({
2195
3417
  return results;
2196
3418
  }
2197
3419
  results.push({ status: "pass", message: "Plugins directory exists" });
2198
- const pkgPath = path20.join(ctx.pluginsDir, "package.json");
2199
- if (!fs20.existsSync(pkgPath)) {
3420
+ const pkgPath = path21.join(ctx.pluginsDir, "package.json");
3421
+ if (!fs22.existsSync(pkgPath)) {
2200
3422
  results.push({
2201
3423
  status: "warn",
2202
3424
  message: "Plugins package.json missing",
2203
3425
  fixable: true,
2204
3426
  fixRisk: "safe",
2205
3427
  fix: async () => {
2206
- fs20.writeFileSync(
3428
+ fs22.writeFileSync(
2207
3429
  pkgPath,
2208
3430
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
2209
3431
  );
@@ -2213,7 +3435,7 @@ var init_plugins = __esm({
2213
3435
  return results;
2214
3436
  }
2215
3437
  try {
2216
- const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
3438
+ const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
2217
3439
  const deps = pkg.dependencies || {};
2218
3440
  const count = Object.keys(deps).length;
2219
3441
  results.push({ status: "pass", message: `Plugins package.json valid (${count} plugins)` });
@@ -2224,7 +3446,7 @@ var init_plugins = __esm({
2224
3446
  fixable: true,
2225
3447
  fixRisk: "risky",
2226
3448
  fix: async () => {
2227
- fs20.writeFileSync(
3449
+ fs22.writeFileSync(
2228
3450
  pkgPath,
2229
3451
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
2230
3452
  );
@@ -2239,8 +3461,8 @@ var init_plugins = __esm({
2239
3461
  });
2240
3462
 
2241
3463
  // src/core/doctor/checks/daemon.ts
2242
- import * as fs21 from "fs";
2243
- import * as net from "net";
3464
+ import * as fs23 from "fs";
3465
+ import * as net3 from "net";
2244
3466
  function isProcessAlive(pid) {
2245
3467
  try {
2246
3468
  process.kill(pid, 0);
@@ -2251,7 +3473,7 @@ function isProcessAlive(pid) {
2251
3473
  }
2252
3474
  function checkPortInUse(port) {
2253
3475
  return new Promise((resolve7) => {
2254
- const server = net.createServer();
3476
+ const server = net3.createServer();
2255
3477
  server.once("error", () => resolve7(true));
2256
3478
  server.once("listening", () => {
2257
3479
  server.close();
@@ -2269,8 +3491,8 @@ var init_daemon = __esm({
2269
3491
  order: 7,
2270
3492
  async run(ctx) {
2271
3493
  const results = [];
2272
- if (fs21.existsSync(ctx.pidPath)) {
2273
- const content = fs21.readFileSync(ctx.pidPath, "utf-8").trim();
3494
+ if (fs23.existsSync(ctx.pidPath)) {
3495
+ const content = fs23.readFileSync(ctx.pidPath, "utf-8").trim();
2274
3496
  const pid = parseInt(content, 10);
2275
3497
  if (isNaN(pid)) {
2276
3498
  results.push({
@@ -2279,7 +3501,7 @@ var init_daemon = __esm({
2279
3501
  fixable: true,
2280
3502
  fixRisk: "safe",
2281
3503
  fix: async () => {
2282
- fs21.unlinkSync(ctx.pidPath);
3504
+ fs23.unlinkSync(ctx.pidPath);
2283
3505
  return { success: true, message: "removed invalid PID file" };
2284
3506
  }
2285
3507
  });
@@ -2290,7 +3512,7 @@ var init_daemon = __esm({
2290
3512
  fixable: true,
2291
3513
  fixRisk: "safe",
2292
3514
  fix: async () => {
2293
- fs21.unlinkSync(ctx.pidPath);
3515
+ fs23.unlinkSync(ctx.pidPath);
2294
3516
  return { success: true, message: "removed stale PID file" };
2295
3517
  }
2296
3518
  });
@@ -2298,8 +3520,8 @@ var init_daemon = __esm({
2298
3520
  results.push({ status: "pass", message: `Daemon running (PID ${pid})` });
2299
3521
  }
2300
3522
  }
2301
- if (fs21.existsSync(ctx.portFilePath)) {
2302
- const content = fs21.readFileSync(ctx.portFilePath, "utf-8").trim();
3523
+ if (fs23.existsSync(ctx.portFilePath)) {
3524
+ const content = fs23.readFileSync(ctx.portFilePath, "utf-8").trim();
2303
3525
  const port = parseInt(content, 10);
2304
3526
  if (isNaN(port)) {
2305
3527
  results.push({
@@ -2308,7 +3530,7 @@ var init_daemon = __esm({
2308
3530
  fixable: true,
2309
3531
  fixRisk: "safe",
2310
3532
  fix: async () => {
2311
- fs21.unlinkSync(ctx.portFilePath);
3533
+ fs23.unlinkSync(ctx.portFilePath);
2312
3534
  return { success: true, message: "removed invalid port file" };
2313
3535
  }
2314
3536
  });
@@ -2320,8 +3542,8 @@ var init_daemon = __esm({
2320
3542
  const apiPort = 21420;
2321
3543
  const inUse = await checkPortInUse(apiPort);
2322
3544
  if (inUse) {
2323
- if (fs21.existsSync(ctx.pidPath)) {
2324
- const pid = parseInt(fs21.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
3545
+ if (fs23.existsSync(ctx.pidPath)) {
3546
+ const pid = parseInt(fs23.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
2325
3547
  if (!isNaN(pid) && isProcessAlive(pid)) {
2326
3548
  results.push({ status: "pass", message: `API port ${apiPort} in use by OpenACP daemon` });
2327
3549
  } else {
@@ -2341,17 +3563,17 @@ var init_daemon = __esm({
2341
3563
  });
2342
3564
 
2343
3565
  // src/core/utils/install-binary.ts
2344
- import fs22 from "fs";
2345
- import path21 from "path";
3566
+ import fs24 from "fs";
3567
+ import path22 from "path";
2346
3568
  import https from "https";
2347
3569
  import os5 from "os";
2348
3570
  import { execFileSync as execFileSync4 } from "child_process";
2349
3571
  function downloadFile(url, dest, maxRedirects = 10) {
2350
3572
  return new Promise((resolve7, reject) => {
2351
- const file = fs22.createWriteStream(dest);
3573
+ const file = fs24.createWriteStream(dest);
2352
3574
  const cleanup = () => {
2353
3575
  try {
2354
- if (fs22.existsSync(dest)) fs22.unlinkSync(dest);
3576
+ if (fs24.existsSync(dest)) fs24.unlinkSync(dest);
2355
3577
  } catch {
2356
3578
  }
2357
3579
  };
@@ -2417,36 +3639,36 @@ function getDownloadUrl(spec) {
2417
3639
  async function ensureBinary(spec, binDir) {
2418
3640
  const resolvedBinDir = binDir ?? DEFAULT_BIN_DIR;
2419
3641
  const binName = IS_WINDOWS ? `${spec.name}.exe` : spec.name;
2420
- const binPath = path21.join(resolvedBinDir, binName);
3642
+ const binPath = path22.join(resolvedBinDir, binName);
2421
3643
  if (commandExists(spec.name)) {
2422
3644
  log18.debug({ name: spec.name }, "Found in PATH");
2423
3645
  return spec.name;
2424
3646
  }
2425
- if (fs22.existsSync(binPath)) {
2426
- if (!IS_WINDOWS) fs22.chmodSync(binPath, "755");
3647
+ if (fs24.existsSync(binPath)) {
3648
+ if (!IS_WINDOWS) fs24.chmodSync(binPath, "755");
2427
3649
  log18.debug({ name: spec.name, path: binPath }, "Found in ~/.openacp/bin");
2428
3650
  return binPath;
2429
3651
  }
2430
3652
  log18.info({ name: spec.name }, "Not found, downloading from GitHub...");
2431
- fs22.mkdirSync(resolvedBinDir, { recursive: true });
3653
+ fs24.mkdirSync(resolvedBinDir, { recursive: true });
2432
3654
  const url = getDownloadUrl(spec);
2433
3655
  const isArchive = spec.isArchive?.(url) ?? false;
2434
- const downloadDest = isArchive ? path21.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
3656
+ const downloadDest = isArchive ? path22.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
2435
3657
  await downloadFile(url, downloadDest);
2436
3658
  if (isArchive) {
2437
3659
  const listing = execFileSync4("tar", ["-tf", downloadDest], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
2438
3660
  validateTarContents(listing, resolvedBinDir);
2439
3661
  execFileSync4("tar", ["-xzf", downloadDest, "-C", resolvedBinDir], { stdio: "pipe" });
2440
3662
  try {
2441
- fs22.unlinkSync(downloadDest);
3663
+ fs24.unlinkSync(downloadDest);
2442
3664
  } catch {
2443
3665
  }
2444
3666
  }
2445
- if (!fs22.existsSync(binPath)) {
3667
+ if (!fs24.existsSync(binPath)) {
2446
3668
  throw new Error(`${spec.name}: binary not found at ${binPath} after download/extraction. The archive structure may have changed.`);
2447
3669
  }
2448
3670
  if (!IS_WINDOWS) {
2449
- fs22.chmodSync(binPath, "755");
3671
+ fs24.chmodSync(binPath, "755");
2450
3672
  }
2451
3673
  log18.info({ name: spec.name, path: binPath }, "Installed successfully");
2452
3674
  return binPath;
@@ -2459,7 +3681,7 @@ var init_install_binary = __esm({
2459
3681
  init_agent_dependencies();
2460
3682
  init_agent_installer();
2461
3683
  log18 = createChildLogger({ module: "binary-installer" });
2462
- DEFAULT_BIN_DIR = path21.join(os5.homedir(), ".openacp", "bin");
3684
+ DEFAULT_BIN_DIR = path22.join(os5.homedir(), ".openacp", "bin");
2463
3685
  IS_WINDOWS = os5.platform() === "win32";
2464
3686
  }
2465
3687
  });
@@ -2500,8 +3722,8 @@ var init_install_cloudflared = __esm({
2500
3722
  });
2501
3723
 
2502
3724
  // src/core/doctor/checks/tunnel.ts
2503
- import * as fs23 from "fs";
2504
- import * as path22 from "path";
3725
+ import * as fs25 from "fs";
3726
+ import * as path23 from "path";
2505
3727
  import * as os6 from "os";
2506
3728
  import { execFileSync as execFileSync5 } from "child_process";
2507
3729
  var tunnelCheck;
@@ -2518,7 +3740,7 @@ var init_tunnel = __esm({
2518
3740
  return results;
2519
3741
  }
2520
3742
  const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
2521
- const sm = new SettingsManager2(path22.join(ctx.pluginsDir, "data"));
3743
+ const sm = new SettingsManager2(path23.join(ctx.pluginsDir, "data"));
2522
3744
  const tunnelSettings = await sm.loadSettings("@openacp/tunnel");
2523
3745
  const tunnelEnabled = tunnelSettings.enabled ?? false;
2524
3746
  const provider = tunnelSettings.provider ?? "cloudflare";
@@ -2530,9 +3752,9 @@ var init_tunnel = __esm({
2530
3752
  results.push({ status: "pass", message: `Tunnel provider: ${provider}` });
2531
3753
  if (provider === "cloudflare") {
2532
3754
  const binName = os6.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
2533
- const binPath = path22.join(ctx.dataDir, "bin", binName);
3755
+ const binPath = path23.join(ctx.dataDir, "bin", binName);
2534
3756
  let found = false;
2535
- if (fs23.existsSync(binPath)) {
3757
+ if (fs25.existsSync(binPath)) {
2536
3758
  found = true;
2537
3759
  } else {
2538
3760
  try {
@@ -2572,14 +3794,48 @@ var init_tunnel = __esm({
2572
3794
  }
2573
3795
  });
2574
3796
 
3797
+ // src/core/doctor/checks/proxy.ts
3798
+ var proxyCheck;
3799
+ var init_proxy = __esm({
3800
+ "src/core/doctor/checks/proxy.ts"() {
3801
+ "use strict";
3802
+ init_proxy_service();
3803
+ init_proxy_store();
3804
+ proxyCheck = {
3805
+ name: "Proxy routing",
3806
+ order: 85,
3807
+ async run(ctx) {
3808
+ try {
3809
+ if (ctx.dataDir) new ProxyStore(ctx.dataDir).load();
3810
+ } catch (error) {
3811
+ if (error instanceof ProxyStoreCorruptError) return [{ status: "fail", message: `${error.message}. Network consumers are fail-closed; inspect quarantine/LKG before recovery.` }];
3812
+ throw error;
3813
+ }
3814
+ const variables = PROXY_ENV_KEYS.filter((key) => {
3815
+ const value = process.env[key];
3816
+ return !["NO_PROXY", "no_proxy", "NODE_USE_ENV_PROXY"].includes(key) && value !== void 0 && value !== "";
3817
+ });
3818
+ if (!variables.length) {
3819
+ return [{ status: "pass", message: "No daemon-wide proxy environment; native scoped routing is authoritative" }];
3820
+ }
3821
+ return [{
3822
+ status: "warn",
3823
+ message: `Daemon-wide proxy environment active (${variables.join(", ")}); scoped routing is in compatibility mode until legacy wrapper/drop-in injection is removed`
3824
+ }];
3825
+ }
3826
+ };
3827
+ }
3828
+ });
3829
+
2575
3830
  // src/core/doctor/index.ts
2576
- import * as fs24 from "fs";
2577
- import * as path23 from "path";
3831
+ import * as fs26 from "fs";
3832
+ import * as path24 from "path";
2578
3833
  var ALL_CHECKS, CHECK_TIMEOUT_MS, DoctorEngine;
2579
3834
  var init_doctor = __esm({
2580
3835
  "src/core/doctor/index.ts"() {
2581
3836
  "use strict";
2582
3837
  init_config();
3838
+ init_proxy_service();
2583
3839
  init_config2();
2584
3840
  init_agents();
2585
3841
  init_telegram();
@@ -2588,6 +3844,7 @@ var init_doctor = __esm({
2588
3844
  init_plugins();
2589
3845
  init_daemon();
2590
3846
  init_tunnel();
3847
+ init_proxy();
2591
3848
  ALL_CHECKS = [
2592
3849
  configCheck,
2593
3850
  agentsCheck,
@@ -2596,15 +3853,18 @@ var init_doctor = __esm({
2596
3853
  workspaceCheck,
2597
3854
  pluginsCheck,
2598
3855
  daemonCheck,
2599
- tunnelCheck
3856
+ tunnelCheck,
3857
+ proxyCheck
2600
3858
  ];
2601
3859
  CHECK_TIMEOUT_MS = 1e4;
2602
3860
  DoctorEngine = class {
2603
3861
  dryRun;
2604
3862
  dataDir;
3863
+ proxyService;
2605
3864
  constructor(options) {
2606
3865
  this.dryRun = options?.dryRun ?? false;
2607
3866
  this.dataDir = options.dataDir;
3867
+ this.proxyService = options?.proxyService ?? new ProxyService(this.dataDir);
2608
3868
  }
2609
3869
  /**
2610
3870
  * Executes all checks and returns an aggregated report.
@@ -2662,28 +3922,29 @@ var init_doctor = __esm({
2662
3922
  /** Constructs the shared context used by all checks — loads config if available. */
2663
3923
  async buildContext() {
2664
3924
  const dataDir = this.dataDir;
2665
- const configPath = process.env.OPENACP_CONFIG_PATH || path23.join(dataDir, "config.json");
3925
+ const configPath = process.env.OPENACP_CONFIG_PATH || path24.join(dataDir, "config.json");
2666
3926
  let config = null;
2667
3927
  let rawConfig = null;
2668
3928
  try {
2669
- const content = fs24.readFileSync(configPath, "utf-8");
3929
+ const content = fs26.readFileSync(configPath, "utf-8");
2670
3930
  rawConfig = JSON.parse(content);
2671
3931
  const cm = new ConfigManager(configPath);
2672
3932
  await cm.load();
2673
3933
  config = cm.get();
2674
3934
  } catch {
2675
3935
  }
2676
- const logsDir = config ? expandHome2(config.logging.logDir) : path23.join(dataDir, "logs");
3936
+ const logsDir = config ? expandHome2(config.logging.logDir) : path24.join(dataDir, "logs");
2677
3937
  return {
2678
3938
  config,
2679
3939
  rawConfig,
2680
3940
  configPath,
2681
3941
  dataDir,
2682
- sessionsPath: path23.join(dataDir, "sessions.json"),
2683
- pidPath: path23.join(dataDir, "openacp.pid"),
2684
- portFilePath: path23.join(dataDir, "api.port"),
2685
- pluginsDir: path23.join(dataDir, "plugins"),
2686
- logsDir
3942
+ sessionsPath: path24.join(dataDir, "sessions.json"),
3943
+ pidPath: path24.join(dataDir, "openacp.pid"),
3944
+ portFilePath: path24.join(dataDir, "api.port"),
3945
+ pluginsDir: path24.join(dataDir, "plugins"),
3946
+ logsDir,
3947
+ fetchForScope: (scope) => this.proxyService.createFetch(scope)
2687
3948
  };
2688
3949
  }
2689
3950
  };
@@ -2691,11 +3952,11 @@ var init_doctor = __esm({
2691
3952
  });
2692
3953
 
2693
3954
  // src/core/instance/instance-context.ts
2694
- import path25 from "path";
2695
- import fs26 from "fs";
3955
+ import path26 from "path";
3956
+ import fs28 from "fs";
2696
3957
  import os8 from "os";
2697
3958
  function getGlobalRoot() {
2698
- return path25.join(os8.homedir(), ".openacp");
3959
+ return path26.join(os8.homedir(), ".openacp");
2699
3960
  }
2700
3961
  var init_instance_context = __esm({
2701
3962
  "src/core/instance/instance-context.ts"() {
@@ -2704,12 +3965,12 @@ var init_instance_context = __esm({
2704
3965
  });
2705
3966
 
2706
3967
  // src/core/instance/instance-registry.ts
2707
- import fs27 from "fs";
2708
- import path26 from "path";
3968
+ import fs29 from "fs";
3969
+ import path27 from "path";
2709
3970
  import { randomUUID as randomUUID2 } from "crypto";
2710
3971
  function readIdFromConfig(instanceRoot) {
2711
3972
  try {
2712
- const raw = JSON.parse(fs27.readFileSync(path26.join(instanceRoot, "config.json"), "utf-8"));
3973
+ const raw = JSON.parse(fs29.readFileSync(path27.join(instanceRoot, "config.json"), "utf-8"));
2713
3974
  return typeof raw.id === "string" && raw.id ? raw.id : null;
2714
3975
  } catch {
2715
3976
  return null;
@@ -2728,7 +3989,7 @@ var init_instance_registry = __esm({
2728
3989
  /** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
2729
3990
  load() {
2730
3991
  try {
2731
- const raw = fs27.readFileSync(this.registryPath, "utf-8");
3992
+ const raw = fs29.readFileSync(this.registryPath, "utf-8");
2732
3993
  const parsed = JSON.parse(raw);
2733
3994
  if (parsed.version === 1 && parsed.instances) {
2734
3995
  this.data = parsed;
@@ -2755,9 +4016,9 @@ var init_instance_registry = __esm({
2755
4016
  }
2756
4017
  /** Persist the registry to disk, creating parent directories if needed. */
2757
4018
  save() {
2758
- const dir = path26.dirname(this.registryPath);
2759
- fs27.mkdirSync(dir, { recursive: true });
2760
- fs27.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
4019
+ const dir = path27.dirname(this.registryPath);
4020
+ fs29.mkdirSync(dir, { recursive: true });
4021
+ fs29.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
2761
4022
  }
2762
4023
  /** Add or update an instance entry in the registry. Does not persist — call save() after. */
2763
4024
  register(id, root) {
@@ -2824,15 +4085,15 @@ __export(plugin_installer_exports, {
2824
4085
  });
2825
4086
  import { execFile as execFile2 } from "child_process";
2826
4087
  import { promisify as promisify2 } from "util";
2827
- import * as fs29 from "fs/promises";
2828
- import * as path28 from "path";
4088
+ import * as fs31 from "fs/promises";
4089
+ import * as path29 from "path";
2829
4090
  import { pathToFileURL } from "url";
2830
4091
  async function importFromDir(packageName, dir) {
2831
- const pkgDir = path28.join(dir, "node_modules", ...packageName.split("/"));
2832
- const pkgJsonPath = path28.join(pkgDir, "package.json");
4092
+ const pkgDir = path29.join(dir, "node_modules", ...packageName.split("/"));
4093
+ const pkgJsonPath = path29.join(pkgDir, "package.json");
2833
4094
  let pkgJson;
2834
4095
  try {
2835
- pkgJson = JSON.parse(await fs29.readFile(pkgJsonPath, "utf-8"));
4096
+ pkgJson = JSON.parse(await fs31.readFile(pkgJsonPath, "utf-8"));
2836
4097
  } catch (err) {
2837
4098
  throw new Error(`Cannot read package.json for "${packageName}" at ${pkgJsonPath}: ${err.message}`);
2838
4099
  }
@@ -2845,15 +4106,15 @@ async function importFromDir(packageName, dir) {
2845
4106
  } else {
2846
4107
  entry = pkgJson.main ?? "index.js";
2847
4108
  }
2848
- const entryPath = path28.join(pkgDir, entry);
4109
+ const entryPath = path29.join(pkgDir, entry);
2849
4110
  try {
2850
- await fs29.access(entryPath);
4111
+ await fs31.access(entryPath);
2851
4112
  } catch {
2852
4113
  throw new Error(`Entry point "${entry}" not found for "${packageName}" at ${entryPath}`);
2853
4114
  }
2854
4115
  return import(pathToFileURL(entryPath).href);
2855
4116
  }
2856
- async function installNpmPlugin(packageName, pluginsDir) {
4117
+ async function installNpmPlugin(packageName, pluginsDir, childEnv) {
2857
4118
  if (!VALID_NPM_NAME.test(packageName)) {
2858
4119
  throw new Error(`Invalid package name: "${packageName}". Must be a valid npm package name.`);
2859
4120
  }
@@ -2863,7 +4124,8 @@ async function installNpmPlugin(packageName, pluginsDir) {
2863
4124
  } catch {
2864
4125
  }
2865
4126
  await execFileAsync2("npm", ["install", packageName, "--prefix", dir, "--save", "--ignore-scripts"], {
2866
- timeout: 6e4
4127
+ timeout: 6e4,
4128
+ env: childEnv
2867
4129
  });
2868
4130
  return await importFromDir(packageName, dir);
2869
4131
  }
@@ -2939,10 +4201,10 @@ var install_context_exports = {};
2939
4201
  __export(install_context_exports, {
2940
4202
  createInstallContext: () => createInstallContext
2941
4203
  });
2942
- import path29 from "path";
4204
+ import path30 from "path";
2943
4205
  function createInstallContext(opts) {
2944
4206
  const { pluginName, settingsManager, basePath, instanceRoot } = opts;
2945
- const dataDir = path29.join(basePath, pluginName, "data");
4207
+ const dataDir = path30.join(basePath, pluginName, "data");
2946
4208
  return {
2947
4209
  pluginName,
2948
4210
  terminal: createTerminalIO(),
@@ -2967,16 +4229,17 @@ __export(api_client_exports, {
2967
4229
  readApiPort: () => readApiPort,
2968
4230
  readApiSecret: () => readApiSecret,
2969
4231
  removeStalePortFile: () => removeStalePortFile,
4232
+ waitForApiReady: () => waitForApiReady,
2970
4233
  waitForPortFile: () => waitForPortFile
2971
4234
  });
2972
- import * as fs30 from "fs";
2973
- import * as path30 from "path";
4235
+ import * as fs32 from "fs";
4236
+ import * as path31 from "path";
2974
4237
  import { setTimeout as sleep } from "timers/promises";
2975
4238
  function defaultPortFile(root) {
2976
- return path30.join(root, "api.port");
4239
+ return path31.join(root, "api.port");
2977
4240
  }
2978
4241
  function defaultSecretFile(root) {
2979
- return path30.join(root, "api-secret");
4242
+ return path31.join(root, "api-secret");
2980
4243
  }
2981
4244
  async function waitForPortFile(portFilePath, timeoutMs = 5e3, intervalMs = 100) {
2982
4245
  const deadline = Date.now() + timeoutMs;
@@ -2987,10 +4250,30 @@ async function waitForPortFile(portFilePath, timeoutMs = 5e3, intervalMs = 100)
2987
4250
  }
2988
4251
  return readApiPort(portFilePath);
2989
4252
  }
4253
+ async function waitForApiReady(instanceRoot, expectedInstanceId, timeoutMs = 1e4, intervalMs = 100) {
4254
+ const deadline = Date.now() + timeoutMs;
4255
+ while (Date.now() < deadline) {
4256
+ const port = readApiPort(void 0, instanceRoot);
4257
+ if (port !== null) {
4258
+ try {
4259
+ const response = await fetch(`http://127.0.0.1:${port}/api/v1/system/health`, {
4260
+ signal: AbortSignal.timeout(Math.max(intervalMs, 250))
4261
+ });
4262
+ if (response.ok) {
4263
+ const body = await response.json();
4264
+ if (body.status === "ok" && body.instanceId === expectedInstanceId) return port;
4265
+ }
4266
+ } catch {
4267
+ }
4268
+ }
4269
+ await sleep(intervalMs);
4270
+ }
4271
+ return null;
4272
+ }
2990
4273
  function readApiPort(portFilePath, instanceRoot) {
2991
4274
  const filePath = portFilePath ?? defaultPortFile(instanceRoot);
2992
4275
  try {
2993
- const content = fs30.readFileSync(filePath, "utf-8").trim();
4276
+ const content = fs32.readFileSync(filePath, "utf-8").trim();
2994
4277
  const port = parseInt(content, 10);
2995
4278
  return isNaN(port) ? null : port;
2996
4279
  } catch {
@@ -3000,7 +4283,7 @@ function readApiPort(portFilePath, instanceRoot) {
3000
4283
  function readApiSecret(secretFilePath, instanceRoot) {
3001
4284
  const filePath = secretFilePath ?? defaultSecretFile(instanceRoot);
3002
4285
  try {
3003
- const content = fs30.readFileSync(filePath, "utf-8").trim();
4286
+ const content = fs32.readFileSync(filePath, "utf-8").trim();
3004
4287
  return content || null;
3005
4288
  } catch {
3006
4289
  return null;
@@ -3009,7 +4292,7 @@ function readApiSecret(secretFilePath, instanceRoot) {
3009
4292
  function removeStalePortFile(portFilePath, instanceRoot) {
3010
4293
  const filePath = portFilePath ?? defaultPortFile(instanceRoot);
3011
4294
  try {
3012
- fs30.unlinkSync(filePath);
4295
+ fs32.unlinkSync(filePath);
3013
4296
  } catch {
3014
4297
  }
3015
4298
  }
@@ -3146,8 +4429,8 @@ var init_notification = __esm({
3146
4429
  });
3147
4430
 
3148
4431
  // src/plugins/file-service/file-service.ts
3149
- import fs32 from "fs";
3150
- import path33 from "path";
4432
+ import fs34 from "fs";
4433
+ import path34 from "path";
3151
4434
  import { OggOpusDecoder } from "ogg-opus-decoder";
3152
4435
  import wav from "node-wav";
3153
4436
  function classifyMime(mimeType) {
@@ -3204,14 +4487,14 @@ var init_file_service = __esm({
3204
4487
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
3205
4488
  let removed = 0;
3206
4489
  try {
3207
- const entries = await fs32.promises.readdir(this.baseDir, { withFileTypes: true });
4490
+ const entries = await fs34.promises.readdir(this.baseDir, { withFileTypes: true });
3208
4491
  for (const entry of entries) {
3209
4492
  if (!entry.isDirectory()) continue;
3210
- const dirPath = path33.join(this.baseDir, entry.name);
4493
+ const dirPath = path34.join(this.baseDir, entry.name);
3211
4494
  try {
3212
- const stat = await fs32.promises.stat(dirPath);
4495
+ const stat = await fs34.promises.stat(dirPath);
3213
4496
  if (stat.mtimeMs < cutoff) {
3214
- await fs32.promises.rm(dirPath, { recursive: true, force: true });
4497
+ await fs34.promises.rm(dirPath, { recursive: true, force: true });
3215
4498
  removed++;
3216
4499
  }
3217
4500
  } catch {
@@ -3230,11 +4513,11 @@ var init_file_service = __esm({
3230
4513
  * so the user-facing name is not lost.
3231
4514
  */
3232
4515
  async saveFile(sessionId, fileName, data, mimeType) {
3233
- const sessionDir = path33.join(this.baseDir, sessionId);
3234
- await fs32.promises.mkdir(sessionDir, { recursive: true });
4516
+ const sessionDir = path34.join(this.baseDir, sessionId);
4517
+ await fs34.promises.mkdir(sessionDir, { recursive: true });
3235
4518
  const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
3236
- const filePath = path33.join(sessionDir, safeName);
3237
- await fs32.promises.writeFile(filePath, data);
4519
+ const filePath = path34.join(sessionDir, safeName);
4520
+ await fs34.promises.writeFile(filePath, data);
3238
4521
  return {
3239
4522
  type: classifyMime(mimeType),
3240
4523
  filePath,
@@ -3249,14 +4532,14 @@ var init_file_service = __esm({
3249
4532
  */
3250
4533
  async resolveFile(filePath) {
3251
4534
  try {
3252
- const stat = await fs32.promises.stat(filePath);
4535
+ const stat = await fs34.promises.stat(filePath);
3253
4536
  if (!stat.isFile()) return null;
3254
- const ext = path33.extname(filePath).toLowerCase();
4537
+ const ext = path34.extname(filePath).toLowerCase();
3255
4538
  const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
3256
4539
  return {
3257
4540
  type: classifyMime(mimeType),
3258
4541
  filePath,
3259
- fileName: path33.basename(filePath),
4542
+ fileName: path34.basename(filePath),
3260
4543
  mimeType,
3261
4544
  size: stat.size
3262
4545
  };
@@ -3375,6 +4658,10 @@ function globalErrorHandler(error, _request, reply) {
3375
4658
  });
3376
4659
  return;
3377
4660
  }
4661
+ if (error instanceof ConflictError) {
4662
+ reply.status(409).send({ error: { code: error.code, message: error.message, statusCode: 409 } });
4663
+ return;
4664
+ }
3378
4665
  if (error instanceof NotFoundError) {
3379
4666
  reply.status(404).send({
3380
4667
  error: {
@@ -3413,7 +4700,7 @@ function globalErrorHandler(error, _request, reply) {
3413
4700
  }
3414
4701
  });
3415
4702
  }
3416
- var NotFoundError, BadRequestError, ServiceUnavailableError, AuthError;
4703
+ var NotFoundError, BadRequestError, ConflictError, ServiceUnavailableError, AuthError;
3417
4704
  var init_error_handler = __esm({
3418
4705
  "src/plugins/api-server/middleware/error-handler.ts"() {
3419
4706
  "use strict";
@@ -3434,6 +4721,14 @@ var init_error_handler = __esm({
3434
4721
  }
3435
4722
  code;
3436
4723
  };
4724
+ ConflictError = class extends Error {
4725
+ constructor(code, message) {
4726
+ super(message);
4727
+ this.code = code;
4728
+ this.name = "ConflictError";
4729
+ }
4730
+ code;
4731
+ };
3437
4732
  ServiceUnavailableError = class extends Error {
3438
4733
  constructor(code, message) {
3439
4734
  super(message);
@@ -3887,8 +5182,8 @@ data: ${JSON.stringify(data)}
3887
5182
  });
3888
5183
 
3889
5184
  // src/plugins/api-server/static-server.ts
3890
- import * as fs33 from "fs";
3891
- import * as path34 from "path";
5185
+ import * as fs35 from "fs";
5186
+ import * as path35 from "path";
3892
5187
  import { fileURLToPath as fileURLToPath2 } from "url";
3893
5188
  var MIME_TYPES, StaticServer;
3894
5189
  var init_static_server = __esm({
@@ -3912,16 +5207,16 @@ var init_static_server = __esm({
3912
5207
  this.uiDir = uiDir;
3913
5208
  if (!this.uiDir) {
3914
5209
  const __filename = fileURLToPath2(import.meta.url);
3915
- const candidate = path34.resolve(path34.dirname(__filename), "../../ui/dist");
3916
- if (fs33.existsSync(path34.join(candidate, "index.html"))) {
5210
+ const candidate = path35.resolve(path35.dirname(__filename), "../../ui/dist");
5211
+ if (fs35.existsSync(path35.join(candidate, "index.html"))) {
3917
5212
  this.uiDir = candidate;
3918
5213
  }
3919
5214
  if (!this.uiDir) {
3920
- const publishCandidate = path34.resolve(
3921
- path34.dirname(__filename),
5215
+ const publishCandidate = path35.resolve(
5216
+ path35.dirname(__filename),
3922
5217
  "../ui"
3923
5218
  );
3924
- if (fs33.existsSync(path34.join(publishCandidate, "index.html"))) {
5219
+ if (fs35.existsSync(path35.join(publishCandidate, "index.html"))) {
3925
5220
  this.uiDir = publishCandidate;
3926
5221
  }
3927
5222
  }
@@ -3939,23 +5234,23 @@ var init_static_server = __esm({
3939
5234
  serve(req, res) {
3940
5235
  if (!this.uiDir) return false;
3941
5236
  const urlPath = (req.url || "/").split("?")[0];
3942
- const safePath = path34.normalize(urlPath);
3943
- const filePath = path34.join(this.uiDir, safePath);
3944
- if (!filePath.startsWith(this.uiDir + path34.sep) && filePath !== this.uiDir)
5237
+ const safePath = path35.normalize(urlPath);
5238
+ const filePath = path35.join(this.uiDir, safePath);
5239
+ if (!filePath.startsWith(this.uiDir + path35.sep) && filePath !== this.uiDir)
3945
5240
  return false;
3946
5241
  let realFilePath;
3947
5242
  try {
3948
- realFilePath = fs33.realpathSync(filePath);
5243
+ realFilePath = fs35.realpathSync(filePath);
3949
5244
  } catch {
3950
5245
  realFilePath = null;
3951
5246
  }
3952
5247
  if (realFilePath !== null) {
3953
- const realUiDir = fs33.realpathSync(this.uiDir);
3954
- if (!realFilePath.startsWith(realUiDir + path34.sep) && realFilePath !== realUiDir)
5248
+ const realUiDir = fs35.realpathSync(this.uiDir);
5249
+ if (!realFilePath.startsWith(realUiDir + path35.sep) && realFilePath !== realUiDir)
3955
5250
  return false;
3956
5251
  }
3957
- if (realFilePath !== null && fs33.existsSync(realFilePath) && fs33.statSync(realFilePath).isFile()) {
3958
- const ext = path34.extname(filePath);
5252
+ if (realFilePath !== null && fs35.existsSync(realFilePath) && fs35.statSync(realFilePath).isFile()) {
5253
+ const ext = path35.extname(filePath);
3959
5254
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
3960
5255
  const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
3961
5256
  const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
@@ -3963,16 +5258,16 @@ var init_static_server = __esm({
3963
5258
  "Content-Type": contentType,
3964
5259
  "Cache-Control": cacheControl
3965
5260
  });
3966
- fs33.createReadStream(realFilePath).pipe(res);
5261
+ fs35.createReadStream(realFilePath).pipe(res);
3967
5262
  return true;
3968
5263
  }
3969
- const indexPath = path34.join(this.uiDir, "index.html");
3970
- if (fs33.existsSync(indexPath)) {
5264
+ const indexPath = path35.join(this.uiDir, "index.html");
5265
+ if (fs35.existsSync(indexPath)) {
3971
5266
  res.writeHead(200, {
3972
5267
  "Content-Type": "text/html; charset=utf-8",
3973
5268
  "Cache-Control": "no-cache"
3974
5269
  });
3975
- fs33.createReadStream(indexPath).pipe(res);
5270
+ fs35.createReadStream(indexPath).pipe(res);
3976
5271
  return true;
3977
5272
  }
3978
5273
  return false;
@@ -4095,8 +5390,8 @@ var init_exports = __esm({
4095
5390
  });
4096
5391
 
4097
5392
  // src/plugins/context/context-cache.ts
4098
- import * as fs34 from "fs";
4099
- import * as path35 from "path";
5393
+ import * as fs36 from "fs";
5394
+ import * as path36 from "path";
4100
5395
  import * as crypto2 from "crypto";
4101
5396
  var DEFAULT_TTL_MS, ContextCache;
4102
5397
  var init_context_cache = __esm({
@@ -4107,7 +5402,7 @@ var init_context_cache = __esm({
4107
5402
  constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
4108
5403
  this.cacheDir = cacheDir;
4109
5404
  this.ttlMs = ttlMs;
4110
- fs34.mkdirSync(cacheDir, { recursive: true });
5405
+ fs36.mkdirSync(cacheDir, { recursive: true });
4111
5406
  }
4112
5407
  cacheDir;
4113
5408
  ttlMs;
@@ -4115,23 +5410,23 @@ var init_context_cache = __esm({
4115
5410
  return crypto2.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
4116
5411
  }
4117
5412
  filePath(repoPath, queryKey) {
4118
- return path35.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
5413
+ return path36.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
4119
5414
  }
4120
5415
  get(repoPath, queryKey) {
4121
5416
  const fp = this.filePath(repoPath, queryKey);
4122
5417
  try {
4123
- const stat = fs34.statSync(fp);
5418
+ const stat = fs36.statSync(fp);
4124
5419
  if (Date.now() - stat.mtimeMs > this.ttlMs) {
4125
- fs34.unlinkSync(fp);
5420
+ fs36.unlinkSync(fp);
4126
5421
  return null;
4127
5422
  }
4128
- return JSON.parse(fs34.readFileSync(fp, "utf-8"));
5423
+ return JSON.parse(fs36.readFileSync(fp, "utf-8"));
4129
5424
  } catch {
4130
5425
  return null;
4131
5426
  }
4132
5427
  }
4133
5428
  set(repoPath, queryKey, result) {
4134
- fs34.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
5429
+ fs36.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
4135
5430
  }
4136
5431
  };
4137
5432
  }
@@ -5105,8 +6400,8 @@ function formatToolSummary(name, rawInput, displaySummary) {
5105
6400
  }
5106
6401
  if (lowerName === "grep") {
5107
6402
  const pattern = args.pattern ?? "";
5108
- const path36 = args.path ?? "";
5109
- return pattern ? `\u{1F50D} Grep "${pattern}"${path36 ? ` in ${path36}` : ""}` : `\u{1F527} ${name}`;
6403
+ const path37 = args.path ?? "";
6404
+ return pattern ? `\u{1F50D} Grep "${pattern}"${path37 ? ` in ${path37}` : ""}` : `\u{1F527} ${name}`;
5110
6405
  }
5111
6406
  if (lowerName === "glob") {
5112
6407
  const pattern = args.pattern ?? "";
@@ -5142,8 +6437,8 @@ function formatToolTitle(name, rawInput, displayTitle) {
5142
6437
  }
5143
6438
  if (lowerName === "grep") {
5144
6439
  const pattern = args.pattern ?? "";
5145
- const path36 = args.path ?? "";
5146
- return pattern ? `"${pattern}"${path36 ? ` in ${path36}` : ""}` : name;
6440
+ const path37 = args.path ?? "";
6441
+ return pattern ? `"${pattern}"${path37 ? ` in ${path37}` : ""}` : name;
5147
6442
  }
5148
6443
  if (lowerName === "glob") {
5149
6444
  return String(args.pattern ?? name);
@@ -6454,11 +7749,23 @@ __export(version_exports, {
6454
7749
  compareVersions: () => compareVersions,
6455
7750
  getCurrentVersion: () => getCurrentVersion,
6456
7751
  getLatestVersion: () => getLatestVersion,
7752
+ getUpdateNetwork: () => getUpdateNetwork,
6457
7753
  runUpdate: () => runUpdate
6458
7754
  });
6459
7755
  import { fileURLToPath as fileURLToPath3 } from "url";
6460
7756
  import { dirname as dirname12, join as join16, resolve as resolve6 } from "path";
6461
- import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
7757
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
7758
+ function getUpdateNetwork(instanceRoot) {
7759
+ if (!instanceRoot) return {};
7760
+ const proxy = new ProxyService(instanceRoot);
7761
+ const inherited = Object.fromEntries(
7762
+ Object.entries(process.env).filter((entry) => entry[1] !== void 0)
7763
+ );
7764
+ return {
7765
+ fetcher: proxy.createFetch("services.npmUpdate"),
7766
+ environment: proxy.buildChildEnv("services.npmUpdate", inherited)
7767
+ };
7768
+ }
6462
7769
  function findPackageJson() {
6463
7770
  let dir = dirname12(fileURLToPath3(import.meta.url));
6464
7771
  for (let i = 0; i < 5; i++) {
@@ -6474,15 +7781,15 @@ function getCurrentVersion() {
6474
7781
  try {
6475
7782
  const pkgPath = findPackageJson();
6476
7783
  if (!pkgPath) return "0.0.0-dev";
6477
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
7784
+ const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
6478
7785
  return pkg.version;
6479
7786
  } catch {
6480
7787
  return "0.0.0-dev";
6481
7788
  }
6482
7789
  }
6483
- async function getLatestVersion() {
7790
+ async function getLatestVersion(fetcher = globalThis.fetch) {
6484
7791
  try {
6485
- const res = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`, {
7792
+ const res = await fetcher(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`, {
6486
7793
  signal: AbortSignal.timeout(5e3)
6487
7794
  });
6488
7795
  if (!res.ok) return null;
@@ -6501,12 +7808,13 @@ function compareVersions(current, latest) {
6501
7808
  }
6502
7809
  return 0;
6503
7810
  }
6504
- async function runUpdate() {
7811
+ async function runUpdate(environment) {
6505
7812
  const { spawn: spawn4 } = await import("child_process");
6506
7813
  return new Promise((resolve7) => {
6507
7814
  const child = spawn4("npm", ["install", "-g", `${NPM_PACKAGE}@latest`], {
6508
7815
  stdio: "inherit",
6509
- shell: false
7816
+ shell: false,
7817
+ env: environment
6510
7818
  });
6511
7819
  const onSignal = () => {
6512
7820
  child.kill("SIGTERM");
@@ -6521,11 +7829,12 @@ async function runUpdate() {
6521
7829
  });
6522
7830
  });
6523
7831
  }
6524
- async function checkAndPromptUpdate() {
7832
+ async function checkAndPromptUpdate(instanceRoot) {
6525
7833
  if (process.env.OPENACP_DEV_LOOP || process.env.OPENACP_SKIP_UPDATE_CHECK || !process.stdin.isTTY) return;
6526
7834
  const current = getCurrentVersion();
6527
7835
  if (current === "0.0.0-dev") return;
6528
- const latest = await getLatestVersion();
7836
+ const network = getUpdateNetwork(instanceRoot);
7837
+ const latest = await getLatestVersion(network.fetcher);
6529
7838
  if (!latest || compareVersions(current, latest) >= 0) return;
6530
7839
  console.log(`\x1B[33mUpdate available: v${current} \u2192 v${latest}\x1B[0m`);
6531
7840
  const clack3 = await import("@clack/prompts");
@@ -6535,7 +7844,7 @@ async function checkAndPromptUpdate() {
6535
7844
  if (clack3.isCancel(yes) || !yes) {
6536
7845
  return;
6537
7846
  }
6538
- const ok2 = await runUpdate();
7847
+ const ok2 = await runUpdate(network.environment);
6539
7848
  if (ok2) {
6540
7849
  console.log(`\x1B[32m\u2713 Updated to v${latest}. Please re-run your command.\x1B[0m`);
6541
7850
  process.exit(0);
@@ -6547,6 +7856,7 @@ var NPM_PACKAGE;
6547
7856
  var init_version = __esm({
6548
7857
  "src/cli/version.ts"() {
6549
7858
  "use strict";
7859
+ init_proxy_service();
6550
7860
  NPM_PACKAGE = "@n1creator/openacp-cli";
6551
7861
  }
6552
7862
  });
@@ -6729,7 +8039,8 @@ async function handleUpdate(ctx, core) {
6729
8039
  `\u{1F50D} Checking for updates... (current: v${escapeHtml(current)})`,
6730
8040
  { parse_mode: "HTML" }
6731
8041
  );
6732
- const latest = await getLatestVersion2();
8042
+ const updateFetch = core.proxyService.createFetch("services.npmUpdate");
8043
+ const latest = await getLatestVersion2(updateFetch);
6733
8044
  if (!latest) {
6734
8045
  await ctx.api.editMessageText(
6735
8046
  ctx.chat.id,
@@ -6754,7 +8065,11 @@ async function handleUpdate(ctx, core) {
6754
8065
  `\u2B07\uFE0F Updating v${escapeHtml(current)} \u2192 v${escapeHtml(latest)}...`,
6755
8066
  { parse_mode: "HTML" }
6756
8067
  );
6757
- const ok2 = await runUpdate2();
8068
+ const updateEnv = core.proxyService.buildChildEnv(
8069
+ "services.npmUpdate",
8070
+ Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== void 0))
8071
+ );
8072
+ const ok2 = await runUpdate2(updateEnv);
6758
8073
  if (!ok2) {
6759
8074
  await ctx.api.editMessageText(
6760
8075
  ctx.chat.id,
@@ -7341,7 +8656,7 @@ __export(integrate_exports, {
7341
8656
  listIntegrations: () => listIntegrations,
7342
8657
  uninstallIntegration: () => uninstallIntegration
7343
8658
  });
7344
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, chmodSync, rmdirSync } from "fs";
8659
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, chmodSync, rmdirSync } from "fs";
7345
8660
  import { join as join17, dirname as dirname13 } from "path";
7346
8661
  import { homedir as homedir4 } from "os";
7347
8662
  function isHooksIntegrationSpec(spec) {
@@ -7504,7 +8819,7 @@ function mergeSettingsJson(settingsPath, hookEvent, hookScriptPath) {
7504
8819
  const fullPath = expandPath(settingsPath);
7505
8820
  let settings = {};
7506
8821
  if (existsSync18(fullPath)) {
7507
- const raw = readFileSync15(fullPath, "utf-8");
8822
+ const raw = readFileSync16(fullPath, "utf-8");
7508
8823
  writeFileSync11(`${fullPath}.bak`, raw);
7509
8824
  settings = JSON.parse(raw);
7510
8825
  }
@@ -7527,7 +8842,7 @@ function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
7527
8842
  const fullPath = expandPath(settingsPath);
7528
8843
  let config = { version: 1 };
7529
8844
  if (existsSync18(fullPath)) {
7530
- const raw = readFileSync15(fullPath, "utf-8");
8845
+ const raw = readFileSync16(fullPath, "utf-8");
7531
8846
  writeFileSync11(`${fullPath}.bak`, raw);
7532
8847
  config = JSON.parse(raw);
7533
8848
  }
@@ -7545,7 +8860,7 @@ function mergeHooksJson(settingsPath, hookEvent, hookScriptPath) {
7545
8860
  function removeFromSettingsJson(settingsPath, hookEvent) {
7546
8861
  const fullPath = expandPath(settingsPath);
7547
8862
  if (!existsSync18(fullPath)) return;
7548
- const raw = readFileSync15(fullPath, "utf-8");
8863
+ const raw = readFileSync16(fullPath, "utf-8");
7549
8864
  const settings = JSON.parse(raw);
7550
8865
  const hooks = settings.hooks;
7551
8866
  if (!hooks?.[hookEvent]) return;
@@ -7560,7 +8875,7 @@ function removeFromSettingsJson(settingsPath, hookEvent) {
7560
8875
  function removeFromHooksJson(settingsPath, hookEvent) {
7561
8876
  const fullPath = expandPath(settingsPath);
7562
8877
  if (!existsSync18(fullPath)) return;
7563
- const raw = readFileSync15(fullPath, "utf-8");
8878
+ const raw = readFileSync16(fullPath, "utf-8");
7564
8879
  const config = JSON.parse(raw);
7565
8880
  const hooks = config.hooks;
7566
8881
  if (!hooks?.[hookEvent]) return;
@@ -7999,6 +9314,7 @@ Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
7999
9314
  };
8000
9315
  const result = await catalog.install(nameOrId, progress);
8001
9316
  if (result.ok) {
9317
+ core.proxyService.registerScope(`agents.${result.agentKey}`);
8002
9318
  const { getAgentCapabilities: getAgentCapabilities2 } = await Promise.resolve().then(() => (init_agent_dependencies(), agent_dependencies_exports));
8003
9319
  const caps = getAgentCapabilities2(result.agentKey);
8004
9320
  if (caps.integration) {
@@ -10089,9 +11405,12 @@ __export(validators_exports, {
10089
11405
  validateBotToken: () => validateBotToken,
10090
11406
  validateChatId: () => validateChatId
10091
11407
  });
10092
- async function validateBotToken(token) {
11408
+ function safeNetworkError(error) {
11409
+ return redactNetworkSecrets(error instanceof Error ? error.message : String(error));
11410
+ }
11411
+ async function validateBotToken(token, telegramFetch) {
10093
11412
  try {
10094
- const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
11413
+ const res = await telegramFetch(`https://api.telegram.org/bot${token}/getMe`);
10095
11414
  const data = await res.json();
10096
11415
  if (data.ok && data.result) {
10097
11416
  return {
@@ -10102,12 +11421,12 @@ async function validateBotToken(token) {
10102
11421
  }
10103
11422
  return { ok: false, error: data.description || "Invalid token" };
10104
11423
  } catch (err) {
10105
- return { ok: false, error: err.message };
11424
+ return { ok: false, error: safeNetworkError(err) };
10106
11425
  }
10107
11426
  }
10108
- async function validateChatId(token, chatId) {
11427
+ async function validateChatId(token, chatId, telegramFetch) {
10109
11428
  try {
10110
- const res = await fetch(`https://api.telegram.org/bot${token}/getChat`, {
11429
+ const res = await telegramFetch(`https://api.telegram.org/bot${token}/getChat`, {
10111
11430
  method: "POST",
10112
11431
  headers: { "Content-Type": "application/json" },
10113
11432
  body: JSON.stringify({ chat_id: chatId })
@@ -10126,17 +11445,17 @@ async function validateChatId(token, chatId) {
10126
11445
  isForum: data.result.is_forum === true
10127
11446
  };
10128
11447
  } catch (err) {
10129
- return { ok: false, error: err.message };
11448
+ return { ok: false, error: safeNetworkError(err) };
10130
11449
  }
10131
11450
  }
10132
- async function validateBotAdmin(token, chatId) {
11451
+ async function validateBotAdmin(token, chatId, telegramFetch) {
10133
11452
  try {
10134
- const meRes = await fetch(`https://api.telegram.org/bot${token}/getMe`);
11453
+ const meRes = await telegramFetch(`https://api.telegram.org/bot${token}/getMe`);
10135
11454
  const meData = await meRes.json();
10136
11455
  if (!meData.ok || !meData.result) {
10137
11456
  return { ok: false, error: "Could not retrieve bot info" };
10138
11457
  }
10139
- const res = await fetch(
11458
+ const res = await telegramFetch(
10140
11459
  `https://api.telegram.org/bot${token}/getChatMember`,
10141
11460
  {
10142
11461
  method: "POST",
@@ -10167,14 +11486,14 @@ async function validateBotAdmin(token, chatId) {
10167
11486
  error: `Bot is "${status}" in this group \u2014 it must be an admin. Ask a group admin to go to Group Settings \u2192 Administrators \u2192 add the bot \u2192 tap Save/Done. Note: only existing admins can promote the bot.`
10168
11487
  };
10169
11488
  } catch (err) {
10170
- return { ok: false, error: err.message };
11489
+ return { ok: false, error: safeNetworkError(err) };
10171
11490
  }
10172
11491
  }
10173
- async function checkTopicsPrerequisites(token, chatId) {
11492
+ async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
10174
11493
  const issues = [];
10175
11494
  log36.info({ chatId }, "checkTopicsPrerequisites: starting checks");
10176
11495
  try {
10177
- const res = await fetch(`https://api.telegram.org/bot${token}/getChat`, {
11496
+ const res = await telegramFetch(`https://api.telegram.org/bot${token}/getChat`, {
10178
11497
  method: "POST",
10179
11498
  headers: { "Content-Type": "application/json" },
10180
11499
  body: JSON.stringify({ chat_id: chatId })
@@ -10193,7 +11512,7 @@ async function checkTopicsPrerequisites(token, chatId) {
10193
11512
  log36.warn({ err, chatId }, "checkTopicsPrerequisites: getChat failed (network error)");
10194
11513
  issues.push("\u274C Could not check if Topics are enabled (network error).");
10195
11514
  }
10196
- const adminResult = await validateBotAdmin(token, chatId);
11515
+ const adminResult = await validateBotAdmin(token, chatId, telegramFetch);
10197
11516
  log36.info(
10198
11517
  { chatId, adminOk: adminResult.ok, canManageTopics: adminResult.ok ? adminResult.canManageTopics : void 0, error: !adminResult.ok ? adminResult.error : void 0 },
10199
11518
  "checkTopicsPrerequisites: validateBotAdmin result"
@@ -10218,13 +11537,14 @@ var init_validators = __esm({
10218
11537
  "src/plugins/telegram/validators.ts"() {
10219
11538
  "use strict";
10220
11539
  init_log();
11540
+ init_network_redaction();
10221
11541
  log36 = createChildLogger({ module: "telegram-validators" });
10222
11542
  }
10223
11543
  });
10224
11544
 
10225
11545
  // src/plugins/telegram/adapter.ts
10226
11546
  import { Bot, InputFile, InlineKeyboard as InlineKeyboard13 } from "grammy";
10227
- function patchedFetch(input2, init) {
11547
+ function patchedFetch(input2, init, delegate) {
10228
11548
  if (init?.signal && !(init.signal instanceof AbortSignal)) {
10229
11549
  const nativeController = new AbortController();
10230
11550
  const polyfillSignal = init.signal;
@@ -10235,7 +11555,7 @@ function patchedFetch(input2, init) {
10235
11555
  }
10236
11556
  init = { ...init, signal: nativeController.signal };
10237
11557
  }
10238
- return fetch(input2, init);
11558
+ return delegate(input2, init);
10239
11559
  }
10240
11560
  var log37, TelegramAdapter;
10241
11561
  var init_adapter = __esm({
@@ -10309,6 +11629,10 @@ var init_adapter = __esm({
10309
11629
  _prerequisiteWatcher = null;
10310
11630
  /** Set during normal shutdown so bot.stop() does not trigger a self-restart. */
10311
11631
  _stopping = false;
11632
+ unregisterProxyTester;
11633
+ telegramFetch() {
11634
+ return this.core.proxyService.createFetch("channels.telegram");
11635
+ }
10312
11636
  /** Returns the configured Telegram supergroup chat ID. */
10313
11637
  getChatId() {
10314
11638
  return this.telegramConfig.chatId;
@@ -10381,6 +11705,16 @@ var init_adapter = __esm({
10381
11705
  this.core = core;
10382
11706
  this.telegramConfig = config;
10383
11707
  this.saveTopicIds = saveTopicIds;
11708
+ this.unregisterProxyTester = this.core.proxyService.registerRouteTester(
11709
+ "channels.telegram",
11710
+ async (fetcher) => {
11711
+ const response = await fetcher(`https://api.telegram.org/bot${this.telegramConfig.botToken}/getMe`, {
11712
+ signal: AbortSignal.timeout(1e4)
11713
+ });
11714
+ const body = await response.json().catch(() => null);
11715
+ if (!response.ok || !body?.ok) throw new Error("Telegram rejected the candidate proxy route");
11716
+ }
11717
+ );
10384
11718
  }
10385
11719
  /**
10386
11720
  * Set up the grammY bot, register all callback and message handlers, then perform
@@ -10393,7 +11727,11 @@ var init_adapter = __esm({
10393
11727
  this.bot = new Bot(this.telegramConfig.botToken, {
10394
11728
  client: {
10395
11729
  baseFetchConfig: { duplex: "half" },
10396
- fetch: patchedFetch
11730
+ fetch: (input2, init) => patchedFetch(
11731
+ input2,
11732
+ init,
11733
+ this.telegramFetch()
11734
+ )
10397
11735
  }
10398
11736
  });
10399
11737
  this.fileService = this.core.fileService;
@@ -10708,7 +12046,8 @@ ${p}` : p;
10708
12046
  const { checkTopicsPrerequisites: checkTopicsPrerequisites2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
10709
12047
  const prereqResult = await checkTopicsPrerequisites2(
10710
12048
  this.telegramConfig.botToken,
10711
- this.telegramConfig.chatId
12049
+ this.telegramConfig.chatId,
12050
+ this.telegramFetch()
10712
12051
  );
10713
12052
  if (prereqResult.ok) {
10714
12053
  log37.info("Telegram adapter: prerequisites OK, initializing topic-dependent features");
@@ -10936,7 +12275,8 @@ ${p}` : p;
10936
12275
  const { checkTopicsPrerequisites: checkTopicsPrerequisites2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
10937
12276
  const result = await checkTopicsPrerequisites2(
10938
12277
  this.telegramConfig.botToken,
10939
- this.telegramConfig.chatId
12278
+ this.telegramConfig.chatId,
12279
+ this.telegramFetch()
10940
12280
  );
10941
12281
  if (result.ok) {
10942
12282
  try {
@@ -10987,7 +12327,8 @@ OpenACP will automatically retry every 30 seconds. After fixing the issues above
10987
12327
  const { checkTopicsPrerequisites: checkTopicsPrerequisites2 } = await Promise.resolve().then(() => (init_validators(), validators_exports));
10988
12328
  const result = await checkTopicsPrerequisites2(
10989
12329
  this.telegramConfig.botToken,
10990
- this.telegramConfig.chatId
12330
+ this.telegramConfig.chatId,
12331
+ this.telegramFetch()
10991
12332
  );
10992
12333
  if (result.ok) {
10993
12334
  log37.info("Prerequisites met \u2014 completing Telegram adapter initialization");
@@ -11026,6 +12367,8 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
11026
12367
  */
11027
12368
  async stop() {
11028
12369
  this._stopping = true;
12370
+ this.unregisterProxyTester?.();
12371
+ this.unregisterProxyTester = void 0;
11029
12372
  if (this._prerequisiteWatcher !== null) {
11030
12373
  clearTimeout(this._prerequisiteWatcher);
11031
12374
  this._prerequisiteWatcher = null;
@@ -11765,7 +13108,7 @@ Task completed.
11765
13108
  const file = await this.bot.api.getFile(fileId);
11766
13109
  if (!file.file_path) return null;
11767
13110
  const url = `https://api.telegram.org/file/bot${this.telegramConfig.botToken}/${file.file_path}`;
11768
- const response = await fetch(url);
13111
+ const response = await this.telegramFetch()(url);
11769
13112
  if (!response.ok) return null;
11770
13113
  const buffer = Buffer.from(await response.arrayBuffer());
11771
13114
  return { buffer, filePath: file.file_path };
@@ -12697,7 +14040,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
12697
14040
  *
12698
14041
  * Does NOT create a session — callers must follow up with newSession or loadSession.
12699
14042
  */
12700
- static async spawnSubprocess(agentDef, workingDirectory, allowedPaths = []) {
14043
+ static async spawnSubprocess(agentDef, workingDirectory, allowedPaths = [], environment) {
12701
14044
  const instance = new _AgentInstance(agentDef.name);
12702
14045
  const resolved = resolveAgentCommand(agentDef.command);
12703
14046
  log4.debug(
@@ -12722,7 +14065,7 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
12722
14065
  cwd: workingDirectory,
12723
14066
  // envWhitelist from workspace.security.envWhitelist config would extend DEFAULT_ENV_WHITELIST.
12724
14067
  // Tracked as follow-up: pass workspace security config through spawn/resume call chain.
12725
- env: filterEnv(process.env, agentDef.env)
14068
+ env: environment ?? filterEnv(process.env, agentDef.env)
12726
14069
  }
12727
14070
  );
12728
14071
  await new Promise((resolve7, reject) => {
@@ -12881,10 +14224,10 @@ ${stderr}`
12881
14224
  * @param mcpServers - Optional MCP server configs to extend agent capabilities
12882
14225
  * @param allowedPaths - Extra filesystem paths the agent may access
12883
14226
  */
12884
- static async spawn(agentDef, workingDirectory, mcpServers, allowedPaths) {
14227
+ static async spawn(agentDef, workingDirectory, mcpServers, allowedPaths, environment) {
12885
14228
  log4.debug({ agentName: agentDef.name, command: agentDef.command }, "Spawning agent");
12886
14229
  const spawnStart = Date.now();
12887
- const instance = await _AgentInstance.spawnSubprocess(agentDef, workingDirectory, allowedPaths);
14230
+ const instance = await _AgentInstance.spawnSubprocess(agentDef, workingDirectory, allowedPaths, environment);
12888
14231
  await instance.claimForSession(workingDirectory, mcpServers, agentDef.initTimeoutMs);
12889
14232
  log4.info(
12890
14233
  {
@@ -12906,13 +14249,14 @@ ${stderr}`
12906
14249
  *
12907
14250
  * @param agentSessionId - The agent-side session ID to restore
12908
14251
  */
12909
- static async resume(agentDef, workingDirectory, agentSessionId, mcpServers, allowedPaths) {
14252
+ static async resume(agentDef, workingDirectory, agentSessionId, mcpServers, allowedPaths, environment) {
12910
14253
  log4.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
12911
14254
  const spawnStart = Date.now();
12912
14255
  const instance = await _AgentInstance.spawnSubprocess(
12913
14256
  agentDef,
12914
14257
  workingDirectory,
12915
- allowedPaths
14258
+ allowedPaths,
14259
+ environment
12916
14260
  );
12917
14261
  const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
12918
14262
  try {
@@ -13411,13 +14755,24 @@ function pathListsEqual(a, b) {
13411
14755
  return true;
13412
14756
  }
13413
14757
  var AgentManager = class {
13414
- constructor(catalog) {
14758
+ constructor(catalog, proxyService) {
13415
14759
  this.catalog = catalog;
14760
+ this.proxyService = proxyService;
14761
+ if (proxyService?.registerScope) for (const name of Object.keys(catalog.getInstalledEntries())) proxyService.registerScope(`agents.${name}`);
13416
14762
  }
13417
14763
  catalog;
14764
+ proxyService;
13418
14765
  warmEntry = null;
13419
14766
  /** In-flight prewarm promise — guards against concurrent prewarm calls. */
13420
14767
  warming = null;
14768
+ currentPolicyGeneration() {
14769
+ return typeof this.proxyService?.getPolicyGeneration === "function" ? this.proxyService.getPolicyGeneration() : 0;
14770
+ }
14771
+ childEnv(agentDef, routeName = agentDef.name) {
14772
+ if (!this.proxyService) return void 0;
14773
+ const filtered = filterEnv(process.env, agentDef.env);
14774
+ return this.proxyService.buildAgentEnv(routeName, filtered);
14775
+ }
13421
14776
  /** Return definitions for all installed agents. */
13422
14777
  getAvailableAgents() {
13423
14778
  const installed = this.catalog.getInstalledEntries();
@@ -13465,10 +14820,12 @@ var AgentManager = class {
13465
14820
  log5.debug({ agentName }, "prewarm: agent not installed, skipping");
13466
14821
  return;
13467
14822
  }
14823
+ const policyGeneration = this.currentPolicyGeneration();
13468
14824
  this.warming = (async () => {
13469
14825
  try {
13470
- const instance = await AgentInstance.spawnSubprocess(agentDef, workingDir, [...allowedPaths]);
13471
- if (this.warmEntry) {
14826
+ const environment = this.childEnv(agentDef, agentName);
14827
+ const instance = environment ? await AgentInstance.spawnSubprocess(agentDef, workingDir, [...allowedPaths], environment) : await AgentInstance.spawnSubprocess(agentDef, workingDir, [...allowedPaths]);
14828
+ if (this.warmEntry || this.currentPolicyGeneration() !== policyGeneration) {
13472
14829
  await instance.destroy().catch(() => {
13473
14830
  });
13474
14831
  return;
@@ -13478,7 +14835,8 @@ var AgentManager = class {
13478
14835
  workingDir,
13479
14836
  allowedPaths: [...allowedPaths],
13480
14837
  instance,
13481
- createdAt: Date.now()
14838
+ createdAt: Date.now(),
14839
+ policyGeneration
13482
14840
  };
13483
14841
  log5.info({ agentName, workingDir }, "Agent warm-pool: instance ready");
13484
14842
  } catch (err) {
@@ -13494,6 +14852,13 @@ var AgentManager = class {
13494
14852
  * Best-effort — errors are swallowed since shutdown should not fail.
13495
14853
  */
13496
14854
  async destroyWarm() {
14855
+ const inFlight = this.warming;
14856
+ if (inFlight) {
14857
+ try {
14858
+ await inFlight;
14859
+ } catch {
14860
+ }
14861
+ }
13497
14862
  const entry = this.warmEntry;
13498
14863
  this.warmEntry = null;
13499
14864
  if (entry) {
@@ -13517,6 +14882,12 @@ var AgentManager = class {
13517
14882
  const entry = this.warmEntry;
13518
14883
  if (!entry) return null;
13519
14884
  if (entry.agentName !== agentName || entry.workingDir !== workingDir) return null;
14885
+ if (entry.policyGeneration !== this.currentPolicyGeneration()) {
14886
+ this.warmEntry = null;
14887
+ entry.instance.destroy().catch(() => {
14888
+ });
14889
+ return null;
14890
+ }
13520
14891
  if (!pathListsEqual(entry.allowedPaths, allowedPaths)) {
13521
14892
  log5.debug(
13522
14893
  { agentName, workingDir },
@@ -13576,7 +14947,8 @@ var AgentManager = class {
13576
14947
  });
13577
14948
  }
13578
14949
  }
13579
- return AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths);
14950
+ const environment = this.childEnv(agentDef, agentName);
14951
+ return environment ? AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths, environment) : AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths);
13580
14952
  }
13581
14953
  /**
13582
14954
  * Spawn a subprocess and resume an existing agent session.
@@ -13591,7 +14963,8 @@ var AgentManager = class {
13591
14963
  `Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`
13592
14964
  );
13593
14965
  }
13594
- return AgentInstance.resume(agentDef, workingDirectory, agentSessionId, void 0, allowedPaths);
14966
+ const environment = this.childEnv(agentDef, agentName);
14967
+ return environment ? AgentInstance.resume(agentDef, workingDirectory, agentSessionId, void 0, allowedPaths, environment) : AgentInstance.resume(agentDef, workingDirectory, agentSessionId, void 0, allowedPaths);
13595
14968
  }
13596
14969
  };
13597
14970
 
@@ -14560,7 +15933,7 @@ ${result.text}` : result.text;
14560
15933
  }
14561
15934
  this.queue.clear();
14562
15935
  await this.agentInstance.destroy();
14563
- closeSessionLogger(this.log);
15936
+ await closeSessionLogger(this.log);
14564
15937
  }
14565
15938
  };
14566
15939
 
@@ -15611,12 +16984,12 @@ var SessionBridge = class {
15611
16984
  break;
15612
16985
  case "image_content": {
15613
16986
  if (this.deps.fileService) {
15614
- const fs35 = this.deps.fileService;
16987
+ const fs37 = this.deps.fileService;
15615
16988
  const sid = this.session.id;
15616
16989
  const { data, mimeType } = event;
15617
16990
  const buffer = Buffer.from(data, "base64");
15618
- const ext = fs35.extensionFromMime(mimeType);
15619
- fs35.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
16991
+ const ext = fs37.extensionFromMime(mimeType);
16992
+ fs37.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
15620
16993
  this.sendMessage(sid, {
15621
16994
  type: "attachment",
15622
16995
  text: "",
@@ -15628,12 +17001,12 @@ var SessionBridge = class {
15628
17001
  }
15629
17002
  case "audio_content": {
15630
17003
  if (this.deps.fileService) {
15631
- const fs35 = this.deps.fileService;
17004
+ const fs37 = this.deps.fileService;
15632
17005
  const sid = this.session.id;
15633
17006
  const { data, mimeType } = event;
15634
17007
  const buffer = Buffer.from(data, "base64");
15635
- const ext = fs35.extensionFromMime(mimeType);
15636
- fs35.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
17008
+ const ext = fs37.extensionFromMime(mimeType);
17009
+ fs37.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
15637
17010
  this.sendMessage(sid, {
15638
17011
  type: "attachment",
15639
17012
  text: "",
@@ -16245,7 +17618,7 @@ var SessionFactory = class {
16245
17618
  };
16246
17619
 
16247
17620
  // src/core/core.ts
16248
- import path15 from "path";
17621
+ import path16 from "path";
16249
17622
  import { nanoid as nanoid3 } from "nanoid";
16250
17623
 
16251
17624
  // src/core/sessions/session-store.ts
@@ -16634,17 +18007,21 @@ var log12 = createChildLogger({ module: "agent-catalog" });
16634
18007
  var REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
16635
18008
  var DEFAULT_TTL_HOURS = 24;
16636
18009
  var AgentCatalog = class {
18010
+ constructor(store, cachePath, agentsDir, scopedFetch = globalThis.fetch, registryUrl = REGISTRY_URL) {
18011
+ this.scopedFetch = scopedFetch;
18012
+ this.registryUrl = registryUrl;
18013
+ this.store = store;
18014
+ this.cachePath = cachePath;
18015
+ this.agentsDir = agentsDir;
18016
+ }
18017
+ scopedFetch;
18018
+ registryUrl;
16637
18019
  store;
16638
18020
  /** Agents available in the remote registry (cached in memory after load). */
16639
18021
  registryAgents = [];
16640
18022
  cachePath;
16641
18023
  /** Directory where binary agent archives are extracted to. */
16642
18024
  agentsDir;
16643
- constructor(store, cachePath, agentsDir) {
16644
- this.store = store;
16645
- this.cachePath = cachePath;
16646
- this.agentsDir = agentsDir;
16647
- }
16648
18025
  /**
16649
18026
  * Load installed agents from disk and hydrate the registry from cache/snapshot.
16650
18027
  *
@@ -16661,7 +18038,7 @@ var AgentCatalog = class {
16661
18038
  async fetchRegistry() {
16662
18039
  try {
16663
18040
  log12.info("Fetching agent registry from CDN...");
16664
- const response = await fetch(REGISTRY_URL);
18041
+ const response = await this.scopedFetch(this.registryUrl);
16665
18042
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
16666
18043
  const data = await response.json();
16667
18044
  this.registryAgents = data.agents ?? [];
@@ -16787,7 +18164,7 @@ var AgentCatalog = class {
16787
18164
  await progress?.onError(msg);
16788
18165
  return { ok: false, agentKey, error: msg };
16789
18166
  }
16790
- return installAgent(agent, this.store, progress, this.agentsDir);
18167
+ return installAgent(agent, this.store, progress, this.agentsDir, this.scopedFetch);
16791
18168
  }
16792
18169
  /**
16793
18170
  * Register an agent directly into the catalog store without going through
@@ -18361,6 +19738,7 @@ function registerCoreMenuItems(registry) {
18361
19738
  init_log();
18362
19739
  init_native_stt();
18363
19740
  init_events();
19741
+ init_proxy_service();
18364
19742
  var log17 = createChildLogger({ module: "core" });
18365
19743
  var OpenACPCore = class {
18366
19744
  configManager;
@@ -18383,6 +19761,8 @@ var OpenACPCore = class {
18383
19761
  menuRegistry = new MenuRegistry();
18384
19762
  assistantRegistry = new AssistantRegistry();
18385
19763
  assistantManager;
19764
+ /** Scoped proxy policy shared by transports, agent spawns, API and commands. */
19765
+ proxyService;
18386
19766
  // Services (security, notifications, speech, etc.) are provided by plugins that
18387
19767
  // register during boot. Core accesses them lazily via ServiceRegistry so it doesn't
18388
19768
  // need compile-time dependencies on plugin implementations.
@@ -18430,13 +19810,21 @@ var OpenACPCore = class {
18430
19810
  this.configManager = configManager;
18431
19811
  this.instanceContext = ctx;
18432
19812
  const config = configManager.get();
19813
+ this.proxyService = new ProxyService(ctx.root);
18433
19814
  this.agentCatalog = new AgentCatalog(
18434
19815
  new AgentStore(ctx.paths.agents),
18435
19816
  ctx.paths.registryCache,
18436
- ctx.paths.agentsDir
19817
+ ctx.paths.agentsDir,
19818
+ this.proxyService.createFetch("services.agentRegistry")
18437
19819
  );
18438
19820
  this.agentCatalog.load();
18439
- this.agentManager = new AgentManager(this.agentCatalog);
19821
+ this.agentManager = new AgentManager(this.agentCatalog, this.proxyService);
19822
+ this.proxyService.onRouteChanged(async (scope) => {
19823
+ if (scope === "global" || scope === "agents.default" || scope.startsWith("agents.")) {
19824
+ await this.agentManager.destroyWarm();
19825
+ log17.info({ scope }, "Proxy route changed; idle agent warm pool invalidated");
19826
+ }
19827
+ });
18440
19828
  const storePath = ctx.paths.sessions;
18441
19829
  this.sessionStore = new JsonFileSessionStore(
18442
19830
  storePath,
@@ -18465,6 +19853,7 @@ var OpenACPCore = class {
18465
19853
  instanceRoot: ctx.root,
18466
19854
  log: createChildLogger({ module: "plugin" })
18467
19855
  });
19856
+ this.lifecycleManager.serviceRegistry.register("proxy", this.proxyService, "@openacp/core");
18468
19857
  this.sessionFactory.middlewareChain = this.lifecycleManager.middlewareChain;
18469
19858
  this.sessionManager.middlewareChain = this.lifecycleManager.middlewareChain;
18470
19859
  this.sessionFactory.sessionStore = this.sessionStore;
@@ -18512,7 +19901,7 @@ var OpenACPCore = class {
18512
19901
  }
18513
19902
  );
18514
19903
  registerCoreMenuItems(this.menuRegistry);
18515
- this.assistantRegistry.setInstanceRoot(path15.dirname(ctx.root));
19904
+ this.assistantRegistry.setInstanceRoot(path16.dirname(ctx.root));
18516
19905
  this.assistantRegistry.register(createSessionsSection(this));
18517
19906
  this.assistantRegistry.register(createAgentsSection(this));
18518
19907
  this.assistantRegistry.register(createConfigSection(this));
@@ -19332,29 +20721,71 @@ init_doctor();
19332
20721
  init_config_registry();
19333
20722
 
19334
20723
  // src/core/config/config-editor.ts
19335
- import * as path31 from "path";
20724
+ import * as path32 from "path";
19336
20725
  import * as clack2 from "@clack/prompts";
19337
20726
 
19338
20727
  // src/cli/autostart.ts
19339
20728
  init_log();
19340
20729
  import { execFileSync as execFileSync6 } from "child_process";
19341
- import * as fs25 from "fs";
19342
- import * as path24 from "path";
20730
+ import * as fs27 from "fs";
20731
+ import * as path25 from "path";
19343
20732
  import * as os7 from "os";
19344
20733
  var log19 = createChildLogger({ module: "autostart" });
19345
- var LEGACY_LAUNCHD_PLIST_PATH = path24.join(os7.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
19346
- var LEGACY_SYSTEMD_SERVICE_PATH = path24.join(os7.homedir(), ".config", "systemd", "user", "openacp.service");
20734
+ var LEGACY_LAUNCHD_PLIST_PATH = path25.join(os7.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
20735
+ var LEGACY_SYSTEMD_SERVICE_PATH = path25.join(os7.homedir(), ".config", "systemd", "user", "openacp.service");
19347
20736
  function getLaunchdLabel(instanceId) {
19348
20737
  return `com.openacp.daemon.${instanceId}`;
19349
20738
  }
19350
20739
  function getLaunchdPlistPath(instanceId) {
19351
- return path24.join(os7.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
20740
+ return path25.join(os7.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
19352
20741
  }
19353
20742
  function getSystemdServiceName(instanceId) {
19354
20743
  return `openacp-${instanceId}`;
19355
20744
  }
19356
20745
  function getSystemdServicePath(instanceId) {
19357
- return path24.join(os7.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
20746
+ return path25.join(os7.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
20747
+ }
20748
+ function getAutoStartState(instanceId) {
20749
+ if (process.platform === "linux") {
20750
+ const installed = fs27.existsSync(getSystemdServicePath(instanceId));
20751
+ if (!installed) return { installed: false, manager: null, active: false };
20752
+ try {
20753
+ const output = execFileSync6("systemctl", [
20754
+ "--user",
20755
+ "show",
20756
+ getSystemdServiceName(instanceId),
20757
+ "--property=ActiveState",
20758
+ "--property=MainPID"
20759
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
20760
+ const properties = Object.fromEntries(output.trim().split(/\r?\n/).map((line) => {
20761
+ const index = line.indexOf("=");
20762
+ return index >= 0 ? [line.slice(0, index), line.slice(index + 1)] : [line, ""];
20763
+ }));
20764
+ const active = properties.ActiveState === "active";
20765
+ const pid = Number(properties.MainPID);
20766
+ return { installed: true, manager: "systemd", active, ...pid > 0 ? { pid } : {} };
20767
+ } catch {
20768
+ return { installed: true, manager: "systemd", active: false };
20769
+ }
20770
+ }
20771
+ if (process.platform === "darwin") {
20772
+ const installed = fs27.existsSync(getLaunchdPlistPath(instanceId));
20773
+ if (!installed) return { installed: false, manager: null, active: false };
20774
+ try {
20775
+ const uid = process.getuid();
20776
+ const output = execFileSync6("launchctl", ["print", `gui/${uid}/${getLaunchdLabel(instanceId)}`], {
20777
+ encoding: "utf8",
20778
+ stdio: ["ignore", "pipe", "pipe"]
20779
+ });
20780
+ const active = /^\s*state\s*=\s*running\s*$/m.test(output);
20781
+ const pidMatch = output.match(/^\s*pid\s*=\s*(\d+)\s*$/m);
20782
+ const pid = pidMatch ? Number(pidMatch[1]) : 0;
20783
+ return { installed: true, manager: "launchd", active, ...pid > 0 ? { pid } : {} };
20784
+ } catch {
20785
+ return { installed: true, manager: "launchd", active: false };
20786
+ }
20787
+ }
20788
+ return { installed: false, manager: null, active: false };
19358
20789
  }
19359
20790
  function isAutoStartSupported() {
19360
20791
  return process.platform === "darwin" || process.platform === "linux";
@@ -19368,7 +20799,7 @@ function escapeSystemdValue(str) {
19368
20799
  }
19369
20800
  function generateLaunchdPlist(nodePath, cliPath, logDir2, instanceRoot, instanceId) {
19370
20801
  const label = getLaunchdLabel(instanceId);
19371
- const logFile = path24.join(logDir2, "openacp.log");
20802
+ const logFile = path25.join(logDir2, "openacp.log");
19372
20803
  return `<?xml version="1.0" encoding="UTF-8"?>
19373
20804
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
19374
20805
  <plist version="1.0">
@@ -19385,6 +20816,8 @@ function generateLaunchdPlist(nodePath, cliPath, logDir2, instanceRoot, instance
19385
20816
  <dict>
19386
20817
  <key>OPENACP_INSTANCE_ROOT</key>
19387
20818
  <string>${escapeXml(instanceRoot)}</string>
20819
+ <key>OPENACP_SUPERVISOR</key>
20820
+ <string>launchd</string>
19388
20821
  </dict>
19389
20822
  <key>RunAtLoad</key>
19390
20823
  <true/>
@@ -19409,6 +20842,7 @@ Description=OpenACP Daemon (${instanceId})
19409
20842
  [Service]
19410
20843
  ExecStart=${escapeSystemdValue(nodePath)} ${escapeSystemdValue(cliPath)} --daemon-child
19411
20844
  Environment=OPENACP_INSTANCE_ROOT=${escapeSystemdValue(instanceRoot)}
20845
+ Environment=OPENACP_SUPERVISOR=systemd
19412
20846
  Restart=on-failure
19413
20847
 
19414
20848
  [Install]
@@ -19417,25 +20851,25 @@ WantedBy=default.target
19417
20851
  `;
19418
20852
  }
19419
20853
  function migrateLegacy() {
19420
- if (process.platform === "darwin" && fs25.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
20854
+ if (process.platform === "darwin" && fs27.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
19421
20855
  try {
19422
20856
  const uid = process.getuid();
19423
20857
  execFileSync6("launchctl", ["bootout", `gui/${uid}`, "com.openacp.daemon"], { stdio: "pipe" });
19424
20858
  } catch {
19425
20859
  }
19426
20860
  try {
19427
- fs25.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
20861
+ fs27.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
19428
20862
  } catch {
19429
20863
  }
19430
20864
  log19.info("Removed legacy single-instance LaunchAgent");
19431
20865
  }
19432
- if (process.platform === "linux" && fs25.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
20866
+ if (process.platform === "linux" && fs27.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
19433
20867
  try {
19434
20868
  execFileSync6("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
19435
20869
  } catch {
19436
20870
  }
19437
20871
  try {
19438
- fs25.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
20872
+ fs27.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
19439
20873
  } catch {
19440
20874
  }
19441
20875
  try {
@@ -19450,23 +20884,54 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
19450
20884
  return { success: false, error: "Auto-start not supported on this platform" };
19451
20885
  }
19452
20886
  const nodePath = process.execPath;
19453
- const cliPath = path24.resolve(process.argv[1]);
19454
- const resolvedLogDir = logDir2.startsWith("~") ? path24.join(os7.homedir(), logDir2.slice(1)) : logDir2;
20887
+ const cliPath = path25.resolve(process.argv[1]);
20888
+ const resolvedLogDir = logDir2.startsWith("~") ? path25.join(os7.homedir(), logDir2.slice(1)) : logDir2;
19455
20889
  try {
19456
20890
  migrateLegacy();
19457
20891
  if (process.platform === "darwin") {
19458
20892
  const plistPath = getLaunchdPlistPath(instanceId);
19459
20893
  const plist = generateLaunchdPlist(nodePath, cliPath, resolvedLogDir, instanceRoot, instanceId);
19460
- const dir = path24.dirname(plistPath);
19461
- fs25.mkdirSync(dir, { recursive: true });
19462
- fs25.writeFileSync(plistPath, plist);
20894
+ const dir = path25.dirname(plistPath);
20895
+ fs27.mkdirSync(dir, { recursive: true });
19463
20896
  const uid = process.getuid();
19464
20897
  const domain = `gui/${uid}`;
20898
+ const previous = fs27.existsSync(plistPath) ? fs27.readFileSync(plistPath, "utf8") : void 0;
20899
+ const previousState = getAutoStartState(instanceId);
20900
+ const tmp = `${plistPath}.${process.pid}.tmp`;
20901
+ let replaced = false;
20902
+ let bootedOut = false;
19465
20903
  try {
19466
- execFileSync6("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
19467
- } catch {
20904
+ fs27.writeFileSync(tmp, plist, { mode: 384 });
20905
+ execFileSync6("plutil", ["-lint", tmp], { stdio: "pipe" });
20906
+ fs27.renameSync(tmp, plistPath);
20907
+ replaced = true;
20908
+ if (previousState.active) {
20909
+ execFileSync6("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
20910
+ bootedOut = true;
20911
+ }
20912
+ execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
20913
+ } catch (error) {
20914
+ try {
20915
+ fs27.rmSync(tmp, { force: true });
20916
+ } catch {
20917
+ }
20918
+ if (replaced) {
20919
+ let rollbackBootedOut = false;
20920
+ try {
20921
+ execFileSync6("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
20922
+ rollbackBootedOut = true;
20923
+ } catch {
20924
+ }
20925
+ if (previous === void 0) fs27.rmSync(plistPath, { force: true });
20926
+ else {
20927
+ const rollbackTmp = `${plistPath}.${process.pid}.rollback.tmp`;
20928
+ fs27.writeFileSync(rollbackTmp, previous, { mode: 384 });
20929
+ fs27.renameSync(rollbackTmp, plistPath);
20930
+ if (previousState.active || bootedOut || rollbackBootedOut) execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
20931
+ }
20932
+ }
20933
+ throw error;
19468
20934
  }
19469
- execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
19470
20935
  log19.info({ instanceId }, "LaunchAgent installed");
19471
20936
  return { success: true };
19472
20937
  }
@@ -19474,11 +20939,24 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
19474
20939
  const servicePath = getSystemdServicePath(instanceId);
19475
20940
  const serviceName = getSystemdServiceName(instanceId);
19476
20941
  const unit = generateSystemdUnit(nodePath, cliPath, instanceRoot, instanceId);
19477
- const dir = path24.dirname(servicePath);
19478
- fs25.mkdirSync(dir, { recursive: true });
19479
- fs25.writeFileSync(servicePath, unit);
19480
- execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
19481
- execFileSync6("systemctl", ["--user", "enable", serviceName], { stdio: "pipe" });
20942
+ const dir = path25.dirname(servicePath);
20943
+ fs27.mkdirSync(dir, { recursive: true });
20944
+ const previous = fs27.existsSync(servicePath) ? fs27.readFileSync(servicePath, "utf8") : void 0;
20945
+ const tmp = `${servicePath}.${process.pid}.tmp`;
20946
+ fs27.writeFileSync(tmp, unit, { mode: 384 });
20947
+ fs27.renameSync(tmp, servicePath);
20948
+ try {
20949
+ execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20950
+ execFileSync6("systemctl", ["--user", "enable", serviceName], { stdio: "pipe" });
20951
+ } catch (error) {
20952
+ if (previous === void 0) fs27.unlinkSync(servicePath);
20953
+ else fs27.writeFileSync(servicePath, previous);
20954
+ try {
20955
+ execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20956
+ } catch {
20957
+ }
20958
+ throw error;
20959
+ }
19482
20960
  log19.info({ instanceId }, "systemd user service installed");
19483
20961
  return { success: true };
19484
20962
  }
@@ -19496,13 +20974,13 @@ function uninstallAutoStart(instanceId) {
19496
20974
  try {
19497
20975
  if (process.platform === "darwin") {
19498
20976
  const plistPath = getLaunchdPlistPath(instanceId);
19499
- if (fs25.existsSync(plistPath)) {
20977
+ if (fs27.existsSync(plistPath)) {
19500
20978
  const uid = process.getuid();
19501
20979
  try {
19502
20980
  execFileSync6("launchctl", ["bootout", `gui/${uid}`, plistPath], { stdio: "pipe" });
19503
20981
  } catch {
19504
20982
  }
19505
- fs25.unlinkSync(plistPath);
20983
+ fs27.unlinkSync(plistPath);
19506
20984
  log19.info({ instanceId }, "LaunchAgent removed");
19507
20985
  }
19508
20986
  return { success: true };
@@ -19510,12 +20988,12 @@ function uninstallAutoStart(instanceId) {
19510
20988
  if (process.platform === "linux") {
19511
20989
  const servicePath = getSystemdServicePath(instanceId);
19512
20990
  const serviceName = getSystemdServiceName(instanceId);
19513
- if (fs25.existsSync(servicePath)) {
20991
+ if (fs27.existsSync(servicePath)) {
19514
20992
  try {
19515
20993
  execFileSync6("systemctl", ["--user", "disable", serviceName], { stdio: "pipe" });
19516
20994
  } catch {
19517
20995
  }
19518
- fs25.unlinkSync(servicePath);
20996
+ fs27.unlinkSync(servicePath);
19519
20997
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
19520
20998
  log19.info({ instanceId }, "systemd user service removed");
19521
20999
  }
@@ -19530,10 +21008,10 @@ function uninstallAutoStart(instanceId) {
19530
21008
  }
19531
21009
  function isAutoStartInstalled(instanceId) {
19532
21010
  if (process.platform === "darwin") {
19533
- return fs25.existsSync(getLaunchdPlistPath(instanceId));
21011
+ return fs27.existsSync(getLaunchdPlistPath(instanceId));
19534
21012
  }
19535
21013
  if (process.platform === "linux") {
19536
- return fs25.existsSync(getSystemdServicePath(instanceId));
21014
+ return fs27.existsSync(getSystemdServicePath(instanceId));
19537
21015
  }
19538
21016
  return false;
19539
21017
  }
@@ -19542,25 +21020,25 @@ function isAutoStartInstalled(instanceId) {
19542
21020
  init_instance_context();
19543
21021
  init_instance_registry();
19544
21022
  init_log();
19545
- import fs28 from "fs";
19546
- import path27 from "path";
21023
+ import fs30 from "fs";
21024
+ import path28 from "path";
19547
21025
  var log20 = createChildLogger({ module: "resolve-instance-id" });
19548
21026
  function resolveInstanceId(instanceRoot) {
19549
21027
  try {
19550
- const configPath = path27.join(instanceRoot, "config.json");
19551
- const raw = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
21028
+ const configPath = path28.join(instanceRoot, "config.json");
21029
+ const raw = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
19552
21030
  if (raw.id && typeof raw.id === "string") return raw.id;
19553
21031
  } catch {
19554
21032
  }
19555
21033
  try {
19556
- const reg = new InstanceRegistry(path27.join(getGlobalRoot(), "instances.json"));
21034
+ const reg = new InstanceRegistry(path28.join(getGlobalRoot(), "instances.json"));
19557
21035
  reg.load();
19558
21036
  const entry = reg.getByRoot(instanceRoot);
19559
21037
  if (entry?.id) return entry.id;
19560
21038
  } catch (err) {
19561
21039
  log20.debug({ err: err.message, instanceRoot }, "Could not read instance registry, using fallback id");
19562
21040
  }
19563
- return path27.basename(path27.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
21041
+ return path28.basename(path28.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
19564
21042
  }
19565
21043
 
19566
21044
  // src/core/config/config-editor.ts
@@ -20132,7 +21610,7 @@ async function runConfigEditor(configManager, mode = "file", apiPort, settingsMa
20132
21610
  await configManager.load();
20133
21611
  const config = configManager.get();
20134
21612
  const updates = {};
20135
- const instanceRoot = path31.dirname(configManager.getConfigPath());
21613
+ const instanceRoot = path32.dirname(configManager.getConfigPath());
20136
21614
  console.log(`
20137
21615
  ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
20138
21616
  console.log(dim(`Config: ${configManager.getConfigPath()}`));
@@ -20189,17 +21667,17 @@ ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
20189
21667
  async function sendConfigViaApi(port, updates) {
20190
21668
  const { apiCall: call } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
20191
21669
  const paths = flattenToPaths(updates);
20192
- for (const { path: path36, value } of paths) {
21670
+ for (const { path: path37, value } of paths) {
20193
21671
  const res = await call(port, "/api/config", {
20194
21672
  method: "PATCH",
20195
21673
  headers: { "Content-Type": "application/json" },
20196
- body: JSON.stringify({ path: path36, value })
21674
+ body: JSON.stringify({ path: path37, value })
20197
21675
  });
20198
21676
  const data = await res.json();
20199
21677
  if (!res.ok) {
20200
- console.log(warn(`Failed to update ${path36}: ${data.error}`));
21678
+ console.log(warn(`Failed to update ${path37}: ${data.error}`));
20201
21679
  } else if (data.needsRestart) {
20202
- console.log(warn(`${path36} updated \u2014 restart required`));
21680
+ console.log(warn(`${path37} updated \u2014 restart required`));
20203
21681
  }
20204
21682
  }
20205
21683
  }
@@ -20219,25 +21697,25 @@ function flattenToPaths(obj, prefix = "") {
20219
21697
  // src/cli/daemon.ts
20220
21698
  init_config();
20221
21699
  import { spawn as spawn3 } from "child_process";
20222
- import * as fs31 from "fs";
20223
- import * as path32 from "path";
21700
+ import * as fs33 from "fs";
21701
+ import * as path33 from "path";
20224
21702
  function getPidPath(root) {
20225
- return path32.join(root, "openacp.pid");
21703
+ return path33.join(root, "openacp.pid");
20226
21704
  }
20227
21705
  function getLogDir(root) {
20228
- return path32.join(root, "logs");
21706
+ return path33.join(root, "logs");
20229
21707
  }
20230
21708
  function getRunningMarker(root) {
20231
- return path32.join(root, "running");
21709
+ return path33.join(root, "running");
20232
21710
  }
20233
21711
  function writePidFile(pidPath, pid) {
20234
- const dir = path32.dirname(pidPath);
20235
- fs31.mkdirSync(dir, { recursive: true });
20236
- fs31.writeFileSync(pidPath, String(pid));
21712
+ const dir = path33.dirname(pidPath);
21713
+ fs33.mkdirSync(dir, { recursive: true });
21714
+ fs33.writeFileSync(pidPath, String(pid));
20237
21715
  }
20238
21716
  function readPidFile(pidPath) {
20239
21717
  try {
20240
- const content = fs31.readFileSync(pidPath, "utf-8").trim();
21718
+ const content = fs33.readFileSync(pidPath, "utf-8").trim();
20241
21719
  const pid = parseInt(content, 10);
20242
21720
  return isNaN(pid) ? null : pid;
20243
21721
  } catch {
@@ -20246,7 +21724,7 @@ function readPidFile(pidPath) {
20246
21724
  }
20247
21725
  function removePidFile(pidPath) {
20248
21726
  try {
20249
- fs31.unlinkSync(pidPath);
21727
+ fs33.unlinkSync(pidPath);
20250
21728
  } catch {
20251
21729
  }
20252
21730
  }
@@ -20279,12 +21757,12 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
20279
21757
  return { error: `Already running (PID ${pid})` };
20280
21758
  }
20281
21759
  const resolvedLogDir = logDir2 ? expandHome2(logDir2) : getLogDir(instanceRoot);
20282
- fs31.mkdirSync(resolvedLogDir, { recursive: true });
20283
- const logFile = path32.join(resolvedLogDir, "openacp.log");
20284
- const cliPath = path32.resolve(process.argv[1]);
21760
+ fs33.mkdirSync(resolvedLogDir, { recursive: true });
21761
+ const logFile = path33.join(resolvedLogDir, "openacp.log");
21762
+ const cliPath = path33.resolve(process.argv[1]);
20285
21763
  const nodePath = process.execPath;
20286
- const out = fs31.openSync(logFile, "a");
20287
- const err = fs31.openSync(logFile, "a");
21764
+ const out = fs33.openSync(logFile, "a");
21765
+ const err = fs33.openSync(logFile, "a");
20288
21766
  const child = spawn3(nodePath, [cliPath, "--daemon-child"], {
20289
21767
  detached: true,
20290
21768
  stdio: ["ignore", out, err],
@@ -20293,8 +21771,8 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
20293
21771
  ...instanceRoot ? { OPENACP_INSTANCE_ROOT: instanceRoot } : {}
20294
21772
  }
20295
21773
  });
20296
- fs31.closeSync(out);
20297
- fs31.closeSync(err);
21774
+ fs33.closeSync(out);
21775
+ fs33.closeSync(err);
20298
21776
  if (!child.pid) {
20299
21777
  return { error: "Failed to spawn daemon process" };
20300
21778
  }
@@ -20365,12 +21843,12 @@ async function stopDaemon(pidPath, instanceRoot) {
20365
21843
  }
20366
21844
  function markRunning(root) {
20367
21845
  const marker = getRunningMarker(root);
20368
- fs31.mkdirSync(path32.dirname(marker), { recursive: true });
20369
- fs31.writeFileSync(marker, "");
21846
+ fs33.mkdirSync(path33.dirname(marker), { recursive: true });
21847
+ fs33.writeFileSync(marker, "");
20370
21848
  }
20371
21849
  function clearRunning(root) {
20372
21850
  try {
20373
- fs31.unlinkSync(getRunningMarker(root));
21851
+ fs33.unlinkSync(getRunningMarker(root));
20374
21852
  } catch {
20375
21853
  }
20376
21854
  }
@@ -21219,6 +22697,9 @@ Session records auto-cleanup: 30 days (configurable via \`sessionStore.ttlDays\`
21219
22697
  Session logs auto-cleanup: 30 days (configurable via \`logging.sessionLogRetentionDays\`).
21220
22698
  `;
21221
22699
 
22700
+ // src/core/index.ts
22701
+ init_proxy_service();
22702
+
21222
22703
  // src/index.ts
21223
22704
  init_adapter();
21224
22705
  export {
@@ -21250,8 +22731,10 @@ export {
21250
22731
  OpenACPCore,
21251
22732
  OutputModeResolver,
21252
22733
  PRODUCT_GUIDE,
22734
+ PROXY_ENV_KEYS,
21253
22735
  PermissionGate,
21254
22736
  PromptQueue,
22737
+ ProxyService,
21255
22738
  SSEManager,
21256
22739
  STATUS_ICONS,
21257
22740
  SecurityGuard,