@indigoai-us/hq-cli 5.345.179 → 5.345.181

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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/command-catalog.generated.d.ts +126 -0
  3. package/dist/command-catalog.generated.js +155 -0
  4. package/dist/commands/bot.d.ts +29 -1
  5. package/dist/commands/bot.js +282 -22
  6. package/dist/commands/channels.d.ts +18 -1
  7. package/dist/commands/channels.js +185 -1
  8. package/dist/commands/lanes.d.ts +1 -1
  9. package/dist/commands/lanes.js +6 -2
  10. package/dist/commands/mesh.js +19 -0
  11. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
  12. package/dist/hq-cloud-sync-runner/package.json +1 -1
  13. package/dist/lib/bot/config.d.ts +8 -0
  14. package/dist/lib/bot/fleet-local.d.ts +7 -3
  15. package/dist/lib/bot/fleet-local.js +234 -64
  16. package/dist/lib/bot/run.js +39 -2
  17. package/dist/lib/bot/runtime-auth.d.ts +119 -0
  18. package/dist/lib/bot/runtime-auth.js +251 -0
  19. package/dist/lib/daemon/catalog.d.ts +1 -0
  20. package/dist/lib/daemon/catalog.js +1 -1
  21. package/dist/lib/daemon/run.d.ts +2 -0
  22. package/dist/lib/daemon/run.js +64 -16
  23. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  24. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  25. package/dist/lib/hot-channels/adapters/meetings.js +3 -1
  26. package/dist/lib/hot-channels/contract.d.ts +6 -0
  27. package/dist/lib/hot-channels/ledger.js +17 -0
  28. package/dist/lib/hot-channels/loop.js +2 -0
  29. package/dist/lib/lanes/accounts.d.ts +1 -17
  30. package/dist/lib/lanes/accounts.js +5 -45
  31. package/dist/lib/lanes/provider-home.d.ts +24 -0
  32. package/dist/lib/lanes/provider-home.js +52 -0
  33. package/dist/lib/lanes/spawn.d.ts +12 -0
  34. package/dist/lib/lanes/spawn.js +27 -0
  35. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  36. package/dist/lib/mesh/live/daemon/doctor.js +13 -0
  37. package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
  38. package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
  39. package/dist/lib/mesh/live/daemon/run.js +31 -0
  40. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
  41. package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
  42. package/dist/lib/mesh/live/paths.d.ts +2 -0
  43. package/dist/lib/mesh/live/paths.js +2 -0
  44. package/dist/lib/sync-core/flag.d.ts +1 -1
  45. package/dist/lib/sync-core/flag.js +1 -1
  46. package/dist/lib/sync-core/reconcile.d.ts +1 -1
  47. package/dist/lib/sync-core/reconcile.js +9 -7
  48. package/dist/lib/sync-core/release.d.ts +3 -3
  49. package/dist/lib/sync-core/release.js +4 -4
  50. package/dist/lib/sync-core/rollout-state.d.ts +1 -0
  51. package/dist/lib/sync-core/rollout-state.js +1 -0
  52. package/package.json +8 -8
