@indigoai-us/hq-cli 5.345.190 → 5.345.193

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +420 -0
  2. package/assets/bot-workers/setup/worker.yaml +91 -34
  3. package/dist/command-catalog.generated.d.ts +96 -19
  4. package/dist/command-catalog.generated.js +124 -21
  5. package/dist/commands/bot-jobs.d.ts +21 -18
  6. package/dist/commands/bot-jobs.js +127 -39
  7. package/dist/commands/bot.d.ts +7 -4
  8. package/dist/commands/bot.js +289 -140
  9. package/dist/commands/core-jobs-schedule.js +10 -0
  10. package/dist/commands/daemon.js +37 -0
  11. package/dist/commands/files.d.ts +41 -6
  12. package/dist/commands/files.js +108 -27
  13. package/dist/commands/pack-start-bot.d.ts +4 -6
  14. package/dist/commands/pack-start-bot.js +4 -8
  15. package/dist/commands/signals-run.d.ts +45 -0
  16. package/dist/commands/signals-run.js +175 -0
  17. package/dist/commands/signals.d.ts +1 -0
  18. package/dist/commands/signals.js +3 -0
  19. package/dist/commands/sources.d.ts +11 -0
  20. package/dist/commands/sources.js +137 -25
  21. package/dist/commands/staff/reset-test-account.d.ts +10 -0
  22. package/dist/commands/staff/reset-test-account.js +18 -2
  23. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
  24. package/dist/hq-cloud-sync-runner/package.json +1 -1
  25. package/dist/lib/bot/api.d.ts +3 -2
  26. package/dist/lib/bot/config.d.ts +2 -0
  27. package/dist/lib/bot/fleet-cron.d.ts +121 -0
  28. package/dist/lib/bot/fleet-cron.js +319 -0
  29. package/dist/lib/bot/fleet-local.d.ts +113 -2
  30. package/dist/lib/bot/fleet-local.js +597 -35
  31. package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
  32. package/dist/lib/bot/fleet-owner-companies.js +30 -0
  33. package/dist/lib/bot/fleet-watch.d.ts +25 -0
  34. package/dist/lib/bot/fleet-watch.js +51 -0
  35. package/dist/lib/bot/index.d.ts +1 -1
  36. package/dist/lib/bot/index.js +1 -1
  37. package/dist/lib/bot/job-run.d.ts +5 -4
  38. package/dist/lib/bot/job-run.js +5 -4
  39. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
  40. package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
  41. package/dist/lib/bot/local-jobs.d.ts +13 -2
  42. package/dist/lib/bot/local-jobs.js +17 -4
  43. package/dist/lib/bot/progress.d.ts +14 -0
  44. package/dist/lib/bot/progress.js +47 -1
  45. package/dist/lib/bot/run.d.ts +55 -2
  46. package/dist/lib/bot/run.js +192 -10
  47. package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
  48. package/dist/lib/bot/setup-tool-offer.js +16 -0
  49. package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
  50. package/dist/lib/bot/signals-engine-flag.js +30 -0
  51. package/dist/lib/daemon/catalog.d.ts +4 -0
  52. package/dist/lib/daemon/catalog.js +29 -9
  53. package/dist/lib/daemon/legacy.d.ts +6 -0
  54. package/dist/lib/daemon/legacy.js +16 -10
  55. package/dist/lib/daemon/run.d.ts +1 -0
  56. package/dist/lib/daemon/run.js +10 -0
  57. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  58. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  59. package/dist/lib/flag-registry.d.ts +13 -1
  60. package/dist/lib/flag-registry.js +46 -0
  61. package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
  62. package/dist/lib/hot-channels/adapters/attio.js +367 -29
  63. package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
  64. package/dist/lib/hot-channels/adapters/gmail.js +59 -2
  65. package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
  66. package/dist/lib/hot-channels/adapters/linear.js +71 -1
  67. package/dist/lib/hot-channels/adapters/meetings.js +4 -1
  68. package/dist/lib/hot-channels/adapters/slack.js +7 -3
  69. package/dist/lib/hot-channels/alert.js +1 -1
  70. package/dist/lib/hot-channels/contract.d.ts +11 -0
  71. package/dist/lib/hot-channels/contract.js +1 -0
  72. package/dist/lib/hot-channels/cursors.js +1 -1
  73. package/dist/lib/hot-channels/landing.d.ts +6 -2
  74. package/dist/lib/hot-channels/landing.js +13 -6
  75. package/dist/lib/hot-channels/ledger.js +2 -0
  76. package/dist/lib/hot-channels/loop.js +18 -4
  77. package/dist/lib/hot-channels/registry.js +9 -1
  78. package/dist/lib/hot-channels/run.d.ts +18 -0
  79. package/dist/lib/hot-channels/run.js +15 -4
  80. package/dist/lib/hot-channels/signals-pass.d.ts +1 -0
  81. package/dist/lib/hot-channels/signals-pass.js +7 -5
  82. package/dist/lib/hot-channels/sync-lock.js +22 -6
  83. package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
  84. package/dist/lib/mesh/live/daemon/install.js +117 -30
  85. package/dist/lib/signals/adapters-catalog.d.ts +77 -0
  86. package/dist/lib/signals/adapters-catalog.js +194 -0
  87. package/dist/lib/signals/engine.d.ts +196 -0
  88. package/dist/lib/signals/engine.js +495 -0
  89. package/dist/lib/signals/failure-alert.d.ts +89 -0
  90. package/dist/lib/signals/failure-alert.js +175 -0
  91. package/dist/lib/signals/local-date.d.ts +3 -0
  92. package/dist/lib/signals/local-date.js +6 -0
  93. package/dist/lib/signals/projection.d.ts +42 -0
  94. package/dist/lib/signals/projection.js +67 -0
  95. package/dist/lib/signals/reply.d.ts +112 -0
  96. package/dist/lib/signals/reply.js +368 -0
  97. package/dist/lib/signals/run-lock.d.ts +36 -0
  98. package/dist/lib/signals/run-lock.js +148 -0
  99. package/dist/lib/signals/run-record.d.ts +24 -0
  100. package/dist/lib/signals/run-record.js +82 -0
  101. package/dist/lib/signals/sanitize.d.ts +105 -0
  102. package/dist/lib/signals/sanitize.js +439 -0
  103. package/dist/lib/signals/settings.d.ts +10 -0
  104. package/dist/lib/signals/settings.js +48 -0
  105. package/dist/lib/signals/steps/adapters.d.ts +14 -0
  106. package/dist/lib/signals/steps/adapters.js +156 -0
  107. package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
  108. package/dist/lib/signals/steps/entity-similarity.js +47 -0
  109. package/dist/lib/signals/steps/extract.d.ts +113 -0
  110. package/dist/lib/signals/steps/extract.js +765 -0
  111. package/dist/lib/signals/steps/garden.d.ts +24 -0
  112. package/dist/lib/signals/steps/garden.js +250 -0
  113. package/dist/lib/signals/steps/types.d.ts +112 -0
  114. package/dist/lib/signals/steps/types.js +4 -0
  115. package/dist/lib/signals/steps/upload.d.ts +43 -0
  116. package/dist/lib/signals/steps/upload.js +324 -0
  117. package/dist/lib/signals/write-guard.d.ts +63 -0
  118. package/dist/lib/signals/write-guard.js +287 -0
  119. package/dist/utils/resident-package-preload.js +16 -0
  120. package/package.json +9 -9
  121. package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
  122. package/dist/lib/bot/fleet-default-flag.js +0 -127
