@indigoai-us/hq-cli 5.345.171 → 5.345.172

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 (46) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/command-catalog.generated.d.ts +74 -2
  3. package/dist/command-catalog.generated.js +88 -2
  4. package/dist/command-registration-plan.d.ts +7 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/bot.js +46 -8
  7. package/dist/commands/cloud.js +9 -5
  8. package/dist/commands/daemon.js +16 -1
  9. package/dist/commands/files.d.ts +1 -0
  10. package/dist/commands/files.js +22 -4
  11. package/dist/commands/sync-core.d.ts +22 -0
  12. package/dist/commands/sync-core.js +124 -0
  13. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +278 -224
  14. package/dist/hq-cloud-sync-runner/package.json +1 -1
  15. package/dist/lib/bot/fleet-local.d.ts +16 -1
  16. package/dist/lib/bot/fleet-local.js +99 -48
  17. package/dist/lib/bot/fleet-python.d.ts +47 -0
  18. package/dist/lib/bot/fleet-python.js +218 -0
  19. package/dist/lib/bot/index.d.ts +1 -0
  20. package/dist/lib/bot/index.js +1 -0
  21. package/dist/lib/bot/prompt.d.ts +6 -1
  22. package/dist/lib/bot/prompt.js +8 -3
  23. package/dist/lib/daemon/catalog.d.ts +12 -4
  24. package/dist/lib/daemon/catalog.js +70 -27
  25. package/dist/lib/daemon/config.d.ts +0 -7
  26. package/dist/lib/daemon/config.js +0 -32
  27. package/dist/lib/daemon/run.d.ts +5 -1
  28. package/dist/lib/daemon/run.js +48 -11
  29. package/dist/lib/daemon/supervisor.js +3 -1
  30. package/dist/lib/sync-core/engine-home.d.ts +44 -0
  31. package/dist/lib/sync-core/engine-home.js +193 -0
  32. package/dist/lib/sync-core/flag.d.ts +17 -0
  33. package/dist/lib/sync-core/flag.js +47 -0
  34. package/dist/lib/sync-core/reconcile.d.ts +31 -0
  35. package/dist/lib/sync-core/reconcile.js +196 -0
  36. package/dist/lib/sync-core/release.d.ts +32 -0
  37. package/dist/lib/sync-core/release.js +140 -0
  38. package/dist/lib/sync-core/rollout-state.d.ts +24 -0
  39. package/dist/lib/sync-core/rollout-state.js +94 -0
  40. package/dist/lib/vault-sync-switches.d.ts +5 -0
  41. package/dist/lib/vault-sync-switches.js +43 -0
  42. package/dist/utils/remote-list-not-converging.d.ts +21 -0
  43. package/dist/utils/remote-list-not-converging.js +40 -0
  44. package/package.json +8 -8
  45. package/dist/lib/daemon/sync-core-canary.d.ts +0 -12
  46. package/dist/lib/daemon/sync-core-canary.js +0 -61
