aibex 0.0.0-stage → 0.0.1

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.
@@ -0,0 +1,1781 @@
1
+ import { spawn, fork } from "node:child_process";
2
+ import { readFileSync, mkdirSync, writeFileSync, renameSync, rmSync, existsSync, readdirSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { homedir } from "node:os";
6
+ import { randomBytes } from "node:crypto";
7
+ import { createServer } from "node:net";
8
+ import { rename } from "node:fs/promises";
9
+ function log(source, message) {
10
+ process.stdout.write(`[${source}] ${message}
11
+ `);
12
+ }
13
+ function formatDuration(ms) {
14
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
15
+ const seconds = Math.round(ms / 1e3);
16
+ if (seconds < 60) return `${seconds}s`;
17
+ const minutes = Math.floor(seconds / 60);
18
+ if (minutes >= 60) {
19
+ const hours = Math.floor(minutes / 60);
20
+ const restMinutes = minutes % 60;
21
+ return restMinutes === 0 ? `${hours}h` : `${hours}h${restMinutes}m`;
22
+ }
23
+ const rest = seconds % 60;
24
+ return rest === 0 ? `${minutes}m` : `${minutes}m${rest}s`;
25
+ }
26
+ const PACKAGE_ROOT = path.resolve(
27
+ path.dirname(fileURLToPath(import.meta.url)),
28
+ ".."
29
+ );
30
+ function readInstalledVersion(root = PACKAGE_ROOT) {
31
+ const manifest = JSON.parse(
32
+ readFileSync(path.join(root, "package.json"), "utf8")
33
+ );
34
+ if (typeof manifest.version !== "string") {
35
+ throw new TypeError(`${root}/package.json has no version`);
36
+ }
37
+ return manifest.version;
38
+ }
39
+ function aibexHome(env = process.env) {
40
+ const override = env.AIBEX_HOME;
41
+ return override ? path.resolve(override) : path.join(homedir(), ".aibex");
42
+ }
43
+ function statePaths(home) {
44
+ return {
45
+ home,
46
+ pidFile: path.join(home, "aibexd.pid"),
47
+ configFile: path.join(home, "config.json"),
48
+ stateFile: path.join(home, "state.json"),
49
+ socketFile: path.join(home, "supervisor.sock"),
50
+ versionsDir: path.join(home, "versions"),
51
+ hostsDir: path.join(home, "hosts")
52
+ };
53
+ }
54
+ function writeFileAtomicSync(file, content) {
55
+ mkdirSync(path.dirname(file), { recursive: true });
56
+ const temporary = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
57
+ try {
58
+ writeFileSync(temporary, content);
59
+ renameSync(temporary, file);
60
+ } catch (error) {
61
+ rmSync(temporary, { force: true });
62
+ throw error;
63
+ }
64
+ }
65
+ function writeJsonAtomicSync(file, value) {
66
+ writeFileAtomicSync(file, `${JSON.stringify(value, void 0, 2)}
67
+ `);
68
+ }
69
+ function readJsonSync(file) {
70
+ let text;
71
+ try {
72
+ text = readFileSync(file, "utf8");
73
+ } catch {
74
+ return void 0;
75
+ }
76
+ try {
77
+ return JSON.parse(text);
78
+ } catch {
79
+ return void 0;
80
+ }
81
+ }
82
+ function writeSupervisorState(paths, state) {
83
+ writeJsonAtomicSync(paths.stateFile, state);
84
+ }
85
+ function readSupervisorState(paths) {
86
+ const value = readJsonSync(paths.stateFile);
87
+ if (typeof value !== "object" || value === null) return void 0;
88
+ const state = value;
89
+ if (typeof state.supervisor?.pid !== "number" || typeof state.link !== "object" || !Array.isArray(state.warnings)) {
90
+ return void 0;
91
+ }
92
+ return state;
93
+ }
94
+ function resolveLinkSource(env = process.env) {
95
+ const localPath = env.AIBEX_LINK_PATH;
96
+ if (!localPath) {
97
+ return { kind: "unavailable", reason: "AIBEX_LINK_PATH is not set" };
98
+ }
99
+ return resolveLocalLink(path.resolve(localPath));
100
+ }
101
+ function resolveLocalLink(directory) {
102
+ let manifest;
103
+ try {
104
+ manifest = JSON.parse(
105
+ readFileSync(path.join(directory, "package.json"), "utf8")
106
+ );
107
+ } catch {
108
+ return {
109
+ kind: "unavailable",
110
+ reason: `AIBEX_LINK_PATH ${directory} has no readable package.json`
111
+ };
112
+ }
113
+ const entry = path.join(
114
+ directory,
115
+ typeof manifest.main === "string" ? manifest.main : "dist/index.js"
116
+ );
117
+ if (!existsSync(entry)) {
118
+ return {
119
+ kind: "unavailable",
120
+ reason: `AIBEX_LINK_PATH ${directory} is not built: ${entry} is missing (run pnpm build)`
121
+ };
122
+ }
123
+ return {
124
+ kind: "local",
125
+ path: directory,
126
+ entry,
127
+ version: typeof manifest.version === "string" ? manifest.version : "unknown"
128
+ };
129
+ }
130
+ const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][\dA-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][\dA-Za-z-]*))*))?(?:\+[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*)?$/;
131
+ function parseSemver(value) {
132
+ const match = SEMVER.exec(value);
133
+ if (!match) return void 0;
134
+ return {
135
+ major: Number(match[1]),
136
+ minor: Number(match[2]),
137
+ patch: Number(match[3]),
138
+ prerelease: match[4] ? match[4].split(".") : []
139
+ };
140
+ }
141
+ function isSemver(value) {
142
+ return typeof value === "string" && parseSemver(value) !== void 0;
143
+ }
144
+ function compareIdentifiers(a, b) {
145
+ const aNumeric = /^\d+$/.test(a);
146
+ const bNumeric = /^\d+$/.test(b);
147
+ if (aNumeric && bNumeric) return Math.sign(Number(a) - Number(b));
148
+ if (aNumeric) return -1;
149
+ if (bNumeric) return 1;
150
+ if (a === b) return 0;
151
+ return a < b ? -1 : 1;
152
+ }
153
+ function compareSemver(a, b) {
154
+ const left = parseSemver(a);
155
+ const right = parseSemver(b);
156
+ if (!left || !right) throw new TypeError(`not semver: ${a} or ${b}`);
157
+ for (const key of ["major", "minor", "patch"]) {
158
+ if (left[key] !== right[key]) return Math.sign(left[key] - right[key]);
159
+ }
160
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
161
+ return right.prerelease.length - left.prerelease.length === 0 ? 0 : left.prerelease.length === 0 ? 1 : -1;
162
+ }
163
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
164
+ for (let index = 0; index < length; index += 1) {
165
+ const l = left.prerelease[index];
166
+ const r = right.prerelease[index];
167
+ if (l === void 0) return -1;
168
+ if (r === void 0) return 1;
169
+ const order = compareIdentifiers(l, r);
170
+ if (order !== 0) return order;
171
+ }
172
+ return 0;
173
+ }
174
+ function isNewer(candidate, running) {
175
+ if (!isSemver(candidate)) return false;
176
+ if (running === null || !isSemver(running)) return true;
177
+ return compareSemver(candidate, running) > 0;
178
+ }
179
+ function machineConfig(timings) {
180
+ return {
181
+ backoffInitialMs: timings.backoffInitialMs,
182
+ backoffMaxMs: timings.backoffMaxMs,
183
+ backoffResetMs: timings.backoffResetMs,
184
+ hungMs: timings.hungMs,
185
+ linkStopMs: timings.linkStopMs,
186
+ failingAfter: 3,
187
+ updateCheckMs: timings.updateCheckMs,
188
+ attemptInitialMs: timings.attemptInitialMs,
189
+ attemptMaxMs: timings.attemptMaxMs,
190
+ probationMs: timings.probationMs,
191
+ readyTimeoutMs: timings.readyTimeoutMs,
192
+ rollbackAfter: 3,
193
+ aibexdCheckMs: timings.aibexdCheckMs
194
+ };
195
+ }
196
+ const WARNING_LINK_FAILING = "link-failing";
197
+ const WARNING_LINK_UNAVAILABLE = "link-unavailable";
198
+ const WARNING_LINK_UPDATE = "link-update";
199
+ const WARNING_LINK_BAD = "link-bad";
200
+ const WARNING_LINK_ROLLED_BACK = "link-rolled-back";
201
+ const WARNING_AIBEXD_OUTDATED = "aibexd-outdated";
202
+ function emptyVersions() {
203
+ return {
204
+ current: null,
205
+ previous: null,
206
+ bad: {},
207
+ attempts: {},
208
+ aibexdNotified: null
209
+ };
210
+ }
211
+ function initialState(config, versions) {
212
+ return {
213
+ mode: versions ? "installed" : "local",
214
+ status: "starting",
215
+ pid: null,
216
+ startedAt: null,
217
+ readyAt: null,
218
+ lastAliveAt: null,
219
+ restartAt: null,
220
+ killAt: null,
221
+ afterStop: "exit",
222
+ delayMs: config.backoffInitialMs,
223
+ failures: 0,
224
+ hung: false,
225
+ notReady: false,
226
+ reason: null,
227
+ warnings: [],
228
+ versions: versions ?? emptyVersions(),
229
+ probation: null,
230
+ update: null,
231
+ report: null,
232
+ nextCheckAt: null,
233
+ nextAibexdCheckAt: null
234
+ };
235
+ }
236
+ function setWarning(warnings, warning) {
237
+ return [...warnings.filter((w) => w.code !== warning.code), warning];
238
+ }
239
+ function clearWarning(warnings, code) {
240
+ return warnings.filter((w) => w.code !== code);
241
+ }
242
+ function clearSuperseded(warnings, current) {
243
+ return warnings.filter((warning) => {
244
+ const [code, version] = warning.code.split("@");
245
+ if (code !== WARNING_LINK_BAD && code !== WARNING_LINK_ROLLED_BACK) {
246
+ return true;
247
+ }
248
+ return !(version !== void 0 && isSemver(version) && isSemver(current) && compareSemver(version, current) < 0);
249
+ });
250
+ }
251
+ function describeExit(code, signal) {
252
+ return signal ? `killed by ${signal}` : `exited with code ${code ?? 0}`;
253
+ }
254
+ function resetIfHealthy(state, now, config) {
255
+ if (state.failures === 0 || state.startedAt === null || now - state.startedAt < config.backoffResetMs) {
256
+ return state;
257
+ }
258
+ return {
259
+ ...state,
260
+ delayMs: config.backoffInitialMs,
261
+ failures: 0,
262
+ warnings: clearWarning(state.warnings, WARNING_LINK_FAILING)
263
+ };
264
+ }
265
+ function fail(state, reason, now, config) {
266
+ const delay = state.delayMs;
267
+ const failures = state.failures + 1;
268
+ let warnings = state.warnings;
269
+ if (failures >= config.failingAfter) {
270
+ warnings = setWarning(warnings, {
271
+ code: WARNING_LINK_FAILING,
272
+ message: `the link has failed ${failures} times in a row (last: ${reason}); aibexd keeps restarting it`
273
+ });
274
+ }
275
+ return {
276
+ state: {
277
+ ...state,
278
+ status: "waiting",
279
+ pid: null,
280
+ startedAt: null,
281
+ readyAt: null,
282
+ lastAliveAt: null,
283
+ restartAt: now + delay,
284
+ killAt: null,
285
+ delayMs: Math.min(delay * 2, config.backoffMaxMs),
286
+ failures,
287
+ hung: false,
288
+ notReady: false,
289
+ reason,
290
+ warnings
291
+ },
292
+ actions: [
293
+ {
294
+ type: "log",
295
+ message: `${reason}; restarting in ${formatDuration(delay)}`
296
+ }
297
+ ]
298
+ };
299
+ }
300
+ function startFresh(state, config) {
301
+ return {
302
+ ...state,
303
+ status: "starting",
304
+ pid: null,
305
+ startedAt: null,
306
+ readyAt: null,
307
+ lastAliveAt: null,
308
+ restartAt: null,
309
+ killAt: null,
310
+ afterStop: "exit",
311
+ delayMs: config.backoffInitialMs,
312
+ failures: 0,
313
+ hung: false,
314
+ notReady: false,
315
+ reason: null,
316
+ warnings: clearWarning(state.warnings, WARNING_LINK_FAILING)
317
+ };
318
+ }
319
+ function rollBack(state, reason, config) {
320
+ const from = state.versions.current ?? "";
321
+ const to = state.versions.previous ?? "";
322
+ const failed = `failed ${config.rollbackAfter} times in its first ${formatDuration(config.probationMs)} (last: ${reason})`;
323
+ const why = `it ${failed}`;
324
+ const actions = [
325
+ {
326
+ type: "log",
327
+ message: `aibex-link ${from} ${failed}; rolling back to ${to}`
328
+ },
329
+ { type: "remove-version", version: from },
330
+ { type: "spawn" }
331
+ ];
332
+ if (state.report) {
333
+ actions.push({
334
+ type: "report",
335
+ report: { kind: "rolled-back", from, to, reason: why }
336
+ });
337
+ }
338
+ const fresh = startFresh(state, config);
339
+ return {
340
+ state: {
341
+ ...fresh,
342
+ versions: {
343
+ ...state.versions,
344
+ current: to,
345
+ previous: null,
346
+ bad: { ...state.versions.bad, [from]: `rolled back: ${why}` }
347
+ },
348
+ probation: null,
349
+ report: null,
350
+ warnings: setWarning(fresh.warnings, {
351
+ code: `${WARNING_LINK_ROLLED_BACK}@${from}`,
352
+ message: `aibex-link ${from} was rolled back to ${to}: ${why}; it will not be tried again`
353
+ })
354
+ },
355
+ actions
356
+ };
357
+ }
358
+ function linkFailed(state, reason, now, config) {
359
+ if (state.probation && state.versions.previous !== null) {
360
+ const failures = state.probation.failures + 1;
361
+ if (failures >= config.rollbackAfter) {
362
+ return rollBack(state, reason, config);
363
+ }
364
+ return fail({ ...state, probation: { failures } }, reason, now, config);
365
+ }
366
+ return fail(resetIfHealthy(state, now, config), reason, now, config);
367
+ }
368
+ function idle(state, reason) {
369
+ return {
370
+ ...state,
371
+ status: "idle",
372
+ pid: null,
373
+ reason,
374
+ warnings: setWarning(state.warnings, {
375
+ code: WARNING_LINK_UNAVAILABLE,
376
+ message: `the link is not running: ${reason}`
377
+ })
378
+ };
379
+ }
380
+ function checked(state, result, now, config) {
381
+ const update = state.update;
382
+ if (update?.phase !== "checking") return { state, actions: [] };
383
+ const force = update.force;
384
+ const { current } = state.versions;
385
+ const done = { ...state, update: null };
386
+ const report = (value) => force ? [{ type: "report", report: value }] : [];
387
+ if (!result.ok) {
388
+ const actions = [
389
+ { type: "log", message: `${result.reason}; checking again later` },
390
+ ...report({ kind: "failed", version: null, reason: result.reason })
391
+ ];
392
+ if (current === null) {
393
+ return {
394
+ state: idle(
395
+ {
396
+ ...done,
397
+ nextCheckAt: now + state.delayMs,
398
+ delayMs: Math.min(state.delayMs * 2, config.backoffMaxMs)
399
+ },
400
+ `no link installed, and ${result.reason}`
401
+ ),
402
+ actions
403
+ };
404
+ }
405
+ return {
406
+ state: { ...done, nextCheckAt: now + config.updateCheckMs },
407
+ actions
408
+ };
409
+ }
410
+ const target = result.version;
411
+ const nextCheckAt = now + config.updateCheckMs;
412
+ if (!isNewer(target, current)) {
413
+ return {
414
+ state: {
415
+ ...done,
416
+ nextCheckAt,
417
+ warnings: clearWarning(state.warnings, WARNING_LINK_UPDATE)
418
+ },
419
+ actions: report({ kind: "up-to-date", version: current ?? target })
420
+ };
421
+ }
422
+ const badReason = state.versions.bad[target];
423
+ if (badReason !== void 0) {
424
+ let next2 = { ...done, nextCheckAt };
425
+ if (current === null) {
426
+ next2 = idle(
427
+ next2,
428
+ `aibex-link ${target}, the latest, will not be run (${badReason}); waiting for a newer release`
429
+ );
430
+ }
431
+ return {
432
+ state: next2,
433
+ actions: report({ kind: "bad", version: target, reason: badReason })
434
+ };
435
+ }
436
+ const attempt = state.versions.attempts[target];
437
+ if (!force && attempt && attempt.nextAt > now) {
438
+ return {
439
+ state: { ...done, nextCheckAt: Math.min(nextCheckAt, attempt.nextAt) },
440
+ actions: []
441
+ };
442
+ }
443
+ let next = {
444
+ ...state,
445
+ update: { phase: "installing", version: target, force },
446
+ nextCheckAt: null
447
+ };
448
+ if (current === null) {
449
+ next = {
450
+ ...next,
451
+ status: "idle",
452
+ reason: `installing aibex-link ${target}`
453
+ };
454
+ }
455
+ return {
456
+ state: next,
457
+ actions: [
458
+ {
459
+ type: "log",
460
+ message: current === null ? `installing aibex-link ${target}` : `aibex-link ${target} is available (running ${current}); installing it`
461
+ },
462
+ { type: "install", version: target }
463
+ ]
464
+ };
465
+ }
466
+ function switchTo(state, version, force, now, config) {
467
+ const { current } = state.versions;
468
+ const attempts = { ...state.versions.attempts };
469
+ delete attempts[version];
470
+ const base = {
471
+ ...state,
472
+ update: null,
473
+ nextCheckAt: now + config.updateCheckMs,
474
+ warnings: clearWarning(state.warnings, WARNING_LINK_UPDATE),
475
+ report: force ? { from: current, to: version } : null,
476
+ versions: {
477
+ ...state.versions,
478
+ current: version,
479
+ previous: current,
480
+ attempts
481
+ },
482
+ probation: current === null ? null : { failures: 0 }
483
+ };
484
+ const actions = [
485
+ {
486
+ type: "log",
487
+ message: current === null ? `installed aibex-link ${version}; starting it` : `installed aibex-link ${version}; switching from ${current}`
488
+ }
489
+ ];
490
+ switch (state.status) {
491
+ case "running": {
492
+ return {
493
+ state: {
494
+ ...base,
495
+ status: "stopping",
496
+ afterStop: "start",
497
+ restartAt: null,
498
+ killAt: now + config.linkStopMs
499
+ },
500
+ actions: [...actions, { type: "kill", signal: "SIGTERM" }]
501
+ };
502
+ }
503
+ case "waiting":
504
+ case "idle":
505
+ case "starting": {
506
+ return {
507
+ state: startFresh(base, config),
508
+ actions: [...actions, { type: "spawn" }]
509
+ };
510
+ }
511
+ case "stopping":
512
+ case "stopped": {
513
+ return { state: base, actions };
514
+ }
515
+ }
516
+ }
517
+ function installed(state, outcome, now, config) {
518
+ const update = state.update;
519
+ if (update?.phase !== "installing" || update.version !== outcome.version) {
520
+ return { state, actions: [] };
521
+ }
522
+ const force = update.force;
523
+ const version = outcome.version;
524
+ if (outcome.ok) return switchTo(state, version, force, now, config);
525
+ const { current } = state.versions;
526
+ const reportFailed = force ? [
527
+ {
528
+ type: "report",
529
+ report: { kind: "failed", version, reason: outcome.reason }
530
+ }
531
+ ] : [];
532
+ if (outcome.kind === "bad") {
533
+ const message2 = `aibex-link ${version} will not be run: ${outcome.reason}`;
534
+ let next2 = {
535
+ ...state,
536
+ update: null,
537
+ nextCheckAt: now + config.updateCheckMs,
538
+ versions: {
539
+ ...state.versions,
540
+ bad: { ...state.versions.bad, [version]: outcome.reason }
541
+ },
542
+ warnings: setWarning(state.warnings, {
543
+ code: `${WARNING_LINK_BAD}@${version}`,
544
+ message: message2
545
+ })
546
+ };
547
+ if (current === null) {
548
+ next2 = idle(
549
+ next2,
550
+ `aibex-link ${version}, the latest, will not be run (${outcome.reason}); waiting for a newer release`
551
+ );
552
+ }
553
+ return {
554
+ state: next2,
555
+ actions: [{ type: "log", message: message2 }, ...reportFailed]
556
+ };
557
+ }
558
+ const failures = (state.versions.attempts[version]?.failures ?? 0) + 1;
559
+ const wait = Math.min(
560
+ config.attemptInitialMs * 2 ** (failures - 1),
561
+ config.attemptMaxMs
562
+ );
563
+ const message = outcome.kind === "not-served" ? `aibex-link ${version} is not served by the registry yet (npm: ${outcome.reason}); trying again in ${formatDuration(wait)}` : `this machine cannot self-update aibex-link to ${version}: npm says: ${outcome.reason}; trying again in ${formatDuration(wait)}`;
564
+ let next = {
565
+ ...state,
566
+ update: null,
567
+ nextCheckAt: Math.min(now + config.updateCheckMs, now + wait),
568
+ versions: {
569
+ ...state.versions,
570
+ attempts: {
571
+ ...state.versions.attempts,
572
+ [version]: { failures, nextAt: now + wait }
573
+ }
574
+ },
575
+ warnings: setWarning(state.warnings, {
576
+ code: WARNING_LINK_UPDATE,
577
+ message
578
+ })
579
+ };
580
+ if (current === null) {
581
+ next = idle(next, `no link installed: ${message}`);
582
+ }
583
+ return {
584
+ state: next,
585
+ actions: [{ type: "log", message }, ...reportFailed]
586
+ };
587
+ }
588
+ function aibexdChecked(state, result, installedVersion, now, config) {
589
+ const next = { ...state, nextAibexdCheckAt: now + config.aibexdCheckMs };
590
+ if (!result.ok) return { state: next, actions: [] };
591
+ const latest = result.version;
592
+ if (!isNewer(latest, installedVersion)) {
593
+ return {
594
+ state: {
595
+ ...next,
596
+ warnings: clearWarning(state.warnings, WARNING_AIBEXD_OUTDATED)
597
+ },
598
+ actions: []
599
+ };
600
+ }
601
+ const message = `aibexd ${latest} is published (installed: ${installedVersion}). Run \`npm i -g aibex@${latest}\`; a running aibexd picks it up on its own within minutes, or at once with \`aibexd restart\``;
602
+ const warned = {
603
+ ...next,
604
+ warnings: setWarning(state.warnings, {
605
+ code: WARNING_AIBEXD_OUTDATED,
606
+ message
607
+ })
608
+ };
609
+ if (state.versions.aibexdNotified === latest) {
610
+ return { state: warned, actions: [] };
611
+ }
612
+ return {
613
+ state: {
614
+ ...warned,
615
+ versions: { ...warned.versions, aibexdNotified: latest }
616
+ },
617
+ actions: [{ type: "log", message }]
618
+ };
619
+ }
620
+ function transition(state, event, now, config) {
621
+ const none = { state, actions: [] };
622
+ switch (event.type) {
623
+ case "start": {
624
+ if (state.status !== "starting") return none;
625
+ const scheduled = {
626
+ ...state,
627
+ nextAibexdCheckAt: now,
628
+ nextCheckAt: state.mode === "installed" ? now : null
629
+ };
630
+ if (state.mode === "installed" && state.versions.current === null) {
631
+ return {
632
+ state: idle(
633
+ scheduled,
634
+ "no link installed yet; checking npm for the latest aibex-link"
635
+ ),
636
+ actions: []
637
+ };
638
+ }
639
+ return { state: scheduled, actions: [{ type: "spawn" }] };
640
+ }
641
+ case "spawned": {
642
+ if (state.status !== "starting") return none;
643
+ return {
644
+ state: {
645
+ ...state,
646
+ status: "running",
647
+ pid: event.pid,
648
+ startedAt: now,
649
+ readyAt: null,
650
+ lastAliveAt: null,
651
+ restartAt: null,
652
+ hung: false,
653
+ notReady: false,
654
+ reason: null,
655
+ warnings: clearWarning(state.warnings, WARNING_LINK_UNAVAILABLE)
656
+ },
657
+ actions: []
658
+ };
659
+ }
660
+ case "spawn-failed": {
661
+ if (state.status !== "starting") return none;
662
+ return linkFailed(
663
+ {
664
+ ...state,
665
+ warnings: setWarning(state.warnings, {
666
+ code: WARNING_LINK_UNAVAILABLE,
667
+ message: `the link is not running: ${event.reason}`
668
+ })
669
+ },
670
+ `link not started: ${event.reason}`,
671
+ now,
672
+ config
673
+ );
674
+ }
675
+ case "ready": {
676
+ if (state.status !== "running") return none;
677
+ const actions = [
678
+ { type: "log", message: `link ready (pid ${state.pid})` }
679
+ ];
680
+ let report = state.report;
681
+ if (report && report.to === state.versions.current) {
682
+ actions.push({
683
+ type: "report",
684
+ report: { kind: "switched", from: report.from, to: report.to }
685
+ });
686
+ report = null;
687
+ }
688
+ return {
689
+ state: { ...state, readyAt: now, lastAliveAt: now, report },
690
+ actions
691
+ };
692
+ }
693
+ case "alive": {
694
+ if (state.status !== "running") return none;
695
+ return { state: { ...state, lastAliveAt: now }, actions: [] };
696
+ }
697
+ case "exited": {
698
+ if (state.status === "stopping") {
699
+ if (state.afterStop === "start") {
700
+ return {
701
+ state: startFresh(state, config),
702
+ actions: [{ type: "spawn" }]
703
+ };
704
+ }
705
+ return {
706
+ state: {
707
+ ...state,
708
+ status: "stopped",
709
+ pid: null,
710
+ killAt: null,
711
+ reason: "aibexd stopped the link"
712
+ },
713
+ actions: [{ type: "stopped" }]
714
+ };
715
+ }
716
+ if (state.status !== "running") return none;
717
+ let reason = `link ${describeExit(event.code, event.signal)}`;
718
+ if (state.hung) {
719
+ reason = `link sent no alive signal for ${formatDuration(config.hungMs)} and was killed`;
720
+ } else if (state.notReady) {
721
+ reason = `link did not report ready in ${formatDuration(config.readyTimeoutMs)} and was killed`;
722
+ }
723
+ return linkFailed(state, reason, now, config);
724
+ }
725
+ case "tick": {
726
+ return tick(state, now, config);
727
+ }
728
+ case "stop": {
729
+ if (state.status === "running") {
730
+ return {
731
+ state: {
732
+ ...state,
733
+ status: "stopping",
734
+ afterStop: "exit",
735
+ restartAt: null,
736
+ killAt: now + config.linkStopMs
737
+ },
738
+ actions: [
739
+ { type: "log", message: `stopping the link (pid ${state.pid})` },
740
+ { type: "kill", signal: "SIGTERM" }
741
+ ]
742
+ };
743
+ }
744
+ if (state.status === "stopping") {
745
+ return { state: { ...state, afterStop: "exit" }, actions: [] };
746
+ }
747
+ if (state.status === "stopped") return none;
748
+ return {
749
+ state: {
750
+ ...state,
751
+ status: "stopped",
752
+ restartAt: null,
753
+ reason: "aibexd stopped the link"
754
+ },
755
+ actions: [{ type: "stopped" }]
756
+ };
757
+ }
758
+ case "check-now": {
759
+ if (state.mode !== "installed" || state.status === "stopped") {
760
+ return none;
761
+ }
762
+ if (state.update !== null) {
763
+ return {
764
+ state: { ...state, update: { ...state.update, force: true } },
765
+ actions: []
766
+ };
767
+ }
768
+ return {
769
+ state: {
770
+ ...state,
771
+ update: { phase: "checking", version: null, force: true },
772
+ nextCheckAt: null
773
+ },
774
+ actions: [{ type: "check" }]
775
+ };
776
+ }
777
+ case "checked": {
778
+ return checked(state, event.result, now, config);
779
+ }
780
+ case "installed": {
781
+ return installed(state, event.outcome, now, config);
782
+ }
783
+ case "aibexd-checked": {
784
+ return aibexdChecked(state, event.result, event.installed, now, config);
785
+ }
786
+ }
787
+ }
788
+ function tickLink(state, now, config) {
789
+ if (state.status === "waiting" && state.restartAt !== null && now >= state.restartAt) {
790
+ return {
791
+ state: { ...state, status: "starting", restartAt: null },
792
+ actions: [{ type: "spawn" }]
793
+ };
794
+ }
795
+ if (state.status === "stopping" && state.killAt !== null && now >= state.killAt) {
796
+ return {
797
+ state: { ...state, killAt: null },
798
+ actions: [
799
+ {
800
+ type: "log",
801
+ message: `link (pid ${state.pid}) did not stop in ${formatDuration(config.linkStopMs)}; killing it`
802
+ },
803
+ { type: "kill", signal: "SIGKILL" }
804
+ ]
805
+ };
806
+ }
807
+ if (state.status !== "running" || state.startedAt === null) {
808
+ return { state, actions: [] };
809
+ }
810
+ const healthy = resetIfHealthy(state, now, config);
811
+ const killed = state.hung || state.notReady;
812
+ const lastHeard = state.lastAliveAt ?? state.startedAt;
813
+ if (!killed && now - lastHeard >= config.hungMs) {
814
+ return {
815
+ state: { ...healthy, hung: true },
816
+ actions: [
817
+ {
818
+ type: "log",
819
+ message: `link (pid ${state.pid}) sent no alive signal for ${formatDuration(config.hungMs)}; killing it`
820
+ },
821
+ { type: "kill", signal: "SIGKILL" }
822
+ ]
823
+ };
824
+ }
825
+ if (state.probation && !killed) {
826
+ if (state.readyAt === null && now - state.startedAt >= config.readyTimeoutMs) {
827
+ return {
828
+ state: { ...healthy, notReady: true },
829
+ actions: [
830
+ {
831
+ type: "log",
832
+ message: `link (pid ${state.pid}) did not report ready in ${formatDuration(config.readyTimeoutMs)}; killing it`
833
+ },
834
+ { type: "kill", signal: "SIGKILL" }
835
+ ]
836
+ };
837
+ }
838
+ if (state.readyAt !== null && now - state.startedAt >= config.probationMs) {
839
+ const { current, previous } = state.versions;
840
+ const keep = [current, previous].filter(
841
+ (version) => version !== null
842
+ );
843
+ return {
844
+ state: {
845
+ ...healthy,
846
+ probation: null,
847
+ warnings: current ? clearSuperseded(healthy.warnings, current) : healthy.warnings
848
+ },
849
+ actions: [
850
+ {
851
+ type: "log",
852
+ message: `aibex-link ${current} stayed up for ${formatDuration(config.probationMs)}; keeping it`
853
+ },
854
+ { type: "prune", keep }
855
+ ]
856
+ };
857
+ }
858
+ }
859
+ return { state: healthy, actions: [] };
860
+ }
861
+ function tick(state, now, config) {
862
+ const linked = tickLink(state, now, config);
863
+ let next = linked.state;
864
+ const actions = [...linked.actions];
865
+ if (next.mode === "installed" && next.status !== "stopped" && next.update === null && next.nextCheckAt !== null && now >= next.nextCheckAt) {
866
+ next = {
867
+ ...next,
868
+ update: { phase: "checking", version: null, force: false },
869
+ nextCheckAt: null
870
+ };
871
+ actions.push({ type: "check" });
872
+ }
873
+ if (next.status !== "stopped" && next.nextAibexdCheckAt !== null && now >= next.nextAibexdCheckAt) {
874
+ next = { ...next, nextAibexdCheckAt: null };
875
+ actions.push({ type: "check-aibexd" });
876
+ }
877
+ return { state: next, actions };
878
+ }
879
+ function linkWake(state, config) {
880
+ switch (state.status) {
881
+ case "waiting": {
882
+ return state.restartAt;
883
+ }
884
+ case "stopping": {
885
+ return state.killAt;
886
+ }
887
+ case "running": {
888
+ if (state.startedAt === null) return null;
889
+ const times = [];
890
+ const killed = state.hung || state.notReady;
891
+ if (!killed) {
892
+ times.push((state.lastAliveAt ?? state.startedAt) + config.hungMs);
893
+ }
894
+ if (state.failures > 0) {
895
+ times.push(state.startedAt + config.backoffResetMs);
896
+ }
897
+ if (state.probation && !killed) {
898
+ times.push(
899
+ state.startedAt + (state.readyAt === null ? config.readyTimeoutMs : config.probationMs)
900
+ );
901
+ }
902
+ return times.length > 0 ? Math.min(...times) : null;
903
+ }
904
+ default: {
905
+ return null;
906
+ }
907
+ }
908
+ }
909
+ function nextWake(state, config) {
910
+ if (state.status === "stopped") return null;
911
+ const times = [];
912
+ const link = linkWake(state, config);
913
+ if (link !== null) times.push(link);
914
+ if (state.mode === "installed" && state.update === null && state.nextCheckAt !== null) {
915
+ times.push(state.nextCheckAt);
916
+ }
917
+ if (state.nextAibexdCheckAt !== null) times.push(state.nextAibexdCheckAt);
918
+ return times.length > 0 ? Math.min(...times) : null;
919
+ }
920
+ const MAX_REQUEST_BYTES = 64 * 1024;
921
+ const MAX_SOCKET_PATH_BYTES = 100;
922
+ function socketAddress(socketFile) {
923
+ if (Buffer.byteLength(socketFile) <= MAX_SOCKET_PATH_BYTES) return socketFile;
924
+ process.chdir(path.dirname(socketFile));
925
+ return path.basename(socketFile);
926
+ }
927
+ function listenOnSocket(socketFile, handle, onError) {
928
+ rmSync(socketFile, { force: true });
929
+ const server = createServer((connection) => {
930
+ let buffer = "";
931
+ let answered = false;
932
+ connection.setEncoding("utf8");
933
+ const answer = (response) => {
934
+ if (answered) return;
935
+ answered = true;
936
+ connection.end(`${JSON.stringify(response)}
937
+ `);
938
+ };
939
+ connection.on("data", (chunk) => {
940
+ buffer += chunk;
941
+ if (buffer.length > MAX_REQUEST_BYTES) {
942
+ answer({ ok: false, error: "request too large" });
943
+ return;
944
+ }
945
+ const newline = buffer.indexOf("\n");
946
+ if (newline === -1) return;
947
+ let request;
948
+ try {
949
+ request = JSON.parse(buffer.slice(0, newline));
950
+ } catch {
951
+ answer({ ok: false, error: "the request is not JSON" });
952
+ return;
953
+ }
954
+ if (request?.type !== "update") {
955
+ answer({ ok: false, error: "unknown request" });
956
+ return;
957
+ }
958
+ void (async () => {
959
+ try {
960
+ answer(await handle({ type: "update" }));
961
+ } catch (error) {
962
+ answer({ ok: false, error: String(error) });
963
+ }
964
+ })();
965
+ });
966
+ connection.on("error", () => {
967
+ });
968
+ });
969
+ server.on("error", onError);
970
+ try {
971
+ server.listen(socketAddress(socketFile));
972
+ } catch (error) {
973
+ onError(error);
974
+ }
975
+ return server;
976
+ }
977
+ const LINK_PACKAGE = "@aibexhq/aibex-link";
978
+ const AIBEXD_PACKAGE = "aibex";
979
+ function runNpm(args, options) {
980
+ return new Promise((resolve) => {
981
+ let stdout = "";
982
+ let stderr = "";
983
+ let timedOut = false;
984
+ let settled = false;
985
+ const finish = (result) => {
986
+ if (settled) return;
987
+ settled = true;
988
+ clearTimeout(timer);
989
+ resolve(result);
990
+ };
991
+ const child = spawn("npm", args, {
992
+ cwd: options.cwd,
993
+ env: options.env ?? process.env,
994
+ stdio: ["ignore", "pipe", "pipe"]
995
+ });
996
+ const timer = setTimeout(() => {
997
+ timedOut = true;
998
+ child.kill("SIGKILL");
999
+ }, options.timeoutMs);
1000
+ child.stdout.on("data", (chunk) => {
1001
+ stdout += chunk.toString();
1002
+ });
1003
+ child.stderr.on("data", (chunk) => {
1004
+ stderr += chunk.toString();
1005
+ });
1006
+ child.once("error", (error) => {
1007
+ finish({
1008
+ code: null,
1009
+ stdout,
1010
+ stderr,
1011
+ timedOut,
1012
+ spawnError: error.message
1013
+ });
1014
+ });
1015
+ child.once("close", (code) => {
1016
+ finish({ code: timedOut ? null : code, stdout, stderr, timedOut });
1017
+ });
1018
+ });
1019
+ }
1020
+ function firstNpmErrorLine(output) {
1021
+ for (const raw of output.split("\n")) {
1022
+ const line = raw.trim();
1023
+ const match = /^npm (?:error|ERR!)\s*(.*)$/.exec(line);
1024
+ if (!match) continue;
1025
+ const message = (match[1] ?? "").trim();
1026
+ if (message === "" || /^(?:code|syscall|errno)\b/.test(message)) continue;
1027
+ return message.replace(/^notarget\s+/, "");
1028
+ }
1029
+ return void 0;
1030
+ }
1031
+ function npmFailureReason(result, what) {
1032
+ if (result.spawnError) return `could not run npm: ${result.spawnError}`;
1033
+ if (result.timedOut) return `npm ${what} timed out`;
1034
+ return firstNpmErrorLine(result.stderr) ?? `npm ${what} exited with code ${result.code}`;
1035
+ }
1036
+ async function checkLatest(packageName, options) {
1037
+ const result = await runNpm(
1038
+ ["view", packageName, "dist-tags.latest", "--json", "--prefer-online"],
1039
+ options
1040
+ );
1041
+ if (result.code !== 0) {
1042
+ return {
1043
+ ok: false,
1044
+ kind: "unreachable",
1045
+ reason: `could not ask npm for ${packageName}: ${npmFailureReason(result, "view")}`
1046
+ };
1047
+ }
1048
+ let answer;
1049
+ try {
1050
+ answer = JSON.parse(result.stdout);
1051
+ } catch {
1052
+ answer = result.stdout.trim();
1053
+ }
1054
+ if (!isSemver(answer)) {
1055
+ return {
1056
+ ok: false,
1057
+ kind: "malformed",
1058
+ reason: `npm answered ${JSON.stringify(answer)} for the latest ${packageName}, which is not a version`
1059
+ };
1060
+ }
1061
+ return { ok: true, version: answer };
1062
+ }
1063
+ function classifyInstallFailure(result) {
1064
+ if (result.spawnError) {
1065
+ return {
1066
+ kind: "cannot-self-update",
1067
+ reason: `could not run npm: ${result.spawnError}`
1068
+ };
1069
+ }
1070
+ if (result.timedOut) {
1071
+ return { kind: "cannot-self-update", reason: "npm install timed out" };
1072
+ }
1073
+ const output = `${result.stderr}
1074
+ ${result.stdout}`;
1075
+ const code = /^npm (?:error|ERR!) code (\S+)/m.exec(output)?.[1];
1076
+ const reason = firstNpmErrorLine(output) ?? `npm install exited with code ${result.code}`;
1077
+ const releaseAge = /with a date before/.test(output);
1078
+ if ((code === "ETARGET" || code === "E404") && !releaseAge) {
1079
+ return { kind: "not-served", reason };
1080
+ }
1081
+ return { kind: "cannot-self-update", reason };
1082
+ }
1083
+ function versionDirectory(paths, version) {
1084
+ return path.join(paths.versionsDir, version);
1085
+ }
1086
+ function partialDirectory(paths, version) {
1087
+ return path.join(paths.versionsDir, `${version}.partial`);
1088
+ }
1089
+ function linkEntry(directory) {
1090
+ const root = path.join(directory, "node_modules", ...LINK_PACKAGE.split("/"));
1091
+ let main;
1092
+ try {
1093
+ main = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).main;
1094
+ } catch {
1095
+ return void 0;
1096
+ }
1097
+ const entry = path.join(
1098
+ root,
1099
+ typeof main === "string" ? main : "dist/index.js"
1100
+ );
1101
+ return existsSync(entry) ? entry : void 0;
1102
+ }
1103
+ function listInstalledVersions(paths) {
1104
+ let names;
1105
+ try {
1106
+ names = readdirSync(paths.versionsDir);
1107
+ } catch {
1108
+ return [];
1109
+ }
1110
+ return names.filter(
1111
+ (name) => isSemver(name) && linkEntry(versionDirectory(paths, name)) !== void 0
1112
+ ).toSorted(compareSemver);
1113
+ }
1114
+ async function installLinkVersion(paths, version, deps) {
1115
+ const target = versionDirectory(paths, version);
1116
+ if (linkEntry(target) !== void 0) {
1117
+ return { ok: true, version, directory: target };
1118
+ }
1119
+ const partial = partialDirectory(paths, version);
1120
+ rmSync(partial, { recursive: true, force: true });
1121
+ mkdirSync(partial, { recursive: true });
1122
+ const fail2 = (kind, reason) => {
1123
+ rmSync(partial, { recursive: true, force: true });
1124
+ return { ok: false, version, kind, reason };
1125
+ };
1126
+ try {
1127
+ writeJsonAtomicSync(path.join(partial, "package.json"), {
1128
+ name: "aibex-link-install",
1129
+ private: true,
1130
+ description: `aibex-link ${version}, installed by aibexd`
1131
+ });
1132
+ const result = await deps.npm(
1133
+ [
1134
+ "install",
1135
+ "--ignore-scripts",
1136
+ "--no-audit",
1137
+ "--no-fund",
1138
+ `${LINK_PACKAGE}@${version}`
1139
+ ],
1140
+ partial
1141
+ );
1142
+ if (result.code !== 0) {
1143
+ const failure = classifyInstallFailure(result);
1144
+ return fail2(failure.kind, failure.reason);
1145
+ }
1146
+ if (linkEntry(partial) === void 0) {
1147
+ return fail2(
1148
+ "bad",
1149
+ `npm installed ${LINK_PACKAGE}@${version} but it has no entry file`
1150
+ );
1151
+ }
1152
+ for (const check of deps.checks) {
1153
+ const checked2 = await check(partial, version);
1154
+ if (!checked2.ok) {
1155
+ return fail2(checked2.bad ? "bad" : "cannot-self-update", checked2.reason);
1156
+ }
1157
+ }
1158
+ rmSync(target, { recursive: true, force: true });
1159
+ await rename(partial, target);
1160
+ return { ok: true, version, directory: target };
1161
+ } catch (error) {
1162
+ return fail2("cannot-self-update", `install failed: ${String(error)}`);
1163
+ }
1164
+ }
1165
+ function removeVersion(paths, version) {
1166
+ if (!isSemver(version)) return;
1167
+ rmSync(versionDirectory(paths, version), { recursive: true, force: true });
1168
+ }
1169
+ function pruneVersions(paths, keep) {
1170
+ let names;
1171
+ try {
1172
+ names = readdirSync(paths.versionsDir);
1173
+ } catch {
1174
+ return [];
1175
+ }
1176
+ const removed = [];
1177
+ for (const name of names) {
1178
+ if (!isSemver(name) || keep.includes(name)) continue;
1179
+ rmSync(versionDirectory(paths, name), { recursive: true, force: true });
1180
+ removed.push(name);
1181
+ }
1182
+ return removed.toSorted(compareSemver);
1183
+ }
1184
+ const SLSA_PROVENANCE_V1 = "https://slsa.dev/provenance/v1";
1185
+ const IN_TOTO_PAYLOAD = "application/vnd.in-toto+json";
1186
+ const DEFAULT_ATTESTATIONS_REGISTRY = "https://registry.npmjs.org";
1187
+ const AIBEX_LINK_PROVENANCE = {
1188
+ packageName: LINK_PACKAGE,
1189
+ repository: "https://github.com/aibexhq/aibex",
1190
+ workflowPath: ".github/workflows/release-daemon.yml"
1191
+ };
1192
+ function record(value) {
1193
+ return typeof value === "object" && value !== null ? value : void 0;
1194
+ }
1195
+ function attestationsUrl(registry, packageName, version) {
1196
+ return `${registry.replace(/\/+$/, "")}/-/npm/v1/attestations/${packageName.replace("/", "%2f")}@${version}`;
1197
+ }
1198
+ const bad$1 = (reason) => ({ ok: false, bad: true, reason });
1199
+ function checkAttestations(body, version, expected) {
1200
+ const label = `${expected.packageName}@${version}`;
1201
+ const attestations = record(body)?.attestations;
1202
+ if (!Array.isArray(attestations)) {
1203
+ return bad$1(`${label} has no provenance attestation`);
1204
+ }
1205
+ const provenance = attestations.map((item) => record(item)).find((item) => item?.predicateType === SLSA_PROVENANCE_V1);
1206
+ if (!provenance) return bad$1(`${label} has no provenance attestation`);
1207
+ const envelope = record(record(provenance.bundle)?.dsseEnvelope);
1208
+ if (envelope?.payloadType !== IN_TOTO_PAYLOAD || typeof envelope.payload !== "string") {
1209
+ return bad$1(`${label} has an invalid provenance attestation`);
1210
+ }
1211
+ let statement;
1212
+ try {
1213
+ statement = record(
1214
+ JSON.parse(Buffer.from(envelope.payload, "base64").toString("utf8"))
1215
+ );
1216
+ } catch {
1217
+ statement = void 0;
1218
+ }
1219
+ if (!statement || statement.predicateType !== SLSA_PROVENANCE_V1) {
1220
+ return bad$1(`${label} has an invalid provenance attestation`);
1221
+ }
1222
+ const subjectName = `pkg:npm/${expected.packageName}@${version}`;
1223
+ const subjects = Array.isArray(statement.subject) ? statement.subject : [];
1224
+ const about = subjects.some((subject) => {
1225
+ const name = record(subject)?.name;
1226
+ if (typeof name !== "string") return false;
1227
+ try {
1228
+ return decodeURIComponent(name) === subjectName;
1229
+ } catch {
1230
+ return false;
1231
+ }
1232
+ });
1233
+ if (!about) {
1234
+ return bad$1(`${label}'s provenance attestation is about another package`);
1235
+ }
1236
+ const workflow = record(
1237
+ record(
1238
+ record(record(statement.predicate)?.buildDefinition)?.externalParameters
1239
+ )?.workflow
1240
+ );
1241
+ const repository = workflow?.repository;
1242
+ const workflowPath = workflow?.path;
1243
+ if (typeof repository !== "string" || typeof workflowPath !== "string") {
1244
+ return bad$1(`${label}'s provenance names no source repository`);
1245
+ }
1246
+ if (repository.toLowerCase() !== expected.repository.toLowerCase()) {
1247
+ return bad$1(
1248
+ `${label} was built from ${repository}, not ${expected.repository}`
1249
+ );
1250
+ }
1251
+ if (workflowPath !== expected.workflowPath) {
1252
+ return bad$1(
1253
+ `${label} was built by ${workflowPath}, not ${expected.workflowPath}`
1254
+ );
1255
+ }
1256
+ return { ok: true };
1257
+ }
1258
+ function checkAuditSignatures(result, label) {
1259
+ let report;
1260
+ try {
1261
+ report = record(JSON.parse(result.stdout));
1262
+ } catch {
1263
+ report = void 0;
1264
+ }
1265
+ const invalid = Array.isArray(report?.invalid) ? report.invalid : void 0;
1266
+ const missing = Array.isArray(report?.missing) ? report.missing : void 0;
1267
+ if (invalid && invalid.length > 0) {
1268
+ return bad$1(`npm audit signatures found invalid signatures on ${label}`);
1269
+ }
1270
+ if (missing && missing.length > 0) {
1271
+ return bad$1(`npm audit signatures found missing signatures on ${label}`);
1272
+ }
1273
+ if (result.code !== 0 || !invalid || !missing) {
1274
+ return {
1275
+ ok: false,
1276
+ bad: false,
1277
+ reason: `could not verify ${label}'s signatures: ${npmFailureReason(result, "audit signatures")}`
1278
+ };
1279
+ }
1280
+ return { ok: true };
1281
+ }
1282
+ async function verifyProvenance(directory, version, deps) {
1283
+ const label = `${deps.expected.packageName}@${version}`;
1284
+ const audit = await deps.npm(["audit", "signatures", "--json"], directory);
1285
+ const audited = checkAuditSignatures(audit, label);
1286
+ if (!audited.ok) return audited;
1287
+ const url = attestationsUrl(
1288
+ deps.registry,
1289
+ deps.expected.packageName,
1290
+ version
1291
+ );
1292
+ let response;
1293
+ try {
1294
+ response = await deps.fetch(url, {
1295
+ signal: AbortSignal.timeout(deps.timeoutMs)
1296
+ });
1297
+ } catch (error) {
1298
+ return {
1299
+ ok: false,
1300
+ bad: false,
1301
+ reason: `could not fetch ${label}'s attestations: ${error.message}`
1302
+ };
1303
+ }
1304
+ if (response.status === 404) {
1305
+ return bad$1(`${label} has no provenance attestation`);
1306
+ }
1307
+ if (!response.ok) {
1308
+ return {
1309
+ ok: false,
1310
+ bad: false,
1311
+ reason: `could not fetch ${label}'s attestations: HTTP ${response.status}`
1312
+ };
1313
+ }
1314
+ let body;
1315
+ try {
1316
+ body = await response.json();
1317
+ } catch {
1318
+ return bad$1(`${label} has an invalid provenance attestation`);
1319
+ }
1320
+ return checkAttestations(body, version, deps.expected);
1321
+ }
1322
+ const bad = (reason) => ({ ok: false, bad: true, reason });
1323
+ function runSelfTest(entry, version, timeoutMs) {
1324
+ const label = `${LINK_PACKAGE}@${version}`;
1325
+ return new Promise((resolve) => {
1326
+ let stdout = "";
1327
+ let timedOut = false;
1328
+ const child = spawn(process.execPath, [entry, "--self-test"], {
1329
+ stdio: ["ignore", "pipe", "ignore"],
1330
+ // The link gets nothing of aibexd's: it must not reach anything.
1331
+ env: { PATH: process.env.PATH ?? "" }
1332
+ });
1333
+ const timer = setTimeout(() => {
1334
+ timedOut = true;
1335
+ child.kill("SIGKILL");
1336
+ }, timeoutMs);
1337
+ child.stdout.on("data", (chunk) => {
1338
+ stdout += chunk.toString();
1339
+ });
1340
+ child.once("error", (error) => {
1341
+ clearTimeout(timer);
1342
+ resolve(bad(`${label}'s self-test did not run: ${error.message}`));
1343
+ });
1344
+ child.once("close", (code, signal) => {
1345
+ clearTimeout(timer);
1346
+ if (timedOut) {
1347
+ resolve(
1348
+ bad(
1349
+ `${label}'s self-test did not finish in ${formatDuration(timeoutMs)}`
1350
+ )
1351
+ );
1352
+ return;
1353
+ }
1354
+ if (code !== 0) {
1355
+ resolve(
1356
+ bad(
1357
+ `${label}'s self-test ${signal ? `was killed by ${signal}` : `exited with code ${code}`}`
1358
+ )
1359
+ );
1360
+ return;
1361
+ }
1362
+ const last = stdout.trim().split("\n").at(-1) ?? "";
1363
+ let report;
1364
+ try {
1365
+ report = JSON.parse(last);
1366
+ } catch {
1367
+ report = void 0;
1368
+ }
1369
+ if (report?.name !== LINK_PACKAGE || report.version !== version) {
1370
+ resolve(
1371
+ bad(
1372
+ `${label}'s self-test reported ${last === "" ? "nothing" : last}, not ${JSON.stringify({ name: LINK_PACKAGE, version })}`
1373
+ )
1374
+ );
1375
+ return;
1376
+ }
1377
+ resolve({ ok: true });
1378
+ });
1379
+ });
1380
+ }
1381
+ function createUpdater(paths, env, timings) {
1382
+ const npm = (args, cwd) => runNpm(args, { cwd, env, timeoutMs: timings.npmInstallMs });
1383
+ const deps = {
1384
+ npm,
1385
+ checks: [
1386
+ (directory, version) => verifyProvenance(directory, version, {
1387
+ npm,
1388
+ fetch,
1389
+ registry: env.AIBEX_ATTESTATIONS_REGISTRY ?? DEFAULT_ATTESTATIONS_REGISTRY,
1390
+ expected: AIBEX_LINK_PROVENANCE,
1391
+ timeoutMs: timings.npmViewMs
1392
+ }),
1393
+ (directory, version) => {
1394
+ const entry = linkEntry(directory);
1395
+ return entry ? runSelfTest(entry, version, timings.selfTestMs) : Promise.resolve({
1396
+ ok: false,
1397
+ bad: true,
1398
+ reason: `${LINK_PACKAGE}@${version} has no entry file`
1399
+ });
1400
+ }
1401
+ ]
1402
+ };
1403
+ return {
1404
+ latestLink: () => checkLatest(LINK_PACKAGE, { env, timeoutMs: timings.npmViewMs }),
1405
+ latestAibexd: () => checkLatest(AIBEXD_PACKAGE, { env, timeoutMs: timings.npmViewMs }),
1406
+ install: (version) => installLinkVersion(paths, version, deps)
1407
+ };
1408
+ }
1409
+ function iso$1(time) {
1410
+ return new Date(time).toISOString();
1411
+ }
1412
+ function serializeVersions(versions) {
1413
+ const attempts = {};
1414
+ for (const [version, attempt] of Object.entries(versions.attempts)) {
1415
+ attempts[version] = {
1416
+ failures: attempt.failures,
1417
+ nextAt: iso$1(attempt.nextAt)
1418
+ };
1419
+ }
1420
+ return { ...versions, bad: { ...versions.bad }, attempts };
1421
+ }
1422
+ function stringRecord(value) {
1423
+ const result = {};
1424
+ if (typeof value !== "object" || value === null) return result;
1425
+ for (const [key, item] of Object.entries(value)) {
1426
+ if (isSemver(key) && typeof item === "string") result[key] = item;
1427
+ }
1428
+ return result;
1429
+ }
1430
+ function restoreVersions(paths) {
1431
+ const versions = emptyVersions();
1432
+ const recorded = readSupervisorState(paths)?.versions;
1433
+ if (recorded && typeof recorded === "object") {
1434
+ versions.bad = stringRecord(recorded.bad);
1435
+ if (typeof recorded.attempts === "object" && recorded.attempts !== null) {
1436
+ for (const [version, attempt] of Object.entries(recorded.attempts)) {
1437
+ const nextAt = Date.parse(String(attempt?.nextAt));
1438
+ if (isSemver(version) && typeof attempt?.failures === "number" && Number.isFinite(nextAt)) {
1439
+ versions.attempts[version] = { failures: attempt.failures, nextAt };
1440
+ }
1441
+ }
1442
+ }
1443
+ if (typeof recorded.aibexdNotified === "string") {
1444
+ versions.aibexdNotified = recorded.aibexdNotified;
1445
+ }
1446
+ }
1447
+ const installed2 = listInstalledVersions(paths).filter(
1448
+ (version) => versions.bad[version] === void 0
1449
+ );
1450
+ const usable = (version) => typeof version === "string" && installed2.includes(version);
1451
+ versions.current = usable(recorded?.current) ? recorded.current : installed2.at(-1) ?? null;
1452
+ versions.previous = usable(recorded?.previous) && recorded.previous !== versions.current ? recorded.previous : null;
1453
+ return versions;
1454
+ }
1455
+ const DEFAULT_TIMINGS = {
1456
+ backoffInitialMs: 1e3,
1457
+ backoffMaxMs: 5 * 6e4,
1458
+ backoffResetMs: 10 * 6e4,
1459
+ hungMs: 9e4,
1460
+ linkStopMs: 1e4,
1461
+ supervisorStopMs: 12e3,
1462
+ versionCheckMs: 5 * 6e4,
1463
+ stopWaitMs: 15e3,
1464
+ updateCheckMs: 60 * 6e4,
1465
+ attemptInitialMs: 15 * 6e4,
1466
+ attemptMaxMs: 6 * 60 * 6e4,
1467
+ probationMs: 2 * 6e4,
1468
+ readyTimeoutMs: 3e4,
1469
+ selfTestMs: 3e4,
1470
+ npmViewMs: 6e4,
1471
+ npmInstallMs: 10 * 6e4,
1472
+ aibexdCheckMs: 24 * 60 * 6e4,
1473
+ updateWaitMs: 15 * 6e4
1474
+ };
1475
+ function readTimings(env = process.env) {
1476
+ const timings = { ...DEFAULT_TIMINGS };
1477
+ const raw = env.AIBEX_TIMINGS;
1478
+ if (!raw) return timings;
1479
+ let overrides;
1480
+ try {
1481
+ overrides = JSON.parse(raw);
1482
+ } catch {
1483
+ return timings;
1484
+ }
1485
+ if (typeof overrides !== "object" || overrides === null) return timings;
1486
+ for (const key of Object.keys(timings)) {
1487
+ const value = overrides[key];
1488
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
1489
+ timings[key] = value;
1490
+ }
1491
+ }
1492
+ return timings;
1493
+ }
1494
+ function iso(time) {
1495
+ return time === null ? null : new Date(time).toISOString();
1496
+ }
1497
+ function linkEnvironment(env) {
1498
+ const result = { ...env };
1499
+ delete result.AIBEX_HOME;
1500
+ delete result.AIBEX_LINK_PATH;
1501
+ delete result.AIBEX_ATTESTATIONS_REGISTRY;
1502
+ return result;
1503
+ }
1504
+ function runSupervisor(env = process.env) {
1505
+ const paths = statePaths(aibexHome(env));
1506
+ const host = env.AIBEX_HOST ?? "";
1507
+ const timings = readTimings(env);
1508
+ const config = machineConfig(timings);
1509
+ const version = readInstalledVersion();
1510
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1511
+ const localLink = env.AIBEX_LINK_PATH;
1512
+ const updater = createUpdater(paths, env, timings);
1513
+ let state = initialState(
1514
+ config,
1515
+ localLink ? void 0 : restoreVersions(paths)
1516
+ );
1517
+ let child;
1518
+ let link;
1519
+ let wakeTimer;
1520
+ const queue = [];
1521
+ let dispatching = false;
1522
+ const waiting = [];
1523
+ const save = () => {
1524
+ const file = {
1525
+ supervisor: { pid: process.pid, version, startedAt },
1526
+ host,
1527
+ link: {
1528
+ status: state.status,
1529
+ pid: state.pid,
1530
+ version: link?.version ?? state.versions.current,
1531
+ source: link?.source ?? (localLink ? "local" : null),
1532
+ path: link?.path ?? null,
1533
+ startedAt: iso(state.startedAt),
1534
+ readyAt: iso(state.readyAt),
1535
+ lastAliveAt: iso(state.lastAliveAt),
1536
+ restartAt: iso(state.restartAt),
1537
+ reason: state.reason
1538
+ },
1539
+ warnings: state.warnings,
1540
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1541
+ };
1542
+ if (state.mode === "installed") {
1543
+ file.versions = serializeVersions(state.versions);
1544
+ }
1545
+ try {
1546
+ writeSupervisorState(paths, file);
1547
+ } catch (error) {
1548
+ log("supervisor", `could not write ${paths.stateFile}: ${String(error)}`);
1549
+ }
1550
+ };
1551
+ const resolveLink = () => {
1552
+ if (state.mode === "local") {
1553
+ const source = resolveLinkSource(env);
1554
+ if (source.kind === "unavailable") {
1555
+ return { ok: false, reason: source.reason };
1556
+ }
1557
+ return {
1558
+ ok: true,
1559
+ entry: source.entry,
1560
+ link: { version: source.version, source: "local", path: source.path }
1561
+ };
1562
+ }
1563
+ const current = state.versions.current;
1564
+ if (current === null) return { ok: false, reason: "no link installed" };
1565
+ const directory = versionDirectory(paths, current);
1566
+ const entry = linkEntry(directory);
1567
+ if (!entry) {
1568
+ return {
1569
+ ok: false,
1570
+ reason: `aibex-link ${current} is not installed in ${directory}`
1571
+ };
1572
+ }
1573
+ return {
1574
+ ok: true,
1575
+ entry,
1576
+ link: { version: current, source: "installed", path: directory }
1577
+ };
1578
+ };
1579
+ const spawnLink = () => {
1580
+ const resolved = resolveLink();
1581
+ if (!resolved.ok) {
1582
+ queue.push({ type: "spawn-failed", reason: resolved.reason });
1583
+ return;
1584
+ }
1585
+ let started;
1586
+ try {
1587
+ started = fork(resolved.entry, [], {
1588
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
1589
+ env: linkEnvironment(env)
1590
+ });
1591
+ } catch (error) {
1592
+ queue.push({ type: "spawn-failed", reason: String(error) });
1593
+ return;
1594
+ }
1595
+ if (started.pid === void 0) {
1596
+ started.once("error", (error) => {
1597
+ dispatch({ type: "spawn-failed", reason: error.message });
1598
+ });
1599
+ return;
1600
+ }
1601
+ child = started;
1602
+ link = resolved.link;
1603
+ log(
1604
+ "supervisor",
1605
+ `started aibex-link ${resolved.link.version} from ${resolved.link.path} (pid ${started.pid})`
1606
+ );
1607
+ started.on("message", (message) => {
1608
+ if (child !== started) return;
1609
+ if (message.type === "ready") dispatch({ type: "ready" });
1610
+ else if (message.type === "alive") dispatch({ type: "alive" });
1611
+ });
1612
+ started.on("error", (error) => {
1613
+ log("supervisor", `link error: ${error.message}`);
1614
+ });
1615
+ started.on("exit", (code, signal) => {
1616
+ if (child !== started) return;
1617
+ child = void 0;
1618
+ dispatch({ type: "exited", code, signal });
1619
+ });
1620
+ queue.push({ type: "spawned", pid: started.pid });
1621
+ };
1622
+ const background = (work) => {
1623
+ void (async () => {
1624
+ let event;
1625
+ try {
1626
+ event = await work();
1627
+ } catch (error) {
1628
+ log("supervisor", `update step failed: ${String(error)}`);
1629
+ return;
1630
+ }
1631
+ dispatch(event);
1632
+ })();
1633
+ };
1634
+ const perform = (action) => {
1635
+ switch (action.type) {
1636
+ case "spawn": {
1637
+ spawnLink();
1638
+ break;
1639
+ }
1640
+ case "kill": {
1641
+ child?.kill(action.signal);
1642
+ break;
1643
+ }
1644
+ case "log": {
1645
+ log("supervisor", action.message);
1646
+ break;
1647
+ }
1648
+ case "stopped": {
1649
+ clearTimeout(wakeTimer);
1650
+ save();
1651
+ log("supervisor", "stopped");
1652
+ rmSync(paths.socketFile, { force: true });
1653
+ process.exit(0);
1654
+ break;
1655
+ }
1656
+ case "check": {
1657
+ background(async () => ({
1658
+ type: "checked",
1659
+ result: await updater.latestLink()
1660
+ }));
1661
+ break;
1662
+ }
1663
+ case "install": {
1664
+ const target = action.version;
1665
+ background(async () => ({
1666
+ type: "installed",
1667
+ outcome: await updater.install(target)
1668
+ }));
1669
+ break;
1670
+ }
1671
+ case "remove-version": {
1672
+ removeVersion(paths, action.version);
1673
+ break;
1674
+ }
1675
+ case "prune": {
1676
+ const removed = pruneVersions(paths, action.keep);
1677
+ if (removed.length > 0) {
1678
+ log("supervisor", `removed aibex-link ${removed.join(", ")}`);
1679
+ }
1680
+ break;
1681
+ }
1682
+ case "check-aibexd": {
1683
+ background(async () => {
1684
+ const result = await updater.latestAibexd();
1685
+ let installed2 = version;
1686
+ try {
1687
+ installed2 = readInstalledVersion(PACKAGE_ROOT);
1688
+ } catch {
1689
+ }
1690
+ return { type: "aibexd-checked", result, installed: installed2 };
1691
+ });
1692
+ break;
1693
+ }
1694
+ case "report": {
1695
+ for (const resolve of waiting.splice(0)) resolve(action.report);
1696
+ break;
1697
+ }
1698
+ }
1699
+ };
1700
+ const schedule = () => {
1701
+ clearTimeout(wakeTimer);
1702
+ const wake = nextWake(state, config);
1703
+ if (wake === null) return;
1704
+ wakeTimer = setTimeout(
1705
+ () => dispatch({ type: "tick" }),
1706
+ Math.max(0, wake - Date.now())
1707
+ );
1708
+ };
1709
+ function dispatch(event) {
1710
+ queue.push(event);
1711
+ if (dispatching) return;
1712
+ dispatching = true;
1713
+ try {
1714
+ let next;
1715
+ while (next = queue.shift()) {
1716
+ const result = transition(state, next, Date.now(), config);
1717
+ state = result.state;
1718
+ for (const action of result.actions) perform(action);
1719
+ }
1720
+ } finally {
1721
+ dispatching = false;
1722
+ }
1723
+ save();
1724
+ schedule();
1725
+ }
1726
+ const stop = () => dispatch({ type: "stop" });
1727
+ process.on("SIGTERM", stop);
1728
+ process.on("SIGINT", stop);
1729
+ process.on("disconnect", stop);
1730
+ const versionTimer = setInterval(() => {
1731
+ let installed2;
1732
+ try {
1733
+ installed2 = readInstalledVersion(PACKAGE_ROOT);
1734
+ } catch {
1735
+ return;
1736
+ }
1737
+ if (installed2 === version) return;
1738
+ clearInterval(versionTimer);
1739
+ log(
1740
+ "supervisor",
1741
+ `aibexd ${installed2} is installed (running ${version}); restarting the supervisor`
1742
+ );
1743
+ stop();
1744
+ }, timings.versionCheckMs);
1745
+ log(
1746
+ "supervisor",
1747
+ `aibexd ${version} supervisor started (pid ${process.pid}) for ${host}`
1748
+ );
1749
+ if (state.mode === "installed") {
1750
+ const { current, previous } = state.versions;
1751
+ log(
1752
+ "supervisor",
1753
+ current ? `aibex-link ${current} installed${previous ? ` (previous: ${previous})` : ""}` : "no aibex-link installed yet"
1754
+ );
1755
+ }
1756
+ dispatch({ type: "start" });
1757
+ const updateNow = () => new Promise((resolve) => {
1758
+ waiting.push(resolve);
1759
+ dispatch({ type: "check-now" });
1760
+ });
1761
+ listenOnSocket(
1762
+ paths.socketFile,
1763
+ async () => {
1764
+ if (localLink) {
1765
+ return {
1766
+ ok: false,
1767
+ error: `aibexd is running the local link at ${localLink} (AIBEX_LINK_PATH), which it never installs or updates. Stop aibexd and start it without AIBEX_LINK_PATH to run installed versions.`
1768
+ };
1769
+ }
1770
+ return { ok: true, report: await updateNow() };
1771
+ },
1772
+ (error) => {
1773
+ log(
1774
+ "supervisor",
1775
+ `cannot listen on ${paths.socketFile}, so \`aibexd update\` will not reach it: ${error.message}`
1776
+ );
1777
+ }
1778
+ );
1779
+ return { localLink, updateNow };
1780
+ }
1781
+ runSupervisor();