@indigoai-us/hq-cli 5.345.191 → 5.345.194

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 (129) hide show
  1. package/CHANGELOG.md +431 -0
  2. package/assets/bot-workers/setup/worker.yaml +91 -34
  3. package/dist/command-catalog.generated.d.ts +145 -19
  4. package/dist/command-catalog.generated.js +184 -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 +4 -3
  8. package/dist/commands/bot.js +265 -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 +46 -0
  16. package/dist/commands/signals-run.js +180 -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 +58 -0
  20. package/dist/commands/sources.js +294 -26
  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/commands/whoami.d.ts +7 -0
  24. package/dist/commands/whoami.js +35 -2
  25. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
  26. package/dist/hq-cloud-sync-runner/package.json +1 -1
  27. package/dist/lib/bot/api.d.ts +3 -2
  28. package/dist/lib/bot/config.d.ts +2 -0
  29. package/dist/lib/bot/fleet-cron.d.ts +121 -0
  30. package/dist/lib/bot/fleet-cron.js +319 -0
  31. package/dist/lib/bot/fleet-local.d.ts +93 -1
  32. package/dist/lib/bot/fleet-local.js +546 -34
  33. package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
  34. package/dist/lib/bot/fleet-owner-companies.js +30 -0
  35. package/dist/lib/bot/fleet-watch.d.ts +25 -0
  36. package/dist/lib/bot/fleet-watch.js +51 -0
  37. package/dist/lib/bot/index.d.ts +1 -1
  38. package/dist/lib/bot/index.js +1 -1
  39. package/dist/lib/bot/job-run.d.ts +5 -4
  40. package/dist/lib/bot/job-run.js +5 -4
  41. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
  42. package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
  43. package/dist/lib/bot/local-jobs.d.ts +13 -2
  44. package/dist/lib/bot/local-jobs.js +17 -4
  45. package/dist/lib/bot/progress.d.ts +14 -0
  46. package/dist/lib/bot/progress.js +47 -1
  47. package/dist/lib/bot/run.d.ts +55 -2
  48. package/dist/lib/bot/run.js +192 -10
  49. package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
  50. package/dist/lib/bot/setup-tool-offer.js +16 -0
  51. package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
  52. package/dist/lib/bot/signals-engine-flag.js +30 -0
  53. package/dist/lib/daemon/catalog.d.ts +4 -0
  54. package/dist/lib/daemon/catalog.js +29 -9
  55. package/dist/lib/daemon/legacy.d.ts +6 -0
  56. package/dist/lib/daemon/legacy.js +16 -10
  57. package/dist/lib/daemon/run.d.ts +1 -0
  58. package/dist/lib/daemon/run.js +10 -0
  59. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  60. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  61. package/dist/lib/flag-registry.d.ts +13 -1
  62. package/dist/lib/flag-registry.js +46 -0
  63. package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
  64. package/dist/lib/hot-channels/adapters/attio.js +367 -29
  65. package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
  66. package/dist/lib/hot-channels/adapters/gmail.js +59 -2
  67. package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
  68. package/dist/lib/hot-channels/adapters/linear.js +71 -1
  69. package/dist/lib/hot-channels/adapters/meetings.js +4 -1
  70. package/dist/lib/hot-channels/adapters/slack.js +7 -3
  71. package/dist/lib/hot-channels/alert.js +1 -1
  72. package/dist/lib/hot-channels/contract.d.ts +11 -0
  73. package/dist/lib/hot-channels/contract.js +1 -0
  74. package/dist/lib/hot-channels/cursors.js +1 -1
  75. package/dist/lib/hot-channels/landing.d.ts +6 -2
  76. package/dist/lib/hot-channels/landing.js +13 -6
  77. package/dist/lib/hot-channels/ledger.js +2 -0
  78. package/dist/lib/hot-channels/loop.d.ts +6 -0
  79. package/dist/lib/hot-channels/loop.js +19 -4
  80. package/dist/lib/hot-channels/registry.js +9 -1
  81. package/dist/lib/hot-channels/run.d.ts +18 -0
  82. package/dist/lib/hot-channels/run.js +15 -4
  83. package/dist/lib/hot-channels/signals-pass.d.ts +34 -2
  84. package/dist/lib/hot-channels/signals-pass.js +149 -38
  85. package/dist/lib/hot-channels/signals-redo.d.ts +60 -0
  86. package/dist/lib/hot-channels/signals-redo.js +207 -0
  87. package/dist/lib/hot-channels/sync-lock.js +22 -6
  88. package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
  89. package/dist/lib/mesh/live/daemon/install.js +117 -30
  90. package/dist/lib/signals/adapters-catalog.d.ts +77 -0
  91. package/dist/lib/signals/adapters-catalog.js +194 -0
  92. package/dist/lib/signals/engine.d.ts +202 -0
  93. package/dist/lib/signals/engine.js +504 -0
  94. package/dist/lib/signals/failure-alert.d.ts +89 -0
  95. package/dist/lib/signals/failure-alert.js +175 -0
  96. package/dist/lib/signals/local-date.d.ts +3 -0
  97. package/dist/lib/signals/local-date.js +6 -0
  98. package/dist/lib/signals/projection.d.ts +42 -0
  99. package/dist/lib/signals/projection.js +67 -0
  100. package/dist/lib/signals/reply.d.ts +112 -0
  101. package/dist/lib/signals/reply.js +368 -0
  102. package/dist/lib/signals/run-lock.d.ts +36 -0
  103. package/dist/lib/signals/run-lock.js +148 -0
  104. package/dist/lib/signals/run-record.d.ts +24 -0
  105. package/dist/lib/signals/run-record.js +82 -0
  106. package/dist/lib/signals/sanitize.d.ts +105 -0
  107. package/dist/lib/signals/sanitize.js +439 -0
  108. package/dist/lib/signals/settings.d.ts +10 -0
  109. package/dist/lib/signals/settings.js +48 -0
  110. package/dist/lib/signals/steps/adapters.d.ts +14 -0
  111. package/dist/lib/signals/steps/adapters.js +156 -0
  112. package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
  113. package/dist/lib/signals/steps/entity-similarity.js +47 -0
  114. package/dist/lib/signals/steps/extract.d.ts +118 -0
  115. package/dist/lib/signals/steps/extract.js +793 -0
  116. package/dist/lib/signals/steps/garden.d.ts +24 -0
  117. package/dist/lib/signals/steps/garden.js +250 -0
  118. package/dist/lib/signals/steps/types.d.ts +117 -0
  119. package/dist/lib/signals/steps/types.js +4 -0
  120. package/dist/lib/signals/steps/upload.d.ts +43 -0
  121. package/dist/lib/signals/steps/upload.js +324 -0
  122. package/dist/lib/signals/write-guard.d.ts +63 -0
  123. package/dist/lib/signals/write-guard.js +287 -0
  124. package/dist/utils/resident-package-preload.js +16 -0
  125. package/dist/utils/vault-api.d.ts +1 -0
  126. package/dist/utils/vault-api.js +1 -1
  127. package/package.json +9 -9
  128. package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
  129. 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,14 +79,14 @@ 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.18",
