@indigoai-us/hq-cli 5.345.189 → 5.345.191
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 +56 -0
- package/dist/command-catalog.generated.d.ts +29 -0
- package/dist/command-catalog.generated.js +36 -0
- package/dist/commands/bot.d.ts +3 -1
- package/dist/commands/bot.js +28 -4
- package/dist/commands/cloud.js +16 -2
- package/dist/commands/files.d.ts +18 -0
- package/dist/commands/files.js +73 -0
- package/dist/lib/bot/fleet-local.d.ts +61 -2
- package/dist/lib/bot/fleet-local.js +256 -49
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.191] — 2026-11-02
|
|
6
|
+
- `hq bot list` and `hq bot status --json` rows now carry `engine: "hq-fleet-runtime"` for a bot on
|
|
7
|
+
the HQ Fleet Runtime, so the desktop app can tell a Fleet bot from a model-CLI bot (Create brief
|
|
8
|
+
uses this to prefer a Fleet bot).
|
|
9
|
+
- `hq bot auth` on an HQ Fleet Runtime bot gives the restarted gateway up to 15 s to come up before
|
|
10
|
+
reporting; it used to say "signed in, but did not start" while the bot was in fact starting.
|
|
11
|
+
- `hq bot status` on an HQ Fleet Runtime bot shows whether its gateway is installed as a login
|
|
12
|
+
service instead of a "launchd: not installed" line that was about the legacy bot daemon.
|
|
13
|
+
- `hq bot create --no-auto-approve` now reaches an HQ Fleet Runtime bot's profile (`autoApprove:
|
|
14
|
+
false`); with hq-fleet-local 0.4.18 the default profile runs with command approvals off, like a
|
|
15
|
+
model-CLI bot, so a headless bot no longer stalls on an approval nobody can answer.
|
|
16
|
+
- HQ Fleet Runtime pin moves to hq-fleet-local 0.4.18, which stores the bot's
|
|
17
|
+
machine credential with Windows DPAPI under PowerShell 5.1 and no longer
|
|
18
|
+
flashes a console on Windows. The CLI now trusts two release signing keys
|
|
19
|
+
(the 2026-10-30 key and a second key added 2026-11-01); a manifest signed by
|
|
20
|
+
either verifies, and any other key is rejected.
|
|
21
|
+
- `hq bot create` / `hq bot start` (HQ Fleet Runtime): every helper, uv and
|
|
22
|
+
Python child process now always starts with `windowsHide`, which a caller's
|
|
23
|
+
options can no longer turn off, so no console window flashes on Windows.
|
|
24
|
+
A new test records every child process in a Windows create, start, stop and
|
|
25
|
+
status run and checks each one.
|
|
26
|
+
|
|
27
|
+
## [5.345.190] — 2026-11-01
|
|
28
|
+
- `hq files purge <path> --version-id <id>` permanently deletes one old version of a vault file, after a confirmation prompt.
|
|
29
|
+
- `hq bot create` / `hq bot start` on Windows: the shared HQ Fleet Runtime now
|
|
30
|
+
installs under `%USERPROFILE%\.hq-fleet\runtimes\<16 hex>\{v,s\r,s\d}` instead
|
|
31
|
+
of the 64-character identity plus `sources\runtime`, so the runtime's deepest
|
|
32
|
+
files stay under the 260-character Windows path limit when long paths are
|
|
33
|
+
off. Before, extraction stopped partway with "HQ Fleet Local runtime
|
|
34
|
+
extraction failed." Runtimes already installed under the old layout keep
|
|
35
|
+
working, and a half-extracted old one is removed. macOS and Linux paths are
|
|
36
|
+
unchanged.
|
|
37
|
+
- HQ Fleet Local install steps now include the last lines of the failing
|
|
38
|
+
command's stderr in their error, and on Windows the CLI checks the runtime
|
|
39
|
+
archive's longest path before extracting and names the path limit and
|
|
40
|
+
`LongPathsEnabled` setting when it would not fit.
|
|
41
|
+
- The CLI now warns on stderr when the HQ Fleet Local release manifest's
|
|
42
|
+
`compatibility.minimum_hq_cli_version` is newer than the running CLI. It does
|
|
43
|
+
not block the install.
|
|
44
|
+
- `hq-hook`: the `block-credential-file-read` port follows the fixed bash guard
|
|
45
|
+
(hq-core-staging #1753, sha256 7c6a8cfa) and its pin moves to that version.
|
|
46
|
+
Zero-padded brace ranges are read in base 10 and long or stepped ranges
|
|
47
|
+
block; letter ranges expand exactly; the mktemp scan replacement is literal
|
|
48
|
+
(no `patsub_replacement`) with capped loops; `function name { ... }` bodies
|
|
49
|
+
are analysed; extglob groups are an unknown word part; and `ls`, `stat` and
|
|
50
|
+
the other metadata commands are not trusted after an alias, hash, enable,
|
|
51
|
+
PATH change, a write into a bin directory, or when named by a path outside
|
|
52
|
+
/bin, /usr/bin, /sbin and /usr/sbin. The port also picks up the guard's
|
|
53
|
+
cygpath and `upstream-pin.env` changes. The vendored fixture and corpus are
|
|
54
|
+
refreshed. The port flag stays off.
|
|
55
|
+
- `hq sync now`: when the push leg holds files after an account sign-in change
|
|
56
|
+
on this computer, the push line now shows the held count, prints how to
|
|
57
|
+
release them (`hq sync held`, `hq sync release-held`), skips the "Sync now
|
|
58
|
+
complete" line and exits 1. Before, it printed "0 uploaded, 0 skipped" and a
|
|
59
|
+
success line (feedback_2356e54f).
|
|
60
|
+
|
|
5
61
|
## [5.345.189] — 2026-11-01
|
|
6
62
|
- A local bot that cannot reach HQ when it starts now retries the identity check up to five times
|
|
7
63
|
(waits of 5 s, 15 s, 45 s and 2 min) on a network error (connection refused, reset, DNS failure,
|
|
@@ -3243,6 +3243,35 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3243
3243
|
readonly hidden: false;
|
|
3244
3244
|
}];
|
|
3245
3245
|
readonly subcommands: readonly [];
|
|
3246
|
+
}, {
|
|
3247
|
+
readonly name: "purge";
|
|
3248
|
+
readonly description: "Permanently delete ONE retained (noncurrent) version of an exact vault key, for example a version that held data which must not live in HQ. Owner only; audited; cannot be undone. Delete the file first if the version is current. Use --personal for your personal vault.";
|
|
3249
|
+
readonly aliases: readonly [];
|
|
3250
|
+
readonly hidden: false;
|
|
3251
|
+
readonly helpOption: true;
|
|
3252
|
+
readonly helpCommand: false;
|
|
3253
|
+
readonly usage: "[options] <path>";
|
|
3254
|
+
readonly helpText: readonly [];
|
|
3255
|
+
readonly arguments: readonly [{
|
|
3256
|
+
readonly name: "path";
|
|
3257
|
+
readonly required: true;
|
|
3258
|
+
readonly variadic: false;
|
|
3259
|
+
readonly description: "";
|
|
3260
|
+
}];
|
|
3261
|
+
readonly options: readonly [{
|
|
3262
|
+
readonly flags: "--personal";
|
|
3263
|
+
readonly description: "Target your own personal vault instead of a company vault (mutually exclusive with --company)";
|
|
3264
|
+
readonly hidden: false;
|
|
3265
|
+
}, {
|
|
3266
|
+
readonly flags: "--version-id <id>";
|
|
3267
|
+
readonly description: "Exact version ID to purge (see `hq files versions`)";
|
|
3268
|
+
readonly hidden: false;
|
|
3269
|
+
}, {
|
|
3270
|
+
readonly flags: "-y, --yes";
|
|
3271
|
+
readonly description: "Skip the confirmation prompt (for scripts)";
|
|
3272
|
+
readonly hidden: false;
|
|
3273
|
+
}];
|
|
3274
|
+
readonly subcommands: readonly [];
|
|
3246
3275
|
}, {
|
|
3247
3276
|
readonly name: "trash";
|
|
3248
3277
|
readonly description: "List deleted vault keys retained as tombstones. Use --personal for your personal vault.";
|
|
@@ -3912,6 +3912,42 @@ export const COMMAND_CATALOG = [
|
|
|
3912
3912
|
],
|
|
3913
3913
|
"subcommands": []
|
|
3914
3914
|
},
|
|
3915
|
+
{
|
|
3916
|
+
"name": "purge",
|
|
3917
|
+
"description": "Permanently delete ONE retained (noncurrent) version of an exact vault key, for example a version that held data which must not live in HQ. Owner only; audited; cannot be undone. Delete the file first if the version is current. Use --personal for your personal vault.",
|
|
3918
|
+
"aliases": [],
|
|
3919
|
+
"hidden": false,
|
|
3920
|
+
"helpOption": true,
|
|
3921
|
+
"helpCommand": false,
|
|
3922
|
+
"usage": "[options] <path>",
|
|
3923
|
+
"helpText": [],
|
|
3924
|
+
"arguments": [
|
|
3925
|
+
{
|
|
3926
|
+
"name": "path",
|
|
3927
|
+
"required": true,
|
|
3928
|
+
"variadic": false,
|
|
3929
|
+
"description": ""
|
|
3930
|
+
}
|
|
3931
|
+
],
|
|
3932
|
+
"options": [
|
|
3933
|
+
{
|
|
3934
|
+
"flags": "--personal",
|
|
3935
|
+
"description": "Target your own personal vault instead of a company vault (mutually exclusive with --company)",
|
|
3936
|
+
"hidden": false
|
|
3937
|
+
},
|
|
3938
|
+
{
|
|
3939
|
+
"flags": "--version-id <id>",
|
|
3940
|
+
"description": "Exact version ID to purge (see `hq files versions`)",
|
|
3941
|
+
"hidden": false
|
|
3942
|
+
},
|
|
3943
|
+
{
|
|
3944
|
+
"flags": "-y, --yes",
|
|
3945
|
+
"description": "Skip the confirmation prompt (for scripts)",
|
|
3946
|
+
"hidden": false
|
|
3947
|
+
}
|
|
3948
|
+
],
|
|
3949
|
+
"subcommands": []
|
|
3950
|
+
},
|
|
3915
3951
|
{
|
|
3916
3952
|
"name": "trash",
|
|
3917
3953
|
"description": "List deleted vault keys retained as tombstones. Use --personal for your personal vault.",
|
package/dist/commands/bot.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type RuntimeSignInCheck, type RuntimeSignInIssue } from "../lib/bot/run
|
|
|
3
3
|
import type { BotStatusFile } from "../lib/bot/status.js";
|
|
4
4
|
import { probeBotRuntime, type RuntimeProbeResult } from "../lib/bot/probe.js";
|
|
5
5
|
import { type SpawnInteractive } from "../lib/bot/runtime-auth.js";
|
|
6
|
-
import { type BotLocalConfig, type MyLocalBot, type BotKind, type BotMemoryMode, type BotNotRunnableReason, type RuntimeCrash, type BotConfig, type BotRuntimeId, type LaunchctlExec, type BotRole } from "../lib/bot/index.js";
|
|
6
|
+
import { type BotLocalConfig, type MyLocalBot, type BotKind, type BotMemoryMode, type BotNotRunnableReason, type RuntimeCrash, type BotConfig, type BotRuntimeId, type LaunchctlExec, HQ_FLEET_RUNTIME_ENGINE_ID, type BotRole } from "../lib/bot/index.js";
|
|
7
7
|
/** Real launchctl; every failure is a non-ok result, never a throw. */
|
|
8
8
|
export declare const defaultLaunchctl: LaunchctlExec;
|
|
9
9
|
/** The desktop app creates HQ's setup bot under this name, always from the `setup` worker. */
|
|
@@ -13,6 +13,8 @@ export interface BotRow {
|
|
|
13
13
|
agentUid: string;
|
|
14
14
|
ownerUid: string;
|
|
15
15
|
runtime: BotRuntimeId;
|
|
16
|
+
/** `hq-fleet-runtime` for a bot on the HQ Fleet Runtime; absent for a model-CLI bot. */
|
|
17
|
+
engine?: typeof HQ_FLEET_RUNTIME_ENGINE_ID;
|
|
16
18
|
needsSignIn?: boolean;
|
|
17
19
|
authCommand?: string;
|
|
18
20
|
serviceInstallPending?: boolean;
|
package/dist/commands/bot.js
CHANGED
|
@@ -57,7 +57,7 @@ import { runtimeDisplayName, signInCommand } from "../lib/bot/runtime-sign-in.js
|
|
|
57
57
|
import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
|
|
58
58
|
import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
|
|
59
59
|
import { BOT_LOGIN_RUNTIMES, botAuthEnv, botRuntimeAuth, capacityAccountHome, describeBotAuth, formatBotAuth, isBotLoginRuntime, restoreBotSessions, setAsideBotSessions, signInBotRuntime, signOutBotRuntime, withRuntimeAuth, } from "../lib/bot/runtime-auth.js";
|
|
60
|
-
import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, runtimeCrashSinceStart, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetProviderForRuntime, importFleetLocalMachineCredential, fleetProviderNeedsSignIn, fleetDefaultCompanies, fleetOwnerCompanies, fleetRuntimeIsDefault, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
|
|
60
|
+
import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, runtimeCrashSinceStart, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetLocalServiceInstalled, fleetProviderForRuntime, importFleetLocalMachineCredential, fleetProviderNeedsSignIn, fleetDefaultCompanies, fleetOwnerCompanies, fleetRuntimeIsDefault, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
|
|
61
61
|
function collectRepeated(value, previous) {
|
|
62
62
|
return [...previous, value];
|
|
63
63
|
}
|
|
@@ -152,6 +152,22 @@ function fleetBotSystemPrompt(name, config, overrides = {}) {
|
|
|
152
152
|
function fleetAuthCommand(name, config) {
|
|
153
153
|
return `hq bot auth ${name} --provider ${config.runtime === "grok" ? "grok" : config.runtime === "codex" ? "chatgpt" : "claude"}`;
|
|
154
154
|
}
|
|
155
|
+
/** How long a just-(re)started HQ Fleet gateway gets to write its pid before it counts as not started. */
|
|
156
|
+
function fleetStartWaitMs() {
|
|
157
|
+
const raw = Number(process.env.HQ_BOT_FLEET_START_WAIT_MS);
|
|
158
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : 15_000;
|
|
159
|
+
}
|
|
160
|
+
async function fleetGatewayComesUp(agentUid, waitMs, stepMs = 500) {
|
|
161
|
+
const deadline = Date.now() + waitMs;
|
|
162
|
+
for (;;) {
|
|
163
|
+
const pid = fleetLocalGatewayPid(agentUid);
|
|
164
|
+
if (pid !== null && isPidAlive(pid))
|
|
165
|
+
return true;
|
|
166
|
+
if (Date.now() >= deadline)
|
|
167
|
+
return false;
|
|
168
|
+
await new Promise((r) => setTimeout(r, stepMs));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
155
171
|
function runFleetRuntimeControl(name, config, action, json) {
|
|
156
172
|
if ((action === "install" || action === "start" || action === "restart") && config.fleetAuthPending === true) {
|
|
157
173
|
const authCommand = fleetAuthCommand(name, config);
|
|
@@ -236,6 +252,7 @@ function rowFor(name, opts = {}) {
|
|
|
236
252
|
agentUid: config.agentUid,
|
|
237
253
|
ownerUid: config.ownerUid,
|
|
238
254
|
runtime: config.runtime,
|
|
255
|
+
...(fleet ? { engine: HQ_FLEET_RUNTIME_ENGINE_ID } : {}),
|
|
239
256
|
model: config.model,
|
|
240
257
|
effort: effectiveBotEffort(config),
|
|
241
258
|
effortIsDefault: !config.effort,
|
|
@@ -477,6 +494,7 @@ async function adoptFleetRuntimeProfile(name, dir, config) {
|
|
|
477
494
|
systemPrompt: fleetBotSystemPrompt(name, config),
|
|
478
495
|
provider: fleetProviderForRuntime(config.runtime, config.model),
|
|
479
496
|
ownerCompanies: companyScope,
|
|
497
|
+
...(config.autoApprove === false ? { autoApprove: false } : {}),
|
|
480
498
|
}, { python, start: false });
|
|
481
499
|
patchBotConfig(dir, { engine: installation.engine });
|
|
482
500
|
const credentials = botCredsPath(dir);
|
|
@@ -1124,6 +1142,7 @@ async function runBotCreateLocked(name, opts) {
|
|
|
1124
1142
|
systemPrompt: fleetBotSystemPrompt(name, config, { kind, companies }),
|
|
1125
1143
|
provider: fleetProvider,
|
|
1126
1144
|
ownerCompanies,
|
|
1145
|
+
...(opts.autoApprove === false ? { autoApprove: false } : {}),
|
|
1127
1146
|
firstStart: {
|
|
1128
1147
|
intro: introDmText(name, runtime, intro, { kind, companies }),
|
|
1129
1148
|
...(kickoff ? { kickoff } : {}),
|
|
@@ -1452,7 +1471,12 @@ async function runBotStatus(nameArg, opts) {
|
|
|
1452
1471
|
console.log(` inbox: last poll ${status.lastInboxPollAt}`);
|
|
1453
1472
|
}
|
|
1454
1473
|
console.log(` answering: ${row.busy ? chalk.green(`yes${row.busySince ? `, since ${row.busySince}` : ""}`) : chalk.gray("no")}`);
|
|
1455
|
-
|
|
1474
|
+
if (fleet) {
|
|
1475
|
+
console.log(` service: ${fleetLocalServiceInstalled(config.agentUid) ? "installed (HQ Fleet gateway, starts at login)" : row.serviceInstallPending ? "not installed yet; installs on hq bot start" : "not installed"}`);
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
console.log(` launchd: ${row.daemonInstalled ? (row.daemonLoaded ? "installed, loaded" : "installed, not loaded") : "not installed"}`);
|
|
1479
|
+
}
|
|
1456
1480
|
if (row.daemonNodeMissing) {
|
|
1457
1481
|
console.log(` ${chalk.red(`node missing: ${row.daemonNodeMissing}`)} — launchd cannot start ${name}; fix with: hq bot start ${name}`);
|
|
1458
1482
|
}
|
|
@@ -2150,8 +2174,7 @@ async function runBotAuth(nameArg, opts) {
|
|
|
2150
2174
|
model: result.model,
|
|
2151
2175
|
...(b.config.fleetAuthPending === true ? { fleetAuthPending: false, fleetServiceInstallPending: !result.restarted, enabled: result.restarted } : {}),
|
|
2152
2176
|
});
|
|
2153
|
-
const
|
|
2154
|
-
const running = pid !== null && isPidAlive(pid);
|
|
2177
|
+
const running = await fleetGatewayComesUp(b.config.agentUid, result.restarted ? fleetStartWaitMs() : 0);
|
|
2155
2178
|
if (opts.json) {
|
|
2156
2179
|
console.log(JSON.stringify({ ok: true, name: b.name, runtime: HQ_FLEET_RUNTIME_ENGINE_ID, ...result, needsSignIn: false, running, serviceInstallPending: b.config.fleetAuthPending === true && !result.restarted }, null, 2));
|
|
2157
2180
|
return;
|
|
@@ -2446,6 +2469,7 @@ async function adoptRemoteBotLocked(input) {
|
|
|
2446
2469
|
systemPrompt: fleetBotSystemPrompt(name, config),
|
|
2447
2470
|
provider: fleetProviderForRuntime(config.runtime, config.model),
|
|
2448
2471
|
ownerCompanies: await fleetOwnerCompanies(),
|
|
2472
|
+
...(config.autoApprove === false ? { autoApprove: false } : {}),
|
|
2449
2473
|
...(input.preserveFirstStart
|
|
2450
2474
|
? {
|
|
2451
2475
|
firstStart: {
|
package/dist/commands/cloud.js
CHANGED
|
@@ -2351,12 +2351,19 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2351
2351
|
}
|
|
2352
2352
|
const pushProgressCompany = personalMode ? null : (pushSlug ?? null);
|
|
2353
2353
|
const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(pushProgressCompany));
|
|
2354
|
+
// feedback_2356e54f: the push leg held files after an account switch and
|
|
2355
|
+
// still printed "0 uploaded, 0 skipped" and "Sync now complete".
|
|
2356
|
+
const pushIdentityHeld = createIdentityHeldTracker();
|
|
2357
|
+
const pushRecord = trackingProgressRecorder(planLimitTracker, pushProgressCompany);
|
|
2354
2358
|
const pushResult = await share({
|
|
2355
2359
|
paths: pushPaths,
|
|
2356
2360
|
company: targetCompany,
|
|
2357
2361
|
vaultConfig,
|
|
2358
2362
|
hqRoot,
|
|
2359
|
-
progressRecorder:
|
|
2363
|
+
progressRecorder: (event) => {
|
|
2364
|
+
pushIdentityHeld.observe(event);
|
|
2365
|
+
pushRecord(event);
|
|
2366
|
+
},
|
|
2360
2367
|
skipUnchanged: true,
|
|
2361
2368
|
serverOwnedPathPolicy: "skip",
|
|
2362
2369
|
propagateDeletes: true,
|
|
@@ -2370,7 +2377,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2370
2377
|
});
|
|
2371
2378
|
const pushRefused = planLimitTracker.summary();
|
|
2372
2379
|
const pushUploadFailed = readUploadFailures(pushResult);
|
|
2380
|
+
const pushHeldCount = pushIdentityHeld.count();
|
|
2373
2381
|
const pushStatus = pushResult.aborted ||
|
|
2382
|
+
pushHeldCount > 0 ||
|
|
2374
2383
|
pushResult.filesExcludedByScope > 0 ||
|
|
2375
2384
|
(pushResult.filesRefusedStale ?? 0) > 0 ||
|
|
2376
2385
|
pushUploadFailed.count > 0 ||
|
|
@@ -2380,6 +2389,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2380
2389
|
console.log(` ${pushStatus} ` +
|
|
2381
2390
|
`${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
|
|
2382
2391
|
`${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
|
|
2392
|
+
(pushHeldCount > 0 ? `, ${pushHeldCount} held` : "") +
|
|
2383
2393
|
(pushResult.aborted ? " — aborted" : "") +
|
|
2384
2394
|
(pushRefused
|
|
2385
2395
|
? ` — ${describeRefusedCount(pushRefused)} refused (new files paused)`
|
|
@@ -2397,6 +2407,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2397
2407
|
for (const line of uploadFailedLines(pushUploadFailed.count, pushUploadFailed.paths)) {
|
|
2398
2408
|
console.log(chalk.yellow(line));
|
|
2399
2409
|
}
|
|
2410
|
+
for (const line of identityHeldLines(pushHeldCount)) {
|
|
2411
|
+
console.log(chalk.yellow(line));
|
|
2412
|
+
}
|
|
2400
2413
|
if (pushResult.aborted) {
|
|
2401
2414
|
console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
|
|
2402
2415
|
throw new SyncExitError(1);
|
|
@@ -2519,7 +2532,8 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2519
2532
|
}
|
|
2520
2533
|
if (pushResult.filesExcludedByScope > 0 ||
|
|
2521
2534
|
(pushResult.filesRefusedStale ?? 0) > 0 ||
|
|
2522
|
-
pushUploadFailed.count > 0
|
|
2535
|
+
pushUploadFailed.count > 0 ||
|
|
2536
|
+
pushHeldCount > 0) {
|
|
2523
2537
|
console.log(chalk.yellow("\n⚠ Sync now finished, but one or more requested uploads were refused."));
|
|
2524
2538
|
throw new SyncExitError(1, { health: "succeeded" });
|
|
2525
2539
|
}
|
package/dist/commands/files.d.ts
CHANGED
|
@@ -242,6 +242,24 @@ export declare function runFilesRestore(params: {
|
|
|
242
242
|
}, deps?: {
|
|
243
243
|
confirm?: ConfirmFn;
|
|
244
244
|
}): Promise<FilesRestoreResponse | undefined>;
|
|
245
|
+
export interface FilesPurgeResponse {
|
|
246
|
+
key: string;
|
|
247
|
+
purgedVersionId: string;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* feedback_fc711fc3: permanently delete one noncurrent version. The server
|
|
251
|
+
* (hq-pro POST /v1/files/restore, action "purge") enforces owner-only access,
|
|
252
|
+
* refuses the current version and delete markers, and audits before deleting.
|
|
253
|
+
*/
|
|
254
|
+
export declare function runFilesPurge(params: {
|
|
255
|
+
key: string;
|
|
256
|
+
versionId: string;
|
|
257
|
+
yes: boolean;
|
|
258
|
+
personal: boolean;
|
|
259
|
+
companySlug: string | undefined;
|
|
260
|
+
}, deps?: {
|
|
261
|
+
confirm?: ConfirmFn;
|
|
262
|
+
}): Promise<FilesPurgeResponse | undefined>;
|
|
245
263
|
export declare function runFilesTrash(params: {
|
|
246
264
|
prefix: string;
|
|
247
265
|
cursor?: string;
|
package/dist/commands/files.js
CHANGED
|
@@ -731,6 +731,30 @@ export function registerFilesCommand(program) {
|
|
|
731
731
|
process.exit(1);
|
|
732
732
|
}
|
|
733
733
|
});
|
|
734
|
+
files
|
|
735
|
+
.command("purge <path>")
|
|
736
|
+
.description("Permanently delete ONE retained (noncurrent) version of an exact vault key, for example a version that held data which must not live in HQ. Owner only; audited; cannot be undone. Delete the file first if the version is current. Use --personal for your personal vault.")
|
|
737
|
+
.option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
|
|
738
|
+
.requiredOption("--version-id <id>", "Exact version ID to purge (see `hq files versions`)")
|
|
739
|
+
.option("-y, --yes", "Skip the confirmation prompt (for scripts)")
|
|
740
|
+
.action(async (path, opts) => {
|
|
741
|
+
try {
|
|
742
|
+
const companySlug = files.opts().company;
|
|
743
|
+
const personal = opts.personal === true;
|
|
744
|
+
assertRecoveryScope(personal, companySlug);
|
|
745
|
+
await runFilesPurge({
|
|
746
|
+
key: path,
|
|
747
|
+
versionId: opts.versionId,
|
|
748
|
+
yes: opts.yes === true,
|
|
749
|
+
personal,
|
|
750
|
+
companySlug,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
734
758
|
files
|
|
735
759
|
.command("trash")
|
|
736
760
|
.description("List deleted vault keys retained as tombstones. Use --personal for your personal vault.")
|
|
@@ -1767,6 +1791,55 @@ export async function runFilesRestore(params, deps = {}) {
|
|
|
1767
1791
|
console.log(chalk.dim("New version: " + data.newVersionId));
|
|
1768
1792
|
return data;
|
|
1769
1793
|
}
|
|
1794
|
+
/**
|
|
1795
|
+
* feedback_fc711fc3: permanently delete one noncurrent version. The server
|
|
1796
|
+
* (hq-pro POST /v1/files/restore, action "purge") enforces owner-only access,
|
|
1797
|
+
* refuses the current version and delete markers, and audits before deleting.
|
|
1798
|
+
*/
|
|
1799
|
+
export async function runFilesPurge(params, deps = {}) {
|
|
1800
|
+
assertExactRecoveryKey(params.key);
|
|
1801
|
+
if (typeof params.versionId !== "string" || params.versionId.trim() === "") {
|
|
1802
|
+
throw new Error("--version-id is required and must not be empty.");
|
|
1803
|
+
}
|
|
1804
|
+
const confirm = deps.confirm ?? realConfirm;
|
|
1805
|
+
if (!params.yes) {
|
|
1806
|
+
const ok = await confirm("Permanently delete version '" + params.versionId + "' of '" + params.key + "'" +
|
|
1807
|
+
(params.personal ? " in your personal vault" : "") +
|
|
1808
|
+
"? This cannot be undone.");
|
|
1809
|
+
if (!ok) {
|
|
1810
|
+
console.log(chalk.dim("Aborted — nothing was purged."));
|
|
1811
|
+
return undefined;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
const scope = await resolveRecoveryScope(params);
|
|
1815
|
+
const body = scope.personal
|
|
1816
|
+
? { personal: true, key: params.key }
|
|
1817
|
+
: { company: scope.companyUid, key: params.key };
|
|
1818
|
+
body.action = "purge";
|
|
1819
|
+
body.purgeVersionId = params.versionId;
|
|
1820
|
+
// Tripwire: a server without purge support rejects `versionId: null` with
|
|
1821
|
+
// 400 instead of ignoring `action` and restoring (re-publishing) the version.
|
|
1822
|
+
body.versionId = null;
|
|
1823
|
+
const res = await vaultApiFetch({
|
|
1824
|
+
token: scope.token,
|
|
1825
|
+
path: "/v1/files/restore",
|
|
1826
|
+
method: "POST",
|
|
1827
|
+
body,
|
|
1828
|
+
});
|
|
1829
|
+
if (!res.ok) {
|
|
1830
|
+
const errBody = (await res.json().catch(() => ({})));
|
|
1831
|
+
const err = new FilesRecoveryHttpError(res.status, errBody.message ?? errBody.error ?? res.statusText, errBody.code);
|
|
1832
|
+
throw new Error(formatFilesRecoveryError(err, params.key));
|
|
1833
|
+
}
|
|
1834
|
+
const data = (await res.json());
|
|
1835
|
+
// Belt and braces with the tripwire above: only a server that echoes the
|
|
1836
|
+
// purged version id has purged it.
|
|
1837
|
+
if (data.purgedVersionId !== params.versionId) {
|
|
1838
|
+
throw new Error("The server did not confirm the purge (it may not support purge yet). Check `hq files versions` before retrying.");
|
|
1839
|
+
}
|
|
1840
|
+
console.log(chalk.green("Purged version '" + data.purgedVersionId + "' of '" + params.key + "'."));
|
|
1841
|
+
return { key: data.key ?? params.key, purgedVersionId: data.purgedVersionId };
|
|
1842
|
+
}
|
|
1770
1843
|
export async function runFilesTrash(params) {
|
|
1771
1844
|
if (params.prefix.includes("*")) {
|
|
1772
1845
|
throw new Error("Trash prefix must be literal; wildcard expansion is not supported.");
|
|
@@ -41,6 +41,7 @@ export interface FleetLocalProfile {
|
|
|
41
41
|
transport?: string;
|
|
42
42
|
};
|
|
43
43
|
ownerCompanies?: string[];
|
|
44
|
+
autoApprove?: boolean;
|
|
44
45
|
hqRoot: string;
|
|
45
46
|
profileDir: string;
|
|
46
47
|
runtime: {
|
|
@@ -60,7 +61,7 @@ export interface FleetLocalInstallDeps {
|
|
|
60
61
|
hqReleaseLinks?: (release: FleetLocalReleasePin) => Promise<Map<string, string> | null>;
|
|
61
62
|
now?: () => Date;
|
|
62
63
|
release?: FleetLocalReleasePin;
|
|
63
|
-
publicKey?: string;
|
|
64
|
+
publicKey?: string | readonly string[];
|
|
64
65
|
}
|
|
65
66
|
export interface FleetLocalProfileInput {
|
|
66
67
|
agentUid: string;
|
|
@@ -79,6 +80,8 @@ export interface FleetLocalProfileInput {
|
|
|
79
80
|
};
|
|
80
81
|
/** Companies the owner's own turns may use; other senders get none of them. */
|
|
81
82
|
ownerCompanies?: string[];
|
|
83
|
+
/** False keeps the runtime's command approval prompts (`hq bot create --no-auto-approve`). */
|
|
84
|
+
autoApprove?: boolean;
|
|
82
85
|
}
|
|
83
86
|
/** The model the HQ Fleet Runtime asks for when a Claude bot names none. */
|
|
84
87
|
export declare const FLEET_CLAUDE_DEFAULT_MODEL = "claude-opus-5-5";
|
|
@@ -99,8 +102,34 @@ export interface FleetLocalBootstrapDeps {
|
|
|
99
102
|
start?: boolean;
|
|
100
103
|
python?: FleetPython;
|
|
101
104
|
}
|
|
105
|
+
interface SharedFleetRuntime {
|
|
106
|
+
id: string;
|
|
107
|
+
root: string;
|
|
108
|
+
runtimeDir: string;
|
|
109
|
+
sourcesDir: string;
|
|
110
|
+
/** Extracted runtime source tree (`sources/runtime`, or `s/r` on Windows). */
|
|
111
|
+
runtimeSource: string;
|
|
112
|
+
/** Extracted distribution tree (`sources/distribution`, or `s/d` on Windows). */
|
|
113
|
+
distributionSource: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Hex characters of the runtime identity used for the shared runtime
|
|
117
|
+
* directory name on Windows. The full 64-character identity plus
|
|
118
|
+
* `sources\runtime` pushed the runtime's deepest files past MAX_PATH (260).
|
|
119
|
+
*/
|
|
120
|
+
export declare const WINDOWS_RUNTIME_DIR_ID_LENGTH = 16;
|
|
121
|
+
/** Longest path Windows accepts when long paths are turned off (MAX_PATH minus the terminator). */
|
|
122
|
+
export declare const WINDOWS_MAX_PATH_CHARS = 259;
|
|
123
|
+
/**
|
|
124
|
+
* Where a shared runtime lives. macOS and Linux keep the original layout
|
|
125
|
+
* (`runtimes/<64 hex>/{runtime,sources/runtime,sources/distribution}`).
|
|
126
|
+
* Windows uses `runtimes/<16 hex>/{v,s\r,s\d}` so deep runtime files stay
|
|
127
|
+
* under MAX_PATH; `ready.json` records the full identity.
|
|
128
|
+
*/
|
|
129
|
+
export declare function sharedRuntimeLayout(root: string, id: string, platform: NodeJS.Platform): SharedFleetRuntime;
|
|
102
130
|
type FleetLocalProfileDeps = Pick<FleetLocalBootstrapDeps, "fleetHome" | "platform"> & {
|
|
103
131
|
runtimeRoot?: string;
|
|
132
|
+
runtime?: SharedFleetRuntime;
|
|
104
133
|
};
|
|
105
134
|
export interface FleetLocalBootstrapResult {
|
|
106
135
|
profile: string;
|
|
@@ -136,9 +165,22 @@ export interface FleetLocalRemovalResult {
|
|
|
136
165
|
credentialsRemoved: number;
|
|
137
166
|
}
|
|
138
167
|
export declare const HQ_FLEET_LOCAL_RELEASE: FleetLocalReleasePin;
|
|
168
|
+
/** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
|
|
139
169
|
export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEASJoapvo0WMTg+Wu3Jvt0nzlXLOBEr+G1aJRi0JkEb4A=\n-----END PUBLIC KEY-----\n";
|
|
170
|
+
/** hq-fleet-local trust/release-public-v2.pem (added 2026-11-01; signs v0.4.18). */
|
|
171
|
+
export declare const HQ_FLEET_LOCAL_PUBLIC_KEY_V2 = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAmgUi7pKTJUmf3yzwcuH65wSvn1NyJrDfZHZUX08sGN8=\n-----END PUBLIC KEY-----\n";
|
|
172
|
+
/** Every key a release manifest may be signed with. Any one must verify it. */
|
|
173
|
+
export declare const HQ_FLEET_LOCAL_PUBLIC_KEYS: readonly string[];
|
|
174
|
+
/** True when any trusted Ed25519 key verifies the manifest signature. */
|
|
175
|
+
export declare function fleetLocalManifestSignatureValid(manifest: Buffer, signature: Buffer, publicKey?: string | readonly string[]): boolean;
|
|
140
176
|
export declare function fleetHome(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
141
|
-
export declare function verifyFleetLocalRelease(files: FleetLocalReleaseFiles, release?: FleetLocalReleasePin, publicKey?: string): void;
|
|
177
|
+
export declare function verifyFleetLocalRelease(files: FleetLocalReleaseFiles, release?: FleetLocalReleasePin, publicKey?: string | readonly string[]): void;
|
|
178
|
+
/**
|
|
179
|
+
* Warning text when the signed manifest asks for a newer hq CLI than this
|
|
180
|
+
* one, or null. Advisory only: the pinned runtime is still installed, since
|
|
181
|
+
* blocking would strand every user until that CLI version is published.
|
|
182
|
+
*/
|
|
183
|
+
export declare function fleetLocalCliCompatibilityWarning(manifest: Buffer, release?: FleetLocalReleasePin, cliVersion?: string): string | null;
|
|
142
184
|
/** `gh auth token` fallback for the release download token. */
|
|
143
185
|
export declare const FLEET_RELEASE_TOKEN_TIMEOUT_MS = 15000;
|
|
144
186
|
export declare function resolveFleetReleaseToken(env?: NodeJS.ProcessEnv, run?: FleetLocalExec): string;
|
|
@@ -149,6 +191,15 @@ export declare const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS: number;
|
|
|
149
191
|
export declare function installPinnedFleetLocalDistribution(deps?: FleetLocalInstallDeps): Promise<FleetLocalInstallation>;
|
|
150
192
|
/** The process id the bot's gateway recorded, when its file reads; the caller checks whether it is alive. */
|
|
151
193
|
export declare function fleetLocalGatewayPid(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome">): number | null;
|
|
194
|
+
/**
|
|
195
|
+
* Whether the bot's HQ Fleet gateway is installed as a login service. The
|
|
196
|
+
* runtime registers it with launchd as `ai.hq-fleet.gateway-<profile>`; null
|
|
197
|
+
* on platforms where hq-cli does not know the service layout.
|
|
198
|
+
*/
|
|
199
|
+
export declare function fleetLocalServiceInstalled(agentUid: string, deps?: {
|
|
200
|
+
platform?: NodeJS.Platform;
|
|
201
|
+
home?: string;
|
|
202
|
+
}): boolean | null;
|
|
152
203
|
export declare function fleetLocalProfileNeedsAdoption(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome" | "platform"> & {
|
|
153
204
|
release?: FleetLocalReleasePin;
|
|
154
205
|
}): boolean;
|
|
@@ -172,6 +223,14 @@ export declare function writeFleetLocalSoul(agentUid: string, systemPrompt: stri
|
|
|
172
223
|
export declare function scanFleetLocalInstructions(agentUid: string, text: string, deps?: FleetLocalControlDeps): string[];
|
|
173
224
|
/** Checks the instructions with the runtime's scanner, then makes them the bot's persona. */
|
|
174
225
|
export declare function refreshFleetLocalInstructions(agentUid: string, systemPrompt: string, deps?: FleetLocalControlDeps): string;
|
|
226
|
+
/** Last few non-empty stderr lines of a failed child process, trimmed for an error message. */
|
|
227
|
+
export declare function stderrTail(error: unknown): string;
|
|
228
|
+
/**
|
|
229
|
+
* Message for a Windows extraction that cannot fit under MAX_PATH, or null
|
|
230
|
+
* when it fits or long paths are on. `longest` is the longest member name in
|
|
231
|
+
* the archive; members land at `<destination>\<name>`.
|
|
232
|
+
*/
|
|
233
|
+
export declare function windowsExtractionPathProblem(destination: string, longest: number, longPathsEnabled: number | null): string | null;
|
|
175
234
|
/** Moves a fresh machine credential into the bot's secure store and deletes the file. */
|
|
176
235
|
export declare function importFleetLocalMachineCredential(agentUid: string, machineCredentials: string, deps?: FleetLocalControlDeps): void;
|
|
177
236
|
export declare function bootstrapFleetLocalBot(input: FleetLocalProfileInput & {
|
|
@@ -6,6 +6,8 @@ import * as path from "node:path";
|
|
|
6
6
|
import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
|
|
7
7
|
import { withBotLock } from "./lock.js";
|
|
8
8
|
import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
|
|
9
|
+
import semver from "semver";
|
|
10
|
+
import { CLI_VERSION } from "../../cli-version.js";
|
|
9
11
|
import { ensureCognitoToken } from "../../utils/cognito-session.js";
|
|
10
12
|
import { vaultApiFetch } from "../../utils/vault-api.js";
|
|
11
13
|
/** The model the HQ Fleet Runtime asks for when a Claude bot names none. */
|
|
@@ -34,22 +36,84 @@ export function fleetProviderForRuntime(runtime, model) {
|
|
|
34
36
|
export function fleetProviderNeedsSignIn(provider) {
|
|
35
37
|
return provider.id !== "anthropic";
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Hex characters of the runtime identity used for the shared runtime
|
|
41
|
+
* directory name on Windows. The full 64-character identity plus
|
|
42
|
+
* `sources\runtime` pushed the runtime's deepest files past MAX_PATH (260).
|
|
43
|
+
*/
|
|
44
|
+
export const WINDOWS_RUNTIME_DIR_ID_LENGTH = 16;
|
|
45
|
+
/** Longest path Windows accepts when long paths are turned off (MAX_PATH minus the terminator). */
|
|
46
|
+
export const WINDOWS_MAX_PATH_CHARS = 259;
|
|
47
|
+
function legacySharedRuntimeLayout(root, id) {
|
|
48
|
+
const shared = path.join(root, "runtimes", id);
|
|
49
|
+
const sources = path.join(shared, "sources");
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
root: shared,
|
|
53
|
+
runtimeDir: path.join(shared, "runtime"),
|
|
54
|
+
sourcesDir: sources,
|
|
55
|
+
runtimeSource: path.join(sources, "runtime"),
|
|
56
|
+
distributionSource: path.join(sources, "distribution"),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Where a shared runtime lives. macOS and Linux keep the original layout
|
|
61
|
+
* (`runtimes/<64 hex>/{runtime,sources/runtime,sources/distribution}`).
|
|
62
|
+
* Windows uses `runtimes/<16 hex>/{v,s\r,s\d}` so deep runtime files stay
|
|
63
|
+
* under MAX_PATH; `ready.json` records the full identity.
|
|
64
|
+
*/
|
|
65
|
+
export function sharedRuntimeLayout(root, id, platform) {
|
|
66
|
+
if (platform !== "win32")
|
|
67
|
+
return legacySharedRuntimeLayout(root, id);
|
|
68
|
+
const shared = path.join(root, "runtimes", id.slice(0, WINDOWS_RUNTIME_DIR_ID_LENGTH));
|
|
69
|
+
const sources = path.join(shared, "s");
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
root: shared,
|
|
73
|
+
runtimeDir: path.join(shared, "v"),
|
|
74
|
+
sourcesDir: sources,
|
|
75
|
+
runtimeSource: path.join(sources, "r"),
|
|
76
|
+
distributionSource: path.join(sources, "d"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
37
79
|
export const HQ_FLEET_LOCAL_RELEASE = {
|
|
38
80
|
repository: "indigoai-us/hq-fleet-local",
|
|
39
|
-
tag: "v0.4.
|
|
40
|
-
version: "0.4.
|
|
41
|
-
artifact: "hq-fleet-local-0.4.
|
|
42
|
-
artifactSha256: "
|
|
81
|
+
tag: "v0.4.18",
|
|
82
|
+
version: "0.4.18",
|
|
83
|
+
artifact: "hq-fleet-local-0.4.18.zip",
|
|
84
|
+
artifactSha256: "194ae7da24d02205061b18172a84c972754bd0607ed3b4c32af7ac10d73904a7",
|
|
43
85
|
runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
|
|
44
86
|
runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
|
|
45
|
-
manifestSha256: "
|
|
46
|
-
signatureSha256: "
|
|
87
|
+
manifestSha256: "ce14449198a0f5d8558ecc617c19551b7a2a31dba5bd563501a93d2ee58e5ff0",
|
|
88
|
+
signatureSha256: "ca7dac6bcc2d12f6e304d1940e7b00243f9c306e7f0a72fd4aa236b7778d3a93",
|
|
47
89
|
runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
|
|
48
90
|
};
|
|
91
|
+
/** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
|
|
49
92
|
export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
50
93
|
MCowBQYDK2VwAyEASJoapvo0WMTg+Wu3Jvt0nzlXLOBEr+G1aJRi0JkEb4A=
|
|
51
94
|
-----END PUBLIC KEY-----
|
|
52
95
|
`;
|
|
96
|
+
/** hq-fleet-local trust/release-public-v2.pem (added 2026-11-01; signs v0.4.18). */
|
|
97
|
+
export const HQ_FLEET_LOCAL_PUBLIC_KEY_V2 = `-----BEGIN PUBLIC KEY-----
|
|
98
|
+
MCowBQYDK2VwAyEAmgUi7pKTJUmf3yzwcuH65wSvn1NyJrDfZHZUX08sGN8=
|
|
99
|
+
-----END PUBLIC KEY-----
|
|
100
|
+
`;
|
|
101
|
+
/** Every key a release manifest may be signed with. Any one must verify it. */
|
|
102
|
+
export const HQ_FLEET_LOCAL_PUBLIC_KEYS = [HQ_FLEET_LOCAL_PUBLIC_KEY, HQ_FLEET_LOCAL_PUBLIC_KEY_V2];
|
|
103
|
+
function publicKeyList(publicKey) {
|
|
104
|
+
return typeof publicKey === "string" ? [publicKey] : publicKey;
|
|
105
|
+
}
|
|
106
|
+
/** True when any trusted Ed25519 key verifies the manifest signature. */
|
|
107
|
+
export function fleetLocalManifestSignatureValid(manifest, signature, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEYS) {
|
|
108
|
+
return publicKeyList(publicKey).some((key) => {
|
|
109
|
+
try {
|
|
110
|
+
return crypto.verify(null, manifest, crypto.createPublicKey(key), signature);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
53
117
|
function digest(value) {
|
|
54
118
|
return crypto.createHash("sha256").update(value).digest("hex");
|
|
55
119
|
}
|
|
@@ -68,7 +132,7 @@ function engineMetadata(release) {
|
|
|
68
132
|
export function fleetHome(home = os.homedir(), env = process.env) {
|
|
69
133
|
return env.HQ_FLEET_HOME?.trim() || path.join(home, ".hq-fleet");
|
|
70
134
|
}
|
|
71
|
-
export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE, publicKey =
|
|
135
|
+
export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEYS) {
|
|
72
136
|
if (digest(files.archive) !== release.artifactSha256) {
|
|
73
137
|
throw expectedError(`HQ Fleet Local ${release.version} archive hash did not match the release pin.`);
|
|
74
138
|
}
|
|
@@ -100,11 +164,34 @@ export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE,
|
|
|
100
164
|
if (signature.algorithm !== "ed25519" || typeof signature.signature !== "string") {
|
|
101
165
|
throw expectedError(`HQ Fleet Local ${release.version} signature record is invalid.`);
|
|
102
166
|
}
|
|
103
|
-
const valid =
|
|
167
|
+
const valid = fleetLocalManifestSignatureValid(files.manifest, Buffer.from(signature.signature, "base64"), publicKey);
|
|
104
168
|
if (!valid) {
|
|
105
169
|
throw expectedError(`HQ Fleet Local ${release.version} manifest signature is invalid.`);
|
|
106
170
|
}
|
|
107
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Warning text when the signed manifest asks for a newer hq CLI than this
|
|
174
|
+
* one, or null. Advisory only: the pinned runtime is still installed, since
|
|
175
|
+
* blocking would strand every user until that CLI version is published.
|
|
176
|
+
*/
|
|
177
|
+
export function fleetLocalCliCompatibilityWarning(manifest, release = HQ_FLEET_LOCAL_RELEASE, cliVersion = CLI_VERSION) {
|
|
178
|
+
let minimum;
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(manifest.toString("utf8"));
|
|
181
|
+
minimum = parsed.compatibility?.minimum_hq_cli_version;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (typeof minimum !== "string")
|
|
187
|
+
return null;
|
|
188
|
+
const required = semver.valid(semver.coerce(minimum));
|
|
189
|
+
const current = semver.valid(semver.coerce(cliVersion));
|
|
190
|
+
if (!required || !current || semver.gte(current, required))
|
|
191
|
+
return null;
|
|
192
|
+
return `HQ Fleet Local ${release.version} asks for hq CLI ${minimum} or newer; this is ${cliVersion}. ` +
|
|
193
|
+
"Update with `npm install -g @indigoai-us/hq-cli` if the bot misbehaves.";
|
|
194
|
+
}
|
|
108
195
|
/** `gh auth token` fallback for the release download token. */
|
|
109
196
|
export const FLEET_RELEASE_TOKEN_TIMEOUT_MS = 15_000;
|
|
110
197
|
export function resolveFleetReleaseToken(env = process.env, run = execFileSync) {
|
|
@@ -261,7 +348,7 @@ function installFiles(target, files, release, publicKey) {
|
|
|
261
348
|
fs.writeFileSync(path.join(staging, release.runtimeArtifact), files.runtimeArchive, { mode: 0o644 });
|
|
262
349
|
fs.writeFileSync(path.join(staging, "release-manifest.json"), files.manifest, { mode: 0o644 });
|
|
263
350
|
fs.writeFileSync(path.join(staging, "release-manifest.sig"), files.signature, { mode: 0o644 });
|
|
264
|
-
fs.writeFileSync(path.join(staging, "release-public.pem"), publicKey, { mode: 0o644 });
|
|
351
|
+
fs.writeFileSync(path.join(staging, "release-public.pem"), publicKeyList(publicKey).join(""), { mode: 0o644 });
|
|
265
352
|
fs.writeFileSync(path.join(staging, "install.json"), `${JSON.stringify({ schemaVersion: 1, release, engine: engineMetadata(release) }, null, 2)}\n`, { mode: 0o644 });
|
|
266
353
|
if (fs.existsSync(target))
|
|
267
354
|
fs.renameSync(target, backup);
|
|
@@ -301,12 +388,18 @@ function cachedRuntimeArchive(root, release) {
|
|
|
301
388
|
}
|
|
302
389
|
export async function installPinnedFleetLocalDistribution(deps = {}) {
|
|
303
390
|
const release = deps.release ?? HQ_FLEET_LOCAL_RELEASE;
|
|
304
|
-
const publicKey = deps.publicKey ??
|
|
391
|
+
const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEYS;
|
|
305
392
|
const root = deps.fleetHome ?? fleetHome();
|
|
306
393
|
const dir = path.join(root, "distributions", "hq-fleet-local", release.version);
|
|
307
394
|
const current = installedFiles(dir, release);
|
|
395
|
+
const warnCompatibility = (manifest) => {
|
|
396
|
+
const warning = fleetLocalCliCompatibilityWarning(manifest, release);
|
|
397
|
+
if (warning)
|
|
398
|
+
process.stderr.write(`Warning: ${warning}\n`);
|
|
399
|
+
};
|
|
308
400
|
if (current) {
|
|
309
401
|
verifyFleetLocalRelease(current, release, publicKey);
|
|
402
|
+
warnCompatibility(current.manifest);
|
|
310
403
|
}
|
|
311
404
|
else {
|
|
312
405
|
const fetcher = deps.fetch ?? fetch;
|
|
@@ -316,6 +409,7 @@ export async function installPinnedFleetLocalDistribution(deps = {}) {
|
|
|
316
409
|
? await downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime)
|
|
317
410
|
: await downloadFleetLocalRelease(release, fetcher, (deps.token ?? resolveFleetReleaseToken)(), cachedRuntime);
|
|
318
411
|
verifyFleetLocalRelease(files, release, publicKey);
|
|
412
|
+
warnCompatibility(files.manifest);
|
|
319
413
|
installFiles(dir, files, release, publicKey);
|
|
320
414
|
}
|
|
321
415
|
return {
|
|
@@ -377,18 +471,19 @@ function venvExecutable(runtimeDir, name, platform) {
|
|
|
377
471
|
? path.join(runtimeDir, "Scripts", `${name}.exe`)
|
|
378
472
|
: path.join(runtimeDir, "bin", name);
|
|
379
473
|
}
|
|
380
|
-
function sharedRuntimeFromMarker(profileDir, root) {
|
|
474
|
+
function sharedRuntimeFromMarker(profileDir, root, platform = process.platform) {
|
|
381
475
|
try {
|
|
382
476
|
const marker = JSON.parse(fs.readFileSync(path.join(profileDir, "bootstrap.json"), "utf8"));
|
|
383
477
|
if (marker.schemaVersion !== 2 || typeof marker.runtimeId !== "string" || !/^[a-f0-9]{64}$/.test(marker.runtimeId))
|
|
384
478
|
return null;
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
root
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
479
|
+
const layout = sharedRuntimeLayout(root, marker.runtimeId, platform);
|
|
480
|
+
if (platform === "win32" && !fs.existsSync(path.join(layout.root, "ready.json"))) {
|
|
481
|
+
// Bots installed before the short Windows layout keep using their runtime.
|
|
482
|
+
const legacy = legacySharedRuntimeLayout(root, marker.runtimeId);
|
|
483
|
+
if (fs.existsSync(path.join(legacy.root, "ready.json")))
|
|
484
|
+
return legacy;
|
|
485
|
+
}
|
|
486
|
+
return layout;
|
|
392
487
|
}
|
|
393
488
|
catch {
|
|
394
489
|
return null;
|
|
@@ -426,7 +521,7 @@ function fleetLocalExecutables(agentUid, deps) {
|
|
|
426
521
|
const root = deps.fleetHome ?? fleetHome();
|
|
427
522
|
const platform = deps.platform ?? process.platform;
|
|
428
523
|
const profileDir = profileRoot(agentUid, root);
|
|
429
|
-
const shared = sharedRuntimeFromMarker(profileDir, root);
|
|
524
|
+
const shared = sharedRuntimeFromMarker(profileDir, root, platform);
|
|
430
525
|
const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
|
|
431
526
|
const sourcesDir = shared?.sourcesDir ?? path.join(profileDir, "sources");
|
|
432
527
|
const profile = path.join(profileDir, "profile.json");
|
|
@@ -440,7 +535,8 @@ function fleetLocalExecutables(agentUid, deps) {
|
|
|
440
535
|
function runJson(run, executable, args, options, label) {
|
|
441
536
|
let output;
|
|
442
537
|
try {
|
|
443
|
-
|
|
538
|
+
// windowsHide last: a helper, uv or Python child never opens a console window on Windows.
|
|
539
|
+
output = run(executable, args, { encoding: "utf8", ...options, windowsHide: true });
|
|
444
540
|
}
|
|
445
541
|
catch {
|
|
446
542
|
throw expectedError(`HQ Fleet Local ${label} failed.`);
|
|
@@ -468,6 +564,31 @@ export function fleetLocalGatewayPid(agentUid, deps = {}) {
|
|
|
468
564
|
return null;
|
|
469
565
|
}
|
|
470
566
|
}
|
|
567
|
+
/** The first hq-fleet-local release whose profile schema accepts `autoApprove`; older ones reject unknown fields. */
|
|
568
|
+
const AUTO_APPROVE_RELEASE = [0, 4, 18];
|
|
569
|
+
function releaseKnowsAutoApprove(release) {
|
|
570
|
+
const parts = release.version.split(".").map((part) => Number.parseInt(part, 10));
|
|
571
|
+
for (let i = 0; i < AUTO_APPROVE_RELEASE.length; i += 1) {
|
|
572
|
+
const have = Number.isFinite(parts[i]) ? parts[i] : 0;
|
|
573
|
+
if (have !== AUTO_APPROVE_RELEASE[i])
|
|
574
|
+
return have > AUTO_APPROVE_RELEASE[i];
|
|
575
|
+
}
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Whether the bot's HQ Fleet gateway is installed as a login service. The
|
|
580
|
+
* runtime registers it with launchd as `ai.hq-fleet.gateway-<profile>`; null
|
|
581
|
+
* on platforms where hq-cli does not know the service layout.
|
|
582
|
+
*/
|
|
583
|
+
export function fleetLocalServiceInstalled(agentUid, deps = {}) {
|
|
584
|
+
if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
|
|
585
|
+
return null;
|
|
586
|
+
const platform = deps.platform ?? process.platform;
|
|
587
|
+
if (platform !== "darwin")
|
|
588
|
+
return null;
|
|
589
|
+
const home = deps.home ?? os.homedir();
|
|
590
|
+
return fs.existsSync(path.join(home, "Library", "LaunchAgents", `ai.hq-fleet.gateway-${profileName(agentUid)}.plist`));
|
|
591
|
+
}
|
|
471
592
|
export function fleetLocalProfileNeedsAdoption(agentUid, deps = {}) {
|
|
472
593
|
if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
|
|
473
594
|
return true;
|
|
@@ -484,12 +605,12 @@ export function fleetLocalProfileNeedsAdoption(agentUid, deps = {}) {
|
|
|
484
605
|
if (!entries.includes(canonicalName))
|
|
485
606
|
return true;
|
|
486
607
|
const profileDir = path.join(profiles, canonicalName);
|
|
487
|
-
const
|
|
608
|
+
const platform = deps.platform ?? process.platform;
|
|
609
|
+
const shared = sharedRuntimeFromMarker(profileDir, root, platform);
|
|
488
610
|
if (!shared)
|
|
489
611
|
return true;
|
|
490
612
|
if (!profileEngineMatchesRelease(profileDir, deps.release ?? HQ_FLEET_LOCAL_RELEASE))
|
|
491
613
|
return true;
|
|
492
|
-
const platform = deps.platform ?? process.platform;
|
|
493
614
|
if (!["hq-fleet", "hq-fleet-local", "python"].every((name) => fs.existsSync(venvExecutable(shared.runtimeDir, name, platform))))
|
|
494
615
|
return true;
|
|
495
616
|
if (hasEmbeddedRuntime(profileDir, platform) || fs.existsSync(path.join(profileDir, "sources")))
|
|
@@ -639,7 +760,7 @@ export function removeFleetLocalBot(agentUid, deps = {}) {
|
|
|
639
760
|
}
|
|
640
761
|
const platform = deps.platform ?? process.platform;
|
|
641
762
|
const legacyRuntime = path.join(profileDir, "runtime");
|
|
642
|
-
const shared = sharedRuntimeFromMarker(profileDir, deps.fleetHome ?? fleetHome());
|
|
763
|
+
const shared = sharedRuntimeFromMarker(profileDir, deps.fleetHome ?? fleetHome(), platform);
|
|
643
764
|
const installedRuntime = shared?.runtimeDir ?? legacyRuntime;
|
|
644
765
|
if (!["hq-fleet", "hq-fleet-local"].every((name) => fs.existsSync(venvExecutable(installedRuntime, name, platform)))) {
|
|
645
766
|
fs.rmSync(profileDir, { recursive: true, force: true });
|
|
@@ -757,9 +878,11 @@ export function writeFleetLocalProfile(input, deps = {}) {
|
|
|
757
878
|
}
|
|
758
879
|
const root = deps.fleetHome ?? fleetHome();
|
|
759
880
|
const dir = profileRoot(input.agentUid, root);
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
const
|
|
881
|
+
const layout = deps.runtime ??
|
|
882
|
+
(deps.runtimeRoot ? undefined : sharedRuntimeFromMarker(dir, root, deps.platform ?? process.platform) ?? undefined);
|
|
883
|
+
const runtimeRoot = deps.runtimeRoot ?? dir;
|
|
884
|
+
const runtimeDir = layout?.runtimeDir ?? path.join(runtimeRoot, "runtime");
|
|
885
|
+
const distributionSource = layout?.distributionSource ?? path.join(runtimeRoot, "sources", "distribution");
|
|
763
886
|
const destination = path.join(dir, "profile.json");
|
|
764
887
|
const temporary = path.join(dir, `.profile-${process.pid}-${crypto.randomUUID()}`);
|
|
765
888
|
const profile = {
|
|
@@ -767,12 +890,13 @@ export function writeFleetLocalProfile(input, deps = {}) {
|
|
|
767
890
|
botId: input.agentUid,
|
|
768
891
|
ownerUid: input.ownerUid,
|
|
769
892
|
...(input.ownerCompanies?.length ? { ownerCompanies: [...new Set(input.ownerCompanies)].sort() } : {}),
|
|
893
|
+
...(input.autoApprove === false && releaseKnowsAutoApprove(input.installation.release) ? { autoApprove: false } : {}),
|
|
770
894
|
provider: input.provider ?? { id: "custom:personal-chatgpt", model: "gpt-5.6-sol" },
|
|
771
895
|
hqRoot: path.resolve(input.hqRoot),
|
|
772
896
|
profileDir: path.resolve(dir),
|
|
773
897
|
runtime: { commit: input.installation.release.runtimeCommit, path: path.resolve(runtimeDir) },
|
|
774
898
|
paths: {
|
|
775
|
-
pluginSource: path.resolve(path.join(
|
|
899
|
+
pluginSource: path.resolve(path.join(distributionSource, "src", "hq_fleet_local", "plugins", "hqdm")),
|
|
776
900
|
statusFile: path.resolve(path.join(input.botDir, "status.json")),
|
|
777
901
|
logsDir: path.resolve(path.join(input.botDir, "logs")),
|
|
778
902
|
},
|
|
@@ -833,11 +957,11 @@ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
|
|
|
833
957
|
const run = deps.run ?? execFileSync;
|
|
834
958
|
const root = deps.fleetHome ?? fleetHome();
|
|
835
959
|
const profileDir = profileRoot(agentUid, root);
|
|
836
|
-
const
|
|
960
|
+
const platform = deps.platform ?? process.platform;
|
|
961
|
+
const shared = sharedRuntimeFromMarker(profileDir, root, platform);
|
|
837
962
|
const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
|
|
838
|
-
const
|
|
839
|
-
const
|
|
840
|
-
const source = path.join(sourcesDir, "runtime");
|
|
963
|
+
const python = venvExecutable(runtimeDir, "python", platform);
|
|
964
|
+
const source = shared?.runtimeSource ?? path.join(profileDir, "sources", "runtime");
|
|
841
965
|
if (!fs.existsSync(python) || !fs.existsSync(source)) {
|
|
842
966
|
throw expectedError("HQ Fleet Local is not installed for this bot, so its instructions could not be checked.");
|
|
843
967
|
}
|
|
@@ -867,14 +991,83 @@ export function refreshFleetLocalInstructions(agentUid, systemPrompt, deps = {})
|
|
|
867
991
|
}
|
|
868
992
|
return writeFleetLocalSoul(agentUid, systemPrompt, deps);
|
|
869
993
|
}
|
|
994
|
+
const STDERR_TAIL_LINES = 6;
|
|
995
|
+
const STDERR_TAIL_CHARS = 800;
|
|
996
|
+
/** Last few non-empty stderr lines of a failed child process, trimmed for an error message. */
|
|
997
|
+
export function stderrTail(error) {
|
|
998
|
+
const raw = error?.stderr;
|
|
999
|
+
const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : typeof raw === "string" ? raw : "";
|
|
1000
|
+
const lines = text.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
1001
|
+
const tail = lines.slice(-STDERR_TAIL_LINES).join("\n");
|
|
1002
|
+
return tail.length > STDERR_TAIL_CHARS ? `...${tail.slice(-STDERR_TAIL_CHARS)}` : tail;
|
|
1003
|
+
}
|
|
870
1004
|
function checkedRun(run, executable, args, options, label) {
|
|
1005
|
+
// Keep stdout quiet but capture stderr, so a failure names its cause
|
|
1006
|
+
// (for example a path-too-long error during extraction).
|
|
1007
|
+
const stdio = options.stdio === "ignore" ? ["ignore", "ignore", "pipe"] : options.stdio;
|
|
871
1008
|
try {
|
|
872
|
-
run(executable, args, {
|
|
1009
|
+
run(executable, args, {
|
|
1010
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
1011
|
+
...options,
|
|
1012
|
+
...(stdio === undefined ? {} : { stdio }),
|
|
1013
|
+
// Last, so no caller can turn it off: no console window flashes on Windows.
|
|
1014
|
+
windowsHide: true,
|
|
1015
|
+
});
|
|
873
1016
|
}
|
|
874
|
-
catch {
|
|
875
|
-
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
const tail = stderrTail(error);
|
|
1019
|
+
throw expectedError(`HQ Fleet Local ${label} failed.${tail ? `\n${tail}` : ""}`);
|
|
876
1020
|
}
|
|
877
1021
|
}
|
|
1022
|
+
const FLEET_TAR_PATH_PROBE = [
|
|
1023
|
+
"import json, sys, tarfile",
|
|
1024
|
+
"longest = 0",
|
|
1025
|
+
"with tarfile.open(sys.argv[1]) as archive:",
|
|
1026
|
+
" for member in archive:",
|
|
1027
|
+
" longest = max(longest, len(member.name.rstrip('/')))",
|
|
1028
|
+
"enabled = None",
|
|
1029
|
+
"try:",
|
|
1030
|
+
" import winreg",
|
|
1031
|
+
" key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SYSTEM\\CurrentControlSet\\Control\\FileSystem')",
|
|
1032
|
+
" enabled = int(winreg.QueryValueEx(key, 'LongPathsEnabled')[0])",
|
|
1033
|
+
"except Exception:",
|
|
1034
|
+
" pass",
|
|
1035
|
+
"print(json.dumps({'longest': longest, 'longPathsEnabled': enabled}))",
|
|
1036
|
+
].join("\n");
|
|
1037
|
+
/**
|
|
1038
|
+
* Message for a Windows extraction that cannot fit under MAX_PATH, or null
|
|
1039
|
+
* when it fits or long paths are on. `longest` is the longest member name in
|
|
1040
|
+
* the archive; members land at `<destination>\<name>`.
|
|
1041
|
+
*/
|
|
1042
|
+
export function windowsExtractionPathProblem(destination, longest, longPathsEnabled) {
|
|
1043
|
+
const needed = destination.length + 1 + longest;
|
|
1044
|
+
if (needed <= WINDOWS_MAX_PATH_CHARS || longPathsEnabled === 1)
|
|
1045
|
+
return null;
|
|
1046
|
+
return `The HQ Fleet Runtime cannot be extracted on this PC: its deepest file needs a ${needed}-character path under ${destination}, ` +
|
|
1047
|
+
`over the Windows limit of ${WINDOWS_MAX_PATH_CHARS}, and Windows long paths are turned off (LongPathsEnabled = ${longPathsEnabled ?? "unset"}). ` +
|
|
1048
|
+
"Ask an administrator to set HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem\\LongPathsEnabled to 1, or set HQ_FLEET_HOME to a shorter folder such as C:\\hqf, then retry.";
|
|
1049
|
+
}
|
|
1050
|
+
function assertWindowsExtractionFits(run, python, archive, destination) {
|
|
1051
|
+
let probe;
|
|
1052
|
+
try {
|
|
1053
|
+
const output = run(python, ["-I", "-c", FLEET_TAR_PATH_PROBE, archive], {
|
|
1054
|
+
windowsHide: true,
|
|
1055
|
+
encoding: "utf8",
|
|
1056
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1057
|
+
maxBuffer: 1024 * 1024,
|
|
1058
|
+
});
|
|
1059
|
+
probe = JSON.parse((Buffer.isBuffer(output) ? output.toString("utf8") : output).trim());
|
|
1060
|
+
}
|
|
1061
|
+
catch {
|
|
1062
|
+
return; // The probe is advisory; extraction reports its own error.
|
|
1063
|
+
}
|
|
1064
|
+
if (typeof probe?.longest !== "number")
|
|
1065
|
+
return;
|
|
1066
|
+
const enabled = typeof probe.longPathsEnabled === "number" ? probe.longPathsEnabled : null;
|
|
1067
|
+
const problem = windowsExtractionPathProblem(destination, probe.longest, enabled);
|
|
1068
|
+
if (problem)
|
|
1069
|
+
throw expectedError(problem);
|
|
1070
|
+
}
|
|
878
1071
|
/** Moves a fresh machine credential into the bot's secure store and deletes the file. */
|
|
879
1072
|
export function importFleetLocalMachineCredential(agentUid, machineCredentials, deps = {}) {
|
|
880
1073
|
const run = deps.run ?? execFileSync;
|
|
@@ -902,25 +1095,37 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
|
|
|
902
1095
|
platform,
|
|
903
1096
|
extras: ["mcp", "anthropic"],
|
|
904
1097
|
})));
|
|
905
|
-
const
|
|
906
|
-
const
|
|
907
|
-
id: identity,
|
|
908
|
-
root: shared,
|
|
909
|
-
runtimeDir: path.join(shared, "runtime"),
|
|
910
|
-
sourcesDir: path.join(shared, "sources"),
|
|
911
|
-
});
|
|
1098
|
+
const layout = sharedRuntimeLayout(root, identity, platform);
|
|
1099
|
+
const shared = layout.root;
|
|
912
1100
|
return withBotLock(identity, async () => {
|
|
913
1101
|
const marker = path.join(shared, "ready.json");
|
|
914
|
-
const
|
|
915
|
-
const distributionSource = path.join(sources, "distribution");
|
|
916
|
-
const runtimeSource = path.join(sources, "runtime");
|
|
917
|
-
const runtimeDir = path.join(shared, "runtime");
|
|
1102
|
+
const { distributionSource, runtimeSource, runtimeDir } = layout;
|
|
918
1103
|
const executables = ["python", "hq-fleet", "hq-fleet-local"];
|
|
1104
|
+
if (platform === "win32") {
|
|
1105
|
+
const legacy = legacySharedRuntimeLayout(root, identity);
|
|
1106
|
+
if (fs.existsSync(path.join(legacy.root, "ready.json")) &&
|
|
1107
|
+
executables.every((name) => fs.existsSync(venvExecutable(legacy.runtimeDir, name, platform)))) {
|
|
1108
|
+
// A runtime installed before the short Windows layout keeps serving bots.
|
|
1109
|
+
return legacy;
|
|
1110
|
+
}
|
|
1111
|
+
// A half-extracted long-path runtime is never usable; clear it.
|
|
1112
|
+
fs.rmSync(legacy.root, { recursive: true, force: true });
|
|
1113
|
+
}
|
|
919
1114
|
if (fs.existsSync(marker)) {
|
|
1115
|
+
let recordedId;
|
|
1116
|
+
try {
|
|
1117
|
+
recordedId = JSON.parse(fs.readFileSync(marker, "utf8")).runtimeId;
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
recordedId = undefined;
|
|
1121
|
+
}
|
|
1122
|
+
if (recordedId !== undefined && recordedId !== identity) {
|
|
1123
|
+
throw expectedError(`The shared HQ Fleet Runtime folder ${shared} belongs to a different runtime. Remove it and retry.`);
|
|
1124
|
+
}
|
|
920
1125
|
if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
|
|
921
1126
|
throw expectedError("The shared HQ Fleet Runtime is incomplete. Reinstall it before creating another bot.");
|
|
922
1127
|
}
|
|
923
|
-
return
|
|
1128
|
+
return layout;
|
|
924
1129
|
}
|
|
925
1130
|
fs.rmSync(shared, { recursive: true, force: true });
|
|
926
1131
|
fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
|
|
@@ -933,6 +1138,8 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
|
|
|
933
1138
|
installation.archive,
|
|
934
1139
|
distributionSource,
|
|
935
1140
|
], { stdio: "ignore" }, "distribution extraction");
|
|
1141
|
+
if (platform === "win32")
|
|
1142
|
+
assertWindowsExtractionFits(run, python.python, installation.runtimeArchive, runtimeSource);
|
|
936
1143
|
checkedRun(run, python.python, [
|
|
937
1144
|
"-m",
|
|
938
1145
|
"tarfile",
|
|
@@ -961,8 +1168,8 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
|
|
|
961
1168
|
if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
|
|
962
1169
|
throw expectedError("HQ Fleet Local installed without its runtime executables.");
|
|
963
1170
|
}
|
|
964
|
-
fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: installation.engine })}\n`, { mode: 0o600 });
|
|
965
|
-
return
|
|
1171
|
+
fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, runtimeId: identity, engine: installation.engine })}\n`, { mode: 0o600 });
|
|
1172
|
+
return layout;
|
|
966
1173
|
}, { root: path.join(root, "runtimes"), command: "HQ Fleet Runtime installation", waitMs: 20 * 60_000, staleMs: 2 * 60 * 60_000 });
|
|
967
1174
|
}
|
|
968
1175
|
function writeNativeProfileMarker(profileDir, shared, engine) {
|
|
@@ -988,7 +1195,7 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
|
|
|
988
1195
|
const { systemPrompt, machineCredentials, ...profileInput } = input;
|
|
989
1196
|
if (systemPrompt !== undefined)
|
|
990
1197
|
refreshFleetLocalInstructions(input.agentUid, systemPrompt, { fleetHome: root, platform, run });
|
|
991
|
-
const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform,
|
|
1198
|
+
const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform, runtime: shared });
|
|
992
1199
|
const helper = venvExecutable(shared.runtimeDir, "hq-fleet-local", platform);
|
|
993
1200
|
const runtime = venvExecutable(shared.runtimeDir, "hq-fleet", platform);
|
|
994
1201
|
if (!fs.existsSync(helper) || !fs.existsSync(runtime)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.191",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
|
|
115
115
|
},
|
|
116
116
|
"optionalDependencies": {
|
|
117
|
-
"@indigoai-us/hq-hook-win32-x64": "5.345.
|
|
118
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
119
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
120
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
121
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
117
|
+
"@indigoai-us/hq-hook-win32-x64": "5.345.191",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.191",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.191",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.191",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.191",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.191"
|
|
123
123
|
}
|
|
124
124
|
}
|