@@ -1 +1 @@
1
- {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.104","bundleSha256":"40c72e049db21ee4551fa737676d8022cd090149ede9e3c9e53a9fae3f28dadc","formatVersion":1}
1
+ {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.105","bundleSha256":"f57aa12671cdfcf08ce39f11ce6885d6e52d5478f2ae77a75d38d79162194ce5","formatVersion":1}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * bot.json — non-secret per-bot configuration (local-bots US-003/US-005).
3
3
  */
4
+ import type { BotRuntimeAuthMap } from "./runtime-auth.js";
4
5
  export declare const BOT_RUNTIMES: readonly ["claude", "codex", "grok"];
5
6
  export type BotRuntimeId = (typeof BOT_RUNTIMES)[number];
6
7
  export declare const HQ_FLEET_RUNTIME_ENGINE_ID: "hq-fleet-runtime";
@@ -167,6 +168,13 @@ export interface BotConfig {
167
168
  * setup bots, which only need files and shell commands.
168
169
  */
169
170
  lean?: boolean;
171
+ /**
172
+ * Which model login each runtime uses (see runtime-auth.ts): unset = the
173
+ * owner's default sign-in; { mode: "own" } = the bot's own login under
174
+ * ~/.hq/bots/<name>/auth/<runtime>; { mode: "account", account } = a
175
+ * capacity account home. Names only, never a credential.
176
+ */
177
+ runtimeAuth?: BotRuntimeAuthMap;
170
178
  }
171
179
  export declare function readBotConfig(dir: string): BotConfig | null;
172
180
  export declare function writeBotConfig(dir: string, config: BotConfig): void;
@@ -99,6 +99,9 @@ export interface FleetLocalBootstrapDeps {
99
99
  start?: boolean;
100
100
  python?: FleetPython;
101
101
  }
102
+ type FleetLocalProfileDeps = Pick<FleetLocalBootstrapDeps, "fleetHome" | "platform"> & {
103
+ runtimeRoot?: string;
104
+ };
102
105
  export interface FleetLocalBootstrapResult {
103
106
  profile: string;
104
107
  runtime: string;
@@ -133,7 +136,7 @@ export interface FleetLocalRemovalResult {
133
136
  credentialsRemoved: number;
134
137
  }
135
138
  export declare const HQ_FLEET_LOCAL_RELEASE: FleetLocalReleasePin;
136
- export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbYvc1pwOmiCUPJv8yIa3E9RXfniFA84nH2+ROmLMGZU=\n-----END PUBLIC KEY-----\n";
139
+ export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEARXr2I8RNcw2c0Ep77XkYGwylresXBFn8dm9pxN2Zq/U=\n-----END PUBLIC KEY-----\n";
137
140
  export declare function fleetHome(home?: string, env?: NodeJS.ProcessEnv): string;
138
141
  export declare function verifyFleetLocalRelease(files: FleetLocalReleaseFiles, release?: FleetLocalReleasePin, publicKey?: string): void;
139
142
  export declare function resolveFleetReleaseToken(env?: NodeJS.ProcessEnv): string;
@@ -144,12 +147,13 @@ export declare const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS: number;
144
147
  export declare function installPinnedFleetLocalDistribution(deps?: FleetLocalInstallDeps): Promise<FleetLocalInstallation>;
145
148
  /** The process id the bot's gateway recorded, when its file reads; the caller checks whether it is alive. */
146
149
  export declare function fleetLocalGatewayPid(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome">): number | null;
150
+ export declare function fleetLocalProfileNeedsAdoption(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome" | "platform">): boolean;
147
151
  export declare function controlFleetLocalBot(agentUid: string, action: FleetLocalServiceAction, deps?: FleetLocalControlDeps): Record<string, unknown>;
148
152
  export declare function exportFleetLocalMachineCredential(agentUid: string, deps?: FleetLocalControlDeps): BotMachineCreds;
149
153
  export declare function fleetLocalPromotionRuntime(agentUid: string, deps?: FleetLocalControlDeps): LocalBotPromotionRuntime;
150
154
  export declare function removeFleetLocalBot(agentUid: string, deps?: FleetLocalControlDeps): FleetLocalRemovalResult;
151
155
  export declare function authenticateFleetLocalBot(input: FleetLocalAuthInput, deps?: FleetLocalControlDeps): FleetLocalAuthResult;
152
- export declare function writeFleetLocalProfile(input: FleetLocalProfileInput, deps?: Pick<FleetLocalBootstrapDeps, "fleetHome" | "platform">): string;
156
+ export declare function writeFleetLocalProfile(input: FleetLocalProfileInput, deps?: FleetLocalProfileDeps): string;
153
157
  /** The runtime's inbox plugin sends the intro and runs the kickoff once, then stamps this file. */
154
158
  export declare function writeFleetLocalFirstStart(profileDir: string, firstStart: {
155
159
  intro?: string;
@@ -167,7 +171,7 @@ export declare function refreshFleetLocalInstructions(agentUid: string, systemPr
167
171
  /** Moves a fresh machine credential into the bot's secure store and deletes the file. */
168
172
  export declare function importFleetLocalMachineCredential(agentUid: string, machineCredentials: string, deps?: FleetLocalControlDeps): void;
169
173
  export declare function bootstrapFleetLocalBot(input: FleetLocalProfileInput & {
170
- machineCredentials: string;
174
+ machineCredentials?: string;
171
175
  }, deps?: FleetLocalBootstrapDeps): Promise<FleetLocalBootstrapResult>;
172
176
  export {};
173
177
  //# sourceMappingURL=fleet-local.d.ts.map
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
7
+ import { withBotLock } from "./lock.js";
7
8
  import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
8
9
  import { ensureCognitoToken } from "../../utils/cognito-session.js";
9
10
  import { vaultApiFetch } from "../../utils/vault-api.js";
@@ -35,18 +36,18 @@ export function fleetProviderNeedsSignIn(provider) {
35
36
  }
36
37
  export const HQ_FLEET_LOCAL_RELEASE = {
37
38
  repository: "indigoai-us/hq-fleet-local",
38
- tag: "v0.4.13",
39
- version: "0.4.13",
40
- artifact: "hq-fleet-local-0.4.13.zip",
41
- artifactSha256: "81b129ac129578240af9091b06ae190f47f6be38ff933b35ae5459d5b78c72df",
39
+ tag: "v0.4.14",
40
+ version: "0.4.14",
41
+ artifact: "hq-fleet-local-0.4.14.zip",
42
+ artifactSha256: "9ff6ad4f7235988468e1913008c5650f5d064210078d6d20c407b17120310f47",
42
43
  runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
43
44
  runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
44
- manifestSha256: "42654f91264099ce173ce3a9f42e207f566e1412ea4b550dc21775d764327bbb",
45
- signatureSha256: "93cf9b0a94d75c532454243e3391e421e2743f59f8e65989a5a56ec9152e2fa4",
45
+ manifestSha256: "88e71a733a5f57089764cbf9947fb88025c2263c5f3d62a2c6f5ab3635538871",
46
+ signatureSha256: "b892b54d6fcb2aaca86d049d7662fe5549a250d81877d6c529288e79ae1565d9",
46
47
  runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
47
48
  };
48
49
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
49
- MCowBQYDK2VwAyEAbYvc1pwOmiCUPJv8yIa3E9RXfniFA84nH2+ROmLMGZU=
50
+ MCowBQYDK2VwAyEARXr2I8RNcw2c0Ep77XkYGwylresXBFn8dm9pxN2Zq/U=
50
51
  -----END PUBLIC KEY-----
51
52
  `;
52
53
  function digest(value) {
@@ -162,7 +163,7 @@ function trustedReleaseLink(url) {
162
163
  return false;
163
164
  }
164
165
  }
165
- async function downloadFleetLocalReleaseFromHq(release, fetcher, links) {
166
+ async function downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime) {
166
167
  const get = async (name, timeoutMs) => {
167
168
  const url = links.get(name);
168
169
  if (!url)
@@ -178,7 +179,7 @@ async function downloadFleetLocalReleaseFromHq(release, fetcher, links) {
178
179
  };
179
180
  const [archive, runtimeArchive, manifest, signature] = await Promise.all([
180
181
  get(release.artifact, FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS),
181
- get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
182
+ cachedRuntime ?? get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
182
183
  get("release-manifest.json", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
183
184
  get("release-manifest.sig", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
184
185
  ]);
@@ -193,7 +194,7 @@ async function responseBytes(response, label) {
193
194
  export const FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS = 30_000;
194
195
  export const FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
195
196
  export const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS = 20 * 60_000;
196
- async function downloadFleetLocalRelease(release, fetcher, token) {
197
+ async function downloadFleetLocalRelease(release, fetcher, token, cachedRuntime) {
197
198
  const headers = {
198
199
  Accept: "application/vnd.github+json",
199
200
  Authorization: `Bearer ${token}`,
@@ -223,7 +224,7 @@ async function downloadFleetLocalRelease(release, fetcher, token) {
223
224
  };
224
225
  const [archive, runtimeArchive, manifest, signature] = await Promise.all([
225
226
  get(release.artifact, FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS),
226
- get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
227
+ cachedRuntime ?? get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
227
228
  get("release-manifest.json", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
228
229
  get("release-manifest.sig", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
229
230
  ]);
@@ -274,6 +275,24 @@ function installFiles(target, files, release, publicKey) {
274
275
  fs.rmSync(staging, { recursive: true, force: true });
275
276
  }
276
277
  }
278
+ function cachedRuntimeArchive(root, release) {
279
+ const distributions = path.join(root, "distributions", "hq-fleet-local");
280
+ if (!fs.existsSync(distributions))
281
+ return null;
282
+ for (const entry of fs.readdirSync(distributions, { withFileTypes: true })) {
283
+ if (!entry.isDirectory() || entry.name.startsWith("."))
284
+ continue;
285
+ const directory = path.join(distributions, entry.name);
286
+ for (const file of fs.readdirSync(directory, { withFileTypes: true })) {
287
+ if (!file.isFile() || !file.name.startsWith("hq-fleet-runtime-") || !file.name.endsWith(".tar"))
288
+ continue;
289
+ const bytes = fs.readFileSync(path.join(directory, file.name));
290
+ if (digest(bytes) === release.runtimeArtifactSha256)
291
+ return bytes;
292
+ }
293
+ }
294
+ return null;
295
+ }
277
296
  export async function installPinnedFleetLocalDistribution(deps = {}) {
278
297
  const release = deps.release ?? HQ_FLEET_LOCAL_RELEASE;
279
298
  const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEY;
@@ -286,9 +305,10 @@ export async function installPinnedFleetLocalDistribution(deps = {}) {
286
305
  else {
287
306
  const fetcher = deps.fetch ?? fetch;
288
307
  const links = await (deps.hqReleaseLinks ?? fetchHqFleetReleaseLinks)(release);
308
+ const cachedRuntime = cachedRuntimeArchive(root, release);
289
309
  const files = links
290
- ? await downloadFleetLocalReleaseFromHq(release, fetcher, links)
291
- : await downloadFleetLocalRelease(release, fetcher, (deps.token ?? resolveFleetReleaseToken)());
310
+ ? await downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime)
311
+ : await downloadFleetLocalRelease(release, fetcher, (deps.token ?? resolveFleetReleaseToken)(), cachedRuntime);
292
312
  verifyFleetLocalRelease(files, release, publicKey);
293
313
  installFiles(dir, files, release, publicKey);
294
314
  }
@@ -302,14 +322,83 @@ export async function installPinnedFleetLocalDistribution(deps = {}) {
302
322
  engine: engineMetadata(release),
303
323
  };
304
324
  }
325
+ function profileName(agentUid) {
326
+ return agentUid.toLowerCase();
327
+ }
305
328
  function profileRoot(agentUid, root) {
306
- return path.join(root, "profiles", agentUid);
329
+ const profiles = path.join(root, "profiles");
330
+ const canonicalName = profileName(agentUid);
331
+ const canonical = path.join(profiles, canonicalName);
332
+ if (fs.existsSync(canonical))
333
+ return canonical;
334
+ try {
335
+ const legacy = fs.readdirSync(profiles, { withFileTypes: true })
336
+ .find((entry) => entry.isDirectory() && entry.name.toLowerCase() === canonicalName);
337
+ return legacy ? path.join(profiles, legacy.name) : canonical;
338
+ }
339
+ catch {
340
+ return canonical;
341
+ }
342
+ }
343
+ function adoptLegacyProfileDirectory(agentUid, root) {
344
+ const profiles = path.join(root, "profiles");
345
+ const canonicalName = profileName(agentUid);
346
+ fs.mkdirSync(profiles, { recursive: true, mode: 0o700 });
347
+ const matches = fs.readdirSync(profiles, { withFileTypes: true })
348
+ .filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === canonicalName)
349
+ .map((entry) => entry.name);
350
+ if (matches.length > 1) {
351
+ throw expectedError(`HQ Fleet has conflicting profile directories for ${agentUid}.`);
352
+ }
353
+ const existing = matches[0];
354
+ const canonical = path.join(profiles, canonicalName);
355
+ if (!existing || existing === canonicalName)
356
+ return canonical;
357
+ const source = path.join(profiles, existing);
358
+ const temporary = path.join(profiles, `.adopt-${canonicalName}-${process.pid}-${crypto.randomUUID()}`);
359
+ fs.renameSync(source, temporary);
360
+ try {
361
+ fs.renameSync(temporary, canonical);
362
+ }
363
+ catch (error) {
364
+ fs.renameSync(temporary, source);
365
+ throw error;
366
+ }
367
+ return canonical;
307
368
  }
308
369
  function venvExecutable(runtimeDir, name, platform) {
309
370
  return platform === "win32"
310
371
  ? path.join(runtimeDir, "Scripts", `${name}.exe`)
311
372
  : path.join(runtimeDir, "bin", name);
312
373
  }
374
+ function sharedRuntimeFromMarker(profileDir, root) {
375
+ try {
376
+ const marker = JSON.parse(fs.readFileSync(path.join(profileDir, "bootstrap.json"), "utf8"));
377
+ if (marker.schemaVersion !== 2 || typeof marker.runtimeId !== "string" || !/^[a-f0-9]{64}$/.test(marker.runtimeId))
378
+ return null;
379
+ const sharedRoot = path.join(root, "runtimes", marker.runtimeId);
380
+ return {
381
+ id: marker.runtimeId,
382
+ root: sharedRoot,
383
+ runtimeDir: path.join(sharedRoot, "runtime"),
384
+ sourcesDir: path.join(sharedRoot, "sources"),
385
+ };
386
+ }
387
+ catch {
388
+ return null;
389
+ }
390
+ }
391
+ function hasEmbeddedRuntime(profileDir, platform) {
392
+ const runtimeDir = path.join(profileDir, "runtime");
393
+ try {
394
+ if (fs.lstatSync(runtimeDir).isSymbolicLink())
395
+ return true;
396
+ }
397
+ catch {
398
+ return false;
399
+ }
400
+ return ["hq-fleet", "hq-fleet-local"].some((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)));
401
+ }
313
402
  function fleetLocalExecutables(agentUid, deps) {
314
403
  if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid)) {
315
404
  throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
@@ -317,14 +406,16 @@ function fleetLocalExecutables(agentUid, deps) {
317
406
  const root = deps.fleetHome ?? fleetHome();
318
407
  const platform = deps.platform ?? process.platform;
319
408
  const profileDir = profileRoot(agentUid, root);
320
- const runtimeDir = path.join(profileDir, "runtime");
409
+ const shared = sharedRuntimeFromMarker(profileDir, root);
410
+ const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
411
+ const sourcesDir = shared?.sourcesDir ?? path.join(profileDir, "sources");
321
412
  const profile = path.join(profileDir, "profile.json");
322
413
  const runtime = venvExecutable(runtimeDir, "hq-fleet", platform);
323
414
  const helper = venvExecutable(runtimeDir, "hq-fleet-local", platform);
324
415
  if (![profile, runtime, helper].every((item) => fs.existsSync(item))) {
325
416
  throw expectedError(`HQ Fleet Local is not fully installed for ${agentUid}.`);
326
417
  }
327
- return { profileDir, profile, runtime, helper };
418
+ return { profileDir, profile, runtime, helper, runtimeDir, sourcesDir };
328
419
  }
329
420
  function runJson(run, executable, args, options, label) {
330
421
  let output;
@@ -357,10 +448,39 @@ export function fleetLocalGatewayPid(agentUid, deps = {}) {
357
448
  return null;
358
449
  }
359
450
  }
451
+ export function fleetLocalProfileNeedsAdoption(agentUid, deps = {}) {
452
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
453
+ return true;
454
+ const root = deps.fleetHome ?? fleetHome();
455
+ const profiles = path.join(root, "profiles");
456
+ const canonicalName = profileName(agentUid);
457
+ let entries;
458
+ try {
459
+ entries = fs.readdirSync(profiles);
460
+ }
461
+ catch {
462
+ return true;
463
+ }
464
+ if (!entries.includes(canonicalName))
465
+ return true;
466
+ const profileDir = path.join(profiles, canonicalName);
467
+ const shared = sharedRuntimeFromMarker(profileDir, root);
468
+ if (!shared)
469
+ return true;
470
+ const platform = deps.platform ?? process.platform;
471
+ if (!["hq-fleet", "hq-fleet-local", "python"].every((name) => fs.existsSync(venvExecutable(shared.runtimeDir, name, platform))))
472
+ return true;
473
+ if (hasEmbeddedRuntime(profileDir, platform) || fs.existsSync(path.join(profileDir, "sources")))
474
+ return true;
475
+ return false;
476
+ }
360
477
  export function controlFleetLocalBot(agentUid, action, deps = {}) {
361
478
  const run = deps.run ?? execFileSync;
362
479
  const files = fleetLocalExecutables(agentUid, deps);
363
- return runJson(run, files.helper, ["service", action, "--profile", files.profileDir, "--runtime", files.runtime], { stdio: ["ignore", "pipe", "pipe"] }, `service ${action}`);
480
+ const result = runJson(run, files.helper, ["service", action, "--profile", files.profileDir, "--runtime", files.runtime], { stdio: ["ignore", "pipe", "pipe"] }, `service ${action}`);
481
+ if (action === "install" && result.ok === true)
482
+ removeEmbeddedRuntime(files.profileDir, deps.platform ?? process.platform);
483
+ return result;
364
484
  }
365
485
  const providerProfiles = {
366
486
  chatgpt: {
@@ -491,14 +611,15 @@ export function removeFleetLocalBot(agentUid, deps = {}) {
491
611
  const profileDir = profileRoot(agentUid, deps.fleetHome ?? fleetHome());
492
612
  if (!fs.existsSync(profileDir))
493
613
  return { removed: false, credentialsRemoved: 0 };
614
+ if (!fs.existsSync(path.join(profileDir, "profile.json"))) {
615
+ fs.rmSync(profileDir, { recursive: true, force: true });
616
+ return { removed: true, credentialsRemoved: 0 };
617
+ }
494
618
  const platform = deps.platform ?? process.platform;
495
- const runtimeDir = path.join(profileDir, "runtime");
496
- const partial = ![
497
- path.join(profileDir, "profile.json"),
498
- venvExecutable(runtimeDir, "hq-fleet", platform),
499
- venvExecutable(runtimeDir, "hq-fleet-local", platform),
500
- ].every((item) => fs.existsSync(item));
501
- if (partial) {
619
+ const legacyRuntime = path.join(profileDir, "runtime");
620
+ const shared = sharedRuntimeFromMarker(profileDir, deps.fleetHome ?? fleetHome());
621
+ const installedRuntime = shared?.runtimeDir ?? legacyRuntime;
622
+ if (!["hq-fleet", "hq-fleet-local"].every((name) => fs.existsSync(venvExecutable(installedRuntime, name, platform)))) {
502
623
  fs.rmSync(profileDir, { recursive: true, force: true });
503
624
  return { removed: true, credentialsRemoved: 0 };
504
625
  }
@@ -614,7 +735,9 @@ export function writeFleetLocalProfile(input, deps = {}) {
614
735
  }
615
736
  const root = deps.fleetHome ?? fleetHome();
616
737
  const dir = profileRoot(input.agentUid, root);
617
- const runtimeDir = path.join(dir, "runtime");
738
+ const markerRuntime = sharedRuntimeFromMarker(dir, root);
739
+ const runtimeRoot = deps.runtimeRoot ?? markerRuntime?.root ?? dir;
740
+ const runtimeDir = path.join(runtimeRoot, "runtime");
618
741
  const destination = path.join(dir, "profile.json");
619
742
  const temporary = path.join(dir, `.profile-${process.pid}-${crypto.randomUUID()}`);
620
743
  const profile = {
@@ -627,7 +750,7 @@ export function writeFleetLocalProfile(input, deps = {}) {
627
750
  profileDir: path.resolve(dir),
628
751
  runtime: { commit: input.installation.release.runtimeCommit, path: path.resolve(runtimeDir) },
629
752
  paths: {
630
- pluginSource: path.resolve(path.join(dir, "sources", "distribution", "src", "hq_fleet_local", "plugins", "hqdm")),
753
+ pluginSource: path.resolve(path.join(runtimeRoot, "sources", "distribution", "src", "hq_fleet_local", "plugins", "hqdm")),
631
754
  statusFile: path.resolve(path.join(input.botDir, "status.json")),
632
755
  logsDir: path.resolve(path.join(input.botDir, "logs")),
633
756
  },
@@ -686,9 +809,13 @@ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
686
809
  throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
687
810
  }
688
811
  const run = deps.run ?? execFileSync;
689
- const profileDir = profileRoot(agentUid, deps.fleetHome ?? fleetHome());
690
- const python = venvExecutable(path.join(profileDir, "runtime"), "python", deps.platform ?? process.platform);
691
- const source = path.join(profileDir, "sources", "runtime");
812
+ const root = deps.fleetHome ?? fleetHome();
813
+ const profileDir = profileRoot(agentUid, root);
814
+ const shared = sharedRuntimeFromMarker(profileDir, root);
815
+ const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
816
+ const sourcesDir = shared?.sourcesDir ?? path.join(profileDir, "sources");
817
+ const python = venvExecutable(runtimeDir, "python", deps.platform ?? process.platform);
818
+ const source = path.join(sourcesDir, "runtime");
692
819
  if (!fs.existsSync(python) || !fs.existsSync(source)) {
693
820
  throw expectedError("HQ Fleet Local is not installed for this bot, so its instructions could not be checked.");
694
821
  }
@@ -744,21 +871,36 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
744
871
  ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
745
872
  fs.rmSync(machineCredentials, { force: true });
746
873
  }
747
- export async function bootstrapFleetLocalBot(input, deps = {}) {
748
- const run = deps.run ?? execFileSync;
749
- const platform = deps.platform ?? process.platform;
750
- const root = deps.fleetHome ?? fleetHome();
751
- const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
752
- const profileDir = profileRoot(input.agentUid, root);
753
- const sources = path.join(profileDir, "sources");
754
- const distributionSource = path.join(sources, "distribution");
755
- const runtimeSource = path.join(sources, "runtime");
756
- const runtimeDir = path.join(profileDir, "runtime");
757
- const marker = path.join(profileDir, "bootstrap.json");
758
- fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
759
- if (!fs.existsSync(marker)) {
760
- fs.rmSync(sources, { recursive: true, force: true });
761
- fs.rmSync(runtimeDir, { recursive: true, force: true });
874
+ async function installSharedFleetRuntime(installation, root, python, platform, run) {
875
+ const identity = digest(Buffer.from(JSON.stringify({
876
+ version: installation.release.version,
877
+ distribution: installation.release.artifactSha256,
878
+ runtime: installation.release.runtimeArtifactSha256,
879
+ python: python.python,
880
+ platform,
881
+ extras: ["mcp", "anthropic"],
882
+ })));
883
+ const shared = path.join(root, "runtimes", identity);
884
+ const descriptor = () => ({
885
+ id: identity,
886
+ root: shared,
887
+ runtimeDir: path.join(shared, "runtime"),
888
+ sourcesDir: path.join(shared, "sources"),
889
+ });
890
+ return withBotLock(identity, async () => {
891
+ const marker = path.join(shared, "ready.json");
892
+ const sources = path.join(shared, "sources");
893
+ const distributionSource = path.join(sources, "distribution");
894
+ const runtimeSource = path.join(sources, "runtime");
895
+ const runtimeDir = path.join(shared, "runtime");
896
+ const executables = ["python", "hq-fleet", "hq-fleet-local"];
897
+ if (fs.existsSync(marker)) {
898
+ if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
899
+ throw expectedError("The shared HQ Fleet Runtime is incomplete. Reinstall it before creating another bot.");
900
+ }
901
+ return descriptor();
902
+ }
903
+ fs.rmSync(shared, { recursive: true, force: true });
762
904
  fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
763
905
  fs.mkdirSync(runtimeSource, { recursive: true, mode: 0o700 });
764
906
  const uvEnv = fleetUvEnv(python.pythonDir);
@@ -766,14 +908,14 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
766
908
  "-m",
767
909
  "zipfile",
768
910
  "-e",
769
- input.installation.archive,
911
+ installation.archive,
770
912
  distributionSource,
771
913
  ], { stdio: "ignore" }, "distribution extraction");
772
914
  checkedRun(run, python.python, [
773
915
  "-m",
774
916
  "tarfile",
775
917
  "-e",
776
- input.installation.runtimeArchive,
918
+ installation.runtimeArchive,
777
919
  runtimeSource,
778
920
  ], { stdio: "ignore" }, "runtime extraction");
779
921
  checkedRun(run, python.uv, [
@@ -794,35 +936,63 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
794
936
  `${runtimeSource}[mcp,anthropic]`,
795
937
  distributionSource,
796
938
  ], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation");
797
- fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: input.installation.engine }, null, 2)}\n`, { mode: 0o600 });
798
- }
799
- const { systemPrompt, ...profileInput } = input;
939
+ if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
940
+ throw expectedError("HQ Fleet Local installed without its runtime executables.");
941
+ }
942
+ fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: installation.engine })}\n`, { mode: 0o600 });
943
+ return descriptor();
944
+ }, { root: path.join(root, "runtimes"), command: "HQ Fleet Runtime installation", waitMs: 20 * 60_000, staleMs: 2 * 60 * 60_000 });
945
+ }
946
+ function writeNativeProfileMarker(profileDir, shared, engine) {
947
+ const marker = path.join(profileDir, "bootstrap.json");
948
+ const temporary = `${marker}.${process.pid}.${crypto.randomUUID()}`;
949
+ fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 2, runtimeId: shared.id, engine }, null, 2)}\n`, { mode: 0o600 });
950
+ fs.renameSync(temporary, marker);
951
+ }
952
+ function removeEmbeddedRuntime(profileDir, platform = process.platform) {
953
+ fs.rmSync(path.join(profileDir, "sources"), { recursive: true, force: true });
954
+ if (hasEmbeddedRuntime(profileDir, platform))
955
+ fs.rmSync(path.join(profileDir, "runtime"), { recursive: true, force: true });
956
+ }
957
+ export async function bootstrapFleetLocalBot(input, deps = {}) {
958
+ const run = deps.run ?? execFileSync;
959
+ const platform = deps.platform ?? process.platform;
960
+ const root = deps.fleetHome ?? fleetHome();
961
+ const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
962
+ const profileDir = adoptLegacyProfileDirectory(input.agentUid, root);
963
+ fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
964
+ const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run);
965
+ writeNativeProfileMarker(profileDir, shared, input.installation.engine);
966
+ const { systemPrompt, machineCredentials, ...profileInput } = input;
800
967
  if (systemPrompt !== undefined)
