@indigoai-us/hq-cli 5.345.190 → 5.345.193
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 +420 -0
- package/assets/bot-workers/setup/worker.yaml +91 -34
- package/dist/command-catalog.generated.d.ts +96 -19
- package/dist/command-catalog.generated.js +124 -21
- package/dist/commands/bot-jobs.d.ts +21 -18
- package/dist/commands/bot-jobs.js +127 -39
- package/dist/commands/bot.d.ts +7 -4
- package/dist/commands/bot.js +289 -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 +45 -0
- package/dist/commands/signals-run.js +175 -0
- package/dist/commands/signals.d.ts +1 -0
- package/dist/commands/signals.js +3 -0
- package/dist/commands/sources.d.ts +11 -0
- package/dist/commands/sources.js +137 -25
- package/dist/commands/staff/reset-test-account.d.ts +10 -0
- package/dist/commands/staff/reset-test-account.js +18 -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 +113 -2
- package/dist/lib/bot/fleet-local.js +597 -35
- 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.js +18 -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 +1 -0
- package/dist/lib/hot-channels/signals-pass.js +7 -5
- 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 +196 -0
- package/dist/lib/signals/engine.js +495 -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 +113 -0
- package/dist/lib/signals/steps/extract.js +765 -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 +112 -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/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,20 +79,47 @@ 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
|
};
|
|
92
|
+
/** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
|
|
91
93
|
export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
92
94
|
MCowBQYDK2VwAyEASJoapvo0WMTg+Wu3Jvt0nzlXLOBEr+G1aJRi0JkEb4A=
|
|
93
95
|
-----END PUBLIC KEY-----
|
|
94
96
|
`;
|
|
97
|
+
/** hq-fleet-local trust/release-public-v2.pem (added 2026-11-01; signs v0.4.18). */
|
|
98
|
+
export const HQ_FLEET_LOCAL_PUBLIC_KEY_V2 = `-----BEGIN PUBLIC KEY-----
|
|
99
|
+
MCowBQYDK2VwAyEAmgUi7pKTJUmf3yzwcuH65wSvn1NyJrDfZHZUX08sGN8=
|
|
100
|
+
-----END PUBLIC KEY-----
|
|
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
|
+
`;
|
|
107
|
+
/** Every key a release manifest may be signed with. Any one must verify it. */
|
|
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];
|
|
109
|
+
function publicKeyList(publicKey) {
|
|
110
|
+
return typeof publicKey === "string" ? [publicKey] : publicKey;
|
|
111
|
+
}
|
|
112
|
+
/** True when any trusted Ed25519 key verifies the manifest signature. */
|
|
113
|
+
export function fleetLocalManifestSignatureValid(manifest, signature, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEYS) {
|
|
114
|
+
return publicKeyList(publicKey).some((key) => {
|
|
115
|
+
try {
|
|
116
|
+
return crypto.verify(null, manifest, crypto.createPublicKey(key), signature);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
95
123
|
function digest(value) {
|
|
96
124
|
return crypto.createHash("sha256").update(value).digest("hex");
|
|
97
125
|
}
|
|
@@ -110,7 +138,7 @@ function engineMetadata(release) {
|
|
|
110
138
|
export function fleetHome(home = os.homedir(), env = process.env) {
|
|
111
139
|
return env.HQ_FLEET_HOME?.trim() || path.join(home, ".hq-fleet");
|
|
112
140
|
}
|
|
113
|
-
export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE, publicKey =
|
|
141
|
+
export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE, publicKey = HQ_FLEET_LOCAL_PUBLIC_KEYS) {
|
|
114
142
|
if (digest(files.archive) !== release.artifactSha256) {
|
|
115
143
|
throw expectedError(`HQ Fleet Local ${release.version} archive hash did not match the release pin.`);
|
|
116
144
|
}
|
|
@@ -142,10 +170,180 @@ export function verifyFleetLocalRelease(files, release = HQ_FLEET_LOCAL_RELEASE,
|
|
|
142
170
|
if (signature.algorithm !== "ed25519" || typeof signature.signature !== "string") {
|
|
143
171
|
throw expectedError(`HQ Fleet Local ${release.version} signature record is invalid.`);
|
|
144
172
|
}
|
|
145
|
-
const valid =
|
|
173
|
+
const valid = fleetLocalManifestSignatureValid(files.manifest, Buffer.from(signature.signature, "base64"), publicKey);
|
|
146
174
|
if (!valid) {
|
|
147
175
|
throw expectedError(`HQ Fleet Local ${release.version} manifest signature is invalid.`);
|
|
148
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;
|
|
149
347
|
}
|
|
150
348
|
/**
|
|
151
349
|
* Warning text when the signed manifest asks for a newer hq CLI than this
|
|
@@ -225,6 +423,196 @@ export async function fetchHqFleetReleaseLinks(release) {
|
|
|
225
423
|
}
|
|
226
424
|
return links.size > 0 ? links : null;
|
|
227
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
|
+
}
|
|
228
616
|
function trustedReleaseLink(url) {
|
|
229
617
|
try {
|
|
230
618
|
const parsed = new URL(url);
|
|
@@ -326,7 +714,7 @@ function installFiles(target, files, release, publicKey) {
|
|
|
326
714
|
fs.writeFileSync(path.join(staging, release.runtimeArtifact), files.runtimeArchive, { mode: 0o644 });
|
|
327
715
|
fs.writeFileSync(path.join(staging, "release-manifest.json"), files.manifest, { mode: 0o644 });
|
|
328
716
|
fs.writeFileSync(path.join(staging, "release-manifest.sig"), files.signature, { mode: 0o644 });
|
|
329
|
-
fs.writeFileSync(path.join(staging, "release-public.pem"), publicKey, { mode: 0o644 });
|
|
717
|
+
fs.writeFileSync(path.join(staging, "release-public.pem"), publicKeyList(publicKey).join(""), { mode: 0o644 });
|
|
330
718
|
fs.writeFileSync(path.join(staging, "install.json"), `${JSON.stringify({ schemaVersion: 1, release, engine: engineMetadata(release) }, null, 2)}\n`, { mode: 0o644 });
|
|
331
719
|
if (fs.existsSync(target))
|
|
332
720
|
fs.renameSync(target, backup);
|
|
@@ -366,7 +754,7 @@ function cachedRuntimeArchive(root, release) {
|
|
|
366
754
|
}
|
|
367
755
|
export async function installPinnedFleetLocalDistribution(deps = {}) {
|
|
368
756
|
const release = deps.release ?? HQ_FLEET_LOCAL_RELEASE;
|
|
369
|
-
const publicKey = deps.publicKey ??
|
|
757
|
+
const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEYS;
|
|
370
758
|
const root = deps.fleetHome ?? fleetHome();
|
|
371
759
|
const dir = path.join(root, "distributions", "hq-fleet-local", release.version);
|
|
372
760
|
const current = installedFiles(dir, release);
|
|
@@ -471,11 +859,15 @@ function profileEngineMatchesRelease(profileDir, release) {
|
|
|
471
859
|
try {
|
|
472
860
|
const marker = JSON.parse(fs.readFileSync(path.join(profileDir, "bootstrap.json"), "utf8"));
|
|
473
861
|
const expected = engineMetadata(release);
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
862
|
+
if (marker.engine?.id !== expected.id || marker.engine.distribution !== expected.distribution)
|
|
863
|
+
return false;
|
|
864
|
+
if (marker.engine.version === expected.version &&
|
|
477
865
|
marker.engine.artifactSha256 === expected.artifactSha256 &&
|
|
478
|
-
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);
|
|
479
871
|
}
|
|
480
872
|
catch {
|
|
481
873
|
return false;
|
|
@@ -513,7 +905,8 @@ function fleetLocalExecutables(agentUid, deps) {
|
|
|
513
905
|
function runJson(run, executable, args, options, label) {
|
|
514
906
|
let output;
|
|
515
907
|
try {
|
|
516
|
-
|
|
908
|
+
// windowsHide last: a helper, uv or Python child never opens a console window on Windows.
|
|
909
|
+
output = run(executable, args, { encoding: "utf8", ...options, windowsHide: true });
|
|
517
910
|
}
|
|
518
911
|
catch {
|
|
519
912
|
throw expectedError(`HQ Fleet Local ${label} failed.`);
|
|
@@ -541,6 +934,31 @@ export function fleetLocalGatewayPid(agentUid, deps = {}) {
|
|
|
541
934
|
return null;
|
|
542
935
|
}
|
|
543
936
|
}
|
|
937
|
+
/** The first hq-fleet-local release whose profile schema accepts `autoApprove`; older ones reject unknown fields. */
|
|
938
|
+
const AUTO_APPROVE_RELEASE = [0, 4, 19];
|
|
939
|
+
function releaseKnowsAutoApprove(release) {
|
|
940
|
+
const parts = release.version.split(".").map((part) => Number.parseInt(part, 10));
|
|
941
|
+
for (let i = 0; i < AUTO_APPROVE_RELEASE.length; i += 1) {
|
|
942
|
+
const have = Number.isFinite(parts[i]) ? parts[i] : 0;
|
|
943
|
+
if (have !== AUTO_APPROVE_RELEASE[i])
|
|
944
|
+
return have > AUTO_APPROVE_RELEASE[i];
|
|
945
|
+
}
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Whether the bot's HQ Fleet gateway is installed as a login service. The
|
|
950
|
+
* runtime registers it with launchd as `ai.hq-fleet.gateway-<profile>`; null
|
|
951
|
+
* on platforms where hq-cli does not know the service layout.
|
|
952
|
+
*/
|
|
953
|
+
export function fleetLocalServiceInstalled(agentUid, deps = {}) {
|
|
954
|
+
if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
|
|
955
|
+
return null;
|
|
956
|
+
const platform = deps.platform ?? process.platform;
|
|
957
|
+
if (platform !== "darwin")
|
|
958
|
+
return null;
|
|
959
|
+
const home = deps.home ?? os.homedir();
|
|
960
|
+
return fs.existsSync(path.join(home, "Library", "LaunchAgents", `ai.hq-fleet.gateway-${profileName(agentUid)}.plist`));
|
|
961
|
+
}
|
|
544
962
|
export function fleetLocalProfileNeedsAdoption(agentUid, deps = {}) {
|
|
545
963
|
if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
|
|
546
964
|
return true;
|
|
@@ -842,6 +1260,7 @@ export function writeFleetLocalProfile(input, deps = {}) {
|
|
|
842
1260
|
botId: input.agentUid,
|
|
843
1261
|
ownerUid: input.ownerUid,
|
|
844
1262
|
...(input.ownerCompanies?.length ? { ownerCompanies: [...new Set(input.ownerCompanies)].sort() } : {}),
|
|
1263
|
+
...(input.autoApprove === true && releaseKnowsAutoApprove(input.installation.release) ? { autoApprove: true } : {}),
|
|
845
1264
|
provider: input.provider ?? { id: "custom:personal-chatgpt", model: "gpt-5.6-sol" },
|
|
846
1265
|
hqRoot: path.resolve(input.hqRoot),
|
|
847
1266
|
profileDir: path.resolve(dir),
|
|
@@ -934,6 +1353,97 @@ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
|
|
|
934
1353
|
throw failure;
|
|
935
1354
|
return parsed;
|
|
936
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
|
+
}
|
|
937
1447
|
/** Checks the instructions with the runtime's scanner, then makes them the bot's persona. */
|
|
938
1448
|
export function refreshFleetLocalInstructions(agentUid, systemPrompt, deps = {}) {
|
|
939
1449
|
const findings = scanFleetLocalInstructions(agentUid, systemPrompt, deps);
|
|
@@ -952,23 +1462,79 @@ export function stderrTail(error) {
|
|
|
952
1462
|
const tail = lines.slice(-STDERR_TAIL_LINES).join("\n");
|
|
953
1463
|
return tail.length > STDERR_TAIL_CHARS ? `...${tail.slice(-STDERR_TAIL_CHARS)}` : tail;
|
|
954
1464
|
}
|
|
955
|
-
function checkedRun(run, executable, args, options, label) {
|
|
1465
|
+
function checkedRun(run, executable, args, options, label, spawn) {
|
|
956
1466
|
// Keep stdout quiet but capture stderr, so a failure names its cause
|
|
957
1467
|
// (for example a path-too-long error during extraction).
|
|
958
|
-
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
|
+
}
|
|
959
1493
|
try {
|
|
960
|
-
run(executable, args,
|
|
961
|
-
windowsHide: true,
|
|
962
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
963
|
-
...options,
|
|
964
|
-
...(stdio === undefined ? {} : { stdio }),
|
|
965
|
-
});
|
|
1494
|
+
run(executable, args, spawnOptions);
|
|
966
1495
|
}
|
|
967
1496
|
catch (error) {
|
|
968
1497
|
const tail = stderrTail(error);
|
|
969
1498
|
throw expectedError(`HQ Fleet Local ${label} failed.${tail ? `\n${tail}` : ""}`);
|
|
970
1499
|
}
|
|
971
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
|
+
}
|
|
972
1538
|
const FLEET_TAR_PATH_PROBE = [
|
|
973
1539
|
"import json, sys, tarfile",
|
|
974
1540
|
"longest = 0",
|
|
@@ -1036,7 +1602,7 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
|
|
|
1036
1602
|
], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
|
|
1037
1603
|
fs.rmSync(machineCredentials, { force: true });
|
|
1038
1604
|
}
|
|
1039
|
-
async function installSharedFleetRuntime(installation, root, python, platform, run) {
|
|
1605
|
+
async function installSharedFleetRuntime(installation, root, python, platform, run, spawn) {
|
|
1040
1606
|
const identity = digest(Buffer.from(JSON.stringify({
|
|
1041
1607
|
version: installation.release.version,
|
|
1042
1608
|
distribution: installation.release.artifactSha256,
|
|
@@ -1081,13 +1647,8 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
|
|
|
1081
1647
|
fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
|
|
1082
1648
|
fs.mkdirSync(runtimeSource, { recursive: true, mode: 0o700 });
|
|
1083
1649
|
const uvEnv = fleetUvEnv(python.pythonDir);
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
"zipfile",
|
|
1087
|
-
"-e",
|
|
1088
|
-
installation.archive,
|
|
1089
|
-
distributionSource,
|
|
1090
|
-
], { stdio: "ignore" }, "distribution extraction");
|
|
1650
|
+
extractZipEntries(fs.readFileSync(installation.archive), distributionSource, installation.release.version);
|
|
1651
|
+
verifyExtractedDistribution(distributionSource, fs.readFileSync(installation.manifest), installation.release.version);
|
|
1091
1652
|
if (platform === "win32")
|
|
1092
1653
|
assertWindowsExtractionFits(run, python.python, installation.runtimeArchive, runtimeSource);
|
|
1093
1654
|
checkedRun(run, python.python, [
|
|
@@ -1114,7 +1675,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
|
|
|
1114
1675
|
"--editable",
|
|
1115
1676
|
`${runtimeSource}[mcp,anthropic]`,
|
|
1116
1677
|
distributionSource,
|
|
1117
|
-
], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation");
|
|
1678
|
+
], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation", spawn);
|
|
1118
1679
|
if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
|
|
1119
1680
|
throw expectedError("HQ Fleet Local installed without its runtime executables.");
|
|
1120
1681
|
}
|
|
@@ -1140,7 +1701,8 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
|
|
|
1140
1701
|
const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
|
|
1141
1702
|
const profileDir = adoptLegacyProfileDirectory(input.agentUid, root);
|
|
1142
1703
|
fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
|
|
1143
|
-
const
|
|
1704
|
+
const spawn = deps.spawn ?? (deps.run ? spawnFromRun(deps.run) : spawnSync);
|
|
1705
|
+
const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run, spawn);
|
|
1144
1706
|
writeNativeProfileMarker(profileDir, shared, input.installation.engine);
|
|
1145
1707
|
const { systemPrompt, machineCredentials, ...profileInput } = input;
|
|
1146
1708
|
if (systemPrompt !== undefined)
|
|
@@ -1170,7 +1732,7 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
|
|
|
1170
1732
|
checkedRun(run, helper, ["doctor-profile", "--profile", profileDir, "--runtime", runtime], { stdio: "ignore" }, "profile validation");
|
|
1171
1733
|
const start = deps.start !== false;
|
|
1172
1734
|
if (start) {
|
|
1173
|
-
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);
|
|
1174
1736
|
removeEmbeddedRuntime(profileDir, platform);
|
|
1175
1737
|
}
|
|
1176
1738
|
return { profile, runtime, helper, running: start };
|