@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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type HarnessId } from './harness-registry';
|
|
2
|
+
import { type LaunchMode } from './self-update';
|
|
3
|
+
/**
|
|
4
|
+
* A stable per-node offset into the hour, derived from the daemon token.
|
|
5
|
+
*
|
|
6
|
+
* The fleet has to be spread across the hour rather than converging on :00 —
|
|
7
|
+
* with no soak window, the stagger is the ONLY thing limiting how fast a bad
|
|
8
|
+
* publish reaches everyone. Deriving it from the token (rather than random)
|
|
9
|
+
* means a node keeps its slot across restarts, including the restart an
|
|
10
|
+
* auto-roll itself causes, which would otherwise re-converge every rolled
|
|
11
|
+
* node onto the same minute.
|
|
12
|
+
*/
|
|
13
|
+
export declare function tickOffsetMs(token: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Milliseconds until this node's next slot, measured against the wall clock
|
|
16
|
+
* rather than against boot time — so a daemon that restarts twice an hour
|
|
17
|
+
* keeps firing on its own slot instead of drifting to a new one each time.
|
|
18
|
+
*/
|
|
19
|
+
export declare function msUntilNextTick(nowMs: number, offsetMs: number): number;
|
|
20
|
+
export type SkipReason = 'disabled' | 'busy' | 'breaker';
|
|
21
|
+
/**
|
|
22
|
+
* The guards, in the order they are cheapest to check.
|
|
23
|
+
*
|
|
24
|
+
* `busy` is a skip, NOT a drain: an auto-update is elective and there is
|
|
25
|
+
* always another hour, whereas draining would take the node out of rotation
|
|
26
|
+
* for up to ten minutes every time the tick happened to land mid-turn. A node
|
|
27
|
+
* that is never idle never auto-updates, and gets rolled by hand.
|
|
28
|
+
*/
|
|
29
|
+
export declare function shouldRun(s: {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
busy: boolean;
|
|
32
|
+
breakerOpen: boolean;
|
|
33
|
+
}): SkipReason | null;
|
|
34
|
+
export interface HarnessUpgrade {
|
|
35
|
+
id: HarnessId;
|
|
36
|
+
npmPackage: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Which harnesses this tick will `npm i -g …@latest`.
|
|
40
|
+
*
|
|
41
|
+
* Two rules, both deliberate:
|
|
42
|
+
* - npm-able only. grok's `npmPackage` is null (xAI ships its own installer),
|
|
43
|
+
* so it is never touched here — a truthful skip beats a fake upgrade.
|
|
44
|
+
* - installed only. The tick upgrades; it does not provision. Installing a
|
|
45
|
+
* harness nobody asked for is a different feature, and one with a cost
|
|
46
|
+
* (disk, a login prompt that never comes) the user never opted into.
|
|
47
|
+
*/
|
|
48
|
+
export declare function planHarnessUpgrades(installed: HarnessId[]): HarnessUpgrade[];
|
|
49
|
+
export interface RollDecision {
|
|
50
|
+
roll: boolean;
|
|
51
|
+
/** Why not, when roll is false. Logged, and surfaced in capabilities. */
|
|
52
|
+
reason?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether to roll the daemon itself.
|
|
56
|
+
*
|
|
57
|
+
* A source checkout is refused for the same reason a remote version bump is
|
|
58
|
+
* (see planRoll): its version comes from the working tree, so installing and
|
|
59
|
+
* restarting would come back up on exactly the same bits — a no-op wearing a
|
|
60
|
+
* success. Harnesses on such a node are still upgraded; only this half is off.
|
|
61
|
+
*/
|
|
62
|
+
export declare function planAutoRoll(current: string, latest: string | null, mode: LaunchMode): RollDecision;
|
|
63
|
+
export interface HarnessOutcome {
|
|
64
|
+
from?: string;
|
|
65
|
+
to?: string;
|
|
66
|
+
/** Where the binary resolves NOW. You chose npm-for-every-npm-able-harness,
|
|
67
|
+
* so on a machine whose Claude Code came from Anthropic's native installer
|
|
68
|
+
* npm has just put a second copy somewhere else — possibly shadowing the
|
|
69
|
+
* first. The path is what makes that visible instead of silent. */
|
|
70
|
+
path?: string;
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface AutoUpdateState {
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
last_checked_at?: string;
|
|
76
|
+
last_result?: 'ok' | 'skipped' | 'failed';
|
|
77
|
+
skipped_reason?: SkipReason;
|
|
78
|
+
/** Set when a roll actually happened — written by the daemon that came up
|
|
79
|
+
* after it, from the restart handoff. */
|
|
80
|
+
runtime?: {
|
|
81
|
+
from: string;
|
|
82
|
+
to: string;
|
|
83
|
+
wanted?: string;
|
|
84
|
+
} | null;
|
|
85
|
+
harnesses?: Record<string, HarnessOutcome>;
|
|
86
|
+
error?: string;
|
|
87
|
+
}
|
|
88
|
+
/** Read by capabilities.ts so the whole picture rides the heartbeat the AiNode
|
|
89
|
+
* page already reads — no new RPC, no new column, no polling. */
|
|
90
|
+
export declare function autoUpdateState(): AutoUpdateState;
|
|
91
|
+
/** Called by index.ts on boot when the handoff it consumed had no command id,
|
|
92
|
+
* i.e. the roll was ours rather than a button press. */
|
|
93
|
+
export declare function recordAutoRoll(r: {
|
|
94
|
+
from: string;
|
|
95
|
+
to: string;
|
|
96
|
+
wanted?: string;
|
|
97
|
+
}): void;
|
|
98
|
+
/** Only the part of a capabilities probe this module reads. Kept structural
|
|
99
|
+
* rather than importing CapabilitiesShape, which would put capabilities.ts on
|
|
100
|
+
* both ends of an import cycle (it reads autoUpdateState from here). */
|
|
101
|
+
type ProbeShape = Partial<Record<HarnessId, {
|
|
102
|
+
version?: string;
|
|
103
|
+
available?: boolean;
|
|
104
|
+
}>>;
|
|
105
|
+
export interface AutoUpdateDeps {
|
|
106
|
+
/** The daemon token — only used to derive this node's slot. */
|
|
107
|
+
token: () => string | null;
|
|
108
|
+
/** True while this node holds a child process for a live run. */
|
|
109
|
+
isBusy: () => boolean;
|
|
110
|
+
/** The daemon-wide Supabase circuit breaker. */
|
|
111
|
+
breakerOpen: () => boolean;
|
|
112
|
+
/** Whether the server says auto-update is on for this node. */
|
|
113
|
+
enabled: () => boolean;
|
|
114
|
+
/** Full capabilities probe — used for before/after harness versions. */
|
|
115
|
+
probe: () => Promise<ProbeShape>;
|
|
116
|
+
/** Push a heartbeat so Studio sees the new versions without waiting. */
|
|
117
|
+
beat: () => Promise<void>;
|
|
118
|
+
/** The same closure `update_runtime` uses. No command id: nobody asked. */
|
|
119
|
+
roll: (opts: {
|
|
120
|
+
version: string;
|
|
121
|
+
}) => Promise<void>;
|
|
122
|
+
/** This daemon's own version. */
|
|
123
|
+
version: string;
|
|
124
|
+
/** `npm i -g <spec>`; null on success. Injectable so the orchestration below
|
|
125
|
+
* — the before/after diff, the busy re-check — can be tested without
|
|
126
|
+
* actually installing anything. */
|
|
127
|
+
install?: (spec: string) => Promise<string | null>;
|
|
128
|
+
/** What npm calls latest for a package, or null if we couldn't ask. */
|
|
129
|
+
latest?: (pkg: string) => Promise<string | null>;
|
|
130
|
+
/** How this daemon was launched. Injectable because under a test runner
|
|
131
|
+
* argv[1] is the runner, which would read as `source` and mask the roll. */
|
|
132
|
+
mode?: () => LaunchMode;
|
|
133
|
+
}
|
|
134
|
+
/** One pass. Exported so `ainode` can be made to do it on demand later, and
|
|
135
|
+
* so a test can drive it without waiting an hour. */
|
|
136
|
+
export declare function runOnce(d: AutoUpdateDeps): Promise<void>;
|
|
137
|
+
export declare function start(d: AutoUpdateDeps): void;
|
|
138
|
+
export declare function stop(): void;
|
|
139
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Hourly auto-update — this node keeps itself, and its harnesses, current.
|
|
3
|
+
//
|
|
4
|
+
// Until now a node only moved when somebody clicked. `update_runtime` issued
|
|
5
|
+
// by hand from Entity Studio was the ONLY thing that ever rolled a daemon,
|
|
6
|
+
// which is how the fleet came to sit on 0.2.51/0.2.52/0.2.53 for days after
|
|
7
|
+
// 0.2.55 shipped. Harnesses were worse: Studio offers Install only while a
|
|
8
|
+
// harness reads `missing`, so once Claude Code was on a node it could never
|
|
9
|
+
// be upgraded from anywhere.
|
|
10
|
+
//
|
|
11
|
+
// The schedule lives here rather than in a server-side cron because a node
|
|
12
|
+
// asleep, behind NAT, or between pairings then needs no special case — the
|
|
13
|
+
// daemon already wakes on its own for reclaim, diskguard and sleep detection.
|
|
14
|
+
//
|
|
15
|
+
// Everything the tick needs from the rest of the daemon is INJECTED by
|
|
16
|
+
// index.ts (roll, probe, beat, isBusy, isEnabled). That keeps this module's
|
|
17
|
+
// imports down to the registry and the npm helpers, so the decisions below
|
|
18
|
+
// are testable without booting a daemon — and avoids the
|
|
19
|
+
// auto-update → heartbeat → capabilities → auto-update cycle that importing
|
|
20
|
+
// them directly would create.
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.tickOffsetMs = tickOffsetMs;
|
|
23
|
+
exports.msUntilNextTick = msUntilNextTick;
|
|
24
|
+
exports.shouldRun = shouldRun;
|
|
25
|
+
exports.planHarnessUpgrades = planHarnessUpgrades;
|
|
26
|
+
exports.planAutoRoll = planAutoRoll;
|
|
27
|
+
exports.autoUpdateState = autoUpdateState;
|
|
28
|
+
exports.recordAutoRoll = recordAutoRoll;
|
|
29
|
+
exports.runOnce = runOnce;
|
|
30
|
+
exports.start = start;
|
|
31
|
+
exports.stop = stop;
|
|
32
|
+
const harness_registry_1 = require("./harness-registry");
|
|
33
|
+
const self_update_1 = require("./self-update");
|
|
34
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
35
|
+
/** Nothing auto-rolls in the first few minutes of a boot. A node that just
|
|
36
|
+
* came up may BE the result of a roll (or of one that went badly), and
|
|
37
|
+
* rolling again immediately would turn a bad publish into a restart loop. */
|
|
38
|
+
const BOOT_GRACE_MS = 5 * 60 * 1000;
|
|
39
|
+
/* ── when ─────────────────────────────────────────────────────────────── */
|
|
40
|
+
/**
|
|
41
|
+
* A stable per-node offset into the hour, derived from the daemon token.
|
|
42
|
+
*
|
|
43
|
+
* The fleet has to be spread across the hour rather than converging on :00 —
|
|
44
|
+
* with no soak window, the stagger is the ONLY thing limiting how fast a bad
|
|
45
|
+
* publish reaches everyone. Deriving it from the token (rather than random)
|
|
46
|
+
* means a node keeps its slot across restarts, including the restart an
|
|
47
|
+
* auto-roll itself causes, which would otherwise re-converge every rolled
|
|
48
|
+
* node onto the same minute.
|
|
49
|
+
*/
|
|
50
|
+
function tickOffsetMs(token) {
|
|
51
|
+
// FNV-1a. Not cryptographic — it only has to spread evenly and be stable.
|
|
52
|
+
let h = 0x811c9dc5;
|
|
53
|
+
for (let i = 0; i < token.length; i++) {
|
|
54
|
+
h ^= token.charCodeAt(i);
|
|
55
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
56
|
+
}
|
|
57
|
+
return h % HOUR_MS;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Milliseconds until this node's next slot, measured against the wall clock
|
|
61
|
+
* rather than against boot time — so a daemon that restarts twice an hour
|
|
62
|
+
* keeps firing on its own slot instead of drifting to a new one each time.
|
|
63
|
+
*/
|
|
64
|
+
function msUntilNextTick(nowMs, offsetMs) {
|
|
65
|
+
const since = (((nowMs - offsetMs) % HOUR_MS) + HOUR_MS) % HOUR_MS;
|
|
66
|
+
const wait = HOUR_MS - since;
|
|
67
|
+
// since === 0 lands exactly on the slot; wait a full hour rather than
|
|
68
|
+
// firing twice for the same slot.
|
|
69
|
+
return wait === 0 ? HOUR_MS : wait;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The guards, in the order they are cheapest to check.
|
|
73
|
+
*
|
|
74
|
+
* `busy` is a skip, NOT a drain: an auto-update is elective and there is
|
|
75
|
+
* always another hour, whereas draining would take the node out of rotation
|
|
76
|
+
* for up to ten minutes every time the tick happened to land mid-turn. A node
|
|
77
|
+
* that is never idle never auto-updates, and gets rolled by hand.
|
|
78
|
+
*/
|
|
79
|
+
function shouldRun(s) {
|
|
80
|
+
if (!s.enabled)
|
|
81
|
+
return 'disabled';
|
|
82
|
+
if (s.breakerOpen)
|
|
83
|
+
return 'breaker';
|
|
84
|
+
if (s.busy)
|
|
85
|
+
return 'busy';
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Which harnesses this tick will `npm i -g …@latest`.
|
|
90
|
+
*
|
|
91
|
+
* Two rules, both deliberate:
|
|
92
|
+
* - npm-able only. grok's `npmPackage` is null (xAI ships its own installer),
|
|
93
|
+
* so it is never touched here — a truthful skip beats a fake upgrade.
|
|
94
|
+
* - installed only. The tick upgrades; it does not provision. Installing a
|
|
95
|
+
* harness nobody asked for is a different feature, and one with a cost
|
|
96
|
+
* (disk, a login prompt that never comes) the user never opted into.
|
|
97
|
+
*/
|
|
98
|
+
function planHarnessUpgrades(installed) {
|
|
99
|
+
const present = new Set(installed);
|
|
100
|
+
return harness_registry_1.HARNESS_IDS
|
|
101
|
+
.filter(id => present.has(id) && harness_registry_1.HARNESSES[id].npmPackage !== null)
|
|
102
|
+
.map(id => ({ id, npmPackage: harness_registry_1.HARNESSES[id].npmPackage }));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether to roll the daemon itself.
|
|
106
|
+
*
|
|
107
|
+
* A source checkout is refused for the same reason a remote version bump is
|
|
108
|
+
* (see planRoll): its version comes from the working tree, so installing and
|
|
109
|
+
* restarting would come back up on exactly the same bits — a no-op wearing a
|
|
110
|
+
* success. Harnesses on such a node are still upgraded; only this half is off.
|
|
111
|
+
*/
|
|
112
|
+
function planAutoRoll(current, latest, mode) {
|
|
113
|
+
if (mode === 'source')
|
|
114
|
+
return { roll: false, reason: 'source checkout — pull and rebuild it there' };
|
|
115
|
+
if (!latest)
|
|
116
|
+
return { roll: false, reason: 'could not read the latest published version' };
|
|
117
|
+
if (latest === current)
|
|
118
|
+
return { roll: false, reason: 'already latest' };
|
|
119
|
+
return { roll: true };
|
|
120
|
+
}
|
|
121
|
+
let state = { enabled: true };
|
|
122
|
+
let isEnabled = () => true;
|
|
123
|
+
/** Read by capabilities.ts so the whole picture rides the heartbeat the AiNode
|
|
124
|
+
* page already reads — no new RPC, no new column, no polling. */
|
|
125
|
+
function autoUpdateState() {
|
|
126
|
+
return { ...state, enabled: isEnabled() };
|
|
127
|
+
}
|
|
128
|
+
/** Called by index.ts on boot when the handoff it consumed had no command id,
|
|
129
|
+
* i.e. the roll was ours rather than a button press. */
|
|
130
|
+
function recordAutoRoll(r) {
|
|
131
|
+
state = { ...state, runtime: r, last_result: 'ok', last_checked_at: new Date().toISOString() };
|
|
132
|
+
}
|
|
133
|
+
let timer = null;
|
|
134
|
+
let stopped = false;
|
|
135
|
+
let deps = null;
|
|
136
|
+
function harnessVersion(probe, id) {
|
|
137
|
+
const p = probe[id];
|
|
138
|
+
return p?.available ? p.version : undefined;
|
|
139
|
+
}
|
|
140
|
+
function installedHarnesses(probe) {
|
|
141
|
+
return harness_registry_1.HARNESS_IDS.filter(id => probe[id]?.available === true);
|
|
142
|
+
}
|
|
143
|
+
/** One pass. Exported so `ainode` can be made to do it on demand later, and
|
|
144
|
+
* so a test can drive it without waiting an hour. */
|
|
145
|
+
async function runOnce(d) {
|
|
146
|
+
const skip = shouldRun({ enabled: d.enabled(), busy: d.isBusy(), breakerOpen: d.breakerOpen() });
|
|
147
|
+
if (skip) {
|
|
148
|
+
state = { ...state, last_checked_at: new Date().toISOString(), last_result: 'skipped', skipped_reason: skip };
|
|
149
|
+
if (skip !== 'breaker')
|
|
150
|
+
console.log(`[auto-update] skipped — ${skip === 'busy' ? 'the node is mid-run' : 'turned off for this node'}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const checkedAt = new Date().toISOString();
|
|
154
|
+
const harnesses = {};
|
|
155
|
+
let failed;
|
|
156
|
+
const install = d.install ?? self_update_1.npmInstallGlobal;
|
|
157
|
+
const latestOf = d.latest ?? self_update_1.npmLatestVersion;
|
|
158
|
+
/* ── harnesses first — nothing restarts, so this half always lands ──── */
|
|
159
|
+
try {
|
|
160
|
+
const before = await d.probe();
|
|
161
|
+
const upgrades = planHarnessUpgrades(installedHarnesses(before));
|
|
162
|
+
for (const u of upgrades) {
|
|
163
|
+
const from = harnessVersion(before, u.id);
|
|
164
|
+
const err = await install(`${u.npmPackage}@latest`);
|
|
165
|
+
harnesses[u.id] = { from, ...(err ? { error: err } : {}) };
|
|
166
|
+
if (err)
|
|
167
|
+
console.error(`[auto-update] ${u.id}: ${err}`);
|
|
168
|
+
}
|
|
169
|
+
if (upgrades.length > 0) {
|
|
170
|
+
// The finders cached their pre-upgrade lookups; a probe through stale
|
|
171
|
+
// caches would report the OLD version and hide the upgrade entirely.
|
|
172
|
+
(0, harness_registry_1.resetBinaryCaches)();
|
|
173
|
+
const after = await d.probe();
|
|
174
|
+
for (const u of upgrades) {
|
|
175
|
+
const to = harnessVersion(after, u.id);
|
|
176
|
+
const path = harness_registry_1.HARNESSES[u.id].findBinary() ?? undefined;
|
|
177
|
+
harnesses[u.id] = { ...harnesses[u.id], to, path };
|
|
178
|
+
if (harnesses[u.id].from !== to) {
|
|
179
|
+
console.log(`[auto-update] ${u.id} ${harnesses[u.id].from ?? '?'} → ${to ?? '?'} (${path ?? 'not resolvable'})`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
await d.beat();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
failed = `harness upgrade: ${err.message}`;
|
|
187
|
+
console.error(`[auto-update] ${failed}`);
|
|
188
|
+
}
|
|
189
|
+
state = {
|
|
190
|
+
...state,
|
|
191
|
+
last_checked_at: checkedAt,
|
|
192
|
+
last_result: failed ? 'failed' : 'ok',
|
|
193
|
+
skipped_reason: undefined,
|
|
194
|
+
harnesses,
|
|
195
|
+
error: failed,
|
|
196
|
+
};
|
|
197
|
+
/* ── then the daemon itself ─────────────────────────────────────────── */
|
|
198
|
+
const mode = d.mode ? d.mode() : (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
|
|
199
|
+
const latest = mode === 'source' ? null : await latestOf(self_update_1.PACKAGE_NAME);
|
|
200
|
+
const decision = planAutoRoll(d.version, latest, mode);
|
|
201
|
+
if (!decision.roll) {
|
|
202
|
+
if (decision.reason !== 'already latest')
|
|
203
|
+
console.log(`[auto-update] not rolling — ${decision.reason}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Re-check busy: the harness upgrades above can take minutes, and a run may
|
|
207
|
+
// well have arrived while npm was working. Rolling now would guillotine it
|
|
208
|
+
// at the drain ceiling for no reason — there is another hour.
|
|
209
|
+
if (d.isBusy()) {
|
|
210
|
+
state = { ...state, last_result: 'skipped', skipped_reason: 'busy' };
|
|
211
|
+
console.log('[auto-update] a run arrived during the harness pass — leaving the roll for next hour');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
console.log(`[auto-update] rolling ${d.version} → ${latest}`);
|
|
215
|
+
try {
|
|
216
|
+
// This does not return: the roll drains, hands over, and exits. The
|
|
217
|
+
// daemon that comes up completes the story from the handoff file.
|
|
218
|
+
await d.roll({ version: latest });
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
state = { ...state, last_result: 'failed', error: `roll: ${err.message}` };
|
|
222
|
+
console.error(`[auto-update] roll failed — staying on ${d.version}:`, err.message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function schedule(firstRun) {
|
|
226
|
+
if (stopped || !deps)
|
|
227
|
+
return;
|
|
228
|
+
const token = deps.token() ?? '';
|
|
229
|
+
const offset = tickOffsetMs(token);
|
|
230
|
+
let wait = msUntilNextTick(Date.now(), offset);
|
|
231
|
+
if (firstRun)
|
|
232
|
+
wait = Math.max(wait, BOOT_GRACE_MS);
|
|
233
|
+
timer = setTimeout(() => {
|
|
234
|
+
void (async () => {
|
|
235
|
+
try {
|
|
236
|
+
await runOnce(deps);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
console.error('[auto-update] tick crashed:', err.message);
|
|
240
|
+
}
|
|
241
|
+
schedule(false);
|
|
242
|
+
})();
|
|
243
|
+
}, wait);
|
|
244
|
+
// Never keep the process alive just to auto-update.
|
|
245
|
+
timer.unref?.();
|
|
246
|
+
}
|
|
247
|
+
function start(d) {
|
|
248
|
+
if (timer || stopped)
|
|
249
|
+
return;
|
|
250
|
+
deps = d;
|
|
251
|
+
isEnabled = d.enabled;
|
|
252
|
+
state = { ...state, enabled: d.enabled() };
|
|
253
|
+
schedule(true);
|
|
254
|
+
}
|
|
255
|
+
function stop() {
|
|
256
|
+
stopped = true;
|
|
257
|
+
if (timer) {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
timer = null;
|
|
260
|
+
}
|
|
261
|
+
}
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { EngineInfo } from './desktop/provider';
|
|
1
2
|
import { type AutostartState } from './autostart';
|
|
3
|
+
import { type AutoUpdateState } from './auto-update';
|
|
4
|
+
import { type MachineSample } from './machine-metrics';
|
|
2
5
|
interface AuthShape {
|
|
3
6
|
authed: boolean;
|
|
4
7
|
account?: string;
|
|
@@ -42,6 +45,17 @@ interface CapabilitiesShape {
|
|
|
42
45
|
* rather than a column of its own — same channel Studio already reads the
|
|
43
46
|
* harness grid from. */
|
|
44
47
|
autostart?: AutostartState;
|
|
48
|
+
/** Container engine backing Entity Desktops, or null when none is
|
|
49
|
+
* installed. Studio renders an install card in place of the create
|
|
50
|
+
* button when this is null. */
|
|
51
|
+
container_engine?: EngineInfo | null;
|
|
52
|
+
/** CPU / memory / disk / runs at the moment of this probe. The AiNode page
|
|
53
|
+
* has read this key since it was written; nothing produced it until now. */
|
|
54
|
+
machine?: MachineSample;
|
|
55
|
+
/** What the hourly auto-update last did — versions moved, skips, failures.
|
|
56
|
+
* Same channel, same reason: without it the AiNode page is somewhere the
|
|
57
|
+
* version silently changes with no explanation. */
|
|
58
|
+
auto_update?: AutoUpdateState;
|
|
45
59
|
}
|
|
46
60
|
/** Codex's credential store: $CODEX_HOME (or ~/.codex) + /auth.json. */
|
|
47
61
|
export declare function codexHome(): string;
|
package/dist/capabilities.js
CHANGED
|
@@ -41,6 +41,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
41
41
|
exports.codexHome = codexHome;
|
|
42
42
|
exports.codexAuthFromStore = codexAuthFromStore;
|
|
43
43
|
exports.probeCapabilities = probeCapabilities;
|
|
44
|
+
const engine_1 = require("./desktop/engine");
|
|
44
45
|
const child_process_1 = require("child_process");
|
|
45
46
|
const fs = __importStar(require("fs"));
|
|
46
47
|
const path = __importStar(require("path"));
|
|
@@ -53,6 +54,8 @@ const codex_binary_1 = require("./codex-binary");
|
|
|
53
54
|
const win_1 = require("./win");
|
|
54
55
|
const harness_registry_1 = require("./harness-registry");
|
|
55
56
|
const autostart_1 = require("./autostart");
|
|
57
|
+
const auto_update_1 = require("./auto-update");
|
|
58
|
+
const machine_metrics_1 = require("./machine-metrics");
|
|
56
59
|
function readDaemonVersion() {
|
|
57
60
|
try {
|
|
58
61
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -375,13 +378,14 @@ async function probeCapabilities() {
|
|
|
375
378
|
// heartbeat — that would report the runtime permanently offline.
|
|
376
379
|
const shield = (p, fallback) => p.catch(() => fallback);
|
|
377
380
|
const unavailable = { available: false, authed: false };
|
|
378
|
-
const [claude, codex, kimi, gemini, grok, git] = await Promise.all([
|
|
381
|
+
const [claude, codex, kimi, gemini, grok, git, containerEngine] = await Promise.all([
|
|
379
382
|
shield(probeClaude(), unavailable),
|
|
380
383
|
shield(probeCodex(), unavailable),
|
|
381
384
|
shield(probeKimi(), unavailable),
|
|
382
385
|
shield(probeGemini(), unavailable),
|
|
383
386
|
shield(probeGrok(), unavailable),
|
|
384
387
|
shield(probeGit(), unavailable),
|
|
388
|
+
shield((0, engine_1.detectEngine)(), null),
|
|
385
389
|
]);
|
|
386
390
|
// Decorate each harness with its registry facts (efforts, installability,
|
|
387
391
|
// login strategy) so Studio and the TUI render from one source of truth.
|
|
@@ -398,6 +402,9 @@ async function probeCapabilities() {
|
|
|
398
402
|
return {
|
|
399
403
|
daemon_version: readDaemonVersion(),
|
|
400
404
|
autostart: (0, autostart_1.status)(),
|
|
405
|
+
container_engine: containerEngine,
|
|
406
|
+
auto_update: (0, auto_update_1.autoUpdateState)(),
|
|
407
|
+
machine: (0, machine_metrics_1.sampleMachine)(),
|
|
401
408
|
claude: deco('claude', claude),
|
|
402
409
|
codex: deco('codex', codex),
|
|
403
410
|
kimi: deco('kimi', kimi),
|
package/dist/codex-spawn.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ export interface CodexInput {
|
|
|
25
25
|
* Mirrors the claude --strict-mcp-config behavior. */
|
|
26
26
|
mcpServers?: CodexMcpServer[];
|
|
27
27
|
}
|
|
28
|
+
/** Build a config.toml that declares only the supplied MCP servers and
|
|
29
|
+
* return the CODEX_HOME directory plus any env vars the child needs
|
|
30
|
+
* (bearer tokens for remote servers). Caller is responsible for cleanup
|
|
31
|
+
* (we drop it under os.tmpdir() so it gets reaped). */
|
|
32
|
+
export declare function writeCodexHome(servers: CodexMcpServer[], workingDirectory: string): {
|
|
33
|
+
home: string;
|
|
34
|
+
extraEnv: Record<string, string>;
|
|
35
|
+
};
|
|
28
36
|
export interface CodexHandle {
|
|
29
37
|
pid: number | undefined;
|
|
30
38
|
threadId: string | null;
|
package/dist/codex-spawn.js
CHANGED
|
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
40
40
|
};
|
|
41
41
|
})();
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.writeCodexHome = writeCodexHome;
|
|
43
44
|
exports.codexEffort = codexEffort;
|
|
44
45
|
exports.spawnCodex = spawnCodex;
|
|
45
46
|
const child_process_1 = require("child_process");
|
|
@@ -54,15 +55,58 @@ const events_1 = require("./events");
|
|
|
54
55
|
function tomlString(s) {
|
|
55
56
|
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
|
56
57
|
}
|
|
58
|
+
/** Env var name carrying an HTTP MCP's bearer token into the codex process.
|
|
59
|
+
* Only the NAME goes in config.toml — the value rides on the child env, so
|
|
60
|
+
* the token never lands on disk. */
|
|
61
|
+
const bearerEnvVar = (slug) => `AINODE_MCP_${slug.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}_TOKEN`;
|
|
57
62
|
/** Build a config.toml that declares only the supplied MCP servers and
|
|
58
|
-
* return the directory
|
|
59
|
-
*
|
|
63
|
+
* return the CODEX_HOME directory plus any env vars the child needs
|
|
64
|
+
* (bearer tokens for remote servers). Caller is responsible for cleanup
|
|
65
|
+
* (we drop it under os.tmpdir() so it gets reaped). */
|
|
60
66
|
function writeCodexHome(servers, workingDirectory) {
|
|
61
67
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'entities-codex-'));
|
|
68
|
+
const extraEnv = {};
|
|
62
69
|
let toml = '# Generated by ainode — sandboxes codex against host config\n\n';
|
|
63
70
|
for (const s of servers) {
|
|
64
71
|
if (!s.slug || !s.command)
|
|
65
72
|
continue;
|
|
73
|
+
// Remote MCP servers. Registry rows store command='http'|'sse' with
|
|
74
|
+
// args=[url]; codex spells that `url = "..."`, NOT command/args. Writing
|
|
75
|
+
// the row verbatim made codex try to exec a binary called `http`, so the
|
|
76
|
+
// server never started and its tools silently vanished — which is how a
|
|
77
|
+
// provider-ladder hop to codex dropped an entity's GitHub access without
|
|
78
|
+
// any error anywhere. The claude path (install.ts) always handled this;
|
|
79
|
+
// codex and grok did not.
|
|
80
|
+
if (s.command === 'http' || s.command === 'sse') {
|
|
81
|
+
const url = Array.isArray(s.args) && typeof s.args[0] === 'string' ? s.args[0] : null;
|
|
82
|
+
if (!url) {
|
|
83
|
+
console.error(`[codex] MCP ${s.slug}: ${s.command} server has no URL — skipped`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const name = tomlString(s.slug).slice(1, -1);
|
|
87
|
+
toml += `[mcp_servers.${name}]\n`;
|
|
88
|
+
toml += `url = ${tomlString(url)}\n`;
|
|
89
|
+
// codex reads the bearer from its OWN environment (--env is stdio-only),
|
|
90
|
+
// so the value goes on the child env and only the var name in the file.
|
|
91
|
+
const creds = Object.entries(s.env || {})
|
|
92
|
+
.filter(([k, v]) => typeof v === 'string' && v.length > 0 && !k.startsWith('OAUTH_REFRESH'));
|
|
93
|
+
const bearer = creds.find(([k]) => k === 'OAUTH_ACCESS_TOKEN') ?? creds[0];
|
|
94
|
+
if (bearer) {
|
|
95
|
+
const varName = bearerEnvVar(s.slug);
|
|
96
|
+
extraEnv[varName] = String(bearer[1]);
|
|
97
|
+
toml += `bearer_token_env_var = ${tomlString(varName)}\n`;
|
|
98
|
+
if (creds.length > 1) {
|
|
99
|
+
// codex carries one bearer and no arbitrary headers. Anything that
|
|
100
|
+
// authenticates with several custom headers is only partly wired,
|
|
101
|
+
// and saying so beats a server that 401s for no visible reason.
|
|
102
|
+
console.error(`[codex] MCP ${s.slug}: only ${bearer[0]} is forwarded as a bearer token; ` +
|
|
103
|
+
`codex cannot send custom headers, so ${creds.length - 1} other credential ` +
|
|
104
|
+
`field(s) were dropped`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
toml += '\n';
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
66
110
|
// On native Windows, registry commands like `npx` are .cmd shims that
|
|
67
111
|
// codex can't CreateProcess directly — wrap with `cmd /c` (no-op on POSIX).
|
|
68
112
|
const wrapped = (0, win_1.wrapMcpCommandForPlatform)(s.command, Array.isArray(s.args) ? s.args : []);
|
|
@@ -109,7 +153,7 @@ function writeCodexHome(servers, workingDirectory) {
|
|
|
109
153
|
if (fs.existsSync(sessionSkillsDir)) {
|
|
110
154
|
(0, win_1.linkDir)(sessionSkillsDir, path.join(dir, 'skills'));
|
|
111
155
|
}
|
|
112
|
-
return dir;
|
|
156
|
+
return { home: dir, extraEnv };
|
|
113
157
|
}
|
|
114
158
|
function lineToEvents(line) {
|
|
115
159
|
const type = typeof line.type === 'string' ? line.type : '';
|
|
@@ -194,7 +238,7 @@ function spawnCodex(input) {
|
|
|
194
238
|
// Sandbox codex against an isolated CODEX_HOME so the host's
|
|
195
239
|
// ~/.codex/config.toml mcp_servers don't leak in. TERM=dumb keeps
|
|
196
240
|
// codex's TTY heuristics happy (no fancy terminal output).
|
|
197
|
-
const codexHome = writeCodexHome(input.mcpServers ?? [], input.workingDirectory);
|
|
241
|
+
const { home: codexHome, extraEnv: mcpBearerEnv } = writeCodexHome(input.mcpServers ?? [], input.workingDirectory);
|
|
198
242
|
// Register the scratch dir with DiskGuard: marks it active (so a
|
|
199
243
|
// concurrent sweep won't evict a live run) and writes a PID lock so a
|
|
200
244
|
// different runtime process can tell it's still owned. dispose(true)
|
|
@@ -205,6 +249,9 @@ function spawnCodex(input) {
|
|
|
205
249
|
...process.env,
|
|
206
250
|
TERM: 'dumb',
|
|
207
251
|
CODEX_HOME: codexHome,
|
|
252
|
+
// Bearer tokens for remote (http/sse) MCP servers. config.toml names these
|
|
253
|
+
// vars via bearer_token_env_var; the values only ever live here.
|
|
254
|
+
...mcpBearerEnv,
|
|
208
255
|
};
|
|
209
256
|
// Resolve binary path before spawn. Previously called spawn('codex', ...)
|
|
210
257
|
// bare, which threw ENOENT under launchd / desktop-app PATH inheritance
|
package/dist/command-runner.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type RestartHook = (opts: {
|
|
2
|
-
|
|
2
|
+
/** Absent when the hourly auto-update is the one rolling: nobody issued a
|
|
3
|
+
* command, so there is nothing to close on the other side. */
|
|
4
|
+
commandId?: string;
|
|
3
5
|
version: string;
|
|
4
6
|
/** Bounce this daemon on the version it is already running: no npm
|
|
5
7
|
* install, no version change. See runUpdateRuntime. */
|