@indigoai-us/hq-cli 5.345.171 → 5.345.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/command-catalog.generated.d.ts +74 -2
- package/dist/command-catalog.generated.js +88 -2
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/bot.js +46 -8
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/daemon.js +16 -1
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +22 -4
- package/dist/commands/sync-core.d.ts +22 -0
- package/dist/commands/sync-core.js +126 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +278 -224
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/fleet-local.d.ts +16 -1
- package/dist/lib/bot/fleet-local.js +99 -48
- package/dist/lib/bot/fleet-python.d.ts +47 -0
- package/dist/lib/bot/fleet-python.js +218 -0
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/prompt.d.ts +6 -1
- package/dist/lib/bot/prompt.js +8 -3
- package/dist/lib/daemon/catalog.d.ts +12 -4
- package/dist/lib/daemon/catalog.js +70 -27
- package/dist/lib/daemon/config.d.ts +0 -7
- package/dist/lib/daemon/config.js +0 -32
- package/dist/lib/daemon/run.d.ts +5 -1
- package/dist/lib/daemon/run.js +48 -11
- package/dist/lib/daemon/supervisor.js +3 -1
- package/dist/lib/sync-core/engine-home.d.ts +45 -0
- package/dist/lib/sync-core/engine-home.js +194 -0
- package/dist/lib/sync-core/flag.d.ts +17 -0
- package/dist/lib/sync-core/flag.js +47 -0
- package/dist/lib/sync-core/reconcile.d.ts +31 -0
- package/dist/lib/sync-core/reconcile.js +217 -0
- package/dist/lib/sync-core/release.d.ts +32 -0
- package/dist/lib/sync-core/release.js +142 -0
- package/dist/lib/sync-core/rollout-state.d.ts +28 -0
- package/dist/lib/sync-core/rollout-state.js +115 -0
- package/dist/lib/sync-core/skip.d.ts +7 -0
- package/dist/lib/sync-core/skip.js +30 -0
- package/dist/lib/vault-sync-switches.d.ts +5 -0
- package/dist/lib/vault-sync-switches.js +43 -0
- package/dist/utils/remote-list-not-converging.d.ts +21 -0
- package/dist/utils/remote-list-not-converging.js +40 -0
- package/package.json +8 -8
- package/dist/lib/daemon/sync-core-canary.d.ts +0 -12
- package/dist/lib/daemon/sync-core-canary.js +0 -61
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
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.
|
|
10
|
-
version: "0.4.
|
|
11
|
-
artifact: "hq-fleet-local-0.4.
|
|
12
|
-
artifactSha256: "
|
|
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: "
|
|
16
|
-
signatureSha256: "
|
|
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
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
601
|
-
"-m",
|
|
647
|
+
checkedRun(run, python.uv, [
|
|
602
648
|
"pip",
|
|
603
649
|
"install",
|
|
604
|
-
"--
|
|
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
|
|
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
|
package/dist/lib/bot/index.d.ts
CHANGED
package/dist/lib/bot/index.js
CHANGED
package/dist/lib/bot/prompt.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
123
|
+
syncCoreVaults?: readonly SyncCoreVaultPlan[];
|
|
124
|
+
syncCoreRunning?: SyncCoreRunning;
|
|
117
125
|
legacySyncReadyForCore?: boolean;
|
|
118
|
-
|
|
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. */
|