@openape/ape-agent 2.6.3 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.mjs +548 -79
- package/package.json +6 -5
package/dist/bridge.mjs
CHANGED
|
@@ -333,7 +333,7 @@ async function prompt(message, opts = {}) {
|
|
|
333
333
|
}
|
|
334
334
|
throw new Error(`Unknown prompt type: ${opts.type}`);
|
|
335
335
|
}
|
|
336
|
-
var src, hasRequiredSrc, srcExports, picocolors, hasRequiredPicocolors, picocolorsExports, e, Q, P$1, X, DD, uD, FD, m, L$1, N, I, r, tD, eD, iD, v, CD, w$1, W$1, rD, R, y, V$1, z, ED, _, nD, oD, aD, c, S, AD, pD, h, x, fD, bD, mD, Y, wD, SD, $D,
|
|
336
|
+
var src, hasRequiredSrc, srcExports, picocolors, hasRequiredPicocolors, picocolorsExports, e, Q, P$1, X, DD, uD, FD, m, L$1, N, I, r, tD, eD, iD, v, CD, w$1, W$1, rD, R, y, V$1, z, ED, _, nD, oD, aD, c, S, AD, pD, h, x, fD, bD, mD, Y, wD, SD, $D, q2, jD, PD, V, u, le, L, W, C, o, d, k, P, A, T, F, w, B, he, ye, ve, fe, kCancel;
|
|
337
337
|
var init_prompt = __esm({
|
|
338
338
|
"../../node_modules/.pnpm/consola@3.4.2/node_modules/consola/dist/chunks/prompt.mjs"() {
|
|
339
339
|
"use strict";
|
|
@@ -609,10 +609,10 @@ var init_prompt = __esm({
|
|
|
609
609
|
};
|
|
610
610
|
SD = Object.defineProperty;
|
|
611
611
|
$D = (t2, u3, F3) => u3 in t2 ? SD(t2, u3, { enumerable: true, configurable: true, writable: true, value: F3 }) : t2[u3] = F3;
|
|
612
|
-
|
|
612
|
+
q2 = (t2, u3, F3) => ($D(t2, typeof u3 != "symbol" ? u3 + "" : u3, F3), F3);
|
|
613
613
|
jD = class extends x {
|
|
614
614
|
constructor(u3) {
|
|
615
|
-
super(u3, false),
|
|
615
|
+
super(u3, false), q2(this, "options"), q2(this, "cursor", 0), this.options = u3.options, this.cursor = this.options.findIndex(({ value: F3 }) => F3 === u3.initialValue), this.cursor === -1 && (this.cursor = 0), this.changeValue(), this.on("cursor", (F3) => {
|
|
616
616
|
switch (F3) {
|
|
617
617
|
case "left":
|
|
618
618
|
case "up":
|
|
@@ -1036,7 +1036,7 @@ var require_shell_quote = __commonJS({
|
|
|
1036
1036
|
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
1037
1037
|
import { homedir as homedir9 } from "os";
|
|
1038
1038
|
import { join as join9 } from "path";
|
|
1039
|
-
import
|
|
1039
|
+
import process3 from "process";
|
|
1040
1040
|
|
|
1041
1041
|
// ../../packages/cli-auth/dist/index.js
|
|
1042
1042
|
import { ofetch } from "ofetch";
|
|
@@ -1333,6 +1333,66 @@ async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
|
|
|
1333
1333
|
return next;
|
|
1334
1334
|
}
|
|
1335
1335
|
|
|
1336
|
+
// ../../packages/prompt-injection-detector/dist/index.js
|
|
1337
|
+
var DEFAULT_THRESHOLD = 0.7;
|
|
1338
|
+
var DEFAULT_OWNER_THRESHOLD = 0.95;
|
|
1339
|
+
async function decide(detector, input, opts = {}) {
|
|
1340
|
+
const threshold = input.sender.isOwner ? opts.ownerThreshold ?? DEFAULT_OWNER_THRESHOLD : opts.threshold ?? DEFAULT_THRESHOLD;
|
|
1341
|
+
const result = await detector.classify(input);
|
|
1342
|
+
return {
|
|
1343
|
+
...result,
|
|
1344
|
+
threshold,
|
|
1345
|
+
blocked: result.score >= threshold
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
var PATTERNS = [
|
|
1349
|
+
// Instruction-override family. The defining phrase of prompt
|
|
1350
|
+
// injection — telling the model to discard its instructions in
|
|
1351
|
+
// favour of new ones.
|
|
1352
|
+
{ re: /\bignore (?:all |any |the |your )?(?:previous|prior|above|earlier|preceding) (?:instructions?|rules?|context|prompts?|messages?)\b/i, weight: 0.6, reason: "instruction-override" },
|
|
1353
|
+
{ re: /\bdisregard (?:all |any |the |your )?(?:previous|prior|above|earlier|preceding)?\s*(?:instructions?|rules?|context)\b/i, weight: 0.6, reason: "instruction-override" },
|
|
1354
|
+
{ re: /\b(?:you are|act as|pretend to be|roleplay as) (?:now |a |an )?(?:different|new|unrestricted|jailbroken|dan|do anything now)\b/i, weight: 0.55, reason: "role-override" },
|
|
1355
|
+
{ re: /\b(?:forget|drop|reset) (?:everything|all|your) (?:above|prior|previous|instructions?|rules?|context)\b/i, weight: 0.55, reason: "context-reset" },
|
|
1356
|
+
// Filesystem-exfiltration. Specific paths that have no business
|
|
1357
|
+
// appearing in normal chat — auth tokens, SSH keys, agent config.
|
|
1358
|
+
// `\b` would fail on `/etc/passwd` (slash is non-word, no boundary
|
|
1359
|
+
// with preceding space) — match the literal forms instead.
|
|
1360
|
+
{ re: /(?:~\/\.config\/apes|~\/\.openape|~\/\.ssh|\/etc\/passwd|\/etc\/shadow|\bid_rsa\b|\bid_ed25519\b|\bauth\.json\b|\.env(?:\.[\w-]+)?\b)/i, weight: 0.45, reason: "sensitive-path" },
|
|
1361
|
+
// Tool-call coercion. Phrases that try to talk the agent into
|
|
1362
|
+
// executing tools or running shell commands as part of the reply.
|
|
1363
|
+
{ re: /\b(?:run|execute|invoke|call)\s+(?:the\s+)?(?:shell|bash|sh|cmd|powershell|tool|command|script)\b/i, weight: 0.35, reason: "tool-coercion" },
|
|
1364
|
+
{ re: /\b(?:and\s+)?(?:post|send|share|paste|return|reply with|output)\s+(?:the\s+)?(?:contents?|output|result|file|secret|token|api[-_ ]?key)\b/i, weight: 0.3, reason: "exfil-request" },
|
|
1365
|
+
// Override + override-and-do (combined "do X without telling Y" forms).
|
|
1366
|
+
{ re: /\bwithout (?:telling|asking|informing|notifying|consulting|the consent of)\b/i, weight: 0.4, reason: "covert-action" },
|
|
1367
|
+
// System-prompt extraction.
|
|
1368
|
+
{ re: /\b(?:show|print|reveal|repeat|tell me|what is|what's) (?:your |the )?(?:system prompt|initial prompt|instructions|rules|directives|guidelines)\b/i, weight: 0.5, reason: "prompt-extraction" },
|
|
1369
|
+
// Encoding-based bypass attempts.
|
|
1370
|
+
{ re: /\b(?:base64|rot13|decode|decrypt) (?:this|the following|below)\b/i, weight: 0.3, reason: "encoding-bypass" }
|
|
1371
|
+
];
|
|
1372
|
+
function classifyHeuristic(input) {
|
|
1373
|
+
const text = input.text;
|
|
1374
|
+
let total = 0;
|
|
1375
|
+
const reasons = [];
|
|
1376
|
+
for (const p of PATTERNS) {
|
|
1377
|
+
if (p.re.test(text)) {
|
|
1378
|
+
total += p.weight;
|
|
1379
|
+
if (!reasons.includes(p.reason)) reasons.push(p.reason);
|
|
1380
|
+
if (total >= 1) break;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const score = Math.min(1, total);
|
|
1384
|
+
return {
|
|
1385
|
+
score,
|
|
1386
|
+
backend: "heuristic",
|
|
1387
|
+
...reasons.length > 0 ? { reason: reasons.join(", ") } : {}
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
function createHeuristicDetector() {
|
|
1391
|
+
return {
|
|
1392
|
+
classify: async (input) => classifyHeuristic(input)
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1336
1396
|
// src/bridge.ts
|
|
1337
1397
|
import { decodeJwt } from "jose";
|
|
1338
1398
|
import WebSocket from "ws";
|
|
@@ -1450,11 +1510,14 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as read
|
|
|
1450
1510
|
import { homedir as homedir6 } from "os";
|
|
1451
1511
|
import { join as join6 } from "path";
|
|
1452
1512
|
|
|
1453
|
-
// ../../packages/apes/dist/chunk-
|
|
1513
|
+
// ../../packages/apes/dist/chunk-VHZEVW2N.js
|
|
1454
1514
|
import { spawn } from "child_process";
|
|
1455
1515
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1456
1516
|
import { homedir as homedir3 } from "os";
|
|
1457
1517
|
import { dirname, normalize, resolve } from "path";
|
|
1518
|
+
import { homedir as homedir22 } from "os";
|
|
1519
|
+
import { resolve as resolve2 } from "path";
|
|
1520
|
+
import process2 from "process";
|
|
1458
1521
|
import { execFileSync } from "child_process";
|
|
1459
1522
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1460
1523
|
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -1466,6 +1529,57 @@ function capStdio(s2) {
|
|
|
1466
1529
|
return `${buf.subarray(0, MAX_STDIO_BYTES).toString("utf8")}
|
|
1467
1530
|
[truncated to ${MAX_STDIO_BYTES} bytes]`;
|
|
1468
1531
|
}
|
|
1532
|
+
function runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1533
|
+
return new Promise((resolveResult) => {
|
|
1534
|
+
const child = spawn(BIN, ["-c", cmd], {
|
|
1535
|
+
env: { ...process.env, APE_WAIT: "1" },
|
|
1536
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1537
|
+
});
|
|
1538
|
+
let stdout2 = "";
|
|
1539
|
+
let stderr = "";
|
|
1540
|
+
let timedOut = false;
|
|
1541
|
+
let spawnError = null;
|
|
1542
|
+
child.stdout.on("data", (chunk) => {
|
|
1543
|
+
stdout2 += chunk.toString("utf8");
|
|
1544
|
+
});
|
|
1545
|
+
child.stderr.on("data", (chunk) => {
|
|
1546
|
+
stderr += chunk.toString("utf8");
|
|
1547
|
+
});
|
|
1548
|
+
child.on("error", (err) => {
|
|
1549
|
+
spawnError = err;
|
|
1550
|
+
});
|
|
1551
|
+
const timer = setTimeout(() => {
|
|
1552
|
+
timedOut = true;
|
|
1553
|
+
child.kill("SIGTERM");
|
|
1554
|
+
setTimeout(() => {
|
|
1555
|
+
try {
|
|
1556
|
+
child.kill("SIGKILL");
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
}, 5e3);
|
|
1560
|
+
}, timeoutMs);
|
|
1561
|
+
child.on("close", (code) => {
|
|
1562
|
+
clearTimeout(timer);
|
|
1563
|
+
if (spawnError) {
|
|
1564
|
+
resolveResult({
|
|
1565
|
+
stdout: "",
|
|
1566
|
+
stderr: "",
|
|
1567
|
+
exit_code: -1,
|
|
1568
|
+
error: spawnError.message,
|
|
1569
|
+
hint: `Could not exec '${BIN}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH.`
|
|
1570
|
+
});
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
resolveResult({
|
|
1574
|
+
stdout: capStdio(stdout2),
|
|
1575
|
+
stderr: capStdio(stderr),
|
|
1576
|
+
exit_code: code ?? -1,
|
|
1577
|
+
...timedOut ? { timed_out: true } : {}
|
|
1578
|
+
});
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
var DEFAULTS = { DEFAULT_TIMEOUT_MS };
|
|
1469
1583
|
var bashTools = [
|
|
1470
1584
|
{
|
|
1471
1585
|
name: "bash",
|
|
@@ -1489,52 +1603,8 @@ var bashTools = [
|
|
|
1489
1603
|
if (typeof a2.cmd !== "string" || a2.cmd.trim() === "") {
|
|
1490
1604
|
throw new Error("cmd must be a non-empty string");
|
|
1491
1605
|
}
|
|
1492
|
-
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULT_TIMEOUT_MS;
|
|
1493
|
-
return await
|
|
1494
|
-
const child = spawn(BIN, ["-c", a2.cmd], {
|
|
1495
|
-
env: { ...process.env, APE_WAIT: "1" },
|
|
1496
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1497
|
-
});
|
|
1498
|
-
let stdout2 = "";
|
|
1499
|
-
let stderr = "";
|
|
1500
|
-
let timedOut = false;
|
|
1501
|
-
let spawnError = null;
|
|
1502
|
-
child.stdout.on("data", (chunk) => {
|
|
1503
|
-
stdout2 += chunk.toString("utf8");
|
|
1504
|
-
});
|
|
1505
|
-
child.stderr.on("data", (chunk) => {
|
|
1506
|
-
stderr += chunk.toString("utf8");
|
|
1507
|
-
});
|
|
1508
|
-
child.on("error", (err) => {
|
|
1509
|
-
spawnError = err;
|
|
1510
|
-
});
|
|
1511
|
-
const timer = setTimeout(() => {
|
|
1512
|
-
timedOut = true;
|
|
1513
|
-
child.kill("SIGTERM");
|
|
1514
|
-
setTimeout(() => {
|
|
1515
|
-
try {
|
|
1516
|
-
child.kill("SIGKILL");
|
|
1517
|
-
} catch {
|
|
1518
|
-
}
|
|
1519
|
-
}, 5e3);
|
|
1520
|
-
}, timeout);
|
|
1521
|
-
child.on("close", (code) => {
|
|
1522
|
-
clearTimeout(timer);
|
|
1523
|
-
if (spawnError) {
|
|
1524
|
-
resolveResult({
|
|
1525
|
-
error: spawnError.message,
|
|
1526
|
-
hint: `Could not exec '${BIN}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH.`
|
|
1527
|
-
});
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
resolveResult({
|
|
1531
|
-
stdout: capStdio(stdout2),
|
|
1532
|
-
stderr: capStdio(stderr),
|
|
1533
|
-
exit_code: code ?? -1,
|
|
1534
|
-
...timedOut ? { timed_out: true } : {}
|
|
1535
|
-
});
|
|
1536
|
-
});
|
|
1537
|
-
});
|
|
1606
|
+
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULTS.DEFAULT_TIMEOUT_MS;
|
|
1607
|
+
return await runApeShell(a2.cmd, timeout);
|
|
1538
1608
|
}
|
|
1539
1609
|
}
|
|
1540
1610
|
];
|
|
@@ -1593,6 +1663,337 @@ var fileTools = [
|
|
|
1593
1663
|
writeFileSync2(p, a2.content, { encoding: "utf8" });
|
|
1594
1664
|
return { path: p, bytes: Buffer.byteLength(a2.content, "utf8") };
|
|
1595
1665
|
}
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
name: "file.edit",
|
|
1669
|
+
description: "Replace an exact substring in a file under the agent's home directory. Prefer this over file.write for edits \u2014 it touches only the changed region instead of rewriting the whole file. `old_string` must appear exactly once unless `replace_all` is true. Path traversal blocked, 1MB max.",
|
|
1670
|
+
parameters: {
|
|
1671
|
+
type: "object",
|
|
1672
|
+
properties: {
|
|
1673
|
+
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME)." },
|
|
1674
|
+
old_string: { type: "string", description: "Exact text to replace. Include enough surrounding context to be unique unless replace_all is set." },
|
|
1675
|
+
new_string: { type: "string", description: "Replacement text. Must differ from old_string." },
|
|
1676
|
+
replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring a unique match. Default false." }
|
|
1677
|
+
},
|
|
1678
|
+
required: ["path", "old_string", "new_string"]
|
|
1679
|
+
},
|
|
1680
|
+
execute: async (args) => {
|
|
1681
|
+
const a2 = args;
|
|
1682
|
+
if (typeof a2.old_string !== "string" || a2.old_string === "") {
|
|
1683
|
+
throw new Error("old_string must be a non-empty string");
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof a2.new_string !== "string") {
|
|
1686
|
+
throw new TypeError("new_string must be a string");
|
|
1687
|
+
}
|
|
1688
|
+
if (a2.old_string === a2.new_string) {
|
|
1689
|
+
throw new Error("old_string and new_string are identical \u2014 nothing to change");
|
|
1690
|
+
}
|
|
1691
|
+
const replaceAll = a2.replace_all === true;
|
|
1692
|
+
const p = jailPath(a2.path);
|
|
1693
|
+
const before = readFileSync3(p, "utf8");
|
|
1694
|
+
const occurrences = before.split(a2.old_string).length - 1;
|
|
1695
|
+
if (occurrences === 0) {
|
|
1696
|
+
throw new Error("old_string not found in file");
|
|
1697
|
+
}
|
|
1698
|
+
if (occurrences > 1 && !replaceAll) {
|
|
1699
|
+
throw new Error(`old_string occurs ${occurrences} times \u2014 pass replace_all:true or add surrounding context to make it unique`);
|
|
1700
|
+
}
|
|
1701
|
+
const after = replaceAll ? before.split(a2.old_string).join(a2.new_string) : before.replace(a2.old_string, a2.new_string);
|
|
1702
|
+
if (Buffer.byteLength(after, "utf8") > MAX_BYTES) {
|
|
1703
|
+
throw new Error(`result exceeds ${MAX_BYTES} byte cap`);
|
|
1704
|
+
}
|
|
1705
|
+
writeFileSync2(p, after, { encoding: "utf8" });
|
|
1706
|
+
return { path: p, replacements: replaceAll ? occurrences : 1 };
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
];
|
|
1710
|
+
var BRANCH_RE = /^[\w./-]{1,200}$/;
|
|
1711
|
+
var ID_RE = /^\d{1,12}$/;
|
|
1712
|
+
function shq(s2) {
|
|
1713
|
+
return `'${String(s2).replace(/'/g, "'\\''")}'`;
|
|
1714
|
+
}
|
|
1715
|
+
function assertBranch(v2) {
|
|
1716
|
+
if (typeof v2 !== "string" || !BRANCH_RE.test(v2)) {
|
|
1717
|
+
throw new Error("branch must match ^[A-Za-z0-9._/-]{1,200}$");
|
|
1718
|
+
}
|
|
1719
|
+
return v2;
|
|
1720
|
+
}
|
|
1721
|
+
function assertId(v2) {
|
|
1722
|
+
if (typeof v2 !== "string" && typeof v2 !== "number") throw new Error("id required");
|
|
1723
|
+
const s2 = String(v2);
|
|
1724
|
+
if (!ID_RE.test(s2)) throw new Error("id must be a number");
|
|
1725
|
+
return s2;
|
|
1726
|
+
}
|
|
1727
|
+
var githubAdapter = {
|
|
1728
|
+
id: "github",
|
|
1729
|
+
matchesRemote: (url) => /github\.com/i.test(url),
|
|
1730
|
+
prCreate: (i2) => {
|
|
1731
|
+
const head = assertBranch(i2.head);
|
|
1732
|
+
const parts = ["gh", "pr", "create", "--title", shq(i2.title), "--body", shq(i2.body), "--head", shq(head)];
|
|
1733
|
+
if (i2.base !== void 0) parts.push("--base", shq(assertBranch(i2.base)));
|
|
1734
|
+
return parts.join(" ");
|
|
1735
|
+
},
|
|
1736
|
+
prMerge: (i2) => {
|
|
1737
|
+
const ref = String(i2.ref);
|
|
1738
|
+
const refTok = ID_RE.test(ref) ? ref : assertBranch(ref);
|
|
1739
|
+
const parts = ["gh", "pr", "merge", shq(refTok)];
|
|
1740
|
+
if (i2.squash === true) parts.push("--squash");
|
|
1741
|
+
if (i2.auto) parts.push("--auto");
|
|
1742
|
+
if (i2.deleteBranch) parts.push("--delete-branch");
|
|
1743
|
+
return parts.join(" ");
|
|
1744
|
+
},
|
|
1745
|
+
prStatus: (ref) => {
|
|
1746
|
+
const r3 = String(ref);
|
|
1747
|
+
const refTok = ID_RE.test(r3) ? r3 : assertBranch(r3);
|
|
1748
|
+
return `gh pr view ${shq(refTok)} --json state,mergeStateStatus,statusCheckRollup,reviewDecision`;
|
|
1749
|
+
},
|
|
1750
|
+
issueGet: (ref) => `gh issue view ${assertId(ref)} --json number,title,body,labels`
|
|
1751
|
+
};
|
|
1752
|
+
var azureAdapter = {
|
|
1753
|
+
id: "azure",
|
|
1754
|
+
matchesRemote: (url) => /dev\.azure\.com|visualstudio\.com/i.test(url),
|
|
1755
|
+
prCreate: (i2) => {
|
|
1756
|
+
const head = assertBranch(i2.head);
|
|
1757
|
+
const parts = ["az", "repos", "pr", "create", "--title", shq(i2.title), "--description", shq(i2.body), "--source-branch", shq(head)];
|
|
1758
|
+
if (i2.base !== void 0) parts.push("--target-branch", shq(assertBranch(i2.base)));
|
|
1759
|
+
return parts.join(" ");
|
|
1760
|
+
},
|
|
1761
|
+
prMerge: (i2) => {
|
|
1762
|
+
const id = assertId(i2.ref);
|
|
1763
|
+
const parts = ["az", "repos", "pr", "update", "--id", id];
|
|
1764
|
+
if (i2.auto) parts.push("--auto-complete", "true");
|
|
1765
|
+
else parts.push("--status", "completed");
|
|
1766
|
+
if (i2.squash === true) parts.push("--merge-commit-message-style", "squash");
|
|
1767
|
+
if (i2.deleteBranch) parts.push("--delete-source-branch", "true");
|
|
1768
|
+
return parts.join(" ");
|
|
1769
|
+
},
|
|
1770
|
+
prStatus: (ref) => `az repos pr show --id ${assertId(ref)}`,
|
|
1771
|
+
issueGet: (ref) => `az boards work-item show --id ${assertId(ref)}`
|
|
1772
|
+
};
|
|
1773
|
+
var registry = /* @__PURE__ */ new Map([
|
|
1774
|
+
[githubAdapter.id, githubAdapter],
|
|
1775
|
+
[azureAdapter.id, azureAdapter]
|
|
1776
|
+
]);
|
|
1777
|
+
function listForges() {
|
|
1778
|
+
return [...registry.keys()];
|
|
1779
|
+
}
|
|
1780
|
+
function getForge(id) {
|
|
1781
|
+
const a2 = registry.get(id);
|
|
1782
|
+
if (!a2) {
|
|
1783
|
+
throw new Error(`unknown forge '${id}'. Registered: ${listForges().join(", ")}. Add one with registerForge().`);
|
|
1784
|
+
}
|
|
1785
|
+
return a2;
|
|
1786
|
+
}
|
|
1787
|
+
function detectForge(remoteUrl) {
|
|
1788
|
+
if (typeof remoteUrl !== "string" || remoteUrl === "") {
|
|
1789
|
+
throw new Error("remote URL required to detect forge");
|
|
1790
|
+
}
|
|
1791
|
+
for (const a2 of registry.values()) {
|
|
1792
|
+
if (a2.matchesRemote(remoteUrl)) return a2.id;
|
|
1793
|
+
}
|
|
1794
|
+
throw new Error(`no forge adapter matches remote: ${remoteUrl}. Registered: ${listForges().join(", ")}. Register one with registerForge() (e.g. GitLab/Bitbucket/Gitea).`);
|
|
1795
|
+
}
|
|
1796
|
+
function buildPrCreate(input) {
|
|
1797
|
+
return getForge(input.forge).prCreate(input);
|
|
1798
|
+
}
|
|
1799
|
+
function buildPrMerge(input) {
|
|
1800
|
+
return getForge(input.forge).prMerge(input);
|
|
1801
|
+
}
|
|
1802
|
+
function buildPrStatus(forge, ref) {
|
|
1803
|
+
return getForge(forge).prStatus(ref);
|
|
1804
|
+
}
|
|
1805
|
+
function buildIssueGet(forge, ref) {
|
|
1806
|
+
return getForge(forge).issueGet(ref);
|
|
1807
|
+
}
|
|
1808
|
+
function resolveForge(a2) {
|
|
1809
|
+
if (typeof a2.forge === "string" && a2.forge !== "") return a2.forge;
|
|
1810
|
+
if (typeof a2.remote === "string") return detectForge(a2.remote);
|
|
1811
|
+
throw new Error("provide a forge id (e.g. github, azure, or a registered adapter) or a remote URL to detect it");
|
|
1812
|
+
}
|
|
1813
|
+
var forgeParam = { type: "string", description: "Target forge id (github, azure, or a registered adapter). Omit to auto-detect from `remote`." };
|
|
1814
|
+
var remoteParam = { type: "string", description: "git remote URL \u2014 used to auto-detect the forge when `forge` is omitted." };
|
|
1815
|
+
var forgeTools = [
|
|
1816
|
+
{
|
|
1817
|
+
name: "forge.pr.create",
|
|
1818
|
+
description: "Open a pull request on GitHub (gh) or Azure DevOps (az). Gated via the DDISA grant cycle. Provider chosen by `forge` or auto-detected from `remote`.",
|
|
1819
|
+
parameters: {
|
|
1820
|
+
type: "object",
|
|
1821
|
+
properties: {
|
|
1822
|
+
forge: forgeParam,
|
|
1823
|
+
remote: remoteParam,
|
|
1824
|
+
title: { type: "string", description: "PR title." },
|
|
1825
|
+
body: { type: "string", description: "PR description / body." },
|
|
1826
|
+
head: { type: "string", description: "Source branch." },
|
|
1827
|
+
base: { type: "string", description: "Target branch. Omit for the repo default." }
|
|
1828
|
+
},
|
|
1829
|
+
required: ["title", "body", "head"]
|
|
1830
|
+
},
|
|
1831
|
+
execute: async (args) => {
|
|
1832
|
+
const a2 = args;
|
|
1833
|
+
const cmd = buildPrCreate({ forge: resolveForge(a2), title: a2.title, body: a2.body, head: a2.head, base: a2.base });
|
|
1834
|
+
return await runApeShell(cmd);
|
|
1835
|
+
}
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
name: "forge.pr.merge",
|
|
1839
|
+
description: 'Merge a PR \u2014 or with auto=true, arm "merge when checks pass" (gh --auto / az auto-complete) so the platform merges only on green CI. Gated. Never bypasses required checks (branch protection is the server-side gate).',
|
|
1840
|
+
parameters: {
|
|
1841
|
+
type: "object",
|
|
1842
|
+
properties: {
|
|
1843
|
+
forge: forgeParam,
|
|
1844
|
+
remote: remoteParam,
|
|
1845
|
+
ref: { type: "string", description: "GitHub: PR number or branch. Azure: PR id." },
|
|
1846
|
+
auto: { type: "boolean", description: "Arm merge-when-green instead of immediate merge. Recommended." },
|
|
1847
|
+
squash: { type: "boolean", description: "Squash-merge. Default true." },
|
|
1848
|
+
delete_branch: { type: "boolean", description: "Delete the source branch after merge." }
|
|
1849
|
+
},
|
|
1850
|
+
required: ["ref"]
|
|
1851
|
+
},
|
|
1852
|
+
execute: async (args) => {
|
|
1853
|
+
const a2 = args;
|
|
1854
|
+
const cmd = buildPrMerge({ forge: resolveForge(a2), ref: a2.ref, auto: a2.auto, squash: a2.squash, deleteBranch: a2.delete_branch });
|
|
1855
|
+
return await runApeShell(cmd);
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
name: "forge.pr.status",
|
|
1860
|
+
description: "Fetch a PR's state + checks + review decision. Gated (read).",
|
|
1861
|
+
parameters: {
|
|
1862
|
+
type: "object",
|
|
1863
|
+
properties: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "PR number/branch (GitHub) or id (Azure)." } },
|
|
1864
|
+
required: ["ref"]
|
|
1865
|
+
},
|
|
1866
|
+
execute: async (args) => {
|
|
1867
|
+
const a2 = args;
|
|
1868
|
+
return await runApeShell(buildPrStatus(resolveForge(a2), a2.ref));
|
|
1869
|
+
}
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
name: "forge.issue.get",
|
|
1873
|
+
description: "Fetch an issue (GitHub) or work-item (Azure) \u2014 title, body, labels. Gated (read). Use to turn an assigned task into a coding run.",
|
|
1874
|
+
parameters: {
|
|
1875
|
+
type: "object",
|
|
1876
|
+
properties: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "Issue number (GitHub) or work-item id (Azure)." } },
|
|
1877
|
+
required: ["ref"]
|
|
1878
|
+
},
|
|
1879
|
+
execute: async (args) => {
|
|
1880
|
+
const a2 = args;
|
|
1881
|
+
return await runApeShell(buildIssueGet(resolveForge(a2), a2.ref));
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
];
|
|
1885
|
+
function jailedRoot(envVar, fallbackName) {
|
|
1886
|
+
const home = homedir22();
|
|
1887
|
+
const raw = process2.env[envVar];
|
|
1888
|
+
const dir = raw ? resolve2(raw) : resolve2(home, fallbackName);
|
|
1889
|
+
if (dir !== home && !dir.startsWith(`${home}/`)) {
|
|
1890
|
+
throw new Error(`${envVar} (${dir}) must resolve inside the agent's home`);
|
|
1891
|
+
}
|
|
1892
|
+
return dir;
|
|
1893
|
+
}
|
|
1894
|
+
function workRoot() {
|
|
1895
|
+
return jailedRoot("OPENAPE_CODING_WORK_DIR", "work");
|
|
1896
|
+
}
|
|
1897
|
+
function reposRoot() {
|
|
1898
|
+
return jailedRoot("OPENAPE_CODING_REPOS_DIR", "repos");
|
|
1899
|
+
}
|
|
1900
|
+
var TASK_ID_RE = /^[\w.-]{1,64}$/;
|
|
1901
|
+
var BRANCH_RE2 = /^[\w./-]{1,128}$/;
|
|
1902
|
+
var URL_RE = /^(?:https:\/\/|git@)[\w@:/.-]{3,256}$/;
|
|
1903
|
+
function assertTaskId(v2) {
|
|
1904
|
+
if (typeof v2 !== "string" || !TASK_ID_RE.test(v2)) {
|
|
1905
|
+
throw new Error("task_id must match ^[a-zA-Z0-9._-]{1,64}$");
|
|
1906
|
+
}
|
|
1907
|
+
return v2;
|
|
1908
|
+
}
|
|
1909
|
+
function assertBranch2(v2) {
|
|
1910
|
+
if (typeof v2 !== "string" || !BRANCH_RE2.test(v2)) {
|
|
1911
|
+
throw new Error("branch must match ^[A-Za-z0-9._/-]{1,128}$");
|
|
1912
|
+
}
|
|
1913
|
+
return v2;
|
|
1914
|
+
}
|
|
1915
|
+
function resolveRepo(repo) {
|
|
1916
|
+
if (typeof repo !== "string" || repo === "") {
|
|
1917
|
+
throw new Error("repo must be a non-empty string (URL or path under $HOME)");
|
|
1918
|
+
}
|
|
1919
|
+
const home = homedir22();
|
|
1920
|
+
if (URL_RE.test(repo)) {
|
|
1921
|
+
const tail = repo.replace(/\.git$/, "").replace(/[/:]+$/, "");
|
|
1922
|
+
const parts = tail.split(/[/:]/).filter(Boolean).slice(-2);
|
|
1923
|
+
const base = parts.join("-").replace(/[^\w.-]/g, "");
|
|
1924
|
+
if (!base) throw new Error("could not derive a clone name from repo URL");
|
|
1925
|
+
return { source: repo, baseDir: resolve2(reposRoot(), base), isUrl: true };
|
|
1926
|
+
}
|
|
1927
|
+
const candidate = repo.startsWith("~/") ? resolve2(home, repo.slice(2)) : resolve2(home, repo);
|
|
1928
|
+
if (candidate !== home && !candidate.startsWith(`${home}/`)) {
|
|
1929
|
+
throw new Error(`repo path "${repo}" resolves outside the agent's home`);
|
|
1930
|
+
}
|
|
1931
|
+
return { source: candidate, baseDir: candidate, isUrl: false };
|
|
1932
|
+
}
|
|
1933
|
+
function worktreePathFor(taskId) {
|
|
1934
|
+
return resolve2(workRoot(), assertTaskId(taskId));
|
|
1935
|
+
}
|
|
1936
|
+
var q = (s2) => `'${s2}'`;
|
|
1937
|
+
function buildCreateCommand(repo, taskId, branch) {
|
|
1938
|
+
const id = assertTaskId(taskId);
|
|
1939
|
+
const br = assertBranch2(branch);
|
|
1940
|
+
const { source, baseDir, isUrl } = resolveRepo(repo);
|
|
1941
|
+
const wt = worktreePathFor(id);
|
|
1942
|
+
const clone = isUrl ? `if [ ! -d ${q(baseDir)}/.git ]; then git clone ${q(source)} ${q(baseDir)}; fi` : `test -d ${q(baseDir)}/.git`;
|
|
1943
|
+
return [
|
|
1944
|
+
`mkdir -p ${q(reposRoot())} ${q(workRoot())}`,
|
|
1945
|
+
clone,
|
|
1946
|
+
`git -C ${q(baseDir)} fetch --quiet || true`,
|
|
1947
|
+
`git -C ${q(baseDir)} worktree add -b ${q(br)} ${q(wt)}`,
|
|
1948
|
+
`echo ${q(wt)}`
|
|
1949
|
+
].join(" && ");
|
|
1950
|
+
}
|
|
1951
|
+
function buildRemoveCommand(repo, taskId) {
|
|
1952
|
+
const id = assertTaskId(taskId);
|
|
1953
|
+
const { baseDir } = resolveRepo(repo);
|
|
1954
|
+
const wt = worktreePathFor(id);
|
|
1955
|
+
return `git -C ${q(baseDir)} worktree remove --force ${q(wt)} && git -C ${q(baseDir)} worktree prune`;
|
|
1956
|
+
}
|
|
1957
|
+
function buildListCommand() {
|
|
1958
|
+
return `ls -1 ${q(workRoot())} 2>/dev/null || true`;
|
|
1959
|
+
}
|
|
1960
|
+
var gitWorktreeTools = [
|
|
1961
|
+
{
|
|
1962
|
+
name: "git.worktree",
|
|
1963
|
+
description: "Manage isolated git worktrees for coding tasks. action=create clones the repo (cached under ~/repos) and adds a fresh worktree under ~/work/<task_id> on a new branch. action=remove tears it down. action=list shows current task worktrees. Git operations go through the DDISA grant cycle (git-shape).",
|
|
1964
|
+
parameters: {
|
|
1965
|
+
type: "object",
|
|
1966
|
+
properties: {
|
|
1967
|
+
action: { type: "string", enum: ["create", "remove", "list"], description: "create | remove | list" },
|
|
1968
|
+
repo: { type: "string", description: "For create/remove: git remote URL (https/git@) or a path under $HOME to an existing clone." },
|
|
1969
|
+
task_id: { type: "string", description: "For create/remove: identifier for the worktree, ^[a-zA-Z0-9._-]{1,64}$. The worktree lands at ~/work/<task_id>." },
|
|
1970
|
+
branch: { type: "string", description: "For create: new branch name, ^[A-Za-z0-9._/-]{1,128}$." }
|
|
1971
|
+
},
|
|
1972
|
+
required: ["action"]
|
|
1973
|
+
},
|
|
1974
|
+
execute: async (args) => {
|
|
1975
|
+
const a2 = args;
|
|
1976
|
+
let cmd;
|
|
1977
|
+
if (a2.action === "create") {
|
|
1978
|
+
if (typeof a2.branch !== "string") throw new Error("branch is required for action=create");
|
|
1979
|
+
cmd = buildCreateCommand(a2.repo, assertTaskId(a2.task_id), a2.branch);
|
|
1980
|
+
} else if (a2.action === "remove") {
|
|
1981
|
+
cmd = buildRemoveCommand(a2.repo, assertTaskId(a2.task_id));
|
|
1982
|
+
} else if (a2.action === "list") {
|
|
1983
|
+
cmd = buildListCommand();
|
|
1984
|
+
} else {
|
|
1985
|
+
throw new Error("action must be one of: create, remove, list");
|
|
1986
|
+
}
|
|
1987
|
+
const res = await runApeShell(cmd);
|
|
1988
|
+
return {
|
|
1989
|
+
action: a2.action,
|
|
1990
|
+
...a2.action !== "list" ? { worktree: worktreePathFor(assertTaskId(a2.task_id)) } : {},
|
|
1991
|
+
stdout: res.stdout,
|
|
1992
|
+
stderr: res.stderr,
|
|
1993
|
+
exit_code: res.exit_code,
|
|
1994
|
+
...res.error ? { error: res.error, hint: res.hint } : {}
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1596
1997
|
}
|
|
1597
1998
|
];
|
|
1598
1999
|
var MAX_BYTES2 = 1024 * 1024;
|
|
@@ -1823,13 +2224,53 @@ var timeTools = [
|
|
|
1823
2224
|
}
|
|
1824
2225
|
}
|
|
1825
2226
|
];
|
|
2227
|
+
var CWD_RE = /^[\w./-]{1,256}$/;
|
|
2228
|
+
async function runVerify(cwd, command, timeoutMs) {
|
|
2229
|
+
if (typeof cwd !== "string" || !CWD_RE.test(cwd)) {
|
|
2230
|
+
throw new Error("cwd must match ^[A-Za-z0-9._/-]{1,256}$");
|
|
2231
|
+
}
|
|
2232
|
+
if (typeof command !== "string" || command.trim() === "") {
|
|
2233
|
+
throw new Error("verify command must be a non-empty string");
|
|
2234
|
+
}
|
|
2235
|
+
const res = await runApeShell(`cd '${cwd}' && ${command}`, timeoutMs);
|
|
2236
|
+
return {
|
|
2237
|
+
passed: res.exit_code === 0,
|
|
2238
|
+
exit_code: res.exit_code,
|
|
2239
|
+
stdout: res.stdout,
|
|
2240
|
+
stderr: res.stderr,
|
|
2241
|
+
...res.timed_out ? { timed_out: true } : {}
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
var verifyTools = [
|
|
2245
|
+
{
|
|
2246
|
+
name: "verify",
|
|
2247
|
+
description: "Run the verification command (tests/build/lint) in a worktree and report pass/fail. The coding loop must NOT open or merge a PR when this fails. Runs through the DDISA grant cycle (same as bash). Returns { passed, exit_code, stdout, stderr }.",
|
|
2248
|
+
parameters: {
|
|
2249
|
+
type: "object",
|
|
2250
|
+
properties: {
|
|
2251
|
+
cwd: { type: "string", description: "Worktree path to run in (e.g. ~/work/issue-42)." },
|
|
2252
|
+
command: { type: "string", description: "Verification command, e.g. `pnpm test` or `npm run build && npm test`." },
|
|
2253
|
+
timeout_ms: { type: "number", description: "Wall-clock cap incl. approval wait. Default 300000." }
|
|
2254
|
+
},
|
|
2255
|
+
required: ["cwd", "command"]
|
|
2256
|
+
},
|
|
2257
|
+
execute: async (args) => {
|
|
2258
|
+
const a2 = args;
|
|
2259
|
+
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : void 0;
|
|
2260
|
+
return await runVerify(a2.cwd, a2.command, timeout);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
];
|
|
1826
2264
|
var ALL_TOOLS = [
|
|
1827
2265
|
...timeTools,
|
|
1828
2266
|
...httpTools,
|
|
1829
2267
|
...fileTools,
|
|
1830
2268
|
...tasksTools,
|
|
1831
2269
|
...mailTools,
|
|
1832
|
-
...bashTools
|
|
2270
|
+
...bashTools,
|
|
2271
|
+
...gitWorktreeTools,
|
|
2272
|
+
...verifyTools,
|
|
2273
|
+
...forgeTools
|
|
1833
2274
|
];
|
|
1834
2275
|
var TOOLS = Object.fromEntries(
|
|
1835
2276
|
ALL_TOOLS.map((t2) => [t2.name, t2])
|
|
@@ -3112,10 +3553,8 @@ var consola = createConsola2();
|
|
|
3112
3553
|
// ../../packages/apes/dist/chunk-DYSFQ26B.js
|
|
3113
3554
|
var import_shell_quote = __toESM(require_shell_quote(), 1);
|
|
3114
3555
|
|
|
3115
|
-
// ../../node_modules/.pnpm/
|
|
3116
|
-
import "
|
|
3117
|
-
|
|
3118
|
-
// ../../node_modules/.pnpm/citty@0.1.6/node_modules/citty/dist/index.mjs
|
|
3556
|
+
// ../../node_modules/.pnpm/citty@0.2.2/node_modules/citty/dist/index.mjs
|
|
3557
|
+
import { parseArgs as parseArgs$1 } from "util";
|
|
3119
3558
|
function defineCommand(def) {
|
|
3120
3559
|
return def;
|
|
3121
3560
|
}
|
|
@@ -3832,7 +4271,7 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
|
|
|
3832
4271
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
3833
4272
|
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "fs";
|
|
3834
4273
|
import { homedir as homedir8 } from "os";
|
|
3835
|
-
import { dirname as dirname2, join as join8, resolve as
|
|
4274
|
+
import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
|
|
3836
4275
|
import { fileURLToPath } from "url";
|
|
3837
4276
|
import { parse as parseYaml } from "yaml";
|
|
3838
4277
|
var SKILLS_SUBDIR = [".openape", "agent", "skills"];
|
|
@@ -3941,7 +4380,7 @@ function scanSkillsDir(dir) {
|
|
|
3941
4380
|
}
|
|
3942
4381
|
function defaultSkillsDir() {
|
|
3943
4382
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
3944
|
-
return
|
|
4383
|
+
return resolve3(here, "..", "default-skills");
|
|
3945
4384
|
}
|
|
3946
4385
|
function composeSkills(home, enabledTools) {
|
|
3947
4386
|
const enabled = new Set(enabledTools);
|
|
@@ -4005,7 +4444,7 @@ function readDefaultPersona() {
|
|
|
4005
4444
|
if (_defaultPersonaCache !== void 0) return _defaultPersonaCache;
|
|
4006
4445
|
try {
|
|
4007
4446
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
4008
|
-
const path =
|
|
4447
|
+
const path = resolve3(here, "..", "default-persona.md");
|
|
4009
4448
|
if (!existsSync6(path)) {
|
|
4010
4449
|
_defaultPersonaCache = null;
|
|
4011
4450
|
return null;
|
|
@@ -4287,8 +4726,8 @@ function loadBridgeEnvFile() {
|
|
|
4287
4726
|
const key = trimmed.slice(0, eq).trim();
|
|
4288
4727
|
const value = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
4289
4728
|
if (!key) continue;
|
|
4290
|
-
if (
|
|
4291
|
-
|
|
4729
|
+
if (process3.env[key] === void 0) {
|
|
4730
|
+
process3.env[key] = value;
|
|
4292
4731
|
}
|
|
4293
4732
|
}
|
|
4294
4733
|
} catch {
|
|
@@ -4296,24 +4735,24 @@ function loadBridgeEnvFile() {
|
|
|
4296
4735
|
}
|
|
4297
4736
|
function readConfig() {
|
|
4298
4737
|
loadBridgeEnvFile();
|
|
4299
|
-
const toolsRaw =
|
|
4738
|
+
const toolsRaw = process3.env.APE_CHAT_BRIDGE_TOOLS ?? "";
|
|
4300
4739
|
const tools = toolsRaw.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
4301
|
-
const maxStepsRaw =
|
|
4740
|
+
const maxStepsRaw = process3.env.APE_CHAT_BRIDGE_MAX_STEPS;
|
|
4302
4741
|
const maxSteps = maxStepsRaw ? Number.parseInt(maxStepsRaw, 10) : DEFAULT_MAX_STEPS;
|
|
4303
|
-
const model =
|
|
4742
|
+
const model = process3.env.APE_CHAT_BRIDGE_MODEL;
|
|
4304
4743
|
if (!model) {
|
|
4305
4744
|
throw new Error(
|
|
4306
4745
|
"APE_CHAT_BRIDGE_MODEL is not set. Set it in the bridge .env (usually `~/Library/Application Support/openape/bridge/.env` on macOS) or globally in `~/litellm/.env` so resolveBridgeConfig picks it up at spawn time. Common values: `gpt-5.4` (ChatGPT-only LiteLLM proxy), `claude-haiku-4-5` (Anthropic-only)."
|
|
4307
4746
|
);
|
|
4308
4747
|
}
|
|
4309
4748
|
return {
|
|
4310
|
-
endpoint: (
|
|
4311
|
-
apesBin:
|
|
4749
|
+
endpoint: (process3.env.APE_CHAT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
|
|
4750
|
+
apesBin: process3.env.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
|
|
4312
4751
|
model,
|
|
4313
|
-
systemPrompt:
|
|
4752
|
+
systemPrompt: process3.env.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
|
|
4314
4753
|
tools,
|
|
4315
4754
|
maxSteps: Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : DEFAULT_MAX_STEPS,
|
|
4316
|
-
roomFilter:
|
|
4755
|
+
roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM
|
|
4317
4756
|
};
|
|
4318
4757
|
}
|
|
4319
4758
|
async function getIdentity() {
|
|
@@ -4325,15 +4764,21 @@ async function getIdentity() {
|
|
|
4325
4764
|
return { email: claims.sub };
|
|
4326
4765
|
}
|
|
4327
4766
|
function log(line) {
|
|
4328
|
-
|
|
4767
|
+
process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
4329
4768
|
`);
|
|
4330
4769
|
}
|
|
4331
4770
|
function sleep(ms) {
|
|
4332
|
-
return new Promise((
|
|
4771
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
4333
4772
|
}
|
|
4334
4773
|
function truncate(s2, n2) {
|
|
4335
4774
|
return s2.length <= n2 ? s2 : `${s2.slice(0, n2 - 1)}\u2026`;
|
|
4336
4775
|
}
|
|
4776
|
+
function refusalText(reason) {
|
|
4777
|
+
const base = "I won't process this message \u2014 it looks like a prompt-injection attempt.";
|
|
4778
|
+
return reason ? `${base}
|
|
4779
|
+
|
|
4780
|
+
(matched: ${reason})` : base;
|
|
4781
|
+
}
|
|
4337
4782
|
var Bridge = class {
|
|
4338
4783
|
constructor(cfg, selfEmail, ownerEmail) {
|
|
4339
4784
|
this.cfg = cfg;
|
|
@@ -4349,7 +4794,7 @@ var Bridge = class {
|
|
|
4349
4794
|
chat: this.chat,
|
|
4350
4795
|
ownerEmail: this.ownerEmail,
|
|
4351
4796
|
log,
|
|
4352
|
-
troopUrl: (
|
|
4797
|
+
troopUrl: (process3.env.OPENAPE_TROOP_URL ?? "https://troop.openape.ai").replace(/\/$/, ""),
|
|
4353
4798
|
bearer: this.bearer
|
|
4354
4799
|
});
|
|
4355
4800
|
this.cron.start();
|
|
@@ -4364,14 +4809,18 @@ var Bridge = class {
|
|
|
4364
4809
|
chat;
|
|
4365
4810
|
bearer;
|
|
4366
4811
|
cron;
|
|
4812
|
+
// Prompt-injection gate (#277). Pure heuristic by default — pluggable
|
|
4813
|
+
// backend later. The bridge is the choke-point for every chat message
|
|
4814
|
+
// before it reaches the agent runtime, so this is the right place.
|
|
4815
|
+
injectionDetector = createHeuristicDetector();
|
|
4367
4816
|
/**
|
|
4368
4817
|
* RuntimeConfig is shared across thread sessions and the cron runner.
|
|
4369
4818
|
* The bridge resolves it from its own env at boot and reuses for the
|
|
4370
4819
|
* whole process lifetime.
|
|
4371
4820
|
*/
|
|
4372
4821
|
runtimeConfig() {
|
|
4373
|
-
const apiBase = (
|
|
4374
|
-
const apiKey =
|
|
4822
|
+
const apiBase = (process3.env.LITELLM_BASE_URL ?? "http://127.0.0.1:4000/v1").replace(/\/$/, "");
|
|
4823
|
+
const apiKey = process3.env.LITELLM_API_KEY ?? process3.env.LITELLM_MASTER_KEY ?? "";
|
|
4375
4824
|
if (!apiKey) {
|
|
4376
4825
|
throw new Error("LITELLM_API_KEY (or LITELLM_MASTER_KEY) must be set in the bridge env.");
|
|
4377
4826
|
}
|
|
@@ -4414,7 +4863,7 @@ var Bridge = class {
|
|
|
4414
4863
|
if (accepted.length > 0) log(`accepted: ${accepted.join(", ")}`);
|
|
4415
4864
|
if (skipped.length > 0) log(`skipped (not on allowlist): ${skipped.join(", ")}`);
|
|
4416
4865
|
}
|
|
4417
|
-
handleInbound(msg) {
|
|
4866
|
+
async handleInbound(msg) {
|
|
4418
4867
|
if (msg.senderEmail === this.selfEmail) return;
|
|
4419
4868
|
if (!msg.body.trim()) return;
|
|
4420
4869
|
if (this.cfg.roomFilter && msg.roomId !== this.cfg.roomFilter) return;
|
|
@@ -4423,6 +4872,26 @@ var Bridge = class {
|
|
|
4423
4872
|
return;
|
|
4424
4873
|
}
|
|
4425
4874
|
log(`[${msg.roomId}/${msg.threadId.slice(0, 8)}] in: ${truncate(msg.body, 80)}`);
|
|
4875
|
+
const decision = await decide(this.injectionDetector, {
|
|
4876
|
+
text: msg.body,
|
|
4877
|
+
sender: {
|
|
4878
|
+
email: msg.senderEmail,
|
|
4879
|
+
isOwner: msg.senderEmail === this.ownerEmail
|
|
4880
|
+
}
|
|
4881
|
+
});
|
|
4882
|
+
if (decision.blocked) {
|
|
4883
|
+
log(`[${msg.roomId}/${msg.threadId.slice(0, 8)}] BLOCKED prompt-injection (score=${decision.score.toFixed(2)}, reason=${decision.reason ?? "n/a"})`);
|
|
4884
|
+
try {
|
|
4885
|
+
await this.chat.postMessage(msg.roomId, refusalText(decision.reason), {
|
|
4886
|
+
replyTo: msg.id,
|
|
4887
|
+
threadId: msg.threadId
|
|
4888
|
+
});
|
|
4889
|
+
} catch (err) {
|
|
4890
|
+
const m2 = err instanceof Error ? err.message : String(err);
|
|
4891
|
+
log(`[${msg.roomId}] failed to post refusal: ${m2}`);
|
|
4892
|
+
}
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4426
4895
|
const session = this.getOrCreateThread(msg.roomId, msg.threadId);
|
|
4427
4896
|
session.enqueue(msg.body, msg.id);
|
|
4428
4897
|
}
|
|
@@ -4461,7 +4930,7 @@ var Bridge = class {
|
|
|
4461
4930
|
const bearer = await this.bearer();
|
|
4462
4931
|
const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}/api/ws?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
|
|
4463
4932
|
const ws = new WebSocket(wsUrl);
|
|
4464
|
-
return new Promise((
|
|
4933
|
+
return new Promise((resolve4, reject) => {
|
|
4465
4934
|
let pingTimer;
|
|
4466
4935
|
let allowlistTimer;
|
|
4467
4936
|
ws.on("open", () => {
|
|
@@ -4489,12 +4958,12 @@ var Bridge = class {
|
|
|
4489
4958
|
return;
|
|
4490
4959
|
}
|
|
4491
4960
|
if (frame.type !== "message") return;
|
|
4492
|
-
this.handleInbound(frame.payload);
|
|
4961
|
+
void this.handleInbound(frame.payload);
|
|
4493
4962
|
});
|
|
4494
4963
|
ws.on("close", () => {
|
|
4495
4964
|
if (pingTimer) clearInterval(pingTimer);
|
|
4496
4965
|
if (allowlistTimer) clearInterval(allowlistTimer);
|
|
4497
|
-
|
|
4966
|
+
resolve4();
|
|
4498
4967
|
});
|
|
4499
4968
|
ws.on("error", (err) => {
|
|
4500
4969
|
if (pingTimer) clearInterval(pingTimer);
|
|
@@ -4533,7 +5002,7 @@ async function main() {
|
|
|
4533
5002
|
}
|
|
4534
5003
|
main().catch((err) => {
|
|
4535
5004
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4536
|
-
|
|
5005
|
+
process3.stderr.write(`fatal: ${msg}
|
|
4537
5006
|
`);
|
|
4538
|
-
|
|
5007
|
+
process3.exit(1);
|
|
4539
5008
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/ape-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "OpenApe agent runtime: per-agent process that connects to chat.openape.ai, runs the LLM loop with tools + cron tasks, and streams replies back to owners.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,17 +23,18 @@
|
|
|
23
23
|
"ofetch": "^1.4.1",
|
|
24
24
|
"ws": "^8.18.0",
|
|
25
25
|
"yaml": "^2.8.0",
|
|
26
|
-
"@openape/apes": "1.
|
|
27
|
-
"@openape/cli-auth": "0.4.
|
|
26
|
+
"@openape/apes": "1.26.0",
|
|
27
|
+
"@openape/cli-auth": "0.4.1",
|
|
28
|
+
"@openape/prompt-injection-detector": "0.1.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@antfu/eslint-config": "^7.6.1",
|
|
31
32
|
"@types/node": "^22.19.13",
|
|
32
33
|
"@types/ws": "^8.5.13",
|
|
33
|
-
"eslint": "^
|
|
34
|
+
"eslint": "^10.4.0",
|
|
34
35
|
"tsup": "^8.5.1",
|
|
35
36
|
"typescript": "^5.9.3",
|
|
36
|
-
"vitest": "^
|
|
37
|
+
"vitest": "^4.1.7"
|
|
37
38
|
},
|
|
38
39
|
"engines": {
|
|
39
40
|
"node": ">=22"
|