@@ -1,8 +1,9 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
2
  import * as crypto from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
+ import * as zlib from "node:zlib";
6
7
  import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
7
8
  import { withBotLock } from "./lock.js";
8
9
  import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
@@ -78,20 +79,47 @@ export function sharedRuntimeLayout(root, id, platform) {
78
79
  }
79
80
  export const HQ_FLEET_LOCAL_RELEASE = {
80
81
  repository: "indigoai-us/hq-fleet-local",
81
- tag: "v0.4.17",
82
- version: "0.4.17",
83
- artifact: "hq-fleet-local-0.4.17.zip",
84
- artifactSha256: "d2c5c49d4ef47da6003f4c2f3be7dcd2d8a287523c8577dfe0239e6e0705e4b4",
82
+ tag: "v0.4.20",
83
+ version: "0.4.20",
84
+ artifact: "hq-fleet-local-0.4.20.zip",
85
+ artifactSha256: "5c49402f17408f3eaf99d27b3c2d4d0f59a24f33946aa8dfc3098a3d09e13798",
85
86
  runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
86
87
  runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
87
- manifestSha256: "63663ffe8c6bef4f2aefa587d20431156cab4170a0f23533cdab7aab444a7d35",
88
- signatureSha256: "bb4af0b453ae03fdbc18e6335ca23e2f0de42bd9e322dba7c4f31d0c8dc08aa7",
88
+ manifestSha256: "3cec47eb7f47af13e66d4ad29669d3c103e1ca452661f5eaad760243118ce214",
89
+ signatureSha256: "5504c3a956f36d148f3cdf2212a9b171d852098c82eb199c7c415ad3545e3b05",
89
90
  runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
90
91
  };