82
- version: "0.4.18",
83
- artifact: "hq-fleet-local-0.4.18.zip",
84
- artifactSha256: "194ae7da24d02205061b18172a84c972754bd0607ed3b4c32af7ac10d73904a7",
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: "ce14449198a0f5d8558ecc617c19551b7a2a31dba5bd563501a93d2ee58e5ff0",
88
- signatureSha256: "ca7dac6bcc2d12f6e304d1940e7b00243f9c306e7f0a72fd4aa236b7778d3a93",
88
+ manifestSha256: "3cec47eb7f47af13e66d4ad29669d3c103e1ca452661f5eaad760243118ce214",
89
+ signatureSha256: "5504c3a956f36d148f3cdf2212a9b171d852098c82eb199c7c415ad3545e3b05",
89
90
  runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
90
91
  };
91
92
  /** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
@@ -98,8 +99,13 @@ export const HQ_FLEET_LOCAL_PUBLIC_KEY_V2 = `-----BEGIN PUBLIC KEY-----
98
99
  MCowBQYDK2VwAyEAmgUi7pKTJUmf3yzwcuH65wSvn1NyJrDfZHZUX08sGN8=
99
100
  -----END PUBLIC KEY-----
100
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
+ `;
101
107
  /** Every key a release manifest may be signed with. Any one must verify it. */
