@openape/ape-agent 2.9.2 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.mjs +1255 -325
- package/dist/index.d.ts +356 -0
- package/dist/index.mjs +4923 -0
- package/dist/service-bridge-main.mjs +4440 -0
- package/package.json +16 -4
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;
|
|
@@ -802,17 +802,58 @@ ${e.cyan(d)}
|
|
|
802
802
|
}
|
|
803
803
|
});
|
|
804
804
|
|
|
805
|
-
// ../../node_modules/.pnpm/shell-quote@1.8.
|
|
805
|
+
// ../../node_modules/.pnpm/shell-quote@1.8.4/node_modules/shell-quote/quote.js
|
|
806
806
|
var require_quote = __commonJS({
|
|
807
|
-
"../../node_modules/.pnpm/shell-quote@1.8.
|
|
807
|
+
"../../node_modules/.pnpm/shell-quote@1.8.4/node_modules/shell-quote/quote.js"(exports, module) {
|
|
808
808
|
"use strict";
|
|
809
|
+
var OPS = [
|
|
810
|
+
"||",
|
|
811
|
+
"&&",
|
|
812
|
+
";;",
|
|
813
|
+
"|&",
|
|
814
|
+
"<(",
|
|
815
|
+
"<<<",
|
|
816
|
+
">>",
|
|
817
|
+
">&",
|
|
818
|
+
"<&",
|
|
819
|
+
"&",
|
|
820
|
+
";",
|
|
821
|
+
"(",
|
|
822
|
+
")",
|
|
823
|
+
"|",
|
|
824
|
+
"<",
|
|
825
|
+
">"
|
|
826
|
+
];
|
|
827
|
+
var LINE_TERMINATORS = /[\n\r\u2028\u2029]/;
|
|
828
|
+
var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|]/g;
|
|
809
829
|
module.exports = function quote(xs) {
|
|
810
830
|
return xs.map(function(s2) {
|
|
811
831
|
if (s2 === "") {
|
|
812
832
|
return "''";
|
|
813
833
|
}
|
|
814
834
|
if (s2 && typeof s2 === "object") {
|
|
815
|
-
|
|
835
|
+
if (s2.op === "glob") {
|
|
836
|
+
if (typeof s2.pattern !== "string") {
|
|
837
|
+
throw new TypeError("glob token requires a string `pattern`");
|
|
838
|
+
}
|
|
839
|
+
if (LINE_TERMINATORS.test(s2.pattern)) {
|
|
840
|
+
throw new TypeError("glob `pattern` must not contain line terminators");
|
|
841
|
+
}
|
|
842
|
+
return s2.pattern.replace(GLOB_SHELL_SPECIAL, "\\$&");
|
|
843
|
+
}
|
|
844
|
+
if (typeof s2.op === "string") {
|
|
845
|
+
if (OPS.indexOf(s2.op) < 0) {
|
|
846
|
+
throw new TypeError("invalid `op` value: " + JSON.stringify(s2.op));
|
|
847
|
+
}
|
|
848
|
+
return s2.op.replace(/[\s\S]/g, "\\$&");
|
|
849
|
+
}
|
|
850
|
+
if (typeof s2.comment === "string") {
|
|
851
|
+
if (LINE_TERMINATORS.test(s2.comment)) {
|
|
852
|
+
throw new TypeError("`comment` must not contain line terminators");
|
|
853
|
+
}
|
|
854
|
+
return "#" + s2.comment;
|
|
855
|
+
}
|
|
856
|
+
throw new TypeError("unrecognized object token shape");
|
|
816
857
|
}
|
|
817
858
|
if (/["\s\\]/.test(s2) && !/'/.test(s2)) {
|
|
818
859
|
return "'" + s2.replace(/(['])/g, "\\$1") + "'";
|
|
@@ -826,9 +867,9 @@ var require_quote = __commonJS({
|
|
|
826
867
|
}
|
|
827
868
|
});
|
|
828
869
|
|
|
829
|
-
// ../../node_modules/.pnpm/shell-quote@1.8.
|
|
870
|
+
// ../../node_modules/.pnpm/shell-quote@1.8.4/node_modules/shell-quote/parse.js
|
|
830
871
|
var require_parse = __commonJS({
|
|
831
|
-
"../../node_modules/.pnpm/shell-quote@1.8.
|
|
872
|
+
"../../node_modules/.pnpm/shell-quote@1.8.4/node_modules/shell-quote/parse.js"(exports, module) {
|
|
832
873
|
"use strict";
|
|
833
874
|
var CONTROL = "(?:" + [
|
|
834
875
|
"\\|\\|",
|
|
@@ -1023,9 +1064,9 @@ var require_parse = __commonJS({
|
|
|
1023
1064
|
}
|
|
1024
1065
|
});
|
|
1025
1066
|
|
|
1026
|
-
// ../../node_modules/.pnpm/shell-quote@1.8.
|
|
1067
|
+
// ../../node_modules/.pnpm/shell-quote@1.8.4/node_modules/shell-quote/index.js
|
|
1027
1068
|
var require_shell_quote = __commonJS({
|
|
1028
|
-
"../../node_modules/.pnpm/shell-quote@1.8.
|
|
1069
|
+
"../../node_modules/.pnpm/shell-quote@1.8.4/node_modules/shell-quote/index.js"(exports) {
|
|
1029
1070
|
"use strict";
|
|
1030
1071
|
exports.quote = require_quote();
|
|
1031
1072
|
exports.parse = require_parse();
|
|
@@ -1033,9 +1074,9 @@ var require_shell_quote = __commonJS({
|
|
|
1033
1074
|
});
|
|
1034
1075
|
|
|
1035
1076
|
// src/bridge.ts
|
|
1036
|
-
import { existsSync as
|
|
1037
|
-
import { homedir as
|
|
1038
|
-
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";
|
|
1039
1080
|
import process3 from "process";
|
|
1040
1081
|
|
|
1041
1082
|
// ../../packages/cli-auth/dist/index.js
|
|
@@ -1054,22 +1095,33 @@ import { Buffer as Buffer3 } from "buffer";
|
|
|
1054
1095
|
import { createPrivateKey } from "crypto";
|
|
1055
1096
|
import { ofetch as ofetch4 } from "ofetch";
|
|
1056
1097
|
import { ofetch as ofetch5 } from "ofetch";
|
|
1057
|
-
function getConfigDir() {
|
|
1098
|
+
function getConfigDir(authHome) {
|
|
1099
|
+
if (authHome) return join(authHome, ".config", "apes");
|
|
1058
1100
|
const override = process.env.OPENAPE_CLI_AUTH_HOME;
|
|
1059
1101
|
if (override) return override;
|
|
1060
1102
|
return join(homedir(), ".config", "apes");
|
|
1061
1103
|
}
|
|
1062
|
-
function getAuthFile() {
|
|
1063
|
-
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
|
+
}
|
|
1064
1115
|
}
|
|
1065
|
-
function
|
|
1066
|
-
|
|
1116
|
+
function ensureSpTokensDir() {
|
|
1117
|
+
ensureConfigDir();
|
|
1118
|
+
const dir = getSpTokensDir();
|
|
1067
1119
|
if (!existsSync(dir)) {
|
|
1068
1120
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1069
1121
|
}
|
|
1070
1122
|
}
|
|
1071
|
-
function loadIdpAuth() {
|
|
1072
|
-
const file = getAuthFile();
|
|
1123
|
+
function loadIdpAuth(authHome) {
|
|
1124
|
+
const file = getAuthFile(authHome);
|
|
1073
1125
|
if (!existsSync(file)) return null;
|
|
1074
1126
|
try {
|
|
1075
1127
|
const raw = readFileSync(file, "utf-8");
|
|
@@ -1079,9 +1131,9 @@ function loadIdpAuth() {
|
|
|
1079
1131
|
return null;
|
|
1080
1132
|
}
|
|
1081
1133
|
}
|
|
1082
|
-
function saveIdpAuth(auth) {
|
|
1083
|
-
ensureConfigDir();
|
|
1084
|
-
const file = getAuthFile();
|
|
1134
|
+
function saveIdpAuth(auth, authHome) {
|
|
1135
|
+
ensureConfigDir(authHome);
|
|
1136
|
+
const file = getAuthFile(authHome);
|
|
1085
1137
|
let extra = {};
|
|
1086
1138
|
if (existsSync(file)) {
|
|
1087
1139
|
try {
|
|
@@ -1101,6 +1153,27 @@ function saveIdpAuth(auth) {
|
|
|
1101
1153
|
const merged = { ...extra, ...auth };
|
|
1102
1154
|
writeFileSync(file, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
1103
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
|
+
}
|
|
1104
1177
|
var AuthError = class extends Error {
|
|
1105
1178
|
status;
|
|
1106
1179
|
hint;
|
|
@@ -1122,6 +1195,39 @@ var NotLoggedInError = class extends AuthError {
|
|
|
1122
1195
|
this.name = "NotLoggedInError";
|
|
1123
1196
|
}
|
|
1124
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
|
+
}
|
|
1125
1231
|
var OPENSSH_MAGIC = "openssh-key-v1\0";
|
|
1126
1232
|
function loadEd25519PrivateKey(pem) {
|
|
1127
1233
|
if (pem.includes("BEGIN OPENSSH PRIVATE KEY")) {
|
|
@@ -1277,8 +1383,8 @@ async function getTokenEndpoint(idp) {
|
|
|
1277
1383
|
}
|
|
1278
1384
|
return `${idp}/token`;
|
|
1279
1385
|
}
|
|
1280
|
-
async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
1281
|
-
const auth = loadIdpAuth();
|
|
1386
|
+
async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3), authHome) {
|
|
1387
|
+
const auth = loadIdpAuth(authHome);
|
|
1282
1388
|
if (!auth) {
|
|
1283
1389
|
throw new NotLoggedInError();
|
|
1284
1390
|
}
|
|
@@ -1288,7 +1394,7 @@ async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
|
1288
1394
|
if (!auth.refresh_token) {
|
|
1289
1395
|
const refreshed = await refreshAgentToken(auth, now);
|
|
1290
1396
|
if (refreshed) {
|
|
1291
|
-
saveIdpAuth(refreshed);
|
|
1397
|
+
saveIdpAuth(refreshed, authHome);
|
|
1292
1398
|
return refreshed;
|
|
1293
1399
|
}
|
|
1294
1400
|
throw new NotLoggedInError(
|
|
@@ -1310,7 +1416,7 @@ async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
|
1310
1416
|
} catch (err) {
|
|
1311
1417
|
const status = err.status ?? err.statusCode ?? 0;
|
|
1312
1418
|
if (status === 400 || status === 401) {
|
|
1313
|
-
saveIdpAuth({ ...auth, refresh_token: void 0 });
|
|
1419
|
+
saveIdpAuth({ ...auth, refresh_token: void 0 }, authHome);
|
|
1314
1420
|
throw new NotLoggedInError(
|
|
1315
1421
|
`Refresh token rejected by ${auth.idp}. Run \`apes login\` again.`
|
|
1316
1422
|
);
|
|
@@ -1330,9 +1436,26 @@ async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
|
1330
1436
|
refresh_token: response.refresh_token ?? auth.refresh_token,
|
|
1331
1437
|
expires_at: now + (response.expires_in ?? 3600)
|
|
1332
1438
|
};
|
|
1333
|
-
saveIdpAuth(next);
|
|
1439
|
+
saveIdpAuth(next, authHome);
|
|
1334
1440
|
return next;
|
|
1335
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
|
+
}
|
|
1336
1459
|
|
|
1337
1460
|
// ../../packages/prompt-injection-detector/dist/index.js
|
|
1338
1461
|
var DEFAULT_THRESHOLD = 0.7;
|
|
@@ -1398,152 +1521,69 @@ function createHeuristicDetector() {
|
|
|
1398
1521
|
import { decodeJwt } from "jose";
|
|
1399
1522
|
import WebSocket from "ws";
|
|
1400
1523
|
|
|
1401
|
-
//
|
|
1402
|
-
import {
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
function asHistory(msg, agentEmail, ownerEmail) {
|
|
1406
|
-
return {
|
|
1407
|
-
id: msg.id,
|
|
1408
|
-
roomId: msg.chatId,
|
|
1409
|
-
threadId: SYNTHETIC_THREAD_ID,
|
|
1410
|
-
senderEmail: msg.role === "agent" ? agentEmail : ownerEmail,
|
|
1411
|
-
senderAct: msg.role,
|
|
1412
|
-
body: msg.body,
|
|
1413
|
-
replyTo: msg.replyTo,
|
|
1414
|
-
createdAt: msg.createdAt
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
function asPosted(msg) {
|
|
1418
|
-
return {
|
|
1419
|
-
id: msg.id,
|
|
1420
|
-
roomId: msg.chatId,
|
|
1421
|
-
threadId: SYNTHETIC_THREAD_ID,
|
|
1422
|
-
body: msg.body,
|
|
1423
|
-
createdAt: msg.createdAt
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
var TroopChatApi = class {
|
|
1427
|
-
constructor(endpoint, bearer) {
|
|
1428
|
-
this.endpoint = endpoint;
|
|
1429
|
-
this.bearer = bearer;
|
|
1430
|
-
}
|
|
1431
|
-
endpoint;
|
|
1432
|
-
bearer;
|
|
1433
|
-
bootstrap = null;
|
|
1434
|
-
/** Resolve + cache the agent's chat row (lazy fetch on first use). */
|
|
1435
|
-
async getBootstrap() {
|
|
1436
|
-
if (this.bootstrap) return this.bootstrap;
|
|
1437
|
-
this.bootstrap = await ofetch6(`${this.endpoint}/api/agents/me/chat`, {
|
|
1438
|
-
method: "GET",
|
|
1439
|
-
headers: { Authorization: await this.bearer() }
|
|
1440
|
-
});
|
|
1441
|
-
return this.bootstrap;
|
|
1442
|
-
}
|
|
1443
|
-
/** chat.id + (lazy-fetched) ownerEmail for the bridge's frame-translation path. */
|
|
1444
|
-
async getChatContext() {
|
|
1445
|
-
const b2 = await this.getBootstrap();
|
|
1446
|
-
return { chatId: b2.chat.id, ownerEmail: b2.chat.ownerEmail, agentEmail: b2.chat.agentEmail };
|
|
1447
|
-
}
|
|
1448
|
-
async postMessage(roomId, body, opts = {}) {
|
|
1449
|
-
void roomId;
|
|
1450
|
-
void opts.threadId;
|
|
1451
|
-
const payload = {
|
|
1452
|
-
body: body.length > MAX_BODY ? `${body.slice(0, MAX_BODY - 1)}\u2026` : body
|
|
1453
|
-
};
|
|
1454
|
-
if (opts.replyTo) payload.reply_to = opts.replyTo;
|
|
1455
|
-
if (opts.streaming) payload.streaming = true;
|
|
1456
|
-
const msg = await ofetch6(`${this.endpoint}/api/agents/me/chat/messages`, {
|
|
1457
|
-
method: "POST",
|
|
1458
|
-
headers: { Authorization: await this.bearer() },
|
|
1459
|
-
body: payload
|
|
1460
|
-
});
|
|
1461
|
-
return asPosted(msg);
|
|
1462
|
-
}
|
|
1463
|
-
async listMessages(roomId, threadId, limit = 50) {
|
|
1464
|
-
void roomId;
|
|
1465
|
-
void threadId;
|
|
1466
|
-
void limit;
|
|
1467
|
-
const fresh = await ofetch6(`${this.endpoint}/api/agents/me/chat`, {
|
|
1468
|
-
method: "GET",
|
|
1469
|
-
headers: { Authorization: await this.bearer() }
|
|
1470
|
-
});
|
|
1471
|
-
this.bootstrap = fresh;
|
|
1472
|
-
return fresh.messages.map((m2) => asHistory(m2, fresh.chat.agentEmail, fresh.chat.ownerEmail));
|
|
1473
|
-
}
|
|
1474
|
-
async patchMessage(messageId, opts = {}) {
|
|
1475
|
-
const payload = {};
|
|
1476
|
-
if (opts.body !== void 0) {
|
|
1477
|
-
payload.body = opts.body.length > MAX_BODY ? `${opts.body.slice(0, MAX_BODY - 1)}\u2026` : opts.body;
|
|
1478
|
-
}
|
|
1479
|
-
if (opts.streaming !== void 0) payload.streaming = opts.streaming;
|
|
1480
|
-
if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
|
|
1481
|
-
if (Object.keys(payload).length === 0) return;
|
|
1482
|
-
await ofetch6(`${this.endpoint}/api/agents/me/chat/messages/${encodeURIComponent(messageId)}`, {
|
|
1483
|
-
method: "PATCH",
|
|
1484
|
-
headers: { Authorization: await this.bearer() },
|
|
1485
|
-
body: payload
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
/**
|
|
1489
|
-
* Troop's chat doesn't have contacts — synthesize a single
|
|
1490
|
-
* always-connected entry pointing at the owner so the bridge's
|
|
1491
|
-
* initial-contact + allowlist flows are no-ops.
|
|
1492
|
-
*/
|
|
1493
|
-
async listContacts() {
|
|
1494
|
-
const b2 = await this.getBootstrap();
|
|
1495
|
-
return [{
|
|
1496
|
-
peerEmail: b2.chat.ownerEmail,
|
|
1497
|
-
myStatus: "accepted",
|
|
1498
|
-
theirStatus: "accepted",
|
|
1499
|
-
connected: true,
|
|
1500
|
-
roomId: b2.chat.id
|
|
1501
|
-
}];
|
|
1502
|
-
}
|
|
1503
|
-
async requestContact(peerEmail) {
|
|
1504
|
-
void peerEmail;
|
|
1505
|
-
return (await this.listContacts())[0];
|
|
1506
|
-
}
|
|
1507
|
-
async acceptContact(peerEmail) {
|
|
1508
|
-
void peerEmail;
|
|
1509
|
-
return (await this.listContacts())[0];
|
|
1510
|
-
}
|
|
1511
|
-
/**
|
|
1512
|
-
* Troop has no threads — return a synthetic one. The bridge's
|
|
1513
|
-
* cron-runner falls back to the main thread on createThread
|
|
1514
|
-
* failure already, so a stable "main" stand-in is the right shape.
|
|
1515
|
-
*/
|
|
1516
|
-
async createThread(roomId, name) {
|
|
1517
|
-
void roomId;
|
|
1518
|
-
return { id: SYNTHETIC_THREAD_ID, name: name.slice(0, 100) };
|
|
1519
|
-
}
|
|
1520
|
-
};
|
|
1521
|
-
|
|
1522
|
-
// src/cron-runner.ts
|
|
1523
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1524
|
-
import { homedir as homedir6 } from "os";
|
|
1525
|
-
import { join as join4 } from "path";
|
|
1526
|
-
|
|
1527
|
-
// ../../packages/apes/dist/chunk-BA2V3BBO.js
|
|
1528
|
-
init_chunk_OBF7IMQ2();
|
|
1529
|
-
|
|
1530
|
-
// ../../node_modules/.pnpm/citty@0.2.2/node_modules/citty/dist/index.mjs
|
|
1531
|
-
import { parseArgs as parseArgs$1 } from "util";
|
|
1532
|
-
function defineCommand(def) {
|
|
1533
|
-
return def;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
// ../../packages/shapes/dist/index.js
|
|
1537
|
-
import { createHash } from "crypto";
|
|
1538
|
-
import { existsSync as existsSync22, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
1539
|
-
import { homedir as homedir22 } from "os";
|
|
1540
|
-
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";
|
|
1541
1528
|
|
|
1542
1529
|
// ../../packages/core/dist/index.js
|
|
1543
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";
|
|
1544
1541
|
import { lookup } from "dns/promises";
|
|
1545
1542
|
import { isIP } from "net";
|
|
1546
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
|
+
}
|
|
1547
1587
|
function isBlockedAddress(ip) {
|
|
1548
1588
|
const fam = isIP(ip);
|
|
1549
1589
|
if (fam === 4) {
|
|
@@ -1598,6 +1638,100 @@ async function assertPublicUrl(rawUrl, opts = {}) {
|
|
|
1598
1638
|
return url;
|
|
1599
1639
|
}
|
|
1600
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
|
+
|
|
1601
1735
|
// ../../packages/grants/dist/index.js
|
|
1602
1736
|
function normalizeSelector(selector) {
|
|
1603
1737
|
if (!selector)
|
|
@@ -2366,20 +2500,20 @@ var isColorSupported = !isDisabled && (isForced || isWindows && !isDumbTerminal
|
|
|
2366
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)) {
|
|
2367
2501
|
return head + (next < 0 ? tail : replaceClose(next, tail, close, replace));
|
|
2368
2502
|
}
|
|
2369
|
-
function clearBleed(index, string,
|
|
2370
|
-
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;
|
|
2371
2505
|
}
|
|
2372
|
-
function filterEmpty(
|
|
2506
|
+
function filterEmpty(open2, close, replace = open2, at = open2.length + 1) {
|
|
2373
2507
|
return (string) => string || !(string === "" || string === void 0) ? clearBleed(
|
|
2374
2508
|
("" + string).indexOf(close, at),
|
|
2375
2509
|
string,
|
|
2376
|
-
|
|
2510
|
+
open2,
|
|
2377
2511
|
close,
|
|
2378
2512
|
replace
|
|
2379
2513
|
) : "";
|
|
2380
2514
|
}
|
|
2381
|
-
function init(
|
|
2382
|
-
return filterEmpty(`\x1B[${
|
|
2515
|
+
function init(open2, close, replace) {
|
|
2516
|
+
return filterEmpty(`\x1B[${open2}m`, `\x1B[${close}m`, replace);
|
|
2383
2517
|
}
|
|
2384
2518
|
var colorDefs = {
|
|
2385
2519
|
reset: init(0, 0),
|
|
@@ -2999,10 +3133,10 @@ function findByExecutable(executable) {
|
|
|
2999
3133
|
if (!existsSync22(dir))
|
|
3000
3134
|
continue;
|
|
3001
3135
|
try {
|
|
3002
|
-
const files =
|
|
3136
|
+
const files = readdirSync3(dir).filter((f3) => f3.endsWith(".toml"));
|
|
3003
3137
|
for (const file of files) {
|
|
3004
3138
|
const path = join22(dir, file);
|
|
3005
|
-
const content =
|
|
3139
|
+
const content = readFileSync4(path, "utf-8");
|
|
3006
3140
|
const match = content.match(/^\s*executable\s*=\s*"([^"]+)"/m);
|
|
3007
3141
|
if (match && match[1] === executable)
|
|
3008
3142
|
return path;
|
|
@@ -3029,7 +3163,7 @@ function resolveAdapterPath(cliId, explicitPath) {
|
|
|
3029
3163
|
}
|
|
3030
3164
|
function loadAdapter(cliId, explicitPath) {
|
|
3031
3165
|
const source = resolveAdapterPath(cliId, explicitPath);
|
|
3032
|
-
const content =
|
|
3166
|
+
const content = readFileSync4(source, "utf-8");
|
|
3033
3167
|
const adapter = parseAdapterToml(content);
|
|
3034
3168
|
const idMatch = adapter.cli.id === cliId;
|
|
3035
3169
|
const fileMatch = basename(source) === `${cliId}.toml`;
|
|
@@ -3130,7 +3264,7 @@ function extractOption(args, name) {
|
|
|
3130
3264
|
return void 0;
|
|
3131
3265
|
}
|
|
3132
3266
|
|
|
3133
|
-
// ../../packages/apes/dist/chunk-
|
|
3267
|
+
// ../../packages/apes/dist/chunk-MMBFV5WN.js
|
|
3134
3268
|
init_chunk_OBF7IMQ2();
|
|
3135
3269
|
var debug = process.argv.includes("--debug");
|
|
3136
3270
|
|
|
@@ -3139,13 +3273,16 @@ init_chunk_OBF7IMQ2();
|
|
|
3139
3273
|
|
|
3140
3274
|
// ../../packages/agent-runtime/dist/index.js
|
|
3141
3275
|
import { spawn } from "child_process";
|
|
3142
|
-
import { mkdirSync as
|
|
3143
|
-
import { homedir as
|
|
3144
|
-
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";
|
|
3145
3279
|
import { homedir as homedir23 } from "os";
|
|
3146
3280
|
import { resolve as resolve2 } from "path";
|
|
3147
3281
|
import process2 from "process";
|
|
3148
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";
|
|
3149
3286
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3150
3287
|
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3151
3288
|
var MAX_STDIO_BYTES = 64 * 1024;
|
|
@@ -3156,13 +3293,14 @@ function capStdio(s2) {
|
|
|
3156
3293
|
return `${buf.subarray(0, MAX_STDIO_BYTES).toString("utf8")}
|
|
3157
3294
|
[truncated to ${MAX_STDIO_BYTES} bytes]`;
|
|
3158
3295
|
}
|
|
3159
|
-
function runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
3296
|
+
function runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS, cwd) {
|
|
3160
3297
|
const bypass = process.env.OPENAPE_BYPASS_APE_SHELL === "1";
|
|
3161
3298
|
const [execBin, execArgs] = bypass ? ["/bin/bash", ["-c", cmd]] : [BIN, ["-c", cmd]];
|
|
3162
3299
|
return new Promise((resolveResult) => {
|
|
3163
3300
|
const child = spawn(execBin, execArgs, {
|
|
3164
3301
|
env: { ...process.env, APE_WAIT: "1" },
|
|
3165
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3302
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3303
|
+
...cwd ? { cwd } : {}
|
|
3166
3304
|
});
|
|
3167
3305
|
let stdout2 = "";
|
|
3168
3306
|
let stderr = "";
|
|
@@ -3238,21 +3376,31 @@ var bashTools = [
|
|
|
3238
3376
|
}
|
|
3239
3377
|
];
|
|
3240
3378
|
var MAX_BYTES = 1024 * 1024;
|
|
3241
|
-
|
|
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 = {}) {
|
|
3242
3387
|
if (typeof input !== "string" || input === "") {
|
|
3243
3388
|
throw new Error("path must be a non-empty string");
|
|
3244
3389
|
}
|
|
3245
|
-
const home =
|
|
3390
|
+
const home = homedir6();
|
|
3246
3391
|
const candidate = input.startsWith("~/") ? resolve(home, input.slice(2)) : input.startsWith("/") ? normalize(input) : resolve(home, input);
|
|
3247
|
-
if (candidate
|
|
3248
|
-
|
|
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
|
+
}
|
|
3249
3397
|
}
|
|
3250
|
-
|
|
3398
|
+
throw new Error(`path "${input}" resolves outside the agent's home`);
|
|
3251
3399
|
}
|
|
3252
3400
|
var fileTools = [
|
|
3253
3401
|
{
|
|
3254
3402
|
name: "file.read",
|
|
3255
|
-
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.",
|
|
3256
3404
|
parameters: {
|
|
3257
3405
|
type: "object",
|
|
3258
3406
|
properties: {
|
|
@@ -3262,8 +3410,8 @@ var fileTools = [
|
|
|
3262
3410
|
},
|
|
3263
3411
|
execute: async (args) => {
|
|
3264
3412
|
const a2 = args;
|
|
3265
|
-
const p = jailPath(a2.path);
|
|
3266
|
-
const content =
|
|
3413
|
+
const p = jailPath(a2.path, { allowReadRoots: true });
|
|
3414
|
+
const content = readFileSync5(p, "utf8");
|
|
3267
3415
|
if (Buffer.byteLength(content, "utf8") > MAX_BYTES) {
|
|
3268
3416
|
return { path: p, truncated: true, content: content.slice(0, MAX_BYTES) };
|
|
3269
3417
|
}
|
|
@@ -3288,8 +3436,8 @@ var fileTools = [
|
|
|
3288
3436
|
throw new Error(`content exceeds ${MAX_BYTES} byte cap`);
|
|
3289
3437
|
}
|
|
3290
3438
|
const p = jailPath(a2.path);
|
|
3291
|
-
|
|
3292
|
-
|
|
3439
|
+
mkdirSync3(dirname2(p), { recursive: true });
|
|
3440
|
+
writeFileSync3(p, a2.content, { encoding: "utf8" });
|
|
3293
3441
|
return { path: p, bytes: Buffer.byteLength(a2.content, "utf8") };
|
|
3294
3442
|
}
|
|
3295
3443
|
},
|
|
@@ -3319,7 +3467,7 @@ var fileTools = [
|
|
|
3319
3467
|
}
|
|
3320
3468
|
const replaceAll = a2.replace_all === true;
|
|
3321
3469
|
const p = jailPath(a2.path);
|
|
3322
|
-
const before =
|
|
3470
|
+
const before = readFileSync5(p, "utf8");
|
|
3323
3471
|
const occurrences = before.split(a2.old_string).length - 1;
|
|
3324
3472
|
if (occurrences === 0) {
|
|
3325
3473
|
throw new Error("old_string not found in file");
|
|
@@ -3331,7 +3479,7 @@ var fileTools = [
|
|
|
3331
3479
|
if (Buffer.byteLength(after, "utf8") > MAX_BYTES) {
|
|
3332
3480
|
throw new Error(`result exceeds ${MAX_BYTES} byte cap`);
|
|
3333
3481
|
}
|
|
3334
|
-
|
|
3482
|
+
writeFileSync3(p, after, { encoding: "utf8" });
|
|
3335
3483
|
return { path: p, replacements: replaceAll ? occurrences : 1 };
|
|
3336
3484
|
}
|
|
3337
3485
|
}
|
|
@@ -3804,50 +3952,146 @@ var mailTools = [
|
|
|
3804
3952
|
}
|
|
3805
3953
|
}
|
|
3806
3954
|
];
|
|
3807
|
-
function
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
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?)`);
|
|
3814
3972
|
}
|
|
3973
|
+
return (await res.json()).access_token;
|
|
3815
3974
|
}
|
|
3816
|
-
var
|
|
3975
|
+
var spawnTools = [
|
|
3817
3976
|
{
|
|
3818
|
-
name: "
|
|
3819
|
-
description: "
|
|
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.",
|
|
3820
3979
|
parameters: {
|
|
3821
3980
|
type: "object",
|
|
3822
3981
|
properties: {
|
|
3823
|
-
|
|
3824
|
-
|
|
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" }
|
|
3825
3987
|
},
|
|
3826
|
-
required: []
|
|
3988
|
+
required: ["name"]
|
|
3827
3989
|
},
|
|
3828
3990
|
execute: async (args) => {
|
|
3829
|
-
const a2 = args
|
|
3830
|
-
const
|
|
3831
|
-
|
|
3832
|
-
if (a2.team_id) argv2.push("--team", a2.team_id);
|
|
3833
|
-
const out = ape(argv2);
|
|
3991
|
+
const a2 = args;
|
|
3992
|
+
const base = troopBase();
|
|
3993
|
+
let bearer;
|
|
3834
3994
|
try {
|
|
3835
|
-
|
|
3836
|
-
} catch {
|
|
3837
|
-
return {
|
|
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
|
+
];
|
|
4048
|
+
function ape(args) {
|
|
4049
|
+
try {
|
|
4050
|
+
return execFileSync2("ape-tasks", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4051
|
+
} catch (err) {
|
|
4052
|
+
const e2 = err;
|
|
4053
|
+
const stderr = typeof e2.stderr === "string" ? e2.stderr : e2.stderr?.toString("utf8");
|
|
4054
|
+
throw new Error(`ape-tasks failed: ${stderr ?? e2.message ?? err}`);
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
var tasksTools = [
|
|
4058
|
+
{
|
|
4059
|
+
name: "tasks.list",
|
|
4060
|
+
description: "List the owner's open ape-tasks (the user's personal task list at tasks.openape.ai).",
|
|
4061
|
+
parameters: {
|
|
4062
|
+
type: "object",
|
|
4063
|
+
properties: {
|
|
4064
|
+
status: { type: "string", enum: ["open", "doing", "done", "archived"] },
|
|
4065
|
+
team_id: { type: "string" }
|
|
4066
|
+
},
|
|
4067
|
+
required: []
|
|
4068
|
+
},
|
|
4069
|
+
execute: async (args) => {
|
|
4070
|
+
const a2 = args ?? {};
|
|
4071
|
+
const argv2 = ["list", "--json"];
|
|
4072
|
+
if (a2.status) argv2.push("--status", a2.status);
|
|
4073
|
+
if (a2.team_id) argv2.push("--team", a2.team_id);
|
|
4074
|
+
const out = ape(argv2);
|
|
4075
|
+
try {
|
|
4076
|
+
return JSON.parse(out);
|
|
4077
|
+
} catch {
|
|
4078
|
+
return { raw: out };
|
|
3838
4079
|
}
|
|
3839
4080
|
}
|
|
3840
4081
|
},
|
|
3841
4082
|
{
|
|
3842
4083
|
name: "tasks.create",
|
|
3843
|
-
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.",
|
|
3844
4085
|
parameters: {
|
|
3845
4086
|
type: "object",
|
|
3846
4087
|
properties: {
|
|
3847
4088
|
title: { type: "string" },
|
|
3848
4089
|
notes: { type: "string" },
|
|
3849
4090
|
priority: { type: "string", enum: ["low", "med", "high"] },
|
|
3850
|
-
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." }
|
|
3851
4095
|
},
|
|
3852
4096
|
required: ["title"]
|
|
3853
4097
|
},
|
|
@@ -3857,6 +4101,9 @@ var tasksTools = [
|
|
|
3857
4101
|
if (a2.notes) argv2.push("--notes", a2.notes);
|
|
3858
4102
|
if (a2.priority) argv2.push("--priority", a2.priority);
|
|
3859
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);
|
|
3860
4107
|
const out = ape(argv2);
|
|
3861
4108
|
try {
|
|
3862
4109
|
return JSON.parse(out);
|
|
@@ -3881,6 +4128,83 @@ var timeTools = [
|
|
|
3881
4128
|
}
|
|
3882
4129
|
}
|
|
3883
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
|
+
];
|
|
3884
4208
|
var CWD_RE = /^[\w./-]{1,256}$/;
|
|
3885
4209
|
async function runVerify(cwd, command, timeoutMs) {
|
|
3886
4210
|
if (typeof cwd !== "string" || !CWD_RE.test(cwd)) {
|
|
@@ -3927,7 +4251,9 @@ var ALL_TOOLS = [
|
|
|
3927
4251
|
...bashTools,
|
|
3928
4252
|
...gitWorktreeTools,
|
|
3929
4253
|
...verifyTools,
|
|
3930
|
-
...forgeTools
|
|
4254
|
+
...forgeTools,
|
|
4255
|
+
...spawnTools,
|
|
4256
|
+
...troopTools
|
|
3931
4257
|
];
|
|
3932
4258
|
var TOOLS = Object.fromEntries(
|
|
3933
4259
|
ALL_TOOLS.map((t2) => [t2.name, t2])
|
|
@@ -4028,6 +4354,7 @@ async function runLoop(opts) {
|
|
|
4028
4354
|
const requestBody = {
|
|
4029
4355
|
model: opts.config.model,
|
|
4030
4356
|
messages,
|
|
4357
|
+
...opts.config.reasoningEffort ? { reasoning_effort: opts.config.reasoningEffort } : {},
|
|
4031
4358
|
...tools.length > 0 ? { tools, tool_choice: "auto" } : {},
|
|
4032
4359
|
...opts.streamAggregate ? { stream: true } : {}
|
|
4033
4360
|
};
|
|
@@ -4138,10 +4465,180 @@ var REVIEW_SYSTEM = [
|
|
|
4138
4465
|
'Respond ONLY as JSON: {"approved": boolean, "reason": string}.'
|
|
4139
4466
|
].join(" ");
|
|
4140
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
|
+
|
|
4141
4632
|
// src/cron-runner.ts
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
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");
|
|
4638
|
+
function resolveRecipeDir() {
|
|
4639
|
+
return process.env.OPENAPE_RECIPE_DEV_DIR || join7(homedir7(), "recipe");
|
|
4640
|
+
}
|
|
4641
|
+
var TASK_THREADS_PATH = join7(homedir7(), ".openape", "agent", "task-threads.json");
|
|
4145
4642
|
var TICK_INTERVAL_MS = 6e4;
|
|
4146
4643
|
function parseField(token, range, allowStep) {
|
|
4147
4644
|
if (token === "*") return { type: "any" };
|
|
@@ -4183,13 +4680,13 @@ function cronMatches(expr, now) {
|
|
|
4183
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);
|
|
4184
4681
|
}
|
|
4185
4682
|
function readTaskSpecs() {
|
|
4186
|
-
if (!
|
|
4683
|
+
if (!existsSync4(TASK_CACHE_DIR)) return [];
|
|
4187
4684
|
const out = [];
|
|
4188
|
-
for (const entry of
|
|
4685
|
+
for (const entry of readdirSync4(TASK_CACHE_DIR)) {
|
|
4189
4686
|
if (!entry.endsWith(".json")) continue;
|
|
4190
|
-
const path =
|
|
4687
|
+
const path = join7(TASK_CACHE_DIR, entry);
|
|
4191
4688
|
try {
|
|
4192
|
-
const t2 = JSON.parse(
|
|
4689
|
+
const t2 = JSON.parse(readFileSync6(path, "utf8"));
|
|
4193
4690
|
if (t2.taskId && t2.cron && t2.enabled !== false) out.push(t2);
|
|
4194
4691
|
} catch {
|
|
4195
4692
|
}
|
|
@@ -4207,10 +4704,13 @@ ${tail(out, 2500)}`);
|
|
|
4207
4704
|
${tail(err, 2500)}`);
|
|
4208
4705
|
return parts.join("\n\n");
|
|
4209
4706
|
}
|
|
4707
|
+
function shouldReportCommandRun(exitCode, stdout2, stderr) {
|
|
4708
|
+
return exitCode !== 0 || `${stdout2}${stderr}`.trim() !== "";
|
|
4709
|
+
}
|
|
4210
4710
|
function readSystemPrompt() {
|
|
4211
|
-
if (!
|
|
4711
|
+
if (!existsSync4(AGENT_CONFIG_PATH)) return "";
|
|
4212
4712
|
try {
|
|
4213
|
-
const parsed = JSON.parse(
|
|
4713
|
+
const parsed = JSON.parse(readFileSync6(AGENT_CONFIG_PATH, "utf8"));
|
|
4214
4714
|
return typeof parsed.systemPrompt === "string" ? parsed.systemPrompt : "";
|
|
4215
4715
|
} catch {
|
|
4216
4716
|
return "";
|
|
@@ -4237,9 +4737,9 @@ var CronRunner = class {
|
|
|
4237
4737
|
*/
|
|
4238
4738
|
taskThreads = /* @__PURE__ */ new Map();
|
|
4239
4739
|
loadTaskThreads() {
|
|
4240
|
-
if (!
|
|
4740
|
+
if (!existsSync4(TASK_THREADS_PATH)) return;
|
|
4241
4741
|
try {
|
|
4242
|
-
const parsed = JSON.parse(
|
|
4742
|
+
const parsed = JSON.parse(readFileSync6(TASK_THREADS_PATH, "utf8"));
|
|
4243
4743
|
for (const [k2, v2] of Object.entries(parsed)) {
|
|
4244
4744
|
if (typeof v2 === "string") this.taskThreads.set(k2, v2);
|
|
4245
4745
|
}
|
|
@@ -4248,9 +4748,9 @@ var CronRunner = class {
|
|
|
4248
4748
|
}
|
|
4249
4749
|
persistTaskThreads() {
|
|
4250
4750
|
try {
|
|
4251
|
-
const dir =
|
|
4252
|
-
|
|
4253
|
-
|
|
4751
|
+
const dir = join7(homedir7(), ".openape", "agent");
|
|
4752
|
+
mkdirSync4(dir, { recursive: true });
|
|
4753
|
+
writeFileSync4(
|
|
4254
4754
|
TASK_THREADS_PATH,
|
|
4255
4755
|
`${JSON.stringify(Object.fromEntries(this.taskThreads), null, 2)}
|
|
4256
4756
|
`,
|
|
@@ -4327,13 +4827,15 @@ var CronRunner = class {
|
|
|
4327
4827
|
async runTask(sessionId, systemPrompt, spec) {
|
|
4328
4828
|
if (spec.command) {
|
|
4329
4829
|
try {
|
|
4330
|
-
const
|
|
4830
|
+
const recipeDir = resolveRecipeDir();
|
|
4831
|
+
const res = await runApeShell(spec.command, 30 * 60 * 1e3, existsSync4(recipeDir) ? recipeDir : void 0);
|
|
4331
4832
|
const turn = this.pending.get(sessionId);
|
|
4332
4833
|
if (!turn) return;
|
|
4333
4834
|
turn.status = res.exit_code === 0 ? "ok" : "error";
|
|
4334
4835
|
turn.accumulated = composeTaskOutput(spec.command, res.exit_code, res.stdout, res.stderr);
|
|
4335
4836
|
await this.finaliseRun(turn, 1);
|
|
4336
|
-
|
|
4837
|
+
if (shouldReportCommandRun(res.exit_code, res.stdout, res.stderr))
|
|
4838
|
+
await this.postResult(sessionId, turn);
|
|
4337
4839
|
this.pending.delete(sessionId);
|
|
4338
4840
|
} catch (err) {
|
|
4339
4841
|
const turn = this.pending.get(sessionId);
|
|
@@ -4347,8 +4849,9 @@ var CronRunner = class {
|
|
|
4347
4849
|
return;
|
|
4348
4850
|
}
|
|
4349
4851
|
try {
|
|
4852
|
+
const apiKey = this.deps.refreshApiKey ? await this.deps.refreshApiKey() : this.deps.runtimeConfig.apiKey;
|
|
4350
4853
|
const result = await runLoop({
|
|
4351
|
-
config: this.deps.runtimeConfig,
|
|
4854
|
+
config: { ...this.deps.runtimeConfig, apiKey },
|
|
4352
4855
|
systemPrompt,
|
|
4353
4856
|
userMessage: spec.userPrompt,
|
|
4354
4857
|
tools: taskTools(spec.tools),
|
|
@@ -4428,22 +4931,36 @@ ${text}`.slice(0, 9e3);
|
|
|
4428
4931
|
}
|
|
4429
4932
|
};
|
|
4430
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
|
+
|
|
4431
4948
|
// src/identity.ts
|
|
4432
|
-
import { existsSync as
|
|
4433
|
-
import { homedir as
|
|
4434
|
-
import { join as
|
|
4435
|
-
function authPath() {
|
|
4436
|
-
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");
|
|
4437
4954
|
}
|
|
4438
4955
|
function allowlistPath() {
|
|
4439
|
-
return
|
|
4956
|
+
return join8(homedir8(), ".config", "openape", "bridge-allowlist.json");
|
|
4440
4957
|
}
|
|
4441
|
-
function readAgentIdentity() {
|
|
4442
|
-
const path = authPath();
|
|
4443
|
-
if (!
|
|
4958
|
+
function readAgentIdentity(home = homedir8()) {
|
|
4959
|
+
const path = authPath(home);
|
|
4960
|
+
if (!existsSync5(path)) {
|
|
4444
4961
|
throw new Error(`agent identity not found at ${path}`);
|
|
4445
4962
|
}
|
|
4446
|
-
const raw =
|
|
4963
|
+
const raw = readFileSync7(path, "utf8");
|
|
4447
4964
|
const parsed = JSON.parse(raw);
|
|
4448
4965
|
if (!parsed.email) throw new Error(`auth.json at ${path} missing 'email'`);
|
|
4449
4966
|
if (!parsed.idp) throw new Error(`auth.json at ${path} missing 'idp'`);
|
|
@@ -4457,9 +4974,9 @@ function readAgentIdentity() {
|
|
|
4457
4974
|
}
|
|
4458
4975
|
function readAllowlist() {
|
|
4459
4976
|
const path = allowlistPath();
|
|
4460
|
-
if (!
|
|
4977
|
+
if (!existsSync5(path)) return /* @__PURE__ */ new Set();
|
|
4461
4978
|
try {
|
|
4462
|
-
const parsed = JSON.parse(
|
|
4979
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
4463
4980
|
if (!Array.isArray(parsed.emails)) return /* @__PURE__ */ new Set();
|
|
4464
4981
|
return new Set(parsed.emails.map((e2) => e2.toLowerCase()));
|
|
4465
4982
|
} catch {
|
|
@@ -4474,24 +4991,24 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
|
|
|
4474
4991
|
|
|
4475
4992
|
// src/skills.ts
|
|
4476
4993
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
4477
|
-
import { existsSync as
|
|
4478
|
-
import { homedir as
|
|
4479
|
-
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";
|
|
4480
4997
|
import { fileURLToPath } from "url";
|
|
4481
4998
|
import { parse as parseYaml } from "yaml";
|
|
4482
4999
|
var SKILLS_SUBDIR = [".openape", "agent", "skills"];
|
|
4483
5000
|
var SOUL_PATH_PARTS = [".openape", "agent", "SOUL.md"];
|
|
4484
|
-
function soulPath(home =
|
|
4485
|
-
return
|
|
5001
|
+
function soulPath(home = homedir9()) {
|
|
5002
|
+
return join9(home, ...SOUL_PATH_PARTS);
|
|
4486
5003
|
}
|
|
4487
|
-
function skillsDir(home =
|
|
4488
|
-
return
|
|
5004
|
+
function skillsDir(home = homedir9()) {
|
|
5005
|
+
return join9(home, ...SKILLS_SUBDIR);
|
|
4489
5006
|
}
|
|
4490
|
-
function readSoul(home =
|
|
5007
|
+
function readSoul(home = homedir9()) {
|
|
4491
5008
|
const path = soulPath(home);
|
|
4492
|
-
if (!
|
|
5009
|
+
if (!existsSync6(path)) return null;
|
|
4493
5010
|
try {
|
|
4494
|
-
const body =
|
|
5011
|
+
const body = readFileSync8(path, "utf8").trim();
|
|
4495
5012
|
return body.length > 0 ? body : null;
|
|
4496
5013
|
} catch {
|
|
4497
5014
|
return null;
|
|
@@ -4547,27 +5064,27 @@ function hasBinaryOnPath(bin) {
|
|
|
4547
5064
|
return found;
|
|
4548
5065
|
}
|
|
4549
5066
|
function scanSkillsDir(dir) {
|
|
4550
|
-
if (!
|
|
5067
|
+
if (!existsSync6(dir)) return [];
|
|
4551
5068
|
let entries;
|
|
4552
5069
|
try {
|
|
4553
|
-
entries =
|
|
5070
|
+
entries = readdirSync5(dir);
|
|
4554
5071
|
} catch {
|
|
4555
5072
|
return [];
|
|
4556
5073
|
}
|
|
4557
5074
|
const out = [];
|
|
4558
5075
|
for (const entry of entries) {
|
|
4559
|
-
const skillPath =
|
|
4560
|
-
if (!
|
|
5076
|
+
const skillPath = join9(dir, entry, "SKILL.md");
|
|
5077
|
+
if (!existsSync6(skillPath)) continue;
|
|
4561
5078
|
let st;
|
|
4562
5079
|
try {
|
|
4563
|
-
st =
|
|
5080
|
+
st = statSync2(skillPath);
|
|
4564
5081
|
} catch {
|
|
4565
5082
|
continue;
|
|
4566
5083
|
}
|
|
4567
5084
|
if (!st.isFile()) continue;
|
|
4568
5085
|
let body;
|
|
4569
5086
|
try {
|
|
4570
|
-
body =
|
|
5087
|
+
body = readFileSync8(skillPath, "utf8");
|
|
4571
5088
|
} catch {
|
|
4572
5089
|
continue;
|
|
4573
5090
|
}
|
|
@@ -4584,7 +5101,7 @@ function scanSkillsDir(dir) {
|
|
|
4584
5101
|
return out;
|
|
4585
5102
|
}
|
|
4586
5103
|
function defaultSkillsDir() {
|
|
4587
|
-
const here =
|
|
5104
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4588
5105
|
return resolve3(here, "..", "default-skills");
|
|
4589
5106
|
}
|
|
4590
5107
|
function composeSkills(home, enabledTools) {
|
|
@@ -4631,7 +5148,7 @@ function formatSkillsBlock(skills) {
|
|
|
4631
5148
|
return lines.join("\n");
|
|
4632
5149
|
}
|
|
4633
5150
|
function composeSystemPrompt(input) {
|
|
4634
|
-
const home = input.home ??
|
|
5151
|
+
const home = input.home ?? homedir9();
|
|
4635
5152
|
const parts = [];
|
|
4636
5153
|
const defaultPersona = readDefaultPersona();
|
|
4637
5154
|
if (defaultPersona) parts.push(defaultPersona);
|
|
@@ -4648,13 +5165,13 @@ var _defaultPersonaCache;
|
|
|
4648
5165
|
function readDefaultPersona() {
|
|
4649
5166
|
if (_defaultPersonaCache !== void 0) return _defaultPersonaCache;
|
|
4650
5167
|
try {
|
|
4651
|
-
const here =
|
|
5168
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4652
5169
|
const path = resolve3(here, "..", "default-persona.md");
|
|
4653
|
-
if (!
|
|
5170
|
+
if (!existsSync6(path)) {
|
|
4654
5171
|
_defaultPersonaCache = null;
|
|
4655
5172
|
return null;
|
|
4656
5173
|
}
|
|
4657
|
-
const raw =
|
|
5174
|
+
const raw = readFileSync8(path, "utf8").trim();
|
|
4658
5175
|
_defaultPersonaCache = raw.length > 0 ? raw : null;
|
|
4659
5176
|
return _defaultPersonaCache;
|
|
4660
5177
|
} catch {
|
|
@@ -4773,6 +5290,7 @@ var ThreadSession = class {
|
|
|
4773
5290
|
}
|
|
4774
5291
|
};
|
|
4775
5292
|
const { systemPrompt, tools } = this.deps.resolveConfig();
|
|
5293
|
+
const runtimeConfig = this.deps.refreshRuntimeConfig ? await this.deps.refreshRuntimeConfig() : this.deps.runtimeConfig;
|
|
4776
5294
|
await this.backfillHistoryOnce(replyToMessageId, body);
|
|
4777
5295
|
let sawActivity = false;
|
|
4778
5296
|
let turnSettled = false;
|
|
@@ -4788,7 +5306,7 @@ var ThreadSession = class {
|
|
|
4788
5306
|
}, NO_ACTIVITY_TIMEOUT_MS);
|
|
4789
5307
|
try {
|
|
4790
5308
|
const result = await runLoop({
|
|
4791
|
-
config:
|
|
5309
|
+
config: runtimeConfig,
|
|
4792
5310
|
systemPrompt,
|
|
4793
5311
|
userMessage: body,
|
|
4794
5312
|
tools: taskTools(tools),
|
|
@@ -4911,21 +5429,392 @@ var ThreadSession = class {
|
|
|
4911
5429
|
}
|
|
4912
5430
|
};
|
|
4913
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
|
+
|
|
4914
5777
|
// src/bridge.ts
|
|
4915
|
-
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
|
+
}
|
|
4916
5805
|
function resolveSystemPrompt(envFallback) {
|
|
4917
|
-
if (!
|
|
5806
|
+
if (!existsSync7(AGENT_CONFIG_PATH2)) return envFallback;
|
|
4918
5807
|
try {
|
|
4919
|
-
const parsed = JSON.parse(
|
|
5808
|
+
const parsed = JSON.parse(readFileSync9(AGENT_CONFIG_PATH2, "utf8"));
|
|
4920
5809
|
return typeof parsed.systemPrompt === "string" ? parsed.systemPrompt : envFallback;
|
|
4921
5810
|
} catch {
|
|
4922
5811
|
return envFallback;
|
|
4923
5812
|
}
|
|
4924
5813
|
}
|
|
4925
5814
|
function resolveTools(envFallback) {
|
|
4926
|
-
if (
|
|
5815
|
+
if (existsSync7(AGENT_CONFIG_PATH2)) {
|
|
4927
5816
|
try {
|
|
4928
|
-
const parsed = JSON.parse(
|
|
5817
|
+
const parsed = JSON.parse(readFileSync9(AGENT_CONFIG_PATH2, "utf8"));
|
|
4929
5818
|
if (Array.isArray(parsed.tools)) {
|
|
4930
5819
|
return parsed.tools.filter((t2) => typeof t2 === "string");
|
|
4931
5820
|
}
|
|
@@ -4934,35 +5823,10 @@ function resolveTools(envFallback) {
|
|
|
4934
5823
|
}
|
|
4935
5824
|
return envFallback;
|
|
4936
5825
|
}
|
|
4937
|
-
var DEFAULT_ENDPOINT = "https://troop.openape.ai";
|
|
4938
|
-
var DEFAULT_APES_BIN = "apes";
|
|
4939
|
-
var DEFAULT_MAX_STEPS = 10;
|
|
4940
|
-
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.`;
|
|
4941
5826
|
var PING_INTERVAL_MS = 3e4;
|
|
4942
5827
|
var RECONNECT_BASE_MS = 1e3;
|
|
4943
5828
|
var RECONNECT_MAX_MS = 3e4;
|
|
4944
5829
|
var ALLOWLIST_POLL_INTERVAL_MS = 3e4;
|
|
4945
|
-
function readConfig() {
|
|
4946
|
-
const toolsRaw = process3.env.APE_CHAT_BRIDGE_TOOLS ?? "";
|
|
4947
|
-
const tools = toolsRaw.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
4948
|
-
const maxStepsRaw = process3.env.APE_CHAT_BRIDGE_MAX_STEPS;
|
|
4949
|
-
const maxSteps = maxStepsRaw ? Number.parseInt(maxStepsRaw, 10) : DEFAULT_MAX_STEPS;
|
|
4950
|
-
const model = process3.env.APE_CHAT_BRIDGE_MODEL;
|
|
4951
|
-
if (!model) {
|
|
4952
|
-
throw new Error(
|
|
4953
|
-
"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)."
|
|
4954
|
-
);
|
|
4955
|
-
}
|
|
4956
|
-
return {
|
|
4957
|
-
endpoint: (process3.env.OPENAPE_TROOP_URL ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
|
|
4958
|
-
apesBin: process3.env.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
|
|
4959
|
-
model,
|
|
4960
|
-
systemPrompt: process3.env.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
|
|
4961
|
-
tools,
|
|
4962
|
-
maxSteps: Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : DEFAULT_MAX_STEPS,
|
|
4963
|
-
roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM
|
|
4964
|
-
};
|
|
4965
|
-
}
|
|
4966
5830
|
async function getIdentity() {
|
|
4967
5831
|
const idp = await ensureFreshIdpAuth();
|
|
4968
5832
|
const claims = decodeJwt(idp.access_token);
|
|
@@ -4975,23 +5839,18 @@ function log(line) {
|
|
|
4975
5839
|
process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
4976
5840
|
`);
|
|
4977
5841
|
}
|
|
4978
|
-
function
|
|
5842
|
+
function sleep2(ms) {
|
|
4979
5843
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
4980
5844
|
}
|
|
4981
5845
|
function truncate(s2, n2) {
|
|
4982
5846
|
return s2.length <= n2 ? s2 : `${s2.slice(0, n2 - 1)}\u2026`;
|
|
4983
5847
|
}
|
|
4984
|
-
function refusalText(reason) {
|
|
4985
|
-
const base = "I won't process this message \u2014 it looks like a prompt-injection attempt.";
|
|
4986
|
-
return reason ? `${base}
|
|
4987
|
-
|
|
4988
|
-
(matched: ${reason})` : base;
|
|
4989
|
-
}
|
|
4990
5848
|
var Bridge = class {
|
|
4991
|
-
constructor(cfg, selfEmail, ownerEmail) {
|
|
5849
|
+
constructor(cfg, selfEmail, ownerEmail, session) {
|
|
4992
5850
|
this.cfg = cfg;
|
|
4993
5851
|
this.selfEmail = selfEmail;
|
|
4994
5852
|
this.ownerEmail = ownerEmail;
|
|
5853
|
+
this.session = session;
|
|
4995
5854
|
this.bearer = async () => {
|
|
4996
5855
|
const idp = await ensureFreshIdpAuth();
|
|
4997
5856
|
return `Bearer ${idp.access_token}`;
|
|
@@ -4999,6 +5858,10 @@ var Bridge = class {
|
|
|
4999
5858
|
this.chat = new TroopChatApi(this.cfg.endpoint, this.bearer);
|
|
5000
5859
|
this.cron = new CronRunner({
|
|
5001
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),
|
|
5002
5865
|
chat: this.chat,
|
|
5003
5866
|
ownerEmail: this.ownerEmail,
|
|
5004
5867
|
log,
|
|
@@ -5006,10 +5869,13 @@ var Bridge = class {
|
|
|
5006
5869
|
bearer: this.bearer
|
|
5007
5870
|
});
|
|
5008
5871
|
this.cron.start();
|
|
5872
|
+
void this.refreshLlmGatewayKey();
|
|
5873
|
+
setInterval(() => void this.refreshLlmGatewayKey(), 40 * 60 * 1e3);
|
|
5009
5874
|
}
|
|
5010
5875
|
cfg;
|
|
5011
5876
|
selfEmail;
|
|
5012
5877
|
ownerEmail;
|
|
5878
|
+
session;
|
|
5013
5879
|
// Sessions keyed by `${roomId}:${threadId}`. Each ThreadSession holds
|
|
5014
5880
|
// its own message history and calls @openape/apes' runLoop directly
|
|
5015
5881
|
// (no stdio JSON-RPC subprocess — see thread-session.ts).
|
|
@@ -5021,6 +5887,25 @@ var Bridge = class {
|
|
|
5021
5887
|
// backend later. The bridge is the choke-point for every chat message
|
|
5022
5888
|
// before it reaches the agent runtime, so this is the right place.
|
|
5023
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
|
+
}
|
|
5024
5909
|
/**
|
|
5025
5910
|
* RuntimeConfig is shared across thread sessions and the cron runner.
|
|
5026
5911
|
* The bridge resolves it from its own env at boot and reuses for the
|
|
@@ -5028,11 +5913,38 @@ var Bridge = class {
|
|
|
5028
5913
|
*/
|
|
5029
5914
|
runtimeConfig() {
|
|
5030
5915
|
const apiBase = (process3.env.LITELLM_BASE_URL ?? "http://127.0.0.1:4000/v1").replace(/\/$/, "");
|
|
5031
|
-
const apiKey =
|
|
5916
|
+
const apiKey = this.llmKey;
|
|
5032
5917
|
if (!apiKey) {
|
|
5033
5918
|
throw new Error("LITELLM_API_KEY (or LITELLM_MASTER_KEY) must be set in the bridge env.");
|
|
5034
5919
|
}
|
|
5035
|
-
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
|
+
});
|
|
5036
5948
|
}
|
|
5037
5949
|
async sendInitialOwnerRequestIfNeeded() {
|
|
5038
5950
|
const contacts = await this.chat.listContacts();
|
|
@@ -5094,7 +6006,13 @@ var Bridge = class {
|
|
|
5094
6006
|
editedAt: typeof payload.editedAt === "number" ? payload.editedAt : null
|
|
5095
6007
|
};
|
|
5096
6008
|
}
|
|
5097
|
-
|
|
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) {
|
|
5098
6016
|
if (msg.senderEmail === this.selfEmail) return;
|
|
5099
6017
|
if (!msg.body.trim()) return;
|
|
5100
6018
|
if (this.cfg.roomFilter && msg.roomId !== this.cfg.roomFilter) return;
|
|
@@ -5113,7 +6031,7 @@ var Bridge = class {
|
|
|
5113
6031
|
if (decision.blocked) {
|
|
5114
6032
|
log(`[${msg.roomId}/${msg.threadId.slice(0, 8)}] BLOCKED prompt-injection (score=${decision.score.toFixed(2)}, reason=${decision.reason ?? "n/a"})`);
|
|
5115
6033
|
try {
|
|
5116
|
-
await
|
|
6034
|
+
await backend.postMessage(msg.roomId, this.session.refusalText(decision.reason), {
|
|
5117
6035
|
replyTo: msg.id,
|
|
5118
6036
|
threadId: msg.threadId
|
|
5119
6037
|
});
|
|
@@ -5123,18 +6041,21 @@ var Bridge = class {
|
|
|
5123
6041
|
}
|
|
5124
6042
|
return;
|
|
5125
6043
|
}
|
|
5126
|
-
const session = this.getOrCreateThread(msg.roomId, msg.threadId);
|
|
6044
|
+
const session = this.getOrCreateThread(msg.roomId, msg.threadId, backend);
|
|
5127
6045
|
session.enqueue(msg.body, msg.id);
|
|
5128
6046
|
}
|
|
5129
|
-
getOrCreateThread(roomId, threadId) {
|
|
6047
|
+
getOrCreateThread(roomId, threadId, backend) {
|
|
5130
6048
|
const key = `${roomId}:${threadId}`;
|
|
5131
6049
|
let s2 = this.threads.get(key);
|
|
5132
6050
|
if (s2) return s2;
|
|
5133
6051
|
s2 = new ThreadSession({
|
|
5134
6052
|
roomId,
|
|
5135
6053
|
threadId,
|
|
5136
|
-
chat:
|
|
6054
|
+
chat: backend,
|
|
5137
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(),
|
|
5138
6059
|
// Resolve tools + systemPrompt on every turn from agent.json
|
|
5139
6060
|
// (latest sync from troop). Owner edits in the troop UI thus
|
|
5140
6061
|
// take effect on the very next message in an existing thread —
|
|
@@ -5190,7 +6111,7 @@ var Bridge = class {
|
|
|
5190
6111
|
}
|
|
5191
6112
|
if (frame.type !== "message" || !frame.payload) return;
|
|
5192
6113
|
const msg = this.translateTroopPayload(frame.chat_id ?? "", frame.payload);
|
|
5193
|
-
void this.handleInbound(msg);
|
|
6114
|
+
void this.handleInbound(msg, this.chat);
|
|
5194
6115
|
});
|
|
5195
6116
|
ws.on("close", () => {
|
|
5196
6117
|
if (pingTimer) clearInterval(pingTimer);
|
|
@@ -5206,7 +6127,14 @@ var Bridge = class {
|
|
|
5206
6127
|
}
|
|
5207
6128
|
};
|
|
5208
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
|
+
}
|
|
5209
6135
|
const cfg = readConfig();
|
|
6136
|
+
addReadRoot(defaultSkillsDir());
|
|
6137
|
+
ensureMemoryFile();
|
|
5210
6138
|
const idpId = await getIdentity();
|
|
5211
6139
|
const onDisk = readAgentIdentity();
|
|
5212
6140
|
if (onDisk.email.toLowerCase() !== idpId.email.toLowerCase()) {
|
|
@@ -5214,10 +6142,12 @@ async function main() {
|
|
|
5214
6142
|
`auth.json email (${onDisk.email}) doesn't match IdP token sub (${idpId.email}) \u2014 refusing to start`
|
|
5215
6143
|
);
|
|
5216
6144
|
}
|
|
6145
|
+
const session = new AgentSession(onDisk.email, onDisk.ownerEmail, cfg);
|
|
5217
6146
|
log(
|
|
5218
|
-
`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 ?? "*"}`
|
|
5219
6148
|
);
|
|
5220
|
-
const bridge = new Bridge(cfg, onDisk.email, onDisk.ownerEmail);
|
|
6149
|
+
const bridge = new Bridge(cfg, onDisk.email, onDisk.ownerEmail, session);
|
|
6150
|
+
bridge.startExtraChannels();
|
|
5221
6151
|
let attempt = 0;
|
|
5222
6152
|
while (true) {
|
|
5223
6153
|
try {
|
|
@@ -5228,7 +6158,7 @@ async function main() {
|
|
|
5228
6158
|
attempt++;
|
|
5229
6159
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5230
6160
|
log(`disconnected (${msg}) \u2014 reconnecting in ${Math.round(delay / 1e3)}s`);
|
|
5231
|
-
await
|
|
6161
|
+
await sleep2(delay);
|
|
5232
6162
|
}
|
|
5233
6163
|
}
|
|
5234
6164
|
}
|