92
+ /** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
91
93
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
92
94
  MCowBQYDK2VwAyEASJoapvo0WMTg+Wu3Jvt0nzlXLOBEr+G1aJRi0JkEb4A=
93
95
  -----END PUBLIC KEY-----
94
96
  `;
97
+ /** hq-fleet-local trust/release-public-v2.pem (added 2026-11-01; signs v0.4.18). */
98
+ export const HQ_FLEET_LOCAL_PUBLIC_KEY_V2 = `-----BEGIN PUBLIC KEY-----
99
+ MCowBQYDK2VwAyEAmgUi7pKTJUmf3yzwcuH65wSvn1NyJrDfZHZUX08sGN8=
100
+ -----END PUBLIC KEY-----
101
+ `;
102
+ /** hq-fleet-local trust/release-public-v3.pem (added 2026-11-02; signs v0.4.20 and later). */
103
+ export const HQ_FLEET_LOCAL_PUBLIC_KEY_V3 = `-----BEGIN PUBLIC KEY-----
104
+ MCowBQYDK2VwAyEA2NUo6F38D4Kjxmy5qz5ytKRVRwocEvPk8lXqfwB2kS0=
105
+ -----END PUBLIC KEY-----
106
+ `;
107
+ /** Every key a release manifest may be signed with. Any one must verify it. */
108
+ export const HQ_FLEET_LOCAL_PUBLIC_KEYS = [HQ_FLEET_LOCAL_PUBLIC_KEY, HQ_FLEET_LOCAL_PUBLIC_KEY_V2, HQ_FLEET_LOCAL_PUBLIC_KEY_V3];
109
+ function publicKeyList(publicKey) {
110
+ return typeof publicKey === "string" ? [publicKey] : publicKey;
111
+ }
112
+ /** True when any trusted Ed25519 key verifies the manifest signature. */
113
+ export function fleetLocalManifestSignatureValid(manifest, signature, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEYS) {
114
+ return publicKeyList(publicKey).some((key) => {
115
+ try {
116
+ return crypto.verify(null, manifest, crypto.createPublicKey(key), signature);
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ });
122
+ }
95
123
  function digest(value) {
96
124
  return crypto.createHash("sha256").update(value).digest("hex");
97
125
  }
@@ -110,7 +138,7 @@ function engineMetadata(release) {
110
138
  export function fleetHome(home = os.homedir(), env = process.env) {
111
139
  return env.HQ_FLEET_HOME?.trim() || path.join(home, ".hq-fleet");
112
140
  }
113
- export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEY) {
141
+ export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEYS) {
114
142
  if (digest(files.archive) !== release.artifactSha256) {
115
143
  throw expectedError(`HQ Fleet Local ${release.version} archive hash did not match the release pin.`);
116
144
  }
@@ -142,10 +170,180 @@ export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE,
142
170
  if (signature.algorithm !== "ed25519" || typeof signature.signature !== "string") {
143
171
  throw expectedError(`HQ Fleet Local ${release.version} signature record is invalid.`);
144
172
  }
145
- const valid = crypto.verify(null, files.manifest, crypto.createPublicKey(publicKey), Buffer.from(signature.signature, "base64"));
173
+ const valid = fleetLocalManifestSignatureValid(files.manifest, Buffer.from(signature.signature, "base64"), publicKey);
146
174
  if (!valid) {
147
175
  throw expectedError(`HQ Fleet Local ${release.version} manifest signature is invalid.`);
148
176
  }
177
+ if (typeof runtime.archive_sha256 !== "string" || runtime.archive_sha256 !== digest(files.runtimeArchive)) {
178
+ throw expectedError(`HQ Fleet Local ${release.version} runtime archive does not match the signed manifest.`);
179
+ }
180
+ verifyArchiveAgainstManifest(files, manifest, release);
181
+ }
182
+ const RELEASE_METADATA_ENTRIES = new Set(["release-manifest.json", "release-manifest.sig", "release-public.pem"]);
183
+ const MAX_ARCHIVE_ENTRIES = 20_000;
184
+ const MAX_ARCHIVE_ENTRY_BYTES = 64 * 1024 * 1024;
185
+ function verifyArchiveAgainstManifest(files, manifest, release) {
186
+ const payload = manifest.payload;
187
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length === 0) {
188
+ throw expectedError(`HQ Fleet Local ${release.version} manifest lists no payload files.`);
189
+ }
190
+ let entries;
191
+ try {
192
+ entries = readZipEntries(files.archive);
193
+ }
194
+ catch (error) {
195
+ throw expectedError(`HQ Fleet Local ${release.version} archive could not be read: ${error instanceof Error ? error.message : String(error)}`);
196
+ }
197
+ for (const [name, expected] of Object.entries(payload)) {
198
+ const entry = entries.get(name);
199
+ const sha = expected && typeof expected === "object" ? expected.sha256 : undefined;
200
+ if (!entry || typeof sha !== "string" || digest(entry) !== sha) {
201
+ throw expectedError(`HQ Fleet Local ${release.version} archive file ${name} does not match the signed manifest.`);
202
+ }
203
+ }
204
+ for (const [name, entry] of entries) {
205
+ if (Object.prototype.hasOwnProperty.call(payload, name))
206
+ continue;
207
+ if (!RELEASE_METADATA_ENTRIES.has(name)) {
208
+ throw expectedError(`HQ Fleet Local ${release.version} archive has a file the signed manifest does not list: ${name}`);
209
+ }
210
+ if (name === "release-manifest.json" && !entry.equals(files.manifest)) {
211
+ throw expectedError(`HQ Fleet Local ${release.version} archive carries a different manifest.`);
212
+ }
213
+ }
214
+ }
215
+ /**
216
+ * Check an extracted distribution against the signed manifest: every payload
217
+ * file present with its hash, and nothing else but the release metadata.
218
+ */
219
+ export function verifyExtractedDistribution(dir, manifest, version) {
220
+ let payload;
221
+ try {
222
+ const parsed = JSON.parse(manifest.toString("utf8"));
223
+ if (!parsed.payload || typeof parsed.payload !== "object" || Array.isArray(parsed.payload))
224
+ throw new Error("no payload");
225
+ payload = parsed.payload;
226
+ }
227
+ catch (error) {
228
+ throw expectedError(`HQ Fleet Local ${version} manifest has no payload list: ${error instanceof Error ? error.message : String(error)}`);
229
+ }
230
+ const seen = new Set();
231
+ const walk = (current, prefix) => {
232
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
233
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
234
+ const full = path.join(current, entry.name);
235
+ if (entry.isSymbolicLink())
236
+ throw expectedError(`HQ Fleet Local ${version} extracted a link the signed manifest does not list: ${relative}`);
237
+ if (entry.isDirectory()) {
238
+ walk(full, relative);
239
+ continue;
240
+ }
241
+ if (Object.prototype.hasOwnProperty.call(payload, relative)) {
242
+ const expected = payload[relative];
243
+ const sha = expected && typeof expected === "object" ? expected.sha256 : undefined;
244
+ if (typeof sha !== "string" || digest(fs.readFileSync(full)) !== sha) {
245
+ throw expectedError(`HQ Fleet Local ${version} extracted file ${relative} does not match the signed manifest.`);
246
+ }
247
+ seen.add(relative);
248
+ continue;
249
+ }
250
+ if (!RELEASE_METADATA_ENTRIES.has(relative)) {
251
+ throw expectedError(`HQ Fleet Local ${version} extracted a file the signed manifest does not list: ${relative}`);
252
+ }
253
+ }
254
+ };
255
+ walk(dir, "");
256
+ for (const name of Object.keys(payload)) {
257
+ if (!seen.has(name))
258
+ throw expectedError(`HQ Fleet Local ${version} extracted distribution is missing ${name}.`);
259
+ }
260
+ }
261
+ /** Write a ZIP archive's files under `target` with the same reader that verified it. */
262
+ export function extractZipEntries(archive, target, version) {
263
+ let entries;
264
+ try {
265
+ entries = readZipEntries(archive);
266
+ }
267
+ catch (error) {
268
+ throw expectedError(`HQ Fleet Local ${version} archive could not be read: ${error instanceof Error ? error.message : String(error)}`);
269
+ }
270
+ const root = path.resolve(target);
271
+ for (const [name, content] of entries) {
272
+ const parts = name.split("/");
273
+ if (name.includes("\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name) || parts.some((part) => part === "" || part === "." || part === "..")) {
274
+ throw expectedError(`HQ Fleet Local ${version} archive has an unsafe file name: ${name}`);
275
+ }
276
+ const destination = path.join(root, ...parts);
277
+ if (!destination.startsWith(root + path.sep))
278
+ throw expectedError(`HQ Fleet Local ${version} archive has an unsafe file name: ${name}`);
279
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
280
+ fs.writeFileSync(destination, content, { mode: 0o644 });
281
+ }
282
+ }
283
+ /** Every file in a ZIP archive by name (stored and deflated entries only; directories skipped). */
284
+ export function readZipEntries(archive) {
285
+ const eocdSignature = 0x06054b50;
286
+ let eocd = -1;
287
+ for (let at = archive.length - 22; at >= Math.max(0, archive.length - 22 - 0xffff); at -= 1) {
288
+ if (archive.readUInt32LE(at) === eocdSignature) {
289
+ eocd = at;
290
+ break;
291
+ }
292
+ }
293
+ if (eocd < 0)
294
+ throw new Error("no end of central directory");
295
+ const count = archive.readUInt16LE(eocd + 10);
296
+ const directorySize = archive.readUInt32LE(eocd + 12);
297
+ const directoryOffset = archive.readUInt32LE(eocd + 16);
298
+ if (count === 0xffff || directoryOffset === 0xffffffff || count > MAX_ARCHIVE_ENTRIES)
299
+ throw new Error("unsupported archive size");
300
+ if (directoryOffset + directorySize !== eocd)
301
+ throw new Error("central directory is not where the archive end says");
302
+ if (eocd >= 20 && archive.readUInt32LE(eocd - 20) === 0x07064b50)
303
+ throw new Error("zip64 archives are not supported");
304
+ if (count > 0 && archive.readUInt32LE(0) !== 0x04034b50)
305
+ throw new Error("data before the first entry");
306
+ const entries = new Map();
307
+ let at = directoryOffset;
308
+ for (let index = 0; index < count; index += 1) {
309
+ if (at + 46 > eocd || archive.readUInt32LE(at) !== 0x02014b50)
310
+ throw new Error("bad central directory entry");
311
+ const flags = archive.readUInt16LE(at + 8);
312
+ const method = archive.readUInt16LE(at + 10);
313
+ const compressedSize = archive.readUInt32LE(at + 20);
314
+ const size = archive.readUInt32LE(at + 24);
315
+ const nameLength = archive.readUInt16LE(at + 28);
316
+ const extraLength = archive.readUInt16LE(at + 30);
317
+ const commentLength = archive.readUInt16LE(at + 32);
318
+ const localOffset = archive.readUInt32LE(at + 42);
319
+ const name = archive.subarray(at + 46, at + 46 + nameLength).toString("utf8");
320
+ at += 46 + nameLength + extraLength + commentLength;
321
+ if (name.endsWith("/"))
322
+ continue;
323
+ if ((flags & 0x1) !== 0)
324
+ throw new Error(`encrypted entry ${name}`);
325
+ if (size > MAX_ARCHIVE_ENTRY_BYTES || compressedSize > MAX_ARCHIVE_ENTRY_BYTES)
326
+ throw new Error(`entry too large: ${name}`);
327
+ if (entries.has(name))
328
+ throw new Error(`duplicate entry ${name}`);
329
+ if (localOffset + 30 > directoryOffset || archive.readUInt32LE(localOffset) !== 0x04034b50)
330
+ throw new Error(`bad local header for ${name}`);
331
+ const dataStart = localOffset + 30 + archive.readUInt16LE(localOffset + 26) + archive.readUInt16LE(localOffset + 28);
332
+ if (dataStart + compressedSize > directoryOffset)
333
+ throw new Error(`entry out of range: ${name}`);
334
+ const data = archive.subarray(dataStart, dataStart + compressedSize);
335
+ let content;
336
+ if (method === 0)
337
+ content = Buffer.from(data);
338
+ else if (method === 8)
339
+ content = zlib.inflateRawSync(data, { maxOutputLength: MAX_ARCHIVE_ENTRY_BYTES });
340
+ else
341
+ throw new Error(`unsupported compression for ${name}`);
342
+ if (content.length !== size)
343
+ throw new Error(`size mismatch for ${name}`);
344
+ entries.set(name, content);
345
+ }
346
+ return entries;
149
347
  }
150
348
  /**
151
349
  * Warning text when the signed manifest asks for a newer hq CLI than this
@@ -225,6 +423,196 @@ export async function fetchHqFleetReleaseLinks(release) {
225
423
  }
226
424
  return links.size > 0 ? links : null;
227
425
  }
426
+ export const FLEET_LOCAL_LATEST_TIMEOUT_MS = 5_000;
427
+ export const FLEET_LOCAL_LATEST_CACHE_MS = 10 * 60_000;
428
+ const FLEET_LOCAL_LATEST_CACHE_FILE = "release-pointer.json";
429
+ const SHA256_RE = /^[0-9a-f]{64}$/;
430
+ const COMMIT_RE = /^[0-9a-f]{40}$/;
431
+ export function parseFleetLocalReleasePin(value) {
432
+ if (!value || typeof value !== "object" || Array.isArray(value))
433
+ return null;
434
+ const record = value;
435
+ const keys = [
436
+ "repository",
437
+ "tag",
438
+ "version",
439
+ "artifact",
440
+ "artifactSha256",
441
+ "runtimeArtifact",
442
+ "runtimeArtifactSha256",
443
+ "manifestSha256",
444
+ "signatureSha256",
445
+ "runtimeCommit",
446
+ ];
447
+ if (!keys.every((key) => typeof record[key] === "string" && record[key].length > 0))
448
+ return null;
449
+ const pin = Object.fromEntries(keys.map((key) => [key, record[key]]));
450
+ if (pin.repository !== HQ_FLEET_LOCAL_RELEASE.repository)
451
+ return null;
452
+ if (!semver.valid(pin.version) || semver.clean(pin.version) !== pin.version)
453
+ return null;
454
+ if (pin.tag !== `v${pin.version}` || pin.artifact !== `hq-fleet-local-${pin.version}.zip`)
455
+ return null;
456
+ if (!COMMIT_RE.test(pin.runtimeCommit))
457
+ return null;
458
+ if (pin.runtimeArtifact !== `hq-fleet-runtime-${pin.runtimeCommit.slice(0, 12)}.tar`)
459
+ return null;
460
+ if (![pin.artifactSha256, pin.runtimeArtifactSha256, pin.manifestSha256, pin.signatureSha256].every((hash) => SHA256_RE.test(hash))) {
461
+ return null;
462
+ }
463
+ return pin;
464
+ }
465
+ export function parseFleetLocalReleasePointer(body, now = Date.now()) {
466
+ if (!body || typeof body !== "object" || Array.isArray(body))
467
+ return null;
468
+ const record = body;
469
+ const release = parseFleetLocalReleasePin(record.release);
470
+ if (!release || record.version !== release.version)
471
+ return null;
472
+ const minimumHqCli = typeof record.minimumHqCli === "string" && semver.valid(semver.coerce(record.minimumHqCli))
473
+ ? record.minimumHqCli
474
+ : null;
475
+ if (record.minimumHqCli !== undefined && record.minimumHqCli !== null && minimumHqCli === null)
476
+ return null;
477
+ const links = new Map();
478
+ for (const asset of Array.isArray(record.assets) ? record.assets : []) {
479
+ const entry = asset;
480
+ if (typeof entry?.name === "string" && typeof entry.url === "string")
481
+ links.set(entry.name, entry.url);
482
+ }
483
+ const expires = typeof record.expiresInSeconds === "number" && record.expiresInSeconds > 0 ? record.expiresInSeconds : 0;
484
+ return {
485
+ release,
486
+ minimumHqCli,
487
+ links: links.size > 0 && expires > 0 ? links : null,
488
+ linksExpireAt: links.size > 0 && expires > 0 ? now + expires * 1000 : null,
489
+ };
490
+ }
491
+ export function chooseFleetLocalTargetRelease(pointer, compiled = HQ_FLEET_LOCAL_RELEASE, cliVersion = CLI_VERSION) {
492
+ const fallback = { release: compiled, source: "compiled", links: null, linksExpireAt: null };
493
+ if (!pointer || !semver.gt(pointer.release.version, compiled.version))
494
+ return fallback;
495
+ if (pointer.minimumHqCli) {
496
+ const required = semver.valid(semver.coerce(pointer.minimumHqCli));
497
+ const current = semver.valid(semver.coerce(cliVersion));
498
+ if (!required || !current || semver.lt(current, required))
499
+ return fallback;
500
+ }
501
+ return { release: pointer.release, source: "pointer", links: pointer.links, linksExpireAt: pointer.linksExpireAt };
502
+ }
503
+ export async function fetchFleetLocalReleasePointer(deps = {}) {
504
+ const token = await (deps.token ?? (() => ensureCognitoToken({ tokenSource: "person", interactive: false })))();
505
+ const response = await (deps.fetchApi ?? vaultApiFetch)({
506
+ token,
507
+ path: "/v1/fleet-local/releases/latest",
508
+ signal: AbortSignal.timeout(FLEET_LOCAL_LATEST_TIMEOUT_MS),
509
+ });
510
+ if (!response.ok)
511
+ return null;
512
+ return parseFleetLocalReleasePointer(await response.json(), (deps.now ?? Date.now)());
513
+ }
514
+ let memoryPointer = null;
515
+ export function resetFleetLocalReleasePointerCache() {
516
+ memoryPointer = null;
517
+ }
518
+ function readPointerCache(root, now) {
519
+ if (memoryPointer && memoryPointer.root === root && now - memoryPointer.fetchedAt < FLEET_LOCAL_LATEST_CACHE_MS) {
520
+ return { body: memoryPointer.body };
521
+ }
522
+ try {
523
+ const cached = JSON.parse(fs.readFileSync(path.join(root, FLEET_LOCAL_LATEST_CACHE_FILE), "utf8"));
524
+ if (typeof cached.fetchedAt === "number" && now - cached.fetchedAt >= 0 && now - cached.fetchedAt < FLEET_LOCAL_LATEST_CACHE_MS) {
525
+ memoryPointer = { root, fetchedAt: cached.fetchedAt, body: cached.pointer };
526
+ return { body: cached.pointer };
527
+ }
528
+ }
529
+ catch {
530
+ return null;
531
+ }
532
+ return null;
533
+ }
534
+ function writePointerCache(root, now, body) {
535
+ memoryPointer = { root, fetchedAt: now, body };
536
+ try {
537
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
538
+ const file = path.join(root, FLEET_LOCAL_LATEST_CACHE_FILE);
539
+ const temporary = `${file}.${process.pid}.${crypto.randomUUID()}`;
540
+ fs.writeFileSync(temporary, `${JSON.stringify({ fetchedAt: now, pointer: body })}\n`, { mode: 0o600 });
541
+ fs.renameSync(temporary, file);
542
+ }
543
+ catch {
544
+ return;
545
+ }
546
+ }
547
+ function pointerToCache(pointer) {
548
+ if (!pointer)
549
+ return null;
550
+ return {
551
+ release: pointer.release,
552
+ minimumHqCli: pointer.minimumHqCli,
553
+ links: pointer.links ? [...pointer.links.entries()] : null,
554
+ linksExpireAt: pointer.linksExpireAt,
555
+ };
556
+ }
557
+ function pointerFromCache(body) {
558
+ if (!body || typeof body !== "object")
559
+ return null;
560
+ const record = body;
561
+ const release = parseFleetLocalReleasePin(record.release);
562
+ if (!release)
563
+ return null;
564
+ const links = Array.isArray(record.links)
565
+ ? new Map(record.links.filter((entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "string"))
566
+ : null;
567
+ return {
568
+ release,
569
+ minimumHqCli: typeof record.minimumHqCli === "string" ? record.minimumHqCli : null,
570
+ links: links && links.size > 0 ? links : null,
571
+ linksExpireAt: typeof record.linksExpireAt === "number" ? record.linksExpireAt : null,
572
+ };
573
+ }
574
+ async function loadFleetLocalReleasePointer(deps) {
575
+ const root = deps.fleetHome ?? fleetHome();
576
+ const now = (deps.now ?? Date.now)();
577
+ const cached = readPointerCache(root, now);
578
+ if (cached)
579
+ return pointerFromCache(cached.body);
580
+ try {
581
+ const pointer = await (deps.fetchPointer ?? (() => fetchFleetLocalReleasePointer()))();
582
+ writePointerCache(root, now, pointerToCache(pointer));
583
+ return pointer;
584
+ }
585
+ catch {
586
+ return null;
587
+ }
588
+ }
589
+ export async function resolveFleetLocalTargetRelease(deps = {}) {
590
+ const pointer = await loadFleetLocalReleasePointer(deps);
591
+ return chooseFleetLocalTargetRelease(pointer, deps.compiled ?? HQ_FLEET_LOCAL_RELEASE, deps.cliVersion ?? CLI_VERSION);
592
+ }
593
+ export async function resolveFleetLocalPointerTarget(deps = {}) {
594
+ const pointer = await loadFleetLocalReleasePointer(deps);
595
+ if (!pointer)
596
+ return null;
597
+ if (pointer.minimumHqCli) {
598
+ const required = semver.valid(semver.coerce(pointer.minimumHqCli));
599
+ const current = semver.valid(semver.coerce(deps.cliVersion ?? CLI_VERSION));
600
+ if (!required || !current || semver.lt(current, required))
601
+ return null;
602
+ }
603
+ return { release: pointer.release, source: "pointer", links: pointer.links, linksExpireAt: pointer.linksExpireAt };
604
+ }
605
+ export async function installTargetFleetLocalDistribution(deps = {}) {
606
+ const target = deps.target ?? await resolveFleetLocalTargetRelease({ ...(deps.fleetHome ? { fleetHome: deps.fleetHome } : {}) });
607
+ const now = (deps.now ?? Date.now)();
608
+ const freshLinks = target.links && target.linksExpireAt !== null && target.linksExpireAt - now > 60_000 ? target.links : null;
609
+ const { target: _target, now: _now, ...installDeps } = deps;
610
+ return installPinnedFleetLocalDistribution({
611
+ ...installDeps,
612
+ release: target.release,
613
+ ...(freshLinks && !installDeps.hqReleaseLinks ? { hqReleaseLinks: async () => freshLinks } : {}),
614
+ });
615
+ }
228
616
  function trustedReleaseLink(url) {
229
617
  try {
230
618
  const parsed = new URL(url);
@@ -326,7 +714,7 @@ function installFiles(target, files, release, publicKey) {
326
714
  fs.writeFileSync(path.join(staging, release.runtimeArtifact), files.runtimeArchive, { mode: 0o644 });
327
715
  fs.writeFileSync(path.join(staging, "release-manifest.json"), files.manifest, { mode: 0o644 });
328
716
  fs.writeFileSync(path.join(staging, "release-manifest.sig"), files.signature, { mode: 0o644 });
329
- fs.writeFileSync(path.join(staging, "release-public.pem"), publicKey, { mode: 0o644 });
717
+ fs.writeFileSync(path.join(staging, "release-public.pem"), publicKeyList(publicKey).join(""), { mode: 0o644 });
330
718
  fs.writeFileSync(path.join(staging, "install.json"), `${JSON.stringify({ schemaVersion: 1, release, engine: engineMetadata(release) }, null, 2)}\n`, { mode: 0o644 });
331
719
  if (fs.existsSync(target))
332
720
  fs.renameSync(target, backup);
@@ -366,7 +754,7 @@ function cachedRuntimeArchive(root, release) {
366
754
  }
367
755
  export async function installPinnedFleetLocalDistribution(deps = {}) {
368
756
  const release = deps.release ?? HQ_FLEET_LOCAL_RELEASE;
369
- const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEY;
757
+ const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEYS;
370
758
  const root = deps.fleetHome ?? fleetHome();
371
759
  const dir = path.join(root, "distributions", "hq-fleet-local", release.version);
372
760
  const current = installedFiles(dir, release);
@@ -471,11 +859,15 @@ function profileEngineMatchesRelease(profileDir, release) {
471
859
  try {
472
860
  const marker = JSON.parse(fs.readFileSync(path.join(profileDir, "bootstrap.json"), "utf8"));
473
861
  const expected = engineMetadata(release);
474
- return marker.engine?.id === expected.id &&
475
- marker.engine.distribution === expected.distribution &&
476
- marker.engine.version === expected.version &&
862
+ if (marker.engine?.id !== expected.id || marker.engine.distribution !== expected.distribution)
863
+ return false;
864
+ if (marker.engine.version === expected.version &&
477
865
  marker.engine.artifactSha256 === expected.artifactSha256 &&
478
- marker.engine.runtimeCommit === expected.runtimeCommit;
866
+ marker.engine.runtimeCommit === expected.runtimeCommit) {
867
+ return true;
868
+ }
869
+ const installed = typeof marker.engine.version === "string" ? semver.valid(marker.engine.version) : null;
870
+ return installed !== null && semver.valid(expected.version) !== null && semver.gt(installed, expected.version);
479
871
  }
480
872
  catch {
481
873
  return false;
@@ -513,7 +905,8 @@ function fleetLocalExecutables(agentUid, deps) {
513
905
  function runJson(run, executable, args, options, label) {
514
906
  let output;
515
907
  try {
516
- output = run(executable, args, { windowsHide: true, encoding: "utf8", ...options });
908
+ // windowsHide last: a helper, uv or Python child never opens a console window on Windows.
909
+ output = run(executable, args, { encoding: "utf8", ...options, windowsHide: true });
517
910
  }
518
911
  catch {
519
912
  throw expectedError(`HQ Fleet Local ${label} failed.`);
@@ -541,6 +934,31 @@ export function fleetLocalGatewayPid(agentUid, deps = {}) {
541
934
  return null;
542
935
  }
543
936
  }
937
+ /** The first hq-fleet-local release whose profile schema accepts `autoApprove`; older ones reject unknown fields. */
938
+ const AUTO_APPROVE_RELEASE = [0, 4, 19];
939
+ function releaseKnowsAutoApprove(release) {
940
+ const parts = release.version.split(".").map((part) => Number.parseInt(part, 10));
941
+ for (let i = 0; i < AUTO_APPROVE_RELEASE.length; i += 1) {
942
+ const have = Number.isFinite(parts[i]) ? parts[i] : 0;
943
+ if (have !== AUTO_APPROVE_RELEASE[i])
944
+ return have > AUTO_APPROVE_RELEASE[i];
945
+ }
946
+ return true;
947
+ }
948
+ /**
949
+ * Whether the bot's HQ Fleet gateway is installed as a login service. The
950
+ * runtime registers it with launchd as `ai.hq-fleet.gateway-<profile>`; null
951
+ * on platforms where hq-cli does not know the service layout.
952
+ */
953
+ export function fleetLocalServiceInstalled(agentUid, deps = {}) {
954
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
955
+ return null;
956
+ const platform = deps.platform ?? process.platform;
957
+ if (platform !== "darwin")
958
+ return null;
959
+ const home = deps.home ?? os.homedir();
960
+ return fs.existsSync(path.join(home, "Library", "LaunchAgents", `ai.hq-fleet.gateway-${profileName(agentUid)}.plist`));
961
+ }
544
962
  export function fleetLocalProfileNeedsAdoption(agentUid, deps = {}) {
545
963
  if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
546
964
  return true;
@@ -842,6 +1260,7 @@ export function writeFleetLocalProfile(input, deps = {}) {
842
1260
  botId: input.agentUid,
843
1261
  ownerUid: input.ownerUid,
844
1262
  ...(input.ownerCompanies?.length ? { ownerCompanies: [...new Set(input.ownerCompanies)].sort() } : {}),
1263
+ ...(input.autoApprove === true && releaseKnowsAutoApprove(input.installation.release) ? { autoApprove: true } : {}),
845
1264
  provider: input.provider ?? { id: "custom:personal-chatgpt", model: "gpt-5.6-sol" },
846
1265
  hqRoot: path.resolve(input.hqRoot),
847
1266
  profileDir: path.resolve(dir),
@@ -934,6 +1353,97 @@ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
934
1353
  throw failure;
935
1354
  return parsed;
936
1355
  }
1356
+ export function fleetLocalRuntimePython(agentUid, deps = {}) {
1357
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid)) {
1358
+ throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
1359
+ }
1360
+ const root = deps.fleetHome ?? fleetHome();
1361
+ const profileDir = profileRoot(agentUid, root);
1362
+ const platform = deps.platform ?? process.platform;
1363
+ const shared = sharedRuntimeFromMarker(profileDir, root, platform);
1364
+ const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
1365
+ const python = venvExecutable(runtimeDir, "python", platform);
1366
+ const source = shared?.runtimeSource ?? path.join(profileDir, "sources", "runtime");
1367
+ if (!fs.existsSync(python) || !fs.existsSync(source)) {
1368
+ throw expectedError(`HQ Fleet Local is not installed for ${agentUid}.`);
1369
+ }
1370
+ return { python, source, profileDir };
1371
+ }
1372
+ export function fleetProfileOwnerCompanies(agentUid, deps = {}) {
1373
+ let raw;
1374
+ try {
1375
+ raw = JSON.parse(fs.readFileSync(path.join(fleetLocalProfileDir(agentUid, deps), "profile.json"), "utf8"));
1376
+ }
1377
+ catch {
1378
+ return [];
1379
+ }
1380
+ const companies = raw && typeof raw === "object" ? raw.ownerCompanies : undefined;
1381
+ return Array.isArray(companies) ? companies.filter((slug) => typeof slug === "string" && /^[a-z0-9][a-z0-9_-]*$/i.test(slug)) : [];
1382
+ }
1383
+ export function fleetLocalProfileDir(agentUid, deps = {}) {
1384
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid)) {
1385
+ throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
1386
+ }
1387
+ return profileRoot(agentUid, deps.fleetHome ?? fleetHome());
1388
+ }
1389
+ export function readFleetLocalGatewayState(agentUid, deps = {}) {
1390
+ let raw;
1391
+ try {
1392
+ raw = JSON.parse(fs.readFileSync(path.join(fleetLocalProfileDir(agentUid, deps), "gateway_state.json"), "utf8"));
1393
+ }
1394
+ catch {
1395
+ return null;
1396
+ }
1397
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
1398
+ return null;
1399
+ const platforms = {};
1400
+ if (raw.platforms && typeof raw.platforms === "object" && !Array.isArray(raw.platforms)) {
1401
+ for (const [name, value] of Object.entries(raw.platforms)) {
1402
+ const state = value?.state;
1403
+ if (typeof state === "string" && state)
1404
+ platforms[name] = state;
1405
+ }
1406
+ }
1407
+ const active = Number(raw.active_agents);
1408
+ return {
1409
+ pid: typeof raw.pid === "number" && Number.isInteger(raw.pid) && raw.pid > 0 ? raw.pid : null,
1410
+ gatewayState: typeof raw.gateway_state === "string" ? raw.gateway_state : null,
1411
+ activeAgents: Number.isFinite(active) && active > 0 ? Math.floor(active) : 0,
1412
+ platforms,
1413
+ };
1414
+ }
1415
+ export function fleetLocalChannels(state, gatewayAlive) {
1416
+ const lane = (name) => (gatewayAlive && state?.platforms[name]) || "not_connected";
1417
+ return { slack: lane("slack"), telegram: lane("telegram") };
1418
+ }
1419
+ export function fleetLocalBusyReason(agentUid, deps = {}) {
1420
+ const state = readFleetLocalGatewayState(agentUid, deps);
1421
+ const alive = deps.isAlive ?? ((pid) => {
1422
+ try {
1423
+ process.kill(pid, 0);
1424
+ return true;
1425
+ }
1426
+ catch (error) {
1427
+ return error.code === "EPERM";
1428
+ }
1429
+ });
1430
+ if (!state || state.pid === null || !alive(state.pid))
1431
+ return null;
1432
+ if (state.activeAgents > 0)
1433
+ return `${state.activeAgents} turn${state.activeAgents === 1 ? "" : "s"} in progress`;
1434
+ if (state.gatewayState === "draining" || state.gatewayState === "starting")
1435
+ return `gateway is ${state.gatewayState}`;
1436
+ try {
1437
+ const store = JSON.parse(fs.readFileSync(path.join(fleetLocalProfileDir(agentUid, deps), "cron", "jobs.json"), "utf8"));
1438
+ const running = (store.jobs ?? []).find((job) => job && job.fire_claim !== null && job.fire_claim !== undefined);
1439
+ if (running)
1440
+ return typeof running.name === "string" && running.name ? `scheduled job "${running.name}" is running` : "a scheduled job is running";
1441
+ }
1442
+ catch {
1443
+ return null;
1444
+ }
1445
+ return null;
1446
+ }
937
1447
  /** Checks the instructions with the runtime's scanner, then makes them the bot's persona. */
