@openape/nest 2.3.1 → 2.3.2
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 +91 -337
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -59,6 +59,24 @@ function ecosystemPath(agentName) {
|
|
|
59
59
|
}
|
|
60
60
|
function ecosystemContents(apesBin, agentName) {
|
|
61
61
|
void apesBin;
|
|
62
|
+
const envForwards = [
|
|
63
|
+
"APE_CHAT_BRIDGE_MODEL",
|
|
64
|
+
"LITELLM_BASE_URL",
|
|
65
|
+
"LITELLM_API_KEY",
|
|
66
|
+
"APE_CHAT_BRIDGE_TOOLS",
|
|
67
|
+
"APE_CHAT_BRIDGE_MAX_STEPS",
|
|
68
|
+
"APE_CHAT_BRIDGE_SYSTEM_PROMPT",
|
|
69
|
+
// Chat backend selection (chat.openape.ai vs troop.openape.ai) —
|
|
70
|
+
// honoured by the bridge at startup. See ape-agent/src/bridge.ts.
|
|
71
|
+
"OPENAPE_BRIDGE_TARGET",
|
|
72
|
+
"APE_CHAT_ENDPOINT"
|
|
73
|
+
];
|
|
74
|
+
const envLines = envForwards.filter((k) => process2.env[k] !== void 0).map((k) => ` ${k}: ${JSON.stringify(process2.env[k])},`).join("\n");
|
|
75
|
+
const envBlock = envLines ? `
|
|
76
|
+
env: {
|
|
77
|
+
${envLines}
|
|
78
|
+
},
|
|
79
|
+
` : "";
|
|
62
80
|
return `// Auto-generated by Pm2Supervisor for agent '${agentName}'.
|
|
63
81
|
// Edit at runtime via:
|
|
64
82
|
// apes run --as ${agentName} -- pm2 reload ${pm2AppName(agentName)}
|
|
@@ -70,7 +88,7 @@ module.exports = {
|
|
|
70
88
|
max_restarts: 10,
|
|
71
89
|
min_uptime: '30s',
|
|
72
90
|
restart_delay: 2000,
|
|
73
|
-
merge_logs: true
|
|
91
|
+
merge_logs: true,${envBlock}
|
|
74
92
|
}],
|
|
75
93
|
}
|
|
76
94
|
`;
|
|
@@ -85,8 +103,12 @@ function startScriptContents(agentName) {
|
|
|
85
103
|
# Auto-generated by Pm2Supervisor for agent '${agentName}'.
|
|
86
104
|
set -e
|
|
87
105
|
ME="$(whoami)"
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
if command -v getent >/dev/null 2>&1; then
|
|
107
|
+
export HOME="$(getent passwd "$ME" | cut -d: -f6)"
|
|
108
|
+
else
|
|
109
|
+
export HOME="$(dscl . -read "/Users/$ME" NFSHomeDirectory 2>/dev/null | awk '{print $2}')"
|
|
110
|
+
fi
|
|
111
|
+
test -n "$HOME" || { echo "no home dir for $ME (agent ${agentName})" >&2; exit 1; }
|
|
90
112
|
export PM2_HOME="$HOME/.pm2"
|
|
91
113
|
mkdir -p "$(dirname "${log2}")"
|
|
92
114
|
exec pm2 startOrReload ${ecosystem} >> ${log2} 2>&1 < /dev/null
|
|
@@ -171,12 +193,15 @@ var Pm2Supervisor = class {
|
|
|
171
193
|
* always-writable location.
|
|
172
194
|
*/
|
|
173
195
|
async runAsAgent(agentName, args) {
|
|
196
|
+
const bin = process2.env.OPENAPE_BYPASS_APE_SHELL === "1" ? "sudo" : this.deps.apesBin;
|
|
197
|
+
const argv = process2.env.OPENAPE_BYPASS_APE_SHELL === "1" ? ["-n", "-H", "-u", agentName, "--", ...args] : ["run", "--as", agentName, "--wait", "--", ...args];
|
|
174
198
|
try {
|
|
175
|
-
return await execFileAsync(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
199
|
+
return await execFileAsync(bin, argv, {
|
|
200
|
+
maxBuffer: 1024 * 1024,
|
|
201
|
+
env: process2.env,
|
|
202
|
+
timeout: 6e4,
|
|
203
|
+
cwd: "/tmp"
|
|
204
|
+
});
|
|
180
205
|
} catch (err) {
|
|
181
206
|
const e = err;
|
|
182
207
|
const detail = (e.stderr ?? "").trim().split("\n").slice(-3).join(" / ");
|
|
@@ -225,11 +250,10 @@ var TroopSync = class {
|
|
|
225
250
|
}
|
|
226
251
|
async syncOne(name) {
|
|
227
252
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
253
|
+
const bypassApeShell = process3.env.OPENAPE_BYPASS_APE_SHELL === "1";
|
|
254
|
+
const cmd = bypassApeShell ? "/usr/bin/sudo" : this.deps.apesBin;
|
|
255
|
+
const argv = bypassApeShell ? ["-n", "-H", "-u", name, this.deps.apesBin, "agents", "sync"] : ["run", "--as", name, "--wait", "--", "apes", "agents", "sync"];
|
|
256
|
+
await execFileAsync2(cmd, argv, { maxBuffer: 1024 * 1024, env: process3.env, timeout: 6e4 });
|
|
233
257
|
} catch (err) {
|
|
234
258
|
this.deps.log(`troop-sync: ${name} failed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
235
259
|
}
|
|
@@ -237,309 +261,46 @@ var TroopSync = class {
|
|
|
237
261
|
};
|
|
238
262
|
|
|
239
263
|
// src/lib/troop-ws.ts
|
|
240
|
-
import { execFile as execFile3,
|
|
241
|
-
import { createHash } from "crypto";
|
|
264
|
+
import { execFile as execFile3, spawn } from "child_process";
|
|
242
265
|
import { readFileSync as readFileSync3 } from "fs";
|
|
243
|
-
import { hostname
|
|
266
|
+
import { hostname } from "os";
|
|
267
|
+
import WebSocket from "ws";
|
|
244
268
|
|
|
245
|
-
//
|
|
246
|
-
import {
|
|
247
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
269
|
+
// src/lib/nest-device.ts
|
|
270
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
248
271
|
import { homedir as homedir2 } from "os";
|
|
249
272
|
import { join as join3 } from "path";
|
|
250
|
-
import { ofetch
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
function getConfigDir() {
|
|
261
|
-
const override = process.env.OPENAPE_CLI_AUTH_HOME;
|
|
262
|
-
if (override) return override;
|
|
263
|
-
return join3(homedir2(), ".config", "apes");
|
|
264
|
-
}
|
|
265
|
-
function getAuthFile() {
|
|
266
|
-
return join3(getConfigDir(), "auth.json");
|
|
267
|
-
}
|
|
268
|
-
function ensureConfigDir() {
|
|
269
|
-
const dir = getConfigDir();
|
|
270
|
-
if (!existsSync3(dir)) {
|
|
271
|
-
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
function loadIdpAuth() {
|
|
275
|
-
const file = getAuthFile();
|
|
276
|
-
if (!existsSync3(file)) return null;
|
|
277
|
-
try {
|
|
278
|
-
const raw = readFileSync2(file, "utf-8");
|
|
279
|
-
if (!raw.trim()) return null;
|
|
280
|
-
return JSON.parse(raw);
|
|
281
|
-
} catch {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
function saveIdpAuth(auth) {
|
|
286
|
-
ensureConfigDir();
|
|
287
|
-
const file = getAuthFile();
|
|
288
|
-
let extra = {};
|
|
289
|
-
if (existsSync3(file)) {
|
|
290
|
-
try {
|
|
291
|
-
const raw = readFileSync2(file, "utf-8");
|
|
292
|
-
if (raw.trim()) {
|
|
293
|
-
const prev = JSON.parse(raw);
|
|
294
|
-
for (const key of Object.keys(prev)) {
|
|
295
|
-
if (!(key in auth)) {
|
|
296
|
-
extra[key] = prev[key];
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
} catch {
|
|
301
|
-
extra = {};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const merged = { ...extra, ...auth };
|
|
305
|
-
writeFileSync3(file, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
306
|
-
}
|
|
307
|
-
var AuthError = class extends Error {
|
|
308
|
-
status;
|
|
309
|
-
hint;
|
|
310
|
-
constructor(status, message, hint) {
|
|
311
|
-
super(hint ? `${message}
|
|
312
|
-
${hint}` : message);
|
|
313
|
-
this.name = "AuthError";
|
|
314
|
-
this.status = status;
|
|
315
|
-
this.hint = hint;
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
var NotLoggedInError = class extends AuthError {
|
|
319
|
-
constructor(hint) {
|
|
320
|
-
super(
|
|
321
|
-
401,
|
|
322
|
-
"Not logged in",
|
|
323
|
-
hint ?? "Run `apes login <email>` once on this device to authenticate against the OpenApe IdP."
|
|
324
|
-
);
|
|
325
|
-
this.name = "NotLoggedInError";
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
var OPENSSH_MAGIC = "openssh-key-v1\0";
|
|
329
|
-
function loadEd25519PrivateKey(pem) {
|
|
330
|
-
if (pem.includes("BEGIN OPENSSH PRIVATE KEY")) {
|
|
331
|
-
return parseOpenSSHEd25519(pem);
|
|
332
|
-
}
|
|
333
|
-
return createPrivateKey(pem);
|
|
334
|
-
}
|
|
335
|
-
function parseOpenSSHEd25519(pem) {
|
|
336
|
-
const b64 = pem.replace(/-----BEGIN OPENSSH PRIVATE KEY-----/, "").replace(/-----END OPENSSH PRIVATE KEY-----/, "").replace(/\s/g, "");
|
|
337
|
-
const buf = Buffer3.from(b64, "base64");
|
|
338
|
-
let offset = 0;
|
|
339
|
-
const magic = buf.subarray(0, OPENSSH_MAGIC.length).toString("ascii");
|
|
340
|
-
if (magic !== OPENSSH_MAGIC) {
|
|
341
|
-
throw new Error("Not an OpenSSH private key");
|
|
342
|
-
}
|
|
343
|
-
offset += OPENSSH_MAGIC.length;
|
|
344
|
-
const cipherLen = buf.readUInt32BE(offset);
|
|
345
|
-
offset += 4;
|
|
346
|
-
const cipher = buf.subarray(offset, offset + cipherLen).toString();
|
|
347
|
-
offset += cipherLen;
|
|
348
|
-
if (cipher !== "none") {
|
|
349
|
-
throw new Error(`Encrypted keys not supported (cipher: ${cipher}). Decrypt first with: ssh-keygen -p -f <key>`);
|
|
350
|
-
}
|
|
351
|
-
const kdfLen = buf.readUInt32BE(offset);
|
|
352
|
-
offset += 4;
|
|
353
|
-
offset += kdfLen;
|
|
354
|
-
const kdfOptsLen = buf.readUInt32BE(offset);
|
|
355
|
-
offset += 4;
|
|
356
|
-
offset += kdfOptsLen;
|
|
357
|
-
const numKeys = buf.readUInt32BE(offset);
|
|
358
|
-
offset += 4;
|
|
359
|
-
if (numKeys !== 1) {
|
|
360
|
-
throw new Error(`Expected 1 key, got ${numKeys}`);
|
|
361
|
-
}
|
|
362
|
-
const pubSectionLen = buf.readUInt32BE(offset);
|
|
363
|
-
offset += 4;
|
|
364
|
-
offset += pubSectionLen;
|
|
365
|
-
const privSectionLen = buf.readUInt32BE(offset);
|
|
366
|
-
offset += 4;
|
|
367
|
-
const privSection = buf.subarray(offset, offset + privSectionLen);
|
|
368
|
-
let pOffset = 0;
|
|
369
|
-
const check1 = privSection.readUInt32BE(pOffset);
|
|
370
|
-
pOffset += 4;
|
|
371
|
-
const check2 = privSection.readUInt32BE(pOffset);
|
|
372
|
-
pOffset += 4;
|
|
373
|
-
if (check1 !== check2) {
|
|
374
|
-
throw new Error("Check integers mismatch \u2014 key may be corrupted or encrypted");
|
|
375
|
-
}
|
|
376
|
-
const keyTypeLen = privSection.readUInt32BE(pOffset);
|
|
377
|
-
pOffset += 4;
|
|
378
|
-
const keyType = privSection.subarray(pOffset, pOffset + keyTypeLen).toString();
|
|
379
|
-
pOffset += keyTypeLen;
|
|
380
|
-
if (keyType !== "ssh-ed25519") {
|
|
381
|
-
throw new Error(`Expected ssh-ed25519, got ${keyType}`);
|
|
382
|
-
}
|
|
383
|
-
const pubKeyLen = privSection.readUInt32BE(pOffset);
|
|
384
|
-
pOffset += 4;
|
|
385
|
-
const pubKey = privSection.subarray(pOffset, pOffset + pubKeyLen);
|
|
386
|
-
pOffset += pubKeyLen;
|
|
387
|
-
const privKeyLen = privSection.readUInt32BE(pOffset);
|
|
388
|
-
pOffset += 4;
|
|
389
|
-
const privKeyData = privSection.subarray(pOffset, pOffset + privKeyLen);
|
|
390
|
-
const seed = privKeyData.subarray(0, 32);
|
|
391
|
-
return createPrivateKey({
|
|
392
|
-
key: { kty: "OKP", crv: "Ed25519", d: seed.toString("base64url"), x: pubKey.toString("base64url") },
|
|
393
|
-
format: "jwk"
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
async function getEndpoints(idp) {
|
|
397
|
-
let disco = {};
|
|
398
|
-
try {
|
|
399
|
-
disco = await ofetch2(`${idp}/.well-known/openid-configuration`);
|
|
400
|
-
} catch {
|
|
401
|
-
}
|
|
402
|
-
return {
|
|
403
|
-
challenge: disco.ddisa_agent_challenge_endpoint ?? `${idp}/api/agent/challenge`,
|
|
404
|
-
authenticate: disco.ddisa_agent_authenticate_endpoint ?? `${idp}/api/agent/authenticate`
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
function resolveKeyPath(p) {
|
|
408
|
-
if (p.startsWith("~")) return join22(homedir22(), p.slice(1));
|
|
409
|
-
return p;
|
|
410
|
-
}
|
|
411
|
-
function findSigningKey(auth) {
|
|
412
|
-
const candidates = [];
|
|
413
|
-
if (auth.key_path) candidates.push(resolveKeyPath(auth.key_path));
|
|
414
|
-
candidates.push(join22(homedir22(), ".ssh", "id_ed25519"));
|
|
415
|
-
for (const p of candidates) {
|
|
416
|
-
if (existsSync22(p)) {
|
|
417
|
-
try {
|
|
418
|
-
return { keyPath: p, keyContent: readFileSync22(p, "utf-8") };
|
|
419
|
-
} catch {
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
async function refreshAgentToken(auth, now = Math.floor(Date.now() / 1e3)) {
|
|
426
|
-
const key = findSigningKey(auth);
|
|
427
|
-
if (!key) return null;
|
|
428
|
-
let privateKey;
|
|
429
|
-
try {
|
|
430
|
-
privateKey = loadEd25519PrivateKey(key.keyContent);
|
|
431
|
-
} catch {
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
let endpoints;
|
|
435
|
-
try {
|
|
436
|
-
endpoints = await getEndpoints(auth.idp);
|
|
437
|
-
} catch {
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
let challenge;
|
|
441
|
-
try {
|
|
442
|
-
const resp = await ofetch2(endpoints.challenge, {
|
|
443
|
-
method: "POST",
|
|
444
|
-
headers: { "Content-Type": "application/json" },
|
|
445
|
-
body: { agent_id: auth.email }
|
|
446
|
-
});
|
|
447
|
-
challenge = resp.challenge;
|
|
448
|
-
} catch {
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
let signature;
|
|
452
|
-
try {
|
|
453
|
-
signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
|
|
454
|
-
} catch {
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
let authResp;
|
|
273
|
+
import { ofetch } from "ofetch";
|
|
274
|
+
function resolveDevicePath() {
|
|
275
|
+
return process.env.OPENAPE_NEST_DEVICE_PATH ?? join3(homedir2(), "nest-device.json");
|
|
276
|
+
}
|
|
277
|
+
function readDeviceCreds() {
|
|
278
|
+
const envHost = process.env.OPENAPE_NEST_HOST_ID?.trim();
|
|
279
|
+
const envSecret = process.env.OPENAPE_NEST_DEVICE_SECRET?.trim();
|
|
280
|
+
if (envHost && envSecret) return { hostId: envHost, deviceSecret: envSecret };
|
|
281
|
+
const path = resolveDevicePath();
|
|
282
|
+
if (!existsSync3(path)) return null;
|
|
458
283
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
284
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
285
|
+
const hostId = typeof parsed.host_id === "string" ? parsed.host_id.trim() : "";
|
|
286
|
+
const deviceSecret = typeof parsed.device_secret === "string" ? parsed.device_secret : "";
|
|
287
|
+
if (!hostId || !deviceSecret) return null;
|
|
288
|
+
return { hostId, deviceSecret };
|
|
464
289
|
} catch {
|
|
465
290
|
return null;
|
|
466
291
|
}
|
|
467
|
-
return {
|
|
468
|
-
...auth,
|
|
469
|
-
access_token: authResp.token,
|
|
470
|
-
expires_at: now + (authResp.expires_in || 3600),
|
|
471
|
-
key_path: auth.key_path ?? key.keyPath
|
|
472
|
-
};
|
|
473
292
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
try {
|
|
477
|
-
const disco = await ofetch3(`${idp}/.well-known/openid-configuration`);
|
|
478
|
-
if (disco.token_endpoint) return disco.token_endpoint;
|
|
479
|
-
} catch {
|
|
480
|
-
}
|
|
481
|
-
return `${idp}/token`;
|
|
293
|
+
function troopHttpUrl(troopWsUrl) {
|
|
294
|
+
return troopWsUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
|
|
482
295
|
}
|
|
483
|
-
async function
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return auth;
|
|
490
|
-
}
|
|
491
|
-
if (!auth.refresh_token) {
|
|
492
|
-
const refreshed = await refreshAgentToken(auth, now);
|
|
493
|
-
if (refreshed) {
|
|
494
|
-
saveIdpAuth(refreshed);
|
|
495
|
-
return refreshed;
|
|
496
|
-
}
|
|
497
|
-
throw new NotLoggedInError(
|
|
498
|
-
`IdP token expired at ${new Date(auth.expires_at * 1e3).toISOString()} and no refresh_token is stored. Run \`apes login\` again.`
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
const tokenEndpoint = await getTokenEndpoint(auth.idp);
|
|
502
|
-
const body = new URLSearchParams({
|
|
503
|
-
grant_type: "refresh_token",
|
|
504
|
-
refresh_token: auth.refresh_token
|
|
505
|
-
});
|
|
506
|
-
let response;
|
|
507
|
-
try {
|
|
508
|
-
response = await ofetch3(tokenEndpoint, {
|
|
509
|
-
method: "POST",
|
|
510
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
511
|
-
body: body.toString()
|
|
512
|
-
});
|
|
513
|
-
} catch (err) {
|
|
514
|
-
const status = err.status ?? err.statusCode ?? 0;
|
|
515
|
-
if (status === 400 || status === 401) {
|
|
516
|
-
saveIdpAuth({ ...auth, refresh_token: void 0 });
|
|
517
|
-
throw new NotLoggedInError(
|
|
518
|
-
`Refresh token rejected by ${auth.idp}. Run \`apes login\` again.`
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
throw new AuthError(
|
|
522
|
-
0,
|
|
523
|
-
`Network error refreshing IdP token at ${tokenEndpoint}`,
|
|
524
|
-
`Underlying: ${err.message ?? err}`
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
if (!response.access_token) {
|
|
528
|
-
throw new AuthError(0, `IdP refresh response missing access_token (endpoint: ${tokenEndpoint})`);
|
|
529
|
-
}
|
|
530
|
-
const next = {
|
|
531
|
-
...auth,
|
|
532
|
-
access_token: response.access_token,
|
|
533
|
-
refresh_token: response.refresh_token ?? auth.refresh_token,
|
|
534
|
-
expires_at: now + (response.expires_in ?? 3600)
|
|
535
|
-
};
|
|
536
|
-
saveIdpAuth(next);
|
|
537
|
-
return next;
|
|
296
|
+
async function mintNestToken(troopWsUrl, creds) {
|
|
297
|
+
const res = await ofetch(
|
|
298
|
+
`${troopHttpUrl(troopWsUrl)}/api/nests/token`,
|
|
299
|
+
{ method: "POST", body: { host_id: creds.hostId, device_secret: creds.deviceSecret } }
|
|
300
|
+
);
|
|
301
|
+
return { token: res.access_token, expiresAt: res.expires_at };
|
|
538
302
|
}
|
|
539
303
|
|
|
540
|
-
// src/lib/troop-ws.ts
|
|
541
|
-
import WebSocket from "ws";
|
|
542
|
-
|
|
543
304
|
// src/lib/secret-relay.ts
|
|
544
305
|
var SECRETS_REL_DIR = ".config/openape/secrets.d";
|
|
545
306
|
var ENV_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
@@ -570,7 +331,6 @@ var TroopWs = class {
|
|
|
570
331
|
constructor(opts) {
|
|
571
332
|
this.opts = opts;
|
|
572
333
|
this.troopUrl = (opts.troopUrl ?? process.env.OPENAPE_TROOP_WS_URL ?? "wss://troop.openape.ai").replace(/\/$/, "");
|
|
573
|
-
this.hostId = readHostId();
|
|
574
334
|
this.hostname = hostname();
|
|
575
335
|
}
|
|
576
336
|
opts;
|
|
@@ -580,8 +340,14 @@ var TroopWs = class {
|
|
|
580
340
|
reconnectAttempts = 0;
|
|
581
341
|
stopped = false;
|
|
582
342
|
troopUrl;
|
|
583
|
-
|
|
343
|
+
// Set from the device creds on each connect (troop is authoritative for
|
|
344
|
+
// host_id now; the daemon no longer self-fingerprints).
|
|
345
|
+
hostId = "";
|
|
584
346
|
hostname;
|
|
347
|
+
// Short-lived device token, held in memory only and re-minted before
|
|
348
|
+
// expiry. Never persisted — only the long-lived device_secret is on disk.
|
|
349
|
+
cachedToken = null;
|
|
350
|
+
cachedTokenExp = 0;
|
|
585
351
|
start() {
|
|
586
352
|
this.stopped = false;
|
|
587
353
|
void this.connect();
|
|
@@ -600,18 +366,30 @@ var TroopWs = class {
|
|
|
600
366
|
this.socket = null;
|
|
601
367
|
}
|
|
602
368
|
}
|
|
369
|
+
// Mint (or reuse) a short-lived device token. Cached in memory and
|
|
370
|
+
// re-minted ~1 min before expiry; never written to disk.
|
|
371
|
+
async mintToken(creds) {
|
|
372
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
373
|
+
if (this.cachedToken && this.cachedTokenExp - nowSec > 60) return this.cachedToken;
|
|
374
|
+
const { token, expiresAt } = await mintNestToken(this.troopUrl, creds);
|
|
375
|
+
this.cachedToken = token;
|
|
376
|
+
this.cachedTokenExp = expiresAt;
|
|
377
|
+
return token;
|
|
378
|
+
}
|
|
603
379
|
async connect() {
|
|
604
380
|
if (this.stopped) return;
|
|
381
|
+
const creds = readDeviceCreds();
|
|
382
|
+
if (!creds) {
|
|
383
|
+
this.opts.log("troop-ws: no device creds (set OPENAPE_NEST_HOST_ID + OPENAPE_NEST_DEVICE_SECRET, or ~/nest-device.json) \u2014 not connecting, will retry");
|
|
384
|
+
this.scheduleReconnect();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.hostId = creds.hostId;
|
|
605
388
|
let token;
|
|
606
389
|
try {
|
|
607
|
-
|
|
608
|
-
token = auth.access_token;
|
|
390
|
+
token = await this.mintToken(creds);
|
|
609
391
|
} catch (err) {
|
|
610
|
-
|
|
611
|
-
this.opts.log("troop-ws: not logged in (apes login) \u2014 skip connect, will retry");
|
|
612
|
-
} else {
|
|
613
|
-
this.opts.log(`troop-ws: auth refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
614
|
-
}
|
|
392
|
+
this.opts.log(`troop-ws: token mint failed (nest revoked or bad secret?): ${err instanceof Error ? err.message : String(err)}`);
|
|
615
393
|
this.scheduleReconnect();
|
|
616
394
|
return;
|
|
617
395
|
}
|
|
@@ -825,30 +603,6 @@ function runWithInput(bin, args, input) {
|
|
|
825
603
|
child.stdin?.end(input);
|
|
826
604
|
});
|
|
827
605
|
}
|
|
828
|
-
function readHostId() {
|
|
829
|
-
try {
|
|
830
|
-
if (process.platform === "darwin") {
|
|
831
|
-
const out = execFileSync("/usr/sbin/ioreg", ["-d2", "-c", "IOPlatformExpertDevice"], { encoding: "utf8" });
|
|
832
|
-
const match = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
833
|
-
if (match) return match[1];
|
|
834
|
-
}
|
|
835
|
-
} catch {
|
|
836
|
-
}
|
|
837
|
-
return hashBasedHostId();
|
|
838
|
-
}
|
|
839
|
-
function hashBasedHostId() {
|
|
840
|
-
const nics = networkInterfaces();
|
|
841
|
-
const macs = [];
|
|
842
|
-
for (const list of Object.values(nics)) {
|
|
843
|
-
if (!list) continue;
|
|
844
|
-
for (const nic of list) {
|
|
845
|
-
if (!nic.mac || nic.mac === "00:00:00:00:00:00" || nic.internal) continue;
|
|
846
|
-
macs.push(nic.mac);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
const seed = `${hostname()}|${macs.toSorted().join(",")}`;
|
|
850
|
-
return createHash("sha256").update(seed).digest("hex").slice(0, 32);
|
|
851
|
-
}
|
|
852
606
|
function readNestVersion() {
|
|
853
607
|
try {
|
|
854
608
|
const root = new URL("../../package.json", import.meta.url);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/nest",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
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",
|