801
968
  refreshFleetLocalInstructions(input.agentUid, systemPrompt, { fleetHome: root, platform, run });
802
- const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform });
803
- const helper = venvExecutable(runtimeDir, "hq-fleet-local", platform);
804
- const runtime = venvExecutable(runtimeDir, "hq-fleet", platform);
969
+ const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform, runtimeRoot: shared.root });
970
+ const helper = venvExecutable(shared.runtimeDir, "hq-fleet-local", platform);
971
+ const runtime = venvExecutable(shared.runtimeDir, "hq-fleet", platform);
805
972
  if (!fs.existsSync(helper) || !fs.existsSync(runtime)) {
806
973
  throw expectedError("HQ Fleet Local installed without its runtime executables.");
807
974
  }
808
975
  checkedRun(run, helper, ["render-profile", "--spec", profile, "--mode", "create"], { stdio: "ignore" }, "profile rendering");
809
- const machineValue = fs.readFileSync(input.machineCredentials, "utf8").trim();
810
- if (!machineValue) {
811
- throw expectedError(`HQ Fleet Local machine credential file is empty: ${input.machineCredentials}`);
976
+ if (machineCredentials) {
977
+ const machineValue = fs.readFileSync(machineCredentials, "utf8").trim();
978
+ if (!machineValue) {
979
+ throw expectedError(`HQ Fleet Local machine credential file is empty: ${machineCredentials}`);
980
+ }
981
+ checkedRun(run, helper, [
982
+ "machine-credential",
983
+ "import",
984
+ "--profile",
985
+ profileDir,
986
+ "--bot-id",
987
+ input.agentUid,
988
+ ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
989
+ fs.rmSync(machineCredentials, { force: true });
812
990
  }
813
- checkedRun(run, helper, [
814
- "machine-credential",
815
- "import",
816
- "--profile",
817
- profileDir,
818
- "--bot-id",
819
- input.agentUid,
820
- ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
821
- fs.rmSync(input.machineCredentials, { force: true });
822
991
  checkedRun(run, helper, ["doctor-profile", "--profile", profileDir, "--runtime", runtime], { stdio: "ignore" }, "profile validation");
823
992
  const start = deps.start !== false;
824
993
  if (start) {
825
994
  checkedRun(run, helper, ["service", "install", "--profile", profileDir, "--runtime", runtime], { stdio: ["ignore", 2, 2] }, "service installation");
995
+ removeEmbeddedRuntime(profileDir, platform);
826
996
  }
827
997
  return { profile, runtime, helper, running: start };
828
998
  }
@@ -46,6 +46,7 @@
46
46
  *
47
47
  * Everything with a side effect is injectable so the loop is unit-testable.
48
48
  */
49
+ import { botAuthEnv, describeBotAuth, formatBotAuth } from "./runtime-auth.js";
49
50
  import { ownerContextBlock } from "./owner-context.js";
50
51
  import { randomUUID } from "node:crypto";
51
52
  import { hasPromotionHold } from "./promotion-hold.js";
@@ -558,6 +559,12 @@ export async function runBot(deps) {
558
559
  setStatus({ ...fallback(), pid, state: "starting", canDrainForPromotion: false, runtimeSignIn: null, cliVersion });
559
560
  log("info", `starting ${config.name} on hq-cli ${cliVersion} (${config.runtime}, ${kind} bot${companies.length ? ` of ${companies.join(", ")}` : ""}) as ${config.agentUid} for ${config.ownerUid}`);
560
561
  log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid, companySlug), turnCwd)}`);
562
+ {
563
+ const login = describeBotAuth(dir, config);
564
+ log("info", `${runtime.id} login: ${formatBotAuth(login, config.name)}`);
565
+ if (login.login === "own" && !login.present)
566
+ setStatus({ lastError: `own ${runtime.id} login missing: run hq bot login ${config.name} --runtime ${runtime.id}` });
567
+ }
561
568
  deps.cliUpdate?.startup(log);
562
569
  try {
563
570
  const record = await api.getAgent(config.agentUid);
@@ -1048,7 +1055,15 @@ export async function runBot(deps) {
1048
1055
  // resets). Claude gets the full hook set via its payload session id;
1049
1056
  // Codex/Grok only see HQ_SESSION_ID.
1050
1057
  const sid = session?.sessionId ?? randomUUID();
1051
- const env = withOsUser(turnEnv(process.env, kind, config.agentUid, companySlug));
1058
+ let env;
1059
+ try {
1060
+ // The bot's own login or account, when set. A missing one fails the
1061
+ // turn loudly (status + reply); it never falls back to the owner's login.
1062
+ env = withOsUser(botAuthEnv(dir, { ...live, runtime: config.runtime }, turnEnv(process.env, kind, config.agentUid, companySlug)));
1063
+ }
1064
+ catch (err) {
1065
+ throw new CompanyBindingError(`I can't answer yet: ${err instanceof Error ? err.message : String(err)} I did not use anyone else's login; this work is still pending.`);
1066
+ }
1052
1067
  let bind = { env };
1053
1068
  if (companySlug) {
1054
1069
  try {
@@ -1941,7 +1956,29 @@ export async function runBot(deps) {
1941
1956
  // The job's stored company is checked against the bot's current config
1942
1957
  // (read fresh from disk) every time it fires. A company the bot no longer
1943
1958
  // serves is never synced: the run is skipped and the job removed.
1944
- const currentCompany = currentHotSignalSyncCompany(readBotConfig(dir) ?? config);
1959
+ const freshConfig = readBotConfig(dir);
1960
+ if (!freshConfig) {
1961
+ // bot.json is missing or unreadable, so which company the bot serves is
1962
+ // unknown. Skip this run without writing anything; the job stays for the
1963
+ // next fire.
1964
+ const text = `Skipped the hot-channel sync for ${syncCompany}: the bot's config (bot.json) could not be read, so nothing was written. The job will try again at its next run.`;
1965
+ markProcessed(dir, id, now().toISOString());
1966
+ try {
1967
+ await sendToPerson(text);
1968
+ }
1969
+ catch (err) {
1970
+ log("warn", `report for run ${id} of job ${jobId} was not delivered: ${err instanceof Error ? err.message : String(err)}`);
1971
+ }
1972
+ await completeJobRun(jobId, id, { status: "skipped-precondition", output: jobRunOutput(text) });
1973
+ markReported("skipped-precondition");
1974
+ await api.ackInbox(config.agentUid, id);
1975
+ clearInflight(dir, id);
1976
+ attempts.delete(id);
1977
+ setStatus({ lastActivityAt: now().toISOString() });
1978
+ log("warn", `hot-signal-sync job ${jobId} run ${id} skipped: bot.json missing or unreadable`);
1979
+ return;
1980
+ }
1981
+ const currentCompany = currentHotSignalSyncCompany(freshConfig);
1945
1982
  if (currentCompany !== syncCompany) {
1946
1983
  let removedNote = "";
1947
1984
  try {