938
1448
  export function refreshFleetLocalInstructions(agentUid, systemPrompt, deps = {}) {
939
1449
  const findings = scanFleetLocalInstructions(agentUid, systemPrompt, deps);
@@ -952,23 +1462,79 @@ export function stderrTail(error) {
952
1462
  const tail = lines.slice(-STDERR_TAIL_LINES).join("\n");
953
1463
  return tail.length > STDERR_TAIL_CHARS ? `...${tail.slice(-STDERR_TAIL_CHARS)}` : tail;
954
1464
  }
955
- function checkedRun(run, executable, args, options, label) {
1465
+ function checkedRun(run, executable, args, options, label, spawn) {
956
1466
  // Keep stdout quiet but capture stderr, so a failure names its cause
957
1467
  // (for example a path-too-long error during extraction).
958
- const stdio = options.stdio === "ignore" ? ["ignore", "ignore", "pipe"] : options.stdio;
1468
+ const requested = options.stdio === "ignore" ? ["ignore", "ignore", "pipe"] : options.stdio;
1469
+ // Node only applies CREATE_NO_WINDOW when no stdio slot is inherited
1470
+ // (libuv src/win/process.c), so windowsHide alone does not hide a child
1471
+ // given an fd or "inherit". Pipe those slots and copy the output through.
1472
+ const { stdio, forward } = pipeInheritedStdio(requested);
1473
+ const spawnOptions = {
1474
+ maxBuffer: 16 * 1024 * 1024,
1475
+ ...options,
1476
+ ...(stdio === undefined ? {} : { stdio }),
1477
+ // Last, so no caller can turn it off: no console window flashes on Windows.
1478
+ windowsHide: true,
1479
+ };
1480
+ if (forward) {
1481
+ // spawnSync keeps stderr on success too (execFileSync drops it once stdio
1482
+ // is set), so uv warnings and "Installed N packages" still reach the user.
1483
+ const result = (spawn ?? spawnFromRun(run))(executable, args, spawnOptions);
1484
+ copyChildOutput(result.stdout);
1485
+ if (!result.error && result.status === 0) {
1486
+ copyChildOutput(result.stderr);
1487
+ return;
1488
+ }
1489
+ // On failure stderr appears once, as the tail in the error message.
1490
+ const tail = stderrTail(result);
1491
+ throw expectedError(`HQ Fleet Local ${label} failed.${tail ? `\n${tail}` : ""}`);
1492
+ }
959
1493
  try {
960
- run(executable, args, {
961
- windowsHide: true,
962
- maxBuffer: 16 * 1024 * 1024,
963
- ...options,
964
- ...(stdio === undefined ? {} : { stdio }),
965
- });
1494
+ run(executable, args, spawnOptions);
966
1495
  }
967
1496
  catch (error) {
968
1497
  const tail = stderrTail(error);
969
1498
  throw expectedError(`HQ Fleet Local ${label} failed.${tail ? `\n${tail}` : ""}`);
970
1499
  }
971
1500
  }
1501
+ /** A FleetLocalSpawn backed by an execFileSync-shaped runner (stderr is only known on failure). */
1502
+ export function spawnFromRun(run) {
1503
+ return (executable, args, options) => {
1504
+ try {
1505
+ return { status: 0, stdout: run(executable, args, options), stderr: "" };
1506
+ }
1507
+ catch (error) {
1508
+ const failed = (error ?? {});
1509
+ return { status: typeof failed.status === "number" ? failed.status : 1, stdout: failed.stdout ?? null, stderr: failed.stderr ?? null };
1510
+ }
1511
+ };
1512
+ }
1513
+ /**
1514
+ * Replaces inherited stdio slots (an fd number or "inherit") with "pipe".
1515
+ * `forward` is true when any slot was replaced, so the caller copies the
1516
+ * captured output to this process's stderr.
1517
+ */
1518
+ export function pipeInheritedStdio(stdio) {
1519
+ if (stdio === "inherit")
1520
+ return { stdio: ["ignore", "pipe", "pipe"], forward: true };
1521
+ if (!Array.isArray(stdio))
1522
+ return { stdio, forward: false };
1523
+ let forward = false;
1524
+ const piped = stdio.map((slot, index) => {
1525
+ if (typeof slot === "number" || slot === "inherit") {
1526
+ forward = true;
1527
+ return index === 0 ? "ignore" : "pipe";
1528
+ }
1529
+ return slot;
1530
+ });
1531
+ return { stdio: piped, forward };
1532
+ }
1533
+ function copyChildOutput(output) {
1534
+ const text = Buffer.isBuffer(output) ? output.toString("utf8") : typeof output === "string" ? output : "";
1535
+ if (text)
1536
+ process.stderr.write(text);
1537
+ }
972
1538
  const FLEET_TAR_PATH_PROBE = [
973
1539
  "import json, sys, tarfile",
974
1540
  "longest = 0",
@@ -1036,7 +1602,7 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
1036
1602
  ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
1037
1603
  fs.rmSync(machineCredentials, { force: true });
1038
1604
  }
1039
- async function installSharedFleetRuntime(installation, root, python, platform, run) {
1605
+ async function installSharedFleetRuntime(installation, root, python, platform, run, spawn) {
1040
1606
  const identity = digest(Buffer.from(JSON.stringify({
1041
1607
  version: installation.release.version,
1042
1608
  distribution: installation.release.artifactSha256,
@@ -1081,13 +1647,8 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1081
1647
  fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
1082
1648
  fs.mkdirSync(runtimeSource, { recursive: true, mode: 0o700 });
1083
1649
  const uvEnv = fleetUvEnv(python.pythonDir);
1084
- checkedRun(run, python.python, [
1085
- "-m",
1086
- "zipfile",
1087
- "-e",
1088
- installation.archive,
1089
- distributionSource,
1090
- ], { stdio: "ignore" }, "distribution extraction");
1650
+ extractZipEntries(fs.readFileSync(installation.archive), distributionSource, installation.release.version);
1651
+ verifyExtractedDistribution(distributionSource, fs.readFileSync(installation.manifest), installation.release.version);
1091
1652
  if (platform === "win32")
1092
1653
  assertWindowsExtractionFits(run, python.python, installation.runtimeArchive, runtimeSource);
1093
1654
  checkedRun(run, python.python, [
@@ -1114,7 +1675,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1114
1675
  "--editable",
1115
1676
  `${runtimeSource}[mcp,anthropic]`,
1116
1677
  distributionSource,
1117
- ], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation");
1678
+ ], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation", spawn);
1118
1679
  if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
1119
1680
  throw expectedError("HQ Fleet Local installed without its runtime executables.");
1120
1681
  }
@@ -1140,7 +1701,8 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
1140
1701
  const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
1141
1702
  const profileDir = adoptLegacyProfileDirectory(input.agentUid, root);
1142
1703
  fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
1143
- const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run);
1704
+ const spawn = deps.spawn ?? (deps.run ? spawnFromRun(deps.run) : spawnSync);
1705
+ const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run, spawn);
1144
1706
  writeNativeProfileMarker(profileDir, shared, input.installation.engine);
1145
1707
  const { systemPrompt, machineCredentials, ...profileInput } = input;
1146
1708
  if (systemPrompt !== undefined)
@@ -1170,7 +1732,7 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
1170
1732
  checkedRun(run, helper, ["doctor-profile", "--profile", profileDir, "--runtime", runtime], { stdio: "ignore" }, "profile validation");
1171
1733
  const start = deps.start !== false;
1172
1734
  if (start) {
1173
- checkedRun(run, helper, ["service", "install", "--profile", profileDir, "--runtime", runtime], { stdio: ["ignore", 2, 2] }, "service installation");
1735
+ checkedRun(run, helper, ["service", "install", "--profile", profileDir, "--runtime", runtime], { stdio: ["ignore", 2, 2] }, "service installation", spawn);
1174
1736
  removeEmbeddedRuntime(profileDir, platform);
1175
1737
  }
1176
1738
  return { profile, runtime, helper, running: start };