@indigoai-us/hq-cli 5.345.191 → 5.345.194
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +431 -0
- package/assets/bot-workers/setup/worker.yaml +91 -34
- package/dist/command-catalog.generated.d.ts +145 -19
- package/dist/command-catalog.generated.js +184 -21
- package/dist/commands/bot-jobs.d.ts +21 -18
- package/dist/commands/bot-jobs.js +127 -39
- package/dist/commands/bot.d.ts +4 -3
- package/dist/commands/bot.js +265 -140
- package/dist/commands/core-jobs-schedule.js +10 -0
- package/dist/commands/daemon.js +37 -0
- package/dist/commands/files.d.ts +41 -6
- package/dist/commands/files.js +108 -27
- package/dist/commands/pack-start-bot.d.ts +4 -6
- package/dist/commands/pack-start-bot.js +4 -8
- package/dist/commands/signals-run.d.ts +46 -0
- package/dist/commands/signals-run.js +180 -0
- package/dist/commands/signals.d.ts +1 -0
- package/dist/commands/signals.js +3 -0
- package/dist/commands/sources.d.ts +58 -0
- package/dist/commands/sources.js +294 -26
- package/dist/commands/staff/reset-test-account.d.ts +10 -0
- package/dist/commands/staff/reset-test-account.js +18 -2
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +35 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/api.d.ts +3 -2
- package/dist/lib/bot/config.d.ts +2 -0
- package/dist/lib/bot/fleet-cron.d.ts +121 -0
- package/dist/lib/bot/fleet-cron.js +319 -0
- package/dist/lib/bot/fleet-local.d.ts +93 -1
- package/dist/lib/bot/fleet-local.js +546 -34
- package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
- package/dist/lib/bot/fleet-owner-companies.js +30 -0
- package/dist/lib/bot/fleet-watch.d.ts +25 -0
- package/dist/lib/bot/fleet-watch.js +51 -0
- package/dist/lib/bot/index.d.ts +1 -1
- package/dist/lib/bot/index.js +1 -1
- package/dist/lib/bot/job-run.d.ts +5 -4
- package/dist/lib/bot/job-run.js +5 -4
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
- package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
- package/dist/lib/bot/local-jobs.d.ts +13 -2
- package/dist/lib/bot/local-jobs.js +17 -4
- package/dist/lib/bot/progress.d.ts +14 -0
- package/dist/lib/bot/progress.js +47 -1
- package/dist/lib/bot/run.d.ts +55 -2
- package/dist/lib/bot/run.js +192 -10
- package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
- package/dist/lib/bot/setup-tool-offer.js +16 -0
- package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
- package/dist/lib/bot/signals-engine-flag.js +30 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +29 -9
- package/dist/lib/daemon/legacy.d.ts +6 -0
- package/dist/lib/daemon/legacy.js +16 -10
- package/dist/lib/daemon/run.d.ts +1 -0
- package/dist/lib/daemon/run.js +10 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.d.ts +13 -1
- package/dist/lib/flag-registry.js +46 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
- package/dist/lib/hot-channels/adapters/attio.js +367 -29
- package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
- package/dist/lib/hot-channels/adapters/gmail.js +59 -2
- package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
- package/dist/lib/hot-channels/adapters/linear.js +71 -1
- package/dist/lib/hot-channels/adapters/meetings.js +4 -1
- package/dist/lib/hot-channels/adapters/slack.js +7 -3
- package/dist/lib/hot-channels/alert.js +1 -1
- package/dist/lib/hot-channels/contract.d.ts +11 -0
- package/dist/lib/hot-channels/contract.js +1 -0
- package/dist/lib/hot-channels/cursors.js +1 -1
- package/dist/lib/hot-channels/landing.d.ts +6 -2
- package/dist/lib/hot-channels/landing.js +13 -6
- package/dist/lib/hot-channels/ledger.js +2 -0
- package/dist/lib/hot-channels/loop.d.ts +6 -0
- package/dist/lib/hot-channels/loop.js +19 -4
- package/dist/lib/hot-channels/registry.js +9 -1
- package/dist/lib/hot-channels/run.d.ts +18 -0
- package/dist/lib/hot-channels/run.js +15 -4
- package/dist/lib/hot-channels/signals-pass.d.ts +34 -2
- package/dist/lib/hot-channels/signals-pass.js +149 -38
- package/dist/lib/hot-channels/signals-redo.d.ts +60 -0
- package/dist/lib/hot-channels/signals-redo.js +207 -0
- package/dist/lib/hot-channels/sync-lock.js +22 -6
- package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
- package/dist/lib/mesh/live/daemon/install.js +117 -30
- package/dist/lib/signals/adapters-catalog.d.ts +77 -0
- package/dist/lib/signals/adapters-catalog.js +194 -0
- package/dist/lib/signals/engine.d.ts +202 -0
- package/dist/lib/signals/engine.js +504 -0
- package/dist/lib/signals/failure-alert.d.ts +89 -0
- package/dist/lib/signals/failure-alert.js +175 -0
- package/dist/lib/signals/local-date.d.ts +3 -0
- package/dist/lib/signals/local-date.js +6 -0
- package/dist/lib/signals/projection.d.ts +42 -0
- package/dist/lib/signals/projection.js +67 -0
- package/dist/lib/signals/reply.d.ts +112 -0
- package/dist/lib/signals/reply.js +368 -0
- package/dist/lib/signals/run-lock.d.ts +36 -0
- package/dist/lib/signals/run-lock.js +148 -0
- package/dist/lib/signals/run-record.d.ts +24 -0
- package/dist/lib/signals/run-record.js +82 -0
- package/dist/lib/signals/sanitize.d.ts +105 -0
- package/dist/lib/signals/sanitize.js +439 -0
- package/dist/lib/signals/settings.d.ts +10 -0
- package/dist/lib/signals/settings.js +48 -0
- package/dist/lib/signals/steps/adapters.d.ts +14 -0
- package/dist/lib/signals/steps/adapters.js +156 -0
- package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
- package/dist/lib/signals/steps/entity-similarity.js +47 -0
- package/dist/lib/signals/steps/extract.d.ts +118 -0
- package/dist/lib/signals/steps/extract.js +793 -0
- package/dist/lib/signals/steps/garden.d.ts +24 -0
- package/dist/lib/signals/steps/garden.js +250 -0
- package/dist/lib/signals/steps/types.d.ts +117 -0
- package/dist/lib/signals/steps/types.js +4 -0
- package/dist/lib/signals/steps/upload.d.ts +43 -0
- package/dist/lib/signals/steps/upload.js +324 -0
- package/dist/lib/signals/write-guard.d.ts +63 -0
- package/dist/lib/signals/write-guard.js +287 -0
- package/dist/utils/resident-package-preload.js +16 -0
- package/dist/utils/vault-api.d.ts +1 -0
- package/dist/utils/vault-api.js +1 -1
- package/package.json +9 -9
- package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
- package/dist/lib/bot/fleet-default-flag.js +0 -127
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import * as crypto from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import * as zlib from "node:zlib";
|
|
6
7
|
import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
|
|
7
8
|
import { withBotLock } from "./lock.js";
|
|
8
9
|
import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
|
|
@@ -78,14 +79,14 @@ export function sharedRuntimeLayout(root, id, platform) {
|
|
|
78
79
|
}
|
|
79
80
|
export const HQ_FLEET_LOCAL_RELEASE = {
|
|
80
81
|
repository: "indigoai-us/hq-fleet-local",
|
|
81
|
-
tag: "v0.4.
|
|
82
|
-
version: "0.4.
|
|
83
|
-
artifact: "hq-fleet-local-0.4.
|
|
84
|
-
artifactSha256: "
|
|
82
|
+
tag: "v0.4.20",
|
|
83
|
+
version: "0.4.20",
|
|
84
|
+
artifact: "hq-fleet-local-0.4.20.zip",
|
|
85
|
+
artifactSha256: "5c49402f17408f3eaf99d27b3c2d4d0f59a24f33946aa8dfc3098a3d09e13798",
|
|
85
86
|
runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
|
|
86
87
|
runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
|
|
87
|
-
manifestSha256: "
|
|
88
|
-
signatureSha256: "
|
|
88
|
+
manifestSha256: "3cec47eb7f47af13e66d4ad29669d3c103e1ca452661f5eaad760243118ce214",
|
|
89
|
+
signatureSha256: "5504c3a956f36d148f3cdf2212a9b171d852098c82eb199c7c415ad3545e3b05",
|
|
89
90
|
runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
|
|
90
91
|
};
|
|
91
92
|
/** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
|
|
@@ -98,8 +99,13 @@ export const HQ_FLEET_LOCAL_PUBLIC_KEY_V2 = `-----BEGIN PUBLIC KEY-----
|
|
|
98
99
|
MCowBQYDK2VwAyEAmgUi7pKTJUmf3yzwcuH65wSvn1NyJrDfZHZUX08sGN8=
|
|
99
100
|
-----END PUBLIC KEY-----
|
|
100
101
|
`;
|
|
102
|
+
/** hq-fleet-local trust/release-public-v3.pem (added 2026-11-02; signs v0.4.20 and later). */
|
|
103
|
+
export const HQ_FLEET_LOCAL_PUBLIC_KEY_V3 = `-----BEGIN PUBLIC KEY-----
|
|
104
|
+
MCowBQYDK2VwAyEA2NUo6F38D4Kjxmy5qz5ytKRVRwocEvPk8lXqfwB2kS0=
|
|
105
|
+
-----END PUBLIC KEY-----
|
|
106
|
+
`;
|
|
101
107
|
/** Every key a release manifest may be signed with. Any one must verify it. */
|
|
102
|
-
export const HQ_FLEET_LOCAL_PUBLIC_KEYS = [HQ_FLEET_LOCAL_PUBLIC_KEY, HQ_FLEET_LOCAL_PUBLIC_KEY_V2];
|
|
108
|
+
export const HQ_FLEET_LOCAL_PUBLIC_KEYS = [HQ_FLEET_LOCAL_PUBLIC_KEY, HQ_FLEET_LOCAL_PUBLIC_KEY_V2, HQ_FLEET_LOCAL_PUBLIC_KEY_V3];
|
|
103
109
|
function publicKeyList(publicKey) {
|
|
104
110
|
return typeof publicKey === "string" ? [publicKey] : publicKey;
|
|
105
111
|
}
|
|
@@ -168,6 +174,176 @@ export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE,
|
|
|
168
174
|
if (!valid) {
|
|
169
175
|
throw expectedError(`HQ Fleet Local ${release.version} manifest signature is invalid.`);
|
|
170
176
|
}
|
|
177
|
+
if (typeof runtime.archive_sha256 !== "string" || runtime.archive_sha256 !== digest(files.runtimeArchive)) {
|
|
178
|
+
throw expectedError(`HQ Fleet Local ${release.version} runtime archive does not match the signed manifest.`);
|
|
179
|
+
}
|
|
180
|
+
verifyArchiveAgainstManifest(files, manifest, release);
|
|
181
|
+
}
|
|
182
|
+
const RELEASE_METADATA_ENTRIES = new Set(["release-manifest.json", "release-manifest.sig", "release-public.pem"]);
|
|
183
|
+
const MAX_ARCHIVE_ENTRIES = 20_000;
|
|
184
|
+
const MAX_ARCHIVE_ENTRY_BYTES = 64 * 1024 * 1024;
|
|
185
|
+
function verifyArchiveAgainstManifest(files, manifest, release) {
|
|
186
|
+
const payload = manifest.payload;
|
|
187
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length === 0) {
|
|
188
|
+
throw expectedError(`HQ Fleet Local ${release.version} manifest lists no payload files.`);
|
|
189
|
+
}
|
|
190
|
+
let entries;
|
|
191
|
+
try {
|
|
192
|
+
entries = readZipEntries(files.archive);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
throw expectedError(`HQ Fleet Local ${release.version} archive could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
196
|
+
}
|
|
197
|
+
for (const [name, expected] of Object.entries(payload)) {
|
|
198
|
+
const entry = entries.get(name);
|
|
199
|
+
const sha = expected && typeof expected === "object" ? expected.sha256 : undefined;
|
|
200
|
+
if (!entry || typeof sha !== "string" || digest(entry) !== sha) {
|
|
201
|
+
throw expectedError(`HQ Fleet Local ${release.version} archive file ${name} does not match the signed manifest.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const [name, entry] of entries) {
|
|
205
|
+
if (Object.prototype.hasOwnProperty.call(payload, name))
|
|
206
|
+
continue;
|
|
207
|
+
if (!RELEASE_METADATA_ENTRIES.has(name)) {
|
|
208
|
+
throw expectedError(`HQ Fleet Local ${release.version} archive has a file the signed manifest does not list: ${name}`);
|
|
209
|
+
}
|
|
210
|
+
if (name === "release-manifest.json" && !entry.equals(files.manifest)) {
|
|
211
|
+
throw expectedError(`HQ Fleet Local ${release.version} archive carries a different manifest.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Check an extracted distribution against the signed manifest: every payload
|
|
217
|
+
* file present with its hash, and nothing else but the release metadata.
|
|
218
|
+
*/
|
|
219
|
+
export function verifyExtractedDistribution(dir, manifest, version) {
|
|
220
|
+
let payload;
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(manifest.toString("utf8"));
|
|
223
|
+
if (!parsed.payload || typeof parsed.payload !== "object" || Array.isArray(parsed.payload))
|
|
224
|
+
throw new Error("no payload");
|
|
225
|
+
payload = parsed.payload;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
throw expectedError(`HQ Fleet Local ${version} manifest has no payload list: ${error instanceof Error ? error.message : String(error)}`);
|
|
229
|
+
}
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
const walk = (current, prefix) => {
|
|
232
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
233
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
234
|
+
const full = path.join(current, entry.name);
|
|
235
|
+
if (entry.isSymbolicLink())
|
|
236
|
+
throw expectedError(`HQ Fleet Local ${version} extracted a link the signed manifest does not list: ${relative}`);
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
walk(full, relative);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (Object.prototype.hasOwnProperty.call(payload, relative)) {
|
|
242
|
+
const expected = payload[relative];
|
|
243
|
+
const sha = expected && typeof expected === "object" ? expected.sha256 : undefined;
|
|
244
|
+
if (typeof sha !== "string" || digest(fs.readFileSync(full)) !== sha) {
|
|
245
|
+
throw expectedError(`HQ Fleet Local ${version} extracted file ${relative} does not match the signed manifest.`);
|
|
246
|
+
}
|
|
247
|
+
seen.add(relative);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (!RELEASE_METADATA_ENTRIES.has(relative)) {
|
|
251
|
+
throw expectedError(`HQ Fleet Local ${version} extracted a file the signed manifest does not list: ${relative}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
walk(dir, "");
|
|
256
|
+
for (const name of Object.keys(payload)) {
|
|
257
|
+
if (!seen.has(name))
|
|
258
|
+
throw expectedError(`HQ Fleet Local ${version} extracted distribution is missing ${name}.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Write a ZIP archive's files under `target` with the same reader that verified it. */
|
|
262
|
+
export function extractZipEntries(archive, target, version) {
|
|
263
|
+
let entries;
|
|
264
|
+
try {
|
|
265
|
+
entries = readZipEntries(archive);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
throw expectedError(`HQ Fleet Local ${version} archive could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
269
|
+
}
|
|
270
|
+
const root = path.resolve(target);
|
|
271
|
+
for (const [name, content] of entries) {
|
|
272
|
+
const parts = name.split("/");
|
|
273
|
+
if (name.includes("\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name) || parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
274
|
+
throw expectedError(`HQ Fleet Local ${version} archive has an unsafe file name: ${name}`);
|
|
275
|
+
}
|
|
276
|
+
const destination = path.join(root, ...parts);
|
|
277
|
+
if (!destination.startsWith(root + path.sep))
|
|
278
|
+
throw expectedError(`HQ Fleet Local ${version} archive has an unsafe file name: ${name}`);
|
|
279
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
280
|
+
fs.writeFileSync(destination, content, { mode: 0o644 });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** Every file in a ZIP archive by name (stored and deflated entries only; directories skipped). */
|
|
284
|
+
export function readZipEntries(archive) {
|
|
285
|
+
const eocdSignature = 0x06054b50;
|
|
286
|
+
let eocd = -1;
|
|
287
|
+
for (let at = archive.length - 22; at >= Math.max(0, archive.length - 22 - 0xffff); at -= 1) {
|
|
288
|
+
if (archive.readUInt32LE(at) === eocdSignature) {
|
|
289
|
+
eocd = at;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (eocd < 0)
|
|
294
|
+
throw new Error("no end of central directory");
|
|
295
|
+
const count = archive.readUInt16LE(eocd + 10);
|
|
296
|
+
const directorySize = archive.readUInt32LE(eocd + 12);
|
|
297
|
+
const directoryOffset = archive.readUInt32LE(eocd + 16);
|
|
298
|
+
if (count === 0xffff || directoryOffset === 0xffffffff || count > MAX_ARCHIVE_ENTRIES)
|
|
299
|
+
throw new Error("unsupported archive size");
|
|
300
|
+
if (directoryOffset + directorySize !== eocd)
|
|
301
|
+
throw new Error("central directory is not where the archive end says");
|
|
302
|
+
if (eocd >= 20 && archive.readUInt32LE(eocd - 20) === 0x07064b50)
|
|
303
|
+
throw new Error("zip64 archives are not supported");
|
|
304
|
+
if (count > 0 && archive.readUInt32LE(0) !== 0x04034b50)
|
|
305
|
+
throw new Error("data before the first entry");
|
|
306
|
+
const entries = new Map();
|
|
307
|
+
let at = directoryOffset;
|
|
308
|
+
for (let index = 0; index < count; index += 1) {
|
|
309
|
+
if (at + 46 > eocd || archive.readUInt32LE(at) !== 0x02014b50)
|
|
310
|
+
throw new Error("bad central directory entry");
|
|
311
|
+
const flags = archive.readUInt16LE(at + 8);
|
|
312
|
+
const method = archive.readUInt16LE(at + 10);
|
|
313
|
+
const compressedSize = archive.readUInt32LE(at + 20);
|
|
314
|
+
const size = archive.readUInt32LE(at + 24);
|
|
315
|
+
const nameLength = archive.readUInt16LE(at + 28);
|
|
316
|
+
const extraLength = archive.readUInt16LE(at + 30);
|
|
317
|
+
const commentLength = archive.readUInt16LE(at + 32);
|
|
318
|
+
const localOffset = archive.readUInt32LE(at + 42);
|
|
319
|
+
const name = archive.subarray(at + 46, at + 46 + nameLength).toString("utf8");
|
|
320
|
+
at += 46 + nameLength + extraLength + commentLength;
|
|
321
|
+
if (name.endsWith("/"))
|
|
322
|
+
continue;
|
|
323
|
+
if ((flags & 0x1) !== 0)
|
|
324
|
+
throw new Error(`encrypted entry ${name}`);
|
|
325
|
+
if (size > MAX_ARCHIVE_ENTRY_BYTES || compressedSize > MAX_ARCHIVE_ENTRY_BYTES)
|
|
326
|
+
throw new Error(`entry too large: ${name}`);
|
|
327
|
+
if (entries.has(name))
|
|
328
|
+
throw new Error(`duplicate entry ${name}`);
|
|
329
|
+
if (localOffset + 30 > directoryOffset || archive.readUInt32LE(localOffset) !== 0x04034b50)
|
|
330
|
+
throw new Error(`bad local header for ${name}`);
|
|
331
|
+
const dataStart = localOffset + 30 + archive.readUInt16LE(localOffset + 26) + archive.readUInt16LE(localOffset + 28);
|
|
332
|
+
if (dataStart + compressedSize > directoryOffset)
|
|
333
|
+
throw new Error(`entry out of range: ${name}`);
|
|
334
|
+
const data = archive.subarray(dataStart, dataStart + compressedSize);
|
|
335
|
+
let content;
|
|
336
|
+
if (method === 0)
|
|
337
|
+
content = Buffer.from(data);
|
|
338
|
+
else if (method === 8)
|
|
339
|
+
content = zlib.inflateRawSync(data, { maxOutputLength: MAX_ARCHIVE_ENTRY_BYTES });
|
|
340
|
+
else
|
|
341
|
+
throw new Error(`unsupported compression for ${name}`);
|
|
342
|
+
if (content.length !== size)
|
|
343
|
+
throw new Error(`size mismatch for ${name}`);
|
|
344
|
+
entries.set(name, content);
|
|
345
|
+
}
|
|
346
|
+
return entries;
|
|
171
347
|
}
|
|
172
348
|
/**
|
|
173
349
|
* Warning text when the signed manifest asks for a newer hq CLI than this
|
|
@@ -247,6 +423,196 @@ export async function fetchHqFleetReleaseLinks(release) {
|
|
|
247
423
|
}
|
|
248
424
|
return links.size > 0 ? links : null;
|
|
249
425
|
}
|
|
426
|
+
export const FLEET_LOCAL_LATEST_TIMEOUT_MS = 5_000;
|
|
427
|
+
export const FLEET_LOCAL_LATEST_CACHE_MS = 10 * 60_000;
|
|
428
|
+
const FLEET_LOCAL_LATEST_CACHE_FILE = "release-pointer.json";
|
|
429
|
+
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
430
|
+
const COMMIT_RE = /^[0-9a-f]{40}$/;
|
|
431
|
+
export function parseFleetLocalReleasePin(value) {
|
|
432
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
433
|
+
return null;
|
|
434
|
+
const record = value;
|
|
435
|
+
const keys = [
|
|
436
|
+
"repository",
|
|
437
|
+
"tag",
|
|
438
|
+
"version",
|
|
439
|
+
"artifact",
|
|
440
|
+
"artifactSha256",
|
|
441
|
+
"runtimeArtifact",
|
|
442
|
+
"runtimeArtifactSha256",
|
|
443
|
+
"manifestSha256",
|
|
444
|
+
"signatureSha256",
|
|
445
|
+
"runtimeCommit",
|
|
446
|
+
];
|
|
447
|
+
if (!keys.every((key) => typeof record[key] === "string" && record[key].length > 0))
|
|
448
|
+
return null;
|
|
449
|
+
const pin = Object.fromEntries(keys.map((key) => [key, record[key]]));
|
|
450
|
+
if (pin.repository !== HQ_FLEET_LOCAL_RELEASE.repository)
|
|
451
|
+
return null;
|
|
452
|
+
if (!semver.valid(pin.version) || semver.clean(pin.version) !== pin.version)
|
|
453
|
+
return null;
|
|
454
|
+
if (pin.tag !== `v${pin.version}` || pin.artifact !== `hq-fleet-local-${pin.version}.zip`)
|
|
455
|
+
return null;
|
|
456
|
+
if (!COMMIT_RE.test(pin.runtimeCommit))
|
|
457
|
+
return null;
|
|
458
|
+
if (pin.runtimeArtifact !== `hq-fleet-runtime-${pin.runtimeCommit.slice(0, 12)}.tar`)
|
|
459
|
+
return null;
|
|
460
|
+
if (![pin.artifactSha256, pin.runtimeArtifactSha256, pin.manifestSha256, pin.signatureSha256].every((hash) => SHA256_RE.test(hash))) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
return pin;
|
|
464
|
+
}
|
|
465
|
+
export function parseFleetLocalReleasePointer(body, now = Date.now()) {
|
|
466
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
467
|
+
return null;
|
|
468
|
+
const record = body;
|
|
469
|
+
const release = parseFleetLocalReleasePin(record.release);
|
|
470
|
+
if (!release || record.version !== release.version)
|
|
471
|
+
return null;
|
|
472
|
+
const minimumHqCli = typeof record.minimumHqCli === "string" && semver.valid(semver.coerce(record.minimumHqCli))
|
|
473
|
+
? record.minimumHqCli
|
|
474
|
+
: null;
|
|
475
|
+
if (record.minimumHqCli !== undefined && record.minimumHqCli !== null && minimumHqCli === null)
|
|
476
|
+
return null;
|
|
477
|
+
const links = new Map();
|
|
478
|
+
for (const asset of Array.isArray(record.assets) ? record.assets : []) {
|
|
479
|
+
const entry = asset;
|
|
480
|
+
if (typeof entry?.name === "string" && typeof entry.url === "string")
|
|
481
|
+
links.set(entry.name, entry.url);
|
|
482
|
+
}
|
|
483
|
+
const expires = typeof record.expiresInSeconds === "number" && record.expiresInSeconds > 0 ? record.expiresInSeconds : 0;
|
|
484
|
+
return {
|
|
485
|
+
release,
|
|
486
|
+
minimumHqCli,
|
|
487
|
+
links: links.size > 0 && expires > 0 ? links : null,
|
|
488
|
+
linksExpireAt: links.size > 0 && expires > 0 ? now + expires * 1000 : null,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
export function chooseFleetLocalTargetRelease(pointer, compiled = HQ_FLEET_LOCAL_RELEASE, cliVersion = CLI_VERSION) {
|
|
492
|
+
const fallback = { release: compiled, source: "compiled", links: null, linksExpireAt: null };
|
|
493
|
+
if (!pointer || !semver.gt(pointer.release.version, compiled.version))
|
|
494
|
+
return fallback;
|
|
495
|
+
if (pointer.minimumHqCli) {
|
|
496
|
+
const required = semver.valid(semver.coerce(pointer.minimumHqCli));
|
|
497
|
+
const current = semver.valid(semver.coerce(cliVersion));
|
|
498
|
+
if (!required || !current || semver.lt(current, required))
|
|
499
|
+
return fallback;
|
|
500
|
+
}
|
|
501
|
+
return { release: pointer.release, source: "pointer", links: pointer.links, linksExpireAt: pointer.linksExpireAt };
|
|
502
|
+
}
|
|
503
|
+
export async function fetchFleetLocalReleasePointer(deps = {}) {
|
|
504
|
+
const token = await (deps.token ?? (() => ensureCognitoToken({ tokenSource: "person", interactive: false })))();
|
|
505
|
+
const response = await (deps.fetchApi ?? vaultApiFetch)({
|
|
506
|
+
token,
|
|
507
|
+
path: "/v1/fleet-local/releases/latest",
|
|
508
|
+
signal: AbortSignal.timeout(FLEET_LOCAL_LATEST_TIMEOUT_MS),
|
|
509
|
+
});
|
|
510
|
+
if (!response.ok)
|
|
511
|
+
return null;
|
|
512
|
+
return parseFleetLocalReleasePointer(await response.json(), (deps.now ?? Date.now)());
|
|
513
|
+
}
|
|
514
|
+
let memoryPointer = null;
|
|
515
|
+
export function resetFleetLocalReleasePointerCache() {
|
|
516
|
+
memoryPointer = null;
|
|
517
|
+
}
|
|
518
|
+
function readPointerCache(root, now) {
|
|
519
|
+
if (memoryPointer && memoryPointer.root === root && now - memoryPointer.fetchedAt < FLEET_LOCAL_LATEST_CACHE_MS) {
|
|
520
|
+
return { body: memoryPointer.body };
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const cached = JSON.parse(fs.readFileSync(path.join(root, FLEET_LOCAL_LATEST_CACHE_FILE), "utf8"));
|
|
524
|
+
if (typeof cached.fetchedAt === "number" && now - cached.fetchedAt >= 0 && now - cached.fetchedAt < FLEET_LOCAL_LATEST_CACHE_MS) {
|
|
525
|
+
memoryPointer = { root, fetchedAt: cached.fetchedAt, body: cached.pointer };
|
|
526
|
+
return { body: cached.pointer };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
function writePointerCache(root, now, body) {
|
|
535
|
+
memoryPointer = { root, fetchedAt: now, body };
|
|
536
|
+
try {
|
|
537
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
538
|
+
const file = path.join(root, FLEET_LOCAL_LATEST_CACHE_FILE);
|
|
539
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}`;
|
|
540
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ fetchedAt: now, pointer: body })}\n`, { mode: 0o600 });
|
|
541
|
+
fs.renameSync(temporary, file);
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function pointerToCache(pointer) {
|
|
548
|
+
if (!pointer)
|
|
549
|
+
return null;
|
|
550
|
+
return {
|
|
551
|
+
release: pointer.release,
|
|
552
|
+
minimumHqCli: pointer.minimumHqCli,
|
|
553
|
+
links: pointer.links ? [...pointer.links.entries()] : null,
|
|
554
|
+
linksExpireAt: pointer.linksExpireAt,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function pointerFromCache(body) {
|
|
558
|
+
if (!body || typeof body !== "object")
|
|
559
|
+
return null;
|
|
560
|
+
const record = body;
|
|
561
|
+
const release = parseFleetLocalReleasePin(record.release);
|
|
562
|
+
if (!release)
|
|
563
|
+
return null;
|
|
564
|
+
const links = Array.isArray(record.links)
|
|
565
|
+
? new Map(record.links.filter((entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "string"))
|
|
566
|
+
: null;
|
|
567
|
+
return {
|
|
568
|
+
release,
|
|
569
|
+
minimumHqCli: typeof record.minimumHqCli === "string" ? record.minimumHqCli : null,
|
|
570
|
+
links: links && links.size > 0 ? links : null,
|
|
571
|
+
linksExpireAt: typeof record.linksExpireAt === "number" ? record.linksExpireAt : null,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async function loadFleetLocalReleasePointer(deps) {
|
|
575
|
+
const root = deps.fleetHome ?? fleetHome();
|
|
576
|
+
const now = (deps.now ?? Date.now)();
|
|
577
|
+
const cached = readPointerCache(root, now);
|
|
578
|
+
if (cached)
|
|
579
|
+
return pointerFromCache(cached.body);
|
|
580
|
+
try {
|
|
581
|
+
const pointer = await (deps.fetchPointer ?? (() => fetchFleetLocalReleasePointer()))();
|
|
582
|
+
writePointerCache(root, now, pointerToCache(pointer));
|
|
583
|
+
return pointer;
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
export async function resolveFleetLocalTargetRelease(deps = {}) {
|
|
590
|
+
const pointer = await loadFleetLocalReleasePointer(deps);
|
|
591
|
+
return chooseFleetLocalTargetRelease(pointer, deps.compiled ?? HQ_FLEET_LOCAL_RELEASE, deps.cliVersion ?? CLI_VERSION);
|
|
592
|
+
}
|
|
593
|
+
export async function resolveFleetLocalPointerTarget(deps = {}) {
|
|
594
|
+
const pointer = await loadFleetLocalReleasePointer(deps);
|
|
595
|
+
if (!pointer)
|
|
596
|
+
return null;
|
|
597
|
+
if (pointer.minimumHqCli) {
|
|
598
|
+
const required = semver.valid(semver.coerce(pointer.minimumHqCli));
|
|
599
|
+
const current = semver.valid(semver.coerce(deps.cliVersion ?? CLI_VERSION));
|
|
600
|
+
if (!required || !current || semver.lt(current, required))
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
return { release: pointer.release, source: "pointer", links: pointer.links, linksExpireAt: pointer.linksExpireAt };
|
|
604
|
+
}
|
|
605
|
+
export async function installTargetFleetLocalDistribution(deps = {}) {
|
|
606
|
+
const target = deps.target ?? await resolveFleetLocalTargetRelease({ ...(deps.fleetHome ? { fleetHome: deps.fleetHome } : {}) });
|
|
607
|
+
const now = (deps.now ?? Date.now)();
|
|
608
|
+
const freshLinks = target.links && target.linksExpireAt !== null && target.linksExpireAt - now > 60_000 ? target.links : null;
|
|
609
|
+
const { target: _target, now: _now, ...installDeps } = deps;
|
|
610
|
+
return installPinnedFleetLocalDistribution({
|
|
611
|
+
...installDeps,
|
|
612
|
+
release: target.release,
|
|
613
|
+
...(freshLinks && !installDeps.hqReleaseLinks ? { hqReleaseLinks: async () => freshLinks } : {}),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
250
616
|
function trustedReleaseLink(url) {
|
|
251
617
|
try {
|
|
252
618
|
const parsed = new URL(url);
|
|
@@ -493,11 +859,15 @@ function profileEngineMatchesRelease(profileDir, release) {
|
|
|
493
859
|
try {
|
|
494
860
|
const marker = JSON.parse(fs.readFileSync(path.join(profileDir, "bootstrap.json"), "utf8"));
|
|
495
861
|
const expected = engineMetadata(release);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
862
|
+
if (marker.engine?.id !== expected.id || marker.engine.distribution !== expected.distribution)
|
|
863
|
+
return false;
|
|
864
|
+
if (marker.engine.version === expected.version &&
|
|
499
865
|
marker.engine.artifactSha256 === expected.artifactSha256 &&
|
|
500
|
-
marker.engine.runtimeCommit === expected.runtimeCommit
|
|
866
|
+
marker.engine.runtimeCommit === expected.runtimeCommit) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
const installed = typeof marker.engine.version === "string" ? semver.valid(marker.engine.version) : null;
|
|
870
|
+
return installed !== null && semver.valid(expected.version) !== null && semver.gt(installed, expected.version);
|
|
501
871
|
}
|
|
502
872
|
catch {
|
|
503
873
|
return false;
|
|
@@ -565,7 +935,7 @@ export function fleetLocalGatewayPid(agentUid, deps = {}) {
|
|
|
565
935
|
}
|
|
566
936
|
}
|
|
567
937
|
/** The first hq-fleet-local release whose profile schema accepts `autoApprove`; older ones reject unknown fields. */
|
|
568
|
-
const AUTO_APPROVE_RELEASE = [0, 4,
|
|
938
|
+
const AUTO_APPROVE_RELEASE = [0, 4, 19];
|
|
569
939
|
function releaseKnowsAutoApprove(release) {
|
|
570
940
|
const parts = release.version.split(".").map((part) => Number.parseInt(part, 10));
|
|
571
941
|
for (let i = 0; i < AUTO_APPROVE_RELEASE.length; i += 1) {
|
|
@@ -890,7 +1260,7 @@ export function writeFleetLocalProfile(input, deps = {}) {
|
|
|
890
1260
|
botId: input.agentUid,
|
|
891
1261
|
ownerUid: input.ownerUid,
|
|
892
1262
|
...(input.ownerCompanies?.length ? { ownerCompanies: [...new Set(input.ownerCompanies)].sort() } : {}),
|
|
893
|
-
...(input.autoApprove ===
|
|
1263
|
+
...(input.autoApprove === true && releaseKnowsAutoApprove(input.installation.release) ? { autoApprove: true } : {}),
|
|
894
1264
|
provider: input.provider ?? { id: "custom:personal-chatgpt", model: "gpt-5.6-sol" },
|
|
895
1265
|
hqRoot: path.resolve(input.hqRoot),
|
|
896
1266
|
profileDir: path.resolve(dir),
|
|
@@ -983,6 +1353,97 @@ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
|
|
|
983
1353
|
throw failure;
|
|
984
1354
|
return parsed;
|
|
985
1355
|
}
|
|
1356
|
+
export function fleetLocalRuntimePython(agentUid, deps = {}) {
|
|
1357
|
+
if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid)) {
|
|
1358
|
+
throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
|
|
1359
|
+
}
|
|
1360
|
+
const root = deps.fleetHome ?? fleetHome();
|
|
1361
|
+
const profileDir = profileRoot(agentUid, root);
|
|
1362
|
+
const platform = deps.platform ?? process.platform;
|
|
1363
|
+
const shared = sharedRuntimeFromMarker(profileDir, root, platform);
|
|
1364
|
+
const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
|
|
1365
|
+
const python = venvExecutable(runtimeDir, "python", platform);
|
|
1366
|
+
const source = shared?.runtimeSource ?? path.join(profileDir, "sources", "runtime");
|
|
1367
|
+
if (!fs.existsSync(python) || !fs.existsSync(source)) {
|
|
1368
|
+
throw expectedError(`HQ Fleet Local is not installed for ${agentUid}.`);
|
|
1369
|
+
}
|
|
1370
|
+
return { python, source, profileDir };
|
|
1371
|
+
}
|
|
1372
|
+
export function fleetProfileOwnerCompanies(agentUid, deps = {}) {
|
|
1373
|
+
let raw;
|
|
1374
|
+
try {
|
|
1375
|
+
raw = JSON.parse(fs.readFileSync(path.join(fleetLocalProfileDir(agentUid, deps), "profile.json"), "utf8"));
|
|
1376
|
+
}
|
|
1377
|
+
catch {
|
|
1378
|
+
return [];
|
|
1379
|
+
}
|
|
1380
|
+
const companies = raw && typeof raw === "object" ? raw.ownerCompanies : undefined;
|
|
1381
|
+
return Array.isArray(companies) ? companies.filter((slug) => typeof slug === "string" && /^[a-z0-9][a-z0-9_-]*$/i.test(slug)) : [];
|
|
1382
|
+
}
|
|
1383
|
+
export function fleetLocalProfileDir(agentUid, deps = {}) {
|
|
1384
|
+
if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid)) {
|
|
1385
|
+
throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
|
|
1386
|
+
}
|
|
1387
|
+
return profileRoot(agentUid, deps.fleetHome ?? fleetHome());
|
|
1388
|
+
}
|
|
1389
|
+
export function readFleetLocalGatewayState(agentUid, deps = {}) {
|
|
1390
|
+
let raw;
|
|
1391
|
+
try {
|
|
1392
|
+
raw = JSON.parse(fs.readFileSync(path.join(fleetLocalProfileDir(agentUid, deps), "gateway_state.json"), "utf8"));
|
|
1393
|
+
}
|
|
1394
|
+
catch {
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1398
|
+
return null;
|
|
1399
|
+
const platforms = {};
|
|
1400
|
+
if (raw.platforms && typeof raw.platforms === "object" && !Array.isArray(raw.platforms)) {
|
|
1401
|
+
for (const [name, value] of Object.entries(raw.platforms)) {
|
|
1402
|
+
const state = value?.state;
|
|
1403
|
+
if (typeof state === "string" && state)
|
|
1404
|
+
platforms[name] = state;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const active = Number(raw.active_agents);
|
|
1408
|
+
return {
|
|
1409
|
+
pid: typeof raw.pid === "number" && Number.isInteger(raw.pid) && raw.pid > 0 ? raw.pid : null,
|
|
1410
|
+
gatewayState: typeof raw.gateway_state === "string" ? raw.gateway_state : null,
|
|
1411
|
+
activeAgents: Number.isFinite(active) && active > 0 ? Math.floor(active) : 0,
|
|
1412
|
+
platforms,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
export function fleetLocalChannels(state, gatewayAlive) {
|
|
1416
|
+
const lane = (name) => (gatewayAlive && state?.platforms[name]) || "not_connected";
|
|
1417
|
+
return { slack: lane("slack"), telegram: lane("telegram") };
|
|
1418
|
+
}
|
|
1419
|
+
export function fleetLocalBusyReason(agentUid, deps = {}) {
|
|
1420
|
+
const state = readFleetLocalGatewayState(agentUid, deps);
|
|
1421
|
+
const alive = deps.isAlive ?? ((pid) => {
|
|
1422
|
+
try {
|
|
1423
|
+
process.kill(pid, 0);
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
catch (error) {
|
|
1427
|
+
return error.code === "EPERM";
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
if (!state || state.pid === null || !alive(state.pid))
|
|
1431
|
+
return null;
|
|
1432
|
+
if (state.activeAgents > 0)
|
|
1433
|
+
return `${state.activeAgents} turn${state.activeAgents === 1 ? "" : "s"} in progress`;
|
|
1434
|
+
if (state.gatewayState === "draining" || state.gatewayState === "starting")
|
|
1435
|
+
return `gateway is ${state.gatewayState}`;
|
|
1436
|
+
try {
|
|
1437
|
+
const store = JSON.parse(fs.readFileSync(path.join(fleetLocalProfileDir(agentUid, deps), "cron", "jobs.json"), "utf8"));
|
|
1438
|
+
const running = (store.jobs ?? []).find((job) => job && job.fire_claim !== null && job.fire_claim !== undefined);
|
|
1439
|
+
if (running)
|
|
1440
|
+
return typeof running.name === "string" && running.name ? `scheduled job "${running.name}" is running` : "a scheduled job is running";
|
|
1441
|
+
}
|
|
1442
|
+
catch {
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
986
1447
|
/** Checks the instructions with the runtime's scanner, then makes them the bot's persona. */
|
|
987
1448
|
export function refreshFleetLocalInstructions(agentUid, systemPrompt, deps = {}) {
|
|
988
1449
|
const findings = scanFleetLocalInstructions(agentUid, systemPrompt, deps);
|
|
@@ -1001,24 +1462,79 @@ export function stderrTail(error) {
|
|
|
1001
1462
|
const tail = lines.slice(-STDERR_TAIL_LINES).join("\n");
|
|
1002
1463
|
return tail.length > STDERR_TAIL_CHARS ? `...${tail.slice(-STDERR_TAIL_CHARS)}` : tail;
|
|
1003
1464
|
}
|
|
1004
|
-
function checkedRun(run, executable, args, options, label) {
|
|
1465
|
+
function checkedRun(run, executable, args, options, label, spawn) {
|
|
1005
1466
|
// Keep stdout quiet but capture stderr, so a failure names its cause
|
|
1006
1467
|
// (for example a path-too-long error during extraction).
|
|
1007
|
-
const
|
|
1468
|
+
const requested = options.stdio === "ignore" ? ["ignore", "ignore", "pipe"] : options.stdio;
|
|
1469
|
+
// Node only applies CREATE_NO_WINDOW when no stdio slot is inherited
|
|
1470
|
+
// (libuv src/win/process.c), so windowsHide alone does not hide a child
|
|
1471
|
+
// given an fd or "inherit". Pipe those slots and copy the output through.
|
|
1472
|
+
const { stdio, forward } = pipeInheritedStdio(requested);
|
|
1473
|
+
const spawnOptions = {
|
|
1474
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
1475
|
+
...options,
|
|
1476
|
+
...(stdio === undefined ? {} : { stdio }),
|
|
1477
|
+
// Last, so no caller can turn it off: no console window flashes on Windows.
|
|
1478
|
+
windowsHide: true,
|
|
1479
|
+
};
|
|
1480
|
+
if (forward) {
|
|
1481
|
+
// spawnSync keeps stderr on success too (execFileSync drops it once stdio
|
|
1482
|
+
// is set), so uv warnings and "Installed N packages" still reach the user.
|
|
1483
|
+
const result = (spawn ?? spawnFromRun(run))(executable, args, spawnOptions);
|
|
1484
|
+
copyChildOutput(result.stdout);
|
|
1485
|
+
if (!result.error && result.status === 0) {
|
|
1486
|
+
copyChildOutput(result.stderr);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
// On failure stderr appears once, as the tail in the error message.
|
|
1490
|
+
const tail = stderrTail(result);
|
|
1491
|
+
throw expectedError(`HQ Fleet Local ${label} failed.${tail ? `\n${tail}` : ""}`);
|
|
1492
|
+
}
|
|
1008
1493
|
try {
|
|
1009
|
-
run(executable, args,
|
|
1010
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
1011
|
-
...options,
|
|
1012
|
-
...(stdio === undefined ? {} : { stdio }),
|
|
1013
|
-
// Last, so no caller can turn it off: no console window flashes on Windows.
|
|
1014
|
-
windowsHide: true,
|
|
1015
|
-
});
|
|
1494
|
+
run(executable, args, spawnOptions);
|
|
1016
1495
|
}
|
|
1017
1496
|
catch (error) {
|
|
1018
1497
|
const tail = stderrTail(error);
|
|
1019
1498
|
throw expectedError(`HQ Fleet Local ${label} failed.${tail ? `\n${tail}` : ""}`);
|
|
1020
1499
|
}
|
|
1021
1500
|
}
|
|
1501
|
+
/** A FleetLocalSpawn backed by an execFileSync-shaped runner (stderr is only known on failure). */
|
|
1502
|
+
export function spawnFromRun(run) {
|
|
1503
|
+
return (executable, args, options) => {
|
|
1504
|
+
try {
|
|
1505
|
+
return { status: 0, stdout: run(executable, args, options), stderr: "" };
|
|
1506
|
+
}
|
|
1507
|
+
catch (error) {
|
|
1508
|
+
const failed = (error ?? {});
|
|
1509
|
+
return { status: typeof failed.status === "number" ? failed.status : 1, stdout: failed.stdout ?? null, stderr: failed.stderr ?? null };
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Replaces inherited stdio slots (an fd number or "inherit") with "pipe".
|
|
1515
|
+
* `forward` is true when any slot was replaced, so the caller copies the
|
|
1516
|
+
* captured output to this process's stderr.
|
|
1517
|
+
*/
|
|
1518
|
+
export function pipeInheritedStdio(stdio) {
|
|
1519
|
+
if (stdio === "inherit")
|
|
1520
|
+
return { stdio: ["ignore", "pipe", "pipe"], forward: true };
|
|
1521
|
+
if (!Array.isArray(stdio))
|
|
1522
|
+
return { stdio, forward: false };
|
|
1523
|
+
let forward = false;
|
|
1524
|
+
const piped = stdio.map((slot, index) => {
|
|
1525
|
+
if (typeof slot === "number" || slot === "inherit") {
|
|
1526
|
+
forward = true;
|
|
1527
|
+
return index === 0 ? "ignore" : "pipe";
|
|
1528
|
+
}
|
|
1529
|
+
return slot;
|
|
1530
|
+
});
|
|
1531
|
+
return { stdio: piped, forward };
|
|
1532
|
+
}
|
|
1533
|
+
function copyChildOutput(output) {
|
|
1534
|
+
const text = Buffer.isBuffer(output) ? output.toString("utf8") : typeof output === "string" ? output : "";
|
|
1535
|
+
if (text)
|
|
1536
|
+
process.stderr.write(text);
|
|
1537
|
+
}
|
|
1022
1538
|
const FLEET_TAR_PATH_PROBE = [
|
|
1023
1539
|
"import json, sys, tarfile",
|
|
1024
1540
|
"longest = 0",
|
|
@@ -1086,7 +1602,7 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
|
|
|
1086
1602
|
], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
|
|
1087
1603
|
fs.rmSync(machineCredentials, { force: true });
|
|
1088
1604
|
}
|
|
1089
|
-
async function installSharedFleetRuntime(installation, root, python, platform, run) {
|
|
1605
|
+
async function installSharedFleetRuntime(installation, root, python, platform, run, spawn) {
|
|
1090
1606
|
const identity = digest(Buffer.from(JSON.stringify({
|
|
1091
1607
|
version: installation.release.version,
|
|
1092
1608
|
distribution: installation.release.artifactSha256,
|
|
@@ -1131,13 +1647,8 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
|
|
|
1131
1647
|
fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
|
|
1132
1648
|
fs.mkdirSync(runtimeSource, { recursive: true, mode: 0o700 });
|
|
1133
1649
|
const uvEnv = fleetUvEnv(python.pythonDir);
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
"zipfile",
|
|
1137
|
-
"-e",
|
|
1138
|
-
installation.archive,
|
|
1139
|
-
distributionSource,
|
|
1140
|
-
], { stdio: "ignore" }, "distribution extraction");
|
|
1650
|
+
extractZipEntries(fs.readFileSync(installation.archive), distributionSource, installation.release.version);
|
|
1651
|
+
verifyExtractedDistribution(distributionSource, fs.readFileSync(installation.manifest), installation.release.version);
|
|
1141
1652
|
if (platform === "win32")
|
|
1142
1653
|
assertWindowsExtractionFits(run, python.python, installation.runtimeArchive, runtimeSource);
|
|
1143
1654
|
checkedRun(run, python.python, [
|
|
@@ -1164,7 +1675,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
|
|
|
1164
1675
|
"--editable",
|
|
1165
1676
|
`${runtimeSource}[mcp,anthropic]`,
|
|
1166
1677
|
distributionSource,
|
|
1167
|
-
], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation");
|
|
1678
|
+
], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation", spawn);
|
|
1168
1679
|
if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
|
|
1169
1680
|
throw expectedError("HQ Fleet Local installed without its runtime executables.");
|
|
1170
1681
|
}
|
|
@@ -1190,7 +1701,8 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
|
|
|
1190
1701
|
const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
|
|
1191
1702
|
const profileDir = adoptLegacyProfileDirectory(input.agentUid, root);
|
|
1192
1703
|
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
|
|
1193
|
-
const
|
|
1704
|
+
const spawn = deps.spawn ?? (deps.run ? spawnFromRun(deps.run) : spawnSync);
|
|
1705
|
+
const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run, spawn);
|
|
1194
1706
|
writeNativeProfileMarker(profileDir, shared, input.installation.engine);
|
|
1195
1707
|
const { systemPrompt, machineCredentials, ...profileInput } = input;
|
|
1196
1708
|
if (systemPrompt !== undefined)
|
|
@@ -1220,7 +1732,7 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
|
|
|
1220
1732
|
checkedRun(run, helper, ["doctor-profile", "--profile", profileDir, "--runtime", runtime], { stdio: "ignore" }, "profile validation");
|
|
1221
1733
|
const start = deps.start !== false;
|
|
1222
1734
|
if (start) {
|
|
1223
|
-
checkedRun(run, helper, ["service", "install", "--profile", profileDir, "--runtime", runtime], { stdio: ["ignore", 2, 2] }, "service installation");
|
|
1735
|
+
checkedRun(run, helper, ["service", "install", "--profile", profileDir, "--runtime", runtime], { stdio: ["ignore", 2, 2] }, "service installation", spawn);
|
|
1224
1736
|
removeEmbeddedRuntime(profileDir, platform);
|
|
1225
1737
|
}
|
|
1226
1738
|
return { profile, runtime, helper, running: start };
|