@addai/node 0.11.3 → 0.13.0
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/dist/auto-update.d.ts +139 -0
- package/dist/auto-update.js +261 -0
- package/dist/capabilities.d.ts +14 -0
- package/dist/capabilities.js +8 -1
- package/dist/codex-spawn.d.ts +8 -0
- package/dist/codex-spawn.js +51 -4
- package/dist/command-runner.d.ts +3 -1
- package/dist/command-runner.js +117 -0
- package/dist/desktop/creds.d.ts +7 -0
- package/dist/desktop/creds.js +134 -0
- package/dist/desktop/docker.d.ts +19 -0
- package/dist/desktop/docker.js +91 -0
- package/dist/desktop/engine.d.ts +3 -0
- package/dist/desktop/engine.js +44 -0
- package/dist/desktop/launcher.d.ts +30 -0
- package/dist/desktop/launcher.js +35 -0
- package/dist/desktop/manager.d.ts +28 -0
- package/dist/desktop/manager.js +214 -0
- package/dist/desktop/provider.d.ts +35 -0
- package/dist/desktop/provider.js +53 -0
- package/dist/desktop/relay-client.d.ts +2 -0
- package/dist/desktop/relay-client.js +170 -0
- package/dist/desktop/spec.d.ts +37 -0
- package/dist/desktop/spec.js +62 -0
- package/dist/grok-spawn.d.ts +4 -0
- package/dist/grok-spawn.js +13 -0
- package/dist/heartbeat.d.ts +1 -0
- package/dist/heartbeat.js +12 -0
- package/dist/index.js +43 -1
- package/dist/machine-metrics.d.ts +72 -0
- package/dist/machine-metrics.js +175 -0
- package/dist/self-update.d.ts +40 -2
- package/dist/self-update.js +107 -14
- package/dist/store.d.ts +4 -1
- package/dist/store.js +22 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -49,11 +49,16 @@ const pairing_1 = require("./pairing");
|
|
|
49
49
|
const heartbeat_1 = require("./heartbeat");
|
|
50
50
|
const command_runner_1 = require("./command-runner");
|
|
51
51
|
const self_update_1 = require("./self-update");
|
|
52
|
+
const auto_update_1 = require("./auto-update");
|
|
53
|
+
const machine_metrics_1 = require("./machine-metrics");
|
|
54
|
+
const capabilities_1 = require("./capabilities");
|
|
52
55
|
const autostart_1 = require("./autostart");
|
|
53
56
|
const sleep_detector_1 = require("./sleep-detector");
|
|
54
57
|
const claude_config_1 = require("./claude-config");
|
|
55
58
|
const request_pump_1 = require("./request-pump");
|
|
56
59
|
const projects_1 = require("./projects");
|
|
60
|
+
const manager_1 = require("./desktop/manager");
|
|
61
|
+
const relay_client_1 = require("./desktop/relay-client");
|
|
57
62
|
const session_runner_1 = require("./session-runner");
|
|
58
63
|
const diskguard_1 = require("./diskguard");
|
|
59
64
|
const pty_helper_1 = require("./pty-helper");
|
|
@@ -143,6 +148,11 @@ async function start(argv = []) {
|
|
|
143
148
|
// server reports queued install/login commands, the command runner
|
|
144
149
|
// drains them.
|
|
145
150
|
(0, heartbeat_1.setPendingCommandsHook)(command_runner_1.wake);
|
|
151
|
+
// So each machine sample can say how many runs the box was holding at the
|
|
152
|
+
// time — "pinned at 90%" and "pinned at 90% running four things" are
|
|
153
|
+
// different stories. Hooked rather than imported so capabilities.ts needn't
|
|
154
|
+
// pull in request-pump, and through it session-runner.
|
|
155
|
+
(0, machine_metrics_1.setRunsInFlightHook)(() => (0, request_pump_1.activeRequestIdList)().length);
|
|
146
156
|
(0, heartbeat_1.start)();
|
|
147
157
|
// If we are the daemon a remote `update_runtime` restarted INTO, close out
|
|
148
158
|
// that command now — with the version we actually came up as. The process
|
|
@@ -198,6 +208,10 @@ async function start(argv = []) {
|
|
|
198
208
|
(0, request_pump_1.start)();
|
|
199
209
|
// …and for pending projects (git clones, future MCP/skill installs).
|
|
200
210
|
(0, projects_1.start)();
|
|
211
|
+
// …and to keep desktops and the containers behind them in agreement.
|
|
212
|
+
(0, manager_1.startDesktopManager)();
|
|
213
|
+
// …and dial out to the relay so a running desktop is watchable from Studio.
|
|
214
|
+
(0, relay_client_1.startRelayClient)();
|
|
201
215
|
let stopped = false;
|
|
202
216
|
// Outcome of the last drain, so a remote roll can report whether it left
|
|
203
217
|
// work unfinished instead of claiming a clean restart either way.
|
|
@@ -209,8 +223,11 @@ async function start(argv = []) {
|
|
|
209
223
|
// Stop accepting new work first — otherwise the pump would queue
|
|
210
224
|
// another request between drain start and process exit.
|
|
211
225
|
(0, projects_1.stop)();
|
|
226
|
+
(0, manager_1.stopDesktopManager)();
|
|
227
|
+
(0, relay_client_1.stopRelayClient)();
|
|
212
228
|
(0, request_pump_1.stop)();
|
|
213
229
|
(0, heartbeat_1.stop)();
|
|
230
|
+
(0, auto_update_1.stop)();
|
|
214
231
|
sleepMonitor.stop();
|
|
215
232
|
diskGuard.stop();
|
|
216
233
|
// Graceful drain: wait up to drainTimeoutMs for in-flight requests
|
|
@@ -254,7 +271,7 @@ async function start(argv = []) {
|
|
|
254
271
|
// to the same bits (which matters for npx, whose cache dir is per-version)
|
|
255
272
|
// and npm is never called — nothing to fetch, and nothing that can fail
|
|
256
273
|
// offline.
|
|
257
|
-
|
|
274
|
+
const performRestart = async ({ commandId, version, restartOnly }) => {
|
|
258
275
|
const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
|
|
259
276
|
const plan = (0, self_update_1.planRespawn)(mode, process.argv[1] ?? '', version, process.execPath, process.argv.slice(2));
|
|
260
277
|
const supervised = (0, autostart_1.isSupervised)();
|
|
@@ -293,6 +310,21 @@ async function start(argv = []) {
|
|
|
293
310
|
? `[restart] standing down for launchd to restart us (${what}); exiting`
|
|
294
311
|
: `[restart] handed over to ${plan.file} (${what}); exiting`);
|
|
295
312
|
setTimeout(() => process.exit(0), 250).unref?.();
|
|
313
|
+
};
|
|
314
|
+
(0, command_runner_1.setRestartHook)(performRestart);
|
|
315
|
+
// Hourly auto-update — the node keeps itself and its harnesses current
|
|
316
|
+
// without anybody clicking. It rolls through the very same closure the
|
|
317
|
+
// manual button does, minus the command id, so a hand roll and an auto
|
|
318
|
+
// roll cannot drift apart.
|
|
319
|
+
(0, auto_update_1.start)({
|
|
320
|
+
token: () => (0, store_1.readPairing)()?.daemonToken ?? null,
|
|
321
|
+
isBusy: () => (0, request_pump_1.activeRequestIdList)().length > 0,
|
|
322
|
+
breakerOpen: () => (0, supabase_client_1.rpcShouldSkip)(),
|
|
323
|
+
enabled: heartbeat_1.autoUpdateEnabled,
|
|
324
|
+
probe: () => (0, capabilities_1.probeCapabilities)(),
|
|
325
|
+
beat: () => (0, heartbeat_1.beat)(),
|
|
326
|
+
roll: ({ version }) => performRestart({ version }),
|
|
327
|
+
version: VERSION,
|
|
296
328
|
});
|
|
297
329
|
process.once('SIGINT', () => { stop().finally(() => process.exit(0)); });
|
|
298
330
|
process.once('SIGTERM', () => { stop().finally(() => process.exit(0)); });
|
|
@@ -392,6 +424,16 @@ async function finalizeRestartHandoff() {
|
|
|
392
424
|
const changed = h.from_version !== VERSION;
|
|
393
425
|
const wanted = h.target_version;
|
|
394
426
|
const missed = wanted !== 'latest' && wanted !== VERSION;
|
|
427
|
+
// No command id means the hourly auto-update rolled us, not a button. There
|
|
428
|
+
// is nothing to close; the outcome goes into capabilities instead, which the
|
|
429
|
+
// next heartbeat (seconds away) carries to the AiNode page.
|
|
430
|
+
if (!h.command_id) {
|
|
431
|
+
(0, auto_update_1.recordAutoRoll)({ from: h.from_version, to: VERSION, ...(missed ? { wanted } : {}) });
|
|
432
|
+
console.log(missed
|
|
433
|
+
? `[auto-update] restarted, but came up as ${VERSION} (wanted ${wanted})`
|
|
434
|
+
: `[auto-update] back up as ${VERSION} (was ${h.from_version})`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
395
437
|
try {
|
|
396
438
|
await (0, supabase_client_1.rpc)('runtime_command_update', {
|
|
397
439
|
p_token: t,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
export interface MachineSample {
|
|
3
|
+
/** Logical cores. */
|
|
4
|
+
cpus?: number;
|
|
5
|
+
/** Physical memory, MB. */
|
|
6
|
+
mem_total_mb?: number;
|
|
7
|
+
/** Memory in use, 0-100. */
|
|
8
|
+
mem_used_pct?: number;
|
|
9
|
+
/** Processor busy across all cores since the previous sample, 0-100.
|
|
10
|
+
* Absent on the first sample of a daemon's life — there is nothing to
|
|
11
|
+
* measure against yet, and a made-up first value would be a lie that
|
|
12
|
+
* lands in a graph. */
|
|
13
|
+
cpu_used_pct?: number;
|
|
14
|
+
/** 1-minute load average per core. Zero on Windows, where the OS has no
|
|
15
|
+
* such concept and Node reports [0,0,0]. */
|
|
16
|
+
load_per_cpu?: number;
|
|
17
|
+
/** Scratch volume in use, 0-100 — the same signal the health gate refuses
|
|
18
|
+
* new runs on at 97%. A filling disk is the failure that has actually
|
|
19
|
+
* bitten prod, so it belongs on the same timeline as the rest. */
|
|
20
|
+
disk_used_pct?: number;
|
|
21
|
+
/** Agent runs this node was holding at the moment of the sample. Turns
|
|
22
|
+
* "the box was pinned at 90%" into "because it was running four things". */
|
|
23
|
+
runs?: number;
|
|
24
|
+
/** Seconds since the machine booted. */
|
|
25
|
+
uptime_s?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface CpuTimes {
|
|
28
|
+
idle: number;
|
|
29
|
+
total: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Smallest interval, in summed CPU-milliseconds across all cores, we will
|
|
33
|
+
* report a busy figure for.
|
|
34
|
+
*
|
|
35
|
+
* Without a floor, two readings a millisecond apart can straddle a single
|
|
36
|
+
* scheduler tick: idle advances by 0, total by ~1, and the honest arithmetic
|
|
37
|
+
* says 100% busy. That is noise, not measurement, and it lands in the graph as
|
|
38
|
+
* a spike that never happened — which is exactly how it was found, by a test
|
|
39
|
+
* that took two samples back to back and got 100 roughly one run in twelve.
|
|
40
|
+
*
|
|
41
|
+
* 100ms across all cores is ~12ms of wall time on an 8-core box, far below the
|
|
42
|
+
* 30s heartbeat this actually runs at, so no real sample is ever refused.
|
|
43
|
+
*/
|
|
44
|
+
export declare const MIN_SAMPLE_MS = 100;
|
|
45
|
+
/** Summed CPU time across every core, in the units Node reports. */
|
|
46
|
+
export declare function readCpuTimes(cpus: os.CpuInfo[]): CpuTimes;
|
|
47
|
+
/**
|
|
48
|
+
* Busy percentage between two cumulative readings.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately delta-based rather than derived from loadavg: load is a queue
|
|
51
|
+
* length, not a utilisation, it means different things on different kernels,
|
|
52
|
+
* and on Windows it is always zero. Deltas of the counters Node already
|
|
53
|
+
* exposes give a real 0-100 on every platform we run on.
|
|
54
|
+
*
|
|
55
|
+
* Returns null when the comparison is meaningless — no previous reading, or a
|
|
56
|
+
* total that did not advance (same tick) or went backwards (core count
|
|
57
|
+
* changed under us, a VM migrated). A null is "we don't know", which the graph
|
|
58
|
+
* shows as a gap; inventing 0% would draw an idle machine that wasn't.
|
|
59
|
+
*/
|
|
60
|
+
export declare function cpuUsedPct(prev: CpuTimes | null, now: CpuTimes): number | null;
|
|
61
|
+
/** Memory in use as a percentage. Note this counts the page cache as used on
|
|
62
|
+
* Linux, the same way `free` does before you read the -/+ buffers line — it
|
|
63
|
+
* is the number every other tool shows, so it is the one to match. */
|
|
64
|
+
export declare function memUsedPct(totalBytes: number, freeBytes: number): number | null;
|
|
65
|
+
export declare function setRunsInFlightHook(fn: () => number): void;
|
|
66
|
+
/** Reset between tests. */
|
|
67
|
+
export declare function resetCpuBaseline(): void;
|
|
68
|
+
/**
|
|
69
|
+
* One reading of the machine. Never throws: every field is independently
|
|
70
|
+
* best-effort, and a sample with holes in it beats no heartbeat at all.
|
|
71
|
+
*/
|
|
72
|
+
export declare function sampleMachine(): MachineSample;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// What the machine underneath the daemon is actually doing.
|
|
3
|
+
//
|
|
4
|
+
// Entity Studio's AiNode page has read `capabilities.machine` for cores,
|
|
5
|
+
// memory, load and uptime since it was written — but no version of this daemon
|
|
6
|
+
// has ever produced that key, so those readouts have always been dashes and
|
|
7
|
+
// the Memory tile has always silently fallen back to showing the pairing date.
|
|
8
|
+
// This is the producing half.
|
|
9
|
+
//
|
|
10
|
+
// Sampled on the capabilities probe (every heartbeat, ~30s). Everything here
|
|
11
|
+
// is cheap and synchronous: three `os` calls and one statfs. Nothing may throw
|
|
12
|
+
// — a metrics failure must never cost the heartbeat, because the heartbeat is
|
|
13
|
+
// what keeps the node marked online.
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.MIN_SAMPLE_MS = void 0;
|
|
49
|
+
exports.readCpuTimes = readCpuTimes;
|
|
50
|
+
exports.cpuUsedPct = cpuUsedPct;
|
|
51
|
+
exports.memUsedPct = memUsedPct;
|
|
52
|
+
exports.setRunsInFlightHook = setRunsInFlightHook;
|
|
53
|
+
exports.resetCpuBaseline = resetCpuBaseline;
|
|
54
|
+
exports.sampleMachine = sampleMachine;
|
|
55
|
+
const os = __importStar(require("os"));
|
|
56
|
+
const node_health_1 = require("./node-health");
|
|
57
|
+
const paths_1 = require("./paths");
|
|
58
|
+
/**
|
|
59
|
+
* Smallest interval, in summed CPU-milliseconds across all cores, we will
|
|
60
|
+
* report a busy figure for.
|
|
61
|
+
*
|
|
62
|
+
* Without a floor, two readings a millisecond apart can straddle a single
|
|
63
|
+
* scheduler tick: idle advances by 0, total by ~1, and the honest arithmetic
|
|
64
|
+
* says 100% busy. That is noise, not measurement, and it lands in the graph as
|
|
65
|
+
* a spike that never happened — which is exactly how it was found, by a test
|
|
66
|
+
* that took two samples back to back and got 100 roughly one run in twelve.
|
|
67
|
+
*
|
|
68
|
+
* 100ms across all cores is ~12ms of wall time on an 8-core box, far below the
|
|
69
|
+
* 30s heartbeat this actually runs at, so no real sample is ever refused.
|
|
70
|
+
*/
|
|
71
|
+
exports.MIN_SAMPLE_MS = 100;
|
|
72
|
+
/** Summed CPU time across every core, in the units Node reports. */
|
|
73
|
+
function readCpuTimes(cpus) {
|
|
74
|
+
let idle = 0;
|
|
75
|
+
let total = 0;
|
|
76
|
+
for (const c of cpus) {
|
|
77
|
+
for (const key of Object.keys(c.times)) {
|
|
78
|
+
total += c.times[key];
|
|
79
|
+
}
|
|
80
|
+
idle += c.times.idle;
|
|
81
|
+
}
|
|
82
|
+
return { idle, total };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Busy percentage between two cumulative readings.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately delta-based rather than derived from loadavg: load is a queue
|
|
88
|
+
* length, not a utilisation, it means different things on different kernels,
|
|
89
|
+
* and on Windows it is always zero. Deltas of the counters Node already
|
|
90
|
+
* exposes give a real 0-100 on every platform we run on.
|
|
91
|
+
*
|
|
92
|
+
* Returns null when the comparison is meaningless — no previous reading, or a
|
|
93
|
+
* total that did not advance (same tick) or went backwards (core count
|
|
94
|
+
* changed under us, a VM migrated). A null is "we don't know", which the graph
|
|
95
|
+
* shows as a gap; inventing 0% would draw an idle machine that wasn't.
|
|
96
|
+
*/
|
|
97
|
+
function cpuUsedPct(prev, now) {
|
|
98
|
+
if (!prev)
|
|
99
|
+
return null;
|
|
100
|
+
const total = now.total - prev.total;
|
|
101
|
+
const idle = now.idle - prev.idle;
|
|
102
|
+
if (total < exports.MIN_SAMPLE_MS || idle < 0)
|
|
103
|
+
return null;
|
|
104
|
+
const pct = (1 - idle / total) * 100;
|
|
105
|
+
return Math.max(0, Math.min(100, Math.round(pct * 10) / 10));
|
|
106
|
+
}
|
|
107
|
+
/** Memory in use as a percentage. Note this counts the page cache as used on
|
|
108
|
+
* Linux, the same way `free` does before you read the -/+ buffers line — it
|
|
109
|
+
* is the number every other tool shows, so it is the one to match. */
|
|
110
|
+
function memUsedPct(totalBytes, freeBytes) {
|
|
111
|
+
if (!Number.isFinite(totalBytes) || totalBytes <= 0)
|
|
112
|
+
return null;
|
|
113
|
+
const pct = (1 - freeBytes / totalBytes) * 100;
|
|
114
|
+
return Math.max(0, Math.min(100, Math.round(pct * 10) / 10));
|
|
115
|
+
}
|
|
116
|
+
/* ── live sampling ────────────────────────────────────────────────────── */
|
|
117
|
+
let prevCpu = null;
|
|
118
|
+
/** How many runs this node is holding. Wired by index.ts rather than imported,
|
|
119
|
+
* so capabilities.ts does not have to pull in request-pump (and through it
|
|
120
|
+
* session-runner) just to count them. */
|
|
121
|
+
let runsInFlight = null;
|
|
122
|
+
function setRunsInFlightHook(fn) { runsInFlight = fn; }
|
|
123
|
+
/** Reset between tests. */
|
|
124
|
+
function resetCpuBaseline() { prevCpu = null; }
|
|
125
|
+
/**
|
|
126
|
+
* One reading of the machine. Never throws: every field is independently
|
|
127
|
+
* best-effort, and a sample with holes in it beats no heartbeat at all.
|
|
128
|
+
*/
|
|
129
|
+
function sampleMachine() {
|
|
130
|
+
const s = {};
|
|
131
|
+
try {
|
|
132
|
+
const cpus = os.cpus();
|
|
133
|
+
if (cpus.length > 0) {
|
|
134
|
+
s.cpus = cpus.length;
|
|
135
|
+
const now = readCpuTimes(cpus);
|
|
136
|
+
const pct = cpuUsedPct(prevCpu, now);
|
|
137
|
+
if (pct !== null)
|
|
138
|
+
s.cpu_used_pct = pct;
|
|
139
|
+
prevCpu = now;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch { /* no cpu info — the other fields still stand */ }
|
|
143
|
+
try {
|
|
144
|
+
const total = os.totalmem();
|
|
145
|
+
s.mem_total_mb = Math.round(total / (1024 * 1024));
|
|
146
|
+
const pct = memUsedPct(total, os.freemem());
|
|
147
|
+
if (pct !== null)
|
|
148
|
+
s.mem_used_pct = pct;
|
|
149
|
+
}
|
|
150
|
+
catch { /* skip */ }
|
|
151
|
+
try {
|
|
152
|
+
const [one] = os.loadavg();
|
|
153
|
+
const cores = s.cpus ?? 0;
|
|
154
|
+
if (cores > 0 && Number.isFinite(one))
|
|
155
|
+
s.load_per_cpu = Math.round((one / cores) * 100) / 100;
|
|
156
|
+
}
|
|
157
|
+
catch { /* skip */ }
|
|
158
|
+
try {
|
|
159
|
+
const disk = (0, node_health_1.readDiskUsedPct)(paths_1.RUNTIME_HOME);
|
|
160
|
+
if (disk !== null)
|
|
161
|
+
s.disk_used_pct = Math.round(disk * 10) / 10;
|
|
162
|
+
}
|
|
163
|
+
catch { /* skip */ }
|
|
164
|
+
try {
|
|
165
|
+
s.runs = runsInFlight?.() ?? 0;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
s.runs = 0;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
s.uptime_s = Math.round(os.uptime());
|
|
172
|
+
}
|
|
173
|
+
catch { /* skip */ }
|
|
174
|
+
return s;
|
|
175
|
+
}
|
package/dist/self-update.d.ts
CHANGED
|
@@ -53,6 +53,23 @@ export interface RollPlan {
|
|
|
53
53
|
/** Non-null = refuse before anything drains, with this explanation. */
|
|
54
54
|
refusal: string | null;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Is this something we are willing to hand to `npm i -g @addai/node@…`?
|
|
58
|
+
*
|
|
59
|
+
* npm accepts far more than a version after the `@`: a tarball URL, a
|
|
60
|
+
* `github:user/repo`, a `file:` path — each of which installs code of the
|
|
61
|
+
* caller's choosing, which this daemon then re-execs as its own binary. A
|
|
62
|
+
* remotely-supplied spec is therefore restricted to a plain semver or a
|
|
63
|
+
* dist-tag, and everything else is refused before npm is ever invoked.
|
|
64
|
+
*
|
|
65
|
+
* This is the second lock. The first is the RLS policy on runtime_commands,
|
|
66
|
+
* which was found comparing `input ->> 'restart_only'` (text) while this file
|
|
67
|
+
* compared `=== true` (boolean) — so the string "true" read as a harmless
|
|
68
|
+
* restart to the database and as a roll-to-anything here, giving anyone a node
|
|
69
|
+
* was merely SHARED with arbitrary code execution on the owner's machine.
|
|
70
|
+
* The policy is fixed; this makes the same mistake unexploitable next time.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isSafeVersionSpec(v: string): boolean;
|
|
56
73
|
/**
|
|
57
74
|
* Read an `update_runtime` command's input into what this node should do.
|
|
58
75
|
*
|
|
@@ -67,7 +84,12 @@ export declare function planRoll(input: {
|
|
|
67
84
|
restart_only?: boolean;
|
|
68
85
|
} | null, mode: LaunchMode, currentVersion: string): RollPlan;
|
|
69
86
|
export interface RestartHandoff {
|
|
70
|
-
|
|
87
|
+
/** Absent when the roll was the hourly auto-update rather than a button:
|
|
88
|
+
* there is no command to close, so the daemon that comes up records the
|
|
89
|
+
* outcome in capabilities instead. The property that matters survives
|
|
90
|
+
* either way — it is the NEW process that reports, so a restart that
|
|
91
|
+
* never comes back still never reports success. */
|
|
92
|
+
command_id?: string;
|
|
71
93
|
from_version: string;
|
|
72
94
|
target_version: string;
|
|
73
95
|
mode: LaunchMode;
|
|
@@ -87,11 +109,27 @@ export declare function writeHandoff(runtimeDir: string, h: RestartHandoff): voi
|
|
|
87
109
|
/** Reads and DELETES the handoff — it must never be replayed twice. */
|
|
88
110
|
export declare function takeHandoff(runtimeDir: string): RestartHandoff | null;
|
|
89
111
|
/**
|
|
90
|
-
* `npm i -g <
|
|
112
|
+
* `npm i -g <spec>` for any package. Returns null on success, else the reason.
|
|
91
113
|
* Never throws — a failed install must leave the CURRENT daemon running
|
|
92
114
|
* rather than take the node down.
|
|
115
|
+
*
|
|
116
|
+
* On a classic Mac with a root-owned /usr/local prefix the first attempt dies
|
|
117
|
+
* EACCES, so we retry into the daemon's own prefix — the same fallback
|
|
118
|
+
* command-runner's interactive install uses, and the same one the binary
|
|
119
|
+
* finders already search. Without it, auto-update would fail silently forever
|
|
120
|
+
* on exactly the nodes that needed a human least.
|
|
93
121
|
*/
|
|
122
|
+
export declare function npmInstallGlobal(spec: string, timeoutMs?: number): Promise<string | null>;
|
|
123
|
+
/** `npm i -g @addai/node@<version>`. Thin wrapper kept for the roll paths. */
|
|
94
124
|
export declare function installGlobal(targetVersion: string, timeoutMs?: number): Promise<string | null>;
|
|
125
|
+
/**
|
|
126
|
+
* What npm currently calls `latest` for a package, or null if we couldn't ask.
|
|
127
|
+
*
|
|
128
|
+
* Null is deliberately NOT treated as "up to date" by callers — it means the
|
|
129
|
+
* question went unanswered (offline, registry blip), and an unanswered
|
|
130
|
+
* question must never masquerade as a clean bill of health.
|
|
131
|
+
*/
|
|
132
|
+
export declare function npmLatestVersion(pkg: string, timeoutMs?: number): Promise<string | null>;
|
|
95
133
|
/**
|
|
96
134
|
* Spawn the replacement daemon fully detached, so it survives this process's
|
|
97
135
|
* exit. stdio is redirected to the daemon log (inheriting our stdio would tie
|
package/dist/self-update.js
CHANGED
|
@@ -55,16 +55,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
55
55
|
exports.PACKAGE_NAME = void 0;
|
|
56
56
|
exports.detectLaunchMode = detectLaunchMode;
|
|
57
57
|
exports.planRespawn = planRespawn;
|
|
58
|
+
exports.isSafeVersionSpec = isSafeVersionSpec;
|
|
58
59
|
exports.planRoll = planRoll;
|
|
59
60
|
exports.handoffPath = handoffPath;
|
|
60
61
|
exports.writeHandoff = writeHandoff;
|
|
61
62
|
exports.takeHandoff = takeHandoff;
|
|
63
|
+
exports.npmInstallGlobal = npmInstallGlobal;
|
|
62
64
|
exports.installGlobal = installGlobal;
|
|
65
|
+
exports.npmLatestVersion = npmLatestVersion;
|
|
63
66
|
exports.spawnReplacement = spawnReplacement;
|
|
64
67
|
const child_process_1 = require("child_process");
|
|
65
68
|
const fs = __importStar(require("fs"));
|
|
66
69
|
const path = __importStar(require("path"));
|
|
67
70
|
const win_1 = require("./win");
|
|
71
|
+
const paths_1 = require("./paths");
|
|
68
72
|
exports.PACKAGE_NAME = '@addai/node';
|
|
69
73
|
/**
|
|
70
74
|
* Classify the launch from the running script's path.
|
|
@@ -113,6 +117,29 @@ userArgs = []) {
|
|
|
113
117
|
canChangeVersion: mode === 'global',
|
|
114
118
|
};
|
|
115
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Is this something we are willing to hand to `npm i -g @addai/node@…`?
|
|
122
|
+
*
|
|
123
|
+
* npm accepts far more than a version after the `@`: a tarball URL, a
|
|
124
|
+
* `github:user/repo`, a `file:` path — each of which installs code of the
|
|
125
|
+
* caller's choosing, which this daemon then re-execs as its own binary. A
|
|
126
|
+
* remotely-supplied spec is therefore restricted to a plain semver or a
|
|
127
|
+
* dist-tag, and everything else is refused before npm is ever invoked.
|
|
128
|
+
*
|
|
129
|
+
* This is the second lock. The first is the RLS policy on runtime_commands,
|
|
130
|
+
* which was found comparing `input ->> 'restart_only'` (text) while this file
|
|
131
|
+
* compared `=== true` (boolean) — so the string "true" read as a harmless
|
|
132
|
+
* restart to the database and as a roll-to-anything here, giving anyone a node
|
|
133
|
+
* was merely SHARED with arbitrary code execution on the owner's machine.
|
|
134
|
+
* The policy is fixed; this makes the same mistake unexploitable next time.
|
|
135
|
+
*/
|
|
136
|
+
function isSafeVersionSpec(v) {
|
|
137
|
+
// 1.2.3 / 1.2.3-beta.1
|
|
138
|
+
if (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(v))
|
|
139
|
+
return true;
|
|
140
|
+
// a dist-tag: latest, next, canary…
|
|
141
|
+
return /^[a-z][a-z0-9-]{0,31}$/.test(v);
|
|
142
|
+
}
|
|
116
143
|
/**
|
|
117
144
|
* Read an `update_runtime` command's input into what this node should do.
|
|
118
145
|
*
|
|
@@ -128,9 +155,11 @@ function planRoll(input, mode, currentVersion) {
|
|
|
128
155
|
// "did we come up as what was asked for?" check meaningful instead of
|
|
129
156
|
// special-cased — and pins npx, whose cache dir is per-version.
|
|
130
157
|
const target = restartOnly ? currentVersion : (input?.version ?? '').trim() || 'latest';
|
|
131
|
-
const refusal = !restartOnly &&
|
|
132
|
-
?
|
|
133
|
-
:
|
|
158
|
+
const refusal = !restartOnly && !isSafeVersionSpec(target)
|
|
159
|
+
? `refusing to install "${target}" — a remote roll may name a version or a dist-tag, nothing else`
|
|
160
|
+
: !restartOnly && mode === 'source' && target !== 'latest'
|
|
161
|
+
? 'this node runs from a source checkout — pull and rebuild it there; a remote version bump cannot apply'
|
|
162
|
+
: null;
|
|
134
163
|
return { target, restartOnly, refusal };
|
|
135
164
|
}
|
|
136
165
|
function handoffPath(runtimeDir) {
|
|
@@ -150,22 +179,85 @@ function takeHandoff(runtimeDir) {
|
|
|
150
179
|
}
|
|
151
180
|
catch { /* already gone */ }
|
|
152
181
|
const parsed = JSON.parse(raw);
|
|
153
|
-
|
|
182
|
+
// A handoff with no command_id is the auto-update shape, so the id can no
|
|
183
|
+
// longer be what proves the file is one of ours — from_version is.
|
|
184
|
+
return parsed && typeof parsed.from_version === 'string' ? parsed : null;
|
|
154
185
|
}
|
|
155
186
|
catch {
|
|
156
187
|
return null;
|
|
157
188
|
}
|
|
158
189
|
}
|
|
190
|
+
/** One `npm i -g` attempt. Resolves the failure reason, or null on success. */
|
|
191
|
+
function npmInstallAttempt(spec, prefix, timeoutMs) {
|
|
192
|
+
const args = ['i', '-g', ...(prefix ? ['--prefix', prefix] : []), spec];
|
|
193
|
+
const inv = (0, win_1.resolveCliInvocation)('npm', args);
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
let log = '';
|
|
196
|
+
let settled = false;
|
|
197
|
+
const done = (err) => { if (!settled) {
|
|
198
|
+
settled = true;
|
|
199
|
+
resolve({ err, log });
|
|
200
|
+
} };
|
|
201
|
+
try {
|
|
202
|
+
const child = (0, child_process_1.spawn)(inv.file, inv.args, { windowsHide: true, env: process.env });
|
|
203
|
+
const timer = setTimeout(() => { try {
|
|
204
|
+
child.kill('SIGKILL');
|
|
205
|
+
}
|
|
206
|
+
catch { /* gone */ } done(`npm i -g ${spec} timed out`); }, timeoutMs);
|
|
207
|
+
timer.unref?.();
|
|
208
|
+
child.stdout?.on('data', b => { log += b.toString('utf8'); });
|
|
209
|
+
child.stderr?.on('data', b => { log += b.toString('utf8'); });
|
|
210
|
+
child.on('error', err => { clearTimeout(timer); done(`npm i -g failed to start: ${err.message}`); });
|
|
211
|
+
child.on('exit', code => {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
done(code === 0 ? null : `npm i -g ${spec} exited ${code}: ${log.trim().slice(-400)}`);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
done(`npm i -g failed to start: ${err.message}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
159
221
|
/**
|
|
160
|
-
* `npm i -g <
|
|
222
|
+
* `npm i -g <spec>` for any package. Returns null on success, else the reason.
|
|
161
223
|
* Never throws — a failed install must leave the CURRENT daemon running
|
|
162
224
|
* rather than take the node down.
|
|
225
|
+
*
|
|
226
|
+
* On a classic Mac with a root-owned /usr/local prefix the first attempt dies
|
|
227
|
+
* EACCES, so we retry into the daemon's own prefix — the same fallback
|
|
228
|
+
* command-runner's interactive install uses, and the same one the binary
|
|
229
|
+
* finders already search. Without it, auto-update would fail silently forever
|
|
230
|
+
* on exactly the nodes that needed a human least.
|
|
163
231
|
*/
|
|
232
|
+
async function npmInstallGlobal(spec, timeoutMs = 5 * 60_000) {
|
|
233
|
+
const first = await npmInstallAttempt(spec, null, timeoutMs);
|
|
234
|
+
if (!first.err)
|
|
235
|
+
return null;
|
|
236
|
+
if (!/EACCES/.test(first.log))
|
|
237
|
+
return first.err;
|
|
238
|
+
const toolsPrefix = path.join(paths_1.RUNTIME_HOME, 'tools');
|
|
239
|
+
try {
|
|
240
|
+
fs.mkdirSync(toolsPrefix, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
catch { /* the spawn will surface it */ }
|
|
243
|
+
const retry = await npmInstallAttempt(spec, toolsPrefix, timeoutMs);
|
|
244
|
+
return retry.err;
|
|
245
|
+
}
|
|
246
|
+
/** `npm i -g @addai/node@<version>`. Thin wrapper kept for the roll paths. */
|
|
164
247
|
async function installGlobal(targetVersion, timeoutMs = 5 * 60_000) {
|
|
165
|
-
|
|
166
|
-
|
|
248
|
+
return npmInstallGlobal(`${exports.PACKAGE_NAME}@${targetVersion || 'latest'}`, timeoutMs);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* What npm currently calls `latest` for a package, or null if we couldn't ask.
|
|
252
|
+
*
|
|
253
|
+
* Null is deliberately NOT treated as "up to date" by callers — it means the
|
|
254
|
+
* question went unanswered (offline, registry blip), and an unanswered
|
|
255
|
+
* question must never masquerade as a clean bill of health.
|
|
256
|
+
*/
|
|
257
|
+
function npmLatestVersion(pkg, timeoutMs = 60_000) {
|
|
258
|
+
const inv = (0, win_1.resolveCliInvocation)('npm', ['view', pkg, 'version']);
|
|
167
259
|
return new Promise((resolve) => {
|
|
168
|
-
let
|
|
260
|
+
let out = '';
|
|
169
261
|
let settled = false;
|
|
170
262
|
const done = (v) => { if (!settled) {
|
|
171
263
|
settled = true;
|
|
@@ -176,17 +268,18 @@ async function installGlobal(targetVersion, timeoutMs = 5 * 60_000) {
|
|
|
176
268
|
const timer = setTimeout(() => { try {
|
|
177
269
|
child.kill('SIGKILL');
|
|
178
270
|
}
|
|
179
|
-
catch { /* gone */ } done(
|
|
271
|
+
catch { /* gone */ } done(null); }, timeoutMs);
|
|
180
272
|
timer.unref?.();
|
|
181
|
-
child.
|
|
182
|
-
child.on('error',
|
|
273
|
+
child.stdout?.on('data', b => { out += b.toString('utf8'); });
|
|
274
|
+
child.on('error', () => { clearTimeout(timer); done(null); });
|
|
183
275
|
child.on('exit', code => {
|
|
184
276
|
clearTimeout(timer);
|
|
185
|
-
|
|
277
|
+
const v = out.trim();
|
|
278
|
+
done(code === 0 && /^\d+\.\d+\.\d+/.test(v) ? v : null);
|
|
186
279
|
});
|
|
187
280
|
}
|
|
188
|
-
catch
|
|
189
|
-
done(
|
|
281
|
+
catch {
|
|
282
|
+
done(null);
|
|
190
283
|
}
|
|
191
284
|
});
|
|
192
285
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { RuntimePairingState } from './types';
|
|
2
2
|
export declare function readPairing(): RuntimePairingState | null;
|
|
3
3
|
export declare function writePairing(state: RuntimePairingState): void;
|
|
4
|
-
export declare function clearPairing(
|
|
4
|
+
export declare function clearPairing(opts?: {
|
|
5
|
+
stateFile?: string;
|
|
6
|
+
legacy?: string;
|
|
7
|
+
}): void;
|
|
5
8
|
export declare function isPaired(): boolean;
|
package/dist/store.js
CHANGED
|
@@ -43,6 +43,7 @@ exports.writePairing = writePairing;
|
|
|
43
43
|
exports.clearPairing = clearPairing;
|
|
44
44
|
exports.isPaired = isPaired;
|
|
45
45
|
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
46
47
|
const paths_1 = require("./paths");
|
|
47
48
|
const win_1 = require("./win");
|
|
48
49
|
function ensureHome() {
|
|
@@ -71,11 +72,30 @@ function writePairing(state) {
|
|
|
71
72
|
// rename-over-open-file fails EPERM/EBUSY on Windows — retried there.
|
|
72
73
|
(0, win_1.safeRenameSync)(tmp, paths_1.RUNTIME_STATE_FILE);
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
// `opts` exists so this is testable without touching the real home — same
|
|
76
|
+
// injection migrateLegacyStateDir takes, for the same reason.
|
|
77
|
+
function clearPairing(opts = {}) {
|
|
78
|
+
const stateFile = opts.stateFile ?? paths_1.RUNTIME_STATE_FILE;
|
|
79
|
+
const legacy = opts.legacy ?? paths_1.LEGACY_RUNTIME_HOME;
|
|
75
80
|
try {
|
|
76
|
-
fs.unlinkSync(
|
|
81
|
+
fs.unlinkSync(stateFile);
|
|
77
82
|
}
|
|
78
83
|
catch { /* already gone */ }
|
|
84
|
+
// The legacy home too. state-migrate COPIES ~/.entities-runtime/state.json
|
|
85
|
+
// into the new home rather than moving it, so the old file outlives the
|
|
86
|
+
// rename on purpose — but nothing ever cleaned it up, and the migration
|
|
87
|
+
// only skips when the new home already has a state.json. Deleting just the
|
|
88
|
+
// new copy therefore didn't unpair anything: the next start copied the old
|
|
89
|
+
// token straight back in, the daemon believed it was already paired (so an
|
|
90
|
+
// explicit `ainode entities <CODE>` never ran the pairing flow at all), the
|
|
91
|
+
// server rejected the dead token, and it cleared state again. That loop
|
|
92
|
+
// survived three releases on a real machine before we found it.
|
|
93
|
+
//
|
|
94
|
+
// Unpairing means unpaired. Both copies go.
|
|
95
|
+
try {
|
|
96
|
+
fs.unlinkSync(path.join(legacy, 'state.json'));
|
|
97
|
+
}
|
|
98
|
+
catch { /* no legacy home, or already gone */ }
|
|
79
99
|
}
|
|
80
100
|
function isPaired() {
|
|
81
101
|
return readPairing() !== null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"node-pty": "^1.1.0",
|
|
53
|
-
"ws": "^8.21.
|
|
53
|
+
"ws": "^8.21.3"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^20.0.0",
|