@@ -1 +1 @@
1
- {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.102","bundleSha256":"54baedb34f8744f3dbbfa970785c6de703a33145081969bed5b266a2fc4d4d9e","formatVersion":1}
1
+ {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.103","bundleSha256":"3ed362bac2c75d32a61b0e26b4253813af96458579920a798fd38a9583e855f9","formatVersion":1}
@@ -1,5 +1,6 @@
1
1
  import { type BotEngineMetadata } from "./config.js";
2
2
  import type { BotMachineCreds } from "./creds.js";
3
+ import { type FleetPython } from "./fleet-python.js";
3
4
  import type { LocalBotPromotionRuntime } from "./promote.js";
4
5
  export interface FleetLocalReleasePin {
5
6
  repository: string;
@@ -65,6 +66,8 @@ export interface FleetLocalProfileInput {
65
66
  hqRoot: string;
66
67
  botDir: string;
67
68
  installation: FleetLocalInstallation;
69
+ /** The bot's standing HQ instructions; written as the runtime's SOUL.md persona. */
70
+ systemPrompt?: string;
68
71
  }
69
72
  type FleetLocalExec = (executable: string, args: string[], options: Record<string, unknown>) => Buffer | string;
70
73
  export interface FleetLocalBootstrapDeps {
@@ -72,6 +75,7 @@ export interface FleetLocalBootstrapDeps {
72
75
  platform?: NodeJS.Platform;
73
76
  run?: FleetLocalExec;
74
77
  start?: boolean;
78
+ python?: FleetPython;
75
79
  }
76
80
  export interface FleetLocalBootstrapResult {
77
81
  profile: string;
@@ -114,14 +118,25 @@ export declare const FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS = 30000;
114
118
  export declare const FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS: number;
115
119
  export declare const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS: number;
116
120
  export declare function installPinnedFleetLocalDistribution(deps?: FleetLocalInstallDeps): Promise<FleetLocalInstallation>;
121
+ /** The process id the bot's gateway recorded, when its file reads; the caller checks whether it is alive. */
122
+ export declare function fleetLocalGatewayPid(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome">): number | null;
117
123
  export declare function controlFleetLocalBot(agentUid: string, action: FleetLocalServiceAction, deps?: FleetLocalControlDeps): Record<string, unknown>;
118
124
  export declare function exportFleetLocalMachineCredential(agentUid: string, deps?: FleetLocalControlDeps): BotMachineCreds;
119
125
  export declare function fleetLocalPromotionRuntime(agentUid: string, deps?: FleetLocalControlDeps): LocalBotPromotionRuntime;
120
126
  export declare function removeFleetLocalBot(agentUid: string, deps?: FleetLocalControlDeps): FleetLocalRemovalResult;
121
127
  export declare function authenticateFleetLocalBot(input: FleetLocalAuthInput, deps?: FleetLocalControlDeps): FleetLocalAuthResult;
122
128
  export declare function writeFleetLocalProfile(input: FleetLocalProfileInput, deps?: Pick<FleetLocalBootstrapDeps, "fleetHome" | "platform">): string;
129
+ export declare function writeFleetLocalSoul(agentUid: string, systemPrompt: string, deps?: Pick<FleetLocalControlDeps, "fleetHome">): string;
130
+ /**
131
+ * The pinned runtime refuses a persona file that trips its context-file
132
+ * scanner and then starts with no identity at all. The same scanner, run with
133
+ * the bot's own interpreter, says beforehand what it would refuse.
134
+ */
135
+ export declare function scanFleetLocalInstructions(agentUid: string, text: string, deps?: FleetLocalControlDeps): string[];
136
+ /** Checks the instructions with the runtime's scanner, then makes them the bot's persona. */
137
+ export declare function refreshFleetLocalInstructions(agentUid: string, systemPrompt: string, deps?: FleetLocalControlDeps): string;
123
138
  export declare function bootstrapFleetLocalBot(input: FleetLocalProfileInput & {
124
139
  machineCredentials: string;
125
- }, deps?: FleetLocalBootstrapDeps): FleetLocalBootstrapResult;
140
+ }, deps?: FleetLocalBootstrapDeps): Promise<FleetLocalBootstrapResult>;
126
141
  export {};
127
142
  //# sourceMappingURL=fleet-local.d.ts.map
@@ -4,16 +4,17 @@ 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 { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
7
8
  export const HQ_FLEET_LOCAL_RELEASE = {
8
9
  repository: "indigoai-us/hq-fleet-local",
9
- tag: "v0.4.2",
10
- version: "0.4.2",
11
- artifact: "hq-fleet-local-0.4.2.zip",
12
- artifactSha256: "61af88c3e783f5cc6bc1bf99920e981680f67d3485fae6edc52cdfee61ce2108",
10
+ tag: "v0.4.5",
11
+ version: "0.4.5",
12
+ artifact: "hq-fleet-local-0.4.5.zip",
13
+ artifactSha256: "5dc05b1a9c8a3fce0d748c1f642a908c4166958cf4e1c3cec8ee3799148eef80",
13
14
  runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
14
15
  runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
15
- manifestSha256: "d55a4b84d08659991f8bf2dba4b8590bd7874bf3adf4dad6bc1e08eac5462884",
16
- signatureSha256: "31f0d27890f54956f3684e9401d04ed49b9f57fcb6da0440c25f5a31c5db1082",
16
+ manifestSha256: "ffb81d7cbb61cdd05e91208ac3eccf045789d9e068309da3164fe002991f7780",
17
+ signatureSha256: "3792d43c191c65c731e9f27a003d86334b4695a43feb8ac23a7b679f79531616",
17
18
  runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
18
19
  };
19
20
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
@@ -252,6 +253,19 @@ function runJson(run, executable, args, options, label) {
252
253
  throw expectedError(`HQ Fleet Local ${label} returned an invalid result.`);
253
254
  }
254
255
  }
256
+ /** The process id the bot's gateway recorded, when its file reads; the caller checks whether it is alive. */
257
+ export function fleetLocalGatewayPid(agentUid, deps = {}) {
258
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
259
+ return null;
260
+ const file = path.join(profileRoot(agentUid, deps.fleetHome ?? fleetHome()), "gateway.pid");
261
+ try {
262
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
263
+ return typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0 ? parsed.pid : null;
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
255
269
  export function controlFleetLocalBot(agentUid, action, deps = {}) {
256
270
  const run = deps.run ?? execFileSync;
257
271
  const files = fleetLocalExecutables(agentUid, deps);
@@ -518,36 +532,69 @@ export function writeFleetLocalProfile(input, deps = {}) {
518
532
  fs.mkdirSync(profile.paths.logsDir, { recursive: true, mode: 0o700 });
519
533
  fs.writeFileSync(temporary, `${JSON.stringify(profile, null, 2)}\n`, { mode: 0o600 });
520
534
  fs.renameSync(temporary, destination);
535
+ if (input.systemPrompt !== undefined)
536
+ writeFleetLocalSoul(input.agentUid, input.systemPrompt, { fleetHome: root });
521
537
  return destination;
522
538
  }
523
- function pythonCommand(run, platform) {
524
- const candidates = platform === "win32"
525
- ? [
526
- { executable: "py", prefix: ["-3.13"] },
527
- { executable: "py", prefix: ["-3.12"] },
528
- { executable: "py", prefix: ["-3.11"] },
529
- { executable: "python3", prefix: [] },
530
- ]
531
- : [
532
- { executable: "python3.13", prefix: [] },
533
- { executable: "python3.12", prefix: [] },
534
- { executable: "python3.11", prefix: [] },
535
- { executable: "python3", prefix: [] },
536
- ];
537
- for (const candidate of candidates) {
538
- try {
539
- run(candidate.executable, [
540
- ...candidate.prefix,
541
- "-c",
542
- "import sys; raise SystemExit(0 if (3,11) <= sys.version_info[:2] < (3,14) else 1)",
543
- ], { stdio: "ignore", windowsHide: true });
544
- return candidate;
545
- }
546
- catch {
547
- continue;
548
- }
539
+ export function writeFleetLocalSoul(agentUid, systemPrompt, deps = {}) {
540
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid)) {
541
+ throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
542
+ }
543
+ const dir = profileRoot(agentUid, deps.fleetHome ?? fleetHome());
544
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
545
+ const soul = path.join(dir, "SOUL.md");
546
+ const temporary = path.join(dir, `.soul-${process.pid}-${crypto.randomUUID()}`);
547
+ fs.writeFileSync(temporary, `${systemPrompt.trimEnd()}\n`, { mode: 0o600 });
548
+ fs.renameSync(temporary, soul);
549
+ return soul;
550
+ }
551
+ const FLEET_INSTRUCTION_SCAN = [
552
+ "import json, sys",
553
+ "sys.path.insert(0, sys.argv[1])",
554
+ "from tools.threat_patterns import scan_for_threats",
555
+ "print(json.dumps(scan_for_threats(sys.stdin.read(), scope='context')))",
556
+ ].join("\n");
557
+ /**
558
+ * The pinned runtime refuses a persona file that trips its context-file
559
+ * scanner and then starts with no identity at all. The same scanner, run with
560
+ * the bot's own interpreter, says beforehand what it would refuse.
561
+ */
562
+ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
563
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid)) {
564
+ throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
549
565
  }
550
- throw expectedError("HQ Fleet Runtime needs Python 3.11, 3.12, or 3.13. Install one and retry.");
566
+ const run = deps.run ?? execFileSync;
567
+ const profileDir = profileRoot(agentUid, deps.fleetHome ?? fleetHome());
568
+ const python = venvExecutable(path.join(profileDir, "runtime"), "python", deps.platform ?? process.platform);
569
+ const source = path.join(profileDir, "sources", "runtime");
570
+ if (!fs.existsSync(python) || !fs.existsSync(source)) {
571
+ throw expectedError("HQ Fleet Local is not installed for this bot, so its instructions could not be checked.");
572
+ }
573
+ const failure = expectedError("HQ Fleet Local could not check the bot's instructions with its runtime.");
574
+ let parsed;
575
+ try {
576
+ const output = run(python, ["-I", "-c", FLEET_INSTRUCTION_SCAN, source], {
577
+ input: text,
578
+ stdio: ["pipe", "pipe", "pipe"],
579
+ encoding: "utf8",
580
+ windowsHide: true,
581
+ });
582
+ parsed = JSON.parse((Buffer.isBuffer(output) ? output.toString("utf8") : output).trim());
583
+ }
584
+ catch {
585
+ throw failure;
586
+ }
587
+ if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string"))
588
+ throw failure;
589
+ return parsed;
590
+ }
591
+ /** Checks the instructions with the runtime's scanner, then makes them the bot's persona. */
592
+ export function refreshFleetLocalInstructions(agentUid, systemPrompt, deps = {}) {
593
+ const findings = scanFleetLocalInstructions(agentUid, systemPrompt, deps);
594
+ if (findings.length > 0) {
595
+ throw expectedError(`The bot's instructions contain wording the HQ Fleet Runtime refuses to load (${findings.join(", ")}). Reword the bot's persona.md, worker.yaml or memory notes and try again.`);
596
+ }
597
+ return writeFleetLocalSoul(agentUid, systemPrompt, deps);
551
598
  }
552
599
  function checkedRun(run, executable, args, options, label) {
553
600
  try {
@@ -557,58 +604,62 @@ function checkedRun(run, executable, args, options, label) {
557
604
  throw expectedError(`HQ Fleet Local ${label} failed.`);
558
605
  }
559
606
  }
560
- export function bootstrapFleetLocalBot(input, deps = {}) {
607
+ export async function bootstrapFleetLocalBot(input, deps = {}) {
561
608
  const run = deps.run ?? execFileSync;
562
609
  const platform = deps.platform ?? process.platform;
563
610
  const root = deps.fleetHome ?? fleetHome();
611
+ const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
564
612
  const profileDir = profileRoot(input.agentUid, root);
565
613
  const sources = path.join(profileDir, "sources");
566
614
  const distributionSource = path.join(sources, "distribution");
567
615
  const runtimeSource = path.join(sources, "runtime");
568
616
  const runtimeDir = path.join(profileDir, "runtime");
569
617
  const marker = path.join(profileDir, "bootstrap.json");
570
- const python = pythonCommand(run, platform);
571
618
  fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
572
619
  if (!fs.existsSync(marker)) {
573
620
  fs.rmSync(sources, { recursive: true, force: true });
574
621
  fs.rmSync(runtimeDir, { recursive: true, force: true });
575
622
  fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
576
623
  fs.mkdirSync(runtimeSource, { recursive: true, mode: 0o700 });
577
- checkedRun(run, python.executable, [
578
- ...python.prefix,
624
+ const uvEnv = fleetUvEnv(python.pythonDir);
625
+ checkedRun(run, python.python, [
579
626
  "-m",
580
627
  "zipfile",
581
628
  "-e",
582
629
  input.installation.archive,
583
630
  distributionSource,
584
631
  ], { stdio: "ignore" }, "distribution extraction");
585
- checkedRun(run, python.executable, [
586
- ...python.prefix,
632
+ checkedRun(run, python.python, [
587
633
  "-m",
588
634
  "tarfile",
589
635
  "-e",
590
636
  input.installation.runtimeArchive,
591
637
  runtimeSource,
592
638
  ], { stdio: "ignore" }, "runtime extraction");
593
- checkedRun(run, python.executable, [
594
- ...python.prefix,
595
- "-m",
639
+ checkedRun(run, python.uv, [
596
640
  "venv",
641
+ "--no-config",
642
+ "--python",
643
+ python.python,
597
644
  runtimeDir,
598
- ], { stdio: "ignore" }, "environment creation");
645
+ ], { stdio: "ignore", env: uvEnv }, "environment creation");
599
646
  const runtimePython = venvExecutable(runtimeDir, "python", platform);
600
- checkedRun(run, runtimePython, [
601
- "-m",
647
+ checkedRun(run, python.uv, [
602
648
  "pip",
603
649
  "install",
604
- "--disable-pip-version-check",
650
+ "--no-config",
651
+ "--python",
652
+ runtimePython,
605
653
  "--editable",
606
654
  `${runtimeSource}[all,messaging,slack,anthropic]`,
607
655
  distributionSource,
608
- ], { stdio: ["ignore", 2, 2] }, "package installation");
656
+ ], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation");
609
657
  fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: input.installation.engine }, null, 2)}\n`, { mode: 0o600 });
610
658
  }
611
- const profile = writeFleetLocalProfile(input, { fleetHome: root, platform });
659
+ const { systemPrompt, ...profileInput } = input;
660
+ if (systemPrompt !== undefined)
661
+ refreshFleetLocalInstructions(input.agentUid, systemPrompt, { fleetHome: root, platform, run });
662
+ const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform });
612
663
  const helper = venvExecutable(runtimeDir, "hq-fleet-local", platform);
613
664
  const runtime = venvExecutable(runtimeDir, "hq-fleet", platform);
614
665
  if (!fs.existsSync(helper) || !fs.existsSync(runtime)) {
@@ -0,0 +1,47 @@
1
+ type Exec = (executable: string, args: string[], options: Record<string, unknown>) => Buffer | string;
2
+ export interface FleetPythonToolAsset {
3
+ name: string;
4
+ sha256: string;
5
+ }
6
+ export interface FleetPythonToolPin {
7
+ repository: string;
8
+ version: string;
9
+ assets: Record<string, FleetPythonToolAsset>;
10
+ }
11
+ export interface FleetPythonDeps {
12
+ fleetHome?: string;
13
+ platform?: NodeJS.Platform;
14
+ arch?: NodeJS.Architecture;
15
+ fetch?: typeof fetch;
16
+ run?: Exec;
17
+ tool?: FleetPythonToolPin;
18
+ pythonVersion?: string;
19
+ env?: NodeJS.ProcessEnv;
20
+ }
21
+ export interface FleetPython {
22
+ uv: string;
23
+ python: string;
24
+ pythonDir: string;
25
+ }
26
+ export declare const HQ_FLEET_PYTHON_VERSION = "3.13.17";
27
+ export declare const HQ_FLEET_PYTHON_TOOL: FleetPythonToolPin;
28
+ export declare const FLEET_PYTHON_TOOL_DOWNLOAD_TIMEOUT_MS: number;
29
+ /** One lock for the whole machine: the installer and the private Python are shared by every bot. */
30
+ export declare const FLEET_PYTHON_LOCK_NAME = "hq-fleet-python";
31
+ /**
32
+ * The environment uv runs with: the user's own uv settings (index mirrors,
33
+ * cache, insecure hosts, config files) are dropped so the private install only
34
+ * ever sees the pinned sources.
35
+ */
36
+ export declare function fleetUvEnv(pythonDir: string, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
37
+ /** The operating system's own tar when it is where it always is, so a `tar` on PATH or in the current folder is never run. */
38
+ export declare function tarExecutable(platform: NodeJS.Platform, env?: NodeJS.ProcessEnv): string;
39
+ export declare function fleetPythonToolPath(deps?: FleetPythonDeps): string;
40
+ /**
41
+ * The pinned installer and private Python, installed once per machine under
42
+ * the fleet folder and reused by every bot. Concurrent creates wait for each
43
+ * other on one machine-wide lock instead of racing over the shared folder.
44
+ */
45
+ export declare function ensureFleetPython(deps?: FleetPythonDeps): Promise<FleetPython>;
46
+ export {};
47
+ //# sourceMappingURL=fleet-python.d.ts.map
@@ -0,0 +1,218 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import * as crypto from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { fleetHome } from "./fleet-local.js";
6
+ import { withBotLock } from "./lock.js";
7
+ export const HQ_FLEET_PYTHON_VERSION = "3.13.17";
8
+ export const HQ_FLEET_PYTHON_TOOL = {
9
+ repository: "astral-sh/uv",
10
+ version: "0.13.5",
11
+ assets: {
12
+ "darwin-arm64": { name: "uv-aarch64-apple-darwin.tar.gz", sha256: "743f85035a1996ecb350659e01cd9faea9709829be09b1b4b03c73eaf3c9cd73" },
13
+ "darwin-x64": { name: "uv-x86_64-apple-darwin.tar.gz", sha256: "b0f52b0e3945b4107d89024ea38e625683011fbfc05a0d98f9fa2d6dcee28c66" },
14
+ "linux-arm64": { name: "uv-aarch64-unknown-linux-gnu.tar.gz", sha256: "4bb69cf95ed13f23713bb7bc09f241896ea0c75b7c557e615a20604b82f9c403" },
15
+ "linux-x64": { name: "uv-x86_64-unknown-linux-gnu.tar.gz", sha256: "f68d291c75783122dad58ab77def0e13f3233f358731db5d6d7e8715603b928f" },
16
+ "win32-arm64": { name: "uv-aarch64-pc-windows-msvc.zip", sha256: "09f47edf90726084a70f9426f6e3ca4f46e9bf7cbf23b1716ec64d60b055fae3" },
17
+ "win32-x64": { name: "uv-x86_64-pc-windows-msvc.zip", sha256: "a38b8d1902b7cfbc76b423ba1bb456f623f42064cdc72628b6075b89afd1019a" },
18
+ },
19
+ };
20
+ export const FLEET_PYTHON_TOOL_DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
21
+ /** One lock for the whole machine: the installer and the private Python are shared by every bot. */
22
+ export const FLEET_PYTHON_LOCK_NAME = "hq-fleet-python";
23
+ function expectedError(message) {
24
+ return Object.assign(new Error(message), { expected: true });
25
+ }
26
+ function toolBinaryName(platform) {
27
+ return platform === "win32" ? "uv.exe" : "uv";
28
+ }
29
+ /** The last lines a failed child process wrote, or its exit code, so the user sees why. */
30
+ function causeOf(error) {
31
+ if (error && typeof error === "object") {
32
+ const failure = error;
33
+ const stderr = failure.stderr === undefined ? "" : (Buffer.isBuffer(failure.stderr) ? failure.stderr.toString("utf8") : String(failure.stderr));
34
+ const tail = stderr.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-3).join(" ");
35
+ if (tail)
36
+ return tail;
37
+ if (typeof failure.status === "number")
38
+ return `exit code ${failure.status}`;
39
+ if (failure.message)
40
+ return failure.message;
41
+ }
42
+ return String(error);
43
+ }
44
+ /**
45
+ * The environment uv runs with: the user's own uv settings (index mirrors,
46
+ * cache, insecure hosts, config files) are dropped so the private install only
47
+ * ever sees the pinned sources.
48
+ */
49
+ export function fleetUvEnv(pythonDir, base = process.env) {
50
+ const env = {};
51
+ for (const [key, value] of Object.entries(base)) {
52
+ if (!key.toUpperCase().startsWith("UV_"))
53
+ env[key] = value;
54
+ }
55
+ env.UV_PYTHON_INSTALL_DIR = pythonDir;
56
+ env.UV_NO_CONFIG = "1";
57
+ return env;
58
+ }
59
+ /** The operating system's own tar when it is where it always is, so a `tar` on PATH or in the current folder is never run. */
60
+ export function tarExecutable(platform, env = process.env) {
61
+ const candidates = platform === "win32"
62
+ ? [path.join(env.SystemRoot ?? env.windir ?? "C:\\Windows", "System32", "tar.exe")]
63
+ : ["/usr/bin/tar", "/bin/tar"];
64
+ for (const candidate of candidates) {
65
+ if (fs.existsSync(candidate))
66
+ return candidate;
67
+ }
68
+ return "tar";
69
+ }
70
+ export function fleetPythonToolPath(deps = {}) {
71
+ const root = deps.fleetHome ?? fleetHome();
72
+ const tool = deps.tool ?? HQ_FLEET_PYTHON_TOOL;
73
+ return path.join(root, "tools", "uv", tool.version, toolBinaryName(deps.platform ?? process.platform));
74
+ }
75
+ function findFile(dir, name) {
76
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
77
+ const child = path.join(dir, entry.name);
78
+ if (entry.isDirectory()) {
79
+ const found = findFile(child, name);
80
+ if (found)
81
+ return found;
82
+ }
83
+ else if (entry.isFile() && entry.name === name) {
84
+ return child;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ function installedTool(binary) {
90
+ try {
91
+ const stat = fs.statSync(binary);
92
+ return stat.isFile() && stat.size > 0;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ async function installTool(deps) {
99
+ const platform = deps.platform ?? process.platform;
100
+ const arch = deps.arch ?? process.arch;
101
+ const tool = deps.tool ?? HQ_FLEET_PYTHON_TOOL;
102
+ const binary = fleetPythonToolPath(deps);
103
+ if (installedTool(binary))
104
+ return binary;
105
+ const asset = tool.assets[`${platform}-${arch}`];
106
+ if (!asset) {
107
+ throw expectedError(`HQ Fleet Local does not support ${platform} on ${arch} yet.`);
108
+ }
109
+ const fetcher = deps.fetch ?? fetch;
110
+ const run = deps.run ?? execFileSync;
111
+ const url = `https://github.com/${tool.repository}/releases/download/${tool.version}/${asset.name}`;
112
+ let response;
113
+ try {
114
+ response = await fetcher(url, { signal: AbortSignal.timeout(FLEET_PYTHON_TOOL_DOWNLOAD_TIMEOUT_MS) });
115
+ }
116
+ catch {
117
+ throw expectedError("Could not download the HQ Fleet Local runtime installer. Check your internet connection and retry.");
118
+ }
119
+ if (!response.ok) {
120
+ throw expectedError(`Could not download the HQ Fleet Local runtime installer: GitHub returned HTTP ${response.status}.`);
121
+ }
122
+ const bytes = Buffer.from(await response.arrayBuffer());
123
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== asset.sha256) {
124
+ throw expectedError("The HQ Fleet Local runtime installer download did not match its pinned checksum.");
125
+ }
126
+ const toolDir = path.dirname(binary);
127
+ const staging = `${toolDir}.staging-${process.pid}-${crypto.randomUUID()}`;
128
+ fs.mkdirSync(staging, { recursive: true, mode: 0o700 });
129
+ try {
130
+ const archive = path.join(staging, asset.name);
131
+ fs.writeFileSync(archive, bytes, { mode: 0o600 });
132
+ const extracted = path.join(staging, "extracted");
133
+ fs.mkdirSync(extracted, { mode: 0o700 });
134
+ try {
135
+ run(tarExecutable(platform, deps.env ?? process.env), ["-xf", archive, "-C", extracted], { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
136
+ }
137
+ catch (error) {
138
+ throw expectedError(`HQ Fleet Local could not unpack its runtime installer (${causeOf(error)}).`);
139
+ }
140
+ const unpacked = findFile(extracted, toolBinaryName(platform));
141
+ if (!unpacked)
142
+ throw expectedError("The HQ Fleet Local runtime installer archive did not contain the installer.");
143
+ const prepared = path.join(staging, "tool");
144
+ fs.mkdirSync(prepared, { mode: 0o700 });
145
+ fs.copyFileSync(unpacked, path.join(prepared, path.basename(binary)));
146
+ fs.chmodSync(path.join(prepared, path.basename(binary)), 0o700);
147
+ fs.mkdirSync(path.dirname(toolDir), { recursive: true, mode: 0o700 });
148
+ fs.rmSync(toolDir, { recursive: true, force: true });
149
+ fs.renameSync(prepared, toolDir);
150
+ }
151
+ finally {
152
+ fs.rmSync(staging, { recursive: true, force: true });
153
+ }
154
+ return binary;
155
+ }
156
+ function realpathOrSelf(target) {
157
+ try {
158
+ return fs.realpathSync.native(target);
159
+ }
160
+ catch {
161
+ return path.resolve(target);
162
+ }
163
+ }
164
+ function isInside(parent, child, platform) {
165
+ const fold = (value) => (platform === "win32" ? value.toLowerCase() : value);
166
+ const relative = path.relative(fold(realpathOrSelf(parent)), fold(realpathOrSelf(child)));
167
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
168
+ }
169
+ async function ensureFleetPythonLocked(root, deps) {
170
+ const platform = deps.platform ?? process.platform;
171
+ const run = deps.run ?? execFileSync;
172
+ const version = deps.pythonVersion ?? HQ_FLEET_PYTHON_VERSION;
173
+ const pythonDir = path.join(root, "python");
174
+ const uv = await installTool(deps);
175
+ const env = fleetUvEnv(pythonDir, deps.env ?? process.env);
176
+ fs.mkdirSync(pythonDir, { recursive: true, mode: 0o700 });
177
+ try {
178
+ run(uv, ["python", "install", version, "--no-config", "--no-bin", "--no-registry", "--install-dir", pythonDir], {
179
+ stdio: ["ignore", "ignore", "pipe"],
180
+ env,
181
+ windowsHide: true,
182
+ });
183
+ }
184
+ catch (error) {
185
+ throw expectedError(`HQ Fleet Local could not install its private Python ${version} (${causeOf(error)}). If this mentions a download, check your internet connection and retry.`);
186
+ }
187
+ let found;
188
+ try {
189
+ const output = run(uv, ["python", "find", version, "--managed-python", "--no-config"], {
190
+ stdio: ["ignore", "pipe", "pipe"],
191
+ env,
192
+ encoding: "utf8",
193
+ windowsHide: true,
194
+ });
195
+ found = (Buffer.isBuffer(output) ? output.toString("utf8") : output).trim();
196
+ }
197
+ catch (error) {
198
+ throw expectedError(`HQ Fleet Local installed its private Python but could not locate it (${causeOf(error)}).`);
199
+ }
200
+ const python = path.resolve(found);
201
+ if (!found || !fs.existsSync(python) || !isInside(pythonDir, python, platform)) {
202
+ throw expectedError("HQ Fleet Local found a Python outside its private install and refused to use it.");
203
+ }
204
+ return { uv, python, pythonDir };
205
+ }
206
+ /**
207
+ * The pinned installer and private Python, installed once per machine under
208
+ * the fleet folder and reused by every bot. Concurrent creates wait for each
209
+ * other on one machine-wide lock instead of racing over the shared folder.
210
+ */
211
+ export async function ensureFleetPython(deps = {}) {
212
+ const root = deps.fleetHome ?? fleetHome();
213
+ return withBotLock(FLEET_PYTHON_LOCK_NAME, () => ensureFleetPythonLocked(root, deps), {
214
+ command: "hq bot create",
215
+ root,
216
+ });
217
+ }
218
+ //# sourceMappingURL=fleet-python.js.map
@@ -26,4 +26,5 @@ export * from "./lock.js";
26
26
  export * from "./display-name.js";
27
27
  export * from "./decisions.js";
28
28
  export * from "./fleet-local.js";
29
+ export * from "./fleet-python.js";
29
30
  //# sourceMappingURL=index.d.ts.map
@@ -26,4 +26,5 @@ export * from "./lock.js";
26
26
  export * from "./display-name.js";
27
27
  export * from "./decisions.js";
28
28
  export * from "./fleet-local.js";
29
+ export * from "./fleet-python.js";
29
30
  //# sourceMappingURL=index.js.map
@@ -103,6 +103,11 @@ export interface BuildSystemPromptInput {
103
103
  } | null;
104
104
  /** hqRoot-relative (default `${workerDir}/memory`) or absolute. */
105
105
  memoryDir?: string;
106
+ /**
107
+ * The bot runs on the HQ Fleet Runtime, which labels every message with the
108
+ * sender's HQ identity and prepends no "Owner context" block.
109
+ */
110
+ fleetRuntime?: boolean;
106
111
  }
107
112
  /**
108
113
  * One paragraph saying which kind of bot this is and what that means for the
@@ -110,7 +115,7 @@ export interface BuildSystemPromptInput {
110
115
  * acts as its owner everywhere; a company bot acts as itself and cannot
111
116
  * create anything for the owner.
112
117
  */
113
- export declare function botKindParagraph(input: Pick<BuildSystemPromptInput, "agentUid" | "ownerUid" | "kind" | "companies">): string;
118
+ export declare function botKindParagraph(input: Pick<BuildSystemPromptInput, "agentUid" | "ownerUid" | "kind" | "companies" | "fleetRuntime">): string;
114
119
  /**
115
120
  * The standing instructions the bot runs under. The DM body is delivered as
116
121
  * the user turn; this text is the system prompt (claude/codex) or the prompt
@@ -278,12 +278,17 @@ export function botKindParagraph(input) {
278
278
  `If asked to, say plainly that you are a company bot and cannot do that, and suggest your owner ask one of their personal bots (or do it themselves in HQ). ` +
279
279
  `Nobody's messages carry an "Owner context" block; never state which companies your owner belongs to.`);
280
280
  }
281
+ const ownerContext = input.fleetRuntime
282
+ ? `The runtime labels every message with the sender's HQ identity: messages from ${input.ownerUid} are your owner's, and only those carry your owner's authority. ` +
283
+ `When you need to know which companies your owner belongs to, run \`hq whoami\` yourself; never guess. ` +
284
+ `Other people read your answers in rooms: in a room, never state which companies your owner belongs to, and never repeat anything from their files or your DMs.`
285
+ : `Direct messages from your owner start with an "Owner context" block checked with your owner's own sign-in; that is the only source for your owner's companies. ` +
286
+ `If it says the check failed, say you could not check; never say your owner has no company unless that block says so. ` +
287
+ `Room messages do not carry it, and other people read your answers there: in a room, never state which companies your owner belongs to, and never repeat anything from their files or your DMs.`;
281
288
  return (`You are a PERSONAL bot: you act as your owner, ${input.ownerUid}. ` +
282
289
  `Every \`hq\` command you run — in direct messages, in rooms, and on your first task — runs with your owner's own sign-in, so it reaches everything they can: all of their companies and files. ` +
283
290
  `Creating a company, a bot, an invite or a share for them works directly, so do it yourself when they ask — never tell them to open a terminal or run a command. ` +
284
- `Direct messages from your owner start with an "Owner context" block checked with your owner's own sign-in; that is the only source for your owner's companies. ` +
285
- `If it says the check failed, say you could not check; never say your owner has no company unless that block says so. ` +
286
- `Room messages do not carry it, and other people read your answers there: in a room, never state which companies your owner belongs to, and never repeat anything from their files or your DMs.`);
291
+ ownerContext);
287
292
  }
288
293
  /**
289
294
  * The standing instructions the bot runs under. The DM body is delivered as
@@ -11,11 +11,11 @@
11
11
  */
12
12
  import type { MachineType } from "./machine-type.js";
13
13
  import type { SyncOwner } from "./config.js";
14
- import type { SyncCoreCanaryPlan } from "./sync-core-canary.js";
14
+ import { type SyncCoreVaultPlan } from "../sync-core/engine-home.js";
15
15
  import type { RemoteControlPoolsConfig } from "./remote-control-pools.js";
16
16
  import { type SyncRunnerProcess } from "./sync-runner.js";
17
17
  import { type VaultSyncSwitches } from "../vault-sync-switches.js";
18
- export declare const SERVICE_NAMES: readonly ["sync", "mesh", "heartbeat", "inbox", "bots", "remote-control", "codex-remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
18
+ export declare const SERVICE_NAMES: readonly ["sync", "sync-core", "mesh", "heartbeat", "inbox", "bots", "remote-control", "codex-remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
19
19
  export type ServiceName = (typeof SERVICE_NAMES)[number];
20
20
  export declare function isServiceName(value: unknown): value is ServiceName;
21
21
  export declare const SERVICE_DESCRIPTIONS: Record<ServiceName, string>;
@@ -86,6 +86,13 @@ export interface UnitPlan {
86
86
  idleIntervalSec: number;
87
87
  };
88
88
  }
89
+ export interface SyncCoreRunning {
90
+ personal: boolean;
91
+ companies: ReadonlySet<string>;
92
+ }
93
+ export declare const SYNC_CORE_RECONCILE_UNIT = "sync-core";
94
+ export declare const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
95
+ export declare const SYNC_CORE_LEGACY_SETTLE_MS: number;
89
96
  export interface PlanContext {
90
97
  machineType: MachineType;
91
98
  platform: NodeJS.Platform;
@@ -113,9 +120,10 @@ export interface PlanContext {
113
120
  vaultSync?: VaultSyncSwitches;
114
121
  /** Persisted/effective owner selected for this machine. */
115
122
  syncOwner: SyncOwner;
116
- syncCoreCanary?: SyncCoreCanaryPlan;
123
+ syncCoreVaults?: readonly SyncCoreVaultPlan[];
124
+ syncCoreRunning?: SyncCoreRunning;
117
125
  legacySyncReadyForCore?: boolean;
118
- syncCoreCanaryRunning?: boolean;
126
+ now?: number;
119
127
  /** A legacy desktop app or sync unit is still managing sync. */
120
128
  desktopAppManagesSync: boolean;
121
129
  /** Full process list, or null when the daemon could not read it. */