@openape/ape-agent 2.10.0 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 CONFIG_DIR, AUTH_FILE, CONFIG_FILE;
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
- CONFIG_DIR = join(homedir(), ".config", "apes");
43
- AUTH_FILE = join(CONFIG_DIR, "auth.json");
44
- CONFIG_FILE = join(CONFIG_DIR, "config.toml");
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-BA2V3BBO.js
1084
- init_chunk_OBF7IMQ2();
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
- join2(process.cwd(), ".openape", "shapes", "adapters"),
2549
- join2(homedir2(), ".openape", "shapes", "adapters"),
2550
- join2("/etc", "openape", "shapes", "adapters")
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 = join2(dir, file);
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) => join2(dir, `${cliId}.toml`));
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 readFileSync2, writeFileSync } from "fs";
2699
- import { homedir as homedir3 } from "os";
2700
- import { dirname, normalize, resolve } from "path";
2701
- import { homedir as homedir22 } from "os";
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
- var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
2707
- var MAX_STDIO_BYTES = 64 * 1024;
2708
- var BIN = "ape-shell";
2709
- function capStdio(s2) {
2710
- const buf = Buffer.from(s2, "utf8");
2711
- if (buf.byteLength <= MAX_STDIO_BYTES) return s2;
2712
- return `${buf.subarray(0, MAX_STDIO_BYTES).toString("utf8")}
2713
- [truncated to ${MAX_STDIO_BYTES} bytes]`;
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 runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS, cwd) {
2716
- const bypass = process.env.OPENAPE_BYPASS_APE_SHELL === "1";
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
- var DEFAULTS = { DEFAULT_TIMEOUT_MS };
2769
- var bashTools = [
2770
- {
2771
- name: "bash",
2772
- 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.",
2773
- parameters: {
2774
- type: "object",
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
- var MAX_BYTES = 1024 * 1024;
2798
- function jailPath(input) {
2799
- if (typeof input !== "string" || input === "") {
2800
- throw new Error("path must be a non-empty string");
2753
+ }
2754
+ function ensureSpTokensDir() {
2755
+ ensureConfigDir();
2756
+ const dir = getSpTokensDir();
2757
+ if (!existsSync(dir)) {
2758
+ mkdirSync(dir, { recursive: true, mode: 448 });
2801
2759
  }
2802
- const home = homedir3();
2803
- const candidate = input.startsWith("~/") ? resolve(home, input.slice(2)) : input.startsWith("/") ? normalize(input) : resolve(home, input);
2804
- if (candidate !== home && !candidate.startsWith(`${home}/`)) {
2805
- throw new Error(`path "${input}" resolves outside the agent's home`);
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
- var fileTools = [
2810
- {
2811
- name: "file.read",
2812
- description: "Read a UTF-8 file from the agent's home directory ($HOME). Capped at 1MB. Path traversal blocked.",
2813
- parameters: {
2814
- type: "object",
2815
- properties: {
2816
- path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME). `..` segments are rejected." }
2817
- },
2818
- required: ["path"]
2819
- },
2820
- execute: async (args) => {
2821
- const a2 = args;
2822
- const p = jailPath(a2.path);
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
- writeFileSync(p, after, { encoding: "utf8" });
2892
- return { path: p, replacements: replaceAll ? occurrences : 1 };
2787
+ } catch {
2788
+ extra = {};
2893
2789
  }
2894
2790
  }
2895
- ];
2896
- var BRANCH_RE = /^[\w./-]{1,200}$/;
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 assertBranch(v2) {
2902
- if (typeof v2 !== "string" || !BRANCH_RE.test(v2)) {
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 assertId(v2) {
2908
- if (typeof v2 !== "string" && typeof v2 !== "number") throw new Error("id required");
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
- var githubAdapter = {
2914
- id: "github",
2915
- matchesRemote: (url) => /github\.com/i.test(url),
2916
- prCreate: (i2) => {
2917
- const head = assertBranch(i2.head);
2918
- const parts = ["gh", "pr", "create", "--title", shq(i2.title), "--body", shq(i2.body), "--head", shq(head)];
2919
- if (i2.base !== void 0) parts.push("--base", shq(assertBranch(i2.base)));
2920
- return parts.join(" ");
2921
- },
2922
- prMerge: (i2) => {
2923
- const ref = String(i2.ref);
2924
- const refTok = ID_RE.test(ref) ? ref : assertBranch(ref);
2925
- const parts = ["gh", "pr", "merge", shq(refTok)];
2926
- if (i2.squash === true) parts.push("--squash");
2927
- if (i2.auto) parts.push("--auto");
2928
- if (i2.deleteBranch) parts.push("--delete-branch");
2929
- return parts.join(" ");
2930
- },
2931
- prStatus: (ref) => {
2932
- const r3 = String(ref);
2933
- const refTok = ID_RE.test(r3) ? r3 : assertBranch(r3);
2934
- return `gh pr view ${shq(refTok)} --json state,mergeStateStatus,statusCheckRollup,reviewDecision`;
2935
- },
2936
- issueGet: (ref, repo) => `gh issue view ${assertId(ref)}${repo ? ` --repo ${shq(repo)}` : ""} --json number,title,body,labels`
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 azureAdapter = {
2939
- id: "azure",
2940
- matchesRemote: (url) => /dev\.azure\.com|visualstudio\.com/i.test(url),
2941
- prCreate: (i2) => {
2942
- const head = assertBranch(i2.head);
2943
- const parts = ["az", "repos", "pr", "create", "--title", shq(i2.title), "--description", shq(i2.body), "--source-branch", shq(head)];
2944
- if (i2.base !== void 0) parts.push("--target-branch", shq(assertBranch(i2.base)));
2945
- return parts.join(" ");
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
- var registry = /* @__PURE__ */ new Map([
2963
- [githubAdapter.id, githubAdapter],
2964
- [azureAdapter.id, azureAdapter]
2965
- ]);
2966
- function listForges() {
2967
- return [...registry.keys()];
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
- function getForge(id) {
2970
- const a2 = registry.get(id);
2971
- if (!a2) {
2972
- throw new Error(`unknown forge '${id}'. Registered: ${listForges().join(", ")}. Add one with registerForge().`);
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 a2;
2874
+ return createPrivateKey(pem);
2975
2875
  }
2976
- function detectForge(remoteUrl) {
2977
- if (typeof remoteUrl !== "string" || remoteUrl === "") {
2978
- throw new Error("remote URL required to detect forge");
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
- for (const a2 of registry.values()) {
2981
- if (a2.matchesRemote(remoteUrl)) return a2.id;
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
- throw new Error(`no forge adapter matches remote: ${remoteUrl}. Registered: ${listForges().join(", ")}. Register one with registerForge() (e.g. GitLab/Bitbucket/Gitea).`);
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 buildPrCreate(input) {
2986
- return getForge(input.forge).prCreate(input);
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 buildPrMerge(input) {
2989
- return getForge(input.forge).prMerge(input);
2948
+ function resolveKeyPath(p) {
2949
+ if (p.startsWith("~")) return join23(homedir23(), p.slice(1));
2950
+ return p;
2990
2951
  }
2991
- function buildPrStatus(forge, ref) {
2992
- return getForge(forge).prStatus(ref);
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 buildIssueGet(forge, ref, repo) {
2995
- return getForge(forge).issueGet(ref, repo);
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
- function resolveForge(a2) {
2998
- if (typeof a2.forge === "string" && a2.forge !== "") return a2.forge;
2999
- if (typeof a2.remote === "string") return detectForge(a2.remote);
3000
- throw new Error("provide a forge id (e.g. github, azure, or a registered adapter) or a remote URL to detect it");
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
- var forgeParam = { type: "string", description: "Target forge id (github, azure, or a registered adapter). Omit to auto-detect from `remote`." };
3003
- var remoteParam = { type: "string", description: "git remote URL \u2014 used to auto-detect the forge when `forge` is omitted." };
3004
- var forgeTools = [
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: "forge.pr.create",
3007
- 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`.",
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
- forge: forgeParam,
3012
- remote: remoteParam,
3013
- title: { type: "string", description: "PR title." },
3014
- body: { type: "string", description: "PR description / body." },
3015
- head: { type: "string", description: "Source branch." },
3016
- base: { type: "string", description: "Target branch. Omit for the repo default." }
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: ["title", "body", "head"]
3178
+ required: ["cmd"]
3019
3179
  },
3020
3180
  execute: async (args) => {
3021
3181
  const a2 = args;
3022
- const cmd = buildPrCreate({ forge: resolveForge(a2), title: a2.title, body: a2.body, head: a2.head, base: a2.base });
3023
- return await runApeShell(cmd);
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: "forge.pr.merge",
3028
- 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).',
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
- forge: forgeParam,
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: ["ref"]
3218
+ required: ["path"]
3040
3219
  },
3041
3220
  execute: async (args) => {
3042
3221
  const a2 = args;
3043
- const cmd = buildPrMerge({ forge: resolveForge(a2), ref: a2.ref, auto: a2.auto, squash: a2.squash, deleteBranch: a2.delete_branch });
3044
- return await runApeShell(cmd);
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: "forge.pr.status",
3049
- description: "Fetch a PR's state + checks + review decision. Gated (read).",
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: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "PR number/branch (GitHub) or id (Azure)." } },
3053
- required: ["ref"]
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
- return await runApeShell(buildPrStatus(resolveForge(a2), a2.ref));
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: "forge.issue.get",
3062
- 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.",
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: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "Issue number (GitHub) or work-item id (Azure)." } },
3066
- required: ["ref"]
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
- const repo = typeof a2.remote === "string" ? a2.remote : void 0;
3071
- return await runApeShell(buildIssueGet(resolveForge(a2), a2.ref, repo));
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
- function jailedRoot(envVar, fallbackName) {
3076
- const home = homedir22();
3077
- const raw = process2.env[envVar];
3078
- const dir = raw ? resolve2(raw) : resolve2(home, fallbackName);
3079
- if (dir !== home && !dir.startsWith(`${home}/`)) {
3080
- throw new Error(`${envVar} (${dir}) must resolve inside the agent's home`);
3081
- }
3082
- return dir;
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 = homedir22();
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 on the owner's task list at tasks.openape.ai.",
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
- let authResp;
3912
- try {
3913
- authResp = await ofetch2(endpoints.authenticate, {
3914
- method: "POST",
3915
- headers: { "Content-Type": "application/json" },
3916
- body: { agent_id: auth.email, challenge, signature }
3917
- });
3918
- } catch {
3919
- return null;
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
- var EXPIRY_SKEW_SECONDS = 30;
3929
- async function getTokenEndpoint(idp) {
4098
+ function previewJson(value, max = 500) {
4099
+ let s2;
3930
4100
  try {
3931
- const disco = await ofetch3(`${idp}/.well-known/openid-configuration`);
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 `${idp}/token`;
4105
+ return s2.length > max ? `${s2.slice(0, max)}\u2026` : s2;
3936
4106
  }
3937
- async function ensureFreshIdpAuth(now = Math.floor(Date.now() / 1e3)) {
3938
- const auth = loadIdpAuth();
3939
- if (!auth) {
3940
- throw new NotLoggedInError();
3941
- }
3942
- if (auth.expires_at > now + EXPIRY_SKEW_SECONDS) {
3943
- return auth;
3944
- }
3945
- if (!auth.refresh_token) {
3946
- const refreshed = await refreshAgentToken(auth, now);
3947
- if (refreshed) {
3948
- saveIdpAuth(refreshed);
3949
- return refreshed;
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 tokenEndpoint = await getTokenEndpoint(auth.idp);
3956
- const body = new URLSearchParams({
3957
- grant_type: "refresh_token",
3958
- refresh_token: auth.refresh_token
3959
- });
3960
- let response;
3961
- try {
3962
- response = await ofetch3(tokenEndpoint, {
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: { "Content-Type": "application/x-www-form-urlencoded" },
3965
- body: body.toString()
4172
+ headers: {
4173
+ "authorization": `Bearer ${opts.config.apiKey}`,
4174
+ "content-type": "application/json"
4175
+ },
4176
+ body: JSON.stringify(requestBody)
3966
4177
  });
3967
- } catch (err) {
3968
- const status = err.status ?? err.statusCode ?? 0;
3969
- if (status === 400 || status === 401) {
3970
- saveIdpAuth({ ...auth, refresh_token: void 0 });
3971
- throw new NotLoggedInError(
3972
- `Refresh token rejected by ${auth.idp}. Run \`apes login\` again.`
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 next = {
3985
- ...auth,
3986
- access_token: response.access_token,
3987
- refresh_token: response.refresh_token ?? auth.refresh_token,
3988
- expires_at: now + (response.expires_in ?? 3600)
4243
+ const result = {
4244
+ status: "error",
4245
+ finalMessage: `max_steps (${opts.maxSteps}) reached without completion`,
4246
+ stepCount: opts.maxSteps,
4247
+ trace
3989
4248
  };
3990
- saveIdpAuth(next);
3991
- return next;
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 join4 } from "path";
3998
- function authPath() {
3999
- return join4(homedir6(), ".config", "apes", "auth.json");
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: spec.model ? { ...deps.config, model: spec.model } : deps.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
  };