@openape/apes 1.29.1 → 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:
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
#
|
|
2127
|
-
#
|
|
2128
|
-
|
|
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
|
-
#
|
|
2138
|
-
#
|
|
2139
|
-
#
|
|
2140
|
-
#
|
|
2141
|
-
#
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
#
|
|
2146
|
-
#
|
|
2147
|
-
|
|
2148
|
-
if
|
|
2149
|
-
|
|
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
|
-
#
|
|
2164
|
-
|
|
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
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
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(
|
|
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}
|
|
2214
|
-
|
|
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 (
|
|
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/
|
|
2405
|
-
import { execFileSync as
|
|
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 =
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
2652
|
-
import { existsSync as
|
|
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 (!
|
|
2252
|
+
if (!existsSync3(path2)) continue;
|
|
2657
2253
|
try {
|
|
2658
|
-
const v =
|
|
2254
|
+
const v = readFileSync2(path2, "utf-8").trim();
|
|
2659
2255
|
if (v) return v;
|
|
2660
2256
|
} catch {
|
|
2661
2257
|
}
|
|
2662
2258
|
}
|
|
2663
|
-
return
|
|
2259
|
+
return hostname3();
|
|
2664
2260
|
}
|
|
2665
2261
|
function getLinuxHostname() {
|
|
2666
|
-
return
|
|
2262
|
+
return hostname3();
|
|
2667
2263
|
}
|
|
2668
2264
|
|
|
2669
2265
|
// src/lib/host-platform/linux-user.ts
|
|
2670
|
-
import { execFileSync as
|
|
2266
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2671
2267
|
function getentPasswd(name) {
|
|
2672
2268
|
try {
|
|
2673
|
-
return
|
|
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 =
|
|
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
|
|
2718
|
-
import { mkdtempSync
|
|
2719
|
-
import { tmpdir
|
|
2720
|
-
import { join as
|
|
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 =
|
|
2723
|
-
const scriptPath =
|
|
2724
|
-
|
|
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
|
-
|
|
2323
|
+
execFileSync4("bash", [scriptPath], { stdio: "inherit" });
|
|
2728
2324
|
} else {
|
|
2729
|
-
|
|
2325
|
+
execFileSync4("sudo", ["-n", "--", "bash", scriptPath], { stdio: "inherit" });
|
|
2730
2326
|
}
|
|
2731
2327
|
} finally {
|
|
2732
2328
|
try {
|
|
2733
|
-
|
|
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 =
|
|
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
|
|
2749
|
-
import { existsSync as
|
|
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 =
|
|
2375
|
+
existing = readFileSync3(UNIT_PATH, "utf8");
|
|
2780
2376
|
} catch {
|
|
2781
2377
|
}
|
|
2782
2378
|
if (existing !== desired) {
|
|
2783
|
-
|
|
2379
|
+
writeFileSync2(UNIT_PATH, desired, { mode: 420 });
|
|
2784
2380
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
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
|
-
|
|
2386
|
+
execFileSync5("systemctl", ["disable", "--now", UNIT_NAME], { stdio: "inherit" });
|
|
2791
2387
|
} catch {
|
|
2792
2388
|
}
|
|
2793
|
-
if (
|
|
2389
|
+
if (existsSync4(UNIT_PATH)) unlinkSync(UNIT_PATH);
|
|
2794
2390
|
try {
|
|
2795
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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(
|
|
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
|
|
2515
|
+
consola20.success("No agent tombstones \u2014 userdel is clean on Linux.");
|
|
2931
2516
|
return;
|
|
2932
2517
|
}
|
|
2933
|
-
consola20.
|
|
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
|
-
|
|
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
|
|
2527
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
2984
2528
|
import { homedir as homedir5 } from "os";
|
|
2985
|
-
import { join as
|
|
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
|
|
2536
|
+
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync4, watch } from "fs";
|
|
2993
2537
|
import { homedir as homedir4 } from "os";
|
|
2994
|
-
import { join as
|
|
2538
|
+
import { join as join3 } from "path";
|
|
2995
2539
|
import { openString } from "@openape/core";
|
|
2996
|
-
var CONFIG_DIR2 =
|
|
2997
|
-
var SECRETS_DIR =
|
|
2998
|
-
var X25519_KEY_PATH =
|
|
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 (!
|
|
3007
|
-
const k =
|
|
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 (!
|
|
3012
|
-
const k =
|
|
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 &&
|
|
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(
|
|
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 (!
|
|
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 =
|
|
3083
|
-
if (
|
|
3084
|
-
for (const raw of
|
|
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 &&
|
|
3101
|
-
const agentJson =
|
|
3102
|
-
if (
|
|
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(
|
|
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
|
|
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
|
|
2739
|
+
import { join as join5 } from "path";
|
|
3200
2740
|
function resolveRegistryPath() {
|
|
3201
|
-
if (
|
|
3202
|
-
if (
|
|
3203
|
-
|
|
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 (!
|
|
2751
|
+
if (!existsSync7(path2)) return emptyRegistry();
|
|
3211
2752
|
try {
|
|
3212
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
2764
|
+
mkdirSync(dir, { recursive: true });
|
|
3224
2765
|
} catch {
|
|
3225
2766
|
}
|
|
3226
|
-
|
|
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
|
|
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 =
|
|
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 ?? `/
|
|
3368
|
-
consequences.push(`\u2022 Remove
|
|
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
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
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 =
|
|
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 ??
|
|
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
|
|
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 (!
|
|
3011
|
+
if (!existsSync8(keyFile)) {
|
|
3602
3012
|
throw new CliError(`Public-key file not found: ${keyFile}`);
|
|
3603
3013
|
}
|
|
3604
|
-
publicKey =
|
|
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
|
|
3049
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
3640
3050
|
import { homedir as homedir7 } from "os";
|
|
3641
|
-
import { join as
|
|
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 =
|
|
3646
|
-
var TASK_CACHE_DIR =
|
|
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 (!
|
|
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(
|
|
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 =
|
|
3688
|
-
if (!
|
|
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(
|
|
3101
|
+
return JSON.parse(readFileSync8(path2, "utf8"));
|
|
3692
3102
|
}
|
|
3693
|
-
var AGENT_CONFIG_PATH =
|
|
3103
|
+
var AGENT_CONFIG_PATH = join6(homedir7(), ".openape", "agent", "agent.json");
|
|
3694
3104
|
function readAgentConfig() {
|
|
3695
|
-
if (!
|
|
3105
|
+
if (!existsSync9(AGENT_CONFIG_PATH)) return { systemPrompt: "" };
|
|
3696
3106
|
try {
|
|
3697
|
-
return JSON.parse(
|
|
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 =
|
|
3113
|
+
const envPath = join6(homedir7(), "litellm", ".env");
|
|
3704
3114
|
const env = {};
|
|
3705
|
-
if (
|
|
3706
|
-
for (const line of
|
|
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
|
|
3222
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
3813
3223
|
import { homedir as homedir8 } from "os";
|
|
3814
|
-
import { join as
|
|
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 =
|
|
3228
|
+
var AUTH_PATH2 = join7(homedir8(), ".config", "apes", "auth.json");
|
|
3819
3229
|
function readLitellmConfig3(model) {
|
|
3820
|
-
const envPath =
|
|
3230
|
+
const envPath = join7(homedir8(), "litellm", ".env");
|
|
3821
3231
|
const env = {};
|
|
3822
|
-
if (
|
|
3823
|
-
for (const line of
|
|
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 (
|
|
3265
|
+
if (existsSync10(AUTH_PATH2)) {
|
|
3856
3266
|
try {
|
|
3857
|
-
JSON.parse(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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 (
|
|
4014
|
-
return
|
|
3371
|
+
if (existsSync11(pubPath)) {
|
|
3372
|
+
return readFileSync10(pubPath, "utf-8").trim();
|
|
4015
3373
|
}
|
|
4016
|
-
const keyContent =
|
|
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 (!
|
|
4026
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
4282
|
-
const existing = platform.readAgentUser(
|
|
3480
|
+
const osUsername = platform.agentUsername(name);
|
|
3481
|
+
const existing = platform.readAgentUser(osUsername);
|
|
4283
3482
|
if (existing) {
|
|
4284
|
-
throw new CliError(`
|
|
3483
|
+
throw new CliError(`OS user "${existing.name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
|
|
4285
3484
|
}
|
|
4286
|
-
const homeDir = `/var/openape/homes/${
|
|
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
|
-
|
|
4289
|
-
|
|
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
|
-
|
|
4347
|
-
homeDir,
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
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
|
-
|
|
4361
|
-
|
|
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
|
|
4420
|
-
import { homedir as
|
|
4421
|
-
import { join as
|
|
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 =
|
|
4425
|
-
var TASK_CACHE_DIR2 =
|
|
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 (!
|
|
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 =
|
|
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(
|
|
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 =
|
|
4514
|
-
|
|
4515
|
-
chownToAgent(
|
|
3683
|
+
const agentDir = join8(homedir10(), ".openape", "agent");
|
|
3684
|
+
mkdirSync3(agentDir, { recursive: true });
|
|
3685
|
+
chownToAgent(join8(homedir10(), ".openape"));
|
|
4516
3686
|
chownToAgent(agentDir);
|
|
4517
|
-
const agentJsonPath =
|
|
4518
|
-
|
|
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
|
-
|
|
3695
|
+
mkdirSync3(TASK_CACHE_DIR2, { recursive: true });
|
|
4526
3696
|
chownToAgent(TASK_CACHE_DIR2);
|
|
4527
3697
|
for (const task of tasks) {
|
|
4528
|
-
const path2 =
|
|
4529
|
-
|
|
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 =
|
|
4534
|
-
|
|
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
|
-
|
|
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 =
|
|
4549
|
-
|
|
3718
|
+
const skillDir = join8(skillsDir, skill.name);
|
|
3719
|
+
mkdirSync3(skillDir, { recursive: true });
|
|
4550
3720
|
chownToAgent(skillDir);
|
|
4551
|
-
const skillPath =
|
|
4552
|
-
|
|
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
|
|
4585
|
-
import { existsSync as
|
|
4586
|
-
import { join as
|
|
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
|
|
4592
|
-
import { existsSync as
|
|
4593
|
-
import { join as
|
|
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 =
|
|
3766
|
+
var NEST_DATA_DIR = join9(homedir11(), ".openape", "nest");
|
|
4597
3767
|
function nestAgentName() {
|
|
4598
|
-
const raw =
|
|
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 =
|
|
4630
|
-
if (
|
|
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 =
|
|
4634
|
-
const configDir =
|
|
4635
|
-
|
|
4636
|
-
|
|
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
|
-
|
|
3809
|
+
writeFileSync6(join9(sshDir, "id_ed25519"), `${privatePem.trimEnd()}
|
|
4640
3810
|
`, { mode: 384 });
|
|
4641
|
-
|
|
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:
|
|
3828
|
+
keyPath: join9(sshDir, "id_ed25519"),
|
|
4659
3829
|
ownerEmail: ownerAuth.email
|
|
4660
3830
|
});
|
|
4661
|
-
|
|
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 =
|
|
4731
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
4786
|
-
import { homedir as
|
|
4787
|
-
import { dirname as
|
|
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 =
|
|
4856
|
-
|
|
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 =
|
|
4029
|
+
existing = readFileSync13(target, "utf8");
|
|
4860
4030
|
} catch {
|
|
4861
4031
|
}
|
|
4862
4032
|
if (existing === APES_AGENTS_ADAPTER_TOML) return false;
|
|
4863
|
-
|
|
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 [
|
|
4869
|
-
const envFile =
|
|
4870
|
-
|
|
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 (
|
|
4873
|
-
lines =
|
|
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
|
-
|
|
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
|
-
|
|
4053
|
+
join11(homedir12(), ".bun", "bin"),
|
|
4884
4054
|
"/opt/homebrew/bin",
|
|
4885
4055
|
"/usr/local/bin",
|
|
4886
4056
|
"/usr/bin"
|
|
4887
4057
|
]) {
|
|
4888
|
-
const p =
|
|
4889
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
5573
|
-
import { hostname as
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
6079
|
-
import { homedir as
|
|
6080
|
-
import { join as
|
|
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
|
|
5285
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "fs";
|
|
6117
5286
|
import { createRequire } from "module";
|
|
6118
|
-
import { tmpdir as
|
|
6119
|
-
import { dirname as
|
|
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(
|
|
5297
|
+
return resolve3(dirname3(pkgPath), binRel);
|
|
6129
5298
|
}
|
|
6130
5299
|
async function startEphemeralProxy(configToml) {
|
|
6131
|
-
const tmpDir =
|
|
6132
|
-
const configPath =
|
|
6133
|
-
|
|
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
|
-
|
|
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
|
|
6216
|
-
import { tmpdir as
|
|
6217
|
-
import { join as
|
|
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 (
|
|
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 =
|
|
6238
|
-
const path2 =
|
|
6239
|
-
const sys =
|
|
6240
|
-
const local =
|
|
6241
|
-
|
|
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
|
-
|
|
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 =
|
|
6299
|
-
if (!
|
|
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-
|
|
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
|
|
5816
|
+
import { existsSync as existsSync18, copyFileSync, writeFileSync as writeFileSync10 } from "fs";
|
|
6648
5817
|
import { randomBytes } from "crypto";
|
|
6649
|
-
import { execFileSync as
|
|
6650
|
-
import { join as
|
|
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) =>
|
|
5828
|
+
const hasLockFile = (name) => existsSync18(join15(dir, name));
|
|
6660
5829
|
if (hasLockFile("pnpm-lock.yaml")) {
|
|
6661
|
-
|
|
5830
|
+
execFileSync11("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
|
|
6662
5831
|
} else if (hasLockFile("bun.lockb")) {
|
|
6663
|
-
|
|
5832
|
+
execFileSync11("bun", ["install"], { cwd: dir, stdio: "inherit" });
|
|
6664
5833
|
} else {
|
|
6665
|
-
|
|
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 (
|
|
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 =
|
|
6734
|
-
const envFile =
|
|
6735
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
|
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 (
|
|
6970
|
-
publicKey =
|
|
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.
|
|
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
|
|
7473
|
-
import { homedir as
|
|
7474
|
-
import { join as
|
|
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 =
|
|
6647
|
+
var CACHE_FILE = join16(homedir14(), ".config", "apes", ".version-check.json");
|
|
7479
6648
|
function readCache() {
|
|
7480
|
-
if (!
|
|
6649
|
+
if (!existsSync21(CACHE_FILE)) return null;
|
|
7481
6650
|
try {
|
|
7482
|
-
return JSON.parse(
|
|
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 =
|
|
7490
|
-
if (!
|
|
7491
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
6840
|
+
await maybeWarnStaleVersion("1.30.0").catch(() => {
|
|
7672
6841
|
});
|
|
7673
6842
|
runMain(main).catch((err) => {
|
|
7674
6843
|
if (err instanceof CliExit) {
|