@openape/ape-agent 2.10.0 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.mjs +1174 -294
- package/dist/index.d.ts +356 -0
- package/dist/index.mjs +4923 -0
- package/dist/service-bridge-main.mjs +1133 -833
- package/package.json +13 -3
package/dist/bridge.mjs
CHANGED
|
@@ -33,15 +33,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
33
33
|
));
|
|
34
34
|
|
|
35
35
|
// ../../packages/apes/dist/chunk-OBF7IMQ2.js
|
|
36
|
-
import { homedir as
|
|
37
|
-
import { join as
|
|
38
|
-
var
|
|
36
|
+
import { homedir as homedir4 } from "os";
|
|
37
|
+
import { join as join4 } from "path";
|
|
38
|
+
var CONFIG_DIR2, AUTH_FILE, CONFIG_FILE;
|
|
39
39
|
var init_chunk_OBF7IMQ2 = __esm({
|
|
40
40
|
"../../packages/apes/dist/chunk-OBF7IMQ2.js"() {
|
|
41
41
|
"use strict";
|
|
42
|
-
|
|
43
|
-
AUTH_FILE =
|
|
44
|
-
CONFIG_FILE =
|
|
42
|
+
CONFIG_DIR2 = join4(homedir4(), ".config", "apes");
|
|
43
|
+
AUTH_FILE = join4(CONFIG_DIR2, "auth.json");
|
|
44
|
+
CONFIG_FILE = join4(CONFIG_DIR2, "config.toml");
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -117,9 +117,9 @@ function requirePicocolors() {
|
|
|
117
117
|
hasRequiredPicocolors = 1;
|
|
118
118
|
let p = process || {}, argv2 = p.argv || [], env2 = p.env || {};
|
|
119
119
|
let isColorSupported2 = !(!!env2.NO_COLOR || argv2.includes("--no-color")) && (!!env2.FORCE_COLOR || argv2.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env2.TERM !== "dumb" || !!env2.CI);
|
|
120
|
-
let formatter = (
|
|
121
|
-
let string = "" + input, index = string.indexOf(close,
|
|
122
|
-
return ~index ?
|
|
120
|
+
let formatter = (open2, close, replace = open2) => (input) => {
|
|
121
|
+
let string = "" + input, index = string.indexOf(close, open2.length);
|
|
122
|
+
return ~index ? open2 + replaceClose2(string, close, replace, index) + close : open2 + string + close;
|
|
123
123
|
};
|
|
124
124
|
let replaceClose2 = (string, close, replace, index) => {
|
|
125
125
|
let result = "", cursor = 0;
|
|
@@ -1074,9 +1074,9 @@ var require_shell_quote = __commonJS({
|
|
|
1074
1074
|
});
|
|
1075
1075
|
|
|
1076
1076
|
// src/bridge.ts
|
|
1077
|
-
import { existsSync as
|
|
1078
|
-
import { homedir as
|
|
1079
|
-
import { join as
|
|
1077
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
1078
|
+
import { homedir as homedir10 } from "os";
|
|
1079
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
1080
1080
|
import process3 from "process";
|
|
1081
1081
|
|
|
1082
1082
|
// ../../packages/cli-auth/dist/index.js
|
|
@@ -1095,22 +1095,33 @@ import { Buffer as Buffer3 } from "buffer";
|
|
|
1095
1095
|
import { createPrivateKey } from "crypto";
|
|
1096
1096
|
import { ofetch as ofetch4 } from "ofetch";
|
|
1097
1097
|
import { ofetch as ofetch5 } from "ofetch";
|
|
1098
|
-
function getConfigDir() {
|
|
1098
|
+
function getConfigDir(authHome) {
|
|
1099
|
+
if (authHome) return join(authHome, ".config", "apes");
|
|
1099
1100
|
const override = process.env.OPENAPE_CLI_AUTH_HOME;
|
|
1100
1101
|
if (override) return override;
|
|
1101
1102
|
return join(homedir(), ".config", "apes");
|
|
1102
1103
|
}
|
|
1103
|
-
function getAuthFile() {
|
|
1104
|
-
return join(getConfigDir(), "auth.json");
|
|
1104
|
+
function getAuthFile(authHome) {
|
|
1105
|
+
return join(getConfigDir(authHome), "auth.json");
|
|
1106
|
+
}
|
|
1107
|
+
function getSpTokensDir() {
|
|
1108
|
+
return join(getConfigDir(), "sp-tokens");
|
|
1109
|
+
}
|
|
1110
|
+
function ensureConfigDir(authHome) {
|
|
1111
|
+
const dir = getConfigDir(authHome);
|
|
1112
|
+
if (!existsSync(dir)) {
|
|
1113
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1114
|
+
}
|
|
1105
1115
|
}
|
|
1106
|
-
function
|
|
1107
|
-
|
|
1116
|
+
function ensureSpTokensDir() {
|
|
1117
|
+
ensureConfigDir();
|
|
1118
|
+
const dir = getSpTokensDir();
|
|
1108
1119
|
if (!existsSync(dir)) {
|
|
1109
1120
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1110
1121
|
}
|
|
1111
1122
|
}
|
|
1112
|
-
function loadIdpAuth() {
|
|
1113
|
-
const file = getAuthFile();
|
|
1123
|
+
function loadIdpAuth(authHome) {
|
|
1124
|
+
const file = getAuthFile(authHome);
|
|
1114
1125
|
if (!existsSync(file)) return null;
|
|
1115
1126
|
try {
|
|
1116
1127
|
const raw = readFileSync(file, "utf-8");
|
|
@@ -1120,9 +1131,9 @@ function loadIdpAuth() {
|
|
|
1120
1131
|
return null;
|
|
1121
1132
|
}
|
|
1122
1133
|
}
|
|
1123
|
-
function saveIdpAuth(auth) {
|
|
1124
|
-
ensureConfigDir();
|
|
1125
|
-
const file = getAuthFile();
|
|
1134
|
+
function saveIdpAuth(auth, authHome) {
|
|
1135
|
+
ensureConfigDir(authHome);
|
|
1136
|
+
const file = getAuthFile(authHome);
|
|
1126
1137
|
let extra = {};
|
|
1127
1138
|
if (existsSync(file)) {
|
|
1128
1139
|
try {
|
|
@@ -1142,6 +1153,27 @@ function saveIdpAuth(auth) {
|
|
|
1142
1153
|
const merged = { ...extra, ...auth };
|
|
1143
1154
|
writeFileSync(file, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
1144
1155
|
}
|
|
1156
|
+
function audToFilename(aud) {
|
|
1157
|
+
return aud.replace(/[^\w.-]/g, "_");
|
|
1158
|
+
}
|
|
1159
|
+
function spTokenPath(aud) {
|
|
1160
|
+
return join(getSpTokensDir(), `${audToFilename(aud)}.json`);
|
|
1161
|
+
}
|
|
1162
|
+
function loadSpToken(aud) {
|
|
1163
|
+
const path = spTokenPath(aud);
|
|
1164
|
+
if (!existsSync(path)) return null;
|
|
1165
|
+
try {
|
|
1166
|
+
const raw = readFileSync(path, "utf-8");
|
|
1167
|
+
if (!raw.trim()) return null;
|
|
1168
|
+
return JSON.parse(raw);
|
|
1169
|
+
} catch {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function saveSpToken(token) {
|
|
1174
|
+
ensureSpTokensDir();
|
|
1175
|
+
writeFileSync(spTokenPath(token.aud), JSON.stringify(token, null, 2), { mode: 384 });
|
|
1176
|
+
}
|
|
1145
1177
|
var AuthError = class extends Error {
|
|
1146
1178
|
status;
|
|
1147
1179
|
hint;
|
|
@@ -1163,6 +1195,39 @@ var NotLoggedInError = class extends AuthError {
|
|
|
1163
1195
|
this.name = "NotLoggedInError";
|
|
1164
1196
|
}
|
|
1165
1197
|
};
|
|
1198
|
+
async function exchangeForSpToken(idpAuth, request, now = Math.floor(Date.now() / 1e3)) {
|
|
1199
|
+
const url = `${request.endpoint.replace(/\/$/, "")}/api/cli/exchange`;
|
|
1200
|
+
let response;
|
|
1201
|
+
try {
|
|
1202
|
+
response = await ofetch(url, {
|
|
1203
|
+
method: "POST",
|
|
1204
|
+
body: {
|
|
1205
|
+
subject_token: idpAuth.access_token,
|
|
1206
|
+
...request.scopes ? { scopes: request.scopes } : {}
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
const status = err.status ?? err.statusCode ?? 0;
|
|
1211
|
+
const data = err.data;
|
|
1212
|
+
const title = data?.title ?? `Token exchange failed (HTTP ${status})`;
|
|
1213
|
+
const hint = status === 401 ? `IdP token rejected at ${url}. Try \`apes login\` again \u2014 token may be expired or audience-mismatched.` : data?.detail;
|
|
1214
|
+
throw new AuthError(status, title, hint);
|
|
1215
|
+
}
|
|
1216
|
+
if (!response.access_token) {
|
|
1217
|
+
throw new AuthError(0, `Exchange response from ${url} missing access_token`);
|
|
1218
|
+
}
|
|
1219
|
+
const expiresAt = response.expires_at ?? (response.expires_in ? now + response.expires_in : now + 3600);
|
|
1220
|
+
const token = {
|
|
1221
|
+
endpoint: request.endpoint,
|
|
1222
|
+
aud: response.aud ?? request.aud,
|
|
1223
|
+
access_token: response.access_token,
|
|
1224
|
+
expires_at: expiresAt,
|
|
1225
|
+
...request.scopes ? { scopes: request.scopes } : {},
|
|
1226
|
+
issued_from_idp_iat: now
|
|
1227
|
+
};
|
|
1228
|
+
saveSpToken(token);
|
|
1229
|
+
return token;
|
|
1230
|
+
}
|
|
1166
1231
|
var OPENSSH_MAGIC = "openssh-key-v1\0";
|
|
1167
1232
|
function loadEd25519PrivateKey(pem) {
|
|
1168
1233
|
if (pem.includes("BEGIN OPENSSH PRIVATE KEY")) {
|
|
@@ -1318,8 +1383,8 @@ async function getTokenEndpoint(idp) {
|
|
|
1318
1383
|
}
|
|
1319
1384
|
return `${idp}/token`;
|
|
1320
1385
|
}
|
|
1321
|
-
async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
1322
|
-
const auth = loadIdpAuth();
|
|
1386
|
+
async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3), authHome) {
|
|
1387
|
+
const auth = loadIdpAuth(authHome);
|
|
1323
1388
|
if (!auth) {
|
|
1324
1389
|
throw new NotLoggedInError();
|
|
1325
1390
|
}
|
|
@@ -1329,7 +1394,7 @@ async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
|
1329
1394
|
if (!auth.refresh_token) {
|
|
1330
1395
|
const refreshed = await refreshAgentToken(auth, now);
|
|
1331
1396
|
if (refreshed) {
|
|
1332
|
-
saveIdpAuth(refreshed);
|
|
1397
|
+
saveIdpAuth(refreshed, authHome);
|
|
1333
1398
|
return refreshed;
|
|
1334
1399
|
}
|
|
1335
1400
|
throw new NotLoggedInError(
|
|
@@ -1351,7 +1416,7 @@ async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
|
1351
1416
|
} catch (err) {
|
|
1352
1417
|
const status = err.status ?? err.statusCode ?? 0;
|
|
1353
1418
|
if (status === 400 || status === 401) {
|
|
1354
|
-
saveIdpAuth({ ...auth, refresh_token: void 0 });
|
|
1419
|
+
saveIdpAuth({ ...auth, refresh_token: void 0 }, authHome);
|
|
1355
1420
|
throw new NotLoggedInError(
|
|
1356
1421
|
`Refresh token rejected by ${auth.idp}. Run \`apes login\` again.`
|
|
1357
1422
|
);
|
|
@@ -1371,9 +1436,26 @@ async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
|
1371
1436
|
refresh_token: response.refresh_token ?? auth.refresh_token,
|
|
1372
1437
|
expires_at: now + (response.expires_in ?? 3600)
|
|
1373
1438
|
};
|
|
1374
|
-
saveIdpAuth(next);
|
|
1439
|
+
saveIdpAuth(next, authHome);
|
|
1375
1440
|
return next;
|
|
1376
1441
|
}
|
|
1442
|
+
var SP_TOKEN_SKEW_SECONDS = 60;
|
|
1443
|
+
async function getAuthorizedBearer(opts) {
|
|
1444
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1445
|
+
if (!opts.forceRefresh) {
|
|
1446
|
+
const cached = loadSpToken(opts.aud);
|
|
1447
|
+
if (cached && cached.expires_at > now + SP_TOKEN_SKEW_SECONDS) {
|
|
1448
|
+
return `Bearer ${cached.access_token}`;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
const idpAuth = await ensureFreshIdpAuth(now);
|
|
1452
|
+
const sp = await exchangeForSpToken(idpAuth, {
|
|
1453
|
+
endpoint: opts.endpoint,
|
|
1454
|
+
aud: opts.aud,
|
|
1455
|
+
...opts.scopes ? { scopes: opts.scopes } : {}
|
|
1456
|
+
}, now);
|
|
1457
|
+
return `Bearer ${sp.access_token}`;
|
|
1458
|
+
}
|
|
1377
1459
|
|
|
1378
1460
|
// ../../packages/prompt-injection-detector/dist/index.js
|
|
1379
1461
|
var DEFAULT_THRESHOLD = 0.7;
|
|
@@ -1439,152 +1521,69 @@ function createHeuristicDetector() {
|
|
|
1439
1521
|
import { decodeJwt } from "jose";
|
|
1440
1522
|
import WebSocket from "ws";
|
|
1441
1523
|
|
|
1442
|
-
//
|
|
1443
|
-
import {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
function asHistory(msg, agentEmail, ownerEmail) {
|
|
1447
|
-
return {
|
|
1448
|
-
id: msg.id,
|
|
1449
|
-
roomId: msg.chatId,
|
|
1450
|
-
threadId: SYNTHETIC_THREAD_ID,
|
|
1451
|
-
senderEmail: msg.role === "agent" ? agentEmail : ownerEmail,
|
|
1452
|
-
senderAct: msg.role,
|
|
1453
|
-
body: msg.body,
|
|
1454
|
-
replyTo: msg.replyTo,
|
|
1455
|
-
createdAt: msg.createdAt
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
function asPosted(msg) {
|
|
1459
|
-
return {
|
|
1460
|
-
id: msg.id,
|
|
1461
|
-
roomId: msg.chatId,
|
|
1462
|
-
threadId: SYNTHETIC_THREAD_ID,
|
|
1463
|
-
body: msg.body,
|
|
1464
|
-
createdAt: msg.createdAt
|
|
1465
|
-
};
|
|
1466
|
-
}
|
|
1467
|
-
var TroopChatApi = class {
|
|
1468
|
-
constructor(endpoint, bearer) {
|
|
1469
|
-
this.endpoint = endpoint;
|
|
1470
|
-
this.bearer = bearer;
|
|
1471
|
-
}
|
|
1472
|
-
endpoint;
|
|
1473
|
-
bearer;
|
|
1474
|
-
bootstrap = null;
|
|
1475
|
-
/** Resolve + cache the agent's chat row (lazy fetch on first use). */
|
|
1476
|
-
async getBootstrap() {
|
|
1477
|
-
if (this.bootstrap) return this.bootstrap;
|
|
1478
|
-
this.bootstrap = await ofetch6(`${this.endpoint}/api/agents/me/chat`, {
|
|
1479
|
-
method: "GET",
|
|
1480
|
-
headers: { Authorization: await this.bearer() }
|
|
1481
|
-
});
|
|
1482
|
-
return this.bootstrap;
|
|
1483
|
-
}
|
|
1484
|
-
/** chat.id + (lazy-fetched) ownerEmail for the bridge's frame-translation path. */
|
|
1485
|
-
async getChatContext() {
|
|
1486
|
-
const b2 = await this.getBootstrap();
|
|
1487
|
-
return { chatId: b2.chat.id, ownerEmail: b2.chat.ownerEmail, agentEmail: b2.chat.agentEmail };
|
|
1488
|
-
}
|
|
1489
|
-
async postMessage(roomId, body, opts = {}) {
|
|
1490
|
-
void roomId;
|
|
1491
|
-
void opts.threadId;
|
|
1492
|
-
const payload = {
|
|
1493
|
-
body: body.length > MAX_BODY ? `${body.slice(0, MAX_BODY - 1)}\u2026` : body
|
|
1494
|
-
};
|
|
1495
|
-
if (opts.replyTo) payload.reply_to = opts.replyTo;
|
|
1496
|
-
if (opts.streaming) payload.streaming = true;
|
|
1497
|
-
const msg = await ofetch6(`${this.endpoint}/api/agents/me/chat/messages`, {
|
|
1498
|
-
method: "POST",
|
|
1499
|
-
headers: { Authorization: await this.bearer() },
|
|
1500
|
-
body: payload
|
|
1501
|
-
});
|
|
1502
|
-
return asPosted(msg);
|
|
1503
|
-
}
|
|
1504
|
-
async listMessages(roomId, threadId, limit = 50) {
|
|
1505
|
-
void roomId;
|
|
1506
|
-
void threadId;
|
|
1507
|
-
void limit;
|
|
1508
|
-
const fresh = await ofetch6(`${this.endpoint}/api/agents/me/chat`, {
|
|
1509
|
-
method: "GET",
|
|
1510
|
-
headers: { Authorization: await this.bearer() }
|
|
1511
|
-
});
|
|
1512
|
-
this.bootstrap = fresh;
|
|
1513
|
-
return fresh.messages.map((m2) => asHistory(m2, fresh.chat.agentEmail, fresh.chat.ownerEmail));
|
|
1514
|
-
}
|
|
1515
|
-
async patchMessage(messageId, opts = {}) {
|
|
1516
|
-
const payload = {};
|
|
1517
|
-
if (opts.body !== void 0) {
|
|
1518
|
-
payload.body = opts.body.length > MAX_BODY ? `${opts.body.slice(0, MAX_BODY - 1)}\u2026` : opts.body;
|
|
1519
|
-
}
|
|
1520
|
-
if (opts.streaming !== void 0) payload.streaming = opts.streaming;
|
|
1521
|
-
if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
|
|
1522
|
-
if (Object.keys(payload).length === 0) return;
|
|
1523
|
-
await ofetch6(`${this.endpoint}/api/agents/me/chat/messages/${encodeURIComponent(messageId)}`, {
|
|
1524
|
-
method: "PATCH",
|
|
1525
|
-
headers: { Authorization: await this.bearer() },
|
|
1526
|
-
body: payload
|
|
1527
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
/**
|
|
1530
|
-
* Troop's chat doesn't have contacts — synthesize a single
|
|
1531
|
-
* always-connected entry pointing at the owner so the bridge's
|
|
1532
|
-
* initial-contact + allowlist flows are no-ops.
|
|
1533
|
-
*/
|
|
1534
|
-
async listContacts() {
|
|
1535
|
-
const b2 = await this.getBootstrap();
|
|
1536
|
-
return [{
|
|
1537
|
-
peerEmail: b2.chat.ownerEmail,
|
|
1538
|
-
myStatus: "accepted",
|
|
1539
|
-
theirStatus: "accepted",
|
|
1540
|
-
connected: true,
|
|
1541
|
-
roomId: b2.chat.id
|
|
1542
|
-
}];
|
|
1543
|
-
}
|
|
1544
|
-
async requestContact(peerEmail) {
|
|
1545
|
-
void peerEmail;
|
|
1546
|
-
return (await this.listContacts())[0];
|
|
1547
|
-
}
|
|
1548
|
-
async acceptContact(peerEmail) {
|
|
1549
|
-
void peerEmail;
|
|
1550
|
-
return (await this.listContacts())[0];
|
|
1551
|
-
}
|
|
1552
|
-
/**
|
|
1553
|
-
* Troop has no threads — return a synthetic one. The bridge's
|
|
1554
|
-
* cron-runner falls back to the main thread on createThread
|
|
1555
|
-
* failure already, so a stable "main" stand-in is the right shape.
|
|
1556
|
-
*/
|
|
1557
|
-
async createThread(roomId, name) {
|
|
1558
|
-
void roomId;
|
|
1559
|
-
return { id: SYNTHETIC_THREAD_ID, name: name.slice(0, 100) };
|
|
1560
|
-
}
|
|
1561
|
-
};
|
|
1562
|
-
|
|
1563
|
-
// src/cron-runner.ts
|
|
1564
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1565
|
-
import { homedir as homedir6 } from "os";
|
|
1566
|
-
import { join as join4 } from "path";
|
|
1567
|
-
|
|
1568
|
-
// ../../packages/apes/dist/chunk-BA2V3BBO.js
|
|
1569
|
-
init_chunk_OBF7IMQ2();
|
|
1570
|
-
|
|
1571
|
-
// ../../node_modules/.pnpm/citty@0.2.2/node_modules/citty/dist/index.mjs
|
|
1572
|
-
import { parseArgs as parseArgs$1 } from "util";
|
|
1573
|
-
function defineCommand(def) {
|
|
1574
|
-
return def;
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// ../../packages/shapes/dist/index.js
|
|
1578
|
-
import { createHash } from "crypto";
|
|
1579
|
-
import { existsSync as existsSync22, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
1580
|
-
import { homedir as homedir22 } from "os";
|
|
1581
|
-
import { basename, join as join22 } from "path";
|
|
1524
|
+
// ../../packages/apes/dist/chunk-3LH4FT4R.js
|
|
1525
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync, watch, writeFileSync as writeFileSync2 } from "fs";
|
|
1526
|
+
import { homedir as homedir3 } from "os";
|
|
1527
|
+
import { dirname, join as join3 } from "path";
|
|
1582
1528
|
|
|
1583
1529
|
// ../../packages/core/dist/index.js
|
|
1584
1530
|
import * as jose from "jose";
|
|
1531
|
+
import {
|
|
1532
|
+
createCipheriv,
|
|
1533
|
+
createDecipheriv,
|
|
1534
|
+
createPrivateKey as createPrivateKey2,
|
|
1535
|
+
createPublicKey,
|
|
1536
|
+
diffieHellman,
|
|
1537
|
+
generateKeyPairSync,
|
|
1538
|
+
hkdfSync,
|
|
1539
|
+
randomBytes
|
|
1540
|
+
} from "crypto";
|
|
1585
1541
|
import { lookup } from "dns/promises";
|
|
1586
1542
|
import { isIP } from "net";
|
|
1587
1543
|
var HKDF_INFO = new TextEncoder().encode("openape-sealed-box-v1");
|
|
1544
|
+
var IV_LEN = 12;
|
|
1545
|
+
var TAG_LEN = 16;
|
|
1546
|
+
var RAW_KEY_LEN = 32;
|
|
1547
|
+
function b64u(data) {
|
|
1548
|
+
return Buffer.from(data).toString("base64url");
|
|
1549
|
+
}
|
|
1550
|
+
function unb64u(s2) {
|
|
1551
|
+
return Buffer.from(s2, "base64url");
|
|
1552
|
+
}
|
|
1553
|
+
function rawPub(key) {
|
|
1554
|
+
const pub = key.type === "private" ? createPublicKey(key) : key;
|
|
1555
|
+
const jwk = pub.export({ format: "jwk" });
|
|
1556
|
+
return unb64u(jwk.x);
|
|
1557
|
+
}
|
|
1558
|
+
function ephPublicFromRaw(raw) {
|
|
1559
|
+
return createPublicKey({
|
|
1560
|
+
key: { kty: "OKP", crv: "X25519", x: b64u(raw) },
|
|
1561
|
+
format: "jwk"
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
function deriveKey(shared, ephPubRaw, recipPubRaw) {
|
|
1565
|
+
const salt = Buffer.concat([ephPubRaw, recipPubRaw]);
|
|
1566
|
+
return Buffer.from(hkdfSync("sha256", shared, salt, HKDF_INFO, 32));
|
|
1567
|
+
}
|
|
1568
|
+
function open(box2, recipientPrivateKey) {
|
|
1569
|
+
if (box2.v !== 1) throw new Error(`unsupported sealed-box version: ${box2.v}`);
|
|
1570
|
+
const epkRaw = unb64u(box2.epk);
|
|
1571
|
+
if (epkRaw.length !== RAW_KEY_LEN) throw new Error("invalid ephemeral public key length");
|
|
1572
|
+
const iv = unb64u(box2.iv);
|
|
1573
|
+
if (iv.length !== IV_LEN) throw new Error("invalid IV length");
|
|
1574
|
+
const tag = unb64u(box2.tag);
|
|
1575
|
+
if (tag.length !== TAG_LEN) throw new Error("invalid auth tag length");
|
|
1576
|
+
const recipPriv = createPrivateKey2({ key: unb64u(recipientPrivateKey), format: "der", type: "pkcs8" });
|
|
1577
|
+
const ephPub = ephPublicFromRaw(epkRaw);
|
|
1578
|
+
const shared = diffieHellman({ privateKey: recipPriv, publicKey: ephPub });
|
|
1579
|
+
const key = deriveKey(shared, epkRaw, rawPub(recipPriv));
|
|
1580
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
1581
|
+
decipher.setAuthTag(tag);
|
|
1582
|
+
return Buffer.concat([decipher.update(unb64u(box2.ct)), decipher.final()]);
|
|
1583
|
+
}
|
|
1584
|
+
function openString(box2, recipientPrivateKey) {
|
|
1585
|
+
return Buffer.from(open(box2, recipientPrivateKey)).toString("utf8");
|
|
1586
|
+
}
|
|
1588
1587
|
function isBlockedAddress(ip) {
|
|
1589
1588
|
const fam = isIP(ip);
|
|
1590
1589
|
if (fam === 4) {
|
|
@@ -1639,6 +1638,100 @@ async function assertPublicUrl(rawUrl, opts = {}) {
|
|
|
1639
1638
|
return url;
|
|
1640
1639
|
}
|
|
1641
1640
|
|
|
1641
|
+
// ../../packages/apes/dist/chunk-3LH4FT4R.js
|
|
1642
|
+
var CONFIG_DIR = join3(homedir3(), ".config", "openape");
|
|
1643
|
+
var SECRETS_DIR = join3(CONFIG_DIR, "secrets.d");
|
|
1644
|
+
var X25519_KEY_PATH = join3(CONFIG_DIR, "agent-x25519.key");
|
|
1645
|
+
var X25519_PUBKEY_PATH = `${X25519_KEY_PATH}.pub`;
|
|
1646
|
+
function envNameFromFile(file) {
|
|
1647
|
+
if (!file.endsWith(".blob")) return null;
|
|
1648
|
+
const env2 = file.slice(0, -".blob".length);
|
|
1649
|
+
return /^[A-Z][A-Z0-9_]*$/.test(env2) ? env2 : null;
|
|
1650
|
+
}
|
|
1651
|
+
function readAgentEncryptionKey(keyPath = X25519_KEY_PATH) {
|
|
1652
|
+
if (!existsSync3(keyPath)) return null;
|
|
1653
|
+
const k2 = readFileSync3(keyPath, "utf8").trim();
|
|
1654
|
+
return k2.length > 0 ? k2 : null;
|
|
1655
|
+
}
|
|
1656
|
+
function materializeSecrets(opts = {}) {
|
|
1657
|
+
const dir = opts.dir ?? SECRETS_DIR;
|
|
1658
|
+
const env2 = opts.env ?? process.env;
|
|
1659
|
+
const log2 = opts.log ?? (() => {
|
|
1660
|
+
});
|
|
1661
|
+
const applied = [];
|
|
1662
|
+
const failed = [];
|
|
1663
|
+
const key = readAgentEncryptionKey(opts.keyPath);
|
|
1664
|
+
const files = key && existsSync3(dir) ? readdirSync2(dir) : [];
|
|
1665
|
+
for (const file of files) {
|
|
1666
|
+
const name = envNameFromFile(file);
|
|
1667
|
+
if (!name) continue;
|
|
1668
|
+
try {
|
|
1669
|
+
const box2 = JSON.parse(readFileSync3(join3(dir, file), "utf8"));
|
|
1670
|
+
const plaintext = openString(box2, key);
|
|
1671
|
+
const target = typeof box2.materializeTo === "string" ? box2.materializeTo : null;
|
|
1672
|
+
if (target) {
|
|
1673
|
+
const blobMtime = statSync(join3(dir, file)).mtimeMs;
|
|
1674
|
+
if (!existsSync3(target) || statSync(target).mtimeMs < blobMtime) {
|
|
1675
|
+
mkdirSync2(dirname(target), { recursive: true });
|
|
1676
|
+
writeFileSync2(target, plaintext, { mode: 384 });
|
|
1677
|
+
}
|
|
1678
|
+
} else {
|
|
1679
|
+
env2[name] = plaintext;
|
|
1680
|
+
}
|
|
1681
|
+
applied.push(name);
|
|
1682
|
+
} catch (e2) {
|
|
1683
|
+
failed.push(file);
|
|
1684
|
+
log2(`secrets: failed to open ${file}: ${e2.message}`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
const live = new Set(applied);
|
|
1688
|
+
for (const prev of opts.previouslyApplied ?? []) {
|
|
1689
|
+
if (!live.has(prev)) {
|
|
1690
|
+
delete env2[prev];
|
|
1691
|
+
log2(`secrets: revoked ${prev}`);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
return { applied, failed };
|
|
1695
|
+
}
|
|
1696
|
+
function startSecretsWatcher(opts = {}) {
|
|
1697
|
+
const dir = opts.dir ?? SECRETS_DIR;
|
|
1698
|
+
const log2 = opts.log ?? (() => {
|
|
1699
|
+
});
|
|
1700
|
+
let appliedNames = /* @__PURE__ */ new Set();
|
|
1701
|
+
const run = () => {
|
|
1702
|
+
const r3 = materializeSecrets({ ...opts, previouslyApplied: appliedNames });
|
|
1703
|
+
appliedNames = new Set(r3.applied);
|
|
1704
|
+
};
|
|
1705
|
+
run();
|
|
1706
|
+
if (!existsSync3(dir)) return () => {
|
|
1707
|
+
};
|
|
1708
|
+
let timer = null;
|
|
1709
|
+
const watcher = watch(dir, () => {
|
|
1710
|
+
if (timer) clearTimeout(timer);
|
|
1711
|
+
timer = setTimeout(run, 150);
|
|
1712
|
+
});
|
|
1713
|
+
watcher.on("error", (err) => log2(`secrets: watcher error: ${err.message}`));
|
|
1714
|
+
return () => {
|
|
1715
|
+
if (timer) clearTimeout(timer);
|
|
1716
|
+
watcher.close();
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// ../../packages/apes/dist/chunk-BA2V3BBO.js
|
|
1721
|
+
init_chunk_OBF7IMQ2();
|
|
1722
|
+
|
|
1723
|
+
// ../../node_modules/.pnpm/citty@0.2.2/node_modules/citty/dist/index.mjs
|
|
1724
|
+
import { parseArgs as parseArgs$1 } from "util";
|
|
1725
|
+
function defineCommand(def) {
|
|
1726
|
+
return def;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// ../../packages/shapes/dist/index.js
|
|
1730
|
+
import { createHash } from "crypto";
|
|
1731
|
+
import { existsSync as existsSync22, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "fs";
|
|
1732
|
+
import { homedir as homedir22 } from "os";
|
|
1733
|
+
import { basename, join as join22 } from "path";
|
|
1734
|
+
|
|
1642
1735
|
// ../../packages/grants/dist/index.js
|
|
1643
1736
|
function normalizeSelector(selector) {
|
|
1644
1737
|
if (!selector)
|
|
@@ -2407,20 +2500,20 @@ var isColorSupported = !isDisabled && (isForced || isWindows && !isDumbTerminal
|
|
|
2407
2500
|
function replaceClose(index, string, close, replace, head = string.slice(0, Math.max(0, index)) + replace, tail = string.slice(Math.max(0, index + close.length)), next = tail.indexOf(close)) {
|
|
2408
2501
|
return head + (next < 0 ? tail : replaceClose(next, tail, close, replace));
|
|
2409
2502
|
}
|
|
2410
|
-
function clearBleed(index, string,
|
|
2411
|
-
return index < 0 ?
|
|
2503
|
+
function clearBleed(index, string, open2, close, replace) {
|
|
2504
|
+
return index < 0 ? open2 + string + close : open2 + replaceClose(index, string, close, replace) + close;
|
|
2412
2505
|
}
|
|
2413
|
-
function filterEmpty(
|
|
2506
|
+
function filterEmpty(open2, close, replace = open2, at = open2.length + 1) {
|
|
2414
2507
|
return (string) => string || !(string === "" || string === void 0) ? clearBleed(
|
|
2415
2508
|
("" + string).indexOf(close, at),
|
|
2416
2509
|
string,
|
|
2417
|
-
|
|
2510
|
+
open2,
|
|
2418
2511
|
close,
|
|
2419
2512
|
replace
|
|
2420
2513
|
) : "";
|
|
2421
2514
|
}
|
|
2422
|
-
function init(
|
|
2423
|
-
return filterEmpty(`\x1B[${
|
|
2515
|
+
function init(open2, close, replace) {
|
|
2516
|
+
return filterEmpty(`\x1B[${open2}m`, `\x1B[${close}m`, replace);
|
|
2424
2517
|
}
|
|
2425
2518
|
var colorDefs = {
|
|
2426
2519
|
reset: init(0, 0),
|
|
@@ -3040,10 +3133,10 @@ function findByExecutable(executable) {
|
|
|
3040
3133
|
if (!existsSync22(dir))
|
|
3041
3134
|
continue;
|
|
3042
3135
|
try {
|
|
3043
|
-
const files =
|
|
3136
|
+
const files = readdirSync3(dir).filter((f3) => f3.endsWith(".toml"));
|
|
3044
3137
|
for (const file of files) {
|
|
3045
3138
|
const path = join22(dir, file);
|
|
3046
|
-
const content =
|
|
3139
|
+
const content = readFileSync4(path, "utf-8");
|
|
3047
3140
|
const match = content.match(/^\s*executable\s*=\s*"([^"]+)"/m);
|
|
3048
3141
|
if (match && match[1] === executable)
|
|
3049
3142
|
return path;
|
|
@@ -3070,7 +3163,7 @@ function resolveAdapterPath(cliId, explicitPath) {
|
|
|
3070
3163
|
}
|
|
3071
3164
|
function loadAdapter(cliId, explicitPath) {
|
|
3072
3165
|
const source = resolveAdapterPath(cliId, explicitPath);
|
|
3073
|
-
const content =
|
|
3166
|
+
const content = readFileSync4(source, "utf-8");
|
|
3074
3167
|
const adapter = parseAdapterToml(content);
|
|
3075
3168
|
const idMatch = adapter.cli.id === cliId;
|
|
3076
3169
|
const fileMatch = basename(source) === `${cliId}.toml`;
|
|
@@ -3180,13 +3273,16 @@ init_chunk_OBF7IMQ2();
|
|
|
3180
3273
|
|
|
3181
3274
|
// ../../packages/agent-runtime/dist/index.js
|
|
3182
3275
|
import { spawn } from "child_process";
|
|
3183
|
-
import { mkdirSync as
|
|
3184
|
-
import { homedir as
|
|
3185
|
-
import { dirname, normalize, resolve } from "path";
|
|
3276
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
3277
|
+
import { homedir as homedir6 } from "os";
|
|
3278
|
+
import { dirname as dirname2, normalize, resolve } from "path";
|
|
3186
3279
|
import { homedir as homedir23 } from "os";
|
|
3187
3280
|
import { resolve as resolve2 } from "path";
|
|
3188
3281
|
import process2 from "process";
|
|
3189
3282
|
import { execFileSync } from "child_process";
|
|
3283
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
3284
|
+
import { homedir as homedir32 } from "os";
|
|
3285
|
+
import { join as join6 } from "path";
|
|
3190
3286
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3191
3287
|
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3192
3288
|
var MAX_STDIO_BYTES = 64 * 1024;
|
|
@@ -3280,21 +3376,31 @@ var bashTools = [
|
|
|
3280
3376
|
}
|
|
3281
3377
|
];
|
|
3282
3378
|
var MAX_BYTES = 1024 * 1024;
|
|
3283
|
-
|
|
3379
|
+
var extraReadRoots = /* @__PURE__ */ new Set();
|
|
3380
|
+
function addReadRoot(absPath) {
|
|
3381
|
+
if (typeof absPath === "string" && absPath.startsWith("/")) extraReadRoots.add(normalize(absPath));
|
|
3382
|
+
}
|
|
3383
|
+
function isUnder(candidate, root) {
|
|
3384
|
+
return candidate === root || candidate.startsWith(`${root}/`);
|
|
3385
|
+
}
|
|
3386
|
+
function jailPath(input, opts = {}) {
|
|
3284
3387
|
if (typeof input !== "string" || input === "") {
|
|
3285
3388
|
throw new Error("path must be a non-empty string");
|
|
3286
3389
|
}
|
|
3287
|
-
const home =
|
|
3390
|
+
const home = homedir6();
|
|
3288
3391
|
const candidate = input.startsWith("~/") ? resolve(home, input.slice(2)) : input.startsWith("/") ? normalize(input) : resolve(home, input);
|
|
3289
|
-
if (candidate
|
|
3290
|
-
|
|
3392
|
+
if (isUnder(candidate, home)) return candidate;
|
|
3393
|
+
if (opts.allowReadRoots) {
|
|
3394
|
+
for (const root of extraReadRoots) {
|
|
3395
|
+
if (isUnder(candidate, root)) return candidate;
|
|
3396
|
+
}
|
|
3291
3397
|
}
|
|
3292
|
-
|
|
3398
|
+
throw new Error(`path "${input}" resolves outside the agent's home`);
|
|
3293
3399
|
}
|
|
3294
3400
|
var fileTools = [
|
|
3295
3401
|
{
|
|
3296
3402
|
name: "file.read",
|
|
3297
|
-
description: "Read a UTF-8 file from the agent's home directory ($HOME). Capped at 1MB. Path traversal blocked.",
|
|
3403
|
+
description: "Read a UTF-8 file from the agent's home directory ($HOME) or a bundled skill directory (e.g. a skill's SKILL.md). Capped at 1MB. Path traversal blocked.",
|
|
3298
3404
|
parameters: {
|
|
3299
3405
|
type: "object",
|
|
3300
3406
|
properties: {
|
|
@@ -3304,8 +3410,8 @@ var fileTools = [
|
|
|
3304
3410
|
},
|
|
3305
3411
|
execute: async (args) => {
|
|
3306
3412
|
const a2 = args;
|
|
3307
|
-
const p = jailPath(a2.path);
|
|
3308
|
-
const content =
|
|
3413
|
+
const p = jailPath(a2.path, { allowReadRoots: true });
|
|
3414
|
+
const content = readFileSync5(p, "utf8");
|
|
3309
3415
|
if (Buffer.byteLength(content, "utf8") > MAX_BYTES) {
|
|
3310
3416
|
return { path: p, truncated: true, content: content.slice(0, MAX_BYTES) };
|
|
3311
3417
|
}
|
|
@@ -3330,8 +3436,8 @@ var fileTools = [
|
|
|
3330
3436
|
throw new Error(`content exceeds ${MAX_BYTES} byte cap`);
|
|
3331
3437
|
}
|
|
3332
3438
|
const p = jailPath(a2.path);
|
|
3333
|
-
|
|
3334
|
-
|
|
3439
|
+
mkdirSync3(dirname2(p), { recursive: true });
|
|
3440
|
+
writeFileSync3(p, a2.content, { encoding: "utf8" });
|
|
3335
3441
|
return { path: p, bytes: Buffer.byteLength(a2.content, "utf8") };
|
|
3336
3442
|
}
|
|
3337
3443
|
},
|
|
@@ -3361,7 +3467,7 @@ var fileTools = [
|
|
|
3361
3467
|
}
|
|
3362
3468
|
const replaceAll = a2.replace_all === true;
|
|
3363
3469
|
const p = jailPath(a2.path);
|
|
3364
|
-
const before =
|
|
3470
|
+
const before = readFileSync5(p, "utf8");
|
|
3365
3471
|
const occurrences = before.split(a2.old_string).length - 1;
|
|
3366
3472
|
if (occurrences === 0) {
|
|
3367
3473
|
throw new Error("old_string not found in file");
|
|
@@ -3373,7 +3479,7 @@ var fileTools = [
|
|
|
3373
3479
|
if (Buffer.byteLength(after, "utf8") > MAX_BYTES) {
|
|
3374
3480
|
throw new Error(`result exceeds ${MAX_BYTES} byte cap`);
|
|
3375
3481
|
}
|
|
3376
|
-
|
|
3482
|
+
writeFileSync3(p, after, { encoding: "utf8" });
|
|
3377
3483
|
return { path: p, replacements: replaceAll ? occurrences : 1 };
|
|
3378
3484
|
}
|
|
3379
3485
|
}
|
|
@@ -3846,6 +3952,99 @@ var mailTools = [
|
|
|
3846
3952
|
}
|
|
3847
3953
|
}
|
|
3848
3954
|
];
|
|
3955
|
+
function troopBase() {
|
|
3956
|
+
return (process.env.OPENAPE_TROOP_URL ?? "https://troop.openape.ai").replace(/\/+$/, "");
|
|
3957
|
+
}
|
|
3958
|
+
function readAgentToken() {
|
|
3959
|
+
const path = process.env.OPENAPE_CLI_AUTH_HOME ? join6(process.env.OPENAPE_CLI_AUTH_HOME, "auth.json") : join6(homedir32(), ".config", "apes", "auth.json");
|
|
3960
|
+
const auth = JSON.parse(readFileSync22(path, "utf8"));
|
|
3961
|
+
if (!auth.access_token) throw new Error(`no access_token in ${path}`);
|
|
3962
|
+
return auth.access_token;
|
|
3963
|
+
}
|
|
3964
|
+
async function exchangeBearer(base, scope) {
|
|
3965
|
+
const res = await fetch(`${base}/api/cli/exchange`, {
|
|
3966
|
+
method: "POST",
|
|
3967
|
+
headers: { "content-type": "application/json" },
|
|
3968
|
+
body: JSON.stringify({ subject_token: readAgentToken(), scopes: [scope] })
|
|
3969
|
+
});
|
|
3970
|
+
if (!res.ok) {
|
|
3971
|
+
throw new Error(`token exchange ${res.status} \u2014 ${(await res.text().catch(() => "")).slice(0, 200)} (does this agent hold the ${scope} scope?)`);
|
|
3972
|
+
}
|
|
3973
|
+
return (await res.json()).access_token;
|
|
3974
|
+
}
|
|
3975
|
+
var spawnTools = [
|
|
3976
|
+
{
|
|
3977
|
+
name: "agent.spawn",
|
|
3978
|
+
description: "Spawn a worker agent on the nest via troop, tiering its compute by task difficulty: pick `model` (gpt-5.4-mini | gpt-5.4 | gpt-5.5) and `reasoning_effort` (minimal | low | medium | high) \u2014 quick-win = cheap+low, research/architecture = gpt-5.5+high. Optionally attach a `recipe_ref` so the worker runs a known persona. Returns the spawn intent id. Use multiple calls to fan out several workers in parallel.",
|
|
3979
|
+
parameters: {
|
|
3980
|
+
type: "object",
|
|
3981
|
+
properties: {
|
|
3982
|
+
name: { type: "string", description: "unique worker name, /^[a-z][a-z0-9-]{0,23}$/" },
|
|
3983
|
+
model: { type: "string", description: "gpt-5.4-mini | gpt-5.4 | gpt-5.5" },
|
|
3984
|
+
reasoning_effort: { type: "string", description: "minimal | low | medium | high" },
|
|
3985
|
+
recipe_ref: { type: "string", description: "optional recipe, e.g. github.com/openape-ai/agent-catalog/backend-engineer@v0.2.0" },
|
|
3986
|
+
system_prompt: { type: "string", description: "optional system prompt / task brief" }
|
|
3987
|
+
},
|
|
3988
|
+
required: ["name"]
|
|
3989
|
+
},
|
|
3990
|
+
execute: async (args) => {
|
|
3991
|
+
const a2 = args;
|
|
3992
|
+
const base = troopBase();
|
|
3993
|
+
let bearer;
|
|
3994
|
+
try {
|
|
3995
|
+
bearer = await exchangeBearer(base, "troop:spawn-agent");
|
|
3996
|
+
} catch (err) {
|
|
3997
|
+
return `spawn failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
3998
|
+
}
|
|
3999
|
+
const body = { name: a2.name };
|
|
4000
|
+
if (a2.model) body.bridge_model = a2.model;
|
|
4001
|
+
if (a2.reasoning_effort) body.bridge_reasoning_effort = a2.reasoning_effort;
|
|
4002
|
+
if (a2.system_prompt) body.system_prompt = a2.system_prompt;
|
|
4003
|
+
if (a2.recipe_ref) body.recipe = { repo_ref: a2.recipe_ref, params: {} };
|
|
4004
|
+
const spRes = await fetch(`${base}/api/agents/spawn-intent`, {
|
|
4005
|
+
method: "POST",
|
|
4006
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${bearer}` },
|
|
4007
|
+
body: JSON.stringify(body)
|
|
4008
|
+
});
|
|
4009
|
+
if (!spRes.ok) {
|
|
4010
|
+
return `spawn failed: spawn-intent ${spRes.status} \u2014 ${(await spRes.text().catch(() => "")).slice(0, 200)}`;
|
|
4011
|
+
}
|
|
4012
|
+
const sp = await spRes.json();
|
|
4013
|
+
return `spawned worker "${a2.name}" (model=${a2.model ?? "default"}, reasoning=${a2.reasoning_effort ?? "default"}); intent=${sp.intent_id ?? "?"}`;
|
|
4014
|
+
}
|
|
4015
|
+
},
|
|
4016
|
+
{
|
|
4017
|
+
name: "agent.destroy",
|
|
4018
|
+
description: "Destroy a worker agent on the nest (full teardown: OS user, IdP, bridge). The PM calls this after collecting an ephemeral worker's result, so workers do not linger idle. Requires the troop:destroy-agent scope.",
|
|
4019
|
+
parameters: {
|
|
4020
|
+
type: "object",
|
|
4021
|
+
properties: {
|
|
4022
|
+
name: { type: "string", description: "the worker agent name to destroy" }
|
|
4023
|
+
},
|
|
4024
|
+
required: ["name"]
|
|
4025
|
+
},
|
|
4026
|
+
execute: async (args) => {
|
|
4027
|
+
const a2 = args;
|
|
4028
|
+
const base = troopBase();
|
|
4029
|
+
let bearer;
|
|
4030
|
+
try {
|
|
4031
|
+
bearer = await exchangeBearer(base, "troop:destroy-agent");
|
|
4032
|
+
} catch (err) {
|
|
4033
|
+
return `destroy failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
4034
|
+
}
|
|
4035
|
+
const res = await fetch(`${base}/api/agents/destroy-intent`, {
|
|
4036
|
+
method: "POST",
|
|
4037
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${bearer}` },
|
|
4038
|
+
body: JSON.stringify({ name: a2.name })
|
|
4039
|
+
});
|
|
4040
|
+
if (!res.ok) {
|
|
4041
|
+
return `destroy failed: destroy-intent ${res.status} \u2014 ${(await res.text().catch(() => "")).slice(0, 200)}`;
|
|
4042
|
+
}
|
|
4043
|
+
const d2 = await res.json();
|
|
4044
|
+
return `destroying worker "${a2.name}"; intent=${d2.intent_id ?? "?"}`;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
];
|
|
3849
4048
|
function ape(args) {
|
|
3850
4049
|
try {
|
|
3851
4050
|
return execFileSync2("ape-tasks", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -3882,14 +4081,17 @@ var tasksTools = [
|
|
|
3882
4081
|
},
|
|
3883
4082
|
{
|
|
3884
4083
|
name: "tasks.create",
|
|
3885
|
-
description: "Create a new ape-task
|
|
4084
|
+
description: "Create a new ape-task at tasks.openape.ai. Pass `team` (the team id) to file it on a shared team board, and `assignee` (an email) to delegate it to a teammate.",
|
|
3886
4085
|
parameters: {
|
|
3887
4086
|
type: "object",
|
|
3888
4087
|
properties: {
|
|
3889
4088
|
title: { type: "string" },
|
|
3890
4089
|
notes: { type: "string" },
|
|
3891
4090
|
priority: { type: "string", enum: ["low", "med", "high"] },
|
|
3892
|
-
due_at: { type: "string", description: "ISO date or +Nh/+Nd shorthand." }
|
|
4091
|
+
due_at: { type: "string", description: "ISO date or +Nh/+Nd shorthand." },
|
|
4092
|
+
team: { type: "string", description: "Team id to file the task on (required when you belong to a team)." },
|
|
4093
|
+
assignee: { type: "string", description: "Email of the teammate to assign the task to." },
|
|
4094
|
+
dedup_key: { type: "string", description: "Stable id for the source (e.g. a mail Message-ID). If an open task with this key already exists, no duplicate is created \u2014 pass it for recurring triage so the same item is not filed twice." }
|
|
3893
4095
|
},
|
|
3894
4096
|
required: ["title"]
|
|
3895
4097
|
},
|
|
@@ -3899,6 +4101,9 @@ var tasksTools = [
|
|
|
3899
4101
|
if (a2.notes) argv2.push("--notes", a2.notes);
|
|
3900
4102
|
if (a2.priority) argv2.push("--priority", a2.priority);
|
|
3901
4103
|
if (a2.due_at) argv2.push("--due", a2.due_at);
|
|
4104
|
+
if (a2.team) argv2.push("--team", a2.team);
|
|
4105
|
+
if (a2.assignee) argv2.push("--assignee", a2.assignee);
|
|
4106
|
+
if (a2.dedup_key) argv2.push("--dedup-key", a2.dedup_key);
|
|
3902
4107
|
const out = ape(argv2);
|
|
3903
4108
|
try {
|
|
3904
4109
|
return JSON.parse(out);
|
|
@@ -3923,6 +4128,83 @@ var timeTools = [
|
|
|
3923
4128
|
}
|
|
3924
4129
|
}
|
|
3925
4130
|
];
|
|
4131
|
+
var TROOP = "https://troop.openape.ai";
|
|
4132
|
+
var RESOURCES = ["objectives", "reports", "members", "cost-snapshots", "overview"];
|
|
4133
|
+
function pathFor(resource, orgId) {
|
|
4134
|
+
const id = encodeURIComponent(orgId);
|
|
4135
|
+
return resource === "overview" ? `/api/orgs/${id}` : `/api/orgs/${id}/${resource}`;
|
|
4136
|
+
}
|
|
4137
|
+
var OBJECTIVE_STATUS = ["planned", "in_progress", "done", "abandoned"];
|
|
4138
|
+
var troopTools = [
|
|
4139
|
+
{
|
|
4140
|
+
name: "troop.company.read",
|
|
4141
|
+
description: "Read your troop company data on troop.openape.ai. resource: objectives | reports | members | cost-snapshots | overview (vision+budget). Read-only.",
|
|
4142
|
+
parameters: {
|
|
4143
|
+
type: "object",
|
|
4144
|
+
properties: {
|
|
4145
|
+
resource: { type: "string", enum: [...RESOURCES], description: "Which company resource to read." },
|
|
4146
|
+
org_id: { type: "string", description: "Your company (org) id." }
|
|
4147
|
+
},
|
|
4148
|
+
required: ["resource", "org_id"]
|
|
4149
|
+
},
|
|
4150
|
+
execute: async (args) => {
|
|
4151
|
+
const { resource, org_id } = args ?? {};
|
|
4152
|
+
if (!resource || !RESOURCES.includes(resource)) {
|
|
4153
|
+
throw new Error(`troop.company.read: unknown resource '${resource}' (expected ${RESOURCES.join(" | ")})`);
|
|
4154
|
+
}
|
|
4155
|
+
if (!org_id) throw new Error("troop.company.read: org_id is required");
|
|
4156
|
+
const bearer = await getAuthorizedBearer({ endpoint: TROOP, aud: "troop.openape.ai" });
|
|
4157
|
+
const res = await fetch(`${TROOP}${pathFor(resource, org_id)}`, {
|
|
4158
|
+
headers: { authorization: bearer }
|
|
4159
|
+
});
|
|
4160
|
+
if (!res.ok) {
|
|
4161
|
+
throw new Error(`troop.company.read ${resource} \u2192 ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
4162
|
+
}
|
|
4163
|
+
return JSON.stringify(await res.json());
|
|
4164
|
+
}
|
|
4165
|
+
},
|
|
4166
|
+
{
|
|
4167
|
+
name: "troop.objective.upsert",
|
|
4168
|
+
description: "Create or update a company objective on troop.openape.ai. Pass objective_id to update an existing one; omit it to create. Authenticated as the agent (acting for the owner).",
|
|
4169
|
+
parameters: {
|
|
4170
|
+
type: "object",
|
|
4171
|
+
properties: {
|
|
4172
|
+
org_id: { type: "string", description: "Your company (org) id." },
|
|
4173
|
+
objective_id: { type: "string", description: "Omit to create; pass to update an existing objective." },
|
|
4174
|
+
title: { type: "string" },
|
|
4175
|
+
description: { type: "string" },
|
|
4176
|
+
status: { type: "string", enum: [...OBJECTIVE_STATUS] },
|
|
4177
|
+
target_date: { type: "number", description: "Unix seconds, or null to clear." }
|
|
4178
|
+
},
|
|
4179
|
+
required: ["org_id"]
|
|
4180
|
+
},
|
|
4181
|
+
execute: async (args) => {
|
|
4182
|
+
const a2 = args ?? {};
|
|
4183
|
+
if (!a2.org_id) throw new Error("troop.objective.upsert: org_id is required");
|
|
4184
|
+
if (a2.status && !OBJECTIVE_STATUS.includes(a2.status)) {
|
|
4185
|
+
throw new Error(`troop.objective.upsert: bad status '${a2.status}'`);
|
|
4186
|
+
}
|
|
4187
|
+
if (!a2.objective_id && !a2.title) throw new Error("troop.objective.upsert: title is required to create an objective");
|
|
4188
|
+
const bearer = await getAuthorizedBearer({ endpoint: TROOP, aud: "troop.openape.ai" });
|
|
4189
|
+
const id = encodeURIComponent(a2.org_id);
|
|
4190
|
+
const body = {};
|
|
4191
|
+
if (a2.title !== void 0) body.title = a2.title;
|
|
4192
|
+
if (a2.description !== void 0) body.description = a2.description;
|
|
4193
|
+
if (a2.status !== void 0) body.status = a2.status;
|
|
4194
|
+
if (a2.target_date !== void 0) body.target_date = a2.target_date;
|
|
4195
|
+
const url = a2.objective_id ? `${TROOP}/api/orgs/${id}/objectives/${encodeURIComponent(a2.objective_id)}` : `${TROOP}/api/orgs/${id}/objectives`;
|
|
4196
|
+
const res = await fetch(url, {
|
|
4197
|
+
method: a2.objective_id ? "PATCH" : "POST",
|
|
4198
|
+
headers: { authorization: bearer, "content-type": "application/json" },
|
|
4199
|
+
body: JSON.stringify(body)
|
|
4200
|
+
});
|
|
4201
|
+
if (!res.ok) {
|
|
4202
|
+
throw new Error(`troop.objective.upsert \u2192 ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
4203
|
+
}
|
|
4204
|
+
return JSON.stringify(await res.json());
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
];
|
|
3926
4208
|
var CWD_RE = /^[\w./-]{1,256}$/;
|
|
3927
4209
|
async function runVerify(cwd, command, timeoutMs) {
|
|
3928
4210
|
if (typeof cwd !== "string" || !CWD_RE.test(cwd)) {
|
|
@@ -3969,7 +4251,9 @@ var ALL_TOOLS = [
|
|
|
3969
4251
|
...bashTools,
|
|
3970
4252
|
...gitWorktreeTools,
|
|
3971
4253
|
...verifyTools,
|
|
3972
|
-
...forgeTools
|
|
4254
|
+
...forgeTools,
|
|
4255
|
+
...spawnTools,
|
|
4256
|
+
...troopTools
|
|
3973
4257
|
];
|
|
3974
4258
|
var TOOLS = Object.fromEntries(
|
|
3975
4259
|
ALL_TOOLS.map((t2) => [t2.name, t2])
|
|
@@ -4070,6 +4354,7 @@ async function runLoop(opts) {
|
|
|
4070
4354
|
const requestBody = {
|
|
4071
4355
|
model: opts.config.model,
|
|
4072
4356
|
messages,
|
|
4357
|
+
...opts.config.reasoningEffort ? { reasoning_effort: opts.config.reasoningEffort } : {},
|
|
4073
4358
|
...tools.length > 0 ? { tools, tool_choice: "auto" } : {},
|
|
4074
4359
|
...opts.streamAggregate ? { stream: true } : {}
|
|
4075
4360
|
};
|
|
@@ -4180,13 +4465,180 @@ var REVIEW_SYSTEM = [
|
|
|
4180
4465
|
'Respond ONLY as JSON: {"approved": boolean, "reason": string}.'
|
|
4181
4466
|
].join(" ");
|
|
4182
4467
|
|
|
4468
|
+
// src/bridge-config.ts
|
|
4469
|
+
var DEFAULT_ENDPOINT = "https://troop.openape.ai";
|
|
4470
|
+
var DEFAULT_APES_BIN = "apes";
|
|
4471
|
+
var DEFAULT_MAX_STEPS = 10;
|
|
4472
|
+
var DEFAULT_SYSTEM_PROMPT = `You are a helpful assistant in a 1:1 chat. Be concise and friendly. When asked for facts, say "I don't know" rather than guess.`;
|
|
4473
|
+
var REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
4474
|
+
function readTelegramConfig(env2) {
|
|
4475
|
+
const botToken = env2.TELEGRAM_BOT_TOKEN;
|
|
4476
|
+
if (!botToken) return void 0;
|
|
4477
|
+
const raw = env2.TELEGRAM_OWNER_USER_ID;
|
|
4478
|
+
if (raw === void 0 || raw === "") return { botToken };
|
|
4479
|
+
const ownerUserId = Number.parseInt(raw, 10);
|
|
4480
|
+
if (!Number.isInteger(ownerUserId)) {
|
|
4481
|
+
throw new TypeError(`TELEGRAM_OWNER_USER_ID is set but not a number: ${JSON.stringify(raw)}`);
|
|
4482
|
+
}
|
|
4483
|
+
return { botToken, ownerUserId };
|
|
4484
|
+
}
|
|
4485
|
+
function readConfig(env2 = process.env) {
|
|
4486
|
+
const toolsRaw = env2.APE_CHAT_BRIDGE_TOOLS ?? "";
|
|
4487
|
+
const tools = toolsRaw.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
4488
|
+
const maxStepsRaw = env2.APE_CHAT_BRIDGE_MAX_STEPS;
|
|
4489
|
+
const maxSteps = maxStepsRaw ? Number.parseInt(maxStepsRaw, 10) : DEFAULT_MAX_STEPS;
|
|
4490
|
+
const model = env2.APE_CHAT_BRIDGE_MODEL;
|
|
4491
|
+
if (!model) {
|
|
4492
|
+
throw new Error(
|
|
4493
|
+
"APE_CHAT_BRIDGE_MODEL is not set. Set it in the container env (compose environment: block) or globally in `~/litellm/.env`. Common values: `gpt-5.4` (ChatGPT-only LiteLLM proxy), `claude-haiku-4-5` (Anthropic-only)."
|
|
4494
|
+
);
|
|
4495
|
+
}
|
|
4496
|
+
const effortRaw = env2.APE_CHAT_BRIDGE_REASONING_EFFORT;
|
|
4497
|
+
const reasoningEffort = REASONING_EFFORTS.includes(effortRaw) ? effortRaw : void 0;
|
|
4498
|
+
return {
|
|
4499
|
+
endpoint: (env2.OPENAPE_TROOP_URL ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
|
|
4500
|
+
apesBin: env2.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
|
|
4501
|
+
model,
|
|
4502
|
+
reasoningEffort,
|
|
4503
|
+
systemPrompt: env2.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
|
|
4504
|
+
tools,
|
|
4505
|
+
maxSteps: Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : DEFAULT_MAX_STEPS,
|
|
4506
|
+
roomFilter: env2.APE_CHAT_BRIDGE_ROOM,
|
|
4507
|
+
telegram: readTelegramConfig(env2)
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
// src/troop-chat-api.ts
|
|
4512
|
+
import { ofetch as ofetch6 } from "ofetch";
|
|
4513
|
+
var MAX_BODY = 64 * 1024;
|
|
4514
|
+
var SYNTHETIC_THREAD_ID = "main";
|
|
4515
|
+
function asHistory(msg, agentEmail, ownerEmail) {
|
|
4516
|
+
return {
|
|
4517
|
+
id: msg.id,
|
|
4518
|
+
roomId: msg.chatId,
|
|
4519
|
+
threadId: SYNTHETIC_THREAD_ID,
|
|
4520
|
+
senderEmail: msg.role === "agent" ? agentEmail : ownerEmail,
|
|
4521
|
+
senderAct: msg.role,
|
|
4522
|
+
body: msg.body,
|
|
4523
|
+
replyTo: msg.replyTo,
|
|
4524
|
+
createdAt: msg.createdAt
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
function asPosted(msg) {
|
|
4528
|
+
return {
|
|
4529
|
+
id: msg.id,
|
|
4530
|
+
roomId: msg.chatId,
|
|
4531
|
+
threadId: SYNTHETIC_THREAD_ID,
|
|
4532
|
+
body: msg.body,
|
|
4533
|
+
createdAt: msg.createdAt
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4536
|
+
var TroopChatApi = class {
|
|
4537
|
+
constructor(endpoint, bearer) {
|
|
4538
|
+
this.endpoint = endpoint;
|
|
4539
|
+
this.bearer = bearer;
|
|
4540
|
+
}
|
|
4541
|
+
endpoint;
|
|
4542
|
+
bearer;
|
|
4543
|
+
bootstrap = null;
|
|
4544
|
+
/** Resolve + cache the agent's chat row (lazy fetch on first use). */
|
|
4545
|
+
async getBootstrap() {
|
|
4546
|
+
if (this.bootstrap) return this.bootstrap;
|
|
4547
|
+
this.bootstrap = await ofetch6(`${this.endpoint}/api/agents/me/chat`, {
|
|
4548
|
+
method: "GET",
|
|
4549
|
+
headers: { Authorization: await this.bearer() }
|
|
4550
|
+
});
|
|
4551
|
+
return this.bootstrap;
|
|
4552
|
+
}
|
|
4553
|
+
/** chat.id + (lazy-fetched) ownerEmail for the bridge's frame-translation path. */
|
|
4554
|
+
async getChatContext() {
|
|
4555
|
+
const b2 = await this.getBootstrap();
|
|
4556
|
+
return { chatId: b2.chat.id, ownerEmail: b2.chat.ownerEmail, agentEmail: b2.chat.agentEmail };
|
|
4557
|
+
}
|
|
4558
|
+
async postMessage(roomId, body, opts = {}) {
|
|
4559
|
+
void roomId;
|
|
4560
|
+
void opts.threadId;
|
|
4561
|
+
const payload = {
|
|
4562
|
+
body: body.length > MAX_BODY ? `${body.slice(0, MAX_BODY - 1)}\u2026` : body
|
|
4563
|
+
};
|
|
4564
|
+
if (opts.replyTo) payload.reply_to = opts.replyTo;
|
|
4565
|
+
if (opts.streaming) payload.streaming = true;
|
|
4566
|
+
const msg = await ofetch6(`${this.endpoint}/api/agents/me/chat/messages`, {
|
|
4567
|
+
method: "POST",
|
|
4568
|
+
headers: { Authorization: await this.bearer() },
|
|
4569
|
+
body: payload
|
|
4570
|
+
});
|
|
4571
|
+
return asPosted(msg);
|
|
4572
|
+
}
|
|
4573
|
+
async listMessages(roomId, threadId, limit = 50) {
|
|
4574
|
+
void roomId;
|
|
4575
|
+
void threadId;
|
|
4576
|
+
void limit;
|
|
4577
|
+
const fresh = await ofetch6(`${this.endpoint}/api/agents/me/chat`, {
|
|
4578
|
+
method: "GET",
|
|
4579
|
+
headers: { Authorization: await this.bearer() }
|
|
4580
|
+
});
|
|
4581
|
+
this.bootstrap = fresh;
|
|
4582
|
+
return fresh.messages.map((m2) => asHistory(m2, fresh.chat.agentEmail, fresh.chat.ownerEmail));
|
|
4583
|
+
}
|
|
4584
|
+
async patchMessage(messageId, opts = {}) {
|
|
4585
|
+
const payload = {};
|
|
4586
|
+
if (opts.body !== void 0) {
|
|
4587
|
+
payload.body = opts.body.length > MAX_BODY ? `${opts.body.slice(0, MAX_BODY - 1)}\u2026` : opts.body;
|
|
4588
|
+
}
|
|
4589
|
+
if (opts.streaming !== void 0) payload.streaming = opts.streaming;
|
|
4590
|
+
if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
|
|
4591
|
+
if (Object.keys(payload).length === 0) return;
|
|
4592
|
+
await ofetch6(`${this.endpoint}/api/agents/me/chat/messages/${encodeURIComponent(messageId)}`, {
|
|
4593
|
+
method: "PATCH",
|
|
4594
|
+
headers: { Authorization: await this.bearer() },
|
|
4595
|
+
body: payload
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
4598
|
+
/**
|
|
4599
|
+
* Troop's chat doesn't have contacts — synthesize a single
|
|
4600
|
+
* always-connected entry pointing at the owner so the bridge's
|
|
4601
|
+
* initial-contact + allowlist flows are no-ops.
|
|
4602
|
+
*/
|
|
4603
|
+
async listContacts() {
|
|
4604
|
+
const b2 = await this.getBootstrap();
|
|
4605
|
+
return [{
|
|
4606
|
+
peerEmail: b2.chat.ownerEmail,
|
|
4607
|
+
myStatus: "accepted",
|
|
4608
|
+
theirStatus: "accepted",
|
|
4609
|
+
connected: true,
|
|
4610
|
+
roomId: b2.chat.id
|
|
4611
|
+
}];
|
|
4612
|
+
}
|
|
4613
|
+
async requestContact(peerEmail) {
|
|
4614
|
+
void peerEmail;
|
|
4615
|
+
return (await this.listContacts())[0];
|
|
4616
|
+
}
|
|
4617
|
+
async acceptContact(peerEmail) {
|
|
4618
|
+
void peerEmail;
|
|
4619
|
+
return (await this.listContacts())[0];
|
|
4620
|
+
}
|
|
4621
|
+
/**
|
|
4622
|
+
* Troop has no threads — return a synthetic one. The bridge's
|
|
4623
|
+
* cron-runner falls back to the main thread on createThread
|
|
4624
|
+
* failure already, so a stable "main" stand-in is the right shape.
|
|
4625
|
+
*/
|
|
4626
|
+
async createThread(roomId, name) {
|
|
4627
|
+
void roomId;
|
|
4628
|
+
return { id: SYNTHETIC_THREAD_ID, name: name.slice(0, 100) };
|
|
4629
|
+
}
|
|
4630
|
+
};
|
|
4631
|
+
|
|
4183
4632
|
// src/cron-runner.ts
|
|
4184
|
-
|
|
4185
|
-
|
|
4633
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
4634
|
+
import { homedir as homedir7 } from "os";
|
|
4635
|
+
import { join as join7 } from "path";
|
|
4636
|
+
var TASK_CACHE_DIR = join7(homedir7(), ".openape", "agent", "tasks");
|
|
4637
|
+
var AGENT_CONFIG_PATH = join7(homedir7(), ".openape", "agent", "agent.json");
|
|
4186
4638
|
function resolveRecipeDir() {
|
|
4187
|
-
return process.env.OPENAPE_RECIPE_DEV_DIR ||
|
|
4639
|
+
return process.env.OPENAPE_RECIPE_DEV_DIR || join7(homedir7(), "recipe");
|
|
4188
4640
|
}
|
|
4189
|
-
var TASK_THREADS_PATH =
|
|
4641
|
+
var TASK_THREADS_PATH = join7(homedir7(), ".openape", "agent", "task-threads.json");
|
|
4190
4642
|
var TICK_INTERVAL_MS = 6e4;
|
|
4191
4643
|
function parseField(token, range, allowStep) {
|
|
4192
4644
|
if (token === "*") return { type: "any" };
|
|
@@ -4228,13 +4680,13 @@ function cronMatches(expr, now) {
|
|
|
4228
4680
|
return fieldMatches(expr.minute, now.getMinutes()) && fieldMatches(expr.hour, now.getHours()) && fieldMatches(expr.dom, now.getDate()) && fieldMatches(expr.month, now.getMonth() + 1) && (fieldMatches(expr.dow, dow) || expr.dow.type === "fixed" && expr.dow.value === 7 && dow === 0);
|
|
4229
4681
|
}
|
|
4230
4682
|
function readTaskSpecs() {
|
|
4231
|
-
if (!
|
|
4683
|
+
if (!existsSync4(TASK_CACHE_DIR)) return [];
|
|
4232
4684
|
const out = [];
|
|
4233
|
-
for (const entry of
|
|
4685
|
+
for (const entry of readdirSync4(TASK_CACHE_DIR)) {
|
|
4234
4686
|
if (!entry.endsWith(".json")) continue;
|
|
4235
|
-
const path =
|
|
4687
|
+
const path = join7(TASK_CACHE_DIR, entry);
|
|
4236
4688
|
try {
|
|
4237
|
-
const t2 = JSON.parse(
|
|
4689
|
+
const t2 = JSON.parse(readFileSync6(path, "utf8"));
|
|
4238
4690
|
if (t2.taskId && t2.cron && t2.enabled !== false) out.push(t2);
|
|
4239
4691
|
} catch {
|
|
4240
4692
|
}
|
|
@@ -4256,9 +4708,9 @@ function shouldReportCommandRun(exitCode, stdout2, stderr) {
|
|
|
4256
4708
|
return exitCode !== 0 || `${stdout2}${stderr}`.trim() !== "";
|
|
4257
4709
|
}
|
|
4258
4710
|
function readSystemPrompt() {
|
|
4259
|
-
if (!
|
|
4711
|
+
if (!existsSync4(AGENT_CONFIG_PATH)) return "";
|
|
4260
4712
|
try {
|
|
4261
|
-
const parsed = JSON.parse(
|
|
4713
|
+
const parsed = JSON.parse(readFileSync6(AGENT_CONFIG_PATH, "utf8"));
|
|
4262
4714
|
return typeof parsed.systemPrompt === "string" ? parsed.systemPrompt : "";
|
|
4263
4715
|
} catch {
|
|
4264
4716
|
return "";
|
|
@@ -4285,9 +4737,9 @@ var CronRunner = class {
|
|
|
4285
4737
|
*/
|
|
4286
4738
|
taskThreads = /* @__PURE__ */ new Map();
|
|
4287
4739
|
loadTaskThreads() {
|
|
4288
|
-
if (!
|
|
4740
|
+
if (!existsSync4(TASK_THREADS_PATH)) return;
|
|
4289
4741
|
try {
|
|
4290
|
-
const parsed = JSON.parse(
|
|
4742
|
+
const parsed = JSON.parse(readFileSync6(TASK_THREADS_PATH, "utf8"));
|
|
4291
4743
|
for (const [k2, v2] of Object.entries(parsed)) {
|
|
4292
4744
|
if (typeof v2 === "string") this.taskThreads.set(k2, v2);
|
|
4293
4745
|
}
|
|
@@ -4296,9 +4748,9 @@ var CronRunner = class {
|
|
|
4296
4748
|
}
|
|
4297
4749
|
persistTaskThreads() {
|
|
4298
4750
|
try {
|
|
4299
|
-
const dir =
|
|
4300
|
-
|
|
4301
|
-
|
|
4751
|
+
const dir = join7(homedir7(), ".openape", "agent");
|
|
4752
|
+
mkdirSync4(dir, { recursive: true });
|
|
4753
|
+
writeFileSync4(
|
|
4302
4754
|
TASK_THREADS_PATH,
|
|
4303
4755
|
`${JSON.stringify(Object.fromEntries(this.taskThreads), null, 2)}
|
|
4304
4756
|
`,
|
|
@@ -4376,7 +4828,7 @@ var CronRunner = class {
|
|
|
4376
4828
|
if (spec.command) {
|
|
4377
4829
|
try {
|
|
4378
4830
|
const recipeDir = resolveRecipeDir();
|
|
4379
|
-
const res = await runApeShell(spec.command, 30 * 60 * 1e3,
|
|
4831
|
+
const res = await runApeShell(spec.command, 30 * 60 * 1e3, existsSync4(recipeDir) ? recipeDir : void 0);
|
|
4380
4832
|
const turn = this.pending.get(sessionId);
|
|
4381
4833
|
if (!turn) return;
|
|
4382
4834
|
turn.status = res.exit_code === 0 ? "ok" : "error";
|
|
@@ -4397,8 +4849,9 @@ var CronRunner = class {
|
|
|
4397
4849
|
return;
|
|
4398
4850
|
}
|
|
4399
4851
|
try {
|
|
4852
|
+
const apiKey = this.deps.refreshApiKey ? await this.deps.refreshApiKey() : this.deps.runtimeConfig.apiKey;
|
|
4400
4853
|
const result = await runLoop({
|
|
4401
|
-
config: this.deps.runtimeConfig,
|
|
4854
|
+
config: { ...this.deps.runtimeConfig, apiKey },
|
|
4402
4855
|
systemPrompt,
|
|
4403
4856
|
userMessage: spec.userPrompt,
|
|
4404
4857
|
tools: taskTools(spec.tools),
|
|
@@ -4478,22 +4931,36 @@ ${text}`.slice(0, 9e3);
|
|
|
4478
4931
|
}
|
|
4479
4932
|
};
|
|
4480
4933
|
|
|
4934
|
+
// src/llm-gateway-key.ts
|
|
4935
|
+
async function resolveLlmGatewayKey(base, fallback, log2, exchange = getAuthorizedBearer) {
|
|
4936
|
+
if (!base.includes("llms.openape.ai"))
|
|
4937
|
+
return fallback;
|
|
4938
|
+
try {
|
|
4939
|
+
const u3 = new URL(base);
|
|
4940
|
+
const bearer = await exchange({ endpoint: u3.origin, aud: u3.host });
|
|
4941
|
+
return bearer.replace(/^Bearer\s+/i, "");
|
|
4942
|
+
} catch (err) {
|
|
4943
|
+
log2(`llm gateway token exchange failed (keeping current key): ${err instanceof Error ? err.message : String(err)}`);
|
|
4944
|
+
return fallback;
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
|
|
4481
4948
|
// src/identity.ts
|
|
4482
|
-
import { existsSync as
|
|
4483
|
-
import { homedir as
|
|
4484
|
-
import { join as
|
|
4485
|
-
function authPath() {
|
|
4486
|
-
return
|
|
4949
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
|
|
4950
|
+
import { homedir as homedir8 } from "os";
|
|
4951
|
+
import { join as join8 } from "path";
|
|
4952
|
+
function authPath(home) {
|
|
4953
|
+
return join8(home, ".config", "apes", "auth.json");
|
|
4487
4954
|
}
|
|
4488
4955
|
function allowlistPath() {
|
|
4489
|
-
return
|
|
4956
|
+
return join8(homedir8(), ".config", "openape", "bridge-allowlist.json");
|
|
4490
4957
|
}
|
|
4491
|
-
function readAgentIdentity() {
|
|
4492
|
-
const path = authPath();
|
|
4493
|
-
if (!
|
|
4958
|
+
function readAgentIdentity(home = homedir8()) {
|
|
4959
|
+
const path = authPath(home);
|
|
4960
|
+
if (!existsSync5(path)) {
|
|
4494
4961
|
throw new Error(`agent identity not found at ${path}`);
|
|
4495
4962
|
}
|
|
4496
|
-
const raw =
|
|
4963
|
+
const raw = readFileSync7(path, "utf8");
|
|
4497
4964
|
const parsed = JSON.parse(raw);
|
|
4498
4965
|
if (!parsed.email) throw new Error(`auth.json at ${path} missing 'email'`);
|
|
4499
4966
|
if (!parsed.idp) throw new Error(`auth.json at ${path} missing 'idp'`);
|
|
@@ -4507,9 +4974,9 @@ function readAgentIdentity() {
|
|
|
4507
4974
|
}
|
|
4508
4975
|
function readAllowlist() {
|
|
4509
4976
|
const path = allowlistPath();
|
|
4510
|
-
if (!
|
|
4977
|
+
if (!existsSync5(path)) return /* @__PURE__ */ new Set();
|
|
4511
4978
|
try {
|
|
4512
|
-
const parsed = JSON.parse(
|
|
4979
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
4513
4980
|
if (!Array.isArray(parsed.emails)) return /* @__PURE__ */ new Set();
|
|
4514
4981
|
return new Set(parsed.emails.map((e2) => e2.toLowerCase()));
|
|
4515
4982
|
} catch {
|
|
@@ -4524,24 +4991,24 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
|
|
|
4524
4991
|
|
|
4525
4992
|
// src/skills.ts
|
|
4526
4993
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
4527
|
-
import { existsSync as
|
|
4528
|
-
import { homedir as
|
|
4529
|
-
import { dirname as
|
|
4994
|
+
import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
|
|
4995
|
+
import { homedir as homedir9 } from "os";
|
|
4996
|
+
import { dirname as dirname3, join as join9, resolve as resolve3 } from "path";
|
|
4530
4997
|
import { fileURLToPath } from "url";
|
|
4531
4998
|
import { parse as parseYaml } from "yaml";
|
|
4532
4999
|
var SKILLS_SUBDIR = [".openape", "agent", "skills"];
|
|
4533
5000
|
var SOUL_PATH_PARTS = [".openape", "agent", "SOUL.md"];
|
|
4534
|
-
function soulPath(home =
|
|
4535
|
-
return
|
|
5001
|
+
function soulPath(home = homedir9()) {
|
|
5002
|
+
return join9(home, ...SOUL_PATH_PARTS);
|
|
4536
5003
|
}
|
|
4537
|
-
function skillsDir(home =
|
|
4538
|
-
return
|
|
5004
|
+
function skillsDir(home = homedir9()) {
|
|
5005
|
+
return join9(home, ...SKILLS_SUBDIR);
|
|
4539
5006
|
}
|
|
4540
|
-
function readSoul(home =
|
|
5007
|
+
function readSoul(home = homedir9()) {
|
|
4541
5008
|
const path = soulPath(home);
|
|
4542
|
-
if (!
|
|
5009
|
+
if (!existsSync6(path)) return null;
|
|
4543
5010
|
try {
|
|
4544
|
-
const body =
|
|
5011
|
+
const body = readFileSync8(path, "utf8").trim();
|
|
4545
5012
|
return body.length > 0 ? body : null;
|
|
4546
5013
|
} catch {
|
|
4547
5014
|
return null;
|
|
@@ -4597,27 +5064,27 @@ function hasBinaryOnPath(bin) {
|
|
|
4597
5064
|
return found;
|
|
4598
5065
|
}
|
|
4599
5066
|
function scanSkillsDir(dir) {
|
|
4600
|
-
if (!
|
|
5067
|
+
if (!existsSync6(dir)) return [];
|
|
4601
5068
|
let entries;
|
|
4602
5069
|
try {
|
|
4603
|
-
entries =
|
|
5070
|
+
entries = readdirSync5(dir);
|
|
4604
5071
|
} catch {
|
|
4605
5072
|
return [];
|
|
4606
5073
|
}
|
|
4607
5074
|
const out = [];
|
|
4608
5075
|
for (const entry of entries) {
|
|
4609
|
-
const skillPath =
|
|
4610
|
-
if (!
|
|
5076
|
+
const skillPath = join9(dir, entry, "SKILL.md");
|
|
5077
|
+
if (!existsSync6(skillPath)) continue;
|
|
4611
5078
|
let st;
|
|
4612
5079
|
try {
|
|
4613
|
-
st =
|
|
5080
|
+
st = statSync2(skillPath);
|
|
4614
5081
|
} catch {
|
|
4615
5082
|
continue;
|
|
4616
5083
|
}
|
|
4617
5084
|
if (!st.isFile()) continue;
|
|
4618
5085
|
let body;
|
|
4619
5086
|
try {
|
|
4620
|
-
body =
|
|
5087
|
+
body = readFileSync8(skillPath, "utf8");
|
|
4621
5088
|
} catch {
|
|
4622
5089
|
continue;
|
|
4623
5090
|
}
|
|
@@ -4634,7 +5101,7 @@ function scanSkillsDir(dir) {
|
|
|
4634
5101
|
return out;
|
|
4635
5102
|
}
|
|
4636
5103
|
function defaultSkillsDir() {
|
|
4637
|
-
const here =
|
|
5104
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4638
5105
|
return resolve3(here, "..", "default-skills");
|
|
4639
5106
|
}
|
|
4640
5107
|
function composeSkills(home, enabledTools) {
|
|
@@ -4681,7 +5148,7 @@ function formatSkillsBlock(skills) {
|
|
|
4681
5148
|
return lines.join("\n");
|
|
4682
5149
|
}
|
|
4683
5150
|
function composeSystemPrompt(input) {
|
|
4684
|
-
const home = input.home ??
|
|
5151
|
+
const home = input.home ?? homedir9();
|
|
4685
5152
|
const parts = [];
|
|
4686
5153
|
const defaultPersona = readDefaultPersona();
|
|
4687
5154
|
if (defaultPersona) parts.push(defaultPersona);
|
|
@@ -4698,13 +5165,13 @@ var _defaultPersonaCache;
|
|
|
4698
5165
|
function readDefaultPersona() {
|
|
4699
5166
|
if (_defaultPersonaCache !== void 0) return _defaultPersonaCache;
|
|
4700
5167
|
try {
|
|
4701
|
-
const here =
|
|
5168
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4702
5169
|
const path = resolve3(here, "..", "default-persona.md");
|
|
4703
|
-
if (!
|
|
5170
|
+
if (!existsSync6(path)) {
|
|
4704
5171
|
_defaultPersonaCache = null;
|
|
4705
5172
|
return null;
|
|
4706
5173
|
}
|
|
4707
|
-
const raw =
|
|
5174
|
+
const raw = readFileSync8(path, "utf8").trim();
|
|
4708
5175
|
_defaultPersonaCache = raw.length > 0 ? raw : null;
|
|
4709
5176
|
return _defaultPersonaCache;
|
|
4710
5177
|
} catch {
|
|
@@ -4823,6 +5290,7 @@ var ThreadSession = class {
|
|
|
4823
5290
|
}
|
|
4824
5291
|
};
|
|
4825
5292
|
const { systemPrompt, tools } = this.deps.resolveConfig();
|
|
5293
|
+
const runtimeConfig = this.deps.refreshRuntimeConfig ? await this.deps.refreshRuntimeConfig() : this.deps.runtimeConfig;
|
|
4826
5294
|
await this.backfillHistoryOnce(replyToMessageId, body);
|
|
4827
5295
|
let sawActivity = false;
|
|
4828
5296
|
let turnSettled = false;
|
|
@@ -4838,7 +5306,7 @@ var ThreadSession = class {
|
|
|
4838
5306
|
}, NO_ACTIVITY_TIMEOUT_MS);
|
|
4839
5307
|
try {
|
|
4840
5308
|
const result = await runLoop({
|
|
4841
|
-
config:
|
|
5309
|
+
config: runtimeConfig,
|
|
4842
5310
|
systemPrompt,
|
|
4843
5311
|
userMessage: body,
|
|
4844
5312
|
tools: taskTools(tools),
|
|
@@ -4961,21 +5429,392 @@ var ThreadSession = class {
|
|
|
4961
5429
|
}
|
|
4962
5430
|
};
|
|
4963
5431
|
|
|
5432
|
+
// src/agent-session.ts
|
|
5433
|
+
var AgentSession = class {
|
|
5434
|
+
constructor(email, ownerEmail, config) {
|
|
5435
|
+
this.email = email;
|
|
5436
|
+
this.ownerEmail = ownerEmail;
|
|
5437
|
+
this.config = config;
|
|
5438
|
+
}
|
|
5439
|
+
email;
|
|
5440
|
+
ownerEmail;
|
|
5441
|
+
config;
|
|
5442
|
+
/**
|
|
5443
|
+
* Lazily-created prompt-injection detector, shared across this session's
|
|
5444
|
+
* messages. Matches the per-agent bridge, which holds one
|
|
5445
|
+
* `createHeuristicDetector()` for its lifetime.
|
|
5446
|
+
*/
|
|
5447
|
+
injectionDetector;
|
|
5448
|
+
describe() {
|
|
5449
|
+
return `${this.email} (owner ${this.ownerEmail})`;
|
|
5450
|
+
}
|
|
5451
|
+
/**
|
|
5452
|
+
* Build this agent's troop chat WebSocket URL from its resolved endpoint and
|
|
5453
|
+
* a bearer token. Ports the exact derivation the per-agent bridge uses in
|
|
5454
|
+
* `pumpOnce` (http→ws, token carried as a query param, a leading `Bearer `
|
|
5455
|
+
* prefix stripped, the value URL-encoded) so the nest's in-process WS-open
|
|
5456
|
+
* increment connects to the same socket the bridge process opens today — with
|
|
5457
|
+
* no second copy of the URL rule once the nest drives the connection.
|
|
5458
|
+
*/
|
|
5459
|
+
chatSocketUrl(bearer) {
|
|
5460
|
+
const base = this.config.endpoint.replace(/^http/, "ws");
|
|
5461
|
+
const token = encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""));
|
|
5462
|
+
return `${base}/_ws/chat?token=${token}`;
|
|
5463
|
+
}
|
|
5464
|
+
/**
|
|
5465
|
+
* Decode one raw troop chat-socket frame into a {@link TroopChatFrame}, or
|
|
5466
|
+
* `null` for frames the agent ignores. Ports the exact decode + filter the
|
|
5467
|
+
* per-agent bridge applies in `pumpOnce`: tolerate string or `Buffer` data,
|
|
5468
|
+
* skip anything that is not valid JSON, and keep only `{type:'message'}`
|
|
5469
|
+
* frames that carry a payload. This is the canonical home for the framing
|
|
5470
|
+
* rule once the nest drives the connection — the WS-message increment routes
|
|
5471
|
+
* accepted frames into the agent loop with no second copy of the rule.
|
|
5472
|
+
*/
|
|
5473
|
+
parseChatFrame(data) {
|
|
5474
|
+
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : "";
|
|
5475
|
+
if (!text)
|
|
5476
|
+
return null;
|
|
5477
|
+
let frame;
|
|
5478
|
+
try {
|
|
5479
|
+
frame = JSON.parse(text);
|
|
5480
|
+
} catch {
|
|
5481
|
+
return null;
|
|
5482
|
+
}
|
|
5483
|
+
if (frame.type !== "message" || !frame.payload)
|
|
5484
|
+
return null;
|
|
5485
|
+
return { chatId: frame.chat_id ?? "", payload: frame.payload };
|
|
5486
|
+
}
|
|
5487
|
+
/**
|
|
5488
|
+
* Translate an accepted {@link TroopChatFrame} into the {@link TroopMessage}
|
|
5489
|
+
* the agent loop runs on. Ports the bridge's `translateTroopPayload`: troop's
|
|
5490
|
+
* payload carries `role` (human|agent) but no sender email, so the email is
|
|
5491
|
+
* synthesized from role (agent → this session's own email, human → the owner)
|
|
5492
|
+
* — the bridge skips its own echoes via `senderEmail === selfEmail`, so this
|
|
5493
|
+
* mapping must match. `threadId` is the synthetic `'main'` because troop has
|
|
5494
|
+
* no threads. This is the canonical home for the payload→message rule once the
|
|
5495
|
+
* nest drives the connection: the runLoop-dispatch increment feeds this
|
|
5496
|
+
* message straight into the loop with no second copy of the translation.
|
|
5497
|
+
*/
|
|
5498
|
+
toMessage(frame) {
|
|
5499
|
+
const { chatId, payload } = frame;
|
|
5500
|
+
const role = payload.role === "agent" ? "agent" : "human";
|
|
5501
|
+
return {
|
|
5502
|
+
id: String(payload.id ?? ""),
|
|
5503
|
+
roomId: chatId || String(payload.chatId ?? ""),
|
|
5504
|
+
threadId: "main",
|
|
5505
|
+
senderEmail: role === "agent" ? this.email : this.ownerEmail,
|
|
5506
|
+
senderAct: role,
|
|
5507
|
+
body: typeof payload.body === "string" ? payload.body : "",
|
|
5508
|
+
replyTo: typeof payload.replyTo === "string" ? payload.replyTo : null,
|
|
5509
|
+
createdAt: typeof payload.createdAt === "number" ? payload.createdAt : Math.floor(Date.now() / 1e3),
|
|
5510
|
+
editedAt: typeof payload.editedAt === "number" ? payload.editedAt : null
|
|
5511
|
+
};
|
|
5512
|
+
}
|
|
5513
|
+
/**
|
|
5514
|
+
* Whether a translated {@link TroopMessage} is this agent's own echo. troop
|
|
5515
|
+
* fans every chat message back to the socket that sent it, so the agent sees
|
|
5516
|
+
* its own replies; feeding those into the loop would be an infinite feedback
|
|
5517
|
+
* cycle. Ports the bridge's `handleInbound` guard (`senderEmail === selfEmail`)
|
|
5518
|
+
* — the canonical home for the self-echo rule once the nest drives the
|
|
5519
|
+
* connection: the runLoop-dispatch increment skips own echoes before it runs
|
|
5520
|
+
* the loop, with no second copy of the comparison.
|
|
5521
|
+
*/
|
|
5522
|
+
isOwnEcho(message) {
|
|
5523
|
+
return message.senderEmail === this.email;
|
|
5524
|
+
}
|
|
5525
|
+
/**
|
|
5526
|
+
* Whether a translated, non-echo {@link TroopMessage} should reach the agent
|
|
5527
|
+
* loop. Ports the bridge's remaining pre-loop guards in `handleInbound`: an
|
|
5528
|
+
* empty or whitespace-only body carries nothing to act on, and a configured
|
|
5529
|
+
* `roomFilter` scopes the agent to a single chat. (The bridge's `threadId`
|
|
5530
|
+
* guard is moot here — {@link toMessage} always synthesizes `'main'`.) The
|
|
5531
|
+
* own-echo guard stays {@link isOwnEcho}, applied first by the caller. This is
|
|
5532
|
+
* the canonical home for the dispatch-filter rule once the nest drives the
|
|
5533
|
+
* connection: the runLoop-dispatch increment runs the loop only for messages
|
|
5534
|
+
* this accepts, with no second copy of the guards.
|
|
5535
|
+
*/
|
|
5536
|
+
shouldDispatch(message) {
|
|
5537
|
+
if (!message.body.trim())
|
|
5538
|
+
return false;
|
|
5539
|
+
if (this.config.roomFilter && message.roomId !== this.config.roomFilter)
|
|
5540
|
+
return false;
|
|
5541
|
+
return true;
|
|
5542
|
+
}
|
|
5543
|
+
/**
|
|
5544
|
+
* Screen an accepted, non-echo {@link TroopMessage} for prompt injection
|
|
5545
|
+
* before it reaches the agent loop. Ports the bridge's `handleInbound`
|
|
5546
|
+
* choke-point: the bridge runs every inbound message through a heuristic
|
|
5547
|
+
* detector and refuses to forward it when the score crosses the threshold,
|
|
5548
|
+
* because once the text is in the loop's history a refusal is harder and
|
|
5549
|
+
* inconsistent. The owner gets a higher bar (legitimate "run shell, do X"
|
|
5550
|
+
* instructions aren't refused) — handled by `decide` keying the threshold off
|
|
5551
|
+
* `sender.isOwner`. This is the canonical home for the screening rule once the
|
|
5552
|
+
* nest drives the connection: the runLoop-dispatch increment refuses blocked
|
|
5553
|
+
* messages with no second copy of the detector setup or the sender mapping.
|
|
5554
|
+
*/
|
|
5555
|
+
async screenInjection(message) {
|
|
5556
|
+
this.injectionDetector ??= createHeuristicDetector();
|
|
5557
|
+
return decide(this.injectionDetector, {
|
|
5558
|
+
text: message.body,
|
|
5559
|
+
sender: {
|
|
5560
|
+
email: message.senderEmail,
|
|
5561
|
+
isOwner: message.senderEmail === this.ownerEmail
|
|
5562
|
+
}
|
|
5563
|
+
});
|
|
5564
|
+
}
|
|
5565
|
+
/**
|
|
5566
|
+
* The short, neutral refusal the agent posts back when {@link screenInjection}
|
|
5567
|
+
* blocks a message. Ports the bridge's `refusalText`: the matched reason is
|
|
5568
|
+
* appended so the owner sees in their chat history + audit log why a specific
|
|
5569
|
+
* message was blocked, but the phrasing deliberately avoids language an
|
|
5570
|
+
* attacker could copy back ("ignore previous instructions and …") to
|
|
5571
|
+
* re-trigger the detector. This is the canonical home for the refusal-message
|
|
5572
|
+
* rule once the nest drives the connection: the runLoop-dispatch increment
|
|
5573
|
+
* posts this text on a block with no second copy of the wording.
|
|
5574
|
+
*/
|
|
5575
|
+
refusalText(reason) {
|
|
5576
|
+
const base = "I won't process this message \u2014 it looks like a prompt-injection attempt.";
|
|
5577
|
+
return reason ? `${base}
|
|
5578
|
+
|
|
5579
|
+
(matched: ${reason})` : base;
|
|
5580
|
+
}
|
|
5581
|
+
};
|
|
5582
|
+
|
|
5583
|
+
// src/telegram-api.ts
|
|
5584
|
+
import { ofetch as ofetch7 } from "ofetch";
|
|
5585
|
+
function createTelegramTransport(botToken) {
|
|
5586
|
+
const base = `https://api.telegram.org/bot${botToken}`;
|
|
5587
|
+
return async (method, params) => {
|
|
5588
|
+
return await ofetch7(`${base}/${method}`, {
|
|
5589
|
+
method: "POST",
|
|
5590
|
+
body: params,
|
|
5591
|
+
ignoreResponseError: true
|
|
5592
|
+
});
|
|
5593
|
+
};
|
|
5594
|
+
}
|
|
5595
|
+
function chatIdParam(roomId) {
|
|
5596
|
+
return /^-?\d+$/.test(roomId) ? Number(roomId) : roomId;
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
// src/telegram-chat-api.ts
|
|
5600
|
+
var PLACEHOLDER_TEXT = "\u2026";
|
|
5601
|
+
var SYNTHETIC_THREAD_ID2 = "main";
|
|
5602
|
+
function encodeId(roomId, messageId) {
|
|
5603
|
+
return `${roomId}|${messageId}`;
|
|
5604
|
+
}
|
|
5605
|
+
function decodeId(id) {
|
|
5606
|
+
const i2 = id.lastIndexOf("|");
|
|
5607
|
+
return { chatId: chatIdParam(id.slice(0, i2)), messageId: Number(id.slice(i2 + 1)) };
|
|
5608
|
+
}
|
|
5609
|
+
var TelegramChatApi = class {
|
|
5610
|
+
constructor(call) {
|
|
5611
|
+
this.call = call;
|
|
5612
|
+
}
|
|
5613
|
+
call;
|
|
5614
|
+
async postMessage(roomId, body, opts = {}) {
|
|
5615
|
+
const text = body.length > 0 ? body : PLACEHOLDER_TEXT;
|
|
5616
|
+
const params = { chat_id: chatIdParam(roomId), text };
|
|
5617
|
+
if (opts.threadId && opts.threadId !== SYNTHETIC_THREAD_ID2) {
|
|
5618
|
+
params.message_thread_id = Number(opts.threadId);
|
|
5619
|
+
}
|
|
5620
|
+
if (opts.replyTo && /^\d+$/.test(opts.replyTo)) {
|
|
5621
|
+
params.reply_parameters = { message_id: Number(opts.replyTo), allow_sending_without_reply: true };
|
|
5622
|
+
}
|
|
5623
|
+
const res = await this.call("sendMessage", params);
|
|
5624
|
+
if (!res.ok || !res.result) {
|
|
5625
|
+
throw new Error(`telegram sendMessage failed: ${res.description ?? "unknown error"}`);
|
|
5626
|
+
}
|
|
5627
|
+
const sent = res.result;
|
|
5628
|
+
return {
|
|
5629
|
+
id: encodeId(roomId, sent.message_id),
|
|
5630
|
+
roomId,
|
|
5631
|
+
threadId: opts.threadId ?? SYNTHETIC_THREAD_ID2,
|
|
5632
|
+
body: text,
|
|
5633
|
+
createdAt: sent.date ?? Math.floor(Date.now() / 1e3)
|
|
5634
|
+
};
|
|
5635
|
+
}
|
|
5636
|
+
async patchMessage(messageId, opts = {}) {
|
|
5637
|
+
if (opts.streaming !== false || opts.body === void 0) return;
|
|
5638
|
+
const { chatId, messageId: msgId } = decodeId(messageId);
|
|
5639
|
+
const res = await this.call("editMessageText", {
|
|
5640
|
+
chat_id: chatId,
|
|
5641
|
+
message_id: msgId,
|
|
5642
|
+
text: opts.body.length > 0 ? opts.body : PLACEHOLDER_TEXT
|
|
5643
|
+
});
|
|
5644
|
+
if (!res.ok && !/not modified/i.test(res.description ?? "")) {
|
|
5645
|
+
throw new Error(`telegram editMessageText failed: ${res.description ?? "unknown error"}`);
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
// Telegram exposes no history-fetch API. A live ThreadSession keeps its own
|
|
5649
|
+
// in-process history across turns; only a bridge restart loses Telegram
|
|
5650
|
+
// context (acceptable for M0 — persisted backfill is later work).
|
|
5651
|
+
async listMessages() {
|
|
5652
|
+
return [];
|
|
5653
|
+
}
|
|
5654
|
+
// Contacts are a troop concept; the Telegram inbound path never invokes the
|
|
5655
|
+
// bridge's contact handshake, so these are inert stand-ins for the interface.
|
|
5656
|
+
async listContacts() {
|
|
5657
|
+
return [];
|
|
5658
|
+
}
|
|
5659
|
+
async requestContact(peerEmail) {
|
|
5660
|
+
return { peerEmail, myStatus: "accepted", theirStatus: "accepted", connected: true, roomId: null };
|
|
5661
|
+
}
|
|
5662
|
+
async acceptContact(peerEmail) {
|
|
5663
|
+
return { peerEmail, myStatus: "accepted", theirStatus: "accepted", connected: true, roomId: null };
|
|
5664
|
+
}
|
|
5665
|
+
// M0 has no Telegram threads; forum-topic creation is M1.
|
|
5666
|
+
async createThread(roomId, name) {
|
|
5667
|
+
void roomId;
|
|
5668
|
+
return { id: SYNTHETIC_THREAD_ID2, name: name.slice(0, 100) };
|
|
5669
|
+
}
|
|
5670
|
+
};
|
|
5671
|
+
|
|
5672
|
+
// src/telegram-channel.ts
|
|
5673
|
+
var LONG_POLL_SECONDS = 30;
|
|
5674
|
+
var POLL_ERROR_BACKOFF_MS = 3e3;
|
|
5675
|
+
function sleep(ms) {
|
|
5676
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
5677
|
+
}
|
|
5678
|
+
var TelegramChannel = class {
|
|
5679
|
+
constructor(deps) {
|
|
5680
|
+
this.deps = deps;
|
|
5681
|
+
this.owner = deps.ownerUserId ?? deps.loadOwnerPin?.();
|
|
5682
|
+
}
|
|
5683
|
+
deps;
|
|
5684
|
+
name = "telegram";
|
|
5685
|
+
offset = 0;
|
|
5686
|
+
// Chats we've already told "not authorized" — one hint per chat, not per message.
|
|
5687
|
+
warned = /* @__PURE__ */ new Set();
|
|
5688
|
+
// The locked owner: explicit id, else a previously-pinned one, else undefined
|
|
5689
|
+
// until the first message pins it (TOFU).
|
|
5690
|
+
owner;
|
|
5691
|
+
async start(onInbound) {
|
|
5692
|
+
await this.skipBacklog();
|
|
5693
|
+
this.deps.log("telegram channel up");
|
|
5694
|
+
while (true) {
|
|
5695
|
+
let updates = [];
|
|
5696
|
+
try {
|
|
5697
|
+
updates = await this.poll();
|
|
5698
|
+
} catch (err) {
|
|
5699
|
+
this.deps.log(`telegram poll error: ${err instanceof Error ? err.message : String(err)}`);
|
|
5700
|
+
await sleep(POLL_ERROR_BACKOFF_MS);
|
|
5701
|
+
continue;
|
|
5702
|
+
}
|
|
5703
|
+
for (const u3 of updates) {
|
|
5704
|
+
this.offset = Math.max(this.offset, u3.update_id + 1);
|
|
5705
|
+
await this.dispatch(u3, onInbound);
|
|
5706
|
+
}
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
/**
|
|
5710
|
+
* On boot, advance the offset past any messages Telegram buffered while the
|
|
5711
|
+
* bridge was down (it holds updates ~24h) so a restart doesn't replay a
|
|
5712
|
+
* day of old messages as fresh turns.
|
|
5713
|
+
*/
|
|
5714
|
+
async skipBacklog() {
|
|
5715
|
+
const res = await this.deps.call("getUpdates", { timeout: 0, offset: -1 });
|
|
5716
|
+
if (res.ok && Array.isArray(res.result) && res.result.length > 0) {
|
|
5717
|
+
const updates = res.result;
|
|
5718
|
+
const last = updates.at(-1);
|
|
5719
|
+
this.offset = last.update_id + 1;
|
|
5720
|
+
this.deps.log(`telegram: skipped ${updates.length} backlog update(s) on boot`);
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
async poll() {
|
|
5724
|
+
const res = await this.deps.call("getUpdates", { timeout: LONG_POLL_SECONDS, offset: this.offset });
|
|
5725
|
+
if (!res.ok) {
|
|
5726
|
+
throw new Error(res.description ?? "getUpdates failed");
|
|
5727
|
+
}
|
|
5728
|
+
return Array.isArray(res.result) ? res.result : [];
|
|
5729
|
+
}
|
|
5730
|
+
async dispatch(u3, onInbound) {
|
|
5731
|
+
const m2 = u3.message;
|
|
5732
|
+
if (!m2 || typeof m2.text !== "string" || m2.text.length === 0) return;
|
|
5733
|
+
const from = m2.from?.id;
|
|
5734
|
+
if (from === void 0) return;
|
|
5735
|
+
if (this.owner === void 0) {
|
|
5736
|
+
this.owner = from;
|
|
5737
|
+
this.deps.saveOwnerPin?.(from);
|
|
5738
|
+
this.deps.log(`telegram: owner pinned to user ${from} on first contact`);
|
|
5739
|
+
} else if (from !== this.owner) {
|
|
5740
|
+
await this.refuseStranger(m2);
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
if (m2.text === "/start" || m2.text.startsWith("/start ")) {
|
|
5744
|
+
await this.reply(m2.chat.id, "Hi \u{1F44B} \u2014 schreib mir einfach, ich bin dein Agent und antworte direkt hier.", m2.message_thread_id);
|
|
5745
|
+
return;
|
|
5746
|
+
}
|
|
5747
|
+
const msg = {
|
|
5748
|
+
id: String(m2.message_id),
|
|
5749
|
+
roomId: String(m2.chat.id),
|
|
5750
|
+
threadId: m2.message_thread_id ? String(m2.message_thread_id) : "main",
|
|
5751
|
+
senderEmail: this.deps.ownerEmail,
|
|
5752
|
+
senderAct: "human",
|
|
5753
|
+
body: m2.text,
|
|
5754
|
+
replyTo: null,
|
|
5755
|
+
createdAt: m2.date ?? Math.floor(Date.now() / 1e3),
|
|
5756
|
+
editedAt: null
|
|
5757
|
+
};
|
|
5758
|
+
await onInbound(msg, this.deps.backend);
|
|
5759
|
+
}
|
|
5760
|
+
async refuseStranger(m2) {
|
|
5761
|
+
if (this.warned.has(m2.chat.id)) return;
|
|
5762
|
+
this.warned.add(m2.chat.id);
|
|
5763
|
+
this.deps.log(`telegram: ignoring message from non-owner user ${m2.from?.id ?? "unknown"}`);
|
|
5764
|
+
await this.reply(m2.chat.id, "Dieser Bot ist privat und nur f\xFCr seinen Owner.", m2.message_thread_id);
|
|
5765
|
+
}
|
|
5766
|
+
async reply(chatId, text, threadId) {
|
|
5767
|
+
const params = { chat_id: chatIdParam(String(chatId)), text };
|
|
5768
|
+
if (threadId) params.message_thread_id = threadId;
|
|
5769
|
+
try {
|
|
5770
|
+
await this.deps.call("sendMessage", params);
|
|
5771
|
+
} catch (err) {
|
|
5772
|
+
this.deps.log(`telegram reply failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
};
|
|
5776
|
+
|
|
4964
5777
|
// src/bridge.ts
|
|
4965
|
-
var AGENT_CONFIG_PATH2 =
|
|
5778
|
+
var AGENT_CONFIG_PATH2 = join10(homedir10(), ".openape", "agent", "agent.json");
|
|
5779
|
+
var TELEGRAM_OWNER_PIN_PATH = join10(homedir10(), ".openape", "agent", "telegram-owner.json");
|
|
5780
|
+
var MEMORY_PATH = join10(homedir10(), ".openape", "agent", "MEMORY.md");
|
|
5781
|
+
function ensureMemoryFile() {
|
|
5782
|
+
if (existsSync7(MEMORY_PATH)) return;
|
|
5783
|
+
try {
|
|
5784
|
+
mkdirSync5(dirname4(MEMORY_PATH), { recursive: true });
|
|
5785
|
+
writeFileSync5(MEMORY_PATH, "", { flag: "wx" });
|
|
5786
|
+
log("seeded empty MEMORY.md");
|
|
5787
|
+
} catch {
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
5790
|
+
function readTelegramOwnerPin() {
|
|
5791
|
+
try {
|
|
5792
|
+
const parsed = JSON.parse(readFileSync9(TELEGRAM_OWNER_PIN_PATH, "utf8"));
|
|
5793
|
+
return typeof parsed.ownerUserId === "number" ? parsed.ownerUserId : void 0;
|
|
5794
|
+
} catch {
|
|
5795
|
+
return void 0;
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
function writeTelegramOwnerPin(id) {
|
|
5799
|
+
try {
|
|
5800
|
+
writeFileSync5(TELEGRAM_OWNER_PIN_PATH, JSON.stringify({ ownerUserId: id }));
|
|
5801
|
+
} catch (err) {
|
|
5802
|
+
log(`failed to persist telegram owner pin: ${err instanceof Error ? err.message : String(err)}`);
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
4966
5805
|
function resolveSystemPrompt(envFallback) {
|
|
4967
|
-
if (!
|
|
5806
|
+
if (!existsSync7(AGENT_CONFIG_PATH2)) return envFallback;
|
|
4968
5807
|
try {
|
|
4969
|
-
const parsed = JSON.parse(
|
|
5808
|
+
const parsed = JSON.parse(readFileSync9(AGENT_CONFIG_PATH2, "utf8"));
|
|
4970
5809
|
return typeof parsed.systemPrompt === "string" ? parsed.systemPrompt : envFallback;
|
|
4971
5810
|
} catch {
|
|
4972
5811
|
return envFallback;
|
|
4973
5812
|
}
|
|
4974
5813
|
}
|
|
4975
5814
|
function resolveTools(envFallback) {
|
|
4976
|
-
if (
|
|
5815
|
+
if (existsSync7(AGENT_CONFIG_PATH2)) {
|
|
4977
5816
|
try {
|
|
4978
|
-
const parsed = JSON.parse(
|
|
5817
|
+
const parsed = JSON.parse(readFileSync9(AGENT_CONFIG_PATH2, "utf8"));
|
|
4979
5818
|
if (Array.isArray(parsed.tools)) {
|
|
4980
5819
|
return parsed.tools.filter((t2) => typeof t2 === "string");
|
|
4981
5820
|
}
|
|
@@ -4984,35 +5823,10 @@ function resolveTools(envFallback) {
|
|
|
4984
5823
|
}
|
|
4985
5824
|
return envFallback;
|
|
4986
5825
|
}
|
|
4987
|
-
var DEFAULT_ENDPOINT = "https://troop.openape.ai";
|
|
4988
|
-
var DEFAULT_APES_BIN = "apes";
|
|
4989
|
-
var DEFAULT_MAX_STEPS = 10;
|
|
4990
|
-
var DEFAULT_SYSTEM_PROMPT = `You are a helpful assistant in a 1:1 chat. Be concise and friendly. When asked for facts, say "I don't know" rather than guess.`;
|
|
4991
5826
|
var PING_INTERVAL_MS = 3e4;
|
|
4992
5827
|
var RECONNECT_BASE_MS = 1e3;
|
|
4993
5828
|
var RECONNECT_MAX_MS = 3e4;
|
|
4994
5829
|
var ALLOWLIST_POLL_INTERVAL_MS = 3e4;
|
|
4995
|
-
function readConfig() {
|
|
4996
|
-
const toolsRaw = process3.env.APE_CHAT_BRIDGE_TOOLS ?? "";
|
|
4997
|
-
const tools = toolsRaw.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
4998
|
-
const maxStepsRaw = process3.env.APE_CHAT_BRIDGE_MAX_STEPS;
|
|
4999
|
-
const maxSteps = maxStepsRaw ? Number.parseInt(maxStepsRaw, 10) : DEFAULT_MAX_STEPS;
|
|
5000
|
-
const model = process3.env.APE_CHAT_BRIDGE_MODEL;
|
|
5001
|
-
if (!model) {
|
|
5002
|
-
throw new Error(
|
|
5003
|
-
"APE_CHAT_BRIDGE_MODEL is not set. Set it in the container env (compose environment: block) or globally in `~/litellm/.env`. Common values: `gpt-5.4` (ChatGPT-only LiteLLM proxy), `claude-haiku-4-5` (Anthropic-only)."
|
|
5004
|
-
);
|
|
5005
|
-
}
|
|
5006
|
-
return {
|
|
5007
|
-
endpoint: (process3.env.OPENAPE_TROOP_URL ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
|
|
5008
|
-
apesBin: process3.env.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
|
|
5009
|
-
model,
|
|
5010
|
-
systemPrompt: process3.env.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
|
|
5011
|
-
tools,
|
|
5012
|
-
maxSteps: Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : DEFAULT_MAX_STEPS,
|
|
5013
|
-
roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM
|
|
5014
|
-
};
|
|
5015
|
-
}
|
|
5016
5830
|
async function getIdentity() {
|
|
5017
5831
|
const idp = await ensureFreshIdpAuth();
|
|
5018
5832
|
const claims = decodeJwt(idp.access_token);
|
|
@@ -5025,23 +5839,18 @@ function log(line) {
|
|
|
5025
5839
|
process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
5026
5840
|
`);
|
|
5027
5841
|
}
|
|
5028
|
-
function
|
|
5842
|
+
function sleep2(ms) {
|
|
5029
5843
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
5030
5844
|
}
|
|
5031
5845
|
function truncate(s2, n2) {
|
|
5032
5846
|
return s2.length <= n2 ? s2 : `${s2.slice(0, n2 - 1)}\u2026`;
|
|
5033
5847
|
}
|
|
5034
|
-
function refusalText(reason) {
|
|
5035
|
-
const base = "I won't process this message \u2014 it looks like a prompt-injection attempt.";
|
|
5036
|
-
return reason ? `${base}
|
|
5037
|
-
|
|
5038
|
-
(matched: ${reason})` : base;
|
|
5039
|
-
}
|
|
5040
5848
|
var Bridge = class {
|
|
5041
|
-
constructor(cfg, selfEmail, ownerEmail) {
|
|
5849
|
+
constructor(cfg, selfEmail, ownerEmail, session) {
|
|
5042
5850
|
this.cfg = cfg;
|
|
5043
5851
|
this.selfEmail = selfEmail;
|
|
5044
5852
|
this.ownerEmail = ownerEmail;
|
|
5853
|
+
this.session = session;
|
|
5045
5854
|
this.bearer = async () => {
|
|
5046
5855
|
const idp = await ensureFreshIdpAuth();
|
|
5047
5856
|
return `Bearer ${idp.access_token}`;
|
|
@@ -5049,6 +5858,10 @@ var Bridge = class {
|
|
|
5049
5858
|
this.chat = new TroopChatApi(this.cfg.endpoint, this.bearer);
|
|
5050
5859
|
this.cron = new CronRunner({
|
|
5051
5860
|
runtimeConfig: this.runtimeConfig(),
|
|
5861
|
+
// Cron fires off-thread, so it can't ride freshRuntimeConfig() like chat
|
|
5862
|
+
// turns do — give it the same DDISA exchange directly so cron stops
|
|
5863
|
+
// leaning on the boot master key (Todo 1).
|
|
5864
|
+
refreshApiKey: () => resolveLlmGatewayKey(process3.env.LITELLM_BASE_URL ?? "", this.llmKey, log),
|
|
5052
5865
|
chat: this.chat,
|
|
5053
5866
|
ownerEmail: this.ownerEmail,
|
|
5054
5867
|
log,
|
|
@@ -5056,10 +5869,13 @@ var Bridge = class {
|
|
|
5056
5869
|
bearer: this.bearer
|
|
5057
5870
|
});
|
|
5058
5871
|
this.cron.start();
|
|
5872
|
+
void this.refreshLlmGatewayKey();
|
|
5873
|
+
setInterval(() => void this.refreshLlmGatewayKey(), 40 * 60 * 1e3);
|
|
5059
5874
|
}
|
|
5060
5875
|
cfg;
|
|
5061
5876
|
selfEmail;
|
|
5062
5877
|
ownerEmail;
|
|
5878
|
+
session;
|
|
5063
5879
|
// Sessions keyed by `${roomId}:${threadId}`. Each ThreadSession holds
|
|
5064
5880
|
// its own message history and calls @openape/apes' runLoop directly
|
|
5065
5881
|
// (no stdio JSON-RPC subprocess — see thread-session.ts).
|
|
@@ -5071,6 +5887,25 @@ var Bridge = class {
|
|
|
5071
5887
|
// backend later. The bridge is the choke-point for every chat message
|
|
5072
5888
|
// before it reaches the agent runtime, so this is the right place.
|
|
5073
5889
|
injectionDetector = createHeuristicDetector();
|
|
5890
|
+
// LLM gateway key. Starts as the env key (master_key — the rollout fallback);
|
|
5891
|
+
// upgraded to this agent's own DDISA-exchanged token by refreshLlmGatewayKey()
|
|
5892
|
+
// when the gateway is llms.openape.ai. ponytail: cron keeps the boot key,
|
|
5893
|
+
// chat threads pick up the refreshed token — drop master_key + cron-DDISA later.
|
|
5894
|
+
llmKey = process3.env.LITELLM_API_KEY ?? process3.env.LITELLM_MASTER_KEY ?? "";
|
|
5895
|
+
async refreshLlmGatewayKey() {
|
|
5896
|
+
const base = process3.env.LITELLM_BASE_URL ?? "";
|
|
5897
|
+
this.llmKey = await resolveLlmGatewayKey(base, this.llmKey, log);
|
|
5898
|
+
}
|
|
5899
|
+
/**
|
|
5900
|
+
* Re-exchange the gateway token (if stale) and return a fresh runtimeConfig.
|
|
5901
|
+
* Thread sessions call this per turn so a long-lived thread never presents an
|
|
5902
|
+
* expired DDISA token. getAuthorizedBearer is cheap when the cached SP-token
|
|
5903
|
+
* is still valid and re-mints only when it's within the expiry skew.
|
|
5904
|
+
*/
|
|
5905
|
+
async freshRuntimeConfig() {
|
|
5906
|
+
await this.refreshLlmGatewayKey();
|
|
5907
|
+
return this.runtimeConfig();
|
|
5908
|
+
}
|
|
5074
5909
|
/**
|
|
5075
5910
|
* RuntimeConfig is shared across thread sessions and the cron runner.
|
|
5076
5911
|
* The bridge resolves it from its own env at boot and reuses for the
|
|
@@ -5078,11 +5913,38 @@ var Bridge = class {
|
|
|
5078
5913
|
*/
|
|
5079
5914
|
runtimeConfig() {
|
|
5080
5915
|
const apiBase = (process3.env.LITELLM_BASE_URL ?? "http://127.0.0.1:4000/v1").replace(/\/$/, "");
|
|
5081
|
-
const apiKey =
|
|
5916
|
+
const apiKey = this.llmKey;
|
|
5082
5917
|
if (!apiKey) {
|
|
5083
5918
|
throw new Error("LITELLM_API_KEY (or LITELLM_MASTER_KEY) must be set in the bridge env.");
|
|
5084
5919
|
}
|
|
5085
|
-
return { apiBase, apiKey, model: this.cfg.model };
|
|
5920
|
+
return { apiBase, apiKey, model: this.cfg.model, reasoningEffort: this.cfg.reasoningEffort };
|
|
5921
|
+
}
|
|
5922
|
+
/**
|
|
5923
|
+
* Start the per-agent chat adapters configured via sealed secrets (today:
|
|
5924
|
+
* Telegram). Each runs its own long-lived inbound loop concurrently with
|
|
5925
|
+
* the troop WebSocket and feeds messages through the same handleInbound
|
|
5926
|
+
* choke-point, but with its own backend so replies go back to that channel.
|
|
5927
|
+
* Fire-and-forget: a channel crash is logged, never takes the bridge down.
|
|
5928
|
+
*/
|
|
5929
|
+
startExtraChannels() {
|
|
5930
|
+
const tg = this.cfg.telegram;
|
|
5931
|
+
if (!tg) return;
|
|
5932
|
+
const call = createTelegramTransport(tg.botToken);
|
|
5933
|
+
const backend = new TelegramChatApi(call);
|
|
5934
|
+
const channel = new TelegramChannel({
|
|
5935
|
+
call,
|
|
5936
|
+
ownerUserId: tg.ownerUserId,
|
|
5937
|
+
// Persist the trust-on-first-use owner pin next to agent.json so a bridge
|
|
5938
|
+
// restart keeps the lock instead of re-learning (and re-opening the window).
|
|
5939
|
+
loadOwnerPin: readTelegramOwnerPin,
|
|
5940
|
+
saveOwnerPin: writeTelegramOwnerPin,
|
|
5941
|
+
ownerEmail: this.ownerEmail,
|
|
5942
|
+
backend,
|
|
5943
|
+
log
|
|
5944
|
+
});
|
|
5945
|
+
void channel.start((msg, b2) => this.handleInbound(msg, b2)).catch((err) => {
|
|
5946
|
+
log(`telegram channel crashed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5947
|
+
});
|
|
5086
5948
|
}
|
|
5087
5949
|
async sendInitialOwnerRequestIfNeeded() {
|
|
5088
5950
|
const contacts = await this.chat.listContacts();
|
|
@@ -5144,7 +6006,13 @@ var Bridge = class {
|
|
|
5144
6006
|
editedAt: typeof payload.editedAt === "number" ? payload.editedAt : null
|
|
5145
6007
|
};
|
|
5146
6008
|
}
|
|
5147
|
-
|
|
6009
|
+
/**
|
|
6010
|
+
* Handle one inbound message from any channel. `backend` is the channel's
|
|
6011
|
+
* own outbound surface (troop or telegram) — refusals and the agent's reply
|
|
6012
|
+
* go back out through it, so the agent communicates on the same channel it
|
|
6013
|
+
* was reached on.
|
|
6014
|
+
*/
|
|
6015
|
+
async handleInbound(msg, backend) {
|
|
5148
6016
|
if (msg.senderEmail === this.selfEmail) return;
|
|
5149
6017
|
if (!msg.body.trim()) return;
|
|
5150
6018
|
if (this.cfg.roomFilter && msg.roomId !== this.cfg.roomFilter) return;
|
|
@@ -5163,7 +6031,7 @@ var Bridge = class {
|
|
|
5163
6031
|
if (decision.blocked) {
|
|
5164
6032
|
log(`[${msg.roomId}/${msg.threadId.slice(0, 8)}] BLOCKED prompt-injection (score=${decision.score.toFixed(2)}, reason=${decision.reason ?? "n/a"})`);
|
|
5165
6033
|
try {
|
|
5166
|
-
await
|
|
6034
|
+
await backend.postMessage(msg.roomId, this.session.refusalText(decision.reason), {
|
|
5167
6035
|
replyTo: msg.id,
|
|
5168
6036
|
threadId: msg.threadId
|
|
5169
6037
|
});
|
|
@@ -5173,18 +6041,21 @@ var Bridge = class {
|
|
|
5173
6041
|
}
|
|
5174
6042
|
return;
|
|
5175
6043
|
}
|
|
5176
|
-
const session = this.getOrCreateThread(msg.roomId, msg.threadId);
|
|
6044
|
+
const session = this.getOrCreateThread(msg.roomId, msg.threadId, backend);
|
|
5177
6045
|
session.enqueue(msg.body, msg.id);
|
|
5178
6046
|
}
|
|
5179
|
-
getOrCreateThread(roomId, threadId) {
|
|
6047
|
+
getOrCreateThread(roomId, threadId, backend) {
|
|
5180
6048
|
const key = `${roomId}:${threadId}`;
|
|
5181
6049
|
let s2 = this.threads.get(key);
|
|
5182
6050
|
if (s2) return s2;
|
|
5183
6051
|
s2 = new ThreadSession({
|
|
5184
6052
|
roomId,
|
|
5185
6053
|
threadId,
|
|
5186
|
-
chat:
|
|
6054
|
+
chat: backend,
|
|
5187
6055
|
runtimeConfig: this.runtimeConfig(),
|
|
6056
|
+
// Re-resolve the gateway token per turn (short-lived DDISA token would
|
|
6057
|
+
// otherwise expire on a long-lived thread -> 401).
|
|
6058
|
+
refreshRuntimeConfig: () => this.freshRuntimeConfig(),
|
|
5188
6059
|
// Resolve tools + systemPrompt on every turn from agent.json
|
|
5189
6060
|
// (latest sync from troop). Owner edits in the troop UI thus
|
|
5190
6061
|
// take effect on the very next message in an existing thread —
|
|
@@ -5240,7 +6111,7 @@ var Bridge = class {
|
|
|
5240
6111
|
}
|
|
5241
6112
|
if (frame.type !== "message" || !frame.payload) return;
|
|
5242
6113
|
const msg = this.translateTroopPayload(frame.chat_id ?? "", frame.payload);
|
|
5243
|
-
void this.handleInbound(msg);
|
|
6114
|
+
void this.handleInbound(msg, this.chat);
|
|
5244
6115
|
});
|
|
5245
6116
|
ws.on("close", () => {
|
|
5246
6117
|
if (pingTimer) clearInterval(pingTimer);
|
|
@@ -5256,7 +6127,14 @@ var Bridge = class {
|
|
|
5256
6127
|
}
|
|
5257
6128
|
};
|
|
5258
6129
|
async function main() {
|
|
6130
|
+
try {
|
|
6131
|
+
startSecretsWatcher({ log: (m2) => log(m2) });
|
|
6132
|
+
} catch (err) {
|
|
6133
|
+
log(`secrets watcher failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
6134
|
+
}
|
|
5259
6135
|
const cfg = readConfig();
|
|
6136
|
+
addReadRoot(defaultSkillsDir());
|
|
6137
|
+
ensureMemoryFile();
|
|
5260
6138
|
const idpId = await getIdentity();
|
|
5261
6139
|
const onDisk = readAgentIdentity();
|
|
5262
6140
|
if (onDisk.email.toLowerCase() !== idpId.email.toLowerCase()) {
|
|
@@ -5264,10 +6142,12 @@ async function main() {
|
|
|
5264
6142
|
`auth.json email (${onDisk.email}) doesn't match IdP token sub (${idpId.email}) \u2014 refusing to start`
|
|
5265
6143
|
);
|
|
5266
6144
|
}
|
|
6145
|
+
const session = new AgentSession(onDisk.email, onDisk.ownerEmail, cfg);
|
|
5267
6146
|
log(
|
|
5268
|
-
`bridge starting \u2014 agent=${
|
|
6147
|
+
`bridge starting \u2014 agent=${session.describe()} apes=${cfg.apesBin} model=${cfg.model} tools=[${cfg.tools.join(",") || "none"}] max_steps=${cfg.maxSteps} room=${cfg.roomFilter ?? "*"}`
|
|
5269
6148
|
);
|
|
5270
|
-
const bridge = new Bridge(cfg, onDisk.email, onDisk.ownerEmail);
|
|
6149
|
+
const bridge = new Bridge(cfg, onDisk.email, onDisk.ownerEmail, session);
|
|
6150
|
+
bridge.startExtraChannels();
|
|
5271
6151
|
let attempt = 0;
|
|
5272
6152
|
while (true) {
|
|
5273
6153
|
try {
|
|
@@ -5278,7 +6158,7 @@ async function main() {
|
|
|
5278
6158
|
attempt++;
|
|
5279
6159
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5280
6160
|
log(`disconnected (${msg}) \u2014 reconnecting in ${Math.round(delay / 1e3)}s`);
|
|
5281
|
-
await
|
|
6161
|
+
await sleep2(delay);
|
|
5282
6162
|
}
|
|
5283
6163
|
}
|
|
5284
6164
|
}
|