@openape/apes 1.29.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -381,7 +381,7 @@ async function loginWithPKCE(idp) {
381
381
  consola2.success(`Logged in as ${payload.email || payload.sub}`);
382
382
  }
383
383
  async function loginWithKey(idp, keyPath, agentEmail) {
384
- const { readFileSync: readFileSync21 } = await import("fs");
384
+ const { readFileSync: readFileSync18 } = await import("fs");
385
385
  const { sign: sign3 } = await import("crypto");
386
386
  const { loadEd25519PrivateKey: loadEd25519PrivateKey2 } = await import("./ssh-key-YBNNG5K5.js");
387
387
  const challengeUrl = await getAgentChallengeEndpoint(idp);
@@ -395,7 +395,7 @@ async function loginWithKey(idp, keyPath, agentEmail) {
395
395
  throw new CliError(`Challenge failed: ${await challengeResp.text()}`);
396
396
  }
397
397
  const { challenge } = await challengeResp.json();
398
- const keyContent = readFileSync21(keyPath, "utf-8");
398
+ const keyContent = readFileSync18(keyPath, "utf-8");
399
399
  const privateKey = loadEd25519PrivateKey2(keyContent);
400
400
  const signature = sign3(null, Buffer2.from(challenge), privateKey).toString("base64");
401
401
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
@@ -1976,15 +1976,13 @@ var agentCommand = defineCommand21({
1976
1976
  import { defineCommand as defineCommand32 } from "citty";
1977
1977
 
1978
1978
  // src/commands/agents/allow.ts
1979
- import { execFileSync as execFileSync10 } from "child_process";
1979
+ import { execFileSync as execFileSync6 } from "child_process";
1980
1980
  import { defineCommand as defineCommand22 } from "citty";
1981
1981
  import consola19 from "consola";
1982
1982
 
1983
1983
  // src/lib/agent-bootstrap.ts
1984
1984
  import { Buffer as Buffer3 } from "buffer";
1985
- import { execFileSync as execFileSync2 } from "child_process";
1986
1985
  import { createPrivateKey, sign } from "crypto";
1987
- import { rmSync } from "fs";
1988
1986
  import { exchangeWithDelegation } from "@openape/cli-auth";
1989
1987
  var AGENT_NAME_REGEX = /^[a-z][a-z0-9-]{0,23}$/;
1990
1988
  var SSH_ED25519_PREFIX = "ssh-ed25519 ";
@@ -2061,7 +2059,10 @@ async function issueAgentToken(input) {
2061
2059
  const challengeResp = await fetch(challengeUrl, {
2062
2060
  method: "POST",
2063
2061
  headers: { "Content-Type": "application/json" },
2064
- body: JSON.stringify({ agent_id: input.agentEmail })
2062
+ // Canonical /api/auth/challenge expects `id` (M3). The legacy
2063
+ // /api/agent/challenge field was `agent_id`; discovery now resolves
2064
+ // the canonical endpoint, so the payload must use `id` to match.
2065
+ body: JSON.stringify({ id: input.agentEmail })
2065
2066
  });
2066
2067
  if (!challengeResp.ok) {
2067
2068
  const text = await challengeResp.text().catch(() => "");
@@ -2073,7 +2074,8 @@ async function issueAgentToken(input) {
2073
2074
  const authResp = await fetch(authenticateUrl, {
2074
2075
  method: "POST",
2075
2076
  headers: { "Content-Type": "application/json" },
2076
- body: JSON.stringify({ agent_id: input.agentEmail, challenge, signature })
2077
+ // Canonical /api/auth/authenticate expects `id` (same M3 rename).
2078
+ body: JSON.stringify({ id: input.agentEmail, challenge, signature })
2077
2079
  });
2078
2080
  if (!authResp.ok) {
2079
2081
  const text = await authResp.text().catch(() => "");
@@ -2092,7 +2094,7 @@ ${content}
2092
2094
  ${SH_HEREDOC_DELIMITER}`;
2093
2095
  }
2094
2096
  function buildSpawnSetupScript(input) {
2095
- const { name, macOSUsername, homeDir, shellPath } = input;
2097
+ const { name, homeDir, shellPath } = input;
2096
2098
  const privatePemForHeredoc = input.privateKeyPem.endsWith("\n") ? input.privateKeyPem : `${input.privateKeyPem}
2097
2099
  `;
2098
2100
  const claudeBlock = input.claudeSettingsJson && input.hookScriptSource ? `
@@ -2123,95 +2125,50 @@ done
2123
2125
  return `#!/bin/bash
2124
2126
  set -euo pipefail
2125
2127
 
2126
- # escapes-spawned scripts inherit a minimal PATH that doesn't include
2127
- # /usr/sbin \u2014 which is where chown / dscl / pwpolicy live. Force a
2128
- # wide PATH so the privileged setup commands resolve without absolute
2129
- # paths everywhere.
2130
- export PATH="/usr/sbin:/usr/bin:/bin:/sbin:/opt/homebrew/bin:/usr/local/bin"
2128
+ # Wide PATH so useradd / getent / install / chown resolve regardless of
2129
+ # how the privileged wrapper trimmed the environment.
2130
+ export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
2131
2131
 
2132
2132
  NAME=${shQuote(name)}
2133
- MACOS_USER=${shQuote(macOSUsername)}
2134
2133
  HOME_DIR=${shQuote(homeDir)}
2135
2134
  SHELL_PATH=${shQuote(shellPath)}
2136
2135
 
2137
- # Tombstone-reuse: \`apes agents destroy\` leaves the dscl user
2138
- # record behind (opendirectoryd refuses \`dscl . -delete\` from
2139
- # escapes' setuid-root context without admin auth, so we accept the
2140
- # stranded record as a harmless tombstone \u2014 see
2141
- # runPhaseGTeardownInProcess). When the operator re-spawns with the
2142
- # same name, we recycle the tombstone instead of refusing: if the
2143
- # previously-recorded home dir is gone from disk (matching the
2144
- # Phase-G teardown's \`rm -rf\`), we reuse the existing UID +
2145
- # overwrite the home. If the home is still on disk it's a real,
2146
- # live agent \u2014 refuse.
2147
- TOMBSTONE_REUSE=0
2148
- if dscl . -read "/Users/$MACOS_USER" >/dev/null 2>&1; then
2149
- EXISTING_HOME=$(dscl . -read "/Users/$MACOS_USER" NFSHomeDirectory 2>/dev/null | awk '/NFSHomeDirectory:/ {print $2}')
2150
- if [ -n "$EXISTING_HOME" ] && [ -d "$EXISTING_HOME" ]; then
2151
- echo "User $MACOS_USER already exists with a live home at $EXISTING_HOME; refusing to overwrite." >&2
2152
- exit 1
2153
- fi
2154
- NEXT_UID=$(dscl . -read "/Users/$MACOS_USER" UniqueID 2>/dev/null | awk '/UniqueID:/ {print $2}')
2155
- if [ -z "$NEXT_UID" ]; then
2156
- echo "User $MACOS_USER exists but has no UniqueID; record is malformed, refusing to recycle." >&2
2157
- exit 1
2158
- fi
2159
- echo "Recycling tombstone dscl record for $MACOS_USER (uid=$NEXT_UID, prior home $EXISTING_HOME \u2014 gone)"
2160
- TOMBSTONE_REUSE=1
2136
+ # Agent homes live under /var/lib/openape/homes/ (the persisted
2137
+ # openape-homes volume) \u2014 out of /home/ where real operator accounts
2138
+ # live. useradd --create-home makes the leaf dir but not missing
2139
+ # parents; the volume mount provides the parent, but pre-create it
2140
+ # anyway so a bare-metal/non-volume run still works.
2141
+ mkdir -p /var/lib/openape/homes
2142
+ chmod 755 /var/lib/openape/homes
2143
+
2144
+ # Create the agent's OS user if absent. spawn.ts already refused earlier
2145
+ # when the user existed, but guard here too so a re-run of the privileged
2146
+ # script is idempotent rather than erroring on a half-created account.
2147
+ if ! getent passwd "$NAME" >/dev/null 2>&1; then
2148
+ useradd --create-home --home-dir "$HOME_DIR" --shell "$SHELL_PATH" --comment "OpenApe Agent $NAME" "$NAME"
2161
2149
  fi
2162
2150
 
2163
- # Phase G: agent home dirs live under /var/openape/homes/, not
2164
- # /Users/. Pre-create the parent and chmod it world-traversable
2165
- # so per-agent dirs can be reached by their respective uids.
2166
- mkdir -p /var/openape/homes
2167
- chmod 755 /var/openape/homes
2168
-
2169
- if [ "$TOMBSTONE_REUSE" = "0" ]; then
2170
- # Pick the LOWEST free UID in the [200, 500) hidden service-account
2171
- # range. We must skip every occupied UID \u2014 agent users AND the many
2172
- # macOS system _* accounts \u2014 and reuse gaps. The old code took
2173
- # max(existing)+1, so a single agent landing on 499 wedged every
2174
- # future spawn ("No free UID") even with 100+ UIDs free. Now we scan
2175
- # for the first actually-unused UID.
2176
- OCCUPIED_UIDS=$(dscl . -list /Users UniqueID | awk '$2 >= 200 && $2 < 500 {print $2}' | sort -n -u)
2177
- NEXT_UID=""
2178
- candidate=200
2179
- while [ "$candidate" -lt 500 ]; do
2180
- if ! printf '%s
2181
- ' "$OCCUPIED_UIDS" | grep -qx "$candidate"; then
2182
- NEXT_UID=$candidate
2183
- break
2184
- fi
2185
- candidate=$((candidate + 1))
2186
- done
2187
- if [ -z "$NEXT_UID" ]; then
2188
- echo "No free UID in [200, 500) \u2014 refusing to clobber a real user." >&2
2189
- exit 1
2190
- fi
2151
+ # Resolve the uid for the final report line (getent is the canonical read).
2152
+ NEW_UID=$(getent passwd "$NAME" | cut -d: -f3)
2191
2153
 
2192
- dscl . -create "/Users/$MACOS_USER"
2193
- fi
2194
-
2195
- # Idempotent attribute writes \u2014 \`dscl . -create\` on an existing
2196
- # property overwrites in place, so the tombstone-reuse path lands
2197
- # the same end-state as a fresh create.
2198
- dscl . -create "/Users/$MACOS_USER" UserShell "$SHELL_PATH"
2199
- dscl . -create "/Users/$MACOS_USER" RealName "OpenApe Agent $NAME"
2200
- dscl . -create "/Users/$MACOS_USER" UniqueID "$NEXT_UID"
2201
- dscl . -create "/Users/$MACOS_USER" PrimaryGroupID 20
2202
- dscl . -create "/Users/$MACOS_USER" NFSHomeDirectory "$HOME_DIR"
2203
- dscl . -create "/Users/$MACOS_USER" IsHidden 1
2204
-
2205
- mkdir -p "$HOME_DIR/.ssh" "$HOME_DIR/.config/apes"
2154
+ # Identity dirs \u2014 created owned by the agent so the file writes below land
2155
+ # with the right owner even before the final recursive chown.
2156
+ install -d -m 700 -o "$NAME" "$HOME_DIR/.ssh"
2157
+ install -d -m 700 -o "$NAME" "$HOME_DIR/.config"
2158
+ install -d -m 700 -o "$NAME" "$HOME_DIR/.config/apes"
2159
+ install -d -m 700 -o "$NAME" "$HOME_DIR/.config/openape"
2206
2160
 
2207
2161
  cat > "$HOME_DIR/.ssh/id_ed25519" ${shHeredoc(privatePemForHeredoc.trimEnd())}
2208
- cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(`${input.publicKeySshLine}`)}
2162
+ cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(input.publicKeySshLine)}
2209
2163
  cat > "$HOME_DIR/.config/apes/auth.json" ${shHeredoc(input.authJson)}
2210
- mkdir -p "$HOME_DIR/.config/openape"
2211
2164
  cat > "$HOME_DIR/.config/openape/agent-x25519.key" ${shHeredoc(input.x25519PrivateKey)}
2212
2165
  cat > "$HOME_DIR/.config/openape/agent-x25519.key.pub" ${shHeredoc(input.x25519PublicKey)}
2213
- ${claudeBlock}${claudeTokenBlock}${buildBridgeBlock(input.bridge)}${buildTroopBlock(input.troop)}
2214
- chown -R "$MACOS_USER:staff" "$HOME_DIR"
2166
+ ${claudeBlock}${claudeTokenBlock}
2167
+ # Per-agent task dir that \`apes agents sync\` writes to (XDG-style on
2168
+ # Linux; was ~/Library/... on macOS).
2169
+ mkdir -p "$HOME_DIR/.openape/agent/tasks"
2170
+
2171
+ chown -R "$NAME:" "$HOME_DIR"
2215
2172
  chmod 700 "$HOME_DIR/.ssh"
2216
2173
  chmod 700 "$HOME_DIR/.config"
2217
2174
  chmod 700 "$HOME_DIR/.config/openape"
@@ -2221,142 +2178,10 @@ chmod 600 "$HOME_DIR/.config/apes/auth.json"
2221
2178
  chmod 600 "$HOME_DIR/.config/openape/agent-x25519.key"
2222
2179
  chmod 644 "$HOME_DIR/.config/openape/agent-x25519.key.pub"
2223
2180
  if [ -f "$HOME_DIR/.config/openape/claude-token.env" ]; then
2224
- chmod 700 "$HOME_DIR/.config/openape"
2225
2181
  chmod 600 "$HOME_DIR/.config/openape/claude-token.env"
2226
2182
  fi
2227
2183
 
2228
- echo "OK $NAME (macOS user $MACOS_USER) uid=$NEXT_UID home=$HOME_DIR"
2229
- ${buildBridgeBootstrapBlock(input.bridge, name)}${buildTroopBootstrapBlock(input.troop, name)}`;
2230
- }
2231
- function buildBridgeBlock(bridge) {
2232
- if (!bridge) return "";
2233
- return `
2234
- mkdir -p "$HOME_DIR/Library/Application Support/openape/bridge" "$HOME_DIR/Library/Logs"
2235
- cat > "$HOME_DIR/Library/Application Support/openape/bridge/.env" ${shHeredoc(bridge.envFile)}
2236
- chmod 600 "$HOME_DIR/Library/Application Support/openape/bridge/.env"
2237
- `;
2238
- }
2239
- function buildBridgeBootstrapBlock(_bridge, _name) {
2240
- return "";
2241
- }
2242
- function buildTroopBlock(_troop) {
2243
- return `
2244
- mkdir -p "$HOME_DIR/Library/Logs" "$HOME_DIR/.openape/agent/tasks"
2245
- `;
2246
- }
2247
- function buildTroopBootstrapBlock(_troop, _name) {
2248
- return "";
2249
- }
2250
- function runPhaseGTeardownInProcess(input) {
2251
- const { name, homeDir } = input;
2252
- const macOSUser = input.macOSUsername ?? name;
2253
- let uid = null;
2254
- try {
2255
- const out = execFileSync2("/usr/bin/dscl", [".", "-read", `/Users/${macOSUser}`, "UniqueID"], { encoding: "utf8" });
2256
- const m = out.match(/UniqueID:\s*(\d+)/);
2257
- if (m) uid = m[1];
2258
- } catch {
2259
- }
2260
- if (uid) {
2261
- try {
2262
- execFileSync2("/bin/launchctl", ["bootout", `user/${uid}`], { stdio: "ignore" });
2263
- } catch {
2264
- }
2265
- try {
2266
- execFileSync2("/usr/bin/pkill", ["-9", "-u", uid], { stdio: "ignore" });
2267
- } catch {
2268
- }
2269
- }
2270
- const agentDir = `/var/openape/agents/${name}`;
2271
- try {
2272
- rmSync(agentDir, { recursive: true, force: true });
2273
- } catch {
2274
- }
2275
- if (homeDir && homeDir !== "/" && homeDir.startsWith("/var/openape/homes/")) {
2276
- try {
2277
- rmSync(homeDir, { recursive: true, force: true });
2278
- } catch {
2279
- }
2280
- }
2281
- console.log(`OK Phase-G teardown done for ${name} (dscl record /Users/${macOSUser} kept as tombstone)`);
2282
- }
2283
- function buildDestroyTeardownScript(input) {
2284
- const { name, homeDir, adminUser } = input;
2285
- return `#!/bin/bash
2286
- # Best-effort teardown. set -u catches typos; we deliberately do NOT use -e
2287
- # because pkill / launchctl are allowed to fail when the user has no live
2288
- # sessions.
2289
- set -u
2290
-
2291
- NAME=${shQuote(name)}
2292
- HOME_DIR=${shQuote(homeDir)}
2293
- ADMIN_USER=${shQuote(adminUser)}
2294
-
2295
- # Read the admin password from stdin (line 1). The caller pipes it in.
2296
- # We never accept it as an argv element so it can't show up in process
2297
- # listings or escapes' audit log.
2298
- read -r ADMIN_PASSWORD
2299
- if [ -z "$ADMIN_PASSWORD" ]; then
2300
- echo "ERROR: no admin password on stdin (expected one line)." >&2
2301
- exit 2
2302
- fi
2303
-
2304
- UID_OF=$(dscl . -read "/Users/$NAME" UniqueID 2>/dev/null | awk '/UniqueID:/ {print $2}')
2305
-
2306
- if [ -n "$UID_OF" ]; then
2307
- launchctl bootout "user/$UID_OF" 2>/dev/null || true
2308
- pkill -9 -u "$UID_OF" 2>/dev/null || true
2309
- fi
2310
-
2311
- # Per-agent system LaunchDaemon written by spawn (unless --no-bridge). Bootout +
2312
- # delete must come BEFORE we delete the user, otherwise launchd keeps a
2313
- # zombie reference. No-op if the plist isn't there.
2314
- BRIDGE_LABEL="eco.hofmann.apes.bridge.$NAME"
2315
- BRIDGE_PLIST="/Library/LaunchDaemons/$BRIDGE_LABEL.plist"
2316
- if [ -f "$BRIDGE_PLIST" ]; then
2317
- launchctl bootout "system/$BRIDGE_LABEL" 2>/dev/null || true
2318
- rm -f "$BRIDGE_PLIST"
2319
- fi
2320
-
2321
- if [ -d "$HOME_DIR" ] && [ "$HOME_DIR" != "/" ] && [ "$HOME_DIR" != "" ]; then
2322
- rm -rf "$HOME_DIR"
2323
- fi
2324
-
2325
- # \`escapes\` is a plain setuid binary \u2014 opendirectoryd sees no audit/PAM
2326
- # session attached (AUDIT_SESSION_ID=unset) and rejects DirectoryService
2327
- # writes from this context: a bare \`sysadminctl -deleteUser\` or
2328
- # \`dscl . -delete\` hangs ~5 minutes and exits with eUndefinedError -14987
2329
- # at DSRecord.m:563. Passing explicit -adminUser/-adminPassword bypasses
2330
- # opendirectoryd's implicit "is current session admin?" check and
2331
- # authenticates against DirectoryService directly \u2014 the delete then
2332
- # completes in ~1 second.
2333
- if ! command -v sysadminctl >/dev/null 2>&1; then
2334
- echo "ERROR: sysadminctl not available; cannot delete user record." >&2
2335
- exit 1
2336
- fi
2337
-
2338
- sysadminctl \\
2339
- -deleteUser "$NAME" \\
2340
- -adminUser "$ADMIN_USER" \\
2341
- -adminPassword "$ADMIN_PASSWORD"
2342
- SYSAD_EC=$?
2343
- unset ADMIN_PASSWORD
2344
-
2345
- if [ $SYSAD_EC -ne 0 ]; then
2346
- echo "ERROR: sysadminctl -deleteUser failed (exit=$SYSAD_EC)." >&2
2347
- echo " Common causes: wrong admin password, admin user '$ADMIN_USER'" >&2
2348
- echo " not in admin group, or target user '$NAME' is the last secure" >&2
2349
- echo " token holder (run \\\`sysadminctl -secureTokenStatus $NAME\\\`)." >&2
2350
- exit 1
2351
- fi
2352
-
2353
- # Verify the record is actually gone.
2354
- if dscl . -read "/Users/$NAME" >/dev/null 2>&1; then
2355
- echo "ERROR: user record /Users/$NAME still exists after teardown" >&2
2356
- exit 1
2357
- fi
2358
-
2359
- echo "OK destroyed $NAME"
2184
+ echo "OK $NAME (linux user) uid=$NEW_UID home=$HOME_DIR"
2360
2185
  `;
2361
2186
  }
2362
2187
  function shQuote(s) {
@@ -2401,81 +2226,11 @@ print(json.dumps(out))
2401
2226
  '
2402
2227
  `;
2403
2228
 
2404
- // src/lib/macos-user.ts
2405
- import { execFileSync as execFileSync3 } from "child_process";
2406
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2407
- function isDarwin() {
2408
- return process.platform === "darwin";
2409
- }
2410
- var MACOS_USER_PREFIX = "openape-agent-";
2411
- function macOSUsernameForAgent(agentName) {
2412
- return `${MACOS_USER_PREFIX}${agentName}`;
2413
- }
2414
- function lookupMacOSUserForAgent(agentName) {
2415
- return readMacOSUser(macOSUsernameForAgent(agentName)) ?? readMacOSUser(agentName);
2416
- }
2417
- function listOrphanedAgentRecords() {
2418
- if (!isDarwin()) return [];
2419
- let output;
2420
- try {
2421
- output = execFileSync3("dscl", [".", "-list", "/Users", "NFSHomeDirectory"], {
2422
- encoding: "utf-8",
2423
- stdio: ["ignore", "pipe", "pipe"]
2424
- });
2425
- } catch {
2426
- return [];
2427
- }
2428
- const orphans = [];
2429
- for (const line of output.split("\n")) {
2430
- const parts = line.trim().split(/\s+/);
2431
- if (parts.length < 2) continue;
2432
- const name = parts[0];
2433
- const homeDir = parts.slice(1).join(" ");
2434
- const looksLikeAgent = name.startsWith(MACOS_USER_PREFIX) || homeDir.startsWith("/var/openape/homes/");
2435
- if (!looksLikeAgent) continue;
2436
- if (existsSync3(homeDir)) continue;
2437
- const record = readMacOSUser(name);
2438
- orphans.push({ name, uid: record?.uid ?? null, homeDir });
2439
- }
2440
- return orphans;
2441
- }
2442
- function readMacOSUser(name) {
2443
- let output;
2444
- try {
2445
- output = execFileSync3("dscl", [".", "-read", `/Users/${name}`], {
2446
- encoding: "utf-8",
2447
- stdio: ["ignore", "pipe", "pipe"]
2448
- });
2449
- } catch {
2450
- return null;
2451
- }
2452
- const uidMatch = output.match(/UniqueID:\s*(\d+)/);
2453
- const shellMatch = output.match(/UserShell:\s*(\S.*)$/m);
2454
- const homeMatch = output.match(/NFSHomeDirectory:\s*(\S.*)$/m);
2455
- return {
2456
- name,
2457
- uid: uidMatch ? Number.parseInt(uidMatch[1], 10) : null,
2458
- shell: shellMatch ? shellMatch[1].trim() : null,
2459
- homeDir: homeMatch ? homeMatch[1].trim() : null
2460
- };
2461
- }
2462
- function listMacOSUserNames() {
2463
- let output;
2464
- try {
2465
- output = execFileSync3("dscl", [".", "-list", "/Users"], {
2466
- encoding: "utf-8",
2467
- stdio: ["ignore", "pipe", "pipe"]
2468
- });
2469
- } catch {
2470
- return /* @__PURE__ */ new Set();
2471
- }
2472
- return new Set(
2473
- output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0)
2474
- );
2475
- }
2229
+ // src/lib/which.ts
2230
+ import { execFileSync as execFileSync2 } from "child_process";
2476
2231
  function whichBinary(name) {
2477
2232
  try {
2478
- const out = execFileSync3("which", [name], {
2233
+ const out = execFileSync2("which", [name], {
2479
2234
  encoding: "utf-8",
2480
2235
  stdio: ["ignore", "pipe", "ignore"]
2481
2236
  }).trim();
@@ -2484,193 +2239,34 @@ function whichBinary(name) {
2484
2239
  return null;
2485
2240
  }
2486
2241
  }
2487
- function isShellRegistered(shellPath) {
2488
- if (!existsSync3("/etc/shells")) return false;
2489
- const content = readFileSync2("/etc/shells", "utf-8");
2490
- return content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).includes(shellPath);
2491
- }
2492
2242
 
2493
2243
  // src/lib/host-platform/index.ts
2494
2244
  import process2 from "process";
2495
2245
 
2496
- // src/lib/macos-host.ts
2497
- import { execFileSync as execFileSync4 } from "child_process";
2498
- import { hostname as hostname3 } from "os";
2499
- function getHostId() {
2500
- try {
2501
- const output = execFileSync4(
2502
- "/usr/sbin/ioreg",
2503
- ["-d2", "-c", "IOPlatformExpertDevice"],
2504
- { encoding: "utf8", timeout: 2e3 }
2505
- );
2506
- const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
2507
- return match ? match[1].trim().toLowerCase() : "";
2508
- } catch {
2509
- return "";
2510
- }
2511
- }
2512
- function getHostname() {
2513
- try {
2514
- return hostname3();
2515
- } catch {
2516
- return "";
2517
- }
2518
- }
2519
-
2520
- // src/lib/host-platform/darwin-nest.ts
2521
- import { execFileSync as execFileSync5 } from "child_process";
2522
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
2523
- import { userInfo } from "os";
2524
- import { join as join2 } from "path";
2525
- var PLIST_LABEL = "ai.openape.nest";
2526
- function plistPath(userHome) {
2527
- return join2(userHome, "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
2528
- }
2529
- function xmlEscape(s) {
2530
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2531
- }
2532
- function buildNestPlist(spec) {
2533
- const logsDir = join2(spec.userHome, "Library", "Logs");
2534
- return `<?xml version="1.0" encoding="UTF-8"?>
2535
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2536
- <plist version="1.0">
2537
- <dict>
2538
- <key>Label</key>
2539
- <string>${xmlEscape(PLIST_LABEL)}</string>
2540
- <key>ProgramArguments</key>
2541
- <array>
2542
- <string>${xmlEscape(spec.nestBin)}</string>
2543
- </array>
2544
- <key>WorkingDirectory</key>
2545
- <string>${xmlEscape(spec.nestHome)}</string>
2546
- <key>RunAtLoad</key>
2547
- <true/>
2548
- <key>KeepAlive</key>
2549
- <true/>
2550
- <key>ThrottleInterval</key>
2551
- <integer>10</integer>
2552
- <key>EnvironmentVariables</key>
2553
- <dict>
2554
- <key>HOME</key><string>${xmlEscape(spec.nestHome)}</string>
2555
- <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
2556
- <key>OPENAPE_NEST_PORT</key><string>${spec.port}</string>
2557
- <key>OPENAPE_APES_BIN</key><string>${xmlEscape(spec.apesBin)}</string>
2558
- </dict>
2559
- <key>StandardOutPath</key>
2560
- <string>${xmlEscape(logsDir)}/openape-nest.log</string>
2561
- <key>StandardErrorPath</key>
2562
- <string>${xmlEscape(logsDir)}/openape-nest.log</string>
2563
- </dict>
2564
- </plist>
2565
- `;
2566
- }
2567
- async function installNestSupervisorOnDarwin(spec) {
2568
- const path2 = plistPath(spec.userHome);
2569
- mkdirSync(join2(spec.userHome, "Library", "LaunchAgents"), { recursive: true });
2570
- const desired = buildNestPlist(spec);
2571
- let existing = "";
2572
- try {
2573
- existing = readFileSync3(path2, "utf8");
2574
- } catch {
2575
- }
2576
- if (existing !== desired) {
2577
- writeFileSync(path2, desired, { mode: 420 });
2578
- }
2579
- const uid = userInfo().uid;
2580
- try {
2581
- execFileSync5("/bin/launchctl", ["bootout", `gui/${uid}/${PLIST_LABEL}`], { stdio: "ignore" });
2582
- } catch {
2583
- }
2584
- execFileSync5("/bin/launchctl", ["bootstrap", `gui/${uid}`, path2], { stdio: "inherit" });
2585
- }
2586
- async function uninstallNestSupervisorOnDarwin() {
2587
- const home = userInfo().homedir;
2588
- const uid = userInfo().uid;
2589
- const path2 = plistPath(home);
2590
- try {
2591
- execFileSync5("/bin/launchctl", ["bootout", `gui/${uid}/${PLIST_LABEL}`], { stdio: "ignore" });
2592
- } catch {
2593
- }
2594
- if (existsSync4(path2)) unlinkSync(path2);
2595
- }
2596
-
2597
- // src/lib/host-platform/darwin-exec.ts
2598
- import { execFileSync as execFileSync6, spawnSync } from "child_process";
2599
- import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
2600
- import { tmpdir } from "os";
2601
- import { join as join3 } from "path";
2602
- function resolveApesBinary() {
2603
- return process.env.OPENAPE_APES_BIN || "apes";
2604
- }
2605
- async function runPrivilegedBashOnDarwin(script) {
2606
- const dir = mkdtempSync(join3(tmpdir(), "apes-privileged-"));
2607
- const scriptPath = join3(dir, "run.sh");
2608
- writeFileSync2(scriptPath, script, { mode: 448 });
2609
- try {
2610
- if (process.getuid?.() === 0) {
2611
- execFileSync6("bash", [scriptPath], { stdio: "inherit" });
2612
- } else {
2613
- execFileSync6(resolveApesBinary(), ["run", "--as", "root", "--wait", "--", "bash", scriptPath], { stdio: "inherit" });
2614
- }
2615
- } finally {
2616
- try {
2617
- rmSync2(dir, { recursive: true, force: true });
2618
- } catch {
2619
- }
2620
- }
2621
- }
2622
- async function runAsAgentUserOnDarwin(agentName, argv) {
2623
- const r = spawnSync(
2624
- resolveApesBinary(),
2625
- ["run", "--as", agentName, "--wait", "--", ...argv],
2626
- { encoding: "utf8" }
2627
- );
2628
- return {
2629
- stdout: r.stdout ?? "",
2630
- stderr: r.stderr ?? "",
2631
- exitCode: r.status ?? 1
2632
- };
2633
- }
2634
-
2635
- // src/lib/host-platform/darwin.ts
2636
- var darwinHostPlatform = {
2637
- getHostId,
2638
- getHostname,
2639
- agentUsername: macOSUsernameForAgent,
2640
- lookupAgentUser: (agentName) => lookupMacOSUserForAgent(agentName),
2641
- readAgentUser: (osName) => readMacOSUser(osName),
2642
- listAgentUserNames: listMacOSUserNames,
2643
- listOrphanAgentUsers: () => listOrphanedAgentRecords().map((r) => ({ name: r.name, uid: r.uid, homeDir: r.homeDir })),
2644
- installNestSupervisor: installNestSupervisorOnDarwin,
2645
- uninstallNestSupervisor: uninstallNestSupervisorOnDarwin,
2646
- runPrivilegedBash: runPrivilegedBashOnDarwin,
2647
- runAsAgentUser: runAsAgentUserOnDarwin
2648
- };
2649
-
2650
2246
  // src/lib/host-platform/linux-host.ts
2651
- import { hostname as hostname4 } from "os";
2652
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
2247
+ import { hostname as hostname3 } from "os";
2248
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2653
2249
  var FALLBACK_PATHS = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
2654
2250
  function getLinuxHostId() {
2655
2251
  for (const path2 of FALLBACK_PATHS) {
2656
- if (!existsSync5(path2)) continue;
2252
+ if (!existsSync3(path2)) continue;
2657
2253
  try {
2658
- const v = readFileSync4(path2, "utf-8").trim();
2254
+ const v = readFileSync2(path2, "utf-8").trim();
2659
2255
  if (v) return v;
2660
2256
  } catch {
2661
2257
  }
2662
2258
  }
2663
- return hostname4();
2259
+ return hostname3();
2664
2260
  }
2665
2261
  function getLinuxHostname() {
2666
- return hostname4();
2262
+ return hostname3();
2667
2263
  }
2668
2264
 
2669
2265
  // src/lib/host-platform/linux-user.ts
2670
- import { execFileSync as execFileSync7 } from "child_process";
2266
+ import { execFileSync as execFileSync3 } from "child_process";
2671
2267
  function getentPasswd(name) {
2672
2268
  try {
2673
- return execFileSync7("getent", ["passwd", name], {
2269
+ return execFileSync3("getent", ["passwd", name], {
2674
2270
  encoding: "utf-8",
2675
2271
  stdio: ["ignore", "pipe", "ignore"]
2676
2272
  }).trim() || null;
@@ -2698,7 +2294,7 @@ function readLinuxUser(name) {
2698
2294
  }
2699
2295
  function listLinuxUserNames() {
2700
2296
  try {
2701
- const out = execFileSync7("getent", ["passwd"], {
2297
+ const out = execFileSync3("getent", ["passwd"], {
2702
2298
  encoding: "utf-8",
2703
2299
  stdio: ["ignore", "pipe", "ignore"]
2704
2300
  });
@@ -2714,29 +2310,29 @@ function listLinuxUserNames() {
2714
2310
  }
2715
2311
 
2716
2312
  // src/lib/host-platform/linux-exec.ts
2717
- import { execFileSync as execFileSync8, spawnSync as spawnSync2 } from "child_process";
2718
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
2719
- import { tmpdir as tmpdir2 } from "os";
2720
- import { join as join4 } from "path";
2313
+ import { execFileSync as execFileSync4, spawnSync } from "child_process";
2314
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
2315
+ import { tmpdir } from "os";
2316
+ import { join as join2 } from "path";
2721
2317
  async function runPrivilegedBashOnLinux(script) {
2722
- const dir = mkdtempSync2(join4(tmpdir2(), "apes-privileged-"));
2723
- const scriptPath = join4(dir, "run.sh");
2724
- writeFileSync3(scriptPath, script, { mode: 448 });
2318
+ const dir = mkdtempSync(join2(tmpdir(), "apes-privileged-"));
2319
+ const scriptPath = join2(dir, "run.sh");
2320
+ writeFileSync(scriptPath, script, { mode: 448 });
2725
2321
  try {
2726
2322
  if (process.getuid?.() === 0) {
2727
- execFileSync8("bash", [scriptPath], { stdio: "inherit" });
2323
+ execFileSync4("bash", [scriptPath], { stdio: "inherit" });
2728
2324
  } else {
2729
- execFileSync8("sudo", ["-n", "--", "bash", scriptPath], { stdio: "inherit" });
2325
+ execFileSync4("sudo", ["-n", "--", "bash", scriptPath], { stdio: "inherit" });
2730
2326
  }
2731
2327
  } finally {
2732
2328
  try {
2733
- rmSync3(dir, { recursive: true, force: true });
2329
+ rmSync(dir, { recursive: true, force: true });
2734
2330
  } catch {
2735
2331
  }
2736
2332
  }
2737
2333
  }
2738
2334
  async function runAsAgentUserOnLinux(agentName, argv) {
2739
- const r = spawnSync2("sudo", ["-n", "-H", "-u", agentName, "--", ...argv], { encoding: "utf8" });
2335
+ const r = spawnSync("sudo", ["-n", "-H", "-u", agentName, "--", ...argv], { encoding: "utf8" });
2740
2336
  return {
2741
2337
  stdout: r.stdout ?? "",
2742
2338
  stderr: r.stderr ?? "",
@@ -2745,8 +2341,8 @@ async function runAsAgentUserOnLinux(agentName, argv) {
2745
2341
  }
2746
2342
 
2747
2343
  // src/lib/host-platform/linux-nest.ts
2748
- import { execFileSync as execFileSync9 } from "child_process";
2749
- import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2344
+ import { execFileSync as execFileSync5 } from "child_process";
2345
+ import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
2750
2346
  var UNIT_NAME = "openape-nest.service";
2751
2347
  var UNIT_PATH = `/etc/systemd/system/${UNIT_NAME}`;
2752
2348
  function buildNestUnit(spec) {
@@ -2776,23 +2372,23 @@ async function installNestSupervisorOnLinux(spec) {
2776
2372
  const desired = buildNestUnit(spec);
2777
2373
  let existing = "";
2778
2374
  try {
2779
- existing = readFileSync5(UNIT_PATH, "utf8");
2375
+ existing = readFileSync3(UNIT_PATH, "utf8");
2780
2376
  } catch {
2781
2377
  }
2782
2378
  if (existing !== desired) {
2783
- writeFileSync4(UNIT_PATH, desired, { mode: 420 });
2379
+ writeFileSync2(UNIT_PATH, desired, { mode: 420 });
2784
2380
  }
2785
- execFileSync9("systemctl", ["daemon-reload"], { stdio: "inherit" });
2786
- execFileSync9("systemctl", ["enable", "--now", UNIT_NAME], { stdio: "inherit" });
2381
+ execFileSync5("systemctl", ["daemon-reload"], { stdio: "inherit" });
2382
+ execFileSync5("systemctl", ["enable", "--now", UNIT_NAME], { stdio: "inherit" });
2787
2383
  }
2788
2384
  async function uninstallNestSupervisorOnLinux() {
2789
2385
  try {
2790
- execFileSync9("systemctl", ["disable", "--now", UNIT_NAME], { stdio: "inherit" });
2386
+ execFileSync5("systemctl", ["disable", "--now", UNIT_NAME], { stdio: "inherit" });
2791
2387
  } catch {
2792
2388
  }
2793
- if (existsSync6(UNIT_PATH)) unlinkSync2(UNIT_PATH);
2389
+ if (existsSync4(UNIT_PATH)) unlinkSync(UNIT_PATH);
2794
2390
  try {
2795
- execFileSync9("systemctl", ["daemon-reload"], { stdio: "inherit" });
2391
+ execFileSync5("systemctl", ["daemon-reload"], { stdio: "inherit" });
2796
2392
  } catch {
2797
2393
  }
2798
2394
  }
@@ -2818,18 +2414,14 @@ var linuxHostPlatform = {
2818
2414
  };
2819
2415
 
2820
2416
  // src/lib/host-platform/index.ts
2821
- function isDarwin2() {
2822
- return process2.platform === "darwin";
2823
- }
2824
2417
  function isLinux() {
2825
2418
  return process2.platform === "linux";
2826
2419
  }
2827
2420
  var testOverride = null;
2828
2421
  function getHostPlatform() {
2829
2422
  if (testOverride) return testOverride;
2830
- if (isDarwin2()) return darwinHostPlatform;
2831
2423
  if (isLinux()) return linuxHostPlatform;
2832
- throw new Error(`unsupported host platform: ${process2.platform}`);
2424
+ throw new Error(`unsupported host platform: ${process2.platform} \u2014 OpenApe nests are Linux-only`);
2833
2425
  }
2834
2426
 
2835
2427
  // src/commands/agents/allow.ts
@@ -2842,7 +2434,7 @@ var allowAgentCommand = defineCommand22({
2842
2434
  agent: {
2843
2435
  type: "positional",
2844
2436
  required: true,
2845
- description: "Agent name (the macOS short username spawn created)"
2437
+ description: "Agent name (the Linux username spawn created)"
2846
2438
  },
2847
2439
  email: {
2848
2440
  type: "positional",
@@ -2859,11 +2451,8 @@ var allowAgentCommand = defineCommand22({
2859
2451
  if (!email.includes("@")) {
2860
2452
  throw new CliError(`Invalid email "${email}".`);
2861
2453
  }
2862
- if (!isDarwin2()) {
2863
- throw new CliError("`apes agents allow` is currently macOS-only.");
2864
- }
2865
2454
  if (!getHostPlatform().lookupAgentUser(agent)) {
2866
- throw new CliError(`No macOS user for agent "${agent}" \u2014 has it been spawned?`);
2455
+ throw new CliError(`No OS user for agent "${agent}" \u2014 has it been spawned?`);
2867
2456
  }
2868
2457
  const apes = whichBinary("apes");
2869
2458
  if (!apes) throw new CliError("`apes` not found on PATH.");
@@ -2894,7 +2483,7 @@ PY
2894
2483
  chmod 600 "$F"
2895
2484
  `;
2896
2485
  consola19.start(`Adding ${email} to ${agent}'s allowlist\u2026`);
2897
- execFileSync10(apes, ["run", "--as", agent, "--wait", "--", "bash", "-c", script], { stdio: "inherit" });
2486
+ execFileSync6(apes, ["run", "--as", agent, "--wait", "--", "bash", "-c", script], { stdio: "inherit" });
2898
2487
  consola19.success(`${agent} will auto-accept future contact requests from ${email} (within ~30s of next bridge connect).`);
2899
2488
  }
2900
2489
  });
@@ -2903,13 +2492,12 @@ function shQuote2(s) {
2903
2492
  }
2904
2493
 
2905
2494
  // src/commands/agents/cleanup-orphans.ts
2906
- import { execFileSync as execFileSync11 } from "child_process";
2907
2495
  import { defineCommand as defineCommand23 } from "citty";
2908
2496
  import consola20 from "consola";
2909
2497
  var cleanupOrphansCommand = defineCommand23({
2910
2498
  meta: {
2911
2499
  name: "cleanup-orphans",
2912
- description: "Delete tombstoned macOS user records left behind by `apes agents destroy` (run with sudo)."
2500
+ description: "Report agent-user tombstones. Linux userdel is atomic, so there are normally none."
2913
2501
  },
2914
2502
  args: {
2915
2503
  "dry-run": {
@@ -2921,81 +2509,37 @@ var cleanupOrphansCommand = defineCommand23({
2921
2509
  description: "Skip the interactive confirmation. Required when stdin is not a TTY."
2922
2510
  }
2923
2511
  },
2924
- async run({ args }) {
2925
- if (!isDarwin2()) {
2926
- throw new CliError(`\`apes agents cleanup-orphans\` is macOS-only. Detected platform: ${process.platform}.`);
2927
- }
2512
+ async run() {
2928
2513
  const orphans = getHostPlatform().listOrphanAgentUsers();
2929
2514
  if (orphans.length === 0) {
2930
- consola20.success("No agent tombstones found \u2014 dscl is clean.");
2515
+ consola20.success("No agent tombstones \u2014 userdel is clean on Linux.");
2931
2516
  return;
2932
2517
  }
2933
- consola20.info(`Found ${orphans.length} agent tombstone${orphans.length === 1 ? "" : "s"}:`);
2518
+ consola20.warn(`Found ${orphans.length} unexpected agent tombstone${orphans.length === 1 ? "" : "s"}:`);
2934
2519
  for (const o of orphans) {
2935
2520
  console.log(` \u2022 ${o.name}${o.uid !== null ? ` (uid=${o.uid})` : ""} \u2014 was ${o.homeDir}`);
2936
2521
  }
2937
- if (args["dry-run"]) {
2938
- consola20.info("Dry-run \u2014 no records deleted. Re-run without --dry-run to clean up.");
2939
- return;
2940
- }
2941
- if (process.geteuid?.() !== 0) {
2942
- throw new CliError(
2943
- "Must run as root so opendirectoryd accepts the sysadminctl-deleteUser calls. Re-run with `sudo apes agents cleanup-orphans` from a shell login (the sudo session inherits your admin audit-session, which opendirectoryd verifies)."
2944
- );
2945
- }
2946
- if (!args.force) {
2947
- if (!process.stdin.isTTY) {
2948
- throw new CliError(
2949
- "No TTY available for the interactive confirmation. Re-run with --force (this is the same flag CI / scripted callers use)."
2950
- );
2951
- }
2952
- const confirmed = await consola20.prompt(`Delete ${orphans.length} tombstone${orphans.length === 1 ? "" : "s"}?`, {
2953
- type: "confirm",
2954
- initial: false
2955
- });
2956
- if (typeof confirmed === "symbol" || !confirmed) {
2957
- consola20.info("Aborted \u2014 no records deleted.");
2958
- return;
2959
- }
2960
- }
2961
- let deleted = 0;
2962
- let failed = 0;
2963
- for (const o of orphans) {
2964
- try {
2965
- execFileSync11("/usr/sbin/sysadminctl", ["-deleteUser", o.name], {
2966
- stdio: ["ignore", "inherit", "inherit"]
2967
- });
2968
- consola20.success(`Deleted ${o.name}`);
2969
- deleted++;
2970
- } catch (err) {
2971
- consola20.warn(`Failed to delete ${o.name}: ${err instanceof Error ? err.message : String(err)}`);
2972
- failed++;
2973
- }
2974
- }
2975
- if (failed > 0) {
2976
- throw new CliError(`Cleanup finished with errors: ${deleted} deleted, ${failed} failed.`);
2977
- }
2978
- consola20.success(`Cleanup complete \u2014 ${deleted} tombstone${deleted === 1 ? "" : "s"} removed.`);
2522
+ consola20.info("Remove each one manually with `userdel -r <name>`.");
2979
2523
  }
2980
2524
  });
2981
2525
 
2982
2526
  // src/commands/agents/code.ts
2983
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
2527
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2984
2528
  import { homedir as homedir5 } from "os";
2985
- import { join as join6 } from "path";
2529
+ import { join as join4 } from "path";
2986
2530
  import process3 from "process";
2987
2531
  import { defineCommand as defineCommand24 } from "citty";
2988
2532
  import { consola as consola21 } from "consola";
2989
2533
  import { taskTools, runApeShell, runCodingTask, buildIssueGet, detectForge, createLlmReviewer, createLlmRiskAssessor, resolveMergePolicy } from "@openape/agent-runtime";
2990
2534
 
2991
2535
  // src/lib/agent-secrets-runtime.ts
2992
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6, watch } from "fs";
2536
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync4, watch } from "fs";
2993
2537
  import { homedir as homedir4 } from "os";
2994
- import { join as join5 } from "path";
2538
+ import { join as join3 } from "path";
2995
2539
  import { openString } from "@openape/core";
2996
- var CONFIG_DIR2 = join5(homedir4(), ".config", "openape");
2997
- var SECRETS_DIR = join5(CONFIG_DIR2, "secrets.d");
2998
- var X25519_KEY_PATH = join5(CONFIG_DIR2, "agent-x25519.key");
2540
+ var CONFIG_DIR2 = join3(homedir4(), ".config", "openape");
2541
+ var SECRETS_DIR = join3(CONFIG_DIR2, "secrets.d");
2542
+ var X25519_KEY_PATH = join3(CONFIG_DIR2, "agent-x25519.key");
2999
2543
  var X25519_PUBKEY_PATH = `${X25519_KEY_PATH}.pub`;
3000
2544
  function envNameFromFile(file) {
3001
2545
  if (!file.endsWith(".blob")) return null;
@@ -3003,13 +2547,13 @@ function envNameFromFile(file) {
3003
2547
  return /^[A-Z][A-Z0-9_]*$/.test(env) ? env : null;
3004
2548
  }
3005
2549
  function readAgentEncryptionKey(keyPath = X25519_KEY_PATH) {
3006
- if (!existsSync7(keyPath)) return null;
3007
- const k = readFileSync6(keyPath, "utf8").trim();
2550
+ if (!existsSync5(keyPath)) return null;
2551
+ const k = readFileSync4(keyPath, "utf8").trim();
3008
2552
  return k.length > 0 ? k : null;
3009
2553
  }
3010
2554
  function readAgentEncryptionPublicKey(pubPath = X25519_PUBKEY_PATH) {
3011
- if (!existsSync7(pubPath)) return null;
3012
- const k = readFileSync6(pubPath, "utf8").trim();
2555
+ if (!existsSync5(pubPath)) return null;
2556
+ const k = readFileSync4(pubPath, "utf8").trim();
3013
2557
  return k.length > 0 ? k : null;
3014
2558
  }
3015
2559
  function materializeSecrets(opts = {}) {
@@ -3020,12 +2564,12 @@ function materializeSecrets(opts = {}) {
3020
2564
  const applied = [];
3021
2565
  const failed = [];
3022
2566
  const key = readAgentEncryptionKey(opts.keyPath);
3023
- const files = key && existsSync7(dir) ? readdirSync(dir) : [];
2567
+ const files = key && existsSync5(dir) ? readdirSync(dir) : [];
3024
2568
  for (const file of files) {
3025
2569
  const name = envNameFromFile(file);
3026
2570
  if (!name) continue;
3027
2571
  try {
3028
- const box = JSON.parse(readFileSync6(join5(dir, file), "utf8"));
2572
+ const box = JSON.parse(readFileSync4(join3(dir, file), "utf8"));
3029
2573
  env[name] = openString(box, key);
3030
2574
  applied.push(name);
3031
2575
  } catch (e) {
@@ -3052,7 +2596,7 @@ function startSecretsWatcher(opts = {}) {
3052
2596
  appliedNames = new Set(r.applied);
3053
2597
  };
3054
2598
  run();
3055
- if (!existsSync7(dir)) return () => {
2599
+ if (!existsSync5(dir)) return () => {
3056
2600
  };
3057
2601
  let timer = null;
3058
2602
  const watcher = watch(dir, () => {
@@ -3079,9 +2623,9 @@ var DEFAULT_PERSONA = [
3079
2623
  ].join(" ");
3080
2624
  function readLitellmConfig(model) {
3081
2625
  const env = {};
3082
- const envPath = join6(homedir5(), "litellm", ".env");
3083
- if (existsSync8(envPath)) {
3084
- for (const raw of readFileSync7(envPath, "utf8").split("\n")) {
2626
+ const envPath = join4(homedir5(), "litellm", ".env");
2627
+ if (existsSync6(envPath)) {
2628
+ for (const raw of readFileSync5(envPath, "utf8").split("\n")) {
3085
2629
  const line = raw.trim();
3086
2630
  const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line);
3087
2631
  if (m) env[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
@@ -3097,11 +2641,11 @@ function readLitellmConfig(model) {
3097
2641
  return { apiBase, apiKey, model: model || process3.env.APE_CHAT_BRIDGE_MODEL || "claude-haiku-4-5" };
3098
2642
  }
3099
2643
  function readPersona(file) {
3100
- if (file && existsSync8(file)) return readFileSync7(file, "utf8");
3101
- const agentJson = join6(homedir5(), ".openape", "agent", "agent.json");
3102
- if (existsSync8(agentJson)) {
2644
+ if (file && existsSync6(file)) return readFileSync5(file, "utf8");
2645
+ const agentJson = join4(homedir5(), ".openape", "agent", "agent.json");
2646
+ if (existsSync6(agentJson)) {
3103
2647
  try {
3104
- const p = JSON.parse(readFileSync7(agentJson, "utf8"));
2648
+ const p = JSON.parse(readFileSync5(agentJson, "utf8"));
3105
2649
  if (p.systemPrompt?.trim()) return p.systemPrompt;
3106
2650
  } catch {
3107
2651
  }
@@ -3186,30 +2730,27 @@ ${result.reason}`);
3186
2730
  });
3187
2731
 
3188
2732
  // src/commands/agents/destroy.ts
3189
- import { execFileSync as execFileSync12 } from "child_process";
3190
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
3191
- import { tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
3192
- import { join as join8 } from "path";
3193
2733
  import { defineCommand as defineCommand25 } from "citty";
3194
2734
  import consola22 from "consola";
3195
2735
 
3196
2736
  // src/lib/nest-registry.ts
3197
- import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2737
+ import { existsSync as existsSync7, mkdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
3198
2738
  import { homedir as homedir6 } from "os";
3199
- import { join as join7 } from "path";
2739
+ import { join as join5 } from "path";
3200
2740
  function resolveRegistryPath() {
3201
- if (existsSync9("/var/openape/nest/agents.json")) return "/var/openape/nest/agents.json";
3202
- if (existsSync9("/var/openape/nest")) return "/var/openape/nest/agents.json";
3203
- return join7(homedir6(), ".openape", "nest", "agents.json");
2741
+ if (process.env.OPENAPE_NEST_REGISTRY_PATH) return process.env.OPENAPE_NEST_REGISTRY_PATH;
2742
+ if (existsSync7("/var/openape/nest/agents.json")) return "/var/openape/nest/agents.json";
2743
+ if (existsSync7("/var/openape/nest")) return "/var/openape/nest/agents.json";
2744
+ return join5(homedir6(), ".openape", "nest", "agents.json");
3204
2745
  }
3205
2746
  function emptyRegistry() {
3206
2747
  return { version: 1, agents: [] };
3207
2748
  }
3208
2749
  function readNestRegistry() {
3209
2750
  const path2 = resolveRegistryPath();
3210
- if (!existsSync9(path2)) return emptyRegistry();
2751
+ if (!existsSync7(path2)) return emptyRegistry();
3211
2752
  try {
3212
- const parsed = JSON.parse(readFileSync8(path2, "utf8"));
2753
+ const parsed = JSON.parse(readFileSync6(path2, "utf8"));
3213
2754
  if (parsed?.version !== 1 || !Array.isArray(parsed.agents)) return emptyRegistry();
3214
2755
  return parsed;
3215
2756
  } catch {
@@ -3220,10 +2761,10 @@ function writeNestRegistry(reg) {
3220
2761
  const path2 = resolveRegistryPath();
3221
2762
  const dir = path2.replace(/\/agents\.json$/, "");
3222
2763
  try {
3223
- mkdirSync2(dir, { recursive: true });
2764
+ mkdirSync(dir, { recursive: true });
3224
2765
  } catch {
3225
2766
  }
3226
- writeFileSync5(path2, `${JSON.stringify(reg, null, 2)}
2767
+ writeFileSync3(path2, `${JSON.stringify(reg, null, 2)}
3227
2768
  `, { mode: 432 });
3228
2769
  }
3229
2770
  function upsertNestAgent(entry) {
@@ -3242,65 +2783,11 @@ function removeNestAgent(name) {
3242
2783
  return true;
3243
2784
  }
3244
2785
 
3245
- // src/lib/silent-password.ts
3246
- function readPasswordSilent(prompt) {
3247
- if (!process.stdin.isTTY) {
3248
- return Promise.reject(new CliError(
3249
- "No TTY available for the silent password prompt. Set APES_ADMIN_PASSWORD in the environment instead."
3250
- ));
3251
- }
3252
- return new Promise((resolve4, reject) => {
3253
- process.stdout.write(prompt);
3254
- const wasRaw = process.stdin.isRaw ?? false;
3255
- process.stdin.setRawMode(true);
3256
- process.stdin.resume();
3257
- process.stdin.setEncoding("utf8");
3258
- let buf = "";
3259
- let cleanupFn;
3260
- const cleanup = () => cleanupFn?.();
3261
- const onData = (chunk) => {
3262
- for (const ch of chunk) {
3263
- const code = ch.charCodeAt(0);
3264
- if (ch === "\r" || ch === "\n") {
3265
- cleanup();
3266
- process.stdout.write("\n");
3267
- resolve4(buf);
3268
- return;
3269
- }
3270
- if (code === 3) {
3271
- cleanup();
3272
- process.stdout.write("\n");
3273
- reject(new CliError("Aborted by user (Ctrl-C)."));
3274
- return;
3275
- }
3276
- if (code === 4 && buf.length === 0) {
3277
- cleanup();
3278
- process.stdout.write("\n");
3279
- reject(new CliError("Aborted by user (Ctrl-D)."));
3280
- return;
3281
- }
3282
- if (code === 127 || code === 8) {
3283
- if (buf.length > 0) buf = buf.slice(0, -1);
3284
- continue;
3285
- }
3286
- if (code < 32) continue;
3287
- buf += ch;
3288
- }
3289
- };
3290
- cleanupFn = () => {
3291
- process.stdin.removeListener("data", onData);
3292
- process.stdin.setRawMode(wasRaw);
3293
- process.stdin.pause();
3294
- };
3295
- process.stdin.on("data", onData);
3296
- });
3297
- }
3298
-
3299
2786
  // src/commands/agents/destroy.ts
3300
2787
  var destroyAgentCommand = defineCommand25({
3301
2788
  meta: {
3302
2789
  name: "destroy",
3303
- description: "Tear down an agent: remove macOS user, hard-delete IdP agent, drop all SSH keys"
2790
+ description: "Tear down an agent: remove the OS user, hard-delete IdP agent, drop all SSH keys"
3304
2791
  },
3305
2792
  args: {
3306
2793
  name: {
@@ -3319,10 +2806,6 @@ var destroyAgentCommand = defineCommand25({
3319
2806
  "keep-os-user": {
3320
2807
  type: "boolean",
3321
2808
  description: "Skip OS-side teardown. Useful for CI where the agent has no OS user."
3322
- },
3323
- "root-stage": {
3324
- type: "boolean",
3325
- description: "Internal \u2014 destroy.ts re-invokes itself via `apes run --as root --` with this flag set, then runs only the Phase-G teardown (rm home, launchctl bootout, kill processes). Skips IdP + auth + interactive prompts since those already ran in the outer pass."
3326
2809
  }
3327
2810
  },
3328
2811
  async run({ args }) {
@@ -3332,18 +2815,6 @@ var destroyAgentCommand = defineCommand25({
3332
2815
  `Invalid agent name "${name}". Must match /^[a-z][a-z0-9-]{0,23}$/.`
3333
2816
  );
3334
2817
  }
3335
- if (args["root-stage"]) {
3336
- if (process.geteuid?.() !== 0) {
3337
- throw new CliError("--root-stage was passed but this process is not running as root. Refusing to continue.");
3338
- }
3339
- const platform = getHostPlatform();
3340
- const resolved = platform.lookupAgentUser(name);
3341
- const macOSUsername = resolved?.name ?? platform.agentUsername(name);
3342
- const homeDir = resolved?.homeDir ?? `/var/openape/homes/${macOSUsername}`;
3343
- consola22.start(`Running teardown for ${name} (Phase-G, root-stage)\u2026`);
3344
- runPhaseGTeardownInProcess({ name, homeDir, macOSUsername });
3345
- return;
3346
- }
3347
2818
  const auth = loadAuth();
3348
2819
  if (!auth) {
3349
2820
  throw new CliError("Not authenticated. Run `apes login` first.");
@@ -3355,7 +2826,7 @@ var destroyAgentCommand = defineCommand25({
3355
2826
  const owned = await apiFetch("/api/my-agents", { idp });
3356
2827
  const idpAgent = owned.find((u) => u.name === name);
3357
2828
  const idpExists = idpAgent !== void 0;
3358
- const osUser = isDarwin2() ? getHostPlatform().lookupAgentUser(name) : null;
2829
+ const osUser = getHostPlatform().lookupAgentUser(name);
3359
2830
  const osUserExists = !args["keep-os-user"] && osUser !== null;
3360
2831
  if (!idpExists && !osUserExists) {
3361
2832
  consola22.info(`Nothing to destroy: no IdP agent and no OS user for "${name}".`);
@@ -3364,8 +2835,8 @@ var destroyAgentCommand = defineCommand25({
3364
2835
  if (!args.force) {
3365
2836
  const consequences = [];
3366
2837
  if (osUserExists) {
3367
- const home = osUser?.homeDir ?? `/Users/${name}`;
3368
- consequences.push(`\u2022 Remove macOS user ${osUser?.name ?? name} and rm -rf ${home}`);
2838
+ const home = osUser?.homeDir ?? `/home/${name}`;
2839
+ consequences.push(`\u2022 Remove OS user ${osUser?.name ?? name} and rm -rf ${home}`);
3369
2840
  }
3370
2841
  if (idpExists) {
3371
2842
  consequences.push(args.soft ? `\u2022 Deactivate IdP agent ${idpAgent.email} (PATCH isActive=false)` : `\u2022 Hard-delete IdP agent ${idpAgent.email} and all its SSH keys`);
@@ -3395,69 +2866,17 @@ ${consequences.join("\n")}`);
3395
2866
  consola22.info("No IdP agent to remove (skipped).");
3396
2867
  }
3397
2868
  if (osUserExists) {
3398
- const macOSUsername = osUser?.name ?? getHostPlatform().agentUsername(name);
3399
- const fallbackHome = macOSUsername.startsWith("openape-agent-") ? `/var/openape/homes/${macOSUsername}` : `/Users/${macOSUsername}`;
3400
- const homeDir = osUser?.homeDir ?? fallbackHome;
3401
- const isPhaseG = homeDir.startsWith("/var/openape/homes/");
3402
- if (isPhaseG) {
3403
- if (process.geteuid?.() === 0) {
3404
- consola22.start("Running teardown (Phase G \u2014 already root, no grant needed)\u2026");
3405
- runPhaseGTeardownInProcess({ name, homeDir, macOSUsername });
3406
- } else {
3407
- consola22.start("Running teardown (Phase G \u2014 no admin password needed)\u2026");
3408
- execFileSync12("apes", [
3409
- "run",
3410
- "--as",
3411
- "root",
3412
- "--wait",
3413
- "--",
3414
- "apes",
3415
- "agents",
3416
- "destroy",
3417
- name,
3418
- "--force",
3419
- "--root-stage"
3420
- ], { stdio: "inherit" });
3421
- }
3422
- consola22.info(`dscl record /Users/${macOSUsername} kept as tombstone (hidden, no home). Run \`sudo apes agents cleanup-orphans\` to sweep accumulated tombstones.`);
3423
- } else {
3424
- const sudo = whichBinary("sudo");
3425
- if (!sudo) {
3426
- throw new CliError("`sudo` not found on PATH; required for OS teardown.");
3427
- }
3428
- const adminUser = userInfo2().username;
3429
- let adminPassword;
3430
- try {
3431
- adminPassword = await collectAdminPassword({ adminUser });
3432
- } catch (err) {
3433
- const headless = !process.stdin.isTTY && !process.env.APES_ADMIN_PASSWORD;
3434
- if (headless) {
3435
- consola22.warn(`Legacy OS teardown for ${name} requires a TTY or APES_ADMIN_PASSWORD; skipping. Run \`apes agents destroy ${name}\` from a shell later to fully clean up /Users/${name} + dscl record.`);
3436
- adminPassword = "";
3437
- } else {
3438
- throw err;
3439
- }
3440
- }
3441
- if (adminPassword) {
3442
- const scratch = mkdtempSync3(join8(tmpdir3(), `apes-destroy-${name}-`));
3443
- const scriptPath = join8(scratch, "teardown.sh");
3444
- try {
3445
- const script = buildDestroyTeardownScript({ name, homeDir, adminUser });
3446
- writeFileSync6(scriptPath, script, { mode: 448 });
3447
- consola22.start("Running teardown via sudo\u2026");
3448
- execFileSync12(sudo, ["-S", "--prompt=", "--", "bash", scriptPath], {
3449
- input: `${adminPassword}
3450
- ${adminPassword}
3451
- `,
3452
- stdio: ["pipe", "inherit", "inherit"]
3453
- });
3454
- } finally {
3455
- rmSync4(scratch, { recursive: true, force: true });
3456
- }
3457
- }
3458
- }
3459
- } else if (!args["keep-os-user"] && isDarwin2()) {
3460
- consola22.info("No macOS user to remove (skipped).");
2869
+ consola22.start(`Removing OS user ${name}\u2026`);
2870
+ await getHostPlatform().runPrivilegedBash(
2871
+ `#!/bin/bash
2872
+ set -euo pipefail
2873
+ if getent passwd ${JSON.stringify(name)} >/dev/null 2>&1; then
2874
+ pkill -9 -u ${JSON.stringify(name)} 2>/dev/null || true
2875
+ userdel -r ${JSON.stringify(name)}
2876
+ fi
2877
+ `
2878
+ );
2879
+ consola22.success(`Removed OS user ${name}.`);
3461
2880
  }
3462
2881
  try {
3463
2882
  removeNestAgent(name);
@@ -3467,15 +2886,6 @@ ${adminPassword}
3467
2886
  consola22.success(`Destroyed ${name}.`);
3468
2887
  }
3469
2888
  });
3470
- async function collectAdminPassword(opts) {
3471
- const fromEnv = process.env.APES_ADMIN_PASSWORD;
3472
- if (fromEnv && fromEnv.length > 0) return fromEnv;
3473
- const pw = await readPasswordSilent(`Password for ${opts.adminUser}: `);
3474
- if (pw.length === 0) {
3475
- throw new CliExit(0);
3476
- }
3477
- return pw;
3478
- }
3479
2889
 
3480
2890
  // src/commands/agents/list.ts
3481
2891
  import { defineCommand as defineCommand26 } from "citty";
@@ -3507,7 +2917,7 @@ var listAgentsCommand = defineCommand26({
3507
2917
  const all = await apiFetch("/api/my-agents", { idp });
3508
2918
  const filtered = args["include-inactive"] ? all : all.filter((u) => u.isActive !== false);
3509
2919
  const platform = getHostPlatform();
3510
- const osUsers = isDarwin2() ? platform.listAgentUserNames() : /* @__PURE__ */ new Set();
2920
+ const osUsers = platform.listAgentUserNames();
3511
2921
  const osStateOf = (agentName) => {
3512
2922
  const u = platform.lookupAgentUser(agentName);
3513
2923
  if (u) return { osUser: true, home: u.homeDir };
@@ -3543,14 +2953,14 @@ var listAgentsCommand = defineCommand26({
3543
2953
  for (const r of rows) {
3544
2954
  const active = r.isActive ? "\u2713" : "\u2717";
3545
2955
  const os = r.osUser ? "\u2713" : "\u2717";
3546
- const homeCol = r.home ?? (isDarwin2() ? "(missing)" : "(non-darwin)");
2956
+ const homeCol = r.home ?? "(missing)";
3547
2957
  console.log(`${r.name.padEnd(nameW)} ${r.email.padEnd(emailW)} ${active.padEnd(6)} ${os.padEnd(7)} ${homeCol}`);
3548
2958
  }
3549
2959
  }
3550
2960
  });
3551
2961
 
3552
2962
  // src/commands/agents/register.ts
3553
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
2963
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
3554
2964
  import { defineCommand as defineCommand27 } from "citty";
3555
2965
  import consola24 from "consola";
3556
2966
  var registerAgentCommand = defineCommand27({
@@ -3598,10 +3008,10 @@ var registerAgentCommand = defineCommand27({
3598
3008
  throw new CliError("Pass either --public-key or --public-key-file, not both.");
3599
3009
  }
3600
3010
  if (!publicKey && keyFile) {
3601
- if (!existsSync10(keyFile)) {
3011
+ if (!existsSync8(keyFile)) {
3602
3012
  throw new CliError(`Public-key file not found: ${keyFile}`);
3603
3013
  }
3604
- publicKey = readFileSync9(keyFile, "utf-8").trim();
3014
+ publicKey = readFileSync7(keyFile, "utf-8").trim();
3605
3015
  }
3606
3016
  if (!publicKey) {
3607
3017
  throw new CliError('Provide --public-key "<ssh-ed25519 line>" or --public-key-file <path>.');
@@ -3636,19 +3046,19 @@ var registerAgentCommand = defineCommand27({
3636
3046
  });
3637
3047
 
3638
3048
  // src/commands/agents/run.ts
3639
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3049
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
3640
3050
  import { homedir as homedir7 } from "os";
3641
- import { join as join9 } from "path";
3051
+ import { join as join6 } from "path";
3642
3052
  import { defineCommand as defineCommand28 } from "citty";
3643
3053
  import consola25 from "consola";
3644
3054
  import { taskTools as taskTools2, runLoop } from "@openape/agent-runtime";
3645
- var AUTH_PATH = join9(homedir7(), ".config", "apes", "auth.json");
3646
- var TASK_CACHE_DIR = join9(homedir7(), ".openape", "agent", "tasks");
3055
+ var AUTH_PATH = join6(homedir7(), ".config", "apes", "auth.json");
3056
+ var TASK_CACHE_DIR = join6(homedir7(), ".openape", "agent", "tasks");
3647
3057
  function readAuth() {
3648
- if (!existsSync11(AUTH_PATH)) {
3058
+ if (!existsSync9(AUTH_PATH)) {
3649
3059
  throw new CliError(`No agent auth found at ${AUTH_PATH}. Run \`apes agents spawn <name>\` first.`);
3650
3060
  }
3651
- const parsed = JSON.parse(readFileSync10(AUTH_PATH, "utf8"));
3061
+ const parsed = JSON.parse(readFileSync8(AUTH_PATH, "utf8"));
3652
3062
  if (!parsed.access_token) throw new CliError("auth.json missing access_token");
3653
3063
  return parsed;
3654
3064
  }
@@ -3684,26 +3094,26 @@ ${msg}`.slice(0, 9e3);
3684
3094
  }
3685
3095
  }
3686
3096
  function readTaskSpec(taskId) {
3687
- const path2 = join9(TASK_CACHE_DIR, `${taskId}.json`);
3688
- if (!existsSync11(path2)) {
3097
+ const path2 = join6(TASK_CACHE_DIR, `${taskId}.json`);
3098
+ if (!existsSync9(path2)) {
3689
3099
  throw new CliError(`No cached task spec at ${path2}. Run \`apes agents sync\` first to pull the task list from troop.`);
3690
3100
  }
3691
- return JSON.parse(readFileSync10(path2, "utf8"));
3101
+ return JSON.parse(readFileSync8(path2, "utf8"));
3692
3102
  }
3693
- var AGENT_CONFIG_PATH = join9(homedir7(), ".openape", "agent", "agent.json");
3103
+ var AGENT_CONFIG_PATH = join6(homedir7(), ".openape", "agent", "agent.json");
3694
3104
  function readAgentConfig() {
3695
- if (!existsSync11(AGENT_CONFIG_PATH)) return { systemPrompt: "" };
3105
+ if (!existsSync9(AGENT_CONFIG_PATH)) return { systemPrompt: "" };
3696
3106
  try {
3697
- return JSON.parse(readFileSync10(AGENT_CONFIG_PATH, "utf8"));
3107
+ return JSON.parse(readFileSync8(AGENT_CONFIG_PATH, "utf8"));
3698
3108
  } catch {
3699
3109
  return { systemPrompt: "" };
3700
3110
  }
3701
3111
  }
3702
3112
  function readLitellmConfig2(model) {
3703
- const envPath = join9(homedir7(), "litellm", ".env");
3113
+ const envPath = join6(homedir7(), "litellm", ".env");
3704
3114
  const env = {};
3705
- if (existsSync11(envPath)) {
3706
- for (const line of readFileSync10(envPath, "utf8").split(/\r?\n/)) {
3115
+ if (existsSync9(envPath)) {
3116
+ for (const line of readFileSync8(envPath, "utf8").split(/\r?\n/)) {
3707
3117
  const m = line.match(/^([A-Z_]+)=(.*)$/);
3708
3118
  if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
3709
3119
  }
@@ -3809,18 +3219,18 @@ var runAgentCommand = defineCommand28({
3809
3219
  });
3810
3220
 
3811
3221
  // src/commands/agents/serve.ts
3812
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3222
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
3813
3223
  import { homedir as homedir8 } from "os";
3814
- import { join as join10 } from "path";
3224
+ import { join as join7 } from "path";
3815
3225
  import { createInterface } from "readline";
3816
3226
  import { defineCommand as defineCommand29 } from "citty";
3817
3227
  import { taskTools as taskTools3, runLoop as runLoop2, RpcSessionMap } from "@openape/agent-runtime";
3818
- var AUTH_PATH2 = join10(homedir8(), ".config", "apes", "auth.json");
3228
+ var AUTH_PATH2 = join7(homedir8(), ".config", "apes", "auth.json");
3819
3229
  function readLitellmConfig3(model) {
3820
- const envPath = join10(homedir8(), "litellm", ".env");
3230
+ const envPath = join7(homedir8(), "litellm", ".env");
3821
3231
  const env = {};
3822
- if (existsSync12(envPath)) {
3823
- for (const line of readFileSync11(envPath, "utf8").split(/\r?\n/)) {
3232
+ if (existsSync10(envPath)) {
3233
+ for (const line of readFileSync9(envPath, "utf8").split(/\r?\n/)) {
3824
3234
  const m = line.match(/^([A-Z_]+)=(.*)$/);
3825
3235
  if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
3826
3236
  }
@@ -3852,9 +3262,9 @@ var serveAgentCommand = defineCommand29({
3852
3262
  if (!args.rpc) {
3853
3263
  throw new CliError("apes agents serve currently only supports --rpc mode");
3854
3264
  }
3855
- if (existsSync12(AUTH_PATH2)) {
3265
+ if (existsSync10(AUTH_PATH2)) {
3856
3266
  try {
3857
- JSON.parse(readFileSync11(AUTH_PATH2, "utf8"));
3267
+ JSON.parse(readFileSync9(AUTH_PATH2, "utf8"));
3858
3268
  } catch {
3859
3269
  }
3860
3270
  }
@@ -3937,61 +3347,9 @@ async function handleInbound(msg, sessions) {
3937
3347
  import { defineCommand as defineCommand30 } from "citty";
3938
3348
  import consola26 from "consola";
3939
3349
 
3940
- // src/lib/troop-bootstrap.ts
3941
- var SYNC_LABEL_PREFIX = "openape.troop.sync";
3942
- var SYNC_INTERVAL_SECONDS = 300;
3943
- function syncPlistLabel(agentName) {
3944
- return `${SYNC_LABEL_PREFIX}.${agentName}`;
3945
- }
3946
- function escape(s) {
3947
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3948
- }
3949
- function buildSyncPlist(input) {
3950
- const pathDirs = (input.hostBinDirs && input.hostBinDirs.length > 0 ? input.hostBinDirs : ["/opt/homebrew/bin", "/usr/local/bin"]).join(":");
3951
- const pathLine = ` <key>PATH</key><string>${escape(pathDirs)}:/usr/bin:/bin</string>
3952
- `;
3953
- const agentUserLine = ` <key>AGENT_USER</key><string>${escape(input.userName)}</string>
3954
- `;
3955
- const envBlock = input.troopUrl ? ` <key>EnvironmentVariables</key>
3956
- <dict>
3957
- <key>HOME</key><string>${escape(input.homeDir)}</string>
3958
- ${pathLine}${agentUserLine} <key>OPENAPE_TROOP_URL</key><string>${escape(input.troopUrl)}</string>
3959
- </dict>
3960
- ` : ` <key>EnvironmentVariables</key>
3961
- <dict>
3962
- <key>HOME</key><string>${escape(input.homeDir)}</string>
3963
- ${pathLine}${agentUserLine} </dict>
3964
- `;
3965
- return `<?xml version="1.0" encoding="UTF-8"?>
3966
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3967
- <plist version="1.0">
3968
- <dict>
3969
- <key>Label</key>
3970
- <string>${escape(syncPlistLabel(input.agentName))}</string>
3971
- <key>ProgramArguments</key>
3972
- <array>
3973
- <string>${escape(input.apesBin)}</string>
3974
- <string>agents</string>
3975
- <string>sync</string>
3976
- </array>
3977
- <key>WorkingDirectory</key>
3978
- <string>${escape(input.homeDir)}</string>
3979
- ${envBlock} <key>StartInterval</key>
3980
- <integer>${SYNC_INTERVAL_SECONDS}</integer>
3981
- <key>RunAtLoad</key>
3982
- <true/>
3983
- <key>StandardOutPath</key>
3984
- <string>${escape(input.homeDir)}/Library/Logs/openape-troop-sync.log</string>
3985
- <key>StandardErrorPath</key>
3986
- <string>${escape(input.homeDir)}/Library/Logs/openape-troop-sync.log</string>
3987
- </dict>
3988
- </plist>
3989
- `;
3990
- }
3991
-
3992
3350
  // src/lib/keygen.ts
3993
3351
  import { Buffer as Buffer4 } from "buffer";
3994
- import { existsSync as existsSync13, mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
3352
+ import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
3995
3353
  import { generateKeyPairSync } from "crypto";
3996
3354
  import { homedir as homedir9 } from "os";
3997
3355
  import { dirname, resolve as resolve2 } from "path";
@@ -4010,10 +3368,10 @@ function buildSshEd25519Line(rawPub) {
4010
3368
  }
4011
3369
  function readPublicKey(keyPath) {
4012
3370
  const pubPath = `${keyPath}.pub`;
4013
- if (existsSync13(pubPath)) {
4014
- return readFileSync12(pubPath, "utf-8").trim();
3371
+ if (existsSync11(pubPath)) {
3372
+ return readFileSync10(pubPath, "utf-8").trim();
4015
3373
  }
4016
- const keyContent = readFileSync12(keyPath, "utf-8");
3374
+ const keyContent = readFileSync10(keyPath, "utf-8");
4017
3375
  const privateKey = loadEd25519PrivateKey(keyContent);
4018
3376
  const jwk = privateKey.export({ format: "jwk" });
4019
3377
  const pubBytes = Buffer4.from(jwk.x, "base64url");
@@ -4022,16 +3380,16 @@ function readPublicKey(keyPath) {
4022
3380
  function generateAndSaveKey(keyPath) {
4023
3381
  const resolved = resolveKeyPath(keyPath);
4024
3382
  const dir = dirname(resolved);
4025
- if (!existsSync13(dir)) {
4026
- mkdirSync3(dir, { recursive: true });
3383
+ if (!existsSync11(dir)) {
3384
+ mkdirSync2(dir, { recursive: true });
4027
3385
  }
4028
3386
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
4029
3387
  const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
4030
- writeFileSync7(resolved, privatePem, { mode: 384 });
3388
+ writeFileSync4(resolved, privatePem, { mode: 384 });
4031
3389
  const jwk = publicKey.export({ format: "jwk" });
4032
3390
  const pubBytes = Buffer4.from(jwk.x, "base64url");
4033
3391
  const pubKeyStr = buildSshEd25519Line(pubBytes);
4034
- writeFileSync7(`${resolved}.pub`, `${pubKeyStr}
3392
+ writeFileSync4(`${resolved}.pub`, `${pubKeyStr}
4035
3393
  `, { mode: 420 });
4036
3394
  return pubKeyStr;
4037
3395
  }
@@ -4049,145 +3407,8 @@ function generateKeyPairInMemory() {
4049
3407
  };
4050
3408
  }
4051
3409
 
4052
- // src/lib/llm-bridge.ts
4053
- import { execFileSync as execFileSync13 } from "child_process";
4054
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
4055
- import { homedir as homedir10 } from "os";
4056
- import { dirname as dirname2, join as join11 } from "path";
4057
- var PLIST_LABEL_PREFIX = "eco.hofmann.apes.bridge";
4058
- function readLitellmEnv(envPath = join11(homedir10(), "litellm", ".env")) {
4059
- if (!existsSync14(envPath)) return null;
4060
- try {
4061
- const text = readFileSync13(envPath, "utf8");
4062
- const out = {};
4063
- for (const line of text.split("\n")) {
4064
- const trimmed = line.trim();
4065
- if (!trimmed || trimmed.startsWith("#")) continue;
4066
- const eq = trimmed.indexOf("=");
4067
- if (eq < 0) continue;
4068
- const key = trimmed.slice(0, eq).trim();
4069
- const value = trimmed.slice(eq + 1).trim();
4070
- if (key === "LITELLM_MASTER_KEY" || key === "LITELLM_API_KEY") out.apiKey = value;
4071
- if (key === "LITELLM_BASE_URL") out.baseUrl = value;
4072
- if (key === "APE_CHAT_BRIDGE_MODEL") out.model = value;
4073
- }
4074
- return out;
4075
- } catch {
4076
- return null;
4077
- }
4078
- }
4079
- function resolveBridgeConfig(opts) {
4080
- const env = readLitellmEnv(opts.envPath);
4081
- const apiKey = opts.cliKey ?? env?.apiKey;
4082
- const baseUrl = opts.cliBaseUrl ?? env?.baseUrl ?? "http://127.0.0.1:4000/v1";
4083
- const model = opts.cliModel ?? env?.model;
4084
- if (!apiKey) {
4085
- throw new Error(
4086
- "No LITELLM_API_KEY resolved. Pass --bridge-key sk-\u2026 or write LITELLM_MASTER_KEY into ~/litellm/.env first."
4087
- );
4088
- }
4089
- return { baseUrl, apiKey, model };
4090
- }
4091
- function captureHostBinDirs() {
4092
- const dirs = [];
4093
- const seen = /* @__PURE__ */ new Set();
4094
- for (const bin of ["node", "ape-agent", "apes"]) {
4095
- let resolved;
4096
- try {
4097
- resolved = execFileSync13("/usr/bin/which", [bin], { encoding: "utf8" }).trim();
4098
- } catch {
4099
- const installCmd = bin === "ape-agent" ? "npm i -g @openape/ape-agent" : bin === "apes" ? "npm i -g @openape/apes" : "install Node.js (e.g. brew install node)";
4100
- throw new Error(`'${bin}' not found on host PATH. ${installCmd} before spawning agents \u2014 the bridge runtime resolves these at spawn time and bakes the dir into the agent's launchd plist.`);
4101
- }
4102
- const dir = dirname2(resolved);
4103
- if (!seen.has(dir)) {
4104
- seen.add(dir);
4105
- dirs.push(dir);
4106
- }
4107
- }
4108
- return dirs;
4109
- }
4110
- function bridgePlistLabel(agentName) {
4111
- return `${PLIST_LABEL_PREFIX}.${agentName}`;
4112
- }
4113
- function bridgePlistPath(agentName) {
4114
- return `/Library/LaunchDaemons/${bridgePlistLabel(agentName)}.plist`;
4115
- }
4116
- function buildBridgeEnvFile(cfg) {
4117
- const modelLine = cfg.model ? `APE_CHAT_BRIDGE_MODEL=${cfg.model}
4118
- ` : "";
4119
- return `# Auto-generated by 'apes agents spawn'.
4120
- # Read by the chat-bridge daemon at boot to talk to the local LLM proxy.
4121
- LITELLM_BASE_URL=${cfg.baseUrl}
4122
- LITELLM_API_KEY=${cfg.apiKey}
4123
- ${modelLine}`;
4124
- }
4125
- function buildBridgeStartScript(hostBinDirs) {
4126
- const pathLine = `export PATH="${hostBinDirs.join(":")}:/usr/bin:/bin"`;
4127
- return `#!/usr/bin/env bash
4128
- # Auto-generated by 'apes agents spawn'.
4129
- # Slim launcher \u2014 bridge stack lives on the host, no per-agent install.
4130
- set -euo pipefail
4131
-
4132
- ${pathLine}
4133
-
4134
- # Token refresh is in-process via @openape/cli-auth's challenge-response
4135
- # path (auth.json.key_path -> ~/.ssh/id_ed25519). No "apes login" needed
4136
- # at boot \u2014 keeping start.sh slim avoids the rate-limit dance the old
4137
- # refresh hit when KeepAlive crash-restarted the daemon every 1h.
4138
-
4139
- set -a
4140
- . "$HOME/Library/Application Support/openape/bridge/.env"
4141
- set +a
4142
- exec ape-agent
4143
- `;
4144
- }
4145
- function buildBridgePlist(agentName, homeDir, ownerEmail, hostBinDirs) {
4146
- const startScript = `${homeDir}/Library/Application Support/openape/bridge/start.sh`;
4147
- const stdoutLog = `${homeDir}/Library/Logs/ape-agent.log`;
4148
- const stderrLog = `${homeDir}/Library/Logs/ape-agent.err.log`;
4149
- const pathValue = `${hostBinDirs.join(":")}:/usr/bin:/bin`;
4150
- return `<?xml version="1.0" encoding="UTF-8"?>
4151
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4152
- <plist version="1.0">
4153
- <dict>
4154
- <key>Label</key>
4155
- <string>${bridgePlistLabel(agentName)}</string>
4156
- <key>UserName</key>
4157
- <string>${agentName}</string>
4158
- <key>ProgramArguments</key>
4159
- <array>
4160
- <string>/bin/bash</string>
4161
- <string>${startScript}</string>
4162
- </array>
4163
- <key>WorkingDirectory</key>
4164
- <string>${homeDir}</string>
4165
- <key>RunAtLoad</key>
4166
- <true/>
4167
- <key>KeepAlive</key>
4168
- <true/>
4169
- <key>ThrottleInterval</key>
4170
- <integer>10</integer>
4171
- <key>StandardOutPath</key>
4172
- <string>${stdoutLog}</string>
4173
- <key>StandardErrorPath</key>
4174
- <string>${stderrLog}</string>
4175
- <key>EnvironmentVariables</key>
4176
- <dict>
4177
- <key>HOME</key>
4178
- <string>${homeDir}</string>
4179
- <key>PATH</key>
4180
- <string>${pathValue}</string>
4181
- <key>OPENAPE_OWNER_EMAIL</key>
4182
- <string>${ownerEmail}</string>
4183
- </dict>
4184
- </dict>
4185
- </plist>
4186
- `;
4187
- }
4188
-
4189
3410
  // src/commands/agents/spawn.ts
4190
- function readMacOSUidOrNull(name) {
3411
+ function readUidOrNull(name) {
4191
3412
  try {
4192
3413
  const u = getHostPlatform().readAgentUser(name);
4193
3414
  return u?.uid ?? null;
@@ -4198,17 +3419,17 @@ function readMacOSUidOrNull(name) {
4198
3419
  var spawnAgentCommand = defineCommand30({
4199
3420
  meta: {
4200
3421
  name: "spawn",
4201
- description: "Provision a local macOS agent end-to-end (OS user, keypair, IdP agent, Claude hook)"
3422
+ description: "Provision a local Linux agent end-to-end (OS user, keypair, IdP agent, Claude hook)"
4202
3423
  },
4203
3424
  args: {
4204
3425
  name: {
4205
3426
  type: "positional",
4206
- description: "Agent name \u2014 also the macOS short username (lowercase, [a-z0-9-], must start with a letter)",
3427
+ description: "Agent name \u2014 also the Linux username (lowercase, [a-z0-9-], must start with a letter)",
4207
3428
  required: true
4208
3429
  },
4209
3430
  shell: {
4210
3431
  type: "string",
4211
- description: "Login shell for the macOS user. Default: /bin/zsh. Pass $(which ape-shell) to opt into the grant-mediated REPL as login shell."
3432
+ description: "Login shell for the Linux user. Default: /bin/bash. Pass $(which ape-shell) to opt into the grant-mediated REPL as login shell."
4212
3433
  },
4213
3434
  "no-claude-hook": {
4214
3435
  type: "boolean",
@@ -4246,11 +3467,6 @@ var spawnAgentCommand = defineCommand30({
4246
3467
  `Invalid agent name "${name}". Must match /^[a-z][a-z0-9-]{0,23}$/ \u2014 lowercase letters, digits and hyphens, 1\u201324 chars, must start with a letter.`
4247
3468
  );
4248
3469
  }
4249
- if (!isDarwin2()) {
4250
- throw new CliError(
4251
- `\`apes agents spawn\` is currently macOS-only. Detected platform: ${process.platform}. Linux support is a follow-up; for now, use \`apes agents register\` plus a manually provisioned user.`
4252
- );
4253
- }
4254
3470
  const auth = loadAuth();
4255
3471
  if (!auth) {
4256
3472
  throw new CliError("Not authenticated. Run `apes login` first.");
@@ -4259,137 +3475,91 @@ var spawnAgentCommand = defineCommand30({
4259
3475
  if (!idp) {
4260
3476
  throw new CliError("No IdP URL configured. Run `apes login` first.");
4261
3477
  }
4262
- const loginShell = (args.shell ?? "/bin/zsh").toString();
4263
- const apes = whichBinary("apes");
4264
- if (!apes) {
4265
- throw new CliError("`apes` not found on PATH. Install @openape/apes globally first.");
4266
- }
4267
- const escapes = whichBinary("escapes");
4268
- if (!escapes) {
4269
- throw new CliError(
4270
- "`escapes` not found on PATH. spawn delegates the privileged setup phase to escapes; install it before running spawn."
4271
- );
4272
- }
4273
- if (!isShellRegistered(loginShell)) {
4274
- throw new CliError(
4275
- `${loginShell} is not registered in /etc/shells. macOS refuses to set it as a login shell. Run:
4276
- echo ${loginShell} | sudo tee -a /etc/shells
4277
- and try again.`
4278
- );
4279
- }
3478
+ const loginShell = (args.shell ?? "/bin/bash").toString();
4280
3479
  const platform = getHostPlatform();
4281
- const macOSUsername = platform.agentUsername(name);
4282
- const existing = platform.readAgentUser(macOSUsername) ?? platform.readAgentUser(name);
3480
+ const osUsername = platform.agentUsername(name);
3481
+ const existing = platform.readAgentUser(osUsername);
4283
3482
  if (existing) {
4284
- throw new CliError(`macOS user "${existing.name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
3483
+ throw new CliError(`OS user "${existing.name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
4285
3484
  }
4286
- const homeDir = `/var/openape/homes/${macOSUsername}`;
3485
+ const homeDir = `/var/lib/openape/homes/${osUsername}`;
3486
+ consola26.start(`Generating keypair for ${name}\u2026`);
3487
+ const { privatePem, publicSshLine, x25519PrivateKey, x25519PublicKey } = generateKeyPairInMemory();
3488
+ consola26.start(`Registering agent at ${idp}\u2026`);
3489
+ const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
3490
+ consola26.success(`Registered as ${registration.email}`);
3491
+ consola26.start("Issuing agent access token\u2026");
3492
+ const { token, expiresIn } = await issueAgentToken({
3493
+ idp,
3494
+ agentEmail: registration.email,
3495
+ privateKeyPem: privatePem
3496
+ });
3497
+ const authJson = buildAgentAuthJson({
3498
+ idp,
3499
+ accessToken: token,
3500
+ email: registration.email,
3501
+ expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
3502
+ keyPath: `${homeDir}/.ssh/id_ed25519`,
3503
+ // The IdP resolves the owner transitively (when the caller
3504
+ // is itself an agent — e.g. a Nest spawning a child — the
3505
+ // human at the top of the chain becomes owner). Use the
3506
+ // server-resolved owner, not the local caller's auth.email,
3507
+ // otherwise the agent's auth.json will carry the Nest's
3508
+ // email and troop will reject sync calls because the
3509
+ // encoded owner-domain in the agent email doesn't match
3510
+ // the auth.json's owner_email domain.
3511
+ ownerEmail: registration.owner
3512
+ });
3513
+ const includeClaudeHook = !args["no-claude-hook"];
3514
+ const claudeOauthToken = await resolveClaudeToken({
3515
+ flag: typeof args["claude-token"] === "string" ? args["claude-token"] : void 0,
3516
+ fromStdin: !!args["claude-token-stdin"]
3517
+ });
3518
+ const withBridge = !args["no-bridge"];
3519
+ const script = buildSpawnSetupScript({
3520
+ name,
3521
+ homeDir,
3522
+ shellPath: loginShell,
3523
+ privateKeyPem: privatePem,
3524
+ publicKeySshLine: publicSshLine,
3525
+ x25519PrivateKey,
3526
+ x25519PublicKey,
3527
+ authJson,
3528
+ claudeSettingsJson: includeClaudeHook ? CLAUDE_SETTINGS_JSON : null,
3529
+ hookScriptSource: includeClaudeHook ? BASH_VIA_APE_SHELL_HOOK_SOURCE : null,
3530
+ claudeOauthToken
3531
+ });
3532
+ consola26.start("Running privileged setup\u2026");
3533
+ if (process.getuid?.() !== 0) {
3534
+ consola26.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
3535
+ }
3536
+ await platform.runPrivilegedBash(script);
4287
3537
  try {
4288
- consola26.start(`Generating keypair for ${name}\u2026`);
4289
- const { privatePem, publicSshLine, x25519PrivateKey, x25519PublicKey } = generateKeyPairInMemory();
4290
- consola26.start(`Registering agent at ${idp}\u2026`);
4291
- const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
4292
- consola26.success(`Registered as ${registration.email}`);
4293
- consola26.start("Issuing agent access token\u2026");
4294
- const { token, expiresIn } = await issueAgentToken({
4295
- idp,
4296
- agentEmail: registration.email,
4297
- privateKeyPem: privatePem
4298
- });
4299
- const authJson = buildAgentAuthJson({
4300
- idp,
4301
- accessToken: token,
4302
- email: registration.email,
4303
- expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
4304
- keyPath: `${homeDir}/.ssh/id_ed25519`,
4305
- // The IdP resolves the owner transitively (when the caller
4306
- // is itself an agent — e.g. a Nest spawning a child — the
4307
- // human at the top of the chain becomes owner). Use the
4308
- // server-resolved owner, not the local caller's auth.email,
4309
- // otherwise the agent's auth.json will carry the Nest's
4310
- // email and troop will reject sync calls because the
4311
- // encoded owner-domain in the agent email doesn't match
4312
- // the auth.json's owner_email domain.
4313
- ownerEmail: registration.owner
4314
- });
4315
- const includeClaudeHook = !args["no-claude-hook"];
4316
- const claudeOauthToken = await resolveClaudeToken({
4317
- flag: typeof args["claude-token"] === "string" ? args["claude-token"] : void 0,
4318
- fromStdin: !!args["claude-token-stdin"]
4319
- });
4320
- const withBridge = !args["no-bridge"];
4321
- const bridge = withBridge ? (() => {
4322
- const cfg = resolveBridgeConfig({
4323
- cliKey: typeof args["bridge-key"] === "string" ? args["bridge-key"] : void 0,
4324
- cliBaseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0,
4325
- cliModel: typeof args["bridge-model"] === "string" ? args["bridge-model"] : void 0
4326
- });
4327
- const hostBinDirs = captureHostBinDirs();
4328
- return {
4329
- plistLabel: bridgePlistLabel(name),
4330
- plistPath: bridgePlistPath(name),
4331
- plistContent: buildBridgePlist(name, homeDir, auth.email, hostBinDirs),
4332
- startScript: buildBridgeStartScript(hostBinDirs),
4333
- envFile: buildBridgeEnvFile(cfg)
4334
- };
4335
- })() : null;
4336
- const troopPlistLabel = `openape.troop.sync.${name}`;
4337
- const troopPlistPath = `/Library/LaunchDaemons/${troopPlistLabel}.plist`;
4338
- const troopBinDirs = bridge ? captureHostBinDirs() : captureHostBinDirs();
4339
- const troop = {
4340
- plistLabel: troopPlistLabel,
4341
- plistPath: troopPlistPath,
4342
- plistContent: buildSyncPlist({ agentName: name, apesBin: apes, homeDir, userName: name, hostBinDirs: troopBinDirs })
4343
- };
4344
- const script = buildSpawnSetupScript({
3538
+ const uid = readUidOrNull(osUsername) ?? -1;
3539
+ upsertNestAgent({
4345
3540
  name,
4346
- macOSUsername,
4347
- homeDir,
4348
- shellPath: loginShell,
4349
- privateKeyPem: privatePem,
4350
- publicKeySshLine: publicSshLine,
4351
- x25519PrivateKey,
4352
- x25519PublicKey,
4353
- authJson,
4354
- claudeSettingsJson: includeClaudeHook ? CLAUDE_SETTINGS_JSON : null,
4355
- hookScriptSource: includeClaudeHook ? BASH_VIA_APE_SHELL_HOOK_SOURCE : null,
4356
- claudeOauthToken,
4357
- bridge,
4358
- troop
3541
+ uid,
3542
+ home: homeDir,
3543
+ email: registration.email,
3544
+ registeredAt: Math.floor(Date.now() / 1e3),
3545
+ bridge: withBridge ? {
3546
+ baseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0,
3547
+ apiKey: typeof args["bridge-key"] === "string" ? args["bridge-key"] : void 0,
3548
+ model: typeof args["bridge-model"] === "string" ? args["bridge-model"] : void 0
3549
+ } : void 0
4359
3550
  });
4360
- consola26.start("Running privileged setup\u2026");
4361
- if (process.getuid?.() !== 0) {
4362
- consola26.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
4363
- }
4364
- await platform.runPrivilegedBash(script);
4365
- try {
4366
- const uid = readMacOSUidOrNull(macOSUsername) ?? readMacOSUidOrNull(name);
4367
- upsertNestAgent({
4368
- name,
4369
- uid: uid ?? -1,
4370
- home: homeDir,
4371
- email: registration.email,
4372
- registeredAt: Math.floor(Date.now() / 1e3),
4373
- bridge: withBridge ? {
4374
- baseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0,
4375
- apiKey: typeof args["bridge-key"] === "string" ? args["bridge-key"] : void 0,
4376
- model: typeof args["bridge-model"] === "string" ? args["bridge-model"] : void 0
4377
- } : void 0
4378
- });
4379
- } catch (err) {
4380
- consola26.warn(`Could not write to nest registry: ${err instanceof Error ? err.message : String(err)}`);
4381
- }
4382
- consola26.success(`Agent ${name} spawned.`);
4383
- consola26.info(`\u{1F517} Troop: https://troop.openape.ai/agents/${name}`);
4384
- if (withBridge) {
4385
- consola26.info(`On first boot, the bridge will send you a contact request from ${registration.email}.`);
4386
- consola26.info("Open chat.openape.ai and accept it to start chatting with the agent.");
4387
- }
4388
- console.log("");
4389
- console.log("Run as the agent with:");
4390
- console.log(` apes run --as ${name} -- claude --session-name ${name} --dangerously-skip-permissions`);
4391
- } finally {
3551
+ } catch (err) {
3552
+ consola26.warn(`Could not write to nest registry: ${err instanceof Error ? err.message : String(err)}`);
4392
3553
  }
3554
+ consola26.success(`Agent ${name} spawned.`);
3555
+ consola26.info(`\u{1F517} Troop: https://troop.openape.ai/agents/${name}`);
3556
+ if (withBridge) {
3557
+ consola26.info(`On first boot, the bridge will send you a contact request from ${registration.email}.`);
3558
+ consola26.info("Open chat.openape.ai and accept it to start chatting with the agent.");
3559
+ }
3560
+ console.log("");
3561
+ console.log("Run as the agent with:");
3562
+ console.log(` apes run --as ${name} -- claude --session-name ${name} --dangerously-skip-permissions`);
4393
3563
  }
4394
3564
  });
4395
3565
  async function resolveClaudeToken(opts) {
@@ -4416,20 +3586,20 @@ async function resolveClaudeToken(opts) {
4416
3586
  }
4417
3587
 
4418
3588
  // src/commands/agents/sync.ts
4419
- import { chownSync, existsSync as existsSync15, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync14, rmSync as rmSync5, statSync, writeFileSync as writeFileSync8 } from "fs";
4420
- import { homedir as homedir11 } from "os";
4421
- import { join as join12 } from "path";
3589
+ import { chownSync, existsSync as existsSync12, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync2, statSync, writeFileSync as writeFileSync5 } from "fs";
3590
+ import { homedir as homedir10 } from "os";
3591
+ import { join as join8 } from "path";
4422
3592
  import { defineCommand as defineCommand31 } from "citty";
4423
3593
  import consola27 from "consola";
4424
- var AUTH_PATH3 = join12(homedir11(), ".config", "apes", "auth.json");
4425
- var TASK_CACHE_DIR2 = join12(homedir11(), ".openape", "agent", "tasks");
3594
+ var AUTH_PATH3 = join8(homedir10(), ".config", "apes", "auth.json");
3595
+ var TASK_CACHE_DIR2 = join8(homedir10(), ".openape", "agent", "tasks");
4426
3596
  function readAuthJson() {
4427
- if (!existsSync15(AUTH_PATH3)) {
3597
+ if (!existsSync12(AUTH_PATH3)) {
4428
3598
  throw new CliError(
4429
3599
  `No agent auth found at ${AUTH_PATH3}. Run \`apes agents spawn <name>\` to provision an agent first.`
4430
3600
  );
4431
3601
  }
4432
- const raw = readFileSync14(AUTH_PATH3, "utf8");
3602
+ const raw = readFileSync11(AUTH_PATH3, "utf8");
4433
3603
  let parsed;
4434
3604
  try {
4435
3605
  parsed = JSON.parse(raw);
@@ -4496,7 +3666,7 @@ var syncAgentCommand = defineCommand31({
4496
3666
  let agentGid = null;
4497
3667
  if (process.geteuid?.() === 0) {
4498
3668
  try {
4499
- const homeStat = statSync(homedir11());
3669
+ const homeStat = statSync(homedir10());
4500
3670
  agentUid = homeStat.uid;
4501
3671
  agentGid = homeStat.gid;
4502
3672
  } catch {
@@ -4510,46 +3680,46 @@ var syncAgentCommand = defineCommand31({
4510
3680
  }
4511
3681
  }
4512
3682
  }
4513
- const agentDir = join12(homedir11(), ".openape", "agent");
4514
- mkdirSync4(agentDir, { recursive: true });
4515
- chownToAgent(join12(homedir11(), ".openape"));
3683
+ const agentDir = join8(homedir10(), ".openape", "agent");
3684
+ mkdirSync3(agentDir, { recursive: true });
3685
+ chownToAgent(join8(homedir10(), ".openape"));
4516
3686
  chownToAgent(agentDir);
4517
- const agentJsonPath = join12(agentDir, "agent.json");
4518
- writeFileSync8(
3687
+ const agentJsonPath = join8(agentDir, "agent.json");
3688
+ writeFileSync5(
4519
3689
  agentJsonPath,
4520
3690
  `${JSON.stringify({ systemPrompt, tools }, null, 2)}
4521
3691
  `,
4522
3692
  { mode: 384 }
4523
3693
  );
4524
3694
  chownToAgent(agentJsonPath);
4525
- mkdirSync4(TASK_CACHE_DIR2, { recursive: true });
3695
+ mkdirSync3(TASK_CACHE_DIR2, { recursive: true });
4526
3696
  chownToAgent(TASK_CACHE_DIR2);
4527
3697
  for (const task of tasks) {
4528
- const path2 = join12(TASK_CACHE_DIR2, `${task.taskId}.json`);
4529
- writeFileSync8(path2, `${JSON.stringify(task, null, 2)}
3698
+ const path2 = join8(TASK_CACHE_DIR2, `${task.taskId}.json`);
3699
+ writeFileSync5(path2, `${JSON.stringify(task, null, 2)}
4530
3700
  `, { mode: 384 });
4531
3701
  chownToAgent(path2);
4532
3702
  }
4533
- const skillsDir = join12(agentDir, "skills");
4534
- mkdirSync4(skillsDir, { recursive: true });
3703
+ const skillsDir = join8(agentDir, "skills");
3704
+ mkdirSync3(skillsDir, { recursive: true });
4535
3705
  chownToAgent(skillsDir);
4536
3706
  const incomingNames = new Set(skills.map((s) => s.name));
4537
3707
  try {
4538
3708
  for (const entry of readdirSync2(skillsDir)) {
4539
3709
  if (incomingNames.has(entry)) continue;
4540
3710
  try {
4541
- rmSync5(join12(skillsDir, entry), { recursive: true, force: true });
3711
+ rmSync2(join8(skillsDir, entry), { recursive: true, force: true });
4542
3712
  } catch {
4543
3713
  }
4544
3714
  }
4545
3715
  } catch {
4546
3716
  }
4547
3717
  for (const skill of skills) {
4548
- const skillDir = join12(skillsDir, skill.name);
4549
- mkdirSync4(skillDir, { recursive: true });
3718
+ const skillDir = join8(skillsDir, skill.name);
3719
+ mkdirSync3(skillDir, { recursive: true });
4550
3720
  chownToAgent(skillDir);
4551
- const skillPath = join12(skillDir, "SKILL.md");
4552
- writeFileSync8(skillPath, skill.body.endsWith("\n") ? skill.body : `${skill.body}
3721
+ const skillPath = join8(skillDir, "SKILL.md");
3722
+ writeFileSync5(skillPath, skill.body.endsWith("\n") ? skill.body : `${skill.body}
4553
3723
  `, { mode: 384 });
4554
3724
  chownToAgent(skillPath);
4555
3725
  }
@@ -4581,21 +3751,21 @@ var agentsCommand = defineCommand32({
4581
3751
  import { defineCommand as defineCommand40 } from "citty";
4582
3752
 
4583
3753
  // src/commands/nest/authorize.ts
4584
- import { execFileSync as execFileSync14 } from "child_process";
4585
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
4586
- import { join as join14 } from "path";
3754
+ import { execFileSync as execFileSync7 } from "child_process";
3755
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
3756
+ import { join as join10 } from "path";
4587
3757
  import { defineCommand as defineCommand34 } from "citty";
4588
3758
  import consola29 from "consola";
4589
3759
 
4590
3760
  // src/commands/nest/enroll.ts
4591
- import { hostname as hostname5, homedir as homedir12 } from "os";
4592
- import { existsSync as existsSync16, mkdirSync as mkdirSync5, writeFileSync as writeFileSync9, chmodSync } from "fs";
4593
- import { join as join13 } from "path";
3761
+ import { hostname as hostname4, homedir as homedir11 } from "os";
3762
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6, chmodSync } from "fs";
3763
+ import { join as join9 } from "path";
4594
3764
  import { defineCommand as defineCommand33 } from "citty";
4595
3765
  import consola28 from "consola";
4596
- var NEST_DATA_DIR = join13(homedir12(), ".openape", "nest");
3766
+ var NEST_DATA_DIR = join9(homedir11(), ".openape", "nest");
4597
3767
  function nestAgentName() {
4598
- const raw = hostname5().toLowerCase();
3768
+ const raw = hostname4().toLowerCase();
4599
3769
  const head = raw.split(".")[0] ?? raw;
4600
3770
  const safe = head.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
4601
3771
  const trimmed = safe.slice(0, 16);
@@ -4626,19 +3796,19 @@ var enrollNestCommand = defineCommand33({
4626
3796
  throw new CliError("Run `apes login <email>` first \u2014 nest enroll attaches the new identity to your owner account.");
4627
3797
  }
4628
3798
  const name = args.name || nestAgentName();
4629
- const authPath = join13(NEST_DATA_DIR, ".config", "apes", "auth.json");
4630
- if (existsSync16(authPath) && !args.force) {
3799
+ const authPath = join9(NEST_DATA_DIR, ".config", "apes", "auth.json");
3800
+ if (existsSync13(authPath) && !args.force) {
4631
3801
  throw new CliError(`Nest already enrolled at ${authPath}. Pass --force to re-enroll.`);
4632
3802
  }
4633
- const sshDir = join13(NEST_DATA_DIR, ".ssh");
4634
- const configDir = join13(NEST_DATA_DIR, ".config", "apes");
4635
- mkdirSync5(sshDir, { recursive: true });
4636
- mkdirSync5(configDir, { recursive: true });
3803
+ const sshDir = join9(NEST_DATA_DIR, ".ssh");
3804
+ const configDir = join9(NEST_DATA_DIR, ".config", "apes");
3805
+ mkdirSync4(sshDir, { recursive: true });
3806
+ mkdirSync4(configDir, { recursive: true });
4637
3807
  consola28.start(`Generating keypair for ${name}\u2026`);
4638
3808
  const { privatePem, publicSshLine } = generateKeyPairInMemory();
4639
- writeFileSync9(join13(sshDir, "id_ed25519"), `${privatePem.trimEnd()}
3809
+ writeFileSync6(join9(sshDir, "id_ed25519"), `${privatePem.trimEnd()}
4640
3810
  `, { mode: 384 });
4641
- writeFileSync9(join13(sshDir, "id_ed25519.pub"), `${publicSshLine}
3811
+ writeFileSync6(join9(sshDir, "id_ed25519.pub"), `${publicSshLine}
4642
3812
  `, { mode: 420 });
4643
3813
  chmodSync(sshDir, 448);
4644
3814
  consola28.start(`Registering nest at ${idp}\u2026`);
@@ -4655,10 +3825,10 @@ var enrollNestCommand = defineCommand33({
4655
3825
  accessToken: token,
4656
3826
  email: registration.email,
4657
3827
  expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
4658
- keyPath: join13(sshDir, "id_ed25519"),
3828
+ keyPath: join9(sshDir, "id_ed25519"),
4659
3829
  ownerEmail: ownerAuth.email
4660
3830
  });
4661
- writeFileSync9(authPath, authJson, { mode: 384 });
3831
+ writeFileSync6(authPath, authJson, { mode: 384 });
4662
3832
  chmodSync(configDir, 448);
4663
3833
  consola28.success(`Nest enrolled \u2014 auth.json at ${authPath}`);
4664
3834
  consola28.info("");
@@ -4727,11 +3897,11 @@ var authorizeNestCommand = defineCommand34({
4727
3897
  }
4728
3898
  },
4729
3899
  async run({ args }) {
4730
- const nestAuthPath = join14(NEST_DATA_DIR, ".config", "apes", "auth.json");
4731
- if (!existsSync17(nestAuthPath)) {
3900
+ const nestAuthPath = join10(NEST_DATA_DIR, ".config", "apes", "auth.json");
3901
+ if (!existsSync14(nestAuthPath)) {
4732
3902
  throw new CliError("Nest not enrolled. Run `apes nest enroll` first.");
4733
3903
  }
4734
- const nestAuth = JSON.parse(readFileSync15(nestAuthPath, "utf8"));
3904
+ const nestAuth = JSON.parse(readFileSync12(nestAuthPath, "utf8"));
4735
3905
  if (!nestAuth.email) throw new CliError(`${nestAuthPath} has no email`);
4736
3906
  const allow = args.allow ?? DEFAULT_ALLOW_PATTERNS.join(",");
4737
3907
  consola29.info(`Configuring YOLO-policy on ${nestAuth.email} via \`apes yolo set\`\u2026`);
@@ -4748,7 +3918,7 @@ var authorizeNestCommand = defineCommand34({
4748
3918
  cmdArgs.push("--expires-in", args["expires-in"]);
4749
3919
  }
4750
3920
  try {
4751
- execFileSync14("apes", cmdArgs, { stdio: "inherit" });
3921
+ execFileSync7("apes", cmdArgs, { stdio: "inherit" });
4752
3922
  } catch (err) {
4753
3923
  throw new CliError(err instanceof Error ? err.message : String(err));
4754
3924
  }
@@ -4758,7 +3928,7 @@ var authorizeNestCommand = defineCommand34({
4758
3928
  });
4759
3929
 
4760
3930
  // src/commands/nest/destroy.ts
4761
- import { execFileSync as execFileSync15 } from "child_process";
3931
+ import { execFileSync as execFileSync8 } from "child_process";
4762
3932
  import { defineCommand as defineCommand35 } from "citty";
4763
3933
  import consola30 from "consola";
4764
3934
  var destroyNestCommand = defineCommand35({
@@ -4772,7 +3942,7 @@ var destroyNestCommand = defineCommand35({
4772
3942
  async run({ args }) {
4773
3943
  const name = String(args.name);
4774
3944
  try {
4775
- execFileSync15("apes", ["run", "--as", "root", "--wait", "--", "apes", "agents", "destroy", name, "--force"], { stdio: "inherit" });
3945
+ execFileSync8("apes", ["run", "--as", "root", "--wait", "--", "apes", "agents", "destroy", name, "--force"], { stdio: "inherit" });
4776
3946
  consola30.success(`Nest will tear down ${name}'s pm2 process on its next reconcile (\u22642s).`);
4777
3947
  } catch (err) {
4778
3948
  const status = err.status ?? 1;
@@ -4782,9 +3952,9 @@ var destroyNestCommand = defineCommand35({
4782
3952
  });
4783
3953
 
4784
3954
  // src/commands/nest/install.ts
4785
- import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
4786
- import { homedir as homedir13 } from "os";
4787
- import { dirname as dirname3, join as join15 } from "path";
3955
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
3956
+ import { homedir as homedir12 } from "os";
3957
+ import { dirname as dirname2, join as join11 } from "path";
4788
3958
  import { defineCommand as defineCommand36 } from "citty";
4789
3959
  import consola31 from "consola";
4790
3960
 
@@ -4852,41 +4022,41 @@ resource_chain = ["agents:name={name}", "allowlist:email={peer_email}"]
4852
4022
 
4853
4023
  // src/commands/nest/install.ts
4854
4024
  function installAdapter2() {
4855
- const target = join15(homedir13(), ".openape", "shapes", "adapters", "apes-agents.toml");
4856
- mkdirSync6(dirname3(target), { recursive: true });
4025
+ const target = join11(homedir12(), ".openape", "shapes", "adapters", "apes-agents.toml");
4026
+ mkdirSync5(dirname2(target), { recursive: true });
4857
4027
  let existing = "";
4858
4028
  try {
4859
- existing = readFileSync16(target, "utf8");
4029
+ existing = readFileSync13(target, "utf8");
4860
4030
  } catch {
4861
4031
  }
4862
4032
  if (existing === APES_AGENTS_ADAPTER_TOML) return false;
4863
- writeFileSync10(target, APES_AGENTS_ADAPTER_TOML, { mode: 420 });
4033
+ writeFileSync7(target, APES_AGENTS_ADAPTER_TOML, { mode: 420 });
4864
4034
  consola31.success(`Wrote shapes adapter ${target}`);
4865
4035
  return true;
4866
4036
  }
4867
4037
  function writeBridgeModelDefault(model) {
4868
- for (const envDir of [join15(homedir13(), "litellm"), join15(NEST_DATA_DIR, "litellm")]) {
4869
- const envFile = join15(envDir, ".env");
4870
- mkdirSync6(envDir, { recursive: true });
4038
+ for (const envDir of [join11(homedir12(), "litellm"), join11(NEST_DATA_DIR, "litellm")]) {
4039
+ const envFile = join11(envDir, ".env");
4040
+ mkdirSync5(envDir, { recursive: true });
4871
4041
  let lines = [];
4872
- if (existsSync18(envFile)) {
4873
- lines = readFileSync16(envFile, "utf8").split("\n").filter((l) => !l.startsWith("APE_CHAT_BRIDGE_MODEL="));
4042
+ if (existsSync15(envFile)) {
4043
+ lines = readFileSync13(envFile, "utf8").split("\n").filter((l) => !l.startsWith("APE_CHAT_BRIDGE_MODEL="));
4874
4044
  }
4875
4045
  lines.push(`APE_CHAT_BRIDGE_MODEL=${model}`);
4876
4046
  while (lines.length > 0 && lines.at(-1).trim() === "") lines.pop();
4877
- writeFileSync10(envFile, `${lines.join("\n")}
4047
+ writeFileSync7(envFile, `${lines.join("\n")}
4878
4048
  `, { mode: 384 });
4879
4049
  }
4880
4050
  }
4881
4051
  function findBinary(name) {
4882
4052
  for (const dir of [
4883
- join15(homedir13(), ".bun", "bin"),
4053
+ join11(homedir12(), ".bun", "bin"),
4884
4054
  "/opt/homebrew/bin",
4885
4055
  "/usr/local/bin",
4886
4056
  "/usr/bin"
4887
4057
  ]) {
4888
- const p = join15(dir, name);
4889
- if (existsSync18(p)) return p;
4058
+ const p = join11(dir, name);
4059
+ if (existsSync15(p)) return p;
4890
4060
  }
4891
4061
  throw new Error(`could not locate ${name} on PATH; install it first`);
4892
4062
  }
@@ -4906,7 +4076,7 @@ var installNestCommand = defineCommand36({
4906
4076
  }
4907
4077
  },
4908
4078
  async run({ args }) {
4909
- const homeDir = homedir13();
4079
+ const homeDir = homedir12();
4910
4080
  const port = Number(args.port ?? 9091);
4911
4081
  if (!Number.isInteger(port) || port < 1024 || port > 65535) {
4912
4082
  throw new Error(`invalid port ${port}`);
@@ -4922,7 +4092,7 @@ var installNestCommand = defineCommand36({
4922
4092
  consola31.success(`Default bridge model set to ${args["bridge-model"]} (in ~/litellm/.env)`);
4923
4093
  }
4924
4094
  installAdapter2();
4925
- mkdirSync6(NEST_DATA_DIR, { recursive: true });
4095
+ mkdirSync5(NEST_DATA_DIR, { recursive: true });
4926
4096
  await getHostPlatform().installNestSupervisor({
4927
4097
  nestBin,
4928
4098
  apesBin,
@@ -4971,7 +4141,7 @@ var listNestCommand = defineCommand37({
4971
4141
  });
4972
4142
 
4973
4143
  // src/commands/nest/spawn.ts
4974
- import { execFileSync as execFileSync16 } from "child_process";
4144
+ import { execFileSync as execFileSync9 } from "child_process";
4975
4145
  import { defineCommand as defineCommand38 } from "citty";
4976
4146
  import consola33 from "consola";
4977
4147
  var spawnNestCommand = defineCommand38({
@@ -5004,7 +4174,7 @@ var spawnNestCommand = defineCommand38({
5004
4174
  if (typeof args["bridge-base-url"] === "string") apesArgs.push("--bridge-base-url", args["bridge-base-url"]);
5005
4175
  if (typeof args["bridge-model"] === "string") apesArgs.push("--bridge-model", args["bridge-model"]);
5006
4176
  try {
5007
- execFileSync16("apes", apesArgs, { stdio: "inherit" });
4177
+ execFileSync9("apes", apesArgs, { stdio: "inherit" });
5008
4178
  consola33.success(`Nest will pick up ${name} on its next reconcile (\u22642s).`);
5009
4179
  } catch (err) {
5010
4180
  const status = err.status ?? 1;
@@ -5569,14 +4739,13 @@ var adapterCommand = defineCommand45({
5569
4739
  });
5570
4740
 
5571
4741
  // src/commands/run.ts
5572
- import { execFileSync as execFileSync17 } from "child_process";
5573
- import { hostname as hostname6 } from "os";
4742
+ import { execFileSync as execFileSync10 } from "child_process";
4743
+ import { hostname as hostname5 } from "os";
5574
4744
  import { basename } from "path";
5575
4745
  import { defineCommand as defineCommand46 } from "citty";
5576
4746
  import consola39 from "consola";
5577
4747
  function resolveRunAsTarget(runAs) {
5578
4748
  if (!runAs) return runAs;
5579
- if (!isDarwin2()) return runAs;
5580
4749
  if (!AGENT_NAME_REGEX.test(runAs)) return runAs;
5581
4750
  if (runAs.startsWith("openape-agent-")) return runAs;
5582
4751
  const platform = getHostPlatform();
@@ -5754,7 +4923,7 @@ async function runShellMode(command, args) {
5754
4923
  const adapterHandled = await tryAdapterModeFromShell(command, idp, args);
5755
4924
  if (adapterHandled) return;
5756
4925
  const grantsUrl = await getGrantsEndpoint(idp);
5757
- const targetHost = args.host || hostname6();
4926
+ const targetHost = args.host || hostname5();
5758
4927
  try {
5759
4928
  const grants = await apiFetch(
5760
4929
  `${grantsUrl}?requester=${encodeURIComponent(auth.email)}&status=approved&limit=20`
@@ -5850,7 +5019,7 @@ async function tryAdapterModeFromShell(command, idp, args) {
5850
5019
  approveUrl: `${idp}/grant-approval?grant_id=${grant.id}`,
5851
5020
  command: resolved.detail?.display || parsed?.raw || "unknown",
5852
5021
  audience: resolved.adapter?.cli?.audience ?? "shapes",
5853
- host: args.host || hostname6()
5022
+ host: args.host || hostname5()
5854
5023
  });
5855
5024
  if (shouldWaitForGrant(args)) {
5856
5025
  consola39.info(`Grant requested: ${grant.id}`);
@@ -5870,7 +5039,7 @@ function execShellCommand(command) {
5870
5039
  throw new CliError("No command to execute");
5871
5040
  try {
5872
5041
  const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
5873
- execFileSync17(command[0], command.slice(1), {
5042
+ execFileSync10(command[0], command.slice(1), {
5874
5043
  stdio: "inherit",
5875
5044
  env: inheritedEnv
5876
5045
  });
@@ -5971,7 +5140,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
5971
5140
  const idp = getIdpUrl(args.idp);
5972
5141
  const grantsUrl = await getGrantsEndpoint(idp);
5973
5142
  const command = commandArgv ?? action.split(" ");
5974
- const targetHost = args.host || hostname6();
5143
+ const targetHost = args.host || hostname5();
5975
5144
  const runAs = resolveRunAsTarget(args.as ?? void 0);
5976
5145
  const reusableId = await findReusableAudienceGrant({
5977
5146
  grantsUrl,
@@ -6039,7 +5208,7 @@ function executeWithGrantToken(opts) {
6039
5208
  consola39.info(`Executing: ${command.join(" ")}`);
6040
5209
  try {
6041
5210
  const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
6042
- execFileSync17(args["escapes-path"] || "escapes", ["--grant", token, "--", ...command], {
5211
+ execFileSync10(args["escapes-path"] || "escapes", ["--grant", token, "--", ...command], {
6043
5212
  stdio: "inherit",
6044
5213
  env: inheritedEnv
6045
5214
  });
@@ -6075,9 +5244,9 @@ async function findReusableAudienceGrant(opts) {
6075
5244
 
6076
5245
  // src/commands/proxy.ts
6077
5246
  import { spawn as spawn2 } from "child_process";
6078
- import { existsSync as existsSync20 } from "fs";
6079
- import { homedir as homedir14 } from "os";
6080
- import { join as join18 } from "path";
5247
+ import { existsSync as existsSync17 } from "fs";
5248
+ import { homedir as homedir13 } from "os";
5249
+ import { join as join14 } from "path";
6081
5250
  import { defineCommand as defineCommand47 } from "citty";
6082
5251
  import consola40 from "consola";
6083
5252
 
@@ -6113,10 +5282,10 @@ note = "VPC-internal hostname suffix"
6113
5282
 
6114
5283
  // src/proxy/local-proxy.ts
6115
5284
  import { spawn } from "child_process";
6116
- import { mkdtempSync as mkdtempSync4, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
5285
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "fs";
6117
5286
  import { createRequire } from "module";
6118
- import { tmpdir as tmpdir4 } from "os";
6119
- import { dirname as dirname4, join as join16, resolve as resolve3 } from "path";
5287
+ import { tmpdir as tmpdir2 } from "os";
5288
+ import { dirname as dirname3, join as join12, resolve as resolve3 } from "path";
6120
5289
  var require2 = createRequire(import.meta.url);
6121
5290
  function findProxyBin() {
6122
5291
  const pkgPath = require2.resolve("@openape/proxy/package.json");
@@ -6125,12 +5294,12 @@ function findProxyBin() {
6125
5294
  if (!binRel) {
6126
5295
  throw new Error("@openape/proxy is missing the openape-proxy bin entry");
6127
5296
  }
6128
- return resolve3(dirname4(pkgPath), binRel);
5297
+ return resolve3(dirname3(pkgPath), binRel);
6129
5298
  }
6130
5299
  async function startEphemeralProxy(configToml) {
6131
- const tmpDir = mkdtempSync4(join16(tmpdir4(), "openape-proxy-"));
6132
- const configPath = join16(tmpDir, "config.toml");
6133
- writeFileSync11(configPath, configToml, { mode: 384 });
5300
+ const tmpDir = mkdtempSync2(join12(tmpdir2(), "openape-proxy-"));
5301
+ const configPath = join12(tmpDir, "config.toml");
5302
+ writeFileSync8(configPath, configToml, { mode: 384 });
6134
5303
  const binPath = findProxyBin();
6135
5304
  const child = spawn(process.execPath, [binPath, "-c", configPath], {
6136
5305
  stdio: ["ignore", "pipe", "pipe"],
@@ -6138,7 +5307,7 @@ async function startEphemeralProxy(configToml) {
6138
5307
  });
6139
5308
  const cleanupTmp = () => {
6140
5309
  try {
6141
- rmSync6(tmpDir, { recursive: true, force: true });
5310
+ rmSync3(tmpDir, { recursive: true, force: true });
6142
5311
  } catch {
6143
5312
  }
6144
5313
  };
@@ -6212,9 +5381,9 @@ function waitForListenLine(child) {
6212
5381
  }
6213
5382
 
6214
5383
  // src/proxy/trust-bundle.ts
6215
- import { existsSync as existsSync19, mkdtempSync as mkdtempSync5, readFileSync as readFileSync17, rmdirSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync12 } from "fs";
6216
- import { tmpdir as tmpdir5 } from "os";
6217
- import { join as join17 } from "path";
5384
+ import { existsSync as existsSync16, mkdtempSync as mkdtempSync3, readFileSync as readFileSync14, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "fs";
5385
+ import { tmpdir as tmpdir3 } from "os";
5386
+ import { join as join13 } from "path";
6218
5387
  var CANDIDATES = [
6219
5388
  "/etc/ssl/cert.pem",
6220
5389
  // macOS
@@ -6227,25 +5396,25 @@ var CANDIDATES = [
6227
5396
  ];
6228
5397
  function detectSystemCaPath() {
6229
5398
  for (const p of CANDIDATES) {
6230
- if (existsSync19(p)) return p;
5399
+ if (existsSync16(p)) return p;
6231
5400
  }
6232
5401
  throw new Error(
6233
5402
  `Could not locate a system CA bundle. Tried: ${CANDIDATES.join(", ")}. Set NODE_EXTRA_CA_CERTS yourself or pass --allow-no-system-ca.`
6234
5403
  );
6235
5404
  }
6236
5405
  function buildTrustBundle(opts) {
6237
- const dir = mkdtempSync5(join17(tmpdir5(), "openape-trust-"));
6238
- const path2 = join17(dir, "bundle.pem");
6239
- const sys = readFileSync17(opts.systemCaPath, "utf-8");
6240
- const local = readFileSync17(opts.localCaPath, "utf-8");
6241
- writeFileSync12(path2, `${sys.trimEnd()}
5406
+ const dir = mkdtempSync3(join13(tmpdir3(), "openape-trust-"));
5407
+ const path2 = join13(dir, "bundle.pem");
5408
+ const sys = readFileSync14(opts.systemCaPath, "utf-8");
5409
+ const local = readFileSync14(opts.localCaPath, "utf-8");
5410
+ writeFileSync9(path2, `${sys.trimEnd()}
6242
5411
  ${local.trimEnd()}
6243
5412
  `, { mode: 384 });
6244
5413
  return {
6245
5414
  path: path2,
6246
5415
  cleanup: () => {
6247
5416
  try {
6248
- unlinkSync3(path2);
5417
+ unlinkSync2(path2);
6249
5418
  } catch {
6250
5419
  }
6251
5420
  try {
@@ -6295,8 +5464,8 @@ var proxyCommand = defineCommand47({
6295
5464
  if (reuseHostPort) {
6296
5465
  proxyUrl = `http://${reuseHostPort}`;
6297
5466
  consola40.info(`[apes proxy] using long-running daemon at ${proxyUrl}`);
6298
- const localCaPath = join18(homedir14(), ".openape", "proxy", "ca.crt");
6299
- if (!existsSync20(localCaPath)) {
5467
+ const localCaPath = join14(homedir13(), ".openape", "proxy", "ca.crt");
5468
+ if (!existsSync17(localCaPath)) {
6300
5469
  throw new CliError(
6301
5470
  `OPENAPE_PROXY is set but no local CA found at ${localCaPath}. Start the daemon (sudo -u <agent> apes proxy --global < secrets.toml) first.`
6302
5471
  );
@@ -6638,16 +5807,16 @@ var mcpCommand = defineCommand52({
6638
5807
  if (transport !== "stdio" && transport !== "sse") {
6639
5808
  throw new Error('Transport must be "stdio" or "sse"');
6640
5809
  }
6641
- const { startMcpServer } = await import("./server-QN35XDYH.js");
5810
+ const { startMcpServer } = await import("./server-X4HHOCKV.js");
6642
5811
  await startMcpServer(transport, port);
6643
5812
  }
6644
5813
  });
6645
5814
 
6646
5815
  // src/commands/init/index.ts
6647
- import { existsSync as existsSync21, copyFileSync, writeFileSync as writeFileSync13 } from "fs";
5816
+ import { existsSync as existsSync18, copyFileSync, writeFileSync as writeFileSync10 } from "fs";
6648
5817
  import { randomBytes } from "crypto";
6649
- import { execFileSync as execFileSync18 } from "child_process";
6650
- import { join as join19 } from "path";
5818
+ import { execFileSync as execFileSync11 } from "child_process";
5819
+ import { join as join15 } from "path";
6651
5820
  import { defineCommand as defineCommand53 } from "citty";
6652
5821
  import consola43 from "consola";
6653
5822
  var DEFAULT_IDP_URL = "https://id.openape.at";
@@ -6656,13 +5825,13 @@ async function downloadTemplate(repo, targetDir) {
6656
5825
  await gigetDownload(`gh:${repo}`, { dir: targetDir, force: false });
6657
5826
  }
6658
5827
  function installDeps(dir) {
6659
- const hasLockFile = (name) => existsSync21(join19(dir, name));
5828
+ const hasLockFile = (name) => existsSync18(join15(dir, name));
6660
5829
  if (hasLockFile("pnpm-lock.yaml")) {
6661
- execFileSync18("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
5830
+ execFileSync11("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
6662
5831
  } else if (hasLockFile("bun.lockb")) {
6663
- execFileSync18("bun", ["install"], { cwd: dir, stdio: "inherit" });
5832
+ execFileSync11("bun", ["install"], { cwd: dir, stdio: "inherit" });
6664
5833
  } else {
6665
- execFileSync18("npm", ["install"], { cwd: dir, stdio: "inherit" });
5834
+ execFileSync11("npm", ["install"], { cwd: dir, stdio: "inherit" });
6666
5835
  }
6667
5836
  }
6668
5837
  async function promptChoice(message, choices) {
@@ -6721,7 +5890,7 @@ var initCommand = defineCommand53({
6721
5890
  });
6722
5891
  async function initSP(targetDir) {
6723
5892
  const dir = targetDir || "my-app";
6724
- if (existsSync21(join19(dir, "package.json"))) {
5893
+ if (existsSync18(join15(dir, "package.json"))) {
6725
5894
  throw new CliError(`Directory "${dir}" already contains a project.`);
6726
5895
  }
6727
5896
  consola43.start("Scaffolding SP starter...");
@@ -6730,9 +5899,9 @@ async function initSP(targetDir) {
6730
5899
  consola43.start("Installing dependencies...");
6731
5900
  installDeps(dir);
6732
5901
  consola43.success("Dependencies installed");
6733
- const envExample = join19(dir, ".env.example");
6734
- const envFile = join19(dir, ".env");
6735
- if (existsSync21(envExample) && !existsSync21(envFile)) {
5902
+ const envExample = join15(dir, ".env.example");
5903
+ const envFile = join15(dir, ".env");
5904
+ if (existsSync18(envExample) && !existsSync18(envFile)) {
6736
5905
  copyFileSync(envExample, envFile);
6737
5906
  consola43.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
6738
5907
  }
@@ -6746,7 +5915,7 @@ async function initSP(targetDir) {
6746
5915
  }
6747
5916
  async function initIdP(targetDir) {
6748
5917
  const dir = targetDir || "my-idp";
6749
- if (existsSync21(join19(dir, "package.json"))) {
5918
+ if (existsSync18(join15(dir, "package.json"))) {
6750
5919
  throw new CliError(`Directory "${dir}" already contains a project.`);
6751
5920
  }
6752
5921
  const domain = await promptText("Domain for the IdP", "localhost");
@@ -6778,7 +5947,7 @@ async function initIdP(targetDir) {
6778
5947
  `NUXT_OPENAPE_RP_ID=${domain}`,
6779
5948
  `NUXT_OPENAPE_RP_ORIGIN=${origin}`
6780
5949
  ].join("\n");
6781
- writeFileSync13(join19(dir, ".env"), `${envContent}
5950
+ writeFileSync10(join15(dir, ".env"), `${envContent}
6782
5951
  `, { mode: 384 });
6783
5952
  consola43.success(".env created");
6784
5953
  console.log("");
@@ -6799,7 +5968,7 @@ async function initIdP(targetDir) {
6799
5968
 
6800
5969
  // src/commands/enroll.ts
6801
5970
  import { Buffer as Buffer5 } from "buffer";
6802
- import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
5971
+ import { existsSync as existsSync19, readFileSync as readFileSync15 } from "fs";
6803
5972
  import { execFile as execFile2 } from "child_process";
6804
5973
  import { sign as sign2 } from "crypto";
6805
5974
  import { defineCommand as defineCommand54 } from "citty";
@@ -6815,7 +5984,7 @@ function openBrowser2(url) {
6815
5984
  }
6816
5985
  async function pollForEnrollment(idp, agentEmail, keyPath) {
6817
5986
  const resolvedKey = resolveKeyPath(keyPath);
6818
- const keyContent = readFileSync18(resolvedKey, "utf-8");
5987
+ const keyContent = readFileSync15(resolvedKey, "utf-8");
6819
5988
  const privateKey = loadEd25519PrivateKey(keyContent);
6820
5989
  const challengeUrl = await getAgentChallengeEndpoint(idp);
6821
5990
  const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
@@ -6883,7 +6052,7 @@ var enrollCommand = defineCommand54({
6883
6052
  }) || DEFAULT_KEY_PATH;
6884
6053
  const resolvedKey = resolveKeyPath(keyPath);
6885
6054
  let publicKey;
6886
- if (existsSync22(resolvedKey)) {
6055
+ if (existsSync19(resolvedKey)) {
6887
6056
  publicKey = readPublicKey(resolvedKey);
6888
6057
  consola44.success(`Using existing key ${keyPath}`);
6889
6058
  } else {
@@ -6927,7 +6096,7 @@ var enrollCommand = defineCommand54({
6927
6096
  });
6928
6097
 
6929
6098
  // src/commands/register-user.ts
6930
- import { existsSync as existsSync23, readFileSync as readFileSync19 } from "fs";
6099
+ import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
6931
6100
  import { defineCommand as defineCommand55 } from "citty";
6932
6101
  import consola45 from "consola";
6933
6102
  var registerUserCommand = defineCommand55({
@@ -6966,8 +6135,8 @@ var registerUserCommand = defineCommand55({
6966
6135
  throw new CliError("No IdP URL configured. Run `apes login` first.");
6967
6136
  }
6968
6137
  let publicKey = args.key;
6969
- if (existsSync23(args.key)) {
6970
- publicKey = readFileSync19(args.key, "utf-8").trim();
6138
+ if (existsSync20(args.key)) {
6139
+ publicKey = readFileSync16(args.key, "utf-8").trim();
6971
6140
  }
6972
6141
  if (!publicKey.startsWith("ssh-ed25519 ")) {
6973
6142
  throw new CliError("Public key must be in ssh-ed25519 format.");
@@ -7276,7 +6445,7 @@ async function bestEffortGrantCount(idp) {
7276
6445
  }
7277
6446
  }
7278
6447
  async function runHealth(args) {
7279
- const version = true ? "1.29.0" : "0.0.0";
6448
+ const version = true ? "1.30.0" : "0.0.0";
7280
6449
  const auth = loadAuth();
7281
6450
  if (!auth) {
7282
6451
  throw new CliError("Not logged in. Run `apes login` first.", 1);
@@ -7469,26 +6638,26 @@ var workflowsCommand = defineCommand63({
7469
6638
  });
7470
6639
 
7471
6640
  // src/version-check.ts
7472
- import { existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
7473
- import { homedir as homedir15 } from "os";
7474
- import { join as join20 } from "path";
6641
+ import { existsSync as existsSync21, mkdirSync as mkdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
6642
+ import { homedir as homedir14 } from "os";
6643
+ import { join as join16 } from "path";
7475
6644
  import consola51 from "consola";
7476
6645
  var PACKAGE_NAME = "@openape/apes";
7477
6646
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
7478
- var CACHE_FILE = join20(homedir15(), ".config", "apes", ".version-check.json");
6647
+ var CACHE_FILE = join16(homedir14(), ".config", "apes", ".version-check.json");
7479
6648
  function readCache() {
7480
- if (!existsSync24(CACHE_FILE)) return null;
6649
+ if (!existsSync21(CACHE_FILE)) return null;
7481
6650
  try {
7482
- return JSON.parse(readFileSync20(CACHE_FILE, "utf-8"));
6651
+ return JSON.parse(readFileSync17(CACHE_FILE, "utf-8"));
7483
6652
  } catch {
7484
6653
  return null;
7485
6654
  }
7486
6655
  }
7487
6656
  function writeCache(entry) {
7488
6657
  try {
7489
- const dir = join20(homedir15(), ".config", "apes");
7490
- if (!existsSync24(dir)) mkdirSync7(dir, { recursive: true, mode: 448 });
7491
- writeFileSync14(CACHE_FILE, JSON.stringify(entry), { mode: 384 });
6658
+ const dir = join16(homedir14(), ".config", "apes");
6659
+ if (!existsSync21(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
6660
+ writeFileSync11(CACHE_FILE, JSON.stringify(entry), { mode: 384 });
7492
6661
  } catch {
7493
6662
  }
7494
6663
  }
@@ -7549,10 +6718,10 @@ if (shellRewrite) {
7549
6718
  if (shellRewrite.action === "rewrite") {
7550
6719
  process.argv = shellRewrite.argv;
7551
6720
  } else if (shellRewrite.action === "version") {
7552
- console.log(`ape-shell ${"1.29.0"} (OpenApe DDISA shell wrapper)`);
6721
+ console.log(`ape-shell ${"1.30.0"} (OpenApe DDISA shell wrapper)`);
7553
6722
  process.exit(0);
7554
6723
  } else if (shellRewrite.action === "help") {
7555
- console.log(`ape-shell ${"1.29.0"} \u2014 OpenApe DDISA shell wrapper`);
6724
+ console.log(`ape-shell ${"1.30.0"} \u2014 OpenApe DDISA shell wrapper`);
7556
6725
  console.log("");
7557
6726
  console.log("Usage:");
7558
6727
  console.log(" ape-shell Start interactive grant-mediated REPL");
@@ -7610,7 +6779,7 @@ var configCommand = defineCommand64({
7610
6779
  var main = defineCommand64({
7611
6780
  meta: {
7612
6781
  name: "apes",
7613
- version: "1.29.0",
6782
+ version: "1.30.0",
7614
6783
  description: "Unified CLI for OpenApe"
7615
6784
  },
7616
6785
  subCommands: {
@@ -7668,7 +6837,7 @@ async function maybeRefreshAuth() {
7668
6837
  }
7669
6838
  }
7670
6839
  await maybeRefreshAuth();
7671
- await maybeWarnStaleVersion("1.29.0").catch(() => {
6840
+ await maybeWarnStaleVersion("1.30.0").catch(() => {
7672
6841
  });
7673
6842
  runMain(main).catch((err) => {
7674
6843
  if (err instanceof CliExit) {