@openape/ape-agent 2.10.0 → 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 +1174 -294
- package/dist/index.d.ts +356 -0
- package/dist/index.mjs +4923 -0
- package/dist/service-bridge-main.mjs +1133 -833
- package/package.json +13 -3
|
@@ -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 } from "os";
|
|
37
|
-
import { join } from "path";
|
|
38
|
-
var
|
|
36
|
+
import { homedir as homedir2 } from "os";
|
|
37
|
+
import { join as join2 } 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 = join2(homedir2(), ".config", "apes");
|
|
43
|
+
AUTH_FILE = join2(CONFIG_DIR2, "auth.json");
|
|
44
|
+
CONFIG_FILE = join2(CONFIG_DIR2, "config.toml");
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -1080,20 +1080,9 @@ import process4 from "process";
|
|
|
1080
1080
|
import { randomUUID } from "crypto";
|
|
1081
1081
|
import process3 from "process";
|
|
1082
1082
|
|
|
1083
|
-
// ../../packages/apes/dist/chunk-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
// ../../node_modules/.pnpm/citty@0.2.2/node_modules/citty/dist/index.mjs
|
|
1087
|
-
import { parseArgs as parseArgs$1 } from "util";
|
|
1088
|
-
function defineCommand(def) {
|
|
1089
|
-
return def;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// ../../packages/shapes/dist/index.js
|
|
1093
|
-
import { createHash } from "crypto";
|
|
1094
|
-
import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
|
|
1095
|
-
import { homedir as homedir2 } from "os";
|
|
1096
|
-
import { basename, join as join2 } from "path";
|
|
1083
|
+
// ../../packages/apes/dist/chunk-3LH4FT4R.js
|
|
1084
|
+
import { homedir } from "os";
|
|
1085
|
+
import { dirname, join } from "path";
|
|
1097
1086
|
|
|
1098
1087
|
// ../../packages/core/dist/index.js
|
|
1099
1088
|
import * as jose from "jose";
|
|
@@ -1154,6 +1143,27 @@ async function assertPublicUrl(rawUrl, opts = {}) {
|
|
|
1154
1143
|
return url;
|
|
1155
1144
|
}
|
|
1156
1145
|
|
|
1146
|
+
// ../../packages/apes/dist/chunk-3LH4FT4R.js
|
|
1147
|
+
var CONFIG_DIR = join(homedir(), ".config", "openape");
|
|
1148
|
+
var SECRETS_DIR = join(CONFIG_DIR, "secrets.d");
|
|
1149
|
+
var X25519_KEY_PATH = join(CONFIG_DIR, "agent-x25519.key");
|
|
1150
|
+
var X25519_PUBKEY_PATH = `${X25519_KEY_PATH}.pub`;
|
|
1151
|
+
|
|
1152
|
+
// ../../packages/apes/dist/chunk-BA2V3BBO.js
|
|
1153
|
+
init_chunk_OBF7IMQ2();
|
|
1154
|
+
|
|
1155
|
+
// ../../node_modules/.pnpm/citty@0.2.2/node_modules/citty/dist/index.mjs
|
|
1156
|
+
import { parseArgs as parseArgs$1 } from "util";
|
|
1157
|
+
function defineCommand(def) {
|
|
1158
|
+
return def;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// ../../packages/shapes/dist/index.js
|
|
1162
|
+
import { createHash } from "crypto";
|
|
1163
|
+
import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
|
|
1164
|
+
import { homedir as homedir22 } from "os";
|
|
1165
|
+
import { basename, join as join22 } from "path";
|
|
1166
|
+
|
|
1157
1167
|
// ../../packages/grants/dist/index.js
|
|
1158
1168
|
function normalizeSelector(selector) {
|
|
1159
1169
|
if (!selector)
|
|
@@ -2545,9 +2555,9 @@ function digest(content) {
|
|
|
2545
2555
|
}
|
|
2546
2556
|
function adapterDirs() {
|
|
2547
2557
|
return [
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2558
|
+
join22(process.cwd(), ".openape", "shapes", "adapters"),
|
|
2559
|
+
join22(homedir22(), ".openape", "shapes", "adapters"),
|
|
2560
|
+
join22("/etc", "openape", "shapes", "adapters")
|
|
2551
2561
|
];
|
|
2552
2562
|
}
|
|
2553
2563
|
function findByExecutable(executable) {
|
|
@@ -2557,7 +2567,7 @@ function findByExecutable(executable) {
|
|
|
2557
2567
|
try {
|
|
2558
2568
|
const files = readdirSync(dir).filter((f3) => f3.endsWith(".toml"));
|
|
2559
2569
|
for (const file of files) {
|
|
2560
|
-
const path =
|
|
2570
|
+
const path = join22(dir, file);
|
|
2561
2571
|
const content = readFileSync(path, "utf-8");
|
|
2562
2572
|
const match = content.match(/^\s*executable\s*=\s*"([^"]+)"/m);
|
|
2563
2573
|
if (match && match[1] === executable)
|
|
@@ -2574,7 +2584,7 @@ function resolveAdapterPath(cliId, explicitPath) {
|
|
|
2574
2584
|
return explicitPath;
|
|
2575
2585
|
throw new Error(`Adapter file not found: ${explicitPath}`);
|
|
2576
2586
|
}
|
|
2577
|
-
const candidates = adapterDirs().map((dir) =>
|
|
2587
|
+
const candidates = adapterDirs().map((dir) => join22(dir, `${cliId}.toml`));
|
|
2578
2588
|
const match = candidates.find((path) => existsSync2(path));
|
|
2579
2589
|
if (match)
|
|
2580
2590
|
return match;
|
|
@@ -2695,391 +2705,781 @@ init_chunk_OBF7IMQ2();
|
|
|
2695
2705
|
|
|
2696
2706
|
// ../../packages/agent-runtime/dist/index.js
|
|
2697
2707
|
import { spawn } from "child_process";
|
|
2698
|
-
import { mkdirSync, readFileSync as
|
|
2699
|
-
import { homedir as
|
|
2700
|
-
import { dirname, normalize, resolve } from "path";
|
|
2701
|
-
import { homedir as
|
|
2708
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2709
|
+
import { homedir as homedir4 } from "os";
|
|
2710
|
+
import { dirname as dirname2, normalize, resolve } from "path";
|
|
2711
|
+
import { homedir as homedir24 } from "os";
|
|
2702
2712
|
import { resolve as resolve2 } from "path";
|
|
2703
2713
|
import process2 from "process";
|
|
2704
2714
|
import { execFileSync } from "child_process";
|
|
2715
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
2716
|
+
import { homedir as homedir32 } from "os";
|
|
2717
|
+
import { join as join4 } from "path";
|
|
2705
2718
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2719
|
+
|
|
2720
|
+
// ../../packages/cli-auth/dist/index.js
|
|
2721
|
+
import { ofetch } from "ofetch";
|
|
2722
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync as readdirSync2, unlinkSync, writeFileSync } from "fs";
|
|
2723
|
+
import { homedir as homedir3 } from "os";
|
|
2724
|
+
import { join as join3 } from "path";
|
|
2725
|
+
import { ofetch as ofetch3 } from "ofetch";
|
|
2726
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
2727
|
+
import { sign } from "crypto";
|
|
2728
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
|
|
2729
|
+
import { homedir as homedir23 } from "os";
|
|
2730
|
+
import { join as join23 } from "path";
|
|
2731
|
+
import { ofetch as ofetch2 } from "ofetch";
|
|
2732
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
2733
|
+
import { createPrivateKey } from "crypto";
|
|
2734
|
+
import { ofetch as ofetch4 } from "ofetch";
|
|
2735
|
+
import { ofetch as ofetch5 } from "ofetch";
|
|
2736
|
+
function getConfigDir(authHome) {
|
|
2737
|
+
if (authHome) return join3(authHome, ".config", "apes");
|
|
2738
|
+
const override = process.env.OPENAPE_CLI_AUTH_HOME;
|
|
2739
|
+
if (override) return override;
|
|
2740
|
+
return join3(homedir3(), ".config", "apes");
|
|
2714
2741
|
}
|
|
2715
|
-
function
|
|
2716
|
-
|
|
2717
|
-
const [execBin, execArgs] = bypass ? ["/bin/bash", ["-c", cmd]] : [BIN, ["-c", cmd]];
|
|
2718
|
-
return new Promise((resolveResult) => {
|
|
2719
|
-
const child = spawn(execBin, execArgs, {
|
|
2720
|
-
env: { ...process.env, APE_WAIT: "1" },
|
|
2721
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2722
|
-
...cwd ? { cwd } : {}
|
|
2723
|
-
});
|
|
2724
|
-
let stdout2 = "";
|
|
2725
|
-
let stderr = "";
|
|
2726
|
-
let timedOut = false;
|
|
2727
|
-
let spawnError = null;
|
|
2728
|
-
child.stdout.on("data", (chunk) => {
|
|
2729
|
-
stdout2 += chunk.toString("utf8");
|
|
2730
|
-
});
|
|
2731
|
-
child.stderr.on("data", (chunk) => {
|
|
2732
|
-
stderr += chunk.toString("utf8");
|
|
2733
|
-
});
|
|
2734
|
-
child.on("error", (err) => {
|
|
2735
|
-
spawnError = err;
|
|
2736
|
-
});
|
|
2737
|
-
const timer = setTimeout(() => {
|
|
2738
|
-
timedOut = true;
|
|
2739
|
-
child.kill("SIGTERM");
|
|
2740
|
-
setTimeout(() => {
|
|
2741
|
-
try {
|
|
2742
|
-
child.kill("SIGKILL");
|
|
2743
|
-
} catch {
|
|
2744
|
-
}
|
|
2745
|
-
}, 5e3);
|
|
2746
|
-
}, timeoutMs);
|
|
2747
|
-
child.on("close", (code) => {
|
|
2748
|
-
clearTimeout(timer);
|
|
2749
|
-
if (spawnError) {
|
|
2750
|
-
resolveResult({
|
|
2751
|
-
stdout: "",
|
|
2752
|
-
stderr: "",
|
|
2753
|
-
exit_code: -1,
|
|
2754
|
-
error: spawnError.message,
|
|
2755
|
-
hint: `Could not exec '${execBin}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH (or set OPENAPE_BYPASS_APE_SHELL=1 to skip the gated shell entirely \u2014 meant for the OpenApe pod where the container IS the sandbox).`
|
|
2756
|
-
});
|
|
2757
|
-
return;
|
|
2758
|
-
}
|
|
2759
|
-
resolveResult({
|
|
2760
|
-
stdout: capStdio(stdout2),
|
|
2761
|
-
stderr: capStdio(stderr),
|
|
2762
|
-
exit_code: code ?? -1,
|
|
2763
|
-
...timedOut ? { timed_out: true } : {}
|
|
2764
|
-
});
|
|
2765
|
-
});
|
|
2766
|
-
});
|
|
2742
|
+
function getAuthFile(authHome) {
|
|
2743
|
+
return join3(getConfigDir(authHome), "auth.json");
|
|
2767
2744
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
properties: {
|
|
2776
|
-
cmd: {
|
|
2777
|
-
type: "string",
|
|
2778
|
-
description: "Shell command to run, e.g. `ls -la ~/Documents`, `git status`, `curl -fsSL https://example.com`. The whole string is passed to `bash -c`; quote internally as needed."
|
|
2779
|
-
},
|
|
2780
|
-
timeout_ms: {
|
|
2781
|
-
type: "number",
|
|
2782
|
-
description: "Wall-clock cap for the whole approval-and-run cycle in milliseconds. Default 300000 (5 min). Approval waits count against this budget."
|
|
2783
|
-
}
|
|
2784
|
-
},
|
|
2785
|
-
required: ["cmd"]
|
|
2786
|
-
},
|
|
2787
|
-
execute: async (args) => {
|
|
2788
|
-
const a2 = args;
|
|
2789
|
-
if (typeof a2.cmd !== "string" || a2.cmd.trim() === "") {
|
|
2790
|
-
throw new Error("cmd must be a non-empty string");
|
|
2791
|
-
}
|
|
2792
|
-
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULTS.DEFAULT_TIMEOUT_MS;
|
|
2793
|
-
return await runApeShell(a2.cmd, timeout);
|
|
2794
|
-
}
|
|
2745
|
+
function getSpTokensDir() {
|
|
2746
|
+
return join3(getConfigDir(), "sp-tokens");
|
|
2747
|
+
}
|
|
2748
|
+
function ensureConfigDir(authHome) {
|
|
2749
|
+
const dir = getConfigDir(authHome);
|
|
2750
|
+
if (!existsSync(dir)) {
|
|
2751
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2795
2752
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2753
|
+
}
|
|
2754
|
+
function ensureSpTokensDir() {
|
|
2755
|
+
ensureConfigDir();
|
|
2756
|
+
const dir = getSpTokensDir();
|
|
2757
|
+
if (!existsSync(dir)) {
|
|
2758
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2801
2759
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2760
|
+
}
|
|
2761
|
+
function loadIdpAuth(authHome) {
|
|
2762
|
+
const file = getAuthFile(authHome);
|
|
2763
|
+
if (!existsSync(file)) return null;
|
|
2764
|
+
try {
|
|
2765
|
+
const raw = readFileSync2(file, "utf-8");
|
|
2766
|
+
if (!raw.trim()) return null;
|
|
2767
|
+
return JSON.parse(raw);
|
|
2768
|
+
} catch {
|
|
2769
|
+
return null;
|
|
2806
2770
|
}
|
|
2807
|
-
return candidate;
|
|
2808
2771
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
const content = readFileSync2(p, "utf8");
|
|
2824
|
-
if (Buffer.byteLength(content, "utf8") > MAX_BYTES) {
|
|
2825
|
-
return { path: p, truncated: true, content: content.slice(0, MAX_BYTES) };
|
|
2826
|
-
}
|
|
2827
|
-
return { path: p, truncated: false, content };
|
|
2828
|
-
}
|
|
2829
|
-
},
|
|
2830
|
-
{
|
|
2831
|
-
name: "file.write",
|
|
2832
|
-
description: "Write a UTF-8 file under the agent's home directory. Creates parent dirs as needed. 1MB max.",
|
|
2833
|
-
parameters: {
|
|
2834
|
-
type: "object",
|
|
2835
|
-
properties: {
|
|
2836
|
-
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME)." },
|
|
2837
|
-
content: { type: "string", description: "File body. Existing files are overwritten." }
|
|
2838
|
-
},
|
|
2839
|
-
required: ["path", "content"]
|
|
2840
|
-
},
|
|
2841
|
-
execute: async (args) => {
|
|
2842
|
-
const a2 = args;
|
|
2843
|
-
if (typeof a2.content !== "string") throw new Error("content must be a string");
|
|
2844
|
-
if (Buffer.byteLength(a2.content, "utf8") > MAX_BYTES) {
|
|
2845
|
-
throw new Error(`content exceeds ${MAX_BYTES} byte cap`);
|
|
2846
|
-
}
|
|
2847
|
-
const p = jailPath(a2.path);
|
|
2848
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
2849
|
-
writeFileSync(p, a2.content, { encoding: "utf8" });
|
|
2850
|
-
return { path: p, bytes: Buffer.byteLength(a2.content, "utf8") };
|
|
2851
|
-
}
|
|
2852
|
-
},
|
|
2853
|
-
{
|
|
2854
|
-
name: "file.edit",
|
|
2855
|
-
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.",
|
|
2856
|
-
parameters: {
|
|
2857
|
-
type: "object",
|
|
2858
|
-
properties: {
|
|
2859
|
-
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME)." },
|
|
2860
|
-
old_string: { type: "string", description: "Exact text to replace. Include enough surrounding context to be unique unless replace_all is set." },
|
|
2861
|
-
new_string: { type: "string", description: "Replacement text. Must differ from old_string." },
|
|
2862
|
-
replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring a unique match. Default false." }
|
|
2863
|
-
},
|
|
2864
|
-
required: ["path", "old_string", "new_string"]
|
|
2865
|
-
},
|
|
2866
|
-
execute: async (args) => {
|
|
2867
|
-
const a2 = args;
|
|
2868
|
-
if (typeof a2.old_string !== "string" || a2.old_string === "") {
|
|
2869
|
-
throw new Error("old_string must be a non-empty string");
|
|
2870
|
-
}
|
|
2871
|
-
if (typeof a2.new_string !== "string") {
|
|
2872
|
-
throw new TypeError("new_string must be a string");
|
|
2873
|
-
}
|
|
2874
|
-
if (a2.old_string === a2.new_string) {
|
|
2875
|
-
throw new Error("old_string and new_string are identical \u2014 nothing to change");
|
|
2876
|
-
}
|
|
2877
|
-
const replaceAll = a2.replace_all === true;
|
|
2878
|
-
const p = jailPath(a2.path);
|
|
2879
|
-
const before = readFileSync2(p, "utf8");
|
|
2880
|
-
const occurrences = before.split(a2.old_string).length - 1;
|
|
2881
|
-
if (occurrences === 0) {
|
|
2882
|
-
throw new Error("old_string not found in file");
|
|
2883
|
-
}
|
|
2884
|
-
if (occurrences > 1 && !replaceAll) {
|
|
2885
|
-
throw new Error(`old_string occurs ${occurrences} times \u2014 pass replace_all:true or add surrounding context to make it unique`);
|
|
2886
|
-
}
|
|
2887
|
-
const after = replaceAll ? before.split(a2.old_string).join(a2.new_string) : before.replace(a2.old_string, a2.new_string);
|
|
2888
|
-
if (Buffer.byteLength(after, "utf8") > MAX_BYTES) {
|
|
2889
|
-
throw new Error(`result exceeds ${MAX_BYTES} byte cap`);
|
|
2772
|
+
function saveIdpAuth(auth, authHome) {
|
|
2773
|
+
ensureConfigDir(authHome);
|
|
2774
|
+
const file = getAuthFile(authHome);
|
|
2775
|
+
let extra = {};
|
|
2776
|
+
if (existsSync(file)) {
|
|
2777
|
+
try {
|
|
2778
|
+
const raw = readFileSync2(file, "utf-8");
|
|
2779
|
+
if (raw.trim()) {
|
|
2780
|
+
const prev = JSON.parse(raw);
|
|
2781
|
+
for (const key of Object.keys(prev)) {
|
|
2782
|
+
if (!(key in auth)) {
|
|
2783
|
+
extra[key] = prev[key];
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2890
2786
|
}
|
|
2891
|
-
|
|
2892
|
-
|
|
2787
|
+
} catch {
|
|
2788
|
+
extra = {};
|
|
2893
2789
|
}
|
|
2894
2790
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
var ID_RE = /^\d{1,12}$/;
|
|
2898
|
-
function shq(s2) {
|
|
2899
|
-
return `'${String(s2).replace(/'/g, "'\\''")}'`;
|
|
2791
|
+
const merged = { ...extra, ...auth };
|
|
2792
|
+
writeFileSync(file, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
2900
2793
|
}
|
|
2901
|
-
function
|
|
2902
|
-
|
|
2903
|
-
throw new Error("branch must match ^[A-Za-z0-9._/-]{1,200}$");
|
|
2904
|
-
}
|
|
2905
|
-
return v2;
|
|
2794
|
+
function audToFilename(aud) {
|
|
2795
|
+
return aud.replace(/[^\w.-]/g, "_");
|
|
2906
2796
|
}
|
|
2907
|
-
function
|
|
2908
|
-
|
|
2909
|
-
const s2 = String(v2);
|
|
2910
|
-
if (!ID_RE.test(s2)) throw new Error("id must be a number");
|
|
2911
|
-
return s2;
|
|
2797
|
+
function spTokenPath(aud) {
|
|
2798
|
+
return join3(getSpTokensDir(), `${audToFilename(aud)}.json`);
|
|
2912
2799
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2800
|
+
function loadSpToken(aud) {
|
|
2801
|
+
const path = spTokenPath(aud);
|
|
2802
|
+
if (!existsSync(path)) return null;
|
|
2803
|
+
try {
|
|
2804
|
+
const raw = readFileSync2(path, "utf-8");
|
|
2805
|
+
if (!raw.trim()) return null;
|
|
2806
|
+
return JSON.parse(raw);
|
|
2807
|
+
} catch {
|
|
2808
|
+
return null;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
function saveSpToken(token) {
|
|
2812
|
+
ensureSpTokensDir();
|
|
2813
|
+
writeFileSync(spTokenPath(token.aud), JSON.stringify(token, null, 2), { mode: 384 });
|
|
2814
|
+
}
|
|
2815
|
+
var AuthError = class extends Error {
|
|
2816
|
+
status;
|
|
2817
|
+
hint;
|
|
2818
|
+
constructor(status, message, hint) {
|
|
2819
|
+
super(hint ? `${message}
|
|
2820
|
+
${hint}` : message);
|
|
2821
|
+
this.name = "AuthError";
|
|
2822
|
+
this.status = status;
|
|
2823
|
+
this.hint = hint;
|
|
2824
|
+
}
|
|
2937
2825
|
};
|
|
2938
|
-
var
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
}
|
|
2947
|
-
prMerge: (i2) => {
|
|
2948
|
-
const id = assertId(i2.ref);
|
|
2949
|
-
const parts = ["az", "repos", "pr", "update", "--id", id];
|
|
2950
|
-
if (i2.auto) parts.push("--auto-complete", "true");
|
|
2951
|
-
else parts.push("--status", "completed");
|
|
2952
|
-
if (i2.squash === true) parts.push("--merge-commit-message-style", "squash");
|
|
2953
|
-
if (i2.deleteBranch) parts.push("--delete-source-branch", "true");
|
|
2954
|
-
return parts.join(" ");
|
|
2955
|
-
},
|
|
2956
|
-
prStatus: (ref) => `az repos pr show --id ${assertId(ref)}`,
|
|
2957
|
-
// Azure work items are org/project-scoped, not repo-scoped, so `repo`
|
|
2958
|
-
// doesn't apply here — the caller's `az` config (defaults.organization/
|
|
2959
|
-
// project) resolves it.
|
|
2960
|
-
issueGet: (ref, _repo) => `az boards work-item show --id ${assertId(ref)}`
|
|
2826
|
+
var NotLoggedInError = class extends AuthError {
|
|
2827
|
+
constructor(hint) {
|
|
2828
|
+
super(
|
|
2829
|
+
401,
|
|
2830
|
+
"Not logged in",
|
|
2831
|
+
hint ?? "Run `apes login <email>` once on this device to authenticate against the OpenApe IdP."
|
|
2832
|
+
);
|
|
2833
|
+
this.name = "NotLoggedInError";
|
|
2834
|
+
}
|
|
2961
2835
|
};
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2836
|
+
async function exchangeForSpToken(idpAuth, request, now = Math.floor(Date.now() / 1e3)) {
|
|
2837
|
+
const url = `${request.endpoint.replace(/\/$/, "")}/api/cli/exchange`;
|
|
2838
|
+
let response;
|
|
2839
|
+
try {
|
|
2840
|
+
response = await ofetch(url, {
|
|
2841
|
+
method: "POST",
|
|
2842
|
+
body: {
|
|
2843
|
+
subject_token: idpAuth.access_token,
|
|
2844
|
+
...request.scopes ? { scopes: request.scopes } : {}
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
} catch (err) {
|
|
2848
|
+
const status = err.status ?? err.statusCode ?? 0;
|
|
2849
|
+
const data = err.data;
|
|
2850
|
+
const title = data?.title ?? `Token exchange failed (HTTP ${status})`;
|
|
2851
|
+
const hint = status === 401 ? `IdP token rejected at ${url}. Try \`apes login\` again \u2014 token may be expired or audience-mismatched.` : data?.detail;
|
|
2852
|
+
throw new AuthError(status, title, hint);
|
|
2853
|
+
}
|
|
2854
|
+
if (!response.access_token) {
|
|
2855
|
+
throw new AuthError(0, `Exchange response from ${url} missing access_token`);
|
|
2856
|
+
}
|
|
2857
|
+
const expiresAt = response.expires_at ?? (response.expires_in ? now + response.expires_in : now + 3600);
|
|
2858
|
+
const token = {
|
|
2859
|
+
endpoint: request.endpoint,
|
|
2860
|
+
aud: response.aud ?? request.aud,
|
|
2861
|
+
access_token: response.access_token,
|
|
2862
|
+
expires_at: expiresAt,
|
|
2863
|
+
...request.scopes ? { scopes: request.scopes } : {},
|
|
2864
|
+
issued_from_idp_iat: now
|
|
2865
|
+
};
|
|
2866
|
+
saveSpToken(token);
|
|
2867
|
+
return token;
|
|
2968
2868
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
if (
|
|
2972
|
-
|
|
2869
|
+
var OPENSSH_MAGIC = "openssh-key-v1\0";
|
|
2870
|
+
function loadEd25519PrivateKey(pem) {
|
|
2871
|
+
if (pem.includes("BEGIN OPENSSH PRIVATE KEY")) {
|
|
2872
|
+
return parseOpenSSHEd25519(pem);
|
|
2973
2873
|
}
|
|
2974
|
-
return
|
|
2874
|
+
return createPrivateKey(pem);
|
|
2975
2875
|
}
|
|
2976
|
-
function
|
|
2977
|
-
|
|
2978
|
-
|
|
2876
|
+
function parseOpenSSHEd25519(pem) {
|
|
2877
|
+
const b64 = pem.replace(/-----BEGIN OPENSSH PRIVATE KEY-----/, "").replace(/-----END OPENSSH PRIVATE KEY-----/, "").replace(/\s/g, "");
|
|
2878
|
+
const buf = Buffer3.from(b64, "base64");
|
|
2879
|
+
let offset = 0;
|
|
2880
|
+
const magic = buf.subarray(0, OPENSSH_MAGIC.length).toString("ascii");
|
|
2881
|
+
if (magic !== OPENSSH_MAGIC) {
|
|
2882
|
+
throw new Error("Not an OpenSSH private key");
|
|
2979
2883
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2884
|
+
offset += OPENSSH_MAGIC.length;
|
|
2885
|
+
const cipherLen = buf.readUInt32BE(offset);
|
|
2886
|
+
offset += 4;
|
|
2887
|
+
const cipher = buf.subarray(offset, offset + cipherLen).toString();
|
|
2888
|
+
offset += cipherLen;
|
|
2889
|
+
if (cipher !== "none") {
|
|
2890
|
+
throw new Error(`Encrypted keys not supported (cipher: ${cipher}). Decrypt first with: ssh-keygen -p -f <key>`);
|
|
2982
2891
|
}
|
|
2983
|
-
|
|
2892
|
+
const kdfLen = buf.readUInt32BE(offset);
|
|
2893
|
+
offset += 4;
|
|
2894
|
+
offset += kdfLen;
|
|
2895
|
+
const kdfOptsLen = buf.readUInt32BE(offset);
|
|
2896
|
+
offset += 4;
|
|
2897
|
+
offset += kdfOptsLen;
|
|
2898
|
+
const numKeys = buf.readUInt32BE(offset);
|
|
2899
|
+
offset += 4;
|
|
2900
|
+
if (numKeys !== 1) {
|
|
2901
|
+
throw new Error(`Expected 1 key, got ${numKeys}`);
|
|
2902
|
+
}
|
|
2903
|
+
const pubSectionLen = buf.readUInt32BE(offset);
|
|
2904
|
+
offset += 4;
|
|
2905
|
+
offset += pubSectionLen;
|
|
2906
|
+
const privSectionLen = buf.readUInt32BE(offset);
|
|
2907
|
+
offset += 4;
|
|
2908
|
+
const privSection = buf.subarray(offset, offset + privSectionLen);
|
|
2909
|
+
let pOffset = 0;
|
|
2910
|
+
const check1 = privSection.readUInt32BE(pOffset);
|
|
2911
|
+
pOffset += 4;
|
|
2912
|
+
const check2 = privSection.readUInt32BE(pOffset);
|
|
2913
|
+
pOffset += 4;
|
|
2914
|
+
if (check1 !== check2) {
|
|
2915
|
+
throw new Error("Check integers mismatch \u2014 key may be corrupted or encrypted");
|
|
2916
|
+
}
|
|
2917
|
+
const keyTypeLen = privSection.readUInt32BE(pOffset);
|
|
2918
|
+
pOffset += 4;
|
|
2919
|
+
const keyType = privSection.subarray(pOffset, pOffset + keyTypeLen).toString();
|
|
2920
|
+
pOffset += keyTypeLen;
|
|
2921
|
+
if (keyType !== "ssh-ed25519") {
|
|
2922
|
+
throw new Error(`Expected ssh-ed25519, got ${keyType}`);
|
|
2923
|
+
}
|
|
2924
|
+
const pubKeyLen = privSection.readUInt32BE(pOffset);
|
|
2925
|
+
pOffset += 4;
|
|
2926
|
+
const pubKey = privSection.subarray(pOffset, pOffset + pubKeyLen);
|
|
2927
|
+
pOffset += pubKeyLen;
|
|
2928
|
+
const privKeyLen = privSection.readUInt32BE(pOffset);
|
|
2929
|
+
pOffset += 4;
|
|
2930
|
+
const privKeyData = privSection.subarray(pOffset, pOffset + privKeyLen);
|
|
2931
|
+
const seed = privKeyData.subarray(0, 32);
|
|
2932
|
+
return createPrivateKey({
|
|
2933
|
+
key: { kty: "OKP", crv: "Ed25519", d: seed.toString("base64url"), x: pubKey.toString("base64url") },
|
|
2934
|
+
format: "jwk"
|
|
2935
|
+
});
|
|
2984
2936
|
}
|
|
2985
|
-
function
|
|
2986
|
-
|
|
2937
|
+
async function getEndpoints(idp) {
|
|
2938
|
+
let disco = {};
|
|
2939
|
+
try {
|
|
2940
|
+
disco = await ofetch2(`${idp}/.well-known/openid-configuration`);
|
|
2941
|
+
} catch {
|
|
2942
|
+
}
|
|
2943
|
+
return {
|
|
2944
|
+
challenge: disco.ddisa_agent_challenge_endpoint ?? `${idp}/api/agent/challenge`,
|
|
2945
|
+
authenticate: disco.ddisa_agent_authenticate_endpoint ?? `${idp}/api/agent/authenticate`
|
|
2946
|
+
};
|
|
2987
2947
|
}
|
|
2988
|
-
function
|
|
2989
|
-
|
|
2948
|
+
function resolveKeyPath(p) {
|
|
2949
|
+
if (p.startsWith("~")) return join23(homedir23(), p.slice(1));
|
|
2950
|
+
return p;
|
|
2990
2951
|
}
|
|
2991
|
-
function
|
|
2992
|
-
|
|
2952
|
+
function findSigningKey(auth) {
|
|
2953
|
+
const candidates = [];
|
|
2954
|
+
if (auth.key_path) candidates.push(resolveKeyPath(auth.key_path));
|
|
2955
|
+
candidates.push(join23(homedir23(), ".ssh", "id_ed25519"));
|
|
2956
|
+
for (const p of candidates) {
|
|
2957
|
+
if (existsSync22(p)) {
|
|
2958
|
+
try {
|
|
2959
|
+
return { keyPath: p, keyContent: readFileSync22(p, "utf-8") };
|
|
2960
|
+
} catch {
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return null;
|
|
2993
2965
|
}
|
|
2994
|
-
function
|
|
2995
|
-
|
|
2966
|
+
async function refreshAgentToken(auth, now = Math.floor(Date.now() / 1e3)) {
|
|
2967
|
+
const key = findSigningKey(auth);
|
|
2968
|
+
if (!key) return null;
|
|
2969
|
+
let privateKey;
|
|
2970
|
+
try {
|
|
2971
|
+
privateKey = loadEd25519PrivateKey(key.keyContent);
|
|
2972
|
+
} catch {
|
|
2973
|
+
return null;
|
|
2974
|
+
}
|
|
2975
|
+
let endpoints;
|
|
2976
|
+
try {
|
|
2977
|
+
endpoints = await getEndpoints(auth.idp);
|
|
2978
|
+
} catch {
|
|
2979
|
+
return null;
|
|
2980
|
+
}
|
|
2981
|
+
let challenge;
|
|
2982
|
+
try {
|
|
2983
|
+
const resp = await ofetch2(endpoints.challenge, {
|
|
2984
|
+
method: "POST",
|
|
2985
|
+
headers: { "Content-Type": "application/json" },
|
|
2986
|
+
body: { agent_id: auth.email }
|
|
2987
|
+
});
|
|
2988
|
+
challenge = resp.challenge;
|
|
2989
|
+
} catch {
|
|
2990
|
+
return null;
|
|
2991
|
+
}
|
|
2992
|
+
let signature;
|
|
2993
|
+
try {
|
|
2994
|
+
signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
|
|
2995
|
+
} catch {
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
let authResp;
|
|
2999
|
+
try {
|
|
3000
|
+
authResp = await ofetch2(endpoints.authenticate, {
|
|
3001
|
+
method: "POST",
|
|
3002
|
+
headers: { "Content-Type": "application/json" },
|
|
3003
|
+
body: { agent_id: auth.email, challenge, signature }
|
|
3004
|
+
});
|
|
3005
|
+
} catch {
|
|
3006
|
+
return null;
|
|
3007
|
+
}
|
|
3008
|
+
return {
|
|
3009
|
+
...auth,
|
|
3010
|
+
access_token: authResp.token,
|
|
3011
|
+
expires_at: now + (authResp.expires_in || 3600),
|
|
3012
|
+
key_path: auth.key_path ?? key.keyPath
|
|
3013
|
+
};
|
|
2996
3014
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3015
|
+
var EXPIRY_SKEW_SECONDS = 30;
|
|
3016
|
+
async function getTokenEndpoint(idp) {
|
|
3017
|
+
try {
|
|
3018
|
+
const disco = await ofetch3(`${idp}/.well-known/openid-configuration`);
|
|
3019
|
+
if (disco.token_endpoint) return disco.token_endpoint;
|
|
3020
|
+
} catch {
|
|
3021
|
+
}
|
|
3022
|
+
return `${idp}/token`;
|
|
3001
3023
|
}
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3024
|
+
async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3), authHome) {
|
|
3025
|
+
const auth = loadIdpAuth(authHome);
|
|
3026
|
+
if (!auth) {
|
|
3027
|
+
throw new NotLoggedInError();
|
|
3028
|
+
}
|
|
3029
|
+
if (auth.expires_at > now + EXPIRY_SKEW_SECONDS) {
|
|
3030
|
+
return auth;
|
|
3031
|
+
}
|
|
3032
|
+
if (!auth.refresh_token) {
|
|
3033
|
+
const refreshed = await refreshAgentToken(auth, now);
|
|
3034
|
+
if (refreshed) {
|
|
3035
|
+
saveIdpAuth(refreshed, authHome);
|
|
3036
|
+
return refreshed;
|
|
3037
|
+
}
|
|
3038
|
+
throw new NotLoggedInError(
|
|
3039
|
+
`IdP token expired at ${new Date(auth.expires_at * 1e3).toISOString()} and no refresh_token is stored. Run \`apes login\` again.`
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
const tokenEndpoint = await getTokenEndpoint(auth.idp);
|
|
3043
|
+
const body = new URLSearchParams({
|
|
3044
|
+
grant_type: "refresh_token",
|
|
3045
|
+
refresh_token: auth.refresh_token
|
|
3046
|
+
});
|
|
3047
|
+
let response;
|
|
3048
|
+
try {
|
|
3049
|
+
response = await ofetch3(tokenEndpoint, {
|
|
3050
|
+
method: "POST",
|
|
3051
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3052
|
+
body: body.toString()
|
|
3053
|
+
});
|
|
3054
|
+
} catch (err) {
|
|
3055
|
+
const status = err.status ?? err.statusCode ?? 0;
|
|
3056
|
+
if (status === 400 || status === 401) {
|
|
3057
|
+
saveIdpAuth({ ...auth, refresh_token: void 0 }, authHome);
|
|
3058
|
+
throw new NotLoggedInError(
|
|
3059
|
+
`Refresh token rejected by ${auth.idp}. Run \`apes login\` again.`
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
throw new AuthError(
|
|
3063
|
+
0,
|
|
3064
|
+
`Network error refreshing IdP token at ${tokenEndpoint}`,
|
|
3065
|
+
`Underlying: ${err.message ?? err}`
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
if (!response.access_token) {
|
|
3069
|
+
throw new AuthError(0, `IdP refresh response missing access_token (endpoint: ${tokenEndpoint})`);
|
|
3070
|
+
}
|
|
3071
|
+
const next = {
|
|
3072
|
+
...auth,
|
|
3073
|
+
access_token: response.access_token,
|
|
3074
|
+
refresh_token: response.refresh_token ?? auth.refresh_token,
|
|
3075
|
+
expires_at: now + (response.expires_in ?? 3600)
|
|
3076
|
+
};
|
|
3077
|
+
saveIdpAuth(next, authHome);
|
|
3078
|
+
return next;
|
|
3079
|
+
}
|
|
3080
|
+
var SP_TOKEN_SKEW_SECONDS = 60;
|
|
3081
|
+
async function getAuthorizedBearer(opts) {
|
|
3082
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3083
|
+
if (!opts.forceRefresh) {
|
|
3084
|
+
const cached = loadSpToken(opts.aud);
|
|
3085
|
+
if (cached && cached.expires_at > now + SP_TOKEN_SKEW_SECONDS) {
|
|
3086
|
+
return `Bearer ${cached.access_token}`;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
const idpAuth = await ensureFreshIdpAuth(now);
|
|
3090
|
+
const sp = await exchangeForSpToken(idpAuth, {
|
|
3091
|
+
endpoint: opts.endpoint,
|
|
3092
|
+
aud: opts.aud,
|
|
3093
|
+
...opts.scopes ? { scopes: opts.scopes } : {}
|
|
3094
|
+
}, now);
|
|
3095
|
+
return `Bearer ${sp.access_token}`;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
// ../../packages/agent-runtime/dist/index.js
|
|
3099
|
+
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3100
|
+
var MAX_STDIO_BYTES = 64 * 1024;
|
|
3101
|
+
var BIN = "ape-shell";
|
|
3102
|
+
function capStdio(s2) {
|
|
3103
|
+
const buf = Buffer.from(s2, "utf8");
|
|
3104
|
+
if (buf.byteLength <= MAX_STDIO_BYTES) return s2;
|
|
3105
|
+
return `${buf.subarray(0, MAX_STDIO_BYTES).toString("utf8")}
|
|
3106
|
+
[truncated to ${MAX_STDIO_BYTES} bytes]`;
|
|
3107
|
+
}
|
|
3108
|
+
function runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS, cwd) {
|
|
3109
|
+
const bypass = process.env.OPENAPE_BYPASS_APE_SHELL === "1";
|
|
3110
|
+
const [execBin, execArgs] = bypass ? ["/bin/bash", ["-c", cmd]] : [BIN, ["-c", cmd]];
|
|
3111
|
+
return new Promise((resolveResult) => {
|
|
3112
|
+
const child = spawn(execBin, execArgs, {
|
|
3113
|
+
env: { ...process.env, APE_WAIT: "1" },
|
|
3114
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3115
|
+
...cwd ? { cwd } : {}
|
|
3116
|
+
});
|
|
3117
|
+
let stdout2 = "";
|
|
3118
|
+
let stderr = "";
|
|
3119
|
+
let timedOut = false;
|
|
3120
|
+
let spawnError = null;
|
|
3121
|
+
child.stdout.on("data", (chunk) => {
|
|
3122
|
+
stdout2 += chunk.toString("utf8");
|
|
3123
|
+
});
|
|
3124
|
+
child.stderr.on("data", (chunk) => {
|
|
3125
|
+
stderr += chunk.toString("utf8");
|
|
3126
|
+
});
|
|
3127
|
+
child.on("error", (err) => {
|
|
3128
|
+
spawnError = err;
|
|
3129
|
+
});
|
|
3130
|
+
const timer = setTimeout(() => {
|
|
3131
|
+
timedOut = true;
|
|
3132
|
+
child.kill("SIGTERM");
|
|
3133
|
+
setTimeout(() => {
|
|
3134
|
+
try {
|
|
3135
|
+
child.kill("SIGKILL");
|
|
3136
|
+
} catch {
|
|
3137
|
+
}
|
|
3138
|
+
}, 5e3);
|
|
3139
|
+
}, timeoutMs);
|
|
3140
|
+
child.on("close", (code) => {
|
|
3141
|
+
clearTimeout(timer);
|
|
3142
|
+
if (spawnError) {
|
|
3143
|
+
resolveResult({
|
|
3144
|
+
stdout: "",
|
|
3145
|
+
stderr: "",
|
|
3146
|
+
exit_code: -1,
|
|
3147
|
+
error: spawnError.message,
|
|
3148
|
+
hint: `Could not exec '${execBin}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH (or set OPENAPE_BYPASS_APE_SHELL=1 to skip the gated shell entirely \u2014 meant for the OpenApe pod where the container IS the sandbox).`
|
|
3149
|
+
});
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
resolveResult({
|
|
3153
|
+
stdout: capStdio(stdout2),
|
|
3154
|
+
stderr: capStdio(stderr),
|
|
3155
|
+
exit_code: code ?? -1,
|
|
3156
|
+
...timedOut ? { timed_out: true } : {}
|
|
3157
|
+
});
|
|
3158
|
+
});
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
var DEFAULTS = { DEFAULT_TIMEOUT_MS };
|
|
3162
|
+
var bashTools = [
|
|
3005
3163
|
{
|
|
3006
|
-
name: "
|
|
3007
|
-
description: "
|
|
3164
|
+
name: "bash",
|
|
3165
|
+
description: "Run a shell command on the agent host. Every invocation goes through the OpenApe DDISA grant cycle \u2014 auto-approved if the owner has a matching YOLO scope, otherwise the owner gets a push notification to approve. Runs as the agent's macOS user, so file/network access is limited to what that user can see. Returns stdout, stderr, and exit code. For repeated command patterns ask the owner to set up a YOLO scope so approvals don't pile up.",
|
|
3008
3166
|
parameters: {
|
|
3009
3167
|
type: "object",
|
|
3010
3168
|
properties: {
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3169
|
+
cmd: {
|
|
3170
|
+
type: "string",
|
|
3171
|
+
description: "Shell command to run, e.g. `ls -la ~/Documents`, `git status`, `curl -fsSL https://example.com`. The whole string is passed to `bash -c`; quote internally as needed."
|
|
3172
|
+
},
|
|
3173
|
+
timeout_ms: {
|
|
3174
|
+
type: "number",
|
|
3175
|
+
description: "Wall-clock cap for the whole approval-and-run cycle in milliseconds. Default 300000 (5 min). Approval waits count against this budget."
|
|
3176
|
+
}
|
|
3017
3177
|
},
|
|
3018
|
-
required: ["
|
|
3178
|
+
required: ["cmd"]
|
|
3019
3179
|
},
|
|
3020
3180
|
execute: async (args) => {
|
|
3021
3181
|
const a2 = args;
|
|
3022
|
-
|
|
3023
|
-
|
|
3182
|
+
if (typeof a2.cmd !== "string" || a2.cmd.trim() === "") {
|
|
3183
|
+
throw new Error("cmd must be a non-empty string");
|
|
3184
|
+
}
|
|
3185
|
+
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULTS.DEFAULT_TIMEOUT_MS;
|
|
3186
|
+
return await runApeShell(a2.cmd, timeout);
|
|
3024
3187
|
}
|
|
3025
|
-
}
|
|
3188
|
+
}
|
|
3189
|
+
];
|
|
3190
|
+
var MAX_BYTES = 1024 * 1024;
|
|
3191
|
+
var extraReadRoots = /* @__PURE__ */ new Set();
|
|
3192
|
+
function isUnder(candidate, root) {
|
|
3193
|
+
return candidate === root || candidate.startsWith(`${root}/`);
|
|
3194
|
+
}
|
|
3195
|
+
function jailPath(input, opts = {}) {
|
|
3196
|
+
if (typeof input !== "string" || input === "") {
|
|
3197
|
+
throw new Error("path must be a non-empty string");
|
|
3198
|
+
}
|
|
3199
|
+
const home = homedir4();
|
|
3200
|
+
const candidate = input.startsWith("~/") ? resolve(home, input.slice(2)) : input.startsWith("/") ? normalize(input) : resolve(home, input);
|
|
3201
|
+
if (isUnder(candidate, home)) return candidate;
|
|
3202
|
+
if (opts.allowReadRoots) {
|
|
3203
|
+
for (const root of extraReadRoots) {
|
|
3204
|
+
if (isUnder(candidate, root)) return candidate;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
throw new Error(`path "${input}" resolves outside the agent's home`);
|
|
3208
|
+
}
|
|
3209
|
+
var fileTools = [
|
|
3026
3210
|
{
|
|
3027
|
-
name: "
|
|
3028
|
-
description:
|
|
3211
|
+
name: "file.read",
|
|
3212
|
+
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.",
|
|
3029
3213
|
parameters: {
|
|
3030
3214
|
type: "object",
|
|
3031
3215
|
properties: {
|
|
3032
|
-
|
|
3033
|
-
remote: remoteParam,
|
|
3034
|
-
ref: { type: "string", description: "GitHub: PR number or branch. Azure: PR id." },
|
|
3035
|
-
auto: { type: "boolean", description: "Arm merge-when-green instead of immediate merge. Recommended." },
|
|
3036
|
-
squash: { type: "boolean", description: "Squash-merge. Default true." },
|
|
3037
|
-
delete_branch: { type: "boolean", description: "Delete the source branch after merge." }
|
|
3216
|
+
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME). `..` segments are rejected." }
|
|
3038
3217
|
},
|
|
3039
|
-
required: ["
|
|
3218
|
+
required: ["path"]
|
|
3040
3219
|
},
|
|
3041
3220
|
execute: async (args) => {
|
|
3042
3221
|
const a2 = args;
|
|
3043
|
-
const
|
|
3044
|
-
|
|
3222
|
+
const p = jailPath(a2.path, { allowReadRoots: true });
|
|
3223
|
+
const content = readFileSync3(p, "utf8");
|
|
3224
|
+
if (Buffer.byteLength(content, "utf8") > MAX_BYTES) {
|
|
3225
|
+
return { path: p, truncated: true, content: content.slice(0, MAX_BYTES) };
|
|
3226
|
+
}
|
|
3227
|
+
return { path: p, truncated: false, content };
|
|
3045
3228
|
}
|
|
3046
3229
|
},
|
|
3047
3230
|
{
|
|
3048
|
-
name: "
|
|
3049
|
-
description: "
|
|
3231
|
+
name: "file.write",
|
|
3232
|
+
description: "Write a UTF-8 file under the agent's home directory. Creates parent dirs as needed. 1MB max.",
|
|
3050
3233
|
parameters: {
|
|
3051
3234
|
type: "object",
|
|
3052
|
-
properties: {
|
|
3053
|
-
|
|
3235
|
+
properties: {
|
|
3236
|
+
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME)." },
|
|
3237
|
+
content: { type: "string", description: "File body. Existing files are overwritten." }
|
|
3238
|
+
},
|
|
3239
|
+
required: ["path", "content"]
|
|
3054
3240
|
},
|
|
3055
3241
|
execute: async (args) => {
|
|
3056
3242
|
const a2 = args;
|
|
3057
|
-
|
|
3243
|
+
if (typeof a2.content !== "string") throw new Error("content must be a string");
|
|
3244
|
+
if (Buffer.byteLength(a2.content, "utf8") > MAX_BYTES) {
|
|
3245
|
+
throw new Error(`content exceeds ${MAX_BYTES} byte cap`);
|
|
3246
|
+
}
|
|
3247
|
+
const p = jailPath(a2.path);
|
|
3248
|
+
mkdirSync2(dirname2(p), { recursive: true });
|
|
3249
|
+
writeFileSync2(p, a2.content, { encoding: "utf8" });
|
|
3250
|
+
return { path: p, bytes: Buffer.byteLength(a2.content, "utf8") };
|
|
3058
3251
|
}
|
|
3059
3252
|
},
|
|
3060
3253
|
{
|
|
3061
|
-
name: "
|
|
3062
|
-
description: "
|
|
3254
|
+
name: "file.edit",
|
|
3255
|
+
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.",
|
|
3063
3256
|
parameters: {
|
|
3064
3257
|
type: "object",
|
|
3065
|
-
properties: {
|
|
3066
|
-
|
|
3258
|
+
properties: {
|
|
3259
|
+
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME)." },
|
|
3260
|
+
old_string: { type: "string", description: "Exact text to replace. Include enough surrounding context to be unique unless replace_all is set." },
|
|
3261
|
+
new_string: { type: "string", description: "Replacement text. Must differ from old_string." },
|
|
3262
|
+
replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring a unique match. Default false." }
|
|
3263
|
+
},
|
|
3264
|
+
required: ["path", "old_string", "new_string"]
|
|
3067
3265
|
},
|
|
3068
3266
|
execute: async (args) => {
|
|
3069
3267
|
const a2 = args;
|
|
3070
|
-
|
|
3071
|
-
|
|
3268
|
+
if (typeof a2.old_string !== "string" || a2.old_string === "") {
|
|
3269
|
+
throw new Error("old_string must be a non-empty string");
|
|
3270
|
+
}
|
|
3271
|
+
if (typeof a2.new_string !== "string") {
|
|
3272
|
+
throw new TypeError("new_string must be a string");
|
|
3273
|
+
}
|
|
3274
|
+
if (a2.old_string === a2.new_string) {
|
|
3275
|
+
throw new Error("old_string and new_string are identical \u2014 nothing to change");
|
|
3276
|
+
}
|
|
3277
|
+
const replaceAll = a2.replace_all === true;
|
|
3278
|
+
const p = jailPath(a2.path);
|
|
3279
|
+
const before = readFileSync3(p, "utf8");
|
|
3280
|
+
const occurrences = before.split(a2.old_string).length - 1;
|
|
3281
|
+
if (occurrences === 0) {
|
|
3282
|
+
throw new Error("old_string not found in file");
|
|
3283
|
+
}
|
|
3284
|
+
if (occurrences > 1 && !replaceAll) {
|
|
3285
|
+
throw new Error(`old_string occurs ${occurrences} times \u2014 pass replace_all:true or add surrounding context to make it unique`);
|
|
3286
|
+
}
|
|
3287
|
+
const after = replaceAll ? before.split(a2.old_string).join(a2.new_string) : before.replace(a2.old_string, a2.new_string);
|
|
3288
|
+
if (Buffer.byteLength(after, "utf8") > MAX_BYTES) {
|
|
3289
|
+
throw new Error(`result exceeds ${MAX_BYTES} byte cap`);
|
|
3290
|
+
}
|
|
3291
|
+
writeFileSync2(p, after, { encoding: "utf8" });
|
|
3292
|
+
return { path: p, replacements: replaceAll ? occurrences : 1 };
|
|
3072
3293
|
}
|
|
3073
3294
|
}
|
|
3074
3295
|
];
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3296
|
+
var BRANCH_RE = /^[\w./-]{1,200}$/;
|
|
3297
|
+
var ID_RE = /^\d{1,12}$/;
|
|
3298
|
+
function shq(s2) {
|
|
3299
|
+
return `'${String(s2).replace(/'/g, "'\\''")}'`;
|
|
3300
|
+
}
|
|
3301
|
+
function assertBranch(v2) {
|
|
3302
|
+
if (typeof v2 !== "string" || !BRANCH_RE.test(v2)) {
|
|
3303
|
+
throw new Error("branch must match ^[A-Za-z0-9._/-]{1,200}$");
|
|
3304
|
+
}
|
|
3305
|
+
return v2;
|
|
3306
|
+
}
|
|
3307
|
+
function assertId(v2) {
|
|
3308
|
+
if (typeof v2 !== "string" && typeof v2 !== "number") throw new Error("id required");
|
|
3309
|
+
const s2 = String(v2);
|
|
3310
|
+
if (!ID_RE.test(s2)) throw new Error("id must be a number");
|
|
3311
|
+
return s2;
|
|
3312
|
+
}
|
|
3313
|
+
var githubAdapter = {
|
|
3314
|
+
id: "github",
|
|
3315
|
+
matchesRemote: (url) => /github\.com/i.test(url),
|
|
3316
|
+
prCreate: (i2) => {
|
|
3317
|
+
const head = assertBranch(i2.head);
|
|
3318
|
+
const parts = ["gh", "pr", "create", "--title", shq(i2.title), "--body", shq(i2.body), "--head", shq(head)];
|
|
3319
|
+
if (i2.base !== void 0) parts.push("--base", shq(assertBranch(i2.base)));
|
|
3320
|
+
return parts.join(" ");
|
|
3321
|
+
},
|
|
3322
|
+
prMerge: (i2) => {
|
|
3323
|
+
const ref = String(i2.ref);
|
|
3324
|
+
const refTok = ID_RE.test(ref) ? ref : assertBranch(ref);
|
|
3325
|
+
const parts = ["gh", "pr", "merge", shq(refTok)];
|
|
3326
|
+
if (i2.squash === true) parts.push("--squash");
|
|
3327
|
+
if (i2.auto) parts.push("--auto");
|
|
3328
|
+
if (i2.deleteBranch) parts.push("--delete-branch");
|
|
3329
|
+
return parts.join(" ");
|
|
3330
|
+
},
|
|
3331
|
+
prStatus: (ref) => {
|
|
3332
|
+
const r3 = String(ref);
|
|
3333
|
+
const refTok = ID_RE.test(r3) ? r3 : assertBranch(r3);
|
|
3334
|
+
return `gh pr view ${shq(refTok)} --json state,mergeStateStatus,statusCheckRollup,reviewDecision`;
|
|
3335
|
+
},
|
|
3336
|
+
issueGet: (ref, repo) => `gh issue view ${assertId(ref)}${repo ? ` --repo ${shq(repo)}` : ""} --json number,title,body,labels`
|
|
3337
|
+
};
|
|
3338
|
+
var azureAdapter = {
|
|
3339
|
+
id: "azure",
|
|
3340
|
+
matchesRemote: (url) => /dev\.azure\.com|visualstudio\.com/i.test(url),
|
|
3341
|
+
prCreate: (i2) => {
|
|
3342
|
+
const head = assertBranch(i2.head);
|
|
3343
|
+
const parts = ["az", "repos", "pr", "create", "--title", shq(i2.title), "--description", shq(i2.body), "--source-branch", shq(head)];
|
|
3344
|
+
if (i2.base !== void 0) parts.push("--target-branch", shq(assertBranch(i2.base)));
|
|
3345
|
+
return parts.join(" ");
|
|
3346
|
+
},
|
|
3347
|
+
prMerge: (i2) => {
|
|
3348
|
+
const id = assertId(i2.ref);
|
|
3349
|
+
const parts = ["az", "repos", "pr", "update", "--id", id];
|
|
3350
|
+
if (i2.auto) parts.push("--auto-complete", "true");
|
|
3351
|
+
else parts.push("--status", "completed");
|
|
3352
|
+
if (i2.squash === true) parts.push("--merge-commit-message-style", "squash");
|
|
3353
|
+
if (i2.deleteBranch) parts.push("--delete-source-branch", "true");
|
|
3354
|
+
return parts.join(" ");
|
|
3355
|
+
},
|
|
3356
|
+
prStatus: (ref) => `az repos pr show --id ${assertId(ref)}`,
|
|
3357
|
+
// Azure work items are org/project-scoped, not repo-scoped, so `repo`
|
|
3358
|
+
// doesn't apply here — the caller's `az` config (defaults.organization/
|
|
3359
|
+
// project) resolves it.
|
|
3360
|
+
issueGet: (ref, _repo) => `az boards work-item show --id ${assertId(ref)}`
|
|
3361
|
+
};
|
|
3362
|
+
var registry = /* @__PURE__ */ new Map([
|
|
3363
|
+
[githubAdapter.id, githubAdapter],
|
|
3364
|
+
[azureAdapter.id, azureAdapter]
|
|
3365
|
+
]);
|
|
3366
|
+
function listForges() {
|
|
3367
|
+
return [...registry.keys()];
|
|
3368
|
+
}
|
|
3369
|
+
function getForge(id) {
|
|
3370
|
+
const a2 = registry.get(id);
|
|
3371
|
+
if (!a2) {
|
|
3372
|
+
throw new Error(`unknown forge '${id}'. Registered: ${listForges().join(", ")}. Add one with registerForge().`);
|
|
3373
|
+
}
|
|
3374
|
+
return a2;
|
|
3375
|
+
}
|
|
3376
|
+
function detectForge(remoteUrl) {
|
|
3377
|
+
if (typeof remoteUrl !== "string" || remoteUrl === "") {
|
|
3378
|
+
throw new Error("remote URL required to detect forge");
|
|
3379
|
+
}
|
|
3380
|
+
for (const a2 of registry.values()) {
|
|
3381
|
+
if (a2.matchesRemote(remoteUrl)) return a2.id;
|
|
3382
|
+
}
|
|
3383
|
+
throw new Error(`no forge adapter matches remote: ${remoteUrl}. Registered: ${listForges().join(", ")}. Register one with registerForge() (e.g. GitLab/Bitbucket/Gitea).`);
|
|
3384
|
+
}
|
|
3385
|
+
function buildPrCreate(input) {
|
|
3386
|
+
return getForge(input.forge).prCreate(input);
|
|
3387
|
+
}
|
|
3388
|
+
function buildPrMerge(input) {
|
|
3389
|
+
return getForge(input.forge).prMerge(input);
|
|
3390
|
+
}
|
|
3391
|
+
function buildPrStatus(forge, ref) {
|
|
3392
|
+
return getForge(forge).prStatus(ref);
|
|
3393
|
+
}
|
|
3394
|
+
function buildIssueGet(forge, ref, repo) {
|
|
3395
|
+
return getForge(forge).issueGet(ref, repo);
|
|
3396
|
+
}
|
|
3397
|
+
function resolveForge(a2) {
|
|
3398
|
+
if (typeof a2.forge === "string" && a2.forge !== "") return a2.forge;
|
|
3399
|
+
if (typeof a2.remote === "string") return detectForge(a2.remote);
|
|
3400
|
+
throw new Error("provide a forge id (e.g. github, azure, or a registered adapter) or a remote URL to detect it");
|
|
3401
|
+
}
|
|
3402
|
+
var forgeParam = { type: "string", description: "Target forge id (github, azure, or a registered adapter). Omit to auto-detect from `remote`." };
|
|
3403
|
+
var remoteParam = { type: "string", description: "git remote URL \u2014 used to auto-detect the forge when `forge` is omitted." };
|
|
3404
|
+
var forgeTools = [
|
|
3405
|
+
{
|
|
3406
|
+
name: "forge.pr.create",
|
|
3407
|
+
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`.",
|
|
3408
|
+
parameters: {
|
|
3409
|
+
type: "object",
|
|
3410
|
+
properties: {
|
|
3411
|
+
forge: forgeParam,
|
|
3412
|
+
remote: remoteParam,
|
|
3413
|
+
title: { type: "string", description: "PR title." },
|
|
3414
|
+
body: { type: "string", description: "PR description / body." },
|
|
3415
|
+
head: { type: "string", description: "Source branch." },
|
|
3416
|
+
base: { type: "string", description: "Target branch. Omit for the repo default." }
|
|
3417
|
+
},
|
|
3418
|
+
required: ["title", "body", "head"]
|
|
3419
|
+
},
|
|
3420
|
+
execute: async (args) => {
|
|
3421
|
+
const a2 = args;
|
|
3422
|
+
const cmd = buildPrCreate({ forge: resolveForge(a2), title: a2.title, body: a2.body, head: a2.head, base: a2.base });
|
|
3423
|
+
return await runApeShell(cmd);
|
|
3424
|
+
}
|
|
3425
|
+
},
|
|
3426
|
+
{
|
|
3427
|
+
name: "forge.pr.merge",
|
|
3428
|
+
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).',
|
|
3429
|
+
parameters: {
|
|
3430
|
+
type: "object",
|
|
3431
|
+
properties: {
|
|
3432
|
+
forge: forgeParam,
|
|
3433
|
+
remote: remoteParam,
|
|
3434
|
+
ref: { type: "string", description: "GitHub: PR number or branch. Azure: PR id." },
|
|
3435
|
+
auto: { type: "boolean", description: "Arm merge-when-green instead of immediate merge. Recommended." },
|
|
3436
|
+
squash: { type: "boolean", description: "Squash-merge. Default true." },
|
|
3437
|
+
delete_branch: { type: "boolean", description: "Delete the source branch after merge." }
|
|
3438
|
+
},
|
|
3439
|
+
required: ["ref"]
|
|
3440
|
+
},
|
|
3441
|
+
execute: async (args) => {
|
|
3442
|
+
const a2 = args;
|
|
3443
|
+
const cmd = buildPrMerge({ forge: resolveForge(a2), ref: a2.ref, auto: a2.auto, squash: a2.squash, deleteBranch: a2.delete_branch });
|
|
3444
|
+
return await runApeShell(cmd);
|
|
3445
|
+
}
|
|
3446
|
+
},
|
|
3447
|
+
{
|
|
3448
|
+
name: "forge.pr.status",
|
|
3449
|
+
description: "Fetch a PR's state + checks + review decision. Gated (read).",
|
|
3450
|
+
parameters: {
|
|
3451
|
+
type: "object",
|
|
3452
|
+
properties: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "PR number/branch (GitHub) or id (Azure)." } },
|
|
3453
|
+
required: ["ref"]
|
|
3454
|
+
},
|
|
3455
|
+
execute: async (args) => {
|
|
3456
|
+
const a2 = args;
|
|
3457
|
+
return await runApeShell(buildPrStatus(resolveForge(a2), a2.ref));
|
|
3458
|
+
}
|
|
3459
|
+
},
|
|
3460
|
+
{
|
|
3461
|
+
name: "forge.issue.get",
|
|
3462
|
+
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.",
|
|
3463
|
+
parameters: {
|
|
3464
|
+
type: "object",
|
|
3465
|
+
properties: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "Issue number (GitHub) or work-item id (Azure)." } },
|
|
3466
|
+
required: ["ref"]
|
|
3467
|
+
},
|
|
3468
|
+
execute: async (args) => {
|
|
3469
|
+
const a2 = args;
|
|
3470
|
+
const repo = typeof a2.remote === "string" ? a2.remote : void 0;
|
|
3471
|
+
return await runApeShell(buildIssueGet(resolveForge(a2), a2.ref, repo));
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
];
|
|
3475
|
+
function jailedRoot(envVar, fallbackName) {
|
|
3476
|
+
const home = homedir24();
|
|
3477
|
+
const raw = process2.env[envVar];
|
|
3478
|
+
const dir = raw ? resolve2(raw) : resolve2(home, fallbackName);
|
|
3479
|
+
if (dir !== home && !dir.startsWith(`${home}/`)) {
|
|
3480
|
+
throw new Error(`${envVar} (${dir}) must resolve inside the agent's home`);
|
|
3481
|
+
}
|
|
3482
|
+
return dir;
|
|
3083
3483
|
}
|
|
3084
3484
|
function workRoot() {
|
|
3085
3485
|
return jailedRoot("OPENAPE_CODING_WORK_DIR", "work");
|
|
@@ -3106,7 +3506,7 @@ function resolveRepo(repo) {
|
|
|
3106
3506
|
if (typeof repo !== "string" || repo === "") {
|
|
3107
3507
|
throw new Error("repo must be a non-empty string (URL or path under $HOME)");
|
|
3108
3508
|
}
|
|
3109
|
-
const home =
|
|
3509
|
+
const home = homedir24();
|
|
3110
3510
|
if (URL_RE.test(repo)) {
|
|
3111
3511
|
const tail = repo.replace(/\.git$/, "").replace(/[/:]+$/, "");
|
|
3112
3512
|
const parts = tail.split(/[/:]/).filter(Boolean).slice(-2);
|
|
@@ -3361,6 +3761,99 @@ var mailTools = [
|
|
|
3361
3761
|
}
|
|
3362
3762
|
}
|
|
3363
3763
|
];
|
|
3764
|
+
function troopBase() {
|
|
3765
|
+
return (process.env.OPENAPE_TROOP_URL ?? "https://troop.openape.ai").replace(/\/+$/, "");
|
|
3766
|
+
}
|
|
3767
|
+
function readAgentToken() {
|
|
3768
|
+
const path = process.env.OPENAPE_CLI_AUTH_HOME ? join4(process.env.OPENAPE_CLI_AUTH_HOME, "auth.json") : join4(homedir32(), ".config", "apes", "auth.json");
|
|
3769
|
+
const auth = JSON.parse(readFileSync23(path, "utf8"));
|
|
3770
|
+
if (!auth.access_token) throw new Error(`no access_token in ${path}`);
|
|
3771
|
+
return auth.access_token;
|
|
3772
|
+
}
|
|
3773
|
+
async function exchangeBearer(base, scope) {
|
|
3774
|
+
const res = await fetch(`${base}/api/cli/exchange`, {
|
|
3775
|
+
method: "POST",
|
|
3776
|
+
headers: { "content-type": "application/json" },
|
|
3777
|
+
body: JSON.stringify({ subject_token: readAgentToken(), scopes: [scope] })
|
|
3778
|
+
});
|
|
3779
|
+
if (!res.ok) {
|
|
3780
|
+
throw new Error(`token exchange ${res.status} \u2014 ${(await res.text().catch(() => "")).slice(0, 200)} (does this agent hold the ${scope} scope?)`);
|
|
3781
|
+
}
|
|
3782
|
+
return (await res.json()).access_token;
|
|
3783
|
+
}
|
|
3784
|
+
var spawnTools = [
|
|
3785
|
+
{
|
|
3786
|
+
name: "agent.spawn",
|
|
3787
|
+
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.",
|
|
3788
|
+
parameters: {
|
|
3789
|
+
type: "object",
|
|
3790
|
+
properties: {
|
|
3791
|
+
name: { type: "string", description: "unique worker name, /^[a-z][a-z0-9-]{0,23}$/" },
|
|
3792
|
+
model: { type: "string", description: "gpt-5.4-mini | gpt-5.4 | gpt-5.5" },
|
|
3793
|
+
reasoning_effort: { type: "string", description: "minimal | low | medium | high" },
|
|
3794
|
+
recipe_ref: { type: "string", description: "optional recipe, e.g. github.com/openape-ai/agent-catalog/backend-engineer@v0.2.0" },
|
|
3795
|
+
system_prompt: { type: "string", description: "optional system prompt / task brief" }
|
|
3796
|
+
},
|
|
3797
|
+
required: ["name"]
|
|
3798
|
+
},
|
|
3799
|
+
execute: async (args) => {
|
|
3800
|
+
const a2 = args;
|
|
3801
|
+
const base = troopBase();
|
|
3802
|
+
let bearer;
|
|
3803
|
+
try {
|
|
3804
|
+
bearer = await exchangeBearer(base, "troop:spawn-agent");
|
|
3805
|
+
} catch (err) {
|
|
3806
|
+
return `spawn failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
3807
|
+
}
|
|
3808
|
+
const body = { name: a2.name };
|
|
3809
|
+
if (a2.model) body.bridge_model = a2.model;
|
|
3810
|
+
if (a2.reasoning_effort) body.bridge_reasoning_effort = a2.reasoning_effort;
|
|
3811
|
+
if (a2.system_prompt) body.system_prompt = a2.system_prompt;
|
|
3812
|
+
if (a2.recipe_ref) body.recipe = { repo_ref: a2.recipe_ref, params: {} };
|
|
3813
|
+
const spRes = await fetch(`${base}/api/agents/spawn-intent`, {
|
|
3814
|
+
method: "POST",
|
|
3815
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${bearer}` },
|
|
3816
|
+
body: JSON.stringify(body)
|
|
3817
|
+
});
|
|
3818
|
+
if (!spRes.ok) {
|
|
3819
|
+
return `spawn failed: spawn-intent ${spRes.status} \u2014 ${(await spRes.text().catch(() => "")).slice(0, 200)}`;
|
|
3820
|
+
}
|
|
3821
|
+
const sp = await spRes.json();
|
|
3822
|
+
return `spawned worker "${a2.name}" (model=${a2.model ?? "default"}, reasoning=${a2.reasoning_effort ?? "default"}); intent=${sp.intent_id ?? "?"}`;
|
|
3823
|
+
}
|
|
3824
|
+
},
|
|
3825
|
+
{
|
|
3826
|
+
name: "agent.destroy",
|
|
3827
|
+
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.",
|
|
3828
|
+
parameters: {
|
|
3829
|
+
type: "object",
|
|
3830
|
+
properties: {
|
|
3831
|
+
name: { type: "string", description: "the worker agent name to destroy" }
|
|
3832
|
+
},
|
|
3833
|
+
required: ["name"]
|
|
3834
|
+
},
|
|
3835
|
+
execute: async (args) => {
|
|
3836
|
+
const a2 = args;
|
|
3837
|
+
const base = troopBase();
|
|
3838
|
+
let bearer;
|
|
3839
|
+
try {
|
|
3840
|
+
bearer = await exchangeBearer(base, "troop:destroy-agent");
|
|
3841
|
+
} catch (err) {
|
|
3842
|
+
return `destroy failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
3843
|
+
}
|
|
3844
|
+
const res = await fetch(`${base}/api/agents/destroy-intent`, {
|
|
3845
|
+
method: "POST",
|
|
3846
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${bearer}` },
|
|
3847
|
+
body: JSON.stringify({ name: a2.name })
|
|
3848
|
+
});
|
|
3849
|
+
if (!res.ok) {
|
|
3850
|
+
return `destroy failed: destroy-intent ${res.status} \u2014 ${(await res.text().catch(() => "")).slice(0, 200)}`;
|
|
3851
|
+
}
|
|
3852
|
+
const d2 = await res.json();
|
|
3853
|
+
return `destroying worker "${a2.name}"; intent=${d2.intent_id ?? "?"}`;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
];
|
|
3364
3857
|
function ape(args) {
|
|
3365
3858
|
try {
|
|
3366
3859
|
return execFileSync2("ape-tasks", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -3397,14 +3890,17 @@ var tasksTools = [
|
|
|
3397
3890
|
},
|
|
3398
3891
|
{
|
|
3399
3892
|
name: "tasks.create",
|
|
3400
|
-
description: "Create a new ape-task
|
|
3893
|
+
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.",
|
|
3401
3894
|
parameters: {
|
|
3402
3895
|
type: "object",
|
|
3403
3896
|
properties: {
|
|
3404
3897
|
title: { type: "string" },
|
|
3405
3898
|
notes: { type: "string" },
|
|
3406
3899
|
priority: { type: "string", enum: ["low", "med", "high"] },
|
|
3407
|
-
due_at: { type: "string", description: "ISO date or +Nh/+Nd shorthand." }
|
|
3900
|
+
due_at: { type: "string", description: "ISO date or +Nh/+Nd shorthand." },
|
|
3901
|
+
team: { type: "string", description: "Team id to file the task on (required when you belong to a team)." },
|
|
3902
|
+
assignee: { type: "string", description: "Email of the teammate to assign the task to." },
|
|
3903
|
+
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." }
|
|
3408
3904
|
},
|
|
3409
3905
|
required: ["title"]
|
|
3410
3906
|
},
|
|
@@ -3414,6 +3910,9 @@ var tasksTools = [
|
|
|
3414
3910
|
if (a2.notes) argv2.push("--notes", a2.notes);
|
|
3415
3911
|
if (a2.priority) argv2.push("--priority", a2.priority);
|
|
3416
3912
|
if (a2.due_at) argv2.push("--due", a2.due_at);
|
|
3913
|
+
if (a2.team) argv2.push("--team", a2.team);
|
|
3914
|
+
if (a2.assignee) argv2.push("--assignee", a2.assignee);
|
|
3915
|
+
if (a2.dedup_key) argv2.push("--dedup-key", a2.dedup_key);
|
|
3417
3916
|
const out = ape(argv2);
|
|
3418
3917
|
try {
|
|
3419
3918
|
return JSON.parse(out);
|
|
@@ -3438,6 +3937,83 @@ var timeTools = [
|
|
|
3438
3937
|
}
|
|
3439
3938
|
}
|
|
3440
3939
|
];
|
|
3940
|
+
var TROOP = "https://troop.openape.ai";
|
|
3941
|
+
var RESOURCES = ["objectives", "reports", "members", "cost-snapshots", "overview"];
|
|
3942
|
+
function pathFor(resource, orgId) {
|
|
3943
|
+
const id = encodeURIComponent(orgId);
|
|
3944
|
+
return resource === "overview" ? `/api/orgs/${id}` : `/api/orgs/${id}/${resource}`;
|
|
3945
|
+
}
|
|
3946
|
+
var OBJECTIVE_STATUS = ["planned", "in_progress", "done", "abandoned"];
|
|
3947
|
+
var troopTools = [
|
|
3948
|
+
{
|
|
3949
|
+
name: "troop.company.read",
|
|
3950
|
+
description: "Read your troop company data on troop.openape.ai. resource: objectives | reports | members | cost-snapshots | overview (vision+budget). Read-only.",
|
|
3951
|
+
parameters: {
|
|
3952
|
+
type: "object",
|
|
3953
|
+
properties: {
|
|
3954
|
+
resource: { type: "string", enum: [...RESOURCES], description: "Which company resource to read." },
|
|
3955
|
+
org_id: { type: "string", description: "Your company (org) id." }
|
|
3956
|
+
},
|
|
3957
|
+
required: ["resource", "org_id"]
|
|
3958
|
+
},
|
|
3959
|
+
execute: async (args) => {
|
|
3960
|
+
const { resource, org_id } = args ?? {};
|
|
3961
|
+
if (!resource || !RESOURCES.includes(resource)) {
|
|
3962
|
+
throw new Error(`troop.company.read: unknown resource '${resource}' (expected ${RESOURCES.join(" | ")})`);
|
|
3963
|
+
}
|
|
3964
|
+
if (!org_id) throw new Error("troop.company.read: org_id is required");
|
|
3965
|
+
const bearer = await getAuthorizedBearer({ endpoint: TROOP, aud: "troop.openape.ai" });
|
|
3966
|
+
const res = await fetch(`${TROOP}${pathFor(resource, org_id)}`, {
|
|
3967
|
+
headers: { authorization: bearer }
|
|
3968
|
+
});
|
|
3969
|
+
if (!res.ok) {
|
|
3970
|
+
throw new Error(`troop.company.read ${resource} \u2192 ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
3971
|
+
}
|
|
3972
|
+
return JSON.stringify(await res.json());
|
|
3973
|
+
}
|
|
3974
|
+
},
|
|
3975
|
+
{
|
|
3976
|
+
name: "troop.objective.upsert",
|
|
3977
|
+
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).",
|
|
3978
|
+
parameters: {
|
|
3979
|
+
type: "object",
|
|
3980
|
+
properties: {
|
|
3981
|
+
org_id: { type: "string", description: "Your company (org) id." },
|
|
3982
|
+
objective_id: { type: "string", description: "Omit to create; pass to update an existing objective." },
|
|
3983
|
+
title: { type: "string" },
|
|
3984
|
+
description: { type: "string" },
|
|
3985
|
+
status: { type: "string", enum: [...OBJECTIVE_STATUS] },
|
|
3986
|
+
target_date: { type: "number", description: "Unix seconds, or null to clear." }
|
|
3987
|
+
},
|
|
3988
|
+
required: ["org_id"]
|
|
3989
|
+
},
|
|
3990
|
+
execute: async (args) => {
|
|
3991
|
+
const a2 = args ?? {};
|
|
3992
|
+
if (!a2.org_id) throw new Error("troop.objective.upsert: org_id is required");
|
|
3993
|
+
if (a2.status && !OBJECTIVE_STATUS.includes(a2.status)) {
|
|
3994
|
+
throw new Error(`troop.objective.upsert: bad status '${a2.status}'`);
|
|
3995
|
+
}
|
|
3996
|
+
if (!a2.objective_id && !a2.title) throw new Error("troop.objective.upsert: title is required to create an objective");
|
|
3997
|
+
const bearer = await getAuthorizedBearer({ endpoint: TROOP, aud: "troop.openape.ai" });
|
|
3998
|
+
const id = encodeURIComponent(a2.org_id);
|
|
3999
|
+
const body = {};
|
|
4000
|
+
if (a2.title !== void 0) body.title = a2.title;
|
|
4001
|
+
if (a2.description !== void 0) body.description = a2.description;
|
|
4002
|
+
if (a2.status !== void 0) body.status = a2.status;
|
|
4003
|
+
if (a2.target_date !== void 0) body.target_date = a2.target_date;
|
|
4004
|
+
const url = a2.objective_id ? `${TROOP}/api/orgs/${id}/objectives/${encodeURIComponent(a2.objective_id)}` : `${TROOP}/api/orgs/${id}/objectives`;
|
|
4005
|
+
const res = await fetch(url, {
|
|
4006
|
+
method: a2.objective_id ? "PATCH" : "POST",
|
|
4007
|
+
headers: { authorization: bearer, "content-type": "application/json" },
|
|
4008
|
+
body: JSON.stringify(body)
|
|
4009
|
+
});
|
|
4010
|
+
if (!res.ok) {
|
|
4011
|
+
throw new Error(`troop.objective.upsert \u2192 ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
4012
|
+
}
|
|
4013
|
+
return JSON.stringify(await res.json());
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
];
|
|
3441
4017
|
var CWD_RE = /^[\w./-]{1,256}$/;
|
|
3442
4018
|
async function runVerify(cwd, command, timeoutMs) {
|
|
3443
4019
|
if (typeof cwd !== "string" || !CWD_RE.test(cwd)) {
|
|
@@ -3484,7 +4060,9 @@ var ALL_TOOLS = [
|
|
|
3484
4060
|
...bashTools,
|
|
3485
4061
|
...gitWorktreeTools,
|
|
3486
4062
|
...verifyTools,
|
|
3487
|
-
...forgeTools
|
|
4063
|
+
...forgeTools,
|
|
4064
|
+
...spawnTools,
|
|
4065
|
+
...troopTools
|
|
3488
4066
|
];
|
|
3489
4067
|
var TOOLS = Object.fromEntries(
|
|
3490
4068
|
ALL_TOOLS.map((t2) => [t2.name, t2])
|
|
@@ -3497,509 +4075,214 @@ function taskTools(names) {
|
|
|
3497
4075
|
if (!tool) missing.push(name);
|
|
3498
4076
|
else out.push(tool);
|
|
3499
4077
|
}
|
|
3500
|
-
if (missing.length > 0) {
|
|
3501
|
-
throw new Error(`unknown tool(s): ${missing.join(", ")}`);
|
|
3502
|
-
}
|
|
3503
|
-
return out;
|
|
3504
|
-
}
|
|
3505
|
-
function asOpenAiTools(tools) {
|
|
3506
|
-
return tools.map((t2) => ({
|
|
3507
|
-
type: "function",
|
|
3508
|
-
function: { name: wireToolName(t2.name), description: t2.description, parameters: t2.parameters }
|
|
3509
|
-
}));
|
|
3510
|
-
}
|
|
3511
|
-
function wireToolName(local) {
|
|
3512
|
-
return local.replace(/\./g, "_");
|
|
3513
|
-
}
|
|
3514
|
-
function localToolName(wire) {
|
|
3515
|
-
for (const t2 of Object.values(TOOLS)) {
|
|
3516
|
-
if (wireToolName(t2.name) === wire) return t2.name;
|
|
3517
|
-
}
|
|
3518
|
-
return wire;
|
|
3519
|
-
}
|
|
3520
|
-
function previewJson(value, max = 500) {
|
|
3521
|
-
let s2;
|
|
3522
|
-
try {
|
|
3523
|
-
s2 = JSON.stringify(value);
|
|
3524
|
-
} catch {
|
|
3525
|
-
s2 = String(value);
|
|
3526
|
-
}
|
|
3527
|
-
return s2.length > max ? `${s2.slice(0, max)}\u2026` : s2;
|
|
3528
|
-
}
|
|
3529
|
-
async function aggregateChatStream(res) {
|
|
3530
|
-
if (!res.body) throw new Error("LiteLLM streaming response had no body");
|
|
3531
|
-
const reader = res.body.getReader();
|
|
3532
|
-
const decoder = new TextDecoder();
|
|
3533
|
-
let buf = "";
|
|
3534
|
-
let content = "";
|
|
3535
|
-
const toolCalls = /* @__PURE__ */ new Map();
|
|
3536
|
-
let finishReason;
|
|
3537
|
-
while (true) {
|
|
3538
|
-
const { value, done } = await reader.read();
|
|
3539
|
-
if (done) break;
|
|
3540
|
-
buf += decoder.decode(value, { stream: true });
|
|
3541
|
-
while (true) {
|
|
3542
|
-
const nl = buf.indexOf("\n");
|
|
3543
|
-
if (nl === -1) break;
|
|
3544
|
-
const line = buf.slice(0, nl).trim();
|
|
3545
|
-
buf = buf.slice(nl + 1);
|
|
3546
|
-
if (!line.startsWith("data:")) continue;
|
|
3547
|
-
const payload = line.slice(5).trim();
|
|
3548
|
-
if (!payload || payload === "[DONE]") continue;
|
|
3549
|
-
let chunk;
|
|
3550
|
-
try {
|
|
3551
|
-
chunk = JSON.parse(payload);
|
|
3552
|
-
} catch {
|
|
3553
|
-
continue;
|
|
3554
|
-
}
|
|
3555
|
-
const ch0 = chunk.choices?.[0];
|
|
3556
|
-
const delta = ch0?.delta;
|
|
3557
|
-
if (delta?.content) content += delta.content;
|
|
3558
|
-
if (delta?.tool_calls) {
|
|
3559
|
-
for (const tc of delta.tool_calls) {
|
|
3560
|
-
const idx = tc.index ?? 0;
|
|
3561
|
-
const existing = toolCalls.get(idx) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
|
|
3562
|
-
if (tc.id) existing.id = tc.id;
|
|
3563
|
-
if (tc.function?.name) existing.function.name = tc.function.name;
|
|
3564
|
-
if (tc.function?.arguments) existing.function.arguments += tc.function.arguments;
|
|
3565
|
-
toolCalls.set(idx, existing);
|
|
3566
|
-
}
|
|
3567
|
-
}
|
|
3568
|
-
if (ch0?.finish_reason) finishReason = ch0.finish_reason;
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
const message = { role: "assistant", content: content || null };
|
|
3572
|
-
if (toolCalls.size > 0) message.tool_calls = Array.from(toolCalls.values());
|
|
3573
|
-
return { choices: [{ message, finish_reason: finishReason }] };
|
|
3574
|
-
}
|
|
3575
|
-
async function runLoop(opts) {
|
|
3576
|
-
const fetchFn = opts.fetchImpl ?? fetch;
|
|
3577
|
-
const trace = [];
|
|
3578
|
-
const messages = [
|
|
3579
|
-
{ role: "system", content: opts.systemPrompt },
|
|
3580
|
-
...opts.history ?? [],
|
|
3581
|
-
{ role: "user", content: opts.userMessage }
|
|
3582
|
-
];
|
|
3583
|
-
const tools = asOpenAiTools(opts.tools);
|
|
3584
|
-
for (let step = 1; step <= opts.maxSteps; step++) {
|
|
3585
|
-
const requestBody = {
|
|
3586
|
-
model: opts.config.model,
|
|
3587
|
-
messages,
|
|
3588
|
-
...tools.length > 0 ? { tools, tool_choice: "auto" } : {},
|
|
3589
|
-
...opts.streamAggregate ? { stream: true } : {}
|
|
3590
|
-
};
|
|
3591
|
-
const res = await fetchFn(`${opts.config.apiBase}/chat/completions`, {
|
|
3592
|
-
method: "POST",
|
|
3593
|
-
headers: {
|
|
3594
|
-
"authorization": `Bearer ${opts.config.apiKey}`,
|
|
3595
|
-
"content-type": "application/json"
|
|
3596
|
-
},
|
|
3597
|
-
body: JSON.stringify(requestBody)
|
|
3598
|
-
});
|
|
3599
|
-
if (!res.ok) {
|
|
3600
|
-
const text = await res.text().catch(() => "");
|
|
3601
|
-
throw new Error(`LiteLLM ${res.status}: ${text.slice(0, 500)}`);
|
|
3602
|
-
}
|
|
3603
|
-
const data = opts.streamAggregate ? await aggregateChatStream(res) : await res.json();
|
|
3604
|
-
const choice = data.choices?.[0];
|
|
3605
|
-
if (!choice) throw new Error("LiteLLM response had no choices");
|
|
3606
|
-
const assistant = choice.message;
|
|
3607
|
-
messages.push(assistant);
|
|
3608
|
-
if (assistant.content) opts.handlers?.onTextDelta?.(assistant.content);
|
|
3609
|
-
trace.push({
|
|
3610
|
-
step,
|
|
3611
|
-
type: "assistant",
|
|
3612
|
-
preview: previewJson({ content: assistant.content, tool_calls: assistant.tool_calls?.length ?? 0 })
|
|
3613
|
-
});
|
|
3614
|
-
if (!assistant.tool_calls || assistant.tool_calls.length === 0) {
|
|
3615
|
-
const result2 = {
|
|
3616
|
-
status: "ok",
|
|
3617
|
-
finalMessage: assistant.content,
|
|
3618
|
-
stepCount: step,
|
|
3619
|
-
trace
|
|
3620
|
-
};
|
|
3621
|
-
opts.handlers?.onDone?.(result2);
|
|
3622
|
-
return result2;
|
|
3623
|
-
}
|
|
3624
|
-
for (const call of assistant.tool_calls) {
|
|
3625
|
-
const wireName = call.function.name;
|
|
3626
|
-
const localName = localToolName(wireName);
|
|
3627
|
-
const tool = opts.tools.find((t2) => t2.name === localName);
|
|
3628
|
-
let parsedArgs;
|
|
3629
|
-
try {
|
|
3630
|
-
parsedArgs = JSON.parse(call.function.arguments);
|
|
3631
|
-
} catch {
|
|
3632
|
-
parsedArgs = {};
|
|
3633
|
-
}
|
|
3634
|
-
opts.handlers?.onToolCall?.({ name: localName, args: parsedArgs });
|
|
3635
|
-
trace.push({ step, type: "tool_call", tool: localName, preview: previewJson(parsedArgs) });
|
|
3636
|
-
let result2;
|
|
3637
|
-
let isError = false;
|
|
3638
|
-
if (!tool) {
|
|
3639
|
-
result2 = `unknown tool: ${localName}`;
|
|
3640
|
-
isError = true;
|
|
3641
|
-
} else {
|
|
3642
|
-
try {
|
|
3643
|
-
result2 = await tool.execute(parsedArgs);
|
|
3644
|
-
} catch (err) {
|
|
3645
|
-
result2 = err?.message ?? String(err);
|
|
3646
|
-
isError = true;
|
|
3647
|
-
}
|
|
3648
|
-
}
|
|
3649
|
-
if (isError) {
|
|
3650
|
-
opts.handlers?.onToolError?.({ name: localName, error: String(result2) });
|
|
3651
|
-
trace.push({ step, type: "tool_error", tool: localName, preview: previewJson(result2) });
|
|
3652
|
-
} else {
|
|
3653
|
-
opts.handlers?.onToolResult?.({ name: localName, result: result2 });
|
|
3654
|
-
trace.push({ step, type: "tool_result", tool: localName, preview: previewJson(result2) });
|
|
3655
|
-
}
|
|
3656
|
-
messages.push({
|
|
3657
|
-
role: "tool",
|
|
3658
|
-
tool_call_id: call.id,
|
|
3659
|
-
name: wireToolName(localName),
|
|
3660
|
-
content: typeof result2 === "string" ? result2 : JSON.stringify(result2)
|
|
3661
|
-
});
|
|
3662
|
-
}
|
|
3663
|
-
}
|
|
3664
|
-
const result = {
|
|
3665
|
-
status: "error",
|
|
3666
|
-
finalMessage: `max_steps (${opts.maxSteps}) reached without completion`,
|
|
3667
|
-
stepCount: opts.maxSteps,
|
|
3668
|
-
trace
|
|
3669
|
-
};
|
|
3670
|
-
opts.handlers?.onDone?.(result);
|
|
3671
|
-
return result;
|
|
3672
|
-
}
|
|
3673
|
-
var RPC_SESSION_TTL_MS = 60 * 60 * 1e3;
|
|
3674
|
-
var DEFAULT_INSTRUCTIONS = [
|
|
3675
|
-
"Work in the provided worktree. When done:",
|
|
3676
|
-
"- ensure the verification command passes (no PR on red)",
|
|
3677
|
-
"- open a PR that references this issue",
|
|
3678
|
-
"- leave risk-path / agent-judged-risky changes for human approval"
|
|
3679
|
-
].join("\n");
|
|
3680
|
-
var DIFF_CAP = 60 * 1024;
|
|
3681
|
-
var DIFF_CAP2 = 48 * 1024;
|
|
3682
|
-
var RISK_SYSTEM = [
|
|
3683
|
-
"You are a security/risk classifier for an autonomous coding agent.",
|
|
3684
|
-
"Given a diff + changed file paths, decide whether merging it WITHOUT a human is risky.",
|
|
3685
|
-
"Risky = touches authentication, authorization, secrets/credentials, payment, data migrations,",
|
|
3686
|
-
"deploy/release/CI config, cryptography, deletion of data, or anything whose failure is hard to",
|
|
3687
|
-
"reverse in production. Routine code/tests/docs/refactors are NOT risky.",
|
|
3688
|
-
'Respond ONLY as JSON: {"risky": boolean, "reason": string}.'
|
|
3689
|
-
].join(" ");
|
|
3690
|
-
var REVIEW_SYSTEM = [
|
|
3691
|
-
"You are a code reviewer for an autonomous coding agent.",
|
|
3692
|
-
"Given a PR diff, decide whether it is correct, safe, and complete enough to auto-merge.",
|
|
3693
|
-
"Approve only if you would be comfortable shipping it without further human review.",
|
|
3694
|
-
"Block if you see bugs, missing tests, security issues, or incomplete work.",
|
|
3695
|
-
'Respond ONLY as JSON: {"approved": boolean, "reason": string}.'
|
|
3696
|
-
].join(" ");
|
|
3697
|
-
|
|
3698
|
-
// ../../packages/cli-auth/dist/index.js
|
|
3699
|
-
import { ofetch } from "ofetch";
|
|
3700
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
3701
|
-
import { homedir as homedir4 } from "os";
|
|
3702
|
-
import { join as join3 } from "path";
|
|
3703
|
-
import { ofetch as ofetch3 } from "ofetch";
|
|
3704
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
3705
|
-
import { sign } from "crypto";
|
|
3706
|
-
import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
|
|
3707
|
-
import { homedir as homedir23 } from "os";
|
|
3708
|
-
import { join as join22 } from "path";
|
|
3709
|
-
import { ofetch as ofetch2 } from "ofetch";
|
|
3710
|
-
import { Buffer as Buffer3 } from "buffer";
|
|
3711
|
-
import { createPrivateKey } from "crypto";
|
|
3712
|
-
import { ofetch as ofetch4 } from "ofetch";
|
|
3713
|
-
import { ofetch as ofetch5 } from "ofetch";
|
|
3714
|
-
function getConfigDir() {
|
|
3715
|
-
const override = process.env.OPENAPE_CLI_AUTH_HOME;
|
|
3716
|
-
if (override) return override;
|
|
3717
|
-
return join3(homedir4(), ".config", "apes");
|
|
3718
|
-
}
|
|
3719
|
-
function getAuthFile() {
|
|
3720
|
-
return join3(getConfigDir(), "auth.json");
|
|
3721
|
-
}
|
|
3722
|
-
function ensureConfigDir() {
|
|
3723
|
-
const dir = getConfigDir();
|
|
3724
|
-
if (!existsSync(dir)) {
|
|
3725
|
-
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
3726
|
-
}
|
|
3727
|
-
}
|
|
3728
|
-
function loadIdpAuth() {
|
|
3729
|
-
const file = getAuthFile();
|
|
3730
|
-
if (!existsSync(file)) return null;
|
|
3731
|
-
try {
|
|
3732
|
-
const raw = readFileSync3(file, "utf-8");
|
|
3733
|
-
if (!raw.trim()) return null;
|
|
3734
|
-
return JSON.parse(raw);
|
|
3735
|
-
} catch {
|
|
3736
|
-
return null;
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
function saveIdpAuth(auth) {
|
|
3740
|
-
ensureConfigDir();
|
|
3741
|
-
const file = getAuthFile();
|
|
3742
|
-
let extra = {};
|
|
3743
|
-
if (existsSync(file)) {
|
|
3744
|
-
try {
|
|
3745
|
-
const raw = readFileSync3(file, "utf-8");
|
|
3746
|
-
if (raw.trim()) {
|
|
3747
|
-
const prev = JSON.parse(raw);
|
|
3748
|
-
for (const key of Object.keys(prev)) {
|
|
3749
|
-
if (!(key in auth)) {
|
|
3750
|
-
extra[key] = prev[key];
|
|
3751
|
-
}
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
} catch {
|
|
3755
|
-
extra = {};
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
const merged = { ...extra, ...auth };
|
|
3759
|
-
writeFileSync2(file, JSON.stringify(merged, null, 2), { mode: 384 });
|
|
3760
|
-
}
|
|
3761
|
-
var AuthError = class extends Error {
|
|
3762
|
-
status;
|
|
3763
|
-
hint;
|
|
3764
|
-
constructor(status, message, hint) {
|
|
3765
|
-
super(hint ? `${message}
|
|
3766
|
-
${hint}` : message);
|
|
3767
|
-
this.name = "AuthError";
|
|
3768
|
-
this.status = status;
|
|
3769
|
-
this.hint = hint;
|
|
3770
|
-
}
|
|
3771
|
-
};
|
|
3772
|
-
var NotLoggedInError = class extends AuthError {
|
|
3773
|
-
constructor(hint) {
|
|
3774
|
-
super(
|
|
3775
|
-
401,
|
|
3776
|
-
"Not logged in",
|
|
3777
|
-
hint ?? "Run `apes login <email>` once on this device to authenticate against the OpenApe IdP."
|
|
3778
|
-
);
|
|
3779
|
-
this.name = "NotLoggedInError";
|
|
3780
|
-
}
|
|
3781
|
-
};
|
|
3782
|
-
var OPENSSH_MAGIC = "openssh-key-v1\0";
|
|
3783
|
-
function loadEd25519PrivateKey(pem) {
|
|
3784
|
-
if (pem.includes("BEGIN OPENSSH PRIVATE KEY")) {
|
|
3785
|
-
return parseOpenSSHEd25519(pem);
|
|
3786
|
-
}
|
|
3787
|
-
return createPrivateKey(pem);
|
|
3788
|
-
}
|
|
3789
|
-
function parseOpenSSHEd25519(pem) {
|
|
3790
|
-
const b64 = pem.replace(/-----BEGIN OPENSSH PRIVATE KEY-----/, "").replace(/-----END OPENSSH PRIVATE KEY-----/, "").replace(/\s/g, "");
|
|
3791
|
-
const buf = Buffer3.from(b64, "base64");
|
|
3792
|
-
let offset = 0;
|
|
3793
|
-
const magic = buf.subarray(0, OPENSSH_MAGIC.length).toString("ascii");
|
|
3794
|
-
if (magic !== OPENSSH_MAGIC) {
|
|
3795
|
-
throw new Error("Not an OpenSSH private key");
|
|
3796
|
-
}
|
|
3797
|
-
offset += OPENSSH_MAGIC.length;
|
|
3798
|
-
const cipherLen = buf.readUInt32BE(offset);
|
|
3799
|
-
offset += 4;
|
|
3800
|
-
const cipher = buf.subarray(offset, offset + cipherLen).toString();
|
|
3801
|
-
offset += cipherLen;
|
|
3802
|
-
if (cipher !== "none") {
|
|
3803
|
-
throw new Error(`Encrypted keys not supported (cipher: ${cipher}). Decrypt first with: ssh-keygen -p -f <key>`);
|
|
3804
|
-
}
|
|
3805
|
-
const kdfLen = buf.readUInt32BE(offset);
|
|
3806
|
-
offset += 4;
|
|
3807
|
-
offset += kdfLen;
|
|
3808
|
-
const kdfOptsLen = buf.readUInt32BE(offset);
|
|
3809
|
-
offset += 4;
|
|
3810
|
-
offset += kdfOptsLen;
|
|
3811
|
-
const numKeys = buf.readUInt32BE(offset);
|
|
3812
|
-
offset += 4;
|
|
3813
|
-
if (numKeys !== 1) {
|
|
3814
|
-
throw new Error(`Expected 1 key, got ${numKeys}`);
|
|
3815
|
-
}
|
|
3816
|
-
const pubSectionLen = buf.readUInt32BE(offset);
|
|
3817
|
-
offset += 4;
|
|
3818
|
-
offset += pubSectionLen;
|
|
3819
|
-
const privSectionLen = buf.readUInt32BE(offset);
|
|
3820
|
-
offset += 4;
|
|
3821
|
-
const privSection = buf.subarray(offset, offset + privSectionLen);
|
|
3822
|
-
let pOffset = 0;
|
|
3823
|
-
const check1 = privSection.readUInt32BE(pOffset);
|
|
3824
|
-
pOffset += 4;
|
|
3825
|
-
const check2 = privSection.readUInt32BE(pOffset);
|
|
3826
|
-
pOffset += 4;
|
|
3827
|
-
if (check1 !== check2) {
|
|
3828
|
-
throw new Error("Check integers mismatch \u2014 key may be corrupted or encrypted");
|
|
3829
|
-
}
|
|
3830
|
-
const keyTypeLen = privSection.readUInt32BE(pOffset);
|
|
3831
|
-
pOffset += 4;
|
|
3832
|
-
const keyType = privSection.subarray(pOffset, pOffset + keyTypeLen).toString();
|
|
3833
|
-
pOffset += keyTypeLen;
|
|
3834
|
-
if (keyType !== "ssh-ed25519") {
|
|
3835
|
-
throw new Error(`Expected ssh-ed25519, got ${keyType}`);
|
|
3836
|
-
}
|
|
3837
|
-
const pubKeyLen = privSection.readUInt32BE(pOffset);
|
|
3838
|
-
pOffset += 4;
|
|
3839
|
-
const pubKey = privSection.subarray(pOffset, pOffset + pubKeyLen);
|
|
3840
|
-
pOffset += pubKeyLen;
|
|
3841
|
-
const privKeyLen = privSection.readUInt32BE(pOffset);
|
|
3842
|
-
pOffset += 4;
|
|
3843
|
-
const privKeyData = privSection.subarray(pOffset, pOffset + privKeyLen);
|
|
3844
|
-
const seed = privKeyData.subarray(0, 32);
|
|
3845
|
-
return createPrivateKey({
|
|
3846
|
-
key: { kty: "OKP", crv: "Ed25519", d: seed.toString("base64url"), x: pubKey.toString("base64url") },
|
|
3847
|
-
format: "jwk"
|
|
3848
|
-
});
|
|
3849
|
-
}
|
|
3850
|
-
async function getEndpoints(idp) {
|
|
3851
|
-
let disco = {};
|
|
3852
|
-
try {
|
|
3853
|
-
disco = await ofetch2(`${idp}/.well-known/openid-configuration`);
|
|
3854
|
-
} catch {
|
|
3855
|
-
}
|
|
3856
|
-
return {
|
|
3857
|
-
challenge: disco.ddisa_agent_challenge_endpoint ?? `${idp}/api/agent/challenge`,
|
|
3858
|
-
authenticate: disco.ddisa_agent_authenticate_endpoint ?? `${idp}/api/agent/authenticate`
|
|
3859
|
-
};
|
|
3860
|
-
}
|
|
3861
|
-
function resolveKeyPath(p) {
|
|
3862
|
-
if (p.startsWith("~")) return join22(homedir23(), p.slice(1));
|
|
3863
|
-
return p;
|
|
3864
|
-
}
|
|
3865
|
-
function findSigningKey(auth) {
|
|
3866
|
-
const candidates = [];
|
|
3867
|
-
if (auth.key_path) candidates.push(resolveKeyPath(auth.key_path));
|
|
3868
|
-
candidates.push(join22(homedir23(), ".ssh", "id_ed25519"));
|
|
3869
|
-
for (const p of candidates) {
|
|
3870
|
-
if (existsSync22(p)) {
|
|
3871
|
-
try {
|
|
3872
|
-
return { keyPath: p, keyContent: readFileSync22(p, "utf-8") };
|
|
3873
|
-
} catch {
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
}
|
|
3877
|
-
return null;
|
|
3878
|
-
}
|
|
3879
|
-
async function refreshAgentToken(auth, now = Math.floor(Date.now() / 1e3)) {
|
|
3880
|
-
const key = findSigningKey(auth);
|
|
3881
|
-
if (!key) return null;
|
|
3882
|
-
let privateKey;
|
|
3883
|
-
try {
|
|
3884
|
-
privateKey = loadEd25519PrivateKey(key.keyContent);
|
|
3885
|
-
} catch {
|
|
3886
|
-
return null;
|
|
3887
|
-
}
|
|
3888
|
-
let endpoints;
|
|
3889
|
-
try {
|
|
3890
|
-
endpoints = await getEndpoints(auth.idp);
|
|
3891
|
-
} catch {
|
|
3892
|
-
return null;
|
|
3893
|
-
}
|
|
3894
|
-
let challenge;
|
|
3895
|
-
try {
|
|
3896
|
-
const resp = await ofetch2(endpoints.challenge, {
|
|
3897
|
-
method: "POST",
|
|
3898
|
-
headers: { "Content-Type": "application/json" },
|
|
3899
|
-
body: { agent_id: auth.email }
|
|
3900
|
-
});
|
|
3901
|
-
challenge = resp.challenge;
|
|
3902
|
-
} catch {
|
|
3903
|
-
return null;
|
|
3904
|
-
}
|
|
3905
|
-
let signature;
|
|
3906
|
-
try {
|
|
3907
|
-
signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
|
|
3908
|
-
} catch {
|
|
3909
|
-
return null;
|
|
4078
|
+
if (missing.length > 0) {
|
|
4079
|
+
throw new Error(`unknown tool(s): ${missing.join(", ")}`);
|
|
3910
4080
|
}
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
4081
|
+
return out;
|
|
4082
|
+
}
|
|
4083
|
+
function asOpenAiTools(tools) {
|
|
4084
|
+
return tools.map((t2) => ({
|
|
4085
|
+
type: "function",
|
|
4086
|
+
function: { name: wireToolName(t2.name), description: t2.description, parameters: t2.parameters }
|
|
4087
|
+
}));
|
|
4088
|
+
}
|
|
4089
|
+
function wireToolName(local) {
|
|
4090
|
+
return local.replace(/\./g, "_");
|
|
4091
|
+
}
|
|
4092
|
+
function localToolName(wire) {
|
|
4093
|
+
for (const t2 of Object.values(TOOLS)) {
|
|
4094
|
+
if (wireToolName(t2.name) === wire) return t2.name;
|
|
3920
4095
|
}
|
|
3921
|
-
return
|
|
3922
|
-
...auth,
|
|
3923
|
-
access_token: authResp.token,
|
|
3924
|
-
expires_at: now + (authResp.expires_in || 3600),
|
|
3925
|
-
key_path: auth.key_path ?? key.keyPath
|
|
3926
|
-
};
|
|
4096
|
+
return wire;
|
|
3927
4097
|
}
|
|
3928
|
-
|
|
3929
|
-
|
|
4098
|
+
function previewJson(value, max = 500) {
|
|
4099
|
+
let s2;
|
|
3930
4100
|
try {
|
|
3931
|
-
|
|
3932
|
-
if (disco.token_endpoint) return disco.token_endpoint;
|
|
4101
|
+
s2 = JSON.stringify(value);
|
|
3933
4102
|
} catch {
|
|
4103
|
+
s2 = String(value);
|
|
3934
4104
|
}
|
|
3935
|
-
return `${
|
|
4105
|
+
return s2.length > max ? `${s2.slice(0, max)}\u2026` : s2;
|
|
3936
4106
|
}
|
|
3937
|
-
async function
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
const
|
|
3947
|
-
if (
|
|
3948
|
-
|
|
3949
|
-
|
|
4107
|
+
async function aggregateChatStream(res) {
|
|
4108
|
+
if (!res.body) throw new Error("LiteLLM streaming response had no body");
|
|
4109
|
+
const reader = res.body.getReader();
|
|
4110
|
+
const decoder = new TextDecoder();
|
|
4111
|
+
let buf = "";
|
|
4112
|
+
let content = "";
|
|
4113
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
4114
|
+
let finishReason;
|
|
4115
|
+
while (true) {
|
|
4116
|
+
const { value, done } = await reader.read();
|
|
4117
|
+
if (done) break;
|
|
4118
|
+
buf += decoder.decode(value, { stream: true });
|
|
4119
|
+
while (true) {
|
|
4120
|
+
const nl = buf.indexOf("\n");
|
|
4121
|
+
if (nl === -1) break;
|
|
4122
|
+
const line = buf.slice(0, nl).trim();
|
|
4123
|
+
buf = buf.slice(nl + 1);
|
|
4124
|
+
if (!line.startsWith("data:")) continue;
|
|
4125
|
+
const payload = line.slice(5).trim();
|
|
4126
|
+
if (!payload || payload === "[DONE]") continue;
|
|
4127
|
+
let chunk;
|
|
4128
|
+
try {
|
|
4129
|
+
chunk = JSON.parse(payload);
|
|
4130
|
+
} catch {
|
|
4131
|
+
continue;
|
|
4132
|
+
}
|
|
4133
|
+
const ch0 = chunk.choices?.[0];
|
|
4134
|
+
const delta = ch0?.delta;
|
|
4135
|
+
if (delta?.content) content += delta.content;
|
|
4136
|
+
if (delta?.tool_calls) {
|
|
4137
|
+
for (const tc of delta.tool_calls) {
|
|
4138
|
+
const idx = tc.index ?? 0;
|
|
4139
|
+
const existing = toolCalls.get(idx) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
|
|
4140
|
+
if (tc.id) existing.id = tc.id;
|
|
4141
|
+
if (tc.function?.name) existing.function.name = tc.function.name;
|
|
4142
|
+
if (tc.function?.arguments) existing.function.arguments += tc.function.arguments;
|
|
4143
|
+
toolCalls.set(idx, existing);
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
if (ch0?.finish_reason) finishReason = ch0.finish_reason;
|
|
3950
4147
|
}
|
|
3951
|
-
throw new NotLoggedInError(
|
|
3952
|
-
`IdP token expired at ${new Date(auth.expires_at * 1e3).toISOString()} and no refresh_token is stored. Run \`apes login\` again.`
|
|
3953
|
-
);
|
|
3954
4148
|
}
|
|
3955
|
-
const
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
4149
|
+
const message = { role: "assistant", content: content || null };
|
|
4150
|
+
if (toolCalls.size > 0) message.tool_calls = Array.from(toolCalls.values());
|
|
4151
|
+
return { choices: [{ message, finish_reason: finishReason }] };
|
|
4152
|
+
}
|
|
4153
|
+
async function runLoop(opts) {
|
|
4154
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
4155
|
+
const trace = [];
|
|
4156
|
+
const messages = [
|
|
4157
|
+
{ role: "system", content: opts.systemPrompt },
|
|
4158
|
+
...opts.history ?? [],
|
|
4159
|
+
{ role: "user", content: opts.userMessage }
|
|
4160
|
+
];
|
|
4161
|
+
const tools = asOpenAiTools(opts.tools);
|
|
4162
|
+
for (let step = 1; step <= opts.maxSteps; step++) {
|
|
4163
|
+
const requestBody = {
|
|
4164
|
+
model: opts.config.model,
|
|
4165
|
+
messages,
|
|
4166
|
+
...opts.config.reasoningEffort ? { reasoning_effort: opts.config.reasoningEffort } : {},
|
|
4167
|
+
...tools.length > 0 ? { tools, tool_choice: "auto" } : {},
|
|
4168
|
+
...opts.streamAggregate ? { stream: true } : {}
|
|
4169
|
+
};
|
|
4170
|
+
const res = await fetchFn(`${opts.config.apiBase}/chat/completions`, {
|
|
3963
4171
|
method: "POST",
|
|
3964
|
-
headers: {
|
|
3965
|
-
|
|
4172
|
+
headers: {
|
|
4173
|
+
"authorization": `Bearer ${opts.config.apiKey}`,
|
|
4174
|
+
"content-type": "application/json"
|
|
4175
|
+
},
|
|
4176
|
+
body: JSON.stringify(requestBody)
|
|
3966
4177
|
});
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
4178
|
+
if (!res.ok) {
|
|
4179
|
+
const text = await res.text().catch(() => "");
|
|
4180
|
+
throw new Error(`LiteLLM ${res.status}: ${text.slice(0, 500)}`);
|
|
4181
|
+
}
|
|
4182
|
+
const data = opts.streamAggregate ? await aggregateChatStream(res) : await res.json();
|
|
4183
|
+
const choice = data.choices?.[0];
|
|
4184
|
+
if (!choice) throw new Error("LiteLLM response had no choices");
|
|
4185
|
+
const assistant = choice.message;
|
|
4186
|
+
messages.push(assistant);
|
|
4187
|
+
if (assistant.content) opts.handlers?.onTextDelta?.(assistant.content);
|
|
4188
|
+
trace.push({
|
|
4189
|
+
step,
|
|
4190
|
+
type: "assistant",
|
|
4191
|
+
preview: previewJson({ content: assistant.content, tool_calls: assistant.tool_calls?.length ?? 0 })
|
|
4192
|
+
});
|
|
4193
|
+
if (!assistant.tool_calls || assistant.tool_calls.length === 0) {
|
|
4194
|
+
const result2 = {
|
|
4195
|
+
status: "ok",
|
|
4196
|
+
finalMessage: assistant.content,
|
|
4197
|
+
stepCount: step,
|
|
4198
|
+
trace
|
|
4199
|
+
};
|
|
4200
|
+
opts.handlers?.onDone?.(result2);
|
|
4201
|
+
return result2;
|
|
4202
|
+
}
|
|
4203
|
+
for (const call of assistant.tool_calls) {
|
|
4204
|
+
const wireName = call.function.name;
|
|
4205
|
+
const localName = localToolName(wireName);
|
|
4206
|
+
const tool = opts.tools.find((t2) => t2.name === localName);
|
|
4207
|
+
let parsedArgs;
|
|
4208
|
+
try {
|
|
4209
|
+
parsedArgs = JSON.parse(call.function.arguments);
|
|
4210
|
+
} catch {
|
|
4211
|
+
parsedArgs = {};
|
|
4212
|
+
}
|
|
4213
|
+
opts.handlers?.onToolCall?.({ name: localName, args: parsedArgs });
|
|
4214
|
+
trace.push({ step, type: "tool_call", tool: localName, preview: previewJson(parsedArgs) });
|
|
4215
|
+
let result2;
|
|
4216
|
+
let isError = false;
|
|
4217
|
+
if (!tool) {
|
|
4218
|
+
result2 = `unknown tool: ${localName}`;
|
|
4219
|
+
isError = true;
|
|
4220
|
+
} else {
|
|
4221
|
+
try {
|
|
4222
|
+
result2 = await tool.execute(parsedArgs);
|
|
4223
|
+
} catch (err) {
|
|
4224
|
+
result2 = err?.message ?? String(err);
|
|
4225
|
+
isError = true;
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
if (isError) {
|
|
4229
|
+
opts.handlers?.onToolError?.({ name: localName, error: String(result2) });
|
|
4230
|
+
trace.push({ step, type: "tool_error", tool: localName, preview: previewJson(result2) });
|
|
4231
|
+
} else {
|
|
4232
|
+
opts.handlers?.onToolResult?.({ name: localName, result: result2 });
|
|
4233
|
+
trace.push({ step, type: "tool_result", tool: localName, preview: previewJson(result2) });
|
|
4234
|
+
}
|
|
4235
|
+
messages.push({
|
|
4236
|
+
role: "tool",
|
|
4237
|
+
tool_call_id: call.id,
|
|
4238
|
+
name: wireToolName(localName),
|
|
4239
|
+
content: typeof result2 === "string" ? result2 : JSON.stringify(result2)
|
|
4240
|
+
});
|
|
3974
4241
|
}
|
|
3975
|
-
throw new AuthError(
|
|
3976
|
-
0,
|
|
3977
|
-
`Network error refreshing IdP token at ${tokenEndpoint}`,
|
|
3978
|
-
`Underlying: ${err.message ?? err}`
|
|
3979
|
-
);
|
|
3980
|
-
}
|
|
3981
|
-
if (!response.access_token) {
|
|
3982
|
-
throw new AuthError(0, `IdP refresh response missing access_token (endpoint: ${tokenEndpoint})`);
|
|
3983
4242
|
}
|
|
3984
|
-
const
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
4243
|
+
const result = {
|
|
4244
|
+
status: "error",
|
|
4245
|
+
finalMessage: `max_steps (${opts.maxSteps}) reached without completion`,
|
|
4246
|
+
stepCount: opts.maxSteps,
|
|
4247
|
+
trace
|
|
3989
4248
|
};
|
|
3990
|
-
|
|
3991
|
-
return
|
|
4249
|
+
opts.handlers?.onDone?.(result);
|
|
4250
|
+
return result;
|
|
3992
4251
|
}
|
|
4252
|
+
var RPC_SESSION_TTL_MS = 60 * 60 * 1e3;
|
|
4253
|
+
var DEFAULT_INSTRUCTIONS = [
|
|
4254
|
+
"Work in the provided worktree. When done:",
|
|
4255
|
+
"- ensure the verification command passes (no PR on red)",
|
|
4256
|
+
"- open a PR that references this issue",
|
|
4257
|
+
"- leave risk-path / agent-judged-risky changes for human approval"
|
|
4258
|
+
].join("\n");
|
|
4259
|
+
var DIFF_CAP = 60 * 1024;
|
|
4260
|
+
var DIFF_CAP2 = 48 * 1024;
|
|
4261
|
+
var RISK_SYSTEM = [
|
|
4262
|
+
"You are a security/risk classifier for an autonomous coding agent.",
|
|
4263
|
+
"Given a diff + changed file paths, decide whether merging it WITHOUT a human is risky.",
|
|
4264
|
+
"Risky = touches authentication, authorization, secrets/credentials, payment, data migrations,",
|
|
4265
|
+
"deploy/release/CI config, cryptography, deletion of data, or anything whose failure is hard to",
|
|
4266
|
+
"reverse in production. Routine code/tests/docs/refactors are NOT risky.",
|
|
4267
|
+
'Respond ONLY as JSON: {"risky": boolean, "reason": string}.'
|
|
4268
|
+
].join(" ");
|
|
4269
|
+
var REVIEW_SYSTEM = [
|
|
4270
|
+
"You are a code reviewer for an autonomous coding agent.",
|
|
4271
|
+
"Given a PR diff, decide whether it is correct, safe, and complete enough to auto-merge.",
|
|
4272
|
+
"Approve only if you would be comfortable shipping it without further human review.",
|
|
4273
|
+
"Block if you see bugs, missing tests, security issues, or incomplete work.",
|
|
4274
|
+
'Respond ONLY as JSON: {"approved": boolean, "reason": string}.'
|
|
4275
|
+
].join(" ");
|
|
3993
4276
|
|
|
3994
4277
|
// src/identity.ts
|
|
3995
4278
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
3996
4279
|
import { homedir as homedir6 } from "os";
|
|
3997
|
-
import { join as
|
|
3998
|
-
function authPath() {
|
|
3999
|
-
return
|
|
4280
|
+
import { join as join6 } from "path";
|
|
4281
|
+
function authPath(home) {
|
|
4282
|
+
return join6(home, ".config", "apes", "auth.json");
|
|
4000
4283
|
}
|
|
4001
|
-
function readAgentIdentity() {
|
|
4002
|
-
const path = authPath();
|
|
4284
|
+
function readAgentIdentity(home = homedir6()) {
|
|
4285
|
+
const path = authPath(home);
|
|
4003
4286
|
if (!existsSync3(path)) {
|
|
4004
4287
|
throw new Error(`agent identity not found at ${path}`);
|
|
4005
4288
|
}
|
|
@@ -4016,6 +4299,20 @@ function readAgentIdentity() {
|
|
|
4016
4299
|
return { email: parsed.email, ownerEmail, idp: parsed.idp };
|
|
4017
4300
|
}
|
|
4018
4301
|
|
|
4302
|
+
// src/llm-gateway-key.ts
|
|
4303
|
+
async function resolveLlmGatewayKey(base, fallback, log2, exchange = getAuthorizedBearer) {
|
|
4304
|
+
if (!base.includes("llms.openape.ai"))
|
|
4305
|
+
return fallback;
|
|
4306
|
+
try {
|
|
4307
|
+
const u3 = new URL(base);
|
|
4308
|
+
const bearer = await exchange({ endpoint: u3.origin, aud: u3.host });
|
|
4309
|
+
return bearer.replace(/^Bearer\s+/i, "");
|
|
4310
|
+
} catch (err) {
|
|
4311
|
+
log2(`llm gateway token exchange failed (keeping current key): ${err instanceof Error ? err.message : String(err)}`);
|
|
4312
|
+
return fallback;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4019
4316
|
// src/service-bridge.ts
|
|
4020
4317
|
function textArtifact(text) {
|
|
4021
4318
|
return { artifactId: randomUUID(), parts: [{ kind: "text", text }] };
|
|
@@ -4060,8 +4357,10 @@ async function pollOnce(deps) {
|
|
|
4060
4357
|
let artifact;
|
|
4061
4358
|
try {
|
|
4062
4359
|
const spec = parseTaskSpec(task);
|
|
4360
|
+
const apiKey = deps.refreshApiKey ? await deps.refreshApiKey() : deps.config.apiKey;
|
|
4361
|
+
const config = { ...deps.config, apiKey, ...spec.model ? { model: spec.model } : {} };
|
|
4063
4362
|
const result = await deps.runLoopImpl({
|
|
4064
|
-
config
|
|
4363
|
+
config,
|
|
4065
4364
|
userMessage: spec.userMessage,
|
|
4066
4365
|
systemPrompt: spec.systemPrompt,
|
|
4067
4366
|
tools: taskTools(spec.tools ?? []),
|
|
@@ -4117,6 +4416,7 @@ async function runService() {
|
|
|
4117
4416
|
fetchImpl: fetch,
|
|
4118
4417
|
runLoopImpl: runLoop,
|
|
4119
4418
|
config: { apiBase: cfg.apiBase, apiKey: cfg.apiKey, model: cfg.model },
|
|
4419
|
+
refreshApiKey: () => resolveLlmGatewayKey(cfg.apiBase, cfg.apiKey, log),
|
|
4120
4420
|
maxSteps: cfg.maxSteps,
|
|
4121
4421
|
log
|
|
4122
4422
|
};
|