102
- export const HQ_FLEET_LOCAL_PUBLIC_KEYS = [HQ_FLEET_LOCAL_PUBLIC_KEY, HQ_FLEET_LOCAL_PUBLIC_KEY_V2];
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];
103
109
  function publicKeyList(publicKey) {
104
110
  return typeof publicKey === "string" ? [publicKey] : publicKey;
105
111
  }
@@ -168,6 +174,176 @@ export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE,
168
174
  if (!valid) {
169
175
  throw expectedError(`HQ Fleet Local ${release.version} manifest signature is invalid.`);
170
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;
171
347
  }
172
348
  /**
173
349
  * Warning text when the signed manifest asks for a newer hq CLI than this
@@ -247,6 +423,196 @@ export async function fetchHqFleetReleaseLinks(release) {
247
423
  }
248
424
  return links.size > 0 ? links : null;
249
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
+ }
250
616
  function trustedReleaseLink(url) {
251
617
  try {
252
618
  const parsed = new URL(url);
@@ -493,11 +859,15 @@ function profileEngineMatchesRelease(profileDir, release) {
493
859
  try {
494
860
  const marker = JSON.parse(fs.readFileSync(path.join(profileDir, "bootstrap.json"), "utf8"));
495
861
  const expected = engineMetadata(release);
496
- return marker.engine?.id === expected.id &&
497
- marker.engine.distribution === expected.distribution &&
498
- 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 &&
499
865
  marker.engine.artifactSha256 === expected.artifactSha256 &&
500
- 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);
501
871
  }
502
872
  catch {
503
873
  return false;
@@ -565,7 +935,7 @@ export function fleetLocalGatewayPid(agentUid, deps = {}) {
565
935
  }
566
936
  }
567
937
  /** The first hq-fleet-local release whose profile schema accepts `autoApprove`; older ones reject unknown fields. */
