@openape/nest 1.0.0 → 1.1.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/index.mjs +84 -94
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -95,19 +95,18 @@ async function handleAgentSpawn(ctx) {
|
|
|
95
95
|
} : void 0
|
|
96
96
|
};
|
|
97
97
|
upsertAgent(entry);
|
|
98
|
-
ctx.supervisor.reconcile(listAgents());
|
|
98
|
+
await ctx.supervisor.reconcile(listAgents());
|
|
99
99
|
return { name, email: entry.email, uid, home: entry.home };
|
|
100
100
|
}
|
|
101
101
|
async function handleAgentDestroy(ctx, name) {
|
|
102
102
|
if (!NAME_REGEX.test(name)) throw new Error(`invalid agent name "${name}"`);
|
|
103
103
|
const entry = findAgent(name);
|
|
104
104
|
if (!entry) throw new Error(`agent "${name}" not registered with this nest`);
|
|
105
|
-
ctx.supervisor.stop(name);
|
|
106
105
|
ctx.log(`nest: destroying agent "${name}"...`);
|
|
107
106
|
const args = ["run", "--as", "root", "--", "apes", "agents", "destroy", name, "--force"];
|
|
108
107
|
await execFileAsync(ctx.apesBin, args, { maxBuffer: 4 * 1024 * 1024 });
|
|
109
108
|
removeAgent(name);
|
|
110
|
-
ctx.supervisor.reconcile(listAgents());
|
|
109
|
+
await ctx.supervisor.reconcile(listAgents());
|
|
111
110
|
return { name, removed: true };
|
|
112
111
|
}
|
|
113
112
|
async function readUidFromDscl(name) {
|
|
@@ -233,109 +232,97 @@ function reapStaleResponses(log2) {
|
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
|
|
236
|
-
// src/lib/supervisor.ts
|
|
237
|
-
import {
|
|
235
|
+
// src/lib/pm2-supervisor.ts
|
|
236
|
+
import { execFile as execFile2 } from "child_process";
|
|
237
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
238
|
+
import { join as join3 } from "path";
|
|
238
239
|
import process from "process";
|
|
239
|
-
|
|
240
|
-
var
|
|
241
|
-
var
|
|
242
|
-
|
|
240
|
+
import { promisify as promisify2 } from "util";
|
|
241
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
242
|
+
var AGENTS_DIR = "/var/openape/nest/agents";
|
|
243
|
+
function pm2AppName(agentName) {
|
|
244
|
+
return `openape-bridge-${agentName}`;
|
|
245
|
+
}
|
|
246
|
+
function ecosystemPath(agentName) {
|
|
247
|
+
return join3(AGENTS_DIR, agentName, "ecosystem.config.js");
|
|
248
|
+
}
|
|
249
|
+
function ecosystemContents(apesBin, agentName) {
|
|
250
|
+
void apesBin;
|
|
251
|
+
return `// Auto-generated by Pm2Supervisor for agent '${agentName}'.
|
|
252
|
+
// Edit at runtime via:
|
|
253
|
+
// apes run --as ${agentName} -- pm2 reload ${pm2AppName(agentName)}
|
|
254
|
+
module.exports = {
|
|
255
|
+
apps: [{
|
|
256
|
+
name: '${pm2AppName(agentName)}',
|
|
257
|
+
script: 'openape-chat-bridge',
|
|
258
|
+
autorestart: true,
|
|
259
|
+
max_restarts: 10,
|
|
260
|
+
min_uptime: '30s',
|
|
261
|
+
restart_delay: 2000,
|
|
262
|
+
merge_logs: true,
|
|
263
|
+
}],
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
var Pm2Supervisor = class {
|
|
243
268
|
constructor(deps) {
|
|
244
269
|
this.deps = deps;
|
|
245
270
|
}
|
|
246
|
-
|
|
247
|
-
/** Bring
|
|
248
|
-
reconcile(desired) {
|
|
249
|
-
const desiredNames = new Set(desired.filter((a) => a.bridge != null).map((a) => a.name));
|
|
250
|
-
for (const [name] of this.children) {
|
|
251
|
-
if (!desiredNames.has(name)) this.stop(name);
|
|
252
|
-
}
|
|
271
|
+
inflight = /* @__PURE__ */ new Set();
|
|
272
|
+
/** Bring per-agent pm2 state in line with the registry. Idempotent. */
|
|
273
|
+
async reconcile(desired) {
|
|
253
274
|
for (const agent of desired) {
|
|
254
275
|
if (agent.bridge == null) continue;
|
|
255
|
-
if (
|
|
276
|
+
if (this.inflight.has(agent.name)) continue;
|
|
277
|
+
this.inflight.add(agent.name);
|
|
278
|
+
try {
|
|
279
|
+
await this.startOrReload(agent.name);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
this.deps.log(`pm2-supervisor: ${agent.name} startOrReload failed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
282
|
+
} finally {
|
|
283
|
+
this.inflight.delete(agent.name);
|
|
284
|
+
}
|
|
256
285
|
}
|
|
257
286
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
status() {
|
|
262
|
-
const now = Date.now();
|
|
263
|
-
return Array.from(this.children.entries()).map(([name, s]) => ({
|
|
264
|
-
name,
|
|
265
|
-
pid: s.child.pid ?? -1,
|
|
266
|
-
uptimeSec: Math.floor((now - s.startedAt) / 1e3),
|
|
267
|
-
consecutiveCrashes: s.consecutiveCrashes
|
|
268
|
-
}));
|
|
269
|
-
}
|
|
270
|
-
start(agent) {
|
|
271
|
-
if (this.children.has(agent.name)) return;
|
|
272
|
-
this.deps.log(`supervisor: starting bridge for ${agent.name}`);
|
|
273
|
-
this.spawnChild(agent, 0);
|
|
274
|
-
}
|
|
275
|
-
stop(name) {
|
|
276
|
-
const s = this.children.get(name);
|
|
277
|
-
if (!s) return;
|
|
278
|
-
this.deps.log(`supervisor: stopping bridge for ${name}`);
|
|
279
|
-
if (s.restartTimer) clearTimeout(s.restartTimer);
|
|
280
|
-
this.children.delete(name);
|
|
287
|
+
/** Stop one agent's pm2 app — used at destroy time. */
|
|
288
|
+
async stop(agentName) {
|
|
289
|
+
const name = pm2AppName(agentName);
|
|
281
290
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
await this.runAsAgent(agentName, ["pm2", "delete", name]);
|
|
292
|
+
this.deps.log(`pm2-supervisor: deleted ${name}`);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
this.deps.log(`pm2-supervisor: delete ${name}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
284
295
|
}
|
|
285
296
|
}
|
|
286
|
-
|
|
287
|
-
|
|
297
|
+
/** Best-effort cleanup — called on Nest shutdown. We don't kill
|
|
298
|
+
* the per-agent pm2-daemons; they should keep running so bridges
|
|
299
|
+
* stay alive across Nest restarts. No-op for now. */
|
|
300
|
+
async stopAll() {
|
|
288
301
|
}
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// data dir, where its own auth.json lives so apes-cli reads
|
|
297
|
-
// the nest identity for the YOLO grant).
|
|
298
|
-
env: process.env
|
|
299
|
-
});
|
|
300
|
-
child.stdout?.on("data", (chunk) => this.forwardLog(agent.name, "stdout", chunk));
|
|
301
|
-
child.stderr?.on("data", (chunk) => this.forwardLog(agent.name, "stderr", chunk));
|
|
302
|
-
const supervised = {
|
|
303
|
-
child,
|
|
304
|
-
consecutiveCrashes: prevCrashes,
|
|
305
|
-
startedAt: Date.now()
|
|
306
|
-
};
|
|
307
|
-
this.children.set(agent.name, supervised);
|
|
308
|
-
child.on("exit", (code, signal) => {
|
|
309
|
-
const stillManaged = this.children.get(agent.name) === supervised;
|
|
310
|
-
if (!stillManaged) return;
|
|
311
|
-
const ranLongEnough = Date.now() - supervised.startedAt > STABLE_RUNTIME_MS;
|
|
312
|
-
const nextCrashes = ranLongEnough ? 1 : prevCrashes + 1;
|
|
313
|
-
const backoff = Math.min(MAX_BACKOFF_MS, MIN_BACKOFF_MS * 2 ** Math.max(0, nextCrashes - 1));
|
|
314
|
-
this.deps.log(
|
|
315
|
-
`supervisor: ${agent.name} bridge exited code=${code} signal=${signal ?? "none"} consecutive=${nextCrashes} \u2192 respawn in ${backoff}ms`
|
|
316
|
-
);
|
|
317
|
-
supervised.restartTimer = setTimeout(() => {
|
|
318
|
-
if (this.children.get(agent.name) !== supervised) return;
|
|
319
|
-
this.children.delete(agent.name);
|
|
320
|
-
this.spawnChild(agent, nextCrashes);
|
|
321
|
-
}, backoff);
|
|
322
|
-
});
|
|
302
|
+
async startOrReload(agentName) {
|
|
303
|
+
const dir = join3(AGENTS_DIR, agentName);
|
|
304
|
+
mkdirSync3(dir, { recursive: true });
|
|
305
|
+
const path = ecosystemPath(agentName);
|
|
306
|
+
writeFileSync3(path, ecosystemContents(this.deps.apesBin, agentName), { mode: 420 });
|
|
307
|
+
await this.runAsAgent(agentName, ["pm2", "startOrReload", path]);
|
|
308
|
+
this.deps.log(`pm2-supervisor: ${agentName} bridge (re)started via pm2`);
|
|
323
309
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
310
|
+
/** Run a pm2 subcommand AS the agent — escapes-helper does the
|
|
311
|
+
* setuid switch, then exec's pm2 in the agent's uid. */
|
|
312
|
+
async runAsAgent(agentName, args) {
|
|
313
|
+
return execFileAsync2(
|
|
314
|
+
this.deps.apesBin,
|
|
315
|
+
["run", "--as", agentName, "--wait", "--", ...args],
|
|
316
|
+
{ maxBuffer: 1024 * 1024, env: process.env, timeout: 6e4 }
|
|
317
|
+
);
|
|
331
318
|
}
|
|
332
319
|
};
|
|
333
320
|
|
|
334
321
|
// src/lib/troop-sync.ts
|
|
335
|
-
import { execFile as
|
|
322
|
+
import { execFile as execFile3 } from "child_process";
|
|
336
323
|
import process2 from "process";
|
|
337
|
-
import { promisify as
|
|
338
|
-
var
|
|
324
|
+
import { promisify as promisify3 } from "util";
|
|
325
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
339
326
|
var TICK_MS = 5 * 60 * 1e3;
|
|
340
327
|
var TroopSync = class {
|
|
341
328
|
constructor(deps) {
|
|
@@ -369,7 +356,7 @@ var TroopSync = class {
|
|
|
369
356
|
}
|
|
370
357
|
async syncOne(name) {
|
|
371
358
|
try {
|
|
372
|
-
await
|
|
359
|
+
await execFileAsync3(
|
|
373
360
|
this.deps.apesBin,
|
|
374
361
|
["run", "--as", name, "--wait", "--", "apes", "agents", "sync"],
|
|
375
362
|
{ maxBuffer: 1024 * 1024, env: process2.env, timeout: 6e4 }
|
|
@@ -386,17 +373,20 @@ function log(line) {
|
|
|
386
373
|
process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
387
374
|
`);
|
|
388
375
|
}
|
|
389
|
-
var supervisor = new
|
|
376
|
+
var supervisor = new Pm2Supervisor({ apesBin: APES_BIN, log });
|
|
390
377
|
var troopSync = new TroopSync({ apesBin: APES_BIN, log });
|
|
391
378
|
var intentChannel = new IntentChannel({ apesBin: APES_BIN, supervisor, log });
|
|
392
|
-
supervisor.reconcile(listAgents())
|
|
393
|
-
log(
|
|
379
|
+
void supervisor.reconcile(listAgents()).then(
|
|
380
|
+
() => log("nest: pm2-supervisor reconciled with registry")
|
|
381
|
+
).catch((err) => {
|
|
382
|
+
log(`nest: pm2-supervisor reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
383
|
+
});
|
|
394
384
|
troopSync.start();
|
|
395
385
|
intentChannel.start();
|
|
396
386
|
var reaperTimer = setInterval(reapStaleResponses, 60 * 60 * 1e3, log);
|
|
397
387
|
process3.on("SIGTERM", () => {
|
|
398
388
|
log("nest: SIGTERM \u2014 stopping");
|
|
399
|
-
supervisor.stopAll();
|
|
389
|
+
void supervisor.stopAll();
|
|
400
390
|
troopSync.stop();
|
|
401
391
|
intentChannel.stop();
|
|
402
392
|
clearInterval(reaperTimer);
|
|
@@ -404,7 +394,7 @@ process3.on("SIGTERM", () => {
|
|
|
404
394
|
});
|
|
405
395
|
process3.on("SIGINT", () => {
|
|
406
396
|
log("nest: SIGINT \u2014 stopping");
|
|
407
|
-
supervisor.stopAll();
|
|
397
|
+
void supervisor.stopAll();
|
|
408
398
|
troopSync.stop();
|
|
409
399
|
intentChannel.stop();
|
|
410
400
|
clearInterval(reaperTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/nest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "OpenApe Nest — local control-plane daemon that supervises agent processes on this computer. Talks to troop SP for ownership state, spawns/destroys agents via DDISA always-grants, supervises chat-bridge children (replacing per-agent launchd plists).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"ofetch": "^1.4.1",
|
|
20
|
-
"@openape/
|
|
21
|
-
"@openape/
|
|
20
|
+
"@openape/cli-auth": "0.4.0",
|
|
21
|
+
"@openape/core": "0.16.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@antfu/eslint-config": "^7.6.1",
|