568
- const AUTO_APPROVE_RELEASE = [0, 4, 18];
938
+ const AUTO_APPROVE_RELEASE = [0, 4, 19];
569
939
  function releaseKnowsAutoApprove(release) {
570
940
  const parts = release.version.split(".").map((part) => Number.parseInt(part, 10));
571
941
  for (let i = 0; i < AUTO_APPROVE_RELEASE.length; i += 1) {
@@ -890,7 +1260,7 @@ export function writeFleetLocalProfile(input, deps = {}) {
890
1260
  botId: input.agentUid,
891
1261
  ownerUid: input.ownerUid,
892
1262
  ...(input.ownerCompanies?.length ? { ownerCompanies: [...new Set(input.ownerCompanies)].sort() } : {}),
893
- ...(input.autoApprove === false && releaseKnowsAutoApprove(input.installation.release) ? { autoApprove: false } : {}),
1263
+ ...(input.autoApprove === true && releaseKnowsAutoApprove(input.installation.release) ? { autoApprove: true } : {}),
894
1264
  provider: input.provider ?? { id: "custom:personal-chatgpt", model: "gpt-5.6-sol" },
895
1265
  hqRoot: path.resolve(input.hqRoot),
896
1266
  profileDir: path.resolve(dir),
@@ -983,6 +1353,97 @@ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
983
1353
  throw failure;
984
1354
  return parsed;
985
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
+ }
986
1447
  /** Checks the instructions with the runtime's scanner, then makes them the bot's persona. */
987
1448
  export function refreshFleetLocalInstructions(agentUid, systemPrompt, deps = {}) {
988
1449
  const findings = scanFleetLocalInstructions(agentUid, systemPrompt, deps);
@@ -1001,24 +1462,79 @@ export function stderrTail(error) {
1001
1462
  const tail = lines.slice(-STDERR_TAIL_LINES).join("\n");
1002
1463
  return tail.length > STDERR_TAIL_CHARS ? `...${tail.slice(-STDERR_TAIL_CHARS)}` : tail;
1003
1464
  }
1004
- function checkedRun(run, executable, args, options, label) {
1465
+ function checkedRun(run, executable, args, options, label, spawn) {
1005
1466
  // Keep stdout quiet but capture stderr, so a failure names its cause
1006
1467
  // (for example a path-too-long error during extraction).
1007
- 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
+ }
1008
1493
  try {
1009
- run(executable, args, {
1010
- maxBuffer: 16 * 1024 * 1024,
1011
- ...options,
1012
- ...(stdio === undefined ? {} : { stdio }),
1013
- // Last, so no caller can turn it off: no console window flashes on Windows.
1014
- windowsHide: true,
1015
- });
1494
+ run(executable, args, spawnOptions);
1016
1495
  }
1017
1496
  catch (error) {
1018
1497
  const tail = stderrTail(error);
1019
1498
  throw expectedError(`HQ Fleet Local ${label} failed.${tail ? `\n${tail}` : ""}`);
1020
1499
  }
1021
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
+ }
1022
1538
  const FLEET_TAR_PATH_PROBE = [
1023
1539
  "import json, sys, tarfile",
1024
1540
  "longest = 0",
@@ -1086,7 +1602,7 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
1086
1602
  ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
1087
1603
  fs.rmSync(machineCredentials, { force: true });
1088
1604
  }
1089
- async function installSharedFleetRuntime(installation, root, python, platform, run) {
1605
+ async function installSharedFleetRuntime(installation, root, python, platform, run, spawn) {
1090
1606
  const identity = digest(Buffer.from(JSON.stringify({
1091
1607
  version: installation.release.version,
1092
1608
  distribution: installation.release.artifactSha256,
@@ -1131,13 +1647,8 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1131
1647
  fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
1132
1648
  fs.mkdirSync(runtimeSource, { recursive: true, mode: 0o700 });
1133
1649
  const uvEnv = fleetUvEnv(python.pythonDir);
1134
- checkedRun(run, python.python, [
1135
- "-m",
1136
- "zipfile",
1137
- "-e",
1138
- installation.archive,
1139
- distributionSource,
1140
- ], { stdio: "ignore" }, "distribution extraction");
1650
+ extractZipEntries(fs.readFileSync(installation.archive), distributionSource, installation.release.version);
1651
+ verifyExtractedDistribution(distributionSource, fs.readFileSync(installation.manifest), installation.release.version);
1141
1652
  if (platform === "win32")
1142
1653
  assertWindowsExtractionFits(run, python.python, installation.runtimeArchive, runtimeSource);
1143
1654
  checkedRun(run, python.python, [
@@ -1164,7 +1675,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1164
1675
  "--editable",
1165
1676
  `${runtimeSource}[mcp,anthropic]`,
1166
1677
  distributionSource,
1167
- ], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation");
1678
+ ], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation", spawn);
1168
1679
  if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
1169
1680
  throw expectedError("HQ Fleet Local installed without its runtime executables.");
1170
1681
  }
@@ -1190,7 +1701,8 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
1190
1701
  const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
1191
1702
  const profileDir = adoptLegacyProfileDirectory(input.agentUid, root);
1192
1703
  fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
1193
- 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);
1194
1706
  writeNativeProfileMarker(profileDir, shared, input.installation.engine);
1195
1707
  const { systemPrompt, machineCredentials, ...profileInput } = input;
1196
1708
  if (systemPrompt !== undefined)
@@ -1220,7 +1732,7 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
1220
1732
  checkedRun(run, helper, ["doctor-profile", "--profile", profileDir, "--runtime", runtime], { stdio: "ignore" }, "profile validation");
1221
1733
  const start = deps.start !== false;
1222
1734
  if (start) {
1223
- 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);
1224
1736
  removeEmbeddedRuntime(profileDir, platform);
1225
1737
  }
1226
1738
  return { profile, runtime, helper, running: start };