@openape/apes 1.29.1 → 1.31.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/{chunk-NYJSBFLG.js → chunk-MMBFV5WN.js} +14 -6
- package/dist/{chunk-NYJSBFLG.js.map → chunk-MMBFV5WN.js.map} +1 -1
- package/dist/cli.js +672 -1455
- package/dist/cli.js.map +1 -1
- package/dist/{http-UPOTOYQV.js → http-SILH37L7.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{orchestrator-P7QFDBBK.js → orchestrator-6PZXCE54.js} +2 -2
- package/dist/{server-FB7FC2NT.js → server-MDXOGP2U.js} +3 -3
- package/package.json +6 -6
- /package/dist/{http-UPOTOYQV.js.map → http-SILH37L7.js.map} +0 -0
- /package/dist/{orchestrator-P7QFDBBK.js.map → orchestrator-6PZXCE54.js.map} +0 -0
- /package/dist/{server-FB7FC2NT.js.map → server-MDXOGP2U.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
getAgentChallengeEndpoint,
|
|
51
51
|
getDelegationsEndpoint,
|
|
52
52
|
getGrantsEndpoint
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-MMBFV5WN.js";
|
|
54
54
|
import {
|
|
55
55
|
AUTH_FILE,
|
|
56
56
|
CONFIG_DIR,
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
} from "./chunk-OBF7IMQ2.js";
|
|
66
66
|
|
|
67
67
|
// src/cli.ts
|
|
68
|
-
import
|
|
68
|
+
import consola53 from "consola";
|
|
69
69
|
|
|
70
70
|
// src/ape-shell.ts
|
|
71
71
|
import path from "path";
|
|
@@ -98,7 +98,7 @@ function rewriteApeShellArgs(argv, argv0) {
|
|
|
98
98
|
import { defineCommand as defineCommand64, runMain } from "citty";
|
|
99
99
|
|
|
100
100
|
// src/commands/auth/login.ts
|
|
101
|
-
import { Buffer
|
|
101
|
+
import { Buffer } from "buffer";
|
|
102
102
|
import { execFile } from "child_process";
|
|
103
103
|
import { createServer } from "http";
|
|
104
104
|
import { homedir as homedir2 } from "os";
|
|
@@ -308,7 +308,7 @@ async function loginWithPKCE(idp) {
|
|
|
308
308
|
authUrl.searchParams.set("state", state);
|
|
309
309
|
authUrl.searchParams.set("nonce", nonce);
|
|
310
310
|
authUrl.searchParams.set("scope", "openid email profile offline_access");
|
|
311
|
-
const code = await new Promise((
|
|
311
|
+
const code = await new Promise((resolve5, reject) => {
|
|
312
312
|
const server = createServer((req, res) => {
|
|
313
313
|
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
314
314
|
if (url.pathname === "/callback") {
|
|
@@ -325,7 +325,7 @@ async function loginWithPKCE(idp) {
|
|
|
325
325
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
326
326
|
res.end("<h1>Login successful!</h1><p>You can close this window.</p>");
|
|
327
327
|
server.close();
|
|
328
|
-
|
|
328
|
+
resolve5(authCode);
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
331
|
res.writeHead(400);
|
|
@@ -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: readFileSync19 } = 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,9 +395,9 @@ 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 = readFileSync19(keyPath, "utf-8");
|
|
399
399
|
const privateKey = loadEd25519PrivateKey2(keyContent);
|
|
400
|
-
const signature = sign3(null,
|
|
400
|
+
const signature = sign3(null, Buffer.from(challenge), privateKey).toString("base64");
|
|
401
401
|
const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
|
|
402
402
|
const authResp = await fetch(authenticateUrl, {
|
|
403
403
|
method: "POST",
|
|
@@ -888,7 +888,7 @@ async function waitForApproval2(grantsUrl, grantId) {
|
|
|
888
888
|
if (grant.status === "revoked") {
|
|
889
889
|
throw new CliError("Grant revoked.");
|
|
890
890
|
}
|
|
891
|
-
await new Promise((
|
|
891
|
+
await new Promise((resolve5) => setTimeout(resolve5, interval));
|
|
892
892
|
}
|
|
893
893
|
throw new CliError("Timed out waiting for approval.");
|
|
894
894
|
}
|
|
@@ -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
|
-
import { Buffer as
|
|
1985
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
1984
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
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 ";
|
|
@@ -2049,7 +2047,7 @@ function decodeJwtClaims(token) {
|
|
|
2049
2047
|
const part = token.split(".")[1];
|
|
2050
2048
|
if (!part) return null;
|
|
2051
2049
|
const padded = part + "=".repeat((4 - part.length % 4) % 4);
|
|
2052
|
-
const json =
|
|
2050
|
+
const json = Buffer2.from(padded, "base64").toString("utf8");
|
|
2053
2051
|
return JSON.parse(json);
|
|
2054
2052
|
} catch {
|
|
2055
2053
|
return null;
|
|
@@ -2061,19 +2059,23 @@ 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(() => "");
|
|
2068
2069
|
throw new Error(`Challenge failed (${challengeResp.status}): ${text}`);
|
|
2069
2070
|
}
|
|
2070
2071
|
const { challenge } = await challengeResp.json();
|
|
2071
|
-
const signature = sign(null,
|
|
2072
|
+
const signature = sign(null, Buffer2.from(challenge), privateKey).toString("base64");
|
|
2072
2073
|
const authenticateUrl = await getAgentAuthenticateEndpoint(input.idp);
|
|
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 ? `
|
|
@@ -2100,118 +2102,54 @@ mkdir -p "$HOME_DIR/.claude/hooks"
|
|
|
2100
2102
|
cat > "$HOME_DIR/.claude/settings.json" ${shHeredoc(input.claudeSettingsJson)}
|
|
2101
2103
|
cat > "$HOME_DIR/.claude/hooks/bash-via-ape-shell.sh" ${shHeredoc(input.hookScriptSource)}
|
|
2102
2104
|
chmod 755 "$HOME_DIR/.claude/hooks/bash-via-ape-shell.sh"
|
|
2103
|
-
` : "";
|
|
2104
|
-
const claudeTokenBlock = input.claudeOauthToken ? `
|
|
2105
|
-
mkdir -p "$HOME_DIR/.config/openape"
|
|
2106
|
-
cat > "$HOME_DIR/.config/openape/claude-token.env" ${shHeredoc(`# Auto-generated by 'apes agents spawn'. chmod 600 \u2014 contains a long-lived
|
|
2107
|
-
# Claude Code OAuth token. Rotate by editing this file in place; the
|
|
2108
|
-
# .zshenv / .profile source-lines below will pick it up automatically.
|
|
2109
|
-
export CLAUDE_CODE_OAUTH_TOKEN=${shQuote(input.claudeOauthToken)}
|
|
2110
|
-
`)}
|
|
2111
|
-
SOURCE_LINE='[ -f "$HOME/.config/openape/claude-token.env" ] && . "$HOME/.config/openape/claude-token.env"'
|
|
2112
|
-
for f in "$HOME_DIR/.zshenv" "$HOME_DIR/.profile"; do
|
|
2113
|
-
touch "$f"
|
|
2114
|
-
if ! grep -qF 'config/openape/claude-token.env' "$f" 2>/dev/null; then
|
|
2115
|
-
{
|
|
2116
|
-
echo ''
|
|
2117
|
-
echo '# OpenApe: load Claude Code OAuth token (added by apes agents spawn)'
|
|
2118
|
-
echo "$SOURCE_LINE"
|
|
2119
|
-
} >> "$f"
|
|
2120
|
-
fi
|
|
2121
|
-
done
|
|
2122
2105
|
` : "";
|
|
2123
2106
|
return `#!/bin/bash
|
|
2124
2107
|
set -euo pipefail
|
|
2125
2108
|
|
|
2126
|
-
#
|
|
2127
|
-
#
|
|
2128
|
-
|
|
2129
|
-
# paths everywhere.
|
|
2130
|
-
export PATH="/usr/sbin:/usr/bin:/bin:/sbin:/opt/homebrew/bin:/usr/local/bin"
|
|
2109
|
+
# Wide PATH so useradd / getent / install / chown resolve regardless of
|
|
2110
|
+
# how the privileged wrapper trimmed the environment.
|
|
2111
|
+
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
2131
2112
|
|
|
2132
2113
|
NAME=${shQuote(name)}
|
|
2133
|
-
MACOS_USER=${shQuote(macOSUsername)}
|
|
2134
2114
|
HOME_DIR=${shQuote(homeDir)}
|
|
2135
2115
|
SHELL_PATH=${shQuote(shellPath)}
|
|
2136
2116
|
|
|
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
|
|
2117
|
+
# Agent homes live under /var/lib/openape/homes/ (the persisted
|
|
2118
|
+
# openape-homes volume) \u2014 out of /home/ where real operator accounts
|
|
2119
|
+
# live. useradd --create-home makes the leaf dir but not missing
|
|
2120
|
+
# parents; the volume mount provides the parent, but pre-create it
|
|
2121
|
+
# anyway so a bare-metal/non-volume run still works.
|
|
2122
|
+
mkdir -p /var/lib/openape/homes
|
|
2123
|
+
chmod 755 /var/lib/openape/homes
|
|
2124
|
+
|
|
2125
|
+
# Create the agent's OS user if absent. spawn.ts already refused earlier
|
|
2126
|
+
# when the user existed, but guard here too so a re-run of the privileged
|
|
2127
|
+
# script is idempotent rather than erroring on a half-created account.
|
|
2128
|
+
if ! getent passwd "$NAME" >/dev/null 2>&1; then
|
|
2129
|
+
useradd --create-home --home-dir "$HOME_DIR" --shell "$SHELL_PATH" --comment "OpenApe Agent $NAME" "$NAME"
|
|
2161
2130
|
fi
|
|
2162
2131
|
|
|
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
|
|
2191
|
-
|
|
2192
|
-
dscl . -create "/Users/$MACOS_USER"
|
|
2193
|
-
fi
|
|
2194
|
-
|
|
2195
|
-
# Idempotent attribute writes \u2014 \`dscl . -create\` on an existing
|
|
2196
|
-
# property overwrites in place, so the tombstone-reuse path lands
|
|
2197
|
-
# the same end-state as a fresh create.
|
|
2198
|
-
dscl . -create "/Users/$MACOS_USER" UserShell "$SHELL_PATH"
|
|
2199
|
-
dscl . -create "/Users/$MACOS_USER" RealName "OpenApe Agent $NAME"
|
|
2200
|
-
dscl . -create "/Users/$MACOS_USER" UniqueID "$NEXT_UID"
|
|
2201
|
-
dscl . -create "/Users/$MACOS_USER" PrimaryGroupID 20
|
|
2202
|
-
dscl . -create "/Users/$MACOS_USER" NFSHomeDirectory "$HOME_DIR"
|
|
2203
|
-
dscl . -create "/Users/$MACOS_USER" IsHidden 1
|
|
2132
|
+
# Resolve the uid for the final report line (getent is the canonical read).
|
|
2133
|
+
NEW_UID=$(getent passwd "$NAME" | cut -d: -f3)
|
|
2204
2134
|
|
|
2205
|
-
|
|
2135
|
+
# Identity dirs \u2014 created owned by the agent so the file writes below land
|
|
2136
|
+
# with the right owner even before the final recursive chown.
|
|
2137
|
+
install -d -m 700 -o "$NAME" "$HOME_DIR/.ssh"
|
|
2138
|
+
install -d -m 700 -o "$NAME" "$HOME_DIR/.config"
|
|
2139
|
+
install -d -m 700 -o "$NAME" "$HOME_DIR/.config/apes"
|
|
2140
|
+
install -d -m 700 -o "$NAME" "$HOME_DIR/.config/openape"
|
|
2206
2141
|
|
|
2207
2142
|
cat > "$HOME_DIR/.ssh/id_ed25519" ${shHeredoc(privatePemForHeredoc.trimEnd())}
|
|
2208
|
-
cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(
|
|
2143
|
+
cat > "$HOME_DIR/.ssh/id_ed25519.pub" ${shHeredoc(input.publicKeySshLine)}
|
|
2209
2144
|
cat > "$HOME_DIR/.config/apes/auth.json" ${shHeredoc(input.authJson)}
|
|
2210
|
-
mkdir -p "$HOME_DIR/.config/openape"
|
|
2211
2145
|
cat > "$HOME_DIR/.config/openape/agent-x25519.key" ${shHeredoc(input.x25519PrivateKey)}
|
|
2212
2146
|
cat > "$HOME_DIR/.config/openape/agent-x25519.key.pub" ${shHeredoc(input.x25519PublicKey)}
|
|
2213
|
-
${claudeBlock}
|
|
2214
|
-
|
|
2147
|
+
${claudeBlock}
|
|
2148
|
+
# Per-agent task dir that \`apes agents sync\` writes to (XDG-style on
|
|
2149
|
+
# Linux; was ~/Library/... on macOS).
|
|
2150
|
+
mkdir -p "$HOME_DIR/.openape/agent/tasks"
|
|
2151
|
+
|
|
2152
|
+
chown -R "$NAME:" "$HOME_DIR"
|
|
2215
2153
|
chmod 700 "$HOME_DIR/.ssh"
|
|
2216
2154
|
chmod 700 "$HOME_DIR/.config"
|
|
2217
2155
|
chmod 700 "$HOME_DIR/.config/openape"
|
|
@@ -2220,143 +2158,8 @@ chmod 644 "$HOME_DIR/.ssh/id_ed25519.pub"
|
|
|
2220
2158
|
chmod 600 "$HOME_DIR/.config/apes/auth.json"
|
|
2221
2159
|
chmod 600 "$HOME_DIR/.config/openape/agent-x25519.key"
|
|
2222
2160
|
chmod 644 "$HOME_DIR/.config/openape/agent-x25519.key.pub"
|
|
2223
|
-
if [ -f "$HOME_DIR/.config/openape/claude-token.env" ]; then
|
|
2224
|
-
chmod 700 "$HOME_DIR/.config/openape"
|
|
2225
|
-
chmod 600 "$HOME_DIR/.config/openape/claude-token.env"
|
|
2226
|
-
fi
|
|
2227
|
-
|
|
2228
|
-
echo "OK $NAME (macOS user $MACOS_USER) uid=$NEXT_UID home=$HOME_DIR"
|
|
2229
|
-
${buildBridgeBootstrapBlock(input.bridge, name)}${buildTroopBootstrapBlock(input.troop, name)}`;
|
|
2230
|
-
}
|
|
2231
|
-
function buildBridgeBlock(bridge) {
|
|
2232
|
-
if (!bridge) return "";
|
|
2233
|
-
return `
|
|
2234
|
-
mkdir -p "$HOME_DIR/Library/Application Support/openape/bridge" "$HOME_DIR/Library/Logs"
|
|
2235
|
-
cat > "$HOME_DIR/Library/Application Support/openape/bridge/.env" ${shHeredoc(bridge.envFile)}
|
|
2236
|
-
chmod 600 "$HOME_DIR/Library/Application Support/openape/bridge/.env"
|
|
2237
|
-
`;
|
|
2238
|
-
}
|
|
2239
|
-
function buildBridgeBootstrapBlock(_bridge, _name) {
|
|
2240
|
-
return "";
|
|
2241
|
-
}
|
|
2242
|
-
function buildTroopBlock(_troop) {
|
|
2243
|
-
return `
|
|
2244
|
-
mkdir -p "$HOME_DIR/Library/Logs" "$HOME_DIR/.openape/agent/tasks"
|
|
2245
|
-
`;
|
|
2246
|
-
}
|
|
2247
|
-
function buildTroopBootstrapBlock(_troop, _name) {
|
|
2248
|
-
return "";
|
|
2249
|
-
}
|
|
2250
|
-
function runPhaseGTeardownInProcess(input) {
|
|
2251
|
-
const { name, homeDir } = input;
|
|
2252
|
-
const macOSUser = input.macOSUsername ?? name;
|
|
2253
|
-
let uid = null;
|
|
2254
|
-
try {
|
|
2255
|
-
const out = execFileSync2("/usr/bin/dscl", [".", "-read", `/Users/${macOSUser}`, "UniqueID"], { encoding: "utf8" });
|
|
2256
|
-
const m = out.match(/UniqueID:\s*(\d+)/);
|
|
2257
|
-
if (m) uid = m[1];
|
|
2258
|
-
} catch {
|
|
2259
|
-
}
|
|
2260
|
-
if (uid) {
|
|
2261
|
-
try {
|
|
2262
|
-
execFileSync2("/bin/launchctl", ["bootout", `user/${uid}`], { stdio: "ignore" });
|
|
2263
|
-
} catch {
|
|
2264
|
-
}
|
|
2265
|
-
try {
|
|
2266
|
-
execFileSync2("/usr/bin/pkill", ["-9", "-u", uid], { stdio: "ignore" });
|
|
2267
|
-
} catch {
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
const agentDir = `/var/openape/agents/${name}`;
|
|
2271
|
-
try {
|
|
2272
|
-
rmSync(agentDir, { recursive: true, force: true });
|
|
2273
|
-
} catch {
|
|
2274
|
-
}
|
|
2275
|
-
if (homeDir && homeDir !== "/" && homeDir.startsWith("/var/openape/homes/")) {
|
|
2276
|
-
try {
|
|
2277
|
-
rmSync(homeDir, { recursive: true, force: true });
|
|
2278
|
-
} catch {
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
console.log(`OK Phase-G teardown done for ${name} (dscl record /Users/${macOSUser} kept as tombstone)`);
|
|
2282
|
-
}
|
|
2283
|
-
function buildDestroyTeardownScript(input) {
|
|
2284
|
-
const { name, homeDir, adminUser } = input;
|
|
2285
|
-
return `#!/bin/bash
|
|
2286
|
-
# Best-effort teardown. set -u catches typos; we deliberately do NOT use -e
|
|
2287
|
-
# because pkill / launchctl are allowed to fail when the user has no live
|
|
2288
|
-
# sessions.
|
|
2289
|
-
set -u
|
|
2290
|
-
|
|
2291
|
-
NAME=${shQuote(name)}
|
|
2292
|
-
HOME_DIR=${shQuote(homeDir)}
|
|
2293
|
-
ADMIN_USER=${shQuote(adminUser)}
|
|
2294
|
-
|
|
2295
|
-
# Read the admin password from stdin (line 1). The caller pipes it in.
|
|
2296
|
-
# We never accept it as an argv element so it can't show up in process
|
|
2297
|
-
# listings or escapes' audit log.
|
|
2298
|
-
read -r ADMIN_PASSWORD
|
|
2299
|
-
if [ -z "$ADMIN_PASSWORD" ]; then
|
|
2300
|
-
echo "ERROR: no admin password on stdin (expected one line)." >&2
|
|
2301
|
-
exit 2
|
|
2302
|
-
fi
|
|
2303
|
-
|
|
2304
|
-
UID_OF=$(dscl . -read "/Users/$NAME" UniqueID 2>/dev/null | awk '/UniqueID:/ {print $2}')
|
|
2305
|
-
|
|
2306
|
-
if [ -n "$UID_OF" ]; then
|
|
2307
|
-
launchctl bootout "user/$UID_OF" 2>/dev/null || true
|
|
2308
|
-
pkill -9 -u "$UID_OF" 2>/dev/null || true
|
|
2309
|
-
fi
|
|
2310
|
-
|
|
2311
|
-
# Per-agent system LaunchDaemon written by spawn (unless --no-bridge). Bootout +
|
|
2312
|
-
# delete must come BEFORE we delete the user, otherwise launchd keeps a
|
|
2313
|
-
# zombie reference. No-op if the plist isn't there.
|
|
2314
|
-
BRIDGE_LABEL="eco.hofmann.apes.bridge.$NAME"
|
|
2315
|
-
BRIDGE_PLIST="/Library/LaunchDaemons/$BRIDGE_LABEL.plist"
|
|
2316
|
-
if [ -f "$BRIDGE_PLIST" ]; then
|
|
2317
|
-
launchctl bootout "system/$BRIDGE_LABEL" 2>/dev/null || true
|
|
2318
|
-
rm -f "$BRIDGE_PLIST"
|
|
2319
|
-
fi
|
|
2320
|
-
|
|
2321
|
-
if [ -d "$HOME_DIR" ] && [ "$HOME_DIR" != "/" ] && [ "$HOME_DIR" != "" ]; then
|
|
2322
|
-
rm -rf "$HOME_DIR"
|
|
2323
|
-
fi
|
|
2324
|
-
|
|
2325
|
-
# \`escapes\` is a plain setuid binary \u2014 opendirectoryd sees no audit/PAM
|
|
2326
|
-
# session attached (AUDIT_SESSION_ID=unset) and rejects DirectoryService
|
|
2327
|
-
# writes from this context: a bare \`sysadminctl -deleteUser\` or
|
|
2328
|
-
# \`dscl . -delete\` hangs ~5 minutes and exits with eUndefinedError -14987
|
|
2329
|
-
# at DSRecord.m:563. Passing explicit -adminUser/-adminPassword bypasses
|
|
2330
|
-
# opendirectoryd's implicit "is current session admin?" check and
|
|
2331
|
-
# authenticates against DirectoryService directly \u2014 the delete then
|
|
2332
|
-
# completes in ~1 second.
|
|
2333
|
-
if ! command -v sysadminctl >/dev/null 2>&1; then
|
|
2334
|
-
echo "ERROR: sysadminctl not available; cannot delete user record." >&2
|
|
2335
|
-
exit 1
|
|
2336
|
-
fi
|
|
2337
|
-
|
|
2338
|
-
sysadminctl \\
|
|
2339
|
-
-deleteUser "$NAME" \\
|
|
2340
|
-
-adminUser "$ADMIN_USER" \\
|
|
2341
|
-
-adminPassword "$ADMIN_PASSWORD"
|
|
2342
|
-
SYSAD_EC=$?
|
|
2343
|
-
unset ADMIN_PASSWORD
|
|
2344
|
-
|
|
2345
|
-
if [ $SYSAD_EC -ne 0 ]; then
|
|
2346
|
-
echo "ERROR: sysadminctl -deleteUser failed (exit=$SYSAD_EC)." >&2
|
|
2347
|
-
echo " Common causes: wrong admin password, admin user '$ADMIN_USER'" >&2
|
|
2348
|
-
echo " not in admin group, or target user '$NAME' is the last secure" >&2
|
|
2349
|
-
echo " token holder (run \\\`sysadminctl -secureTokenStatus $NAME\\\`)." >&2
|
|
2350
|
-
exit 1
|
|
2351
|
-
fi
|
|
2352
2161
|
|
|
2353
|
-
|
|
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"
|
|
2162
|
+
echo "OK $NAME (linux user) uid=$NEW_UID home=$HOME_DIR"
|
|
2360
2163
|
`;
|
|
2361
2164
|
}
|
|
2362
2165
|
function shQuote(s) {
|
|
@@ -2401,81 +2204,11 @@ print(json.dumps(out))
|
|
|
2401
2204
|
'
|
|
2402
2205
|
`;
|
|
2403
2206
|
|
|
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
|
-
}
|
|
2207
|
+
// src/lib/which.ts
|
|
2208
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2476
2209
|
function whichBinary(name) {
|
|
2477
2210
|
try {
|
|
2478
|
-
const out =
|
|
2211
|
+
const out = execFileSync2("which", [name], {
|
|
2479
2212
|
encoding: "utf-8",
|
|
2480
2213
|
stdio: ["ignore", "pipe", "ignore"]
|
|
2481
2214
|
}).trim();
|
|
@@ -2484,193 +2217,34 @@ function whichBinary(name) {
|
|
|
2484
2217
|
return null;
|
|
2485
2218
|
}
|
|
2486
2219
|
}
|
|
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
2220
|
|
|
2493
2221
|
// src/lib/host-platform/index.ts
|
|
2494
2222
|
import process2 from "process";
|
|
2495
2223
|
|
|
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
2224
|
// src/lib/host-platform/linux-host.ts
|
|
2651
|
-
import { hostname as
|
|
2652
|
-
import { existsSync as
|
|
2225
|
+
import { hostname as hostname3 } from "os";
|
|
2226
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2653
2227
|
var FALLBACK_PATHS = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
|
|
2654
2228
|
function getLinuxHostId() {
|
|
2655
2229
|
for (const path2 of FALLBACK_PATHS) {
|
|
2656
|
-
if (!
|
|
2230
|
+
if (!existsSync3(path2)) continue;
|
|
2657
2231
|
try {
|
|
2658
|
-
const v =
|
|
2232
|
+
const v = readFileSync2(path2, "utf-8").trim();
|
|
2659
2233
|
if (v) return v;
|
|
2660
2234
|
} catch {
|
|
2661
2235
|
}
|
|
2662
2236
|
}
|
|
2663
|
-
return
|
|
2237
|
+
return hostname3();
|
|
2664
2238
|
}
|
|
2665
2239
|
function getLinuxHostname() {
|
|
2666
|
-
return
|
|
2240
|
+
return hostname3();
|
|
2667
2241
|
}
|
|
2668
2242
|
|
|
2669
2243
|
// src/lib/host-platform/linux-user.ts
|
|
2670
|
-
import { execFileSync as
|
|
2244
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2671
2245
|
function getentPasswd(name) {
|
|
2672
2246
|
try {
|
|
2673
|
-
return
|
|
2247
|
+
return execFileSync3("getent", ["passwd", name], {
|
|
2674
2248
|
encoding: "utf-8",
|
|
2675
2249
|
stdio: ["ignore", "pipe", "ignore"]
|
|
2676
2250
|
}).trim() || null;
|
|
@@ -2698,7 +2272,7 @@ function readLinuxUser(name) {
|
|
|
2698
2272
|
}
|
|
2699
2273
|
function listLinuxUserNames() {
|
|
2700
2274
|
try {
|
|
2701
|
-
const out =
|
|
2275
|
+
const out = execFileSync3("getent", ["passwd"], {
|
|
2702
2276
|
encoding: "utf-8",
|
|
2703
2277
|
stdio: ["ignore", "pipe", "ignore"]
|
|
2704
2278
|
});
|
|
@@ -2714,29 +2288,29 @@ function listLinuxUserNames() {
|
|
|
2714
2288
|
}
|
|
2715
2289
|
|
|
2716
2290
|
// src/lib/host-platform/linux-exec.ts
|
|
2717
|
-
import { execFileSync as
|
|
2718
|
-
import { mkdtempSync
|
|
2719
|
-
import { tmpdir
|
|
2720
|
-
import { join as
|
|
2291
|
+
import { execFileSync as execFileSync4, spawnSync } from "child_process";
|
|
2292
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
2293
|
+
import { tmpdir } from "os";
|
|
2294
|
+
import { join as join2 } from "path";
|
|
2721
2295
|
async function runPrivilegedBashOnLinux(script) {
|
|
2722
|
-
const dir =
|
|
2723
|
-
const scriptPath =
|
|
2724
|
-
|
|
2296
|
+
const dir = mkdtempSync(join2(tmpdir(), "apes-privileged-"));
|
|
2297
|
+
const scriptPath = join2(dir, "run.sh");
|
|
2298
|
+
writeFileSync(scriptPath, script, { mode: 448 });
|
|
2725
2299
|
try {
|
|
2726
2300
|
if (process.getuid?.() === 0) {
|
|
2727
|
-
|
|
2301
|
+
execFileSync4("bash", [scriptPath], { stdio: "inherit" });
|
|
2728
2302
|
} else {
|
|
2729
|
-
|
|
2303
|
+
execFileSync4("sudo", ["-n", "--", "bash", scriptPath], { stdio: "inherit" });
|
|
2730
2304
|
}
|
|
2731
2305
|
} finally {
|
|
2732
2306
|
try {
|
|
2733
|
-
|
|
2307
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2734
2308
|
} catch {
|
|
2735
2309
|
}
|
|
2736
2310
|
}
|
|
2737
2311
|
}
|
|
2738
2312
|
async function runAsAgentUserOnLinux(agentName, argv) {
|
|
2739
|
-
const r =
|
|
2313
|
+
const r = spawnSync("sudo", ["-n", "-H", "-u", agentName, "--", ...argv], { encoding: "utf8" });
|
|
2740
2314
|
return {
|
|
2741
2315
|
stdout: r.stdout ?? "",
|
|
2742
2316
|
stderr: r.stderr ?? "",
|
|
@@ -2745,8 +2319,8 @@ async function runAsAgentUserOnLinux(agentName, argv) {
|
|
|
2745
2319
|
}
|
|
2746
2320
|
|
|
2747
2321
|
// src/lib/host-platform/linux-nest.ts
|
|
2748
|
-
import { execFileSync as
|
|
2749
|
-
import { existsSync as
|
|
2322
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
2323
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2750
2324
|
var UNIT_NAME = "openape-nest.service";
|
|
2751
2325
|
var UNIT_PATH = `/etc/systemd/system/${UNIT_NAME}`;
|
|
2752
2326
|
function buildNestUnit(spec) {
|
|
@@ -2776,23 +2350,23 @@ async function installNestSupervisorOnLinux(spec) {
|
|
|
2776
2350
|
const desired = buildNestUnit(spec);
|
|
2777
2351
|
let existing = "";
|
|
2778
2352
|
try {
|
|
2779
|
-
existing =
|
|
2353
|
+
existing = readFileSync3(UNIT_PATH, "utf8");
|
|
2780
2354
|
} catch {
|
|
2781
2355
|
}
|
|
2782
2356
|
if (existing !== desired) {
|
|
2783
|
-
|
|
2357
|
+
writeFileSync2(UNIT_PATH, desired, { mode: 420 });
|
|
2784
2358
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2359
|
+
execFileSync5("systemctl", ["daemon-reload"], { stdio: "inherit" });
|
|
2360
|
+
execFileSync5("systemctl", ["enable", "--now", UNIT_NAME], { stdio: "inherit" });
|
|
2787
2361
|
}
|
|
2788
2362
|
async function uninstallNestSupervisorOnLinux() {
|
|
2789
2363
|
try {
|
|
2790
|
-
|
|
2364
|
+
execFileSync5("systemctl", ["disable", "--now", UNIT_NAME], { stdio: "inherit" });
|
|
2791
2365
|
} catch {
|
|
2792
2366
|
}
|
|
2793
|
-
if (
|
|
2367
|
+
if (existsSync4(UNIT_PATH)) unlinkSync(UNIT_PATH);
|
|
2794
2368
|
try {
|
|
2795
|
-
|
|
2369
|
+
execFileSync5("systemctl", ["daemon-reload"], { stdio: "inherit" });
|
|
2796
2370
|
} catch {
|
|
2797
2371
|
}
|
|
2798
2372
|
}
|
|
@@ -2818,18 +2392,14 @@ var linuxHostPlatform = {
|
|
|
2818
2392
|
};
|
|
2819
2393
|
|
|
2820
2394
|
// src/lib/host-platform/index.ts
|
|
2821
|
-
function isDarwin2() {
|
|
2822
|
-
return process2.platform === "darwin";
|
|
2823
|
-
}
|
|
2824
2395
|
function isLinux() {
|
|
2825
2396
|
return process2.platform === "linux";
|
|
2826
2397
|
}
|
|
2827
2398
|
var testOverride = null;
|
|
2828
2399
|
function getHostPlatform() {
|
|
2829
2400
|
if (testOverride) return testOverride;
|
|
2830
|
-
if (isDarwin2()) return darwinHostPlatform;
|
|
2831
2401
|
if (isLinux()) return linuxHostPlatform;
|
|
2832
|
-
throw new Error(`unsupported host platform: ${process2.platform}`);
|
|
2402
|
+
throw new Error(`unsupported host platform: ${process2.platform} \u2014 OpenApe nests are Linux-only`);
|
|
2833
2403
|
}
|
|
2834
2404
|
|
|
2835
2405
|
// src/commands/agents/allow.ts
|
|
@@ -2842,7 +2412,7 @@ var allowAgentCommand = defineCommand22({
|
|
|
2842
2412
|
agent: {
|
|
2843
2413
|
type: "positional",
|
|
2844
2414
|
required: true,
|
|
2845
|
-
description: "Agent name (the
|
|
2415
|
+
description: "Agent name (the Linux username spawn created)"
|
|
2846
2416
|
},
|
|
2847
2417
|
email: {
|
|
2848
2418
|
type: "positional",
|
|
@@ -2859,11 +2429,8 @@ var allowAgentCommand = defineCommand22({
|
|
|
2859
2429
|
if (!email.includes("@")) {
|
|
2860
2430
|
throw new CliError(`Invalid email "${email}".`);
|
|
2861
2431
|
}
|
|
2862
|
-
if (!isDarwin2()) {
|
|
2863
|
-
throw new CliError("`apes agents allow` is currently macOS-only.");
|
|
2864
|
-
}
|
|
2865
2432
|
if (!getHostPlatform().lookupAgentUser(agent)) {
|
|
2866
|
-
throw new CliError(`No
|
|
2433
|
+
throw new CliError(`No OS user for agent "${agent}" \u2014 has it been spawned?`);
|
|
2867
2434
|
}
|
|
2868
2435
|
const apes = whichBinary("apes");
|
|
2869
2436
|
if (!apes) throw new CliError("`apes` not found on PATH.");
|
|
@@ -2894,7 +2461,7 @@ PY
|
|
|
2894
2461
|
chmod 600 "$F"
|
|
2895
2462
|
`;
|
|
2896
2463
|
consola19.start(`Adding ${email} to ${agent}'s allowlist\u2026`);
|
|
2897
|
-
|
|
2464
|
+
execFileSync6(apes, ["run", "--as", agent, "--wait", "--", "bash", "-c", script], { stdio: "inherit" });
|
|
2898
2465
|
consola19.success(`${agent} will auto-accept future contact requests from ${email} (within ~30s of next bridge connect).`);
|
|
2899
2466
|
}
|
|
2900
2467
|
});
|
|
@@ -2903,13 +2470,12 @@ function shQuote2(s) {
|
|
|
2903
2470
|
}
|
|
2904
2471
|
|
|
2905
2472
|
// src/commands/agents/cleanup-orphans.ts
|
|
2906
|
-
import { execFileSync as execFileSync11 } from "child_process";
|
|
2907
2473
|
import { defineCommand as defineCommand23 } from "citty";
|
|
2908
2474
|
import consola20 from "consola";
|
|
2909
2475
|
var cleanupOrphansCommand = defineCommand23({
|
|
2910
2476
|
meta: {
|
|
2911
2477
|
name: "cleanup-orphans",
|
|
2912
|
-
description: "
|
|
2478
|
+
description: "Report agent-user tombstones. Linux userdel is atomic, so there are normally none."
|
|
2913
2479
|
},
|
|
2914
2480
|
args: {
|
|
2915
2481
|
"dry-run": {
|
|
@@ -2921,81 +2487,37 @@ var cleanupOrphansCommand = defineCommand23({
|
|
|
2921
2487
|
description: "Skip the interactive confirmation. Required when stdin is not a TTY."
|
|
2922
2488
|
}
|
|
2923
2489
|
},
|
|
2924
|
-
async run(
|
|
2925
|
-
if (!isDarwin2()) {
|
|
2926
|
-
throw new CliError(`\`apes agents cleanup-orphans\` is macOS-only. Detected platform: ${process.platform}.`);
|
|
2927
|
-
}
|
|
2490
|
+
async run() {
|
|
2928
2491
|
const orphans = getHostPlatform().listOrphanAgentUsers();
|
|
2929
2492
|
if (orphans.length === 0) {
|
|
2930
|
-
consola20.success("No agent tombstones
|
|
2493
|
+
consola20.success("No agent tombstones \u2014 userdel is clean on Linux.");
|
|
2931
2494
|
return;
|
|
2932
2495
|
}
|
|
2933
|
-
consola20.
|
|
2496
|
+
consola20.warn(`Found ${orphans.length} unexpected agent tombstone${orphans.length === 1 ? "" : "s"}:`);
|
|
2934
2497
|
for (const o of orphans) {
|
|
2935
2498
|
console.log(` \u2022 ${o.name}${o.uid !== null ? ` (uid=${o.uid})` : ""} \u2014 was ${o.homeDir}`);
|
|
2936
2499
|
}
|
|
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.`);
|
|
2500
|
+
consola20.info("Remove each one manually with `userdel -r <name>`.");
|
|
2979
2501
|
}
|
|
2980
2502
|
});
|
|
2981
2503
|
|
|
2982
2504
|
// src/commands/agents/code.ts
|
|
2983
|
-
import { existsSync as
|
|
2505
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
2984
2506
|
import { homedir as homedir5 } from "os";
|
|
2985
|
-
import { join as
|
|
2507
|
+
import { join as join4 } from "path";
|
|
2986
2508
|
import process3 from "process";
|
|
2987
2509
|
import { defineCommand as defineCommand24 } from "citty";
|
|
2988
2510
|
import { consola as consola21 } from "consola";
|
|
2989
2511
|
import { taskTools, runApeShell, runCodingTask, buildIssueGet, detectForge, createLlmReviewer, createLlmRiskAssessor, resolveMergePolicy } from "@openape/agent-runtime";
|
|
2990
2512
|
|
|
2991
2513
|
// src/lib/agent-secrets-runtime.ts
|
|
2992
|
-
import { existsSync as
|
|
2514
|
+
import { existsSync as existsSync5, mkdirSync, readdirSync, readFileSync as readFileSync4, statSync, watch, writeFileSync as writeFileSync3 } from "fs";
|
|
2993
2515
|
import { homedir as homedir4 } from "os";
|
|
2994
|
-
import { join as
|
|
2516
|
+
import { dirname, join as join3 } from "path";
|
|
2995
2517
|
import { openString } from "@openape/core";
|
|
2996
|
-
var CONFIG_DIR2 =
|
|
2997
|
-
var SECRETS_DIR =
|
|
2998
|
-
var X25519_KEY_PATH =
|
|
2518
|
+
var CONFIG_DIR2 = join3(homedir4(), ".config", "openape");
|
|
2519
|
+
var SECRETS_DIR = join3(CONFIG_DIR2, "secrets.d");
|
|
2520
|
+
var X25519_KEY_PATH = join3(CONFIG_DIR2, "agent-x25519.key");
|
|
2999
2521
|
var X25519_PUBKEY_PATH = `${X25519_KEY_PATH}.pub`;
|
|
3000
2522
|
function envNameFromFile(file) {
|
|
3001
2523
|
if (!file.endsWith(".blob")) return null;
|
|
@@ -3003,13 +2525,13 @@ function envNameFromFile(file) {
|
|
|
3003
2525
|
return /^[A-Z][A-Z0-9_]*$/.test(env) ? env : null;
|
|
3004
2526
|
}
|
|
3005
2527
|
function readAgentEncryptionKey(keyPath = X25519_KEY_PATH) {
|
|
3006
|
-
if (!
|
|
3007
|
-
const k =
|
|
2528
|
+
if (!existsSync5(keyPath)) return null;
|
|
2529
|
+
const k = readFileSync4(keyPath, "utf8").trim();
|
|
3008
2530
|
return k.length > 0 ? k : null;
|
|
3009
2531
|
}
|
|
3010
2532
|
function readAgentEncryptionPublicKey(pubPath = X25519_PUBKEY_PATH) {
|
|
3011
|
-
if (!
|
|
3012
|
-
const k =
|
|
2533
|
+
if (!existsSync5(pubPath)) return null;
|
|
2534
|
+
const k = readFileSync4(pubPath, "utf8").trim();
|
|
3013
2535
|
return k.length > 0 ? k : null;
|
|
3014
2536
|
}
|
|
3015
2537
|
function materializeSecrets(opts = {}) {
|
|
@@ -3020,13 +2542,23 @@ function materializeSecrets(opts = {}) {
|
|
|
3020
2542
|
const applied = [];
|
|
3021
2543
|
const failed = [];
|
|
3022
2544
|
const key = readAgentEncryptionKey(opts.keyPath);
|
|
3023
|
-
const files = key &&
|
|
2545
|
+
const files = key && existsSync5(dir) ? readdirSync(dir) : [];
|
|
3024
2546
|
for (const file of files) {
|
|
3025
2547
|
const name = envNameFromFile(file);
|
|
3026
2548
|
if (!name) continue;
|
|
3027
2549
|
try {
|
|
3028
|
-
const box = JSON.parse(
|
|
3029
|
-
|
|
2550
|
+
const box = JSON.parse(readFileSync4(join3(dir, file), "utf8"));
|
|
2551
|
+
const plaintext = openString(box, key);
|
|
2552
|
+
const target = typeof box.materializeTo === "string" ? box.materializeTo : null;
|
|
2553
|
+
if (target) {
|
|
2554
|
+
const blobMtime = statSync(join3(dir, file)).mtimeMs;
|
|
2555
|
+
if (!existsSync5(target) || statSync(target).mtimeMs < blobMtime) {
|
|
2556
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
2557
|
+
writeFileSync3(target, plaintext, { mode: 384 });
|
|
2558
|
+
}
|
|
2559
|
+
} else {
|
|
2560
|
+
env[name] = plaintext;
|
|
2561
|
+
}
|
|
3030
2562
|
applied.push(name);
|
|
3031
2563
|
} catch (e) {
|
|
3032
2564
|
failed.push(file);
|
|
@@ -3052,7 +2584,7 @@ function startSecretsWatcher(opts = {}) {
|
|
|
3052
2584
|
appliedNames = new Set(r.applied);
|
|
3053
2585
|
};
|
|
3054
2586
|
run();
|
|
3055
|
-
if (!
|
|
2587
|
+
if (!existsSync5(dir)) return () => {
|
|
3056
2588
|
};
|
|
3057
2589
|
let timer = null;
|
|
3058
2590
|
const watcher = watch(dir, () => {
|
|
@@ -3079,9 +2611,9 @@ var DEFAULT_PERSONA = [
|
|
|
3079
2611
|
].join(" ");
|
|
3080
2612
|
function readLitellmConfig(model) {
|
|
3081
2613
|
const env = {};
|
|
3082
|
-
const envPath =
|
|
3083
|
-
if (
|
|
3084
|
-
for (const raw of
|
|
2614
|
+
const envPath = join4(homedir5(), "litellm", ".env");
|
|
2615
|
+
if (existsSync6(envPath)) {
|
|
2616
|
+
for (const raw of readFileSync5(envPath, "utf8").split("\n")) {
|
|
3085
2617
|
const line = raw.trim();
|
|
3086
2618
|
const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
3087
2619
|
if (m) env[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
|
|
@@ -3097,11 +2629,11 @@ function readLitellmConfig(model) {
|
|
|
3097
2629
|
return { apiBase, apiKey, model: model || process3.env.APE_CHAT_BRIDGE_MODEL || "claude-haiku-4-5" };
|
|
3098
2630
|
}
|
|
3099
2631
|
function readPersona(file) {
|
|
3100
|
-
if (file &&
|
|
3101
|
-
const agentJson =
|
|
3102
|
-
if (
|
|
2632
|
+
if (file && existsSync6(file)) return readFileSync5(file, "utf8");
|
|
2633
|
+
const agentJson = join4(homedir5(), ".openape", "agent", "agent.json");
|
|
2634
|
+
if (existsSync6(agentJson)) {
|
|
3103
2635
|
try {
|
|
3104
|
-
const p = JSON.parse(
|
|
2636
|
+
const p = JSON.parse(readFileSync5(agentJson, "utf8"));
|
|
3105
2637
|
if (p.systemPrompt?.trim()) return p.systemPrompt;
|
|
3106
2638
|
} catch {
|
|
3107
2639
|
}
|
|
@@ -3186,30 +2718,27 @@ ${result.reason}`);
|
|
|
3186
2718
|
});
|
|
3187
2719
|
|
|
3188
2720
|
// 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
2721
|
import { defineCommand as defineCommand25 } from "citty";
|
|
3194
2722
|
import consola22 from "consola";
|
|
3195
2723
|
|
|
3196
2724
|
// src/lib/nest-registry.ts
|
|
3197
|
-
import { existsSync as
|
|
2725
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
3198
2726
|
import { homedir as homedir6 } from "os";
|
|
3199
|
-
import { join as
|
|
2727
|
+
import { join as join5 } from "path";
|
|
3200
2728
|
function resolveRegistryPath() {
|
|
3201
|
-
if (
|
|
3202
|
-
if (
|
|
3203
|
-
|
|
2729
|
+
if (process.env.OPENAPE_NEST_REGISTRY_PATH) return process.env.OPENAPE_NEST_REGISTRY_PATH;
|
|
2730
|
+
if (existsSync7("/var/openape/nest/agents.json")) return "/var/openape/nest/agents.json";
|
|
2731
|
+
if (existsSync7("/var/openape/nest")) return "/var/openape/nest/agents.json";
|
|
2732
|
+
return join5(homedir6(), ".openape", "nest", "agents.json");
|
|
3204
2733
|
}
|
|
3205
2734
|
function emptyRegistry() {
|
|
3206
2735
|
return { version: 1, agents: [] };
|
|
3207
2736
|
}
|
|
3208
2737
|
function readNestRegistry() {
|
|
3209
2738
|
const path2 = resolveRegistryPath();
|
|
3210
|
-
if (!
|
|
2739
|
+
if (!existsSync7(path2)) return emptyRegistry();
|
|
3211
2740
|
try {
|
|
3212
|
-
const parsed = JSON.parse(
|
|
2741
|
+
const parsed = JSON.parse(readFileSync6(path2, "utf8"));
|
|
3213
2742
|
if (parsed?.version !== 1 || !Array.isArray(parsed.agents)) return emptyRegistry();
|
|
3214
2743
|
return parsed;
|
|
3215
2744
|
} catch {
|
|
@@ -3223,7 +2752,7 @@ function writeNestRegistry(reg) {
|
|
|
3223
2752
|
mkdirSync2(dir, { recursive: true });
|
|
3224
2753
|
} catch {
|
|
3225
2754
|
}
|
|
3226
|
-
|
|
2755
|
+
writeFileSync4(path2, `${JSON.stringify(reg, null, 2)}
|
|
3227
2756
|
`, { mode: 432 });
|
|
3228
2757
|
}
|
|
3229
2758
|
function upsertNestAgent(entry) {
|
|
@@ -3242,65 +2771,11 @@ function removeNestAgent(name) {
|
|
|
3242
2771
|
return true;
|
|
3243
2772
|
}
|
|
3244
2773
|
|
|
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
2774
|
// src/commands/agents/destroy.ts
|
|
3300
2775
|
var destroyAgentCommand = defineCommand25({
|
|
3301
2776
|
meta: {
|
|
3302
2777
|
name: "destroy",
|
|
3303
|
-
description: "Tear down an agent: remove
|
|
2778
|
+
description: "Tear down an agent: remove the OS user, hard-delete IdP agent, drop all SSH keys"
|
|
3304
2779
|
},
|
|
3305
2780
|
args: {
|
|
3306
2781
|
name: {
|
|
@@ -3319,10 +2794,6 @@ var destroyAgentCommand = defineCommand25({
|
|
|
3319
2794
|
"keep-os-user": {
|
|
3320
2795
|
type: "boolean",
|
|
3321
2796
|
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
2797
|
}
|
|
3327
2798
|
},
|
|
3328
2799
|
async run({ args }) {
|
|
@@ -3332,18 +2803,6 @@ var destroyAgentCommand = defineCommand25({
|
|
|
3332
2803
|
`Invalid agent name "${name}". Must match /^[a-z][a-z0-9-]{0,23}$/.`
|
|
3333
2804
|
);
|
|
3334
2805
|
}
|
|
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
2806
|
const auth = loadAuth();
|
|
3348
2807
|
if (!auth) {
|
|
3349
2808
|
throw new CliError("Not authenticated. Run `apes login` first.");
|
|
@@ -3355,7 +2814,7 @@ var destroyAgentCommand = defineCommand25({
|
|
|
3355
2814
|
const owned = await apiFetch("/api/my-agents", { idp });
|
|
3356
2815
|
const idpAgent = owned.find((u) => u.name === name);
|
|
3357
2816
|
const idpExists = idpAgent !== void 0;
|
|
3358
|
-
const osUser =
|
|
2817
|
+
const osUser = getHostPlatform().lookupAgentUser(name);
|
|
3359
2818
|
const osUserExists = !args["keep-os-user"] && osUser !== null;
|
|
3360
2819
|
if (!idpExists && !osUserExists) {
|
|
3361
2820
|
consola22.info(`Nothing to destroy: no IdP agent and no OS user for "${name}".`);
|
|
@@ -3364,8 +2823,8 @@ var destroyAgentCommand = defineCommand25({
|
|
|
3364
2823
|
if (!args.force) {
|
|
3365
2824
|
const consequences = [];
|
|
3366
2825
|
if (osUserExists) {
|
|
3367
|
-
const home = osUser?.homeDir ?? `/
|
|
3368
|
-
consequences.push(`\u2022 Remove
|
|
2826
|
+
const home = osUser?.homeDir ?? `/home/${name}`;
|
|
2827
|
+
consequences.push(`\u2022 Remove OS user ${osUser?.name ?? name} and rm -rf ${home}`);
|
|
3369
2828
|
}
|
|
3370
2829
|
if (idpExists) {
|
|
3371
2830
|
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 +2854,17 @@ ${consequences.join("\n")}`);
|
|
|
3395
2854
|
consola22.info("No IdP agent to remove (skipped).");
|
|
3396
2855
|
}
|
|
3397
2856
|
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).");
|
|
2857
|
+
consola22.start(`Removing OS user ${name}\u2026`);
|
|
2858
|
+
await getHostPlatform().runPrivilegedBash(
|
|
2859
|
+
`#!/bin/bash
|
|
2860
|
+
set -euo pipefail
|
|
2861
|
+
if getent passwd ${JSON.stringify(name)} >/dev/null 2>&1; then
|
|
2862
|
+
pkill -9 -u ${JSON.stringify(name)} 2>/dev/null || true
|
|
2863
|
+
userdel -r ${JSON.stringify(name)}
|
|
2864
|
+
fi
|
|
2865
|
+
`
|
|
2866
|
+
);
|
|
2867
|
+
consola22.success(`Removed OS user ${name}.`);
|
|
3461
2868
|
}
|
|
3462
2869
|
try {
|
|
3463
2870
|
removeNestAgent(name);
|
|
@@ -3467,15 +2874,6 @@ ${adminPassword}
|
|
|
3467
2874
|
consola22.success(`Destroyed ${name}.`);
|
|
3468
2875
|
}
|
|
3469
2876
|
});
|
|
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
2877
|
|
|
3480
2878
|
// src/commands/agents/list.ts
|
|
3481
2879
|
import { defineCommand as defineCommand26 } from "citty";
|
|
@@ -3507,7 +2905,7 @@ var listAgentsCommand = defineCommand26({
|
|
|
3507
2905
|
const all = await apiFetch("/api/my-agents", { idp });
|
|
3508
2906
|
const filtered = args["include-inactive"] ? all : all.filter((u) => u.isActive !== false);
|
|
3509
2907
|
const platform = getHostPlatform();
|
|
3510
|
-
const osUsers =
|
|
2908
|
+
const osUsers = platform.listAgentUserNames();
|
|
3511
2909
|
const osStateOf = (agentName) => {
|
|
3512
2910
|
const u = platform.lookupAgentUser(agentName);
|
|
3513
2911
|
if (u) return { osUser: true, home: u.homeDir };
|
|
@@ -3543,14 +2941,14 @@ var listAgentsCommand = defineCommand26({
|
|
|
3543
2941
|
for (const r of rows) {
|
|
3544
2942
|
const active = r.isActive ? "\u2713" : "\u2717";
|
|
3545
2943
|
const os = r.osUser ? "\u2713" : "\u2717";
|
|
3546
|
-
const homeCol = r.home ??
|
|
2944
|
+
const homeCol = r.home ?? "(missing)";
|
|
3547
2945
|
console.log(`${r.name.padEnd(nameW)} ${r.email.padEnd(emailW)} ${active.padEnd(6)} ${os.padEnd(7)} ${homeCol}`);
|
|
3548
2946
|
}
|
|
3549
2947
|
}
|
|
3550
2948
|
});
|
|
3551
2949
|
|
|
3552
2950
|
// src/commands/agents/register.ts
|
|
3553
|
-
import { existsSync as
|
|
2951
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
3554
2952
|
import { defineCommand as defineCommand27 } from "citty";
|
|
3555
2953
|
import consola24 from "consola";
|
|
3556
2954
|
var registerAgentCommand = defineCommand27({
|
|
@@ -3598,10 +2996,10 @@ var registerAgentCommand = defineCommand27({
|
|
|
3598
2996
|
throw new CliError("Pass either --public-key or --public-key-file, not both.");
|
|
3599
2997
|
}
|
|
3600
2998
|
if (!publicKey && keyFile) {
|
|
3601
|
-
if (!
|
|
2999
|
+
if (!existsSync8(keyFile)) {
|
|
3602
3000
|
throw new CliError(`Public-key file not found: ${keyFile}`);
|
|
3603
3001
|
}
|
|
3604
|
-
publicKey =
|
|
3002
|
+
publicKey = readFileSync7(keyFile, "utf-8").trim();
|
|
3605
3003
|
}
|
|
3606
3004
|
if (!publicKey) {
|
|
3607
3005
|
throw new CliError('Provide --public-key "<ssh-ed25519 line>" or --public-key-file <path>.');
|
|
@@ -3636,19 +3034,19 @@ var registerAgentCommand = defineCommand27({
|
|
|
3636
3034
|
});
|
|
3637
3035
|
|
|
3638
3036
|
// src/commands/agents/run.ts
|
|
3639
|
-
import { existsSync as
|
|
3037
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
3640
3038
|
import { homedir as homedir7 } from "os";
|
|
3641
|
-
import { join as
|
|
3039
|
+
import { join as join6 } from "path";
|
|
3642
3040
|
import { defineCommand as defineCommand28 } from "citty";
|
|
3643
3041
|
import consola25 from "consola";
|
|
3644
3042
|
import { taskTools as taskTools2, runLoop } from "@openape/agent-runtime";
|
|
3645
|
-
var AUTH_PATH =
|
|
3646
|
-
var TASK_CACHE_DIR =
|
|
3043
|
+
var AUTH_PATH = join6(homedir7(), ".config", "apes", "auth.json");
|
|
3044
|
+
var TASK_CACHE_DIR = join6(homedir7(), ".openape", "agent", "tasks");
|
|
3647
3045
|
function readAuth() {
|
|
3648
|
-
if (!
|
|
3046
|
+
if (!existsSync9(AUTH_PATH)) {
|
|
3649
3047
|
throw new CliError(`No agent auth found at ${AUTH_PATH}. Run \`apes agents spawn <name>\` first.`);
|
|
3650
3048
|
}
|
|
3651
|
-
const parsed = JSON.parse(
|
|
3049
|
+
const parsed = JSON.parse(readFileSync8(AUTH_PATH, "utf8"));
|
|
3652
3050
|
if (!parsed.access_token) throw new CliError("auth.json missing access_token");
|
|
3653
3051
|
return parsed;
|
|
3654
3052
|
}
|
|
@@ -3684,26 +3082,26 @@ ${msg}`.slice(0, 9e3);
|
|
|
3684
3082
|
}
|
|
3685
3083
|
}
|
|
3686
3084
|
function readTaskSpec(taskId) {
|
|
3687
|
-
const path2 =
|
|
3688
|
-
if (!
|
|
3085
|
+
const path2 = join6(TASK_CACHE_DIR, `${taskId}.json`);
|
|
3086
|
+
if (!existsSync9(path2)) {
|
|
3689
3087
|
throw new CliError(`No cached task spec at ${path2}. Run \`apes agents sync\` first to pull the task list from troop.`);
|
|
3690
3088
|
}
|
|
3691
|
-
return JSON.parse(
|
|
3089
|
+
return JSON.parse(readFileSync8(path2, "utf8"));
|
|
3692
3090
|
}
|
|
3693
|
-
var AGENT_CONFIG_PATH =
|
|
3091
|
+
var AGENT_CONFIG_PATH = join6(homedir7(), ".openape", "agent", "agent.json");
|
|
3694
3092
|
function readAgentConfig() {
|
|
3695
|
-
if (!
|
|
3093
|
+
if (!existsSync9(AGENT_CONFIG_PATH)) return { systemPrompt: "" };
|
|
3696
3094
|
try {
|
|
3697
|
-
return JSON.parse(
|
|
3095
|
+
return JSON.parse(readFileSync8(AGENT_CONFIG_PATH, "utf8"));
|
|
3698
3096
|
} catch {
|
|
3699
3097
|
return { systemPrompt: "" };
|
|
3700
3098
|
}
|
|
3701
3099
|
}
|
|
3702
3100
|
function readLitellmConfig2(model) {
|
|
3703
|
-
const envPath =
|
|
3101
|
+
const envPath = join6(homedir7(), "litellm", ".env");
|
|
3704
3102
|
const env = {};
|
|
3705
|
-
if (
|
|
3706
|
-
for (const line of
|
|
3103
|
+
if (existsSync9(envPath)) {
|
|
3104
|
+
for (const line of readFileSync8(envPath, "utf8").split(/\r?\n/)) {
|
|
3707
3105
|
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
3708
3106
|
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
3709
3107
|
}
|
|
@@ -3809,18 +3207,18 @@ var runAgentCommand = defineCommand28({
|
|
|
3809
3207
|
});
|
|
3810
3208
|
|
|
3811
3209
|
// src/commands/agents/serve.ts
|
|
3812
|
-
import { existsSync as
|
|
3210
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
3813
3211
|
import { homedir as homedir8 } from "os";
|
|
3814
|
-
import { join as
|
|
3212
|
+
import { join as join7 } from "path";
|
|
3815
3213
|
import { createInterface } from "readline";
|
|
3816
3214
|
import { defineCommand as defineCommand29 } from "citty";
|
|
3817
3215
|
import { taskTools as taskTools3, runLoop as runLoop2, RpcSessionMap } from "@openape/agent-runtime";
|
|
3818
|
-
var AUTH_PATH2 =
|
|
3216
|
+
var AUTH_PATH2 = join7(homedir8(), ".config", "apes", "auth.json");
|
|
3819
3217
|
function readLitellmConfig3(model) {
|
|
3820
|
-
const envPath =
|
|
3218
|
+
const envPath = join7(homedir8(), "litellm", ".env");
|
|
3821
3219
|
const env = {};
|
|
3822
|
-
if (
|
|
3823
|
-
for (const line of
|
|
3220
|
+
if (existsSync10(envPath)) {
|
|
3221
|
+
for (const line of readFileSync9(envPath, "utf8").split(/\r?\n/)) {
|
|
3824
3222
|
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
3825
3223
|
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
3826
3224
|
}
|
|
@@ -3852,9 +3250,9 @@ var serveAgentCommand = defineCommand29({
|
|
|
3852
3250
|
if (!args.rpc) {
|
|
3853
3251
|
throw new CliError("apes agents serve currently only supports --rpc mode");
|
|
3854
3252
|
}
|
|
3855
|
-
if (
|
|
3253
|
+
if (existsSync10(AUTH_PATH2)) {
|
|
3856
3254
|
try {
|
|
3857
|
-
JSON.parse(
|
|
3255
|
+
JSON.parse(readFileSync9(AUTH_PATH2, "utf8"));
|
|
3858
3256
|
} catch {
|
|
3859
3257
|
}
|
|
3860
3258
|
}
|
|
@@ -3937,101 +3335,49 @@ async function handleInbound(msg, sessions) {
|
|
|
3937
3335
|
import { defineCommand as defineCommand30 } from "citty";
|
|
3938
3336
|
import consola26 from "consola";
|
|
3939
3337
|
|
|
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
3338
|
// src/lib/keygen.ts
|
|
3993
|
-
import { Buffer as
|
|
3994
|
-
import { existsSync as
|
|
3339
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
3340
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
3995
3341
|
import { generateKeyPairSync } from "crypto";
|
|
3996
3342
|
import { homedir as homedir9 } from "os";
|
|
3997
|
-
import { dirname, resolve as resolve2 } from "path";
|
|
3343
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
3998
3344
|
import { generateX25519KeyPair } from "@openape/core";
|
|
3999
3345
|
function resolveKeyPath(p) {
|
|
4000
3346
|
return resolve2(p.replace(/^~/, homedir9()));
|
|
4001
3347
|
}
|
|
4002
3348
|
function buildSshEd25519Line(rawPub) {
|
|
4003
3349
|
const keyTypeStr = "ssh-ed25519";
|
|
4004
|
-
const keyTypeLen =
|
|
3350
|
+
const keyTypeLen = Buffer3.alloc(4);
|
|
4005
3351
|
keyTypeLen.writeUInt32BE(keyTypeStr.length);
|
|
4006
|
-
const pubKeyLen =
|
|
3352
|
+
const pubKeyLen = Buffer3.alloc(4);
|
|
4007
3353
|
pubKeyLen.writeUInt32BE(rawPub.length);
|
|
4008
|
-
const blob =
|
|
3354
|
+
const blob = Buffer3.concat([keyTypeLen, Buffer3.from(keyTypeStr), pubKeyLen, rawPub]);
|
|
4009
3355
|
return `ssh-ed25519 ${blob.toString("base64")}`;
|
|
4010
3356
|
}
|
|
4011
3357
|
function readPublicKey(keyPath) {
|
|
4012
3358
|
const pubPath = `${keyPath}.pub`;
|
|
4013
|
-
if (
|
|
4014
|
-
return
|
|
3359
|
+
if (existsSync11(pubPath)) {
|
|
3360
|
+
return readFileSync10(pubPath, "utf-8").trim();
|
|
4015
3361
|
}
|
|
4016
|
-
const keyContent =
|
|
3362
|
+
const keyContent = readFileSync10(keyPath, "utf-8");
|
|
4017
3363
|
const privateKey = loadEd25519PrivateKey(keyContent);
|
|
4018
3364
|
const jwk = privateKey.export({ format: "jwk" });
|
|
4019
|
-
const pubBytes =
|
|
3365
|
+
const pubBytes = Buffer3.from(jwk.x, "base64url");
|
|
4020
3366
|
return buildSshEd25519Line(pubBytes);
|
|
4021
3367
|
}
|
|
4022
3368
|
function generateAndSaveKey(keyPath) {
|
|
4023
3369
|
const resolved = resolveKeyPath(keyPath);
|
|
4024
|
-
const dir =
|
|
4025
|
-
if (!
|
|
3370
|
+
const dir = dirname2(resolved);
|
|
3371
|
+
if (!existsSync11(dir)) {
|
|
4026
3372
|
mkdirSync3(dir, { recursive: true });
|
|
4027
3373
|
}
|
|
4028
3374
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
4029
3375
|
const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
4030
|
-
|
|
3376
|
+
writeFileSync5(resolved, privatePem, { mode: 384 });
|
|
4031
3377
|
const jwk = publicKey.export({ format: "jwk" });
|
|
4032
|
-
const pubBytes =
|
|
3378
|
+
const pubBytes = Buffer3.from(jwk.x, "base64url");
|
|
4033
3379
|
const pubKeyStr = buildSshEd25519Line(pubBytes);
|
|
4034
|
-
|
|
3380
|
+
writeFileSync5(`${resolved}.pub`, `${pubKeyStr}
|
|
4035
3381
|
`, { mode: 420 });
|
|
4036
3382
|
return pubKeyStr;
|
|
4037
3383
|
}
|
|
@@ -4039,7 +3385,7 @@ function generateKeyPairInMemory() {
|
|
|
4039
3385
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
4040
3386
|
const privatePem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
4041
3387
|
const jwk = publicKey.export({ format: "jwk" });
|
|
4042
|
-
const pubBytes =
|
|
3388
|
+
const pubBytes = Buffer3.from(jwk.x, "base64url");
|
|
4043
3389
|
const enc = generateX25519KeyPair();
|
|
4044
3390
|
return {
|
|
4045
3391
|
privatePem,
|
|
@@ -4049,145 +3395,8 @@ function generateKeyPairInMemory() {
|
|
|
4049
3395
|
};
|
|
4050
3396
|
}
|
|
4051
3397
|
|
|
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
3398
|
// src/commands/agents/spawn.ts
|
|
4190
|
-
function
|
|
3399
|
+
function readUidOrNull(name) {
|
|
4191
3400
|
try {
|
|
4192
3401
|
const u = getHostPlatform().readAgentUser(name);
|
|
4193
3402
|
return u?.uid ?? null;
|
|
@@ -4198,30 +3407,22 @@ function readMacOSUidOrNull(name) {
|
|
|
4198
3407
|
var spawnAgentCommand = defineCommand30({
|
|
4199
3408
|
meta: {
|
|
4200
3409
|
name: "spawn",
|
|
4201
|
-
description: "Provision a local
|
|
3410
|
+
description: "Provision a local Linux agent end-to-end (OS user, keypair, IdP agent, Claude hook)"
|
|
4202
3411
|
},
|
|
4203
3412
|
args: {
|
|
4204
3413
|
name: {
|
|
4205
3414
|
type: "positional",
|
|
4206
|
-
description: "Agent name \u2014 also the
|
|
3415
|
+
description: "Agent name \u2014 also the Linux username (lowercase, [a-z0-9-], must start with a letter)",
|
|
4207
3416
|
required: true
|
|
4208
3417
|
},
|
|
4209
3418
|
shell: {
|
|
4210
3419
|
type: "string",
|
|
4211
|
-
description: "Login shell for the
|
|
3420
|
+
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
3421
|
},
|
|
4213
3422
|
"no-claude-hook": {
|
|
4214
3423
|
type: "boolean",
|
|
4215
3424
|
description: "Skip writing ~/.claude/settings.json + the Bash-rewrite hook"
|
|
4216
3425
|
},
|
|
4217
|
-
"claude-token": {
|
|
4218
|
-
type: "string",
|
|
4219
|
-
description: "Claude Code OAuth token (sk-ant-oat01-\u2026) from `claude setup-token`. Visible to ps \u2014 prefer --claude-token-stdin in scripts."
|
|
4220
|
-
},
|
|
4221
|
-
"claude-token-stdin": {
|
|
4222
|
-
type: "boolean",
|
|
4223
|
-
description: "Read the Claude Code OAuth token from stdin (paranoid form of --claude-token)."
|
|
4224
|
-
},
|
|
4225
3426
|
"no-bridge": {
|
|
4226
3427
|
type: "boolean",
|
|
4227
3428
|
description: "Skip the ape-agent runtime install. Default behaviour installs the runtime so the agent answers chat.openape.ai messages (reads LITELLM_API_KEY/BASE_URL from ~/litellm/.env; override via --bridge-key / --bridge-base-url). Use --no-bridge for headless / CI / IdP-only account provisioning where the agent will not run a chat loop."
|
|
@@ -4237,6 +3438,18 @@ var spawnAgentCommand = defineCommand30({
|
|
|
4237
3438
|
"bridge-model": {
|
|
4238
3439
|
type: "string",
|
|
4239
3440
|
description: "Model the bridge sends in chat-completion requests (default: claude-haiku-4-5). Override when fronting a proxy that doesn't route the default \u2014 e.g. ChatGPT-only proxy needs `gpt-5.4`."
|
|
3441
|
+
},
|
|
3442
|
+
"kind": {
|
|
3443
|
+
type: "string",
|
|
3444
|
+
description: `Agent kind: "user" (default, connects to troop-chat) or "service" (polls an SP backend's task queue and runs each task through the LLM). With "service", --serves is required.`
|
|
3445
|
+
},
|
|
3446
|
+
"serves": {
|
|
3447
|
+
type: "string",
|
|
3448
|
+
description: "For --kind service: base URL of the SP backend this agent serves (e.g. https://zaz.delta-mind.at or http://127.0.0.1:3013). The agent pulls GetNextTask / posts ResolveTask there."
|
|
3449
|
+
},
|
|
3450
|
+
"poll-interval": {
|
|
3451
|
+
type: "string",
|
|
3452
|
+
description: "For --kind service: idle poll interval in ms (default 2000)."
|
|
4240
3453
|
}
|
|
4241
3454
|
},
|
|
4242
3455
|
async run({ args }) {
|
|
@@ -4246,11 +3459,13 @@ var spawnAgentCommand = defineCommand30({
|
|
|
4246
3459
|
`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
3460
|
);
|
|
4248
3461
|
}
|
|
4249
|
-
if (
|
|
4250
|
-
throw new CliError(
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
3462
|
+
if (args.kind != null && args.kind !== "user" && args.kind !== "service")
|
|
3463
|
+
throw new CliError(`Invalid --kind "${String(args.kind)}". Must be "user" or "service".`);
|
|
3464
|
+
const isService = args.kind === "service";
|
|
3465
|
+
const servesUrl = typeof args.serves === "string" ? args.serves.replace(/\/$/, "") : void 0;
|
|
3466
|
+
if (isService && !servesUrl)
|
|
3467
|
+
throw new CliError("--kind service requires --serves <SP base URL> (e.g. https://zaz.delta-mind.at).");
|
|
3468
|
+
const pollMs = typeof args["poll-interval"] === "string" ? Number.parseInt(args["poll-interval"], 10) : void 0;
|
|
4254
3469
|
const auth = loadAuth();
|
|
4255
3470
|
if (!auth) {
|
|
4256
3471
|
throw new CliError("Not authenticated. Run `apes login` first.");
|
|
@@ -4259,177 +3474,179 @@ var spawnAgentCommand = defineCommand30({
|
|
|
4259
3474
|
if (!idp) {
|
|
4260
3475
|
throw new CliError("No IdP URL configured. Run `apes login` first.");
|
|
4261
3476
|
}
|
|
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
|
-
}
|
|
3477
|
+
const loginShell = (args.shell ?? "/bin/bash").toString();
|
|
4280
3478
|
const platform = getHostPlatform();
|
|
4281
|
-
const
|
|
4282
|
-
const existing = platform.readAgentUser(
|
|
3479
|
+
const osUsername = platform.agentUsername(name);
|
|
3480
|
+
const existing = platform.readAgentUser(osUsername);
|
|
4283
3481
|
if (existing) {
|
|
4284
|
-
throw new CliError(`
|
|
3482
|
+
throw new CliError(`OS user "${existing.name}" already exists (uid=${existing.uid ?? "?"}). Refusing to overwrite.`);
|
|
3483
|
+
}
|
|
3484
|
+
const homeDir = `/var/lib/openape/homes/${osUsername}`;
|
|
3485
|
+
consola26.start(`Generating keypair for ${name}\u2026`);
|
|
3486
|
+
const { privatePem, publicSshLine, x25519PrivateKey, x25519PublicKey } = generateKeyPairInMemory();
|
|
3487
|
+
consola26.start(`Registering agent at ${idp}\u2026`);
|
|
3488
|
+
const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
|
|
3489
|
+
consola26.success(`Registered as ${registration.email}`);
|
|
3490
|
+
consola26.start("Issuing agent access token\u2026");
|
|
3491
|
+
const { token, expiresIn } = await issueAgentToken({
|
|
3492
|
+
idp,
|
|
3493
|
+
agentEmail: registration.email,
|
|
3494
|
+
privateKeyPem: privatePem
|
|
3495
|
+
});
|
|
3496
|
+
const authJson = buildAgentAuthJson({
|
|
3497
|
+
idp,
|
|
3498
|
+
accessToken: token,
|
|
3499
|
+
email: registration.email,
|
|
3500
|
+
expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
|
|
3501
|
+
keyPath: `${homeDir}/.ssh/id_ed25519`,
|
|
3502
|
+
// The IdP resolves the owner transitively (when the caller
|
|
3503
|
+
// is itself an agent — e.g. a Nest spawning a child — the
|
|
3504
|
+
// human at the top of the chain becomes owner). Use the
|
|
3505
|
+
// server-resolved owner, not the local caller's auth.email,
|
|
3506
|
+
// otherwise the agent's auth.json will carry the Nest's
|
|
3507
|
+
// email and troop will reject sync calls because the
|
|
3508
|
+
// encoded owner-domain in the agent email doesn't match
|
|
3509
|
+
// the auth.json's owner_email domain.
|
|
3510
|
+
ownerEmail: registration.owner
|
|
3511
|
+
});
|
|
3512
|
+
const includeClaudeHook = !args["no-claude-hook"];
|
|
3513
|
+
const withBridge = !args["no-bridge"];
|
|
3514
|
+
const script = buildSpawnSetupScript({
|
|
3515
|
+
name,
|
|
3516
|
+
homeDir,
|
|
3517
|
+
shellPath: loginShell,
|
|
3518
|
+
privateKeyPem: privatePem,
|
|
3519
|
+
publicKeySshLine: publicSshLine,
|
|
3520
|
+
x25519PrivateKey,
|
|
3521
|
+
x25519PublicKey,
|
|
3522
|
+
authJson,
|
|
3523
|
+
claudeSettingsJson: includeClaudeHook ? CLAUDE_SETTINGS_JSON : null,
|
|
3524
|
+
hookScriptSource: includeClaudeHook ? BASH_VIA_APE_SHELL_HOOK_SOURCE : null
|
|
3525
|
+
});
|
|
3526
|
+
consola26.start("Running privileged setup\u2026");
|
|
3527
|
+
if (process.getuid?.() !== 0) {
|
|
3528
|
+
consola26.info("You will be asked to approve the as=root grant in your DDISA inbox; this command blocks until you do.");
|
|
4285
3529
|
}
|
|
4286
|
-
|
|
3530
|
+
await platform.runPrivilegedBash(script);
|
|
4287
3531
|
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({
|
|
3532
|
+
const uid = readUidOrNull(osUsername) ?? -1;
|
|
3533
|
+
upsertNestAgent({
|
|
4345
3534
|
name,
|
|
4346
|
-
|
|
4347
|
-
homeDir,
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
3535
|
+
uid,
|
|
3536
|
+
home: homeDir,
|
|
3537
|
+
email: registration.email,
|
|
3538
|
+
registeredAt: Math.floor(Date.now() / 1e3),
|
|
3539
|
+
kind: isService ? "service" : void 0,
|
|
3540
|
+
service: isService && servesUrl ? { spBaseUrl: servesUrl, pollIntervalMs: pollMs != null && Number.isFinite(pollMs) && pollMs > 0 ? pollMs : void 0 } : void 0,
|
|
3541
|
+
// Service agents also carry bridge config — it's the LLM endpoint the
|
|
3542
|
+
// worker forwards to (baseUrl/key/model from --bridge-*).
|
|
3543
|
+
bridge: withBridge || isService ? {
|
|
3544
|
+
baseUrl: typeof args["bridge-base-url"] === "string" ? args["bridge-base-url"] : void 0,
|
|
3545
|
+
apiKey: typeof args["bridge-key"] === "string" ? args["bridge-key"] : void 0,
|
|
3546
|
+
model: typeof args["bridge-model"] === "string" ? args["bridge-model"] : void 0
|
|
3547
|
+
} : void 0
|
|
4359
3548
|
});
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
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 {
|
|
3549
|
+
} catch (err) {
|
|
3550
|
+
consola26.warn(`Could not write to nest registry: ${err instanceof Error ? err.message : String(err)}`);
|
|
3551
|
+
}
|
|
3552
|
+
consola26.success(`Agent ${name} spawned.`);
|
|
3553
|
+
consola26.info(`\u{1F517} Troop: https://troop.openape.ai/agents/${name}`);
|
|
3554
|
+
if (withBridge) {
|
|
3555
|
+
consola26.info(`On first boot, the bridge will send you a contact request from ${registration.email}.`);
|
|
3556
|
+
consola26.info("Open chat.openape.ai and accept it to start chatting with the agent.");
|
|
4392
3557
|
}
|
|
3558
|
+
console.log("");
|
|
3559
|
+
console.log("Run as the agent with:");
|
|
3560
|
+
console.log(` apes run --as ${name} -- claude --session-name ${name} --dangerously-skip-permissions`);
|
|
4393
3561
|
}
|
|
4394
3562
|
});
|
|
4395
|
-
async function resolveClaudeToken(opts) {
|
|
4396
|
-
if (opts.flag && opts.fromStdin) {
|
|
4397
|
-
throw new CliError("Pass --claude-token OR --claude-token-stdin, not both.");
|
|
4398
|
-
}
|
|
4399
|
-
let raw = null;
|
|
4400
|
-
if (typeof opts.flag === "string") {
|
|
4401
|
-
raw = opts.flag.trim();
|
|
4402
|
-
} else if (opts.fromStdin) {
|
|
4403
|
-
const chunks = [];
|
|
4404
|
-
for await (const chunk of process.stdin) {
|
|
4405
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
4406
|
-
}
|
|
4407
|
-
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
4408
|
-
}
|
|
4409
|
-
if (!raw) return null;
|
|
4410
|
-
if (!raw.startsWith("sk-ant-oat01-")) {
|
|
4411
|
-
throw new CliError(
|
|
4412
|
-
`Claude token doesn't look right (expected sk-ant-oat01-\u2026). Run \`claude setup-token\` and paste the resulting token.`
|
|
4413
|
-
);
|
|
4414
|
-
}
|
|
4415
|
-
return raw;
|
|
4416
|
-
}
|
|
4417
3563
|
|
|
4418
3564
|
// src/commands/agents/sync.ts
|
|
4419
|
-
import { chownSync, existsSync as
|
|
4420
|
-
import { homedir as
|
|
4421
|
-
import { join as
|
|
3565
|
+
import { chownSync, existsSync as existsSync13, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync12, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
3566
|
+
import { homedir as homedir10 } from "os";
|
|
3567
|
+
import { join as join9 } from "path";
|
|
4422
3568
|
import { defineCommand as defineCommand31 } from "citty";
|
|
3569
|
+
import consola28 from "consola";
|
|
3570
|
+
|
|
3571
|
+
// src/lib/recipe-checkout.ts
|
|
3572
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
3573
|
+
import { cpSync, existsSync as existsSync12, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
3574
|
+
import { join as join8, resolve as resolve3, sep } from "path";
|
|
4423
3575
|
import consola27 from "consola";
|
|
4424
|
-
var
|
|
4425
|
-
var
|
|
3576
|
+
var MARKER_FILE = ".recipe-ref";
|
|
3577
|
+
var GITHUB_PREFIX = "github.com/";
|
|
3578
|
+
function ensureRecipeCheckout(recipeRef, recipeDir, exec2 = execFileSync7) {
|
|
3579
|
+
const at = recipeRef.lastIndexOf("@");
|
|
3580
|
+
if (at <= 0) {
|
|
3581
|
+
consola27.warn(`recipe: malformed recipeRef "${recipeRef}" (expected <owner>/<name>[/<subdir>]@<ref>) \u2014 skipping checkout`);
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
const spec = recipeRef.slice(0, at);
|
|
3585
|
+
const ref = recipeRef.slice(at + 1);
|
|
3586
|
+
const slug = spec.startsWith(GITHUB_PREFIX) ? spec.slice(GITHUB_PREFIX.length) : spec;
|
|
3587
|
+
if (!slug || !ref) {
|
|
3588
|
+
consola27.warn(`recipe: malformed recipeRef "${recipeRef}" (empty slug or ref) \u2014 skipping checkout`);
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
const segments = slug.split("/").filter(Boolean);
|
|
3592
|
+
if (segments.length < 2) {
|
|
3593
|
+
consola27.warn(`recipe: malformed recipeRef "${recipeRef}" (expected <owner>/<name>[/<subdir>]@<ref>) \u2014 skipping checkout`);
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
if (segments.some((s) => s === "." || s === ".." || s.includes("\\"))) {
|
|
3597
|
+
consola27.warn(`recipe: unsafe path segment in recipeRef "${recipeRef}" \u2014 skipping checkout`);
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
const repoSlug = segments.slice(0, 2).join("/");
|
|
3601
|
+
const subdir = segments.slice(2).join("/");
|
|
3602
|
+
const markerPath = join8(recipeDir, MARKER_FILE);
|
|
3603
|
+
if (existsSync12(markerPath)) {
|
|
3604
|
+
try {
|
|
3605
|
+
if (readFileSync11(markerPath, "utf8") === recipeRef) return;
|
|
3606
|
+
} catch {
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
const cloneUrl = `https://github.com/${repoSlug}`;
|
|
3610
|
+
const staging = subdir ? `${recipeDir}.checkout` : recipeDir;
|
|
3611
|
+
try {
|
|
3612
|
+
rmSync2(recipeDir, { recursive: true, force: true });
|
|
3613
|
+
if (subdir)
|
|
3614
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
3615
|
+
exec2("git", ["clone", "--depth", "1", "--branch", ref, cloneUrl, staging]);
|
|
3616
|
+
if (subdir) {
|
|
3617
|
+
const src = join8(staging, subdir);
|
|
3618
|
+
if (!resolve3(src).startsWith(resolve3(staging) + sep)) {
|
|
3619
|
+
consola27.warn(`recipe: subdirectory "${subdir}" escapes the checkout dir \u2014 skipping`);
|
|
3620
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
if (!existsSync12(src)) {
|
|
3624
|
+
consola27.warn(`recipe: subdirectory "${subdir}" not found in ${repoSlug}@${ref} \u2014 skipping checkout`);
|
|
3625
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
cpSync(src, recipeDir, { recursive: true });
|
|
3629
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
3630
|
+
}
|
|
3631
|
+
writeFileSync6(markerPath, recipeRef);
|
|
3632
|
+
consola27.info(`recipe: checked out ${slug}@${ref} \u2192 ${recipeDir}`);
|
|
3633
|
+
} catch (err) {
|
|
3634
|
+
consola27.warn(`recipe: checkout of ${slug}@${ref} failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
3635
|
+
if (subdir)
|
|
3636
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
// src/commands/agents/sync.ts
|
|
3641
|
+
var AUTH_PATH3 = join9(homedir10(), ".config", "apes", "auth.json");
|
|
3642
|
+
var TASK_CACHE_DIR2 = join9(homedir10(), ".openape", "agent", "tasks");
|
|
4426
3643
|
function readAuthJson() {
|
|
4427
|
-
if (!
|
|
3644
|
+
if (!existsSync13(AUTH_PATH3)) {
|
|
4428
3645
|
throw new CliError(
|
|
4429
3646
|
`No agent auth found at ${AUTH_PATH3}. Run \`apes agents spawn <name>\` to provision an agent first.`
|
|
4430
3647
|
);
|
|
4431
3648
|
}
|
|
4432
|
-
const raw =
|
|
3649
|
+
const raw = readFileSync12(AUTH_PATH3, "utf8");
|
|
4433
3650
|
let parsed;
|
|
4434
3651
|
try {
|
|
4435
3652
|
parsed = JSON.parse(raw);
|
|
@@ -4478,7 +3695,7 @@ var syncAgentCommand = defineCommand31({
|
|
|
4478
3695
|
if (!auth.owner_email) {
|
|
4479
3696
|
throw new CliError(`${AUTH_PATH3} is missing owner_email \u2014 re-run \`apes agents spawn\` to update.`);
|
|
4480
3697
|
}
|
|
4481
|
-
|
|
3698
|
+
consola28.start(`Syncing ${agentName} (${host}, hostId ${hostId.slice(0, 8)}\u2026) with ${troopUrl}`);
|
|
4482
3699
|
const pubkeyX25519 = readAgentEncryptionPublicKey() ?? void 0;
|
|
4483
3700
|
const sync = await client.sync({
|
|
4484
3701
|
hostname: host,
|
|
@@ -4486,17 +3703,17 @@ var syncAgentCommand = defineCommand31({
|
|
|
4486
3703
|
ownerEmail: auth.owner_email,
|
|
4487
3704
|
...pubkeyX25519 ? { pubkeyX25519 } : {}
|
|
4488
3705
|
});
|
|
4489
|
-
|
|
4490
|
-
if (!pubkeyX25519)
|
|
4491
|
-
const { system_prompt: systemPrompt, tools, skills, tasks } = await client.listTasks();
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
3706
|
+
consola28.info(sync.first_sync ? "\u2713 first sync \u2014 agent registered" : "\u2713 presence updated");
|
|
3707
|
+
if (!pubkeyX25519) consola28.warn("No X25519 public key found on disk \u2014 sealed capability secrets cannot be bound until the agent is re-spawned.");
|
|
3708
|
+
const { system_prompt: systemPrompt, tools, skills, tasks, recipe_ref: recipeRef } = await client.listTasks();
|
|
3709
|
+
consola28.info(`Pulled ${tasks.length} task${tasks.length === 1 ? "" : "s"}`);
|
|
3710
|
+
consola28.info(`Tools enabled: ${tools.length === 0 ? "(none)" : tools.join(", ")}`);
|
|
3711
|
+
consola28.info(`Skills: ${skills.length === 0 ? "(none)" : skills.map((s) => s.name).join(", ")}`);
|
|
4495
3712
|
let agentUid = null;
|
|
4496
3713
|
let agentGid = null;
|
|
4497
3714
|
if (process.geteuid?.() === 0) {
|
|
4498
3715
|
try {
|
|
4499
|
-
const homeStat =
|
|
3716
|
+
const homeStat = statSync2(homedir10());
|
|
4500
3717
|
agentUid = homeStat.uid;
|
|
4501
3718
|
agentGid = homeStat.gid;
|
|
4502
3719
|
} catch {
|
|
@@ -4510,27 +3727,28 @@ var syncAgentCommand = defineCommand31({
|
|
|
4510
3727
|
}
|
|
4511
3728
|
}
|
|
4512
3729
|
}
|
|
4513
|
-
const agentDir =
|
|
3730
|
+
const agentDir = join9(homedir10(), ".openape", "agent");
|
|
4514
3731
|
mkdirSync4(agentDir, { recursive: true });
|
|
4515
|
-
chownToAgent(
|
|
3732
|
+
chownToAgent(join9(homedir10(), ".openape"));
|
|
4516
3733
|
chownToAgent(agentDir);
|
|
4517
|
-
const agentJsonPath =
|
|
4518
|
-
|
|
3734
|
+
const agentJsonPath = join9(agentDir, "agent.json");
|
|
3735
|
+
writeFileSync7(
|
|
4519
3736
|
agentJsonPath,
|
|
4520
|
-
`${JSON.stringify({ systemPrompt, tools }, null, 2)}
|
|
3737
|
+
`${JSON.stringify({ systemPrompt, tools, ...recipeRef ? { recipeRef } : {} }, null, 2)}
|
|
4521
3738
|
`,
|
|
4522
3739
|
{ mode: 384 }
|
|
4523
3740
|
);
|
|
4524
3741
|
chownToAgent(agentJsonPath);
|
|
3742
|
+
if (recipeRef) ensureRecipeCheckout(recipeRef, join9(homedir10(), "recipe"));
|
|
4525
3743
|
mkdirSync4(TASK_CACHE_DIR2, { recursive: true });
|
|
4526
3744
|
chownToAgent(TASK_CACHE_DIR2);
|
|
4527
3745
|
for (const task of tasks) {
|
|
4528
|
-
const path2 =
|
|
4529
|
-
|
|
3746
|
+
const path2 = join9(TASK_CACHE_DIR2, `${task.taskId}.json`);
|
|
3747
|
+
writeFileSync7(path2, `${JSON.stringify(task, null, 2)}
|
|
4530
3748
|
`, { mode: 384 });
|
|
4531
3749
|
chownToAgent(path2);
|
|
4532
3750
|
}
|
|
4533
|
-
const skillsDir =
|
|
3751
|
+
const skillsDir = join9(agentDir, "skills");
|
|
4534
3752
|
mkdirSync4(skillsDir, { recursive: true });
|
|
4535
3753
|
chownToAgent(skillsDir);
|
|
4536
3754
|
const incomingNames = new Set(skills.map((s) => s.name));
|
|
@@ -4538,22 +3756,22 @@ var syncAgentCommand = defineCommand31({
|
|
|
4538
3756
|
for (const entry of readdirSync2(skillsDir)) {
|
|
4539
3757
|
if (incomingNames.has(entry)) continue;
|
|
4540
3758
|
try {
|
|
4541
|
-
|
|
3759
|
+
rmSync3(join9(skillsDir, entry), { recursive: true, force: true });
|
|
4542
3760
|
} catch {
|
|
4543
3761
|
}
|
|
4544
3762
|
}
|
|
4545
3763
|
} catch {
|
|
4546
3764
|
}
|
|
4547
3765
|
for (const skill of skills) {
|
|
4548
|
-
const skillDir =
|
|
3766
|
+
const skillDir = join9(skillsDir, skill.name);
|
|
4549
3767
|
mkdirSync4(skillDir, { recursive: true });
|
|
4550
3768
|
chownToAgent(skillDir);
|
|
4551
|
-
const skillPath =
|
|
4552
|
-
|
|
3769
|
+
const skillPath = join9(skillDir, "SKILL.md");
|
|
3770
|
+
writeFileSync7(skillPath, skill.body.endsWith("\n") ? skill.body : `${skill.body}
|
|
4553
3771
|
`, { mode: 384 });
|
|
4554
3772
|
chownToAgent(skillPath);
|
|
4555
3773
|
}
|
|
4556
|
-
|
|
3774
|
+
consola28.success("Sync complete.");
|
|
4557
3775
|
}
|
|
4558
3776
|
});
|
|
4559
3777
|
|
|
@@ -4581,21 +3799,21 @@ var agentsCommand = defineCommand32({
|
|
|
4581
3799
|
import { defineCommand as defineCommand40 } from "citty";
|
|
4582
3800
|
|
|
4583
3801
|
// src/commands/nest/authorize.ts
|
|
4584
|
-
import { execFileSync as
|
|
4585
|
-
import { existsSync as
|
|
4586
|
-
import { join as
|
|
3802
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
3803
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
3804
|
+
import { join as join11 } from "path";
|
|
4587
3805
|
import { defineCommand as defineCommand34 } from "citty";
|
|
4588
|
-
import
|
|
3806
|
+
import consola30 from "consola";
|
|
4589
3807
|
|
|
4590
3808
|
// src/commands/nest/enroll.ts
|
|
4591
|
-
import { hostname as
|
|
4592
|
-
import { existsSync as
|
|
4593
|
-
import { join as
|
|
3809
|
+
import { hostname as hostname4, homedir as homedir11 } from "os";
|
|
3810
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync5, writeFileSync as writeFileSync8, chmodSync } from "fs";
|
|
3811
|
+
import { join as join10 } from "path";
|
|
4594
3812
|
import { defineCommand as defineCommand33 } from "citty";
|
|
4595
|
-
import
|
|
4596
|
-
var NEST_DATA_DIR =
|
|
3813
|
+
import consola29 from "consola";
|
|
3814
|
+
var NEST_DATA_DIR = join10(homedir11(), ".openape", "nest");
|
|
4597
3815
|
function nestAgentName() {
|
|
4598
|
-
const raw =
|
|
3816
|
+
const raw = hostname4().toLowerCase();
|
|
4599
3817
|
const head = raw.split(".")[0] ?? raw;
|
|
4600
3818
|
const safe = head.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
4601
3819
|
const trimmed = safe.slice(0, 16);
|
|
@@ -4626,25 +3844,25 @@ var enrollNestCommand = defineCommand33({
|
|
|
4626
3844
|
throw new CliError("Run `apes login <email>` first \u2014 nest enroll attaches the new identity to your owner account.");
|
|
4627
3845
|
}
|
|
4628
3846
|
const name = args.name || nestAgentName();
|
|
4629
|
-
const authPath =
|
|
4630
|
-
if (
|
|
3847
|
+
const authPath = join10(NEST_DATA_DIR, ".config", "apes", "auth.json");
|
|
3848
|
+
if (existsSync14(authPath) && !args.force) {
|
|
4631
3849
|
throw new CliError(`Nest already enrolled at ${authPath}. Pass --force to re-enroll.`);
|
|
4632
3850
|
}
|
|
4633
|
-
const sshDir =
|
|
4634
|
-
const configDir =
|
|
3851
|
+
const sshDir = join10(NEST_DATA_DIR, ".ssh");
|
|
3852
|
+
const configDir = join10(NEST_DATA_DIR, ".config", "apes");
|
|
4635
3853
|
mkdirSync5(sshDir, { recursive: true });
|
|
4636
3854
|
mkdirSync5(configDir, { recursive: true });
|
|
4637
|
-
|
|
3855
|
+
consola29.start(`Generating keypair for ${name}\u2026`);
|
|
4638
3856
|
const { privatePem, publicSshLine } = generateKeyPairInMemory();
|
|
4639
|
-
|
|
3857
|
+
writeFileSync8(join10(sshDir, "id_ed25519"), `${privatePem.trimEnd()}
|
|
4640
3858
|
`, { mode: 384 });
|
|
4641
|
-
|
|
3859
|
+
writeFileSync8(join10(sshDir, "id_ed25519.pub"), `${publicSshLine}
|
|
4642
3860
|
`, { mode: 420 });
|
|
4643
3861
|
chmodSync(sshDir, 448);
|
|
4644
|
-
|
|
3862
|
+
consola29.start(`Registering nest at ${idp}\u2026`);
|
|
4645
3863
|
const registration = await registerAgentAtIdp({ name, publicKey: publicSshLine, idp });
|
|
4646
|
-
|
|
4647
|
-
|
|
3864
|
+
consola29.success(`Registered as ${registration.email}`);
|
|
3865
|
+
consola29.start("Issuing nest access token\u2026");
|
|
4648
3866
|
const { token, expiresIn } = await issueAgentToken({
|
|
4649
3867
|
idp,
|
|
4650
3868
|
agentEmail: registration.email,
|
|
@@ -4655,16 +3873,16 @@ var enrollNestCommand = defineCommand33({
|
|
|
4655
3873
|
accessToken: token,
|
|
4656
3874
|
email: registration.email,
|
|
4657
3875
|
expiresAt: Math.floor(Date.now() / 1e3) + expiresIn,
|
|
4658
|
-
keyPath:
|
|
3876
|
+
keyPath: join10(sshDir, "id_ed25519"),
|
|
4659
3877
|
ownerEmail: ownerAuth.email
|
|
4660
3878
|
});
|
|
4661
|
-
|
|
3879
|
+
writeFileSync8(authPath, authJson, { mode: 384 });
|
|
4662
3880
|
chmodSync(configDir, 448);
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
3881
|
+
consola29.success(`Nest enrolled \u2014 auth.json at ${authPath}`);
|
|
3882
|
+
consola29.info("");
|
|
3883
|
+
consola29.info("Next: configure the YOLO-policy so the nest can spawn/destroy without prompts:");
|
|
3884
|
+
consola29.info("");
|
|
3885
|
+
consola29.info(" apes nest authorize");
|
|
4668
3886
|
}
|
|
4669
3887
|
});
|
|
4670
3888
|
|
|
@@ -4727,14 +3945,14 @@ var authorizeNestCommand = defineCommand34({
|
|
|
4727
3945
|
}
|
|
4728
3946
|
},
|
|
4729
3947
|
async run({ args }) {
|
|
4730
|
-
const nestAuthPath =
|
|
4731
|
-
if (!
|
|
3948
|
+
const nestAuthPath = join11(NEST_DATA_DIR, ".config", "apes", "auth.json");
|
|
3949
|
+
if (!existsSync15(nestAuthPath)) {
|
|
4732
3950
|
throw new CliError("Nest not enrolled. Run `apes nest enroll` first.");
|
|
4733
3951
|
}
|
|
4734
|
-
const nestAuth = JSON.parse(
|
|
3952
|
+
const nestAuth = JSON.parse(readFileSync13(nestAuthPath, "utf8"));
|
|
4735
3953
|
if (!nestAuth.email) throw new CliError(`${nestAuthPath} has no email`);
|
|
4736
3954
|
const allow = args.allow ?? DEFAULT_ALLOW_PATTERNS.join(",");
|
|
4737
|
-
|
|
3955
|
+
consola30.info(`Configuring YOLO-policy on ${nestAuth.email} via \`apes yolo set\`\u2026`);
|
|
4738
3956
|
const cmdArgs = [
|
|
4739
3957
|
"yolo",
|
|
4740
3958
|
"set",
|
|
@@ -4748,19 +3966,19 @@ var authorizeNestCommand = defineCommand34({
|
|
|
4748
3966
|
cmdArgs.push("--expires-in", args["expires-in"]);
|
|
4749
3967
|
}
|
|
4750
3968
|
try {
|
|
4751
|
-
|
|
3969
|
+
execFileSync8("apes", cmdArgs, { stdio: "inherit" });
|
|
4752
3970
|
} catch (err) {
|
|
4753
3971
|
throw new CliError(err instanceof Error ? err.message : String(err));
|
|
4754
3972
|
}
|
|
4755
|
-
|
|
4756
|
-
|
|
3973
|
+
consola30.success("Nest-driven agent lifecycle is now zero-prompt.");
|
|
3974
|
+
consola30.info("Test: apes agents spawn <name> via the nest API \u2192 no DDISA prompt.");
|
|
4757
3975
|
}
|
|
4758
3976
|
});
|
|
4759
3977
|
|
|
4760
3978
|
// src/commands/nest/destroy.ts
|
|
4761
|
-
import { execFileSync as
|
|
3979
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
4762
3980
|
import { defineCommand as defineCommand35 } from "citty";
|
|
4763
|
-
import
|
|
3981
|
+
import consola31 from "consola";
|
|
4764
3982
|
var destroyNestCommand = defineCommand35({
|
|
4765
3983
|
meta: {
|
|
4766
3984
|
name: "destroy",
|
|
@@ -4772,8 +3990,8 @@ var destroyNestCommand = defineCommand35({
|
|
|
4772
3990
|
async run({ args }) {
|
|
4773
3991
|
const name = String(args.name);
|
|
4774
3992
|
try {
|
|
4775
|
-
|
|
4776
|
-
|
|
3993
|
+
execFileSync9("apes", ["run", "--as", "root", "--wait", "--", "apes", "agents", "destroy", name, "--force"], { stdio: "inherit" });
|
|
3994
|
+
consola31.success(`Nest will tear down ${name}'s pm2 process on its next reconcile (\u22642s).`);
|
|
4777
3995
|
} catch (err) {
|
|
4778
3996
|
const status = err.status ?? 1;
|
|
4779
3997
|
throw new CliExit(status);
|
|
@@ -4782,11 +4000,11 @@ var destroyNestCommand = defineCommand35({
|
|
|
4782
4000
|
});
|
|
4783
4001
|
|
|
4784
4002
|
// src/commands/nest/install.ts
|
|
4785
|
-
import { existsSync as
|
|
4786
|
-
import { homedir as
|
|
4787
|
-
import { dirname as dirname3, join as
|
|
4003
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
|
|
4004
|
+
import { homedir as homedir12 } from "os";
|
|
4005
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
4788
4006
|
import { defineCommand as defineCommand36 } from "citty";
|
|
4789
|
-
import
|
|
4007
|
+
import consola32 from "consola";
|
|
4790
4008
|
|
|
4791
4009
|
// src/commands/nest/apes-agents-adapter.ts
|
|
4792
4010
|
var APES_AGENTS_ADAPTER_TOML = `schema = "openape-shapes/v1"
|
|
@@ -4852,41 +4070,41 @@ resource_chain = ["agents:name={name}", "allowlist:email={peer_email}"]
|
|
|
4852
4070
|
|
|
4853
4071
|
// src/commands/nest/install.ts
|
|
4854
4072
|
function installAdapter2() {
|
|
4855
|
-
const target =
|
|
4073
|
+
const target = join12(homedir12(), ".openape", "shapes", "adapters", "apes-agents.toml");
|
|
4856
4074
|
mkdirSync6(dirname3(target), { recursive: true });
|
|
4857
4075
|
let existing = "";
|
|
4858
4076
|
try {
|
|
4859
|
-
existing =
|
|
4077
|
+
existing = readFileSync14(target, "utf8");
|
|
4860
4078
|
} catch {
|
|
4861
4079
|
}
|
|
4862
4080
|
if (existing === APES_AGENTS_ADAPTER_TOML) return false;
|
|
4863
|
-
|
|
4864
|
-
|
|
4081
|
+
writeFileSync9(target, APES_AGENTS_ADAPTER_TOML, { mode: 420 });
|
|
4082
|
+
consola32.success(`Wrote shapes adapter ${target}`);
|
|
4865
4083
|
return true;
|
|
4866
4084
|
}
|
|
4867
4085
|
function writeBridgeModelDefault(model) {
|
|
4868
|
-
for (const envDir of [
|
|
4869
|
-
const envFile =
|
|
4086
|
+
for (const envDir of [join12(homedir12(), "litellm"), join12(NEST_DATA_DIR, "litellm")]) {
|
|
4087
|
+
const envFile = join12(envDir, ".env");
|
|
4870
4088
|
mkdirSync6(envDir, { recursive: true });
|
|
4871
4089
|
let lines = [];
|
|
4872
|
-
if (
|
|
4873
|
-
lines =
|
|
4090
|
+
if (existsSync16(envFile)) {
|
|
4091
|
+
lines = readFileSync14(envFile, "utf8").split("\n").filter((l) => !l.startsWith("APE_CHAT_BRIDGE_MODEL="));
|
|
4874
4092
|
}
|
|
4875
4093
|
lines.push(`APE_CHAT_BRIDGE_MODEL=${model}`);
|
|
4876
4094
|
while (lines.length > 0 && lines.at(-1).trim() === "") lines.pop();
|
|
4877
|
-
|
|
4095
|
+
writeFileSync9(envFile, `${lines.join("\n")}
|
|
4878
4096
|
`, { mode: 384 });
|
|
4879
4097
|
}
|
|
4880
4098
|
}
|
|
4881
4099
|
function findBinary(name) {
|
|
4882
4100
|
for (const dir of [
|
|
4883
|
-
|
|
4101
|
+
join12(homedir12(), ".bun", "bin"),
|
|
4884
4102
|
"/opt/homebrew/bin",
|
|
4885
4103
|
"/usr/local/bin",
|
|
4886
4104
|
"/usr/bin"
|
|
4887
4105
|
]) {
|
|
4888
|
-
const p =
|
|
4889
|
-
if (
|
|
4106
|
+
const p = join12(dir, name);
|
|
4107
|
+
if (existsSync16(p)) return p;
|
|
4890
4108
|
}
|
|
4891
4109
|
throw new Error(`could not locate ${name} on PATH; install it first`);
|
|
4892
4110
|
}
|
|
@@ -4906,20 +4124,20 @@ var installNestCommand = defineCommand36({
|
|
|
4906
4124
|
}
|
|
4907
4125
|
},
|
|
4908
4126
|
async run({ args }) {
|
|
4909
|
-
const homeDir =
|
|
4127
|
+
const homeDir = homedir12();
|
|
4910
4128
|
const port = Number(args.port ?? 9091);
|
|
4911
4129
|
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
4912
4130
|
throw new Error(`invalid port ${port}`);
|
|
4913
4131
|
}
|
|
4914
4132
|
const nestBin = findBinary("openape-nest");
|
|
4915
4133
|
const apesBin = findBinary("apes");
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4134
|
+
consola32.info(`Installing nest supervisor`);
|
|
4135
|
+
consola32.info(` nest binary: ${nestBin}`);
|
|
4136
|
+
consola32.info(` apes binary: ${apesBin}`);
|
|
4137
|
+
consola32.info(` HTTP port: ${port}`);
|
|
4920
4138
|
if (typeof args["bridge-model"] === "string" && args["bridge-model"]) {
|
|
4921
4139
|
writeBridgeModelDefault(args["bridge-model"]);
|
|
4922
|
-
|
|
4140
|
+
consola32.success(`Default bridge model set to ${args["bridge-model"]} (in ~/litellm/.env)`);
|
|
4923
4141
|
}
|
|
4924
4142
|
installAdapter2();
|
|
4925
4143
|
mkdirSync6(NEST_DATA_DIR, { recursive: true });
|
|
@@ -4930,20 +4148,20 @@ var installNestCommand = defineCommand36({
|
|
|
4930
4148
|
nestHome: NEST_DATA_DIR,
|
|
4931
4149
|
port
|
|
4932
4150
|
});
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4151
|
+
consola32.success(`Nest daemon bootstrapped \u2014 http://127.0.0.1:${port}`);
|
|
4152
|
+
consola32.info("");
|
|
4153
|
+
consola32.info("Next steps for zero-prompt spawn \u2014 both one-time:");
|
|
4154
|
+
consola32.info("");
|
|
4155
|
+
consola32.info(" 1. apes nest enroll # register nest as DDISA agent (creates own auth.json)");
|
|
4156
|
+
consola32.info(" 2. apes nest authorize # set YOLO-policy on the nest agent");
|
|
4157
|
+
consola32.info("");
|
|
4158
|
+
consola32.info("After that, every `POST http://127.0.0.1:9091/agents` runs without DDISA prompts.");
|
|
4941
4159
|
}
|
|
4942
4160
|
});
|
|
4943
4161
|
|
|
4944
4162
|
// src/commands/nest/list.ts
|
|
4945
4163
|
import { defineCommand as defineCommand37 } from "citty";
|
|
4946
|
-
import
|
|
4164
|
+
import consola33 from "consola";
|
|
4947
4165
|
var listNestCommand = defineCommand37({
|
|
4948
4166
|
meta: {
|
|
4949
4167
|
name: "list",
|
|
@@ -4959,21 +4177,21 @@ var listNestCommand = defineCommand37({
|
|
|
4959
4177
|
return;
|
|
4960
4178
|
}
|
|
4961
4179
|
if (reg.agents.length === 0) {
|
|
4962
|
-
|
|
4180
|
+
consola33.info("(no agents registered with this nest)");
|
|
4963
4181
|
return;
|
|
4964
4182
|
}
|
|
4965
|
-
|
|
4183
|
+
consola33.info(`${reg.agents.length} agent(s) registered with this nest:`);
|
|
4966
4184
|
for (const a of reg.agents) {
|
|
4967
4185
|
const bridge = a.bridge ? " bridge=on" : "";
|
|
4968
|
-
|
|
4186
|
+
consola33.info(` ${a.name.padEnd(16)} uid=${String(a.uid).padEnd(5)} home=${a.home}${bridge}`);
|
|
4969
4187
|
}
|
|
4970
4188
|
}
|
|
4971
4189
|
});
|
|
4972
4190
|
|
|
4973
4191
|
// src/commands/nest/spawn.ts
|
|
4974
|
-
import { execFileSync as
|
|
4192
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
4975
4193
|
import { defineCommand as defineCommand38 } from "citty";
|
|
4976
|
-
import
|
|
4194
|
+
import consola34 from "consola";
|
|
4977
4195
|
var spawnNestCommand = defineCommand38({
|
|
4978
4196
|
meta: {
|
|
4979
4197
|
name: "spawn",
|
|
@@ -5004,8 +4222,8 @@ var spawnNestCommand = defineCommand38({
|
|
|
5004
4222
|
if (typeof args["bridge-base-url"] === "string") apesArgs.push("--bridge-base-url", args["bridge-base-url"]);
|
|
5005
4223
|
if (typeof args["bridge-model"] === "string") apesArgs.push("--bridge-model", args["bridge-model"]);
|
|
5006
4224
|
try {
|
|
5007
|
-
|
|
5008
|
-
|
|
4225
|
+
execFileSync10("apes", apesArgs, { stdio: "inherit" });
|
|
4226
|
+
consola34.success(`Nest will pick up ${name} on its next reconcile (\u22642s).`);
|
|
5009
4227
|
} catch (err) {
|
|
5010
4228
|
const status = err.status ?? 1;
|
|
5011
4229
|
throw new CliExit(status);
|
|
@@ -5015,7 +4233,7 @@ var spawnNestCommand = defineCommand38({
|
|
|
5015
4233
|
|
|
5016
4234
|
// src/commands/nest/uninstall.ts
|
|
5017
4235
|
import { defineCommand as defineCommand39 } from "citty";
|
|
5018
|
-
import
|
|
4236
|
+
import consola35 from "consola";
|
|
5019
4237
|
var uninstallNestCommand = defineCommand39({
|
|
5020
4238
|
meta: {
|
|
5021
4239
|
name: "uninstall",
|
|
@@ -5023,8 +4241,8 @@ var uninstallNestCommand = defineCommand39({
|
|
|
5023
4241
|
},
|
|
5024
4242
|
async run() {
|
|
5025
4243
|
await getHostPlatform().uninstallNestSupervisor();
|
|
5026
|
-
|
|
5027
|
-
|
|
4244
|
+
consola35.success("Nest daemon stopped + supervisor unit removed");
|
|
4245
|
+
consola35.info("Registry at ~/.openape/nest/agents.json kept \u2014 re-run `apes nest install` to resume supervision.");
|
|
5028
4246
|
}
|
|
5029
4247
|
});
|
|
5030
4248
|
|
|
@@ -5050,7 +4268,7 @@ import { defineCommand as defineCommand44 } from "citty";
|
|
|
5050
4268
|
|
|
5051
4269
|
// src/commands/yolo/clear.ts
|
|
5052
4270
|
import { defineCommand as defineCommand41 } from "citty";
|
|
5053
|
-
import
|
|
4271
|
+
import consola36 from "consola";
|
|
5054
4272
|
var yoloClearCommand = defineCommand41({
|
|
5055
4273
|
meta: {
|
|
5056
4274
|
name: "clear",
|
|
@@ -5080,13 +4298,13 @@ var yoloClearCommand = defineCommand41({
|
|
|
5080
4298
|
const text = await res.text().catch(() => "");
|
|
5081
4299
|
throw new CliError(`DELETE /yolo-policy failed (${res.status}): ${text}`);
|
|
5082
4300
|
}
|
|
5083
|
-
|
|
4301
|
+
consola36.success(`YOLO-policy cleared on ${email}`);
|
|
5084
4302
|
}
|
|
5085
4303
|
});
|
|
5086
4304
|
|
|
5087
4305
|
// src/commands/yolo/set.ts
|
|
5088
4306
|
import { defineCommand as defineCommand42 } from "citty";
|
|
5089
|
-
import
|
|
4307
|
+
import consola37 from "consola";
|
|
5090
4308
|
var VALID_MODES = ["allow-list", "deny-list"];
|
|
5091
4309
|
var yoloSetCommand = defineCommand42({
|
|
5092
4310
|
meta: {
|
|
@@ -5136,12 +4354,12 @@ var yoloSetCommand = defineCommand42({
|
|
|
5136
4354
|
const denyPatterns = parseList(args.deny);
|
|
5137
4355
|
const denyRiskThreshold = args["deny-risk"] ?? null;
|
|
5138
4356
|
const expiresAt = parseExpiresIn(args["expires-in"]);
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
if (allowPatterns.length)
|
|
5142
|
-
if (denyPatterns.length)
|
|
5143
|
-
if (denyRiskThreshold)
|
|
5144
|
-
if (expiresAt)
|
|
4357
|
+
consola37.info(`Setting YOLO-policy on ${email}`);
|
|
4358
|
+
consola37.info(` mode: ${mode}`);
|
|
4359
|
+
if (allowPatterns.length) consola37.info(` allow_patterns: ${allowPatterns.join(", ")}`);
|
|
4360
|
+
if (denyPatterns.length) consola37.info(` deny_patterns: ${denyPatterns.join(", ")}`);
|
|
4361
|
+
if (denyRiskThreshold) consola37.info(` deny_risk: ${denyRiskThreshold}`);
|
|
4362
|
+
if (expiresAt) consola37.info(` expires_at: ${new Date(expiresAt * 1e3).toISOString()}`);
|
|
5145
4363
|
const url = `${idp}/api/users/${encodeURIComponent(email)}/yolo-policy`;
|
|
5146
4364
|
const res = await fetch(url, {
|
|
5147
4365
|
method: "PUT",
|
|
@@ -5161,7 +4379,7 @@ var yoloSetCommand = defineCommand42({
|
|
|
5161
4379
|
const text = await res.text().catch(() => "");
|
|
5162
4380
|
throw new CliError(`PUT /yolo-policy failed (${res.status}): ${text}`);
|
|
5163
4381
|
}
|
|
5164
|
-
|
|
4382
|
+
consola37.success(`YOLO-policy applied to ${email}`);
|
|
5165
4383
|
}
|
|
5166
4384
|
});
|
|
5167
4385
|
function parseList(s) {
|
|
@@ -5180,7 +4398,7 @@ function parseExpiresIn(s) {
|
|
|
5180
4398
|
|
|
5181
4399
|
// src/commands/yolo/show.ts
|
|
5182
4400
|
import { defineCommand as defineCommand43 } from "citty";
|
|
5183
|
-
import
|
|
4401
|
+
import consola38 from "consola";
|
|
5184
4402
|
var yoloShowCommand = defineCommand43({
|
|
5185
4403
|
meta: {
|
|
5186
4404
|
name: "show",
|
|
@@ -5218,12 +4436,12 @@ var yoloShowCommand = defineCommand43({
|
|
|
5218
4436
|
console.log(JSON.stringify(policy, null, 2));
|
|
5219
4437
|
return;
|
|
5220
4438
|
}
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
4439
|
+
consola38.info(`YOLO-policy for ${email}`);
|
|
4440
|
+
consola38.info(` mode: ${policy.mode}`);
|
|
4441
|
+
consola38.info(` allow_patterns: ${policy.allowPatterns.length ? policy.allowPatterns.join(", ") : "(none)"}`);
|
|
4442
|
+
consola38.info(` deny_patterns: ${policy.denyPatterns.length ? policy.denyPatterns.join(", ") : "(none)"}`);
|
|
4443
|
+
consola38.info(` deny_risk: ${policy.denyRiskThreshold ?? "(none)"}`);
|
|
4444
|
+
consola38.info(` expires_at: ${policy.expiresAt ? new Date(policy.expiresAt * 1e3).toISOString() : "(never)"}`);
|
|
5227
4445
|
}
|
|
5228
4446
|
});
|
|
5229
4447
|
|
|
@@ -5242,7 +4460,7 @@ var yoloCommand = defineCommand44({
|
|
|
5242
4460
|
|
|
5243
4461
|
// src/commands/adapter/index.ts
|
|
5244
4462
|
import { defineCommand as defineCommand45 } from "citty";
|
|
5245
|
-
import
|
|
4463
|
+
import consola39 from "consola";
|
|
5246
4464
|
var adapterCommand = defineCommand45({
|
|
5247
4465
|
meta: {
|
|
5248
4466
|
name: "adapter",
|
|
@@ -5280,7 +4498,7 @@ var adapterCommand = defineCommand45({
|
|
|
5280
4498
|
`);
|
|
5281
4499
|
return;
|
|
5282
4500
|
}
|
|
5283
|
-
|
|
4501
|
+
consola39.info(`Registry: ${index2.adapters.length} adapters (${index2.generated_at})`);
|
|
5284
4502
|
for (const a of index2.adapters) {
|
|
5285
4503
|
const installed = isInstalled(a.id, false) ? " [installed]" : "";
|
|
5286
4504
|
console.log(` ${a.id.padEnd(12)} ${a.name.padEnd(24)} ${a.category}${installed}`);
|
|
@@ -5302,7 +4520,7 @@ var adapterCommand = defineCommand45({
|
|
|
5302
4520
|
return;
|
|
5303
4521
|
}
|
|
5304
4522
|
if (local.length === 0) {
|
|
5305
|
-
|
|
4523
|
+
consola39.info("No adapters installed. Use `apes adapter list --remote` to see available adapters.");
|
|
5306
4524
|
return;
|
|
5307
4525
|
}
|
|
5308
4526
|
for (const a of local) {
|
|
@@ -5339,20 +4557,20 @@ var adapterCommand = defineCommand45({
|
|
|
5339
4557
|
for (const id of ids) {
|
|
5340
4558
|
const entry = findAdapter(index, id);
|
|
5341
4559
|
if (!entry) {
|
|
5342
|
-
|
|
4560
|
+
consola39.error(`Adapter "${id}" not found in registry. Use \`apes adapter search ${id}\` to search.`);
|
|
5343
4561
|
continue;
|
|
5344
4562
|
}
|
|
5345
4563
|
const conflicts = findConflictingAdapters(entry.executable, id);
|
|
5346
4564
|
if (conflicts.length > 0) {
|
|
5347
4565
|
for (const c of conflicts) {
|
|
5348
|
-
|
|
5349
|
-
|
|
4566
|
+
consola39.warn(`Conflicting adapter found: ${c.path} (id: ${c.adapterId}, executable: ${c.executable})`);
|
|
4567
|
+
consola39.warn(` Remove it with: apes adapter remove ${c.adapterId}`);
|
|
5350
4568
|
}
|
|
5351
4569
|
}
|
|
5352
4570
|
const result = await installAdapter(entry, { local });
|
|
5353
4571
|
const verb = result.updated ? "Updated" : "Installed";
|
|
5354
|
-
|
|
5355
|
-
|
|
4572
|
+
consola39.success(`${verb} ${result.id} \u2192 ${result.path}`);
|
|
4573
|
+
consola39.info(`Digest: ${result.digest}`);
|
|
5356
4574
|
}
|
|
5357
4575
|
}
|
|
5358
4576
|
}),
|
|
@@ -5379,9 +4597,9 @@ var adapterCommand = defineCommand45({
|
|
|
5379
4597
|
let failed = false;
|
|
5380
4598
|
for (const id of ids) {
|
|
5381
4599
|
if (removeAdapter(id, local)) {
|
|
5382
|
-
|
|
4600
|
+
consola39.success(`Removed adapter: ${id}`);
|
|
5383
4601
|
} else {
|
|
5384
|
-
|
|
4602
|
+
consola39.error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
|
|
5385
4603
|
failed = true;
|
|
5386
4604
|
}
|
|
5387
4605
|
}
|
|
@@ -5463,7 +4681,7 @@ var adapterCommand = defineCommand45({
|
|
|
5463
4681
|
return;
|
|
5464
4682
|
}
|
|
5465
4683
|
if (results.length === 0) {
|
|
5466
|
-
|
|
4684
|
+
consola39.info(`No adapters matching "${query}"`);
|
|
5467
4685
|
return;
|
|
5468
4686
|
}
|
|
5469
4687
|
for (const a of results) {
|
|
@@ -5498,29 +4716,29 @@ var adapterCommand = defineCommand45({
|
|
|
5498
4716
|
const targetId = args.id ? String(args.id) : void 0;
|
|
5499
4717
|
const targets = targetId ? [targetId] : index.adapters.map((a) => a.id).filter((id) => isInstalled(id, false));
|
|
5500
4718
|
if (targets.length === 0) {
|
|
5501
|
-
|
|
4719
|
+
consola39.info("No adapters installed to update.");
|
|
5502
4720
|
return;
|
|
5503
4721
|
}
|
|
5504
4722
|
for (const id of targets) {
|
|
5505
4723
|
const entry = findAdapter(index, id);
|
|
5506
4724
|
if (!entry) {
|
|
5507
|
-
|
|
4725
|
+
consola39.warn(`${id}: not found in registry, skipping`);
|
|
5508
4726
|
continue;
|
|
5509
4727
|
}
|
|
5510
4728
|
const localDigest = getInstalledDigest(id, false);
|
|
5511
4729
|
if (localDigest === entry.digest) {
|
|
5512
|
-
|
|
4730
|
+
consola39.info(`${id}: already up to date`);
|
|
5513
4731
|
continue;
|
|
5514
4732
|
}
|
|
5515
4733
|
if (localDigest && !args.yes) {
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
4734
|
+
consola39.warn(`${id}: digest will change \u2014 existing grants for this adapter will be invalidated`);
|
|
4735
|
+
consola39.info(` Old: ${localDigest}`);
|
|
4736
|
+
consola39.info(` New: ${entry.digest}`);
|
|
4737
|
+
consola39.info(" Use --yes to confirm");
|
|
5520
4738
|
continue;
|
|
5521
4739
|
}
|
|
5522
4740
|
const result = await installAdapter(entry);
|
|
5523
|
-
|
|
4741
|
+
consola39.success(`Updated ${result.id} \u2192 ${result.path}`);
|
|
5524
4742
|
}
|
|
5525
4743
|
}
|
|
5526
4744
|
}),
|
|
@@ -5557,7 +4775,7 @@ var adapterCommand = defineCommand45({
|
|
|
5557
4775
|
if (!localDigest)
|
|
5558
4776
|
throw new Error(`Adapter "${id}" is not installed${local ? " locally" : ""}`);
|
|
5559
4777
|
if (localDigest === entry.digest) {
|
|
5560
|
-
|
|
4778
|
+
consola39.success(`${id}: digest matches registry`);
|
|
5561
4779
|
} else {
|
|
5562
4780
|
console.log(` Local: ${localDigest}`);
|
|
5563
4781
|
console.log(` Registry: ${entry.digest}`);
|
|
@@ -5569,14 +4787,13 @@ var adapterCommand = defineCommand45({
|
|
|
5569
4787
|
});
|
|
5570
4788
|
|
|
5571
4789
|
// src/commands/run.ts
|
|
5572
|
-
import { execFileSync as
|
|
5573
|
-
import { hostname as
|
|
4790
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
4791
|
+
import { hostname as hostname5 } from "os";
|
|
5574
4792
|
import { basename } from "path";
|
|
5575
4793
|
import { defineCommand as defineCommand46 } from "citty";
|
|
5576
|
-
import
|
|
4794
|
+
import consola40 from "consola";
|
|
5577
4795
|
function resolveRunAsTarget(runAs) {
|
|
5578
4796
|
if (!runAs) return runAs;
|
|
5579
|
-
if (!isDarwin2()) return runAs;
|
|
5580
4797
|
if (!AGENT_NAME_REGEX.test(runAs)) return runAs;
|
|
5581
4798
|
if (runAs.startsWith("openape-agent-")) return runAs;
|
|
5582
4799
|
const platform = getHostPlatform();
|
|
@@ -5630,7 +4847,7 @@ function printPendingGrantInfo(grant, idp) {
|
|
|
5630
4847
|
const statusCmd = `apes grants status ${grant.id}`;
|
|
5631
4848
|
const executeCmd = `apes grants run ${grant.id}`;
|
|
5632
4849
|
if (mode === "human") {
|
|
5633
|
-
|
|
4850
|
+
consola40.success(`Grant ${grant.id} created \u2014 awaiting your approval`);
|
|
5634
4851
|
console.log(` Approve in browser: ${approveUrl}`);
|
|
5635
4852
|
console.log(` Check status: ${statusCmd}`);
|
|
5636
4853
|
console.log(` Run after approval: ${executeCmd}`);
|
|
@@ -5640,7 +4857,7 @@ function printPendingGrantInfo(grant, idp) {
|
|
|
5640
4857
|
return;
|
|
5641
4858
|
}
|
|
5642
4859
|
const maxMin = getPollMaxMinutes();
|
|
5643
|
-
|
|
4860
|
+
consola40.success(`Grant ${grant.id} created (pending approval)`);
|
|
5644
4861
|
console.log(` Approve: ${approveUrl}`);
|
|
5645
4862
|
console.log(` Status: ${statusCmd} [--json]`);
|
|
5646
4863
|
console.log(` Execute: ${executeCmd} --wait`);
|
|
@@ -5754,7 +4971,7 @@ async function runShellMode(command, args) {
|
|
|
5754
4971
|
const adapterHandled = await tryAdapterModeFromShell(command, idp, args);
|
|
5755
4972
|
if (adapterHandled) return;
|
|
5756
4973
|
const grantsUrl = await getGrantsEndpoint(idp);
|
|
5757
|
-
const targetHost = args.host ||
|
|
4974
|
+
const targetHost = args.host || hostname5();
|
|
5758
4975
|
try {
|
|
5759
4976
|
const grants = await apiFetch(
|
|
5760
4977
|
`${grantsUrl}?requester=${encodeURIComponent(auth.email)}&status=approved&limit=20`
|
|
@@ -5768,7 +4985,7 @@ async function runShellMode(command, args) {
|
|
|
5768
4985
|
}
|
|
5769
4986
|
} catch {
|
|
5770
4987
|
}
|
|
5771
|
-
|
|
4988
|
+
consola40.info(`Requesting ape-shell session grant on ${targetHost}`);
|
|
5772
4989
|
const grant = await apiFetch(grantsUrl, {
|
|
5773
4990
|
method: "POST",
|
|
5774
4991
|
body: {
|
|
@@ -5788,8 +5005,8 @@ async function runShellMode(command, args) {
|
|
|
5788
5005
|
host: targetHost
|
|
5789
5006
|
});
|
|
5790
5007
|
if (shouldWaitForGrant(args)) {
|
|
5791
|
-
|
|
5792
|
-
|
|
5008
|
+
consola40.info(`Grant requested: ${grant.id}`);
|
|
5009
|
+
consola40.info("Waiting for approval...");
|
|
5793
5010
|
const maxWait = 3e5;
|
|
5794
5011
|
const interval = 3e3;
|
|
5795
5012
|
const start = Date.now();
|
|
@@ -5820,13 +5037,13 @@ async function tryAdapterModeFromShell(command, idp, args) {
|
|
|
5820
5037
|
try {
|
|
5821
5038
|
resolved = await resolveCommand(loaded, [normalizedExecutable, ...parsed.argv]);
|
|
5822
5039
|
} catch (err) {
|
|
5823
|
-
|
|
5040
|
+
consola40.debug(`ape-shell: adapter resolve failed for "${parsed.raw}":`, err);
|
|
5824
5041
|
return false;
|
|
5825
5042
|
}
|
|
5826
5043
|
try {
|
|
5827
5044
|
const existingGrantId = await findExistingGrant(resolved, idp);
|
|
5828
5045
|
if (existingGrantId) {
|
|
5829
|
-
|
|
5046
|
+
consola40.info(`Reusing grant ${existingGrantId} for: ${resolved.detail.display}`);
|
|
5830
5047
|
const token = await fetchGrantToken(idp, existingGrantId);
|
|
5831
5048
|
await verifyAndExecute(token, resolved, existingGrantId);
|
|
5832
5049
|
return true;
|
|
@@ -5834,7 +5051,7 @@ async function tryAdapterModeFromShell(command, idp, args) {
|
|
|
5834
5051
|
} catch {
|
|
5835
5052
|
}
|
|
5836
5053
|
const approval = args.approval ?? "once";
|
|
5837
|
-
|
|
5054
|
+
consola40.info(`Requesting grant for: ${resolved.detail.display}`);
|
|
5838
5055
|
const grant = await createShapesGrant(resolved, {
|
|
5839
5056
|
idp,
|
|
5840
5057
|
approval,
|
|
@@ -5842,19 +5059,19 @@ async function tryAdapterModeFromShell(command, idp, args) {
|
|
|
5842
5059
|
});
|
|
5843
5060
|
if (grant.similar_grants?.similar_grants?.length) {
|
|
5844
5061
|
const n = grant.similar_grants.similar_grants.length;
|
|
5845
|
-
|
|
5846
|
-
|
|
5062
|
+
consola40.info("");
|
|
5063
|
+
consola40.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
|
|
5847
5064
|
}
|
|
5848
5065
|
notifyGrantPending({
|
|
5849
5066
|
grantId: grant.id,
|
|
5850
5067
|
approveUrl: `${idp}/grant-approval?grant_id=${grant.id}`,
|
|
5851
5068
|
command: resolved.detail?.display || parsed?.raw || "unknown",
|
|
5852
5069
|
audience: resolved.adapter?.cli?.audience ?? "shapes",
|
|
5853
|
-
host: args.host ||
|
|
5070
|
+
host: args.host || hostname5()
|
|
5854
5071
|
});
|
|
5855
5072
|
if (shouldWaitForGrant(args)) {
|
|
5856
|
-
|
|
5857
|
-
|
|
5073
|
+
consola40.info(`Grant requested: ${grant.id}`);
|
|
5074
|
+
consola40.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
|
|
5858
5075
|
const status = await waitForGrantStatus(idp, grant.id);
|
|
5859
5076
|
if (status !== "approved")
|
|
5860
5077
|
throw new CliError(`Grant ${status}`);
|
|
@@ -5870,7 +5087,7 @@ function execShellCommand(command) {
|
|
|
5870
5087
|
throw new CliError("No command to execute");
|
|
5871
5088
|
try {
|
|
5872
5089
|
const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
|
|
5873
|
-
|
|
5090
|
+
execFileSync11(command[0], command.slice(1), {
|
|
5874
5091
|
stdio: "inherit",
|
|
5875
5092
|
env: inheritedEnv
|
|
5876
5093
|
});
|
|
@@ -5928,7 +5145,7 @@ async function runAdapterMode(command, rawArgs, args) {
|
|
|
5928
5145
|
try {
|
|
5929
5146
|
const existingGrantId = await findExistingGrant(resolved, idp);
|
|
5930
5147
|
if (existingGrantId) {
|
|
5931
|
-
|
|
5148
|
+
consola40.info(`Reusing existing grant: ${existingGrantId}`);
|
|
5932
5149
|
const token = await fetchGrantToken(idp, existingGrantId);
|
|
5933
5150
|
await verifyAndExecute(token, resolved, existingGrantId);
|
|
5934
5151
|
return;
|
|
@@ -5942,17 +5159,17 @@ async function runAdapterMode(command, rawArgs, args) {
|
|
|
5942
5159
|
});
|
|
5943
5160
|
if (grant.similar_grants?.similar_grants?.length) {
|
|
5944
5161
|
const n = grant.similar_grants.similar_grants.length;
|
|
5945
|
-
|
|
5946
|
-
|
|
5162
|
+
consola40.info("");
|
|
5163
|
+
consola40.info(` Similar grant(s) found (${n}). Your approver can extend an existing grant to cover this request.`);
|
|
5947
5164
|
if (grant.similar_grants.widened_details?.length) {
|
|
5948
5165
|
const wider = grant.similar_grants.widened_details.map((d) => d.permission).join(", ");
|
|
5949
|
-
|
|
5166
|
+
consola40.info(` Broader scope: ${wider}`);
|
|
5950
5167
|
}
|
|
5951
|
-
|
|
5168
|
+
consola40.info("");
|
|
5952
5169
|
}
|
|
5953
5170
|
if (shouldWaitForGrant(args)) {
|
|
5954
|
-
|
|
5955
|
-
|
|
5171
|
+
consola40.info(`Grant requested: ${grant.id}`);
|
|
5172
|
+
consola40.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
|
|
5956
5173
|
const status = await waitForGrantStatus(idp, grant.id);
|
|
5957
5174
|
if (status !== "approved")
|
|
5958
5175
|
throw new Error(`Grant ${status}`);
|
|
@@ -5971,7 +5188,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
|
|
|
5971
5188
|
const idp = getIdpUrl(args.idp);
|
|
5972
5189
|
const grantsUrl = await getGrantsEndpoint(idp);
|
|
5973
5190
|
const command = commandArgv ?? action.split(" ");
|
|
5974
|
-
const targetHost = args.host ||
|
|
5191
|
+
const targetHost = args.host || hostname5();
|
|
5975
5192
|
const runAs = resolveRunAsTarget(args.as ?? void 0);
|
|
5976
5193
|
const reusableId = await findReusableAudienceGrant({
|
|
5977
5194
|
grantsUrl,
|
|
@@ -5985,7 +5202,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
|
|
|
5985
5202
|
const { authz_jwt: authz_jwt2 } = await apiFetch(`${grantsUrl}/${reusableId}/token`, { method: "POST" });
|
|
5986
5203
|
return executeWithGrantToken({ audience, command, args, token: authz_jwt2 });
|
|
5987
5204
|
}
|
|
5988
|
-
|
|
5205
|
+
consola40.info(`Requesting ${audience} grant on ${targetHost}: ${command.join(" ")}`);
|
|
5989
5206
|
const grant = await apiFetch(grantsUrl, {
|
|
5990
5207
|
method: "POST",
|
|
5991
5208
|
body: {
|
|
@@ -6002,9 +5219,9 @@ async function runAudienceMode(audience, action, args, commandArgv) {
|
|
|
6002
5219
|
printPendingGrantInfo(grant, idp);
|
|
6003
5220
|
throw new CliExit(getAsyncExitCode());
|
|
6004
5221
|
}
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
5222
|
+
consola40.success(`Grant requested: ${grant.id}`);
|
|
5223
|
+
consola40.info(`Approve at: ${idp}/grant-approval?grant_id=${grant.id}`);
|
|
5224
|
+
consola40.info("Waiting for approval...");
|
|
6008
5225
|
const maxWait = 15 * 60 * 1e3;
|
|
6009
5226
|
const interval = 3e3;
|
|
6010
5227
|
const start = Date.now();
|
|
@@ -6012,7 +5229,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
|
|
|
6012
5229
|
while (Date.now() - start < maxWait) {
|
|
6013
5230
|
const status = await apiFetch(`${grantsUrl}/${grant.id}`);
|
|
6014
5231
|
if (status.status === "approved") {
|
|
6015
|
-
|
|
5232
|
+
consola40.success("Grant approved!");
|
|
6016
5233
|
approved = true;
|
|
6017
5234
|
break;
|
|
6018
5235
|
}
|
|
@@ -6027,7 +5244,7 @@ async function runAudienceMode(audience, action, args, commandArgv) {
|
|
|
6027
5244
|
`Grant approval timed out after ${minutes} min (still pending). Check your DDISA inbox at ${idp}/grant-approval?grant_id=${grant.id} \u2014 if approved later, re-run the same \`apes run\` command and it will reuse the grant.`
|
|
6028
5245
|
);
|
|
6029
5246
|
}
|
|
6030
|
-
|
|
5247
|
+
consola40.info("Fetching grant token...");
|
|
6031
5248
|
const { authz_jwt } = await apiFetch(`${grantsUrl}/${grant.id}/token`, {
|
|
6032
5249
|
method: "POST"
|
|
6033
5250
|
});
|
|
@@ -6036,10 +5253,10 @@ async function runAudienceMode(audience, action, args, commandArgv) {
|
|
|
6036
5253
|
function executeWithGrantToken(opts) {
|
|
6037
5254
|
const { audience, command, args, token } = opts;
|
|
6038
5255
|
if (audience === "escapes") {
|
|
6039
|
-
|
|
5256
|
+
consola40.info(`Executing: ${command.join(" ")}`);
|
|
6040
5257
|
try {
|
|
6041
5258
|
const { APES_SHELL_WRAPPER: _wrapperMarker, ...inheritedEnv } = process.env;
|
|
6042
|
-
|
|
5259
|
+
execFileSync11(args["escapes-path"] || "escapes", ["--grant", token, "--", ...command], {
|
|
6043
5260
|
stdio: "inherit",
|
|
6044
5261
|
env: inheritedEnv
|
|
6045
5262
|
});
|
|
@@ -6075,11 +5292,11 @@ async function findReusableAudienceGrant(opts) {
|
|
|
6075
5292
|
|
|
6076
5293
|
// src/commands/proxy.ts
|
|
6077
5294
|
import { spawn as spawn2 } from "child_process";
|
|
6078
|
-
import { existsSync as
|
|
6079
|
-
import { homedir as
|
|
6080
|
-
import { join as
|
|
5295
|
+
import { existsSync as existsSync18 } from "fs";
|
|
5296
|
+
import { homedir as homedir13 } from "os";
|
|
5297
|
+
import { join as join15 } from "path";
|
|
6081
5298
|
import { defineCommand as defineCommand47 } from "citty";
|
|
6082
|
-
import
|
|
5299
|
+
import consola41 from "consola";
|
|
6083
5300
|
|
|
6084
5301
|
// src/proxy/config.ts
|
|
6085
5302
|
function buildDefaultProxyConfigToml(opts) {
|
|
@@ -6113,10 +5330,10 @@ note = "VPC-internal hostname suffix"
|
|
|
6113
5330
|
|
|
6114
5331
|
// src/proxy/local-proxy.ts
|
|
6115
5332
|
import { spawn } from "child_process";
|
|
6116
|
-
import { mkdtempSync as
|
|
5333
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
|
|
6117
5334
|
import { createRequire } from "module";
|
|
6118
|
-
import { tmpdir as
|
|
6119
|
-
import { dirname as dirname4, join as
|
|
5335
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5336
|
+
import { dirname as dirname4, join as join13, resolve as resolve4 } from "path";
|
|
6120
5337
|
var require2 = createRequire(import.meta.url);
|
|
6121
5338
|
function findProxyBin() {
|
|
6122
5339
|
const pkgPath = require2.resolve("@openape/proxy/package.json");
|
|
@@ -6125,12 +5342,12 @@ function findProxyBin() {
|
|
|
6125
5342
|
if (!binRel) {
|
|
6126
5343
|
throw new Error("@openape/proxy is missing the openape-proxy bin entry");
|
|
6127
5344
|
}
|
|
6128
|
-
return
|
|
5345
|
+
return resolve4(dirname4(pkgPath), binRel);
|
|
6129
5346
|
}
|
|
6130
5347
|
async function startEphemeralProxy(configToml) {
|
|
6131
|
-
const tmpDir =
|
|
6132
|
-
const configPath =
|
|
6133
|
-
|
|
5348
|
+
const tmpDir = mkdtempSync2(join13(tmpdir2(), "openape-proxy-"));
|
|
5349
|
+
const configPath = join13(tmpDir, "config.toml");
|
|
5350
|
+
writeFileSync10(configPath, configToml, { mode: 384 });
|
|
6134
5351
|
const binPath = findProxyBin();
|
|
6135
5352
|
const child = spawn(process.execPath, [binPath, "-c", configPath], {
|
|
6136
5353
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -6138,7 +5355,7 @@ async function startEphemeralProxy(configToml) {
|
|
|
6138
5355
|
});
|
|
6139
5356
|
const cleanupTmp = () => {
|
|
6140
5357
|
try {
|
|
6141
|
-
|
|
5358
|
+
rmSync4(tmpDir, { recursive: true, force: true });
|
|
6142
5359
|
} catch {
|
|
6143
5360
|
}
|
|
6144
5361
|
};
|
|
@@ -6212,9 +5429,9 @@ function waitForListenLine(child) {
|
|
|
6212
5429
|
}
|
|
6213
5430
|
|
|
6214
5431
|
// src/proxy/trust-bundle.ts
|
|
6215
|
-
import { existsSync as
|
|
6216
|
-
import { tmpdir as
|
|
6217
|
-
import { join as
|
|
5432
|
+
import { existsSync as existsSync17, mkdtempSync as mkdtempSync3, readFileSync as readFileSync15, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
|
|
5433
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
5434
|
+
import { join as join14 } from "path";
|
|
6218
5435
|
var CANDIDATES = [
|
|
6219
5436
|
"/etc/ssl/cert.pem",
|
|
6220
5437
|
// macOS
|
|
@@ -6227,25 +5444,25 @@ var CANDIDATES = [
|
|
|
6227
5444
|
];
|
|
6228
5445
|
function detectSystemCaPath() {
|
|
6229
5446
|
for (const p of CANDIDATES) {
|
|
6230
|
-
if (
|
|
5447
|
+
if (existsSync17(p)) return p;
|
|
6231
5448
|
}
|
|
6232
5449
|
throw new Error(
|
|
6233
5450
|
`Could not locate a system CA bundle. Tried: ${CANDIDATES.join(", ")}. Set NODE_EXTRA_CA_CERTS yourself or pass --allow-no-system-ca.`
|
|
6234
5451
|
);
|
|
6235
5452
|
}
|
|
6236
5453
|
function buildTrustBundle(opts) {
|
|
6237
|
-
const dir =
|
|
6238
|
-
const path2 =
|
|
6239
|
-
const sys =
|
|
6240
|
-
const local =
|
|
6241
|
-
|
|
5454
|
+
const dir = mkdtempSync3(join14(tmpdir3(), "openape-trust-"));
|
|
5455
|
+
const path2 = join14(dir, "bundle.pem");
|
|
5456
|
+
const sys = readFileSync15(opts.systemCaPath, "utf-8");
|
|
5457
|
+
const local = readFileSync15(opts.localCaPath, "utf-8");
|
|
5458
|
+
writeFileSync11(path2, `${sys.trimEnd()}
|
|
6242
5459
|
${local.trimEnd()}
|
|
6243
5460
|
`, { mode: 384 });
|
|
6244
5461
|
return {
|
|
6245
5462
|
path: path2,
|
|
6246
5463
|
cleanup: () => {
|
|
6247
5464
|
try {
|
|
6248
|
-
|
|
5465
|
+
unlinkSync2(path2);
|
|
6249
5466
|
} catch {
|
|
6250
5467
|
}
|
|
6251
5468
|
try {
|
|
@@ -6267,7 +5484,7 @@ function resolveProxyConfigOptions() {
|
|
|
6267
5484
|
77
|
|
6268
5485
|
);
|
|
6269
5486
|
}
|
|
6270
|
-
|
|
5487
|
+
consola41.info(`[apes proxy] IdP-mediated mode \u2014 agent=${auth.email}, idp=${auth.idp}`);
|
|
6271
5488
|
return { agentEmail: auth.email, idpUrl: auth.idp, mediated: true };
|
|
6272
5489
|
}
|
|
6273
5490
|
var proxyCommand = defineCommand47({
|
|
@@ -6294,9 +5511,9 @@ var proxyCommand = defineCommand47({
|
|
|
6294
5511
|
let close = null;
|
|
6295
5512
|
if (reuseHostPort) {
|
|
6296
5513
|
proxyUrl = `http://${reuseHostPort}`;
|
|
6297
|
-
|
|
6298
|
-
const localCaPath =
|
|
6299
|
-
if (!
|
|
5514
|
+
consola41.info(`[apes proxy] using long-running daemon at ${proxyUrl}`);
|
|
5515
|
+
const localCaPath = join15(homedir13(), ".openape", "proxy", "ca.crt");
|
|
5516
|
+
if (!existsSync18(localCaPath)) {
|
|
6300
5517
|
throw new CliError(
|
|
6301
5518
|
`OPENAPE_PROXY is set but no local CA found at ${localCaPath}. Start the daemon (sudo -u <agent> apes proxy --global < secrets.toml) first.`
|
|
6302
5519
|
);
|
|
@@ -6305,15 +5522,15 @@ var proxyCommand = defineCommand47({
|
|
|
6305
5522
|
systemCaPath: detectSystemCaPath(),
|
|
6306
5523
|
localCaPath
|
|
6307
5524
|
});
|
|
6308
|
-
|
|
5525
|
+
consola41.debug(`[apes proxy] trust bundle: ${bundle.path}`);
|
|
6309
5526
|
} else if (reuseUrl) {
|
|
6310
5527
|
proxyUrl = reuseUrl;
|
|
6311
|
-
|
|
5528
|
+
consola41.info(`[apes proxy] reusing existing proxy at ${proxyUrl}`);
|
|
6312
5529
|
} else {
|
|
6313
5530
|
const ephemeral = await startEphemeralProxy(buildDefaultProxyConfigToml(resolveProxyConfigOptions()));
|
|
6314
5531
|
proxyUrl = ephemeral.url;
|
|
6315
5532
|
close = ephemeral.close;
|
|
6316
|
-
|
|
5533
|
+
consola41.info(`[apes proxy] started ephemeral proxy at ${proxyUrl}`);
|
|
6317
5534
|
}
|
|
6318
5535
|
const noProxy = process.env.NO_PROXY ?? process.env.no_proxy ?? "127.0.0.1,localhost";
|
|
6319
5536
|
const childEnv = {
|
|
@@ -6354,7 +5571,7 @@ var proxyCommand = defineCommand47({
|
|
|
6354
5571
|
else resolveExit(code ?? 0);
|
|
6355
5572
|
});
|
|
6356
5573
|
child.once("error", (err) => {
|
|
6357
|
-
|
|
5574
|
+
consola41.error(`[apes proxy] failed to spawn '${wrapped[0]}':`, err.message);
|
|
6358
5575
|
resolveExit(127);
|
|
6359
5576
|
});
|
|
6360
5577
|
});
|
|
@@ -6411,7 +5628,7 @@ var explainCommand = defineCommand48({
|
|
|
6411
5628
|
|
|
6412
5629
|
// src/commands/config/get.ts
|
|
6413
5630
|
import { defineCommand as defineCommand49 } from "citty";
|
|
6414
|
-
import
|
|
5631
|
+
import consola42 from "consola";
|
|
6415
5632
|
var configGetCommand = defineCommand49({
|
|
6416
5633
|
meta: {
|
|
6417
5634
|
name: "get",
|
|
@@ -6432,7 +5649,7 @@ var configGetCommand = defineCommand49({
|
|
|
6432
5649
|
if (idp)
|
|
6433
5650
|
console.log(idp);
|
|
6434
5651
|
else
|
|
6435
|
-
|
|
5652
|
+
consola42.info("No IdP configured.");
|
|
6436
5653
|
break;
|
|
6437
5654
|
}
|
|
6438
5655
|
case "email": {
|
|
@@ -6440,7 +5657,7 @@ var configGetCommand = defineCommand49({
|
|
|
6440
5657
|
if (auth?.email)
|
|
6441
5658
|
console.log(auth.email);
|
|
6442
5659
|
else
|
|
6443
|
-
|
|
5660
|
+
consola42.info("Not logged in.");
|
|
6444
5661
|
break;
|
|
6445
5662
|
}
|
|
6446
5663
|
default: {
|
|
@@ -6453,7 +5670,7 @@ var configGetCommand = defineCommand49({
|
|
|
6453
5670
|
if (sectionObj && field in sectionObj) {
|
|
6454
5671
|
console.log(sectionObj[field]);
|
|
6455
5672
|
} else {
|
|
6456
|
-
|
|
5673
|
+
consola42.info(`Key "${key}" not set.`);
|
|
6457
5674
|
}
|
|
6458
5675
|
} else {
|
|
6459
5676
|
throw new CliError(`Unknown key: "${key}". Use: idp, email, defaults.idp, defaults.approval, agent.key, agent.email`);
|
|
@@ -6465,7 +5682,7 @@ var configGetCommand = defineCommand49({
|
|
|
6465
5682
|
|
|
6466
5683
|
// src/commands/config/set.ts
|
|
6467
5684
|
import { defineCommand as defineCommand50 } from "citty";
|
|
6468
|
-
import
|
|
5685
|
+
import consola43 from "consola";
|
|
6469
5686
|
var configSetCommand = defineCommand50({
|
|
6470
5687
|
meta: {
|
|
6471
5688
|
name: "set",
|
|
@@ -6502,7 +5719,7 @@ var configSetCommand = defineCommand50({
|
|
|
6502
5719
|
throw new CliError(`Unknown section: "${section}". Use: defaults, agent`);
|
|
6503
5720
|
}
|
|
6504
5721
|
saveConfig(config);
|
|
6505
|
-
|
|
5722
|
+
consola43.success(`Set ${key} = ${value}`);
|
|
6506
5723
|
}
|
|
6507
5724
|
});
|
|
6508
5725
|
|
|
@@ -6638,42 +5855,42 @@ var mcpCommand = defineCommand52({
|
|
|
6638
5855
|
if (transport !== "stdio" && transport !== "sse") {
|
|
6639
5856
|
throw new Error('Transport must be "stdio" or "sse"');
|
|
6640
5857
|
}
|
|
6641
|
-
const { startMcpServer } = await import("./server-
|
|
5858
|
+
const { startMcpServer } = await import("./server-MDXOGP2U.js");
|
|
6642
5859
|
await startMcpServer(transport, port);
|
|
6643
5860
|
}
|
|
6644
5861
|
});
|
|
6645
5862
|
|
|
6646
5863
|
// src/commands/init/index.ts
|
|
6647
|
-
import { existsSync as
|
|
5864
|
+
import { existsSync as existsSync19, copyFileSync, writeFileSync as writeFileSync12 } from "fs";
|
|
6648
5865
|
import { randomBytes } from "crypto";
|
|
6649
|
-
import { execFileSync as
|
|
6650
|
-
import { join as
|
|
5866
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
5867
|
+
import { join as join16 } from "path";
|
|
6651
5868
|
import { defineCommand as defineCommand53 } from "citty";
|
|
6652
|
-
import
|
|
5869
|
+
import consola44 from "consola";
|
|
6653
5870
|
var DEFAULT_IDP_URL = "https://id.openape.at";
|
|
6654
5871
|
async function downloadTemplate(repo, targetDir) {
|
|
6655
5872
|
const { downloadTemplate: gigetDownload } = await import("giget");
|
|
6656
5873
|
await gigetDownload(`gh:${repo}`, { dir: targetDir, force: false });
|
|
6657
5874
|
}
|
|
6658
5875
|
function installDeps(dir) {
|
|
6659
|
-
const hasLockFile = (name) =>
|
|
5876
|
+
const hasLockFile = (name) => existsSync19(join16(dir, name));
|
|
6660
5877
|
if (hasLockFile("pnpm-lock.yaml")) {
|
|
6661
|
-
|
|
5878
|
+
execFileSync12("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
|
|
6662
5879
|
} else if (hasLockFile("bun.lockb")) {
|
|
6663
|
-
|
|
5880
|
+
execFileSync12("bun", ["install"], { cwd: dir, stdio: "inherit" });
|
|
6664
5881
|
} else {
|
|
6665
|
-
|
|
5882
|
+
execFileSync12("npm", ["install"], { cwd: dir, stdio: "inherit" });
|
|
6666
5883
|
}
|
|
6667
5884
|
}
|
|
6668
5885
|
async function promptChoice(message, choices) {
|
|
6669
|
-
const result = await
|
|
5886
|
+
const result = await consola44.prompt(message, { type: "select", options: choices });
|
|
6670
5887
|
if (typeof result === "symbol") {
|
|
6671
5888
|
throw new CliExit(0);
|
|
6672
5889
|
}
|
|
6673
5890
|
return result;
|
|
6674
5891
|
}
|
|
6675
5892
|
async function promptText(message, defaultValue) {
|
|
6676
|
-
const result = await
|
|
5893
|
+
const result = await consola44.prompt(message, { type: "text", default: defaultValue, placeholder: defaultValue });
|
|
6677
5894
|
if (typeof result === "symbol") {
|
|
6678
5895
|
throw new CliExit(0);
|
|
6679
5896
|
}
|
|
@@ -6721,23 +5938,23 @@ var initCommand = defineCommand53({
|
|
|
6721
5938
|
});
|
|
6722
5939
|
async function initSP(targetDir) {
|
|
6723
5940
|
const dir = targetDir || "my-app";
|
|
6724
|
-
if (
|
|
5941
|
+
if (existsSync19(join16(dir, "package.json"))) {
|
|
6725
5942
|
throw new CliError(`Directory "${dir}" already contains a project.`);
|
|
6726
5943
|
}
|
|
6727
|
-
|
|
5944
|
+
consola44.start("Scaffolding SP starter...");
|
|
6728
5945
|
await downloadTemplate("openape-ai/openape-sp-starter", dir);
|
|
6729
|
-
|
|
6730
|
-
|
|
5946
|
+
consola44.success("Scaffolded from openape-sp-starter");
|
|
5947
|
+
consola44.start("Installing dependencies...");
|
|
6731
5948
|
installDeps(dir);
|
|
6732
|
-
|
|
6733
|
-
const envExample =
|
|
6734
|
-
const envFile =
|
|
6735
|
-
if (
|
|
5949
|
+
consola44.success("Dependencies installed");
|
|
5950
|
+
const envExample = join16(dir, ".env.example");
|
|
5951
|
+
const envFile = join16(dir, ".env");
|
|
5952
|
+
if (existsSync19(envExample) && !existsSync19(envFile)) {
|
|
6736
5953
|
copyFileSync(envExample, envFile);
|
|
6737
|
-
|
|
5954
|
+
consola44.success(`\`.env\` created (using Free IdP at ${DEFAULT_IDP_URL})`);
|
|
6738
5955
|
}
|
|
6739
5956
|
console.log("");
|
|
6740
|
-
|
|
5957
|
+
consola44.box([
|
|
6741
5958
|
`cd ${dir}`,
|
|
6742
5959
|
"npm run dev",
|
|
6743
5960
|
"",
|
|
@@ -6746,7 +5963,7 @@ async function initSP(targetDir) {
|
|
|
6746
5963
|
}
|
|
6747
5964
|
async function initIdP(targetDir) {
|
|
6748
5965
|
const dir = targetDir || "my-idp";
|
|
6749
|
-
if (
|
|
5966
|
+
if (existsSync19(join16(dir, "package.json"))) {
|
|
6750
5967
|
throw new CliError(`Directory "${dir}" already contains a project.`);
|
|
6751
5968
|
}
|
|
6752
5969
|
const domain = await promptText("Domain for the IdP", "localhost");
|
|
@@ -6756,15 +5973,15 @@ async function initIdP(targetDir) {
|
|
|
6756
5973
|
"s3 (S3-compatible)"
|
|
6757
5974
|
]);
|
|
6758
5975
|
const adminEmail = await promptText("Admin email");
|
|
6759
|
-
|
|
5976
|
+
consola44.start("Scaffolding IdP starter...");
|
|
6760
5977
|
await downloadTemplate("openape-ai/openape-idp-starter", dir);
|
|
6761
|
-
|
|
6762
|
-
|
|
5978
|
+
consola44.success("Scaffolded from openape-idp-starter");
|
|
5979
|
+
consola44.start("Installing dependencies...");
|
|
6763
5980
|
installDeps(dir);
|
|
6764
|
-
|
|
5981
|
+
consola44.success("Dependencies installed");
|
|
6765
5982
|
const sessionSecret = randomBytes(32).toString("hex");
|
|
6766
5983
|
const managementToken = randomBytes(32).toString("hex");
|
|
6767
|
-
|
|
5984
|
+
consola44.success("Secrets generated");
|
|
6768
5985
|
const isLocalhost = domain === "localhost";
|
|
6769
5986
|
const origin = isLocalhost ? "http://localhost:3000" : `https://${domain}`;
|
|
6770
5987
|
const envContent = [
|
|
@@ -6778,11 +5995,11 @@ async function initIdP(targetDir) {
|
|
|
6778
5995
|
`NUXT_OPENAPE_RP_ID=${domain}`,
|
|
6779
5996
|
`NUXT_OPENAPE_RP_ORIGIN=${origin}`
|
|
6780
5997
|
].join("\n");
|
|
6781
|
-
|
|
5998
|
+
writeFileSync12(join16(dir, ".env"), `${envContent}
|
|
6782
5999
|
`, { mode: 384 });
|
|
6783
|
-
|
|
6000
|
+
consola44.success(".env created");
|
|
6784
6001
|
console.log("");
|
|
6785
|
-
|
|
6002
|
+
consola44.box([
|
|
6786
6003
|
`cd ${dir}`,
|
|
6787
6004
|
"npm run dev",
|
|
6788
6005
|
"",
|
|
@@ -6798,12 +6015,12 @@ async function initIdP(targetDir) {
|
|
|
6798
6015
|
}
|
|
6799
6016
|
|
|
6800
6017
|
// src/commands/enroll.ts
|
|
6801
|
-
import { Buffer as
|
|
6802
|
-
import { existsSync as
|
|
6018
|
+
import { Buffer as Buffer4 } from "buffer";
|
|
6019
|
+
import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
|
|
6803
6020
|
import { execFile as execFile2 } from "child_process";
|
|
6804
6021
|
import { sign as sign2 } from "crypto";
|
|
6805
6022
|
import { defineCommand as defineCommand54 } from "citty";
|
|
6806
|
-
import
|
|
6023
|
+
import consola45 from "consola";
|
|
6807
6024
|
var DEFAULT_IDP_URL2 = "https://id.openape.at";
|
|
6808
6025
|
var DEFAULT_KEY_PATH = "~/.ssh/id_ed25519";
|
|
6809
6026
|
var POLL_INTERVAL = 3e3;
|
|
@@ -6815,7 +6032,7 @@ function openBrowser2(url) {
|
|
|
6815
6032
|
}
|
|
6816
6033
|
async function pollForEnrollment(idp, agentEmail, keyPath) {
|
|
6817
6034
|
const resolvedKey = resolveKeyPath(keyPath);
|
|
6818
|
-
const keyContent =
|
|
6035
|
+
const keyContent = readFileSync16(resolvedKey, "utf-8");
|
|
6819
6036
|
const privateKey = loadEd25519PrivateKey(keyContent);
|
|
6820
6037
|
const challengeUrl = await getAgentChallengeEndpoint(idp);
|
|
6821
6038
|
const authenticateUrl = await getAgentAuthenticateEndpoint(idp);
|
|
@@ -6829,7 +6046,7 @@ async function pollForEnrollment(idp, agentEmail, keyPath) {
|
|
|
6829
6046
|
});
|
|
6830
6047
|
if (challengeResp.ok) {
|
|
6831
6048
|
const { challenge } = await challengeResp.json();
|
|
6832
|
-
const signature = sign2(null,
|
|
6049
|
+
const signature = sign2(null, Buffer4.from(challenge), privateKey).toString("base64");
|
|
6833
6050
|
const authResp = await fetch(authenticateUrl, {
|
|
6834
6051
|
method: "POST",
|
|
6835
6052
|
headers: { "Content-Type": "application/json" },
|
|
@@ -6842,7 +6059,7 @@ async function pollForEnrollment(idp, agentEmail, keyPath) {
|
|
|
6842
6059
|
}
|
|
6843
6060
|
} catch {
|
|
6844
6061
|
}
|
|
6845
|
-
await new Promise((
|
|
6062
|
+
await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL));
|
|
6846
6063
|
}
|
|
6847
6064
|
throw new Error("Enrollment timed out. Please check the browser and try again.");
|
|
6848
6065
|
}
|
|
@@ -6866,38 +6083,38 @@ var enrollCommand = defineCommand54({
|
|
|
6866
6083
|
}
|
|
6867
6084
|
},
|
|
6868
6085
|
async run({ args }) {
|
|
6869
|
-
const idp = args.idp || await
|
|
6086
|
+
const idp = args.idp || await consola45.prompt("IdP URL", { type: "text", default: DEFAULT_IDP_URL2, placeholder: DEFAULT_IDP_URL2 }).then((r) => {
|
|
6870
6087
|
if (typeof r === "symbol") throw new CliExit(0);
|
|
6871
6088
|
return r;
|
|
6872
6089
|
}) || DEFAULT_IDP_URL2;
|
|
6873
|
-
const agentName = args.name || await
|
|
6090
|
+
const agentName = args.name || await consola45.prompt("Agent name", { type: "text", placeholder: "deploy-bot" }).then((r) => {
|
|
6874
6091
|
if (typeof r === "symbol") throw new CliExit(0);
|
|
6875
6092
|
return r;
|
|
6876
6093
|
});
|
|
6877
6094
|
if (!agentName) {
|
|
6878
6095
|
throw new CliError("Agent name is required.");
|
|
6879
6096
|
}
|
|
6880
|
-
const keyPath = args.key || await
|
|
6097
|
+
const keyPath = args.key || await consola45.prompt("Ed25519 key", { type: "text", default: DEFAULT_KEY_PATH, placeholder: DEFAULT_KEY_PATH }).then((r) => {
|
|
6881
6098
|
if (typeof r === "symbol") throw new CliExit(0);
|
|
6882
6099
|
return r;
|
|
6883
6100
|
}) || DEFAULT_KEY_PATH;
|
|
6884
6101
|
const resolvedKey = resolveKeyPath(keyPath);
|
|
6885
6102
|
let publicKey;
|
|
6886
|
-
if (
|
|
6103
|
+
if (existsSync20(resolvedKey)) {
|
|
6887
6104
|
publicKey = readPublicKey(resolvedKey);
|
|
6888
|
-
|
|
6105
|
+
consola45.success(`Using existing key ${keyPath}`);
|
|
6889
6106
|
} else {
|
|
6890
|
-
|
|
6107
|
+
consola45.start(`Generating Ed25519 key pair at ${keyPath}...`);
|
|
6891
6108
|
publicKey = generateAndSaveKey(keyPath);
|
|
6892
|
-
|
|
6109
|
+
consola45.success(`Key pair generated at ${keyPath}`);
|
|
6893
6110
|
}
|
|
6894
6111
|
const encodedKey = encodeURIComponent(publicKey);
|
|
6895
6112
|
const enrollUrl = `${idp}/enroll?name=${encodeURIComponent(agentName)}&key=${encodedKey}`;
|
|
6896
|
-
|
|
6897
|
-
|
|
6113
|
+
consola45.info("Opening browser for enrollment...");
|
|
6114
|
+
consola45.info(`\u2192 ${idp}/enroll`);
|
|
6898
6115
|
openBrowser2(enrollUrl);
|
|
6899
6116
|
console.log("");
|
|
6900
|
-
const agentEmail = await
|
|
6117
|
+
const agentEmail = await consola45.prompt(
|
|
6901
6118
|
"Agent email (shown in browser after enrollment)",
|
|
6902
6119
|
{ type: "text", placeholder: `agent+${agentName}@...` }
|
|
6903
6120
|
).then((r) => {
|
|
@@ -6907,7 +6124,7 @@ var enrollCommand = defineCommand54({
|
|
|
6907
6124
|
if (!agentEmail) {
|
|
6908
6125
|
throw new CliError("Agent email is required to verify enrollment.");
|
|
6909
6126
|
}
|
|
6910
|
-
|
|
6127
|
+
consola45.start("Verifying enrollment...");
|
|
6911
6128
|
const { token, expiresIn } = await pollForEnrollment(idp, agentEmail, keyPath);
|
|
6912
6129
|
saveAuth({
|
|
6913
6130
|
idp,
|
|
@@ -6919,17 +6136,17 @@ var enrollCommand = defineCommand54({
|
|
|
6919
6136
|
config.defaults = { ...config.defaults, idp };
|
|
6920
6137
|
config.agent = { key: keyPath, email: agentEmail };
|
|
6921
6138
|
saveConfig(config);
|
|
6922
|
-
|
|
6923
|
-
|
|
6139
|
+
consola45.success(`Agent enrolled as ${agentEmail}`);
|
|
6140
|
+
consola45.success("Config saved to ~/.config/apes/");
|
|
6924
6141
|
console.log("");
|
|
6925
|
-
|
|
6142
|
+
consola45.info("Verify with: apes whoami");
|
|
6926
6143
|
}
|
|
6927
6144
|
});
|
|
6928
6145
|
|
|
6929
6146
|
// src/commands/register-user.ts
|
|
6930
|
-
import { existsSync as
|
|
6147
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17 } from "fs";
|
|
6931
6148
|
import { defineCommand as defineCommand55 } from "citty";
|
|
6932
|
-
import
|
|
6149
|
+
import consola46 from "consola";
|
|
6933
6150
|
var registerUserCommand = defineCommand55({
|
|
6934
6151
|
meta: {
|
|
6935
6152
|
name: "register-user",
|
|
@@ -6966,8 +6183,8 @@ var registerUserCommand = defineCommand55({
|
|
|
6966
6183
|
throw new CliError("No IdP URL configured. Run `apes login` first.");
|
|
6967
6184
|
}
|
|
6968
6185
|
let publicKey = args.key;
|
|
6969
|
-
if (
|
|
6970
|
-
publicKey =
|
|
6186
|
+
if (existsSync21(args.key)) {
|
|
6187
|
+
publicKey = readFileSync17(args.key, "utf-8").trim();
|
|
6971
6188
|
}
|
|
6972
6189
|
if (!publicKey.startsWith("ssh-ed25519 ")) {
|
|
6973
6190
|
throw new CliError("Public key must be in ssh-ed25519 format.");
|
|
@@ -6985,7 +6202,7 @@ var registerUserCommand = defineCommand55({
|
|
|
6985
6202
|
...userType ? { type: userType } : {}
|
|
6986
6203
|
}
|
|
6987
6204
|
});
|
|
6988
|
-
|
|
6205
|
+
consola46.success(`User registered: ${result.email} (type: ${result.type}, owner: ${result.owner})`);
|
|
6989
6206
|
}
|
|
6990
6207
|
});
|
|
6991
6208
|
|
|
@@ -6994,7 +6211,7 @@ import { defineCommand as defineCommand57 } from "citty";
|
|
|
6994
6211
|
|
|
6995
6212
|
// src/commands/utils/dig.ts
|
|
6996
6213
|
import { defineCommand as defineCommand56 } from "citty";
|
|
6997
|
-
import
|
|
6214
|
+
import consola47 from "consola";
|
|
6998
6215
|
import { resolveDDISA as resolveDDISA2 } from "@openape/core";
|
|
6999
6216
|
var digCommand = defineCommand56({
|
|
7000
6217
|
meta: {
|
|
@@ -7069,12 +6286,12 @@ var digCommand = defineCommand56({
|
|
|
7069
6286
|
console.log(` domain: ${domain}`);
|
|
7070
6287
|
console.log("");
|
|
7071
6288
|
if (!result.ddisa.found) {
|
|
7072
|
-
|
|
6289
|
+
consola47.warn(`No DDISA record at _ddisa.${domain}`);
|
|
7073
6290
|
if (result.hint) console.log(`
|
|
7074
6291
|
${result.hint}`);
|
|
7075
6292
|
throw new CliError(`No DDISA record found for ${domain}`);
|
|
7076
6293
|
}
|
|
7077
|
-
|
|
6294
|
+
consola47.success(`_ddisa.${domain} \u2192 ${result.ddisa.idp}`);
|
|
7078
6295
|
console.log(` Version: ${result.ddisa.version || "ddisa1"}`);
|
|
7079
6296
|
console.log(` IdP URL: ${result.ddisa.idp}`);
|
|
7080
6297
|
if (result.ddisa.mode) console.log(` Mode: ${result.ddisa.mode}`);
|
|
@@ -7084,13 +6301,13 @@ ${result.hint}`);
|
|
|
7084
6301
|
return;
|
|
7085
6302
|
}
|
|
7086
6303
|
if (result.idpDiscovery.ok) {
|
|
7087
|
-
|
|
6304
|
+
consola47.success(`IdP reachable (${result.idpDiscovery.status ?? 200})`);
|
|
7088
6305
|
if (result.idpDiscovery.issuer) console.log(` Issuer: ${result.idpDiscovery.issuer}`);
|
|
7089
6306
|
if (result.idpDiscovery.ddisaVersion) console.log(` DDISA: v${result.idpDiscovery.ddisaVersion}`);
|
|
7090
6307
|
if (result.idpDiscovery.authMethods?.length) console.log(` Auth: ${result.idpDiscovery.authMethods.join(", ")}`);
|
|
7091
6308
|
if (result.idpDiscovery.grantTypes?.length) console.log(` Grants: ${result.idpDiscovery.grantTypes.join(", ")}`);
|
|
7092
6309
|
} else {
|
|
7093
|
-
|
|
6310
|
+
consola47.warn(`IdP discovery failed${result.idpDiscovery.status ? ` (HTTP ${result.idpDiscovery.status})` : ""}`);
|
|
7094
6311
|
if (result.hint) console.log(`
|
|
7095
6312
|
${result.hint}`);
|
|
7096
6313
|
throw new CliError(`IdP at ${result.ddisa.idp} not reachable`);
|
|
@@ -7114,7 +6331,7 @@ import { defineCommand as defineCommand60 } from "citty";
|
|
|
7114
6331
|
|
|
7115
6332
|
// src/commands/sessions/list.ts
|
|
7116
6333
|
import { defineCommand as defineCommand58 } from "citty";
|
|
7117
|
-
import
|
|
6334
|
+
import consola48 from "consola";
|
|
7118
6335
|
var sessionsListCommand = defineCommand58({
|
|
7119
6336
|
meta: {
|
|
7120
6337
|
name: "list",
|
|
@@ -7133,7 +6350,7 @@ var sessionsListCommand = defineCommand58({
|
|
|
7133
6350
|
return;
|
|
7134
6351
|
}
|
|
7135
6352
|
if (result.data.length === 0) {
|
|
7136
|
-
|
|
6353
|
+
consola48.info("No active sessions.");
|
|
7137
6354
|
return;
|
|
7138
6355
|
}
|
|
7139
6356
|
for (const f of result.data) {
|
|
@@ -7146,7 +6363,7 @@ var sessionsListCommand = defineCommand58({
|
|
|
7146
6363
|
|
|
7147
6364
|
// src/commands/sessions/remove.ts
|
|
7148
6365
|
import { defineCommand as defineCommand59 } from "citty";
|
|
7149
|
-
import
|
|
6366
|
+
import consola49 from "consola";
|
|
7150
6367
|
var sessionsRemoveCommand = defineCommand59({
|
|
7151
6368
|
meta: {
|
|
7152
6369
|
name: "remove",
|
|
@@ -7163,7 +6380,7 @@ var sessionsRemoveCommand = defineCommand59({
|
|
|
7163
6380
|
const id = String(args.familyId).trim();
|
|
7164
6381
|
if (!id) throw new CliError("familyId required");
|
|
7165
6382
|
await apiFetch(`/api/me/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
7166
|
-
|
|
6383
|
+
consola49.success(`Session ${id} revoked. The device using it will need to \`apes login\` again on its next refresh.`);
|
|
7167
6384
|
}
|
|
7168
6385
|
});
|
|
7169
6386
|
|
|
@@ -7181,7 +6398,7 @@ var sessionsCommand = defineCommand60({
|
|
|
7181
6398
|
|
|
7182
6399
|
// src/commands/dns-check.ts
|
|
7183
6400
|
import { defineCommand as defineCommand61 } from "citty";
|
|
7184
|
-
import
|
|
6401
|
+
import consola50 from "consola";
|
|
7185
6402
|
import { resolveDDISA as resolveDDISA3 } from "@openape/core";
|
|
7186
6403
|
var dnsCheckCommand = defineCommand61({
|
|
7187
6404
|
meta: {
|
|
@@ -7197,7 +6414,7 @@ var dnsCheckCommand = defineCommand61({
|
|
|
7197
6414
|
},
|
|
7198
6415
|
async run({ args }) {
|
|
7199
6416
|
const domain = args.domain;
|
|
7200
|
-
|
|
6417
|
+
consola50.start(`Checking _ddisa.${domain}...`);
|
|
7201
6418
|
try {
|
|
7202
6419
|
const result = await resolveDDISA3(domain);
|
|
7203
6420
|
if (!result) {
|
|
@@ -7206,7 +6423,7 @@ var dnsCheckCommand = defineCommand61({
|
|
|
7206
6423
|
console.log(` _ddisa.${domain} TXT "v=ddisa1 idp=https://id.${domain}"`);
|
|
7207
6424
|
throw new CliError(`No DDISA record found for ${domain}`);
|
|
7208
6425
|
}
|
|
7209
|
-
|
|
6426
|
+
consola50.success(`_ddisa.${domain} \u2192 ${result.idp}`);
|
|
7210
6427
|
console.log("");
|
|
7211
6428
|
console.log(` Version: ${result.version || "ddisa1"}`);
|
|
7212
6429
|
console.log(` IdP URL: ${result.idp}`);
|
|
@@ -7215,14 +6432,14 @@ var dnsCheckCommand = defineCommand61({
|
|
|
7215
6432
|
if (result.priority !== void 0)
|
|
7216
6433
|
console.log(` Priority: ${result.priority}`);
|
|
7217
6434
|
console.log("");
|
|
7218
|
-
|
|
6435
|
+
consola50.start(`Verifying IdP at ${result.idp}...`);
|
|
7219
6436
|
const discoResp = await fetch(`${result.idp}/.well-known/openid-configuration`);
|
|
7220
6437
|
if (!discoResp.ok) {
|
|
7221
|
-
|
|
6438
|
+
consola50.warn(`IdP discovery failed (${discoResp.status}). Is the IdP running at ${result.idp}?`);
|
|
7222
6439
|
return;
|
|
7223
6440
|
}
|
|
7224
6441
|
const disco = await discoResp.json();
|
|
7225
|
-
|
|
6442
|
+
consola50.success(`IdP is reachable`);
|
|
7226
6443
|
console.log(` Issuer: ${disco.issuer}`);
|
|
7227
6444
|
console.log(` DDISA: v${disco.ddisa_version || "?"}`);
|
|
7228
6445
|
if (disco.ddisa_auth_methods_supported) {
|
|
@@ -7276,7 +6493,7 @@ async function bestEffortGrantCount(idp) {
|
|
|
7276
6493
|
}
|
|
7277
6494
|
}
|
|
7278
6495
|
async function runHealth(args) {
|
|
7279
|
-
const version = true ? "1.
|
|
6496
|
+
const version = true ? "1.31.0" : "0.0.0";
|
|
7280
6497
|
const auth = loadAuth();
|
|
7281
6498
|
if (!auth) {
|
|
7282
6499
|
throw new CliError("Not logged in. Run `apes login` first.", 1);
|
|
@@ -7358,7 +6575,7 @@ var healthCommand = defineCommand62({
|
|
|
7358
6575
|
|
|
7359
6576
|
// src/commands/workflows.ts
|
|
7360
6577
|
import { defineCommand as defineCommand63 } from "citty";
|
|
7361
|
-
import
|
|
6578
|
+
import consola51 from "consola";
|
|
7362
6579
|
|
|
7363
6580
|
// src/guides/index.ts
|
|
7364
6581
|
var guides = [
|
|
@@ -7429,7 +6646,7 @@ var workflowsCommand = defineCommand63({
|
|
|
7429
6646
|
if (args.id) {
|
|
7430
6647
|
const guide = guides.find((g) => g.id === String(args.id));
|
|
7431
6648
|
if (!guide) {
|
|
7432
|
-
|
|
6649
|
+
consola51.info(`Available: ${guides.map((g) => g.id).join(", ")}`);
|
|
7433
6650
|
throw new CliError(`Guide not found: ${args.id}`);
|
|
7434
6651
|
}
|
|
7435
6652
|
if (args.json) {
|
|
@@ -7469,26 +6686,26 @@ var workflowsCommand = defineCommand63({
|
|
|
7469
6686
|
});
|
|
7470
6687
|
|
|
7471
6688
|
// src/version-check.ts
|
|
7472
|
-
import { existsSync as
|
|
7473
|
-
import { homedir as
|
|
7474
|
-
import { join as
|
|
7475
|
-
import
|
|
6689
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync7, readFileSync as readFileSync18, writeFileSync as writeFileSync13 } from "fs";
|
|
6690
|
+
import { homedir as homedir14 } from "os";
|
|
6691
|
+
import { join as join17 } from "path";
|
|
6692
|
+
import consola52 from "consola";
|
|
7476
6693
|
var PACKAGE_NAME = "@openape/apes";
|
|
7477
6694
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
7478
|
-
var CACHE_FILE =
|
|
6695
|
+
var CACHE_FILE = join17(homedir14(), ".config", "apes", ".version-check.json");
|
|
7479
6696
|
function readCache() {
|
|
7480
|
-
if (!
|
|
6697
|
+
if (!existsSync22(CACHE_FILE)) return null;
|
|
7481
6698
|
try {
|
|
7482
|
-
return JSON.parse(
|
|
6699
|
+
return JSON.parse(readFileSync18(CACHE_FILE, "utf-8"));
|
|
7483
6700
|
} catch {
|
|
7484
6701
|
return null;
|
|
7485
6702
|
}
|
|
7486
6703
|
}
|
|
7487
6704
|
function writeCache(entry) {
|
|
7488
6705
|
try {
|
|
7489
|
-
const dir =
|
|
7490
|
-
if (!
|
|
7491
|
-
|
|
6706
|
+
const dir = join17(homedir14(), ".config", "apes");
|
|
6707
|
+
if (!existsSync22(dir)) mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
6708
|
+
writeFileSync13(CACHE_FILE, JSON.stringify(entry), { mode: 384 });
|
|
7492
6709
|
} catch {
|
|
7493
6710
|
}
|
|
7494
6711
|
}
|
|
@@ -7517,7 +6734,7 @@ async function fetchLatestVersion() {
|
|
|
7517
6734
|
}
|
|
7518
6735
|
function warnIfBehind(currentVersion, latest) {
|
|
7519
6736
|
if (compareSemver(currentVersion, latest) < 0) {
|
|
7520
|
-
|
|
6737
|
+
consola52.warn(
|
|
7521
6738
|
`apes ${currentVersion} is behind latest @openape/apes@${latest}. Run \`npm i -g @openape/apes@latest\` to update. (Suppress with APES_NO_UPDATE_CHECK=1.)`
|
|
7522
6739
|
);
|
|
7523
6740
|
}
|
|
@@ -7549,10 +6766,10 @@ if (shellRewrite) {
|
|
|
7549
6766
|
if (shellRewrite.action === "rewrite") {
|
|
7550
6767
|
process.argv = shellRewrite.argv;
|
|
7551
6768
|
} else if (shellRewrite.action === "version") {
|
|
7552
|
-
console.log(`ape-shell ${"1.
|
|
6769
|
+
console.log(`ape-shell ${"1.31.0"} (OpenApe DDISA shell wrapper)`);
|
|
7553
6770
|
process.exit(0);
|
|
7554
6771
|
} else if (shellRewrite.action === "help") {
|
|
7555
|
-
console.log(`ape-shell ${"1.
|
|
6772
|
+
console.log(`ape-shell ${"1.31.0"} \u2014 OpenApe DDISA shell wrapper`);
|
|
7556
6773
|
console.log("");
|
|
7557
6774
|
console.log("Usage:");
|
|
7558
6775
|
console.log(" ape-shell Start interactive grant-mediated REPL");
|
|
@@ -7567,7 +6784,7 @@ if (shellRewrite) {
|
|
|
7567
6784
|
console.log(" --help, -h Show this help message");
|
|
7568
6785
|
process.exit(0);
|
|
7569
6786
|
} else if (shellRewrite.action === "interactive") {
|
|
7570
|
-
const { runInteractiveShell } = await import("./orchestrator-
|
|
6787
|
+
const { runInteractiveShell } = await import("./orchestrator-6PZXCE54.js");
|
|
7571
6788
|
await runInteractiveShell();
|
|
7572
6789
|
process.exit(0);
|
|
7573
6790
|
} else {
|
|
@@ -7610,7 +6827,7 @@ var configCommand = defineCommand64({
|
|
|
7610
6827
|
var main = defineCommand64({
|
|
7611
6828
|
meta: {
|
|
7612
6829
|
name: "apes",
|
|
7613
|
-
version: "1.
|
|
6830
|
+
version: "1.31.0",
|
|
7614
6831
|
description: "Unified CLI for OpenApe"
|
|
7615
6832
|
},
|
|
7616
6833
|
subCommands: {
|
|
@@ -7662,26 +6879,26 @@ async function maybeRefreshAuth() {
|
|
|
7662
6879
|
const { loadAuth: loadAuth2 } = await import("./config-MOB5DJ6H.js");
|
|
7663
6880
|
if (!loadAuth2()) return;
|
|
7664
6881
|
try {
|
|
7665
|
-
const { ensureFreshToken } = await import("./http-
|
|
6882
|
+
const { ensureFreshToken } = await import("./http-SILH37L7.js");
|
|
7666
6883
|
await ensureFreshToken();
|
|
7667
6884
|
} catch {
|
|
7668
6885
|
}
|
|
7669
6886
|
}
|
|
7670
6887
|
await maybeRefreshAuth();
|
|
7671
|
-
await maybeWarnStaleVersion("1.
|
|
6888
|
+
await maybeWarnStaleVersion("1.31.0").catch(() => {
|
|
7672
6889
|
});
|
|
7673
6890
|
runMain(main).catch((err) => {
|
|
7674
6891
|
if (err instanceof CliExit) {
|
|
7675
6892
|
process.exit(err.exitCode);
|
|
7676
6893
|
}
|
|
7677
6894
|
if (err instanceof CliError) {
|
|
7678
|
-
|
|
6895
|
+
consola53.error(err.message);
|
|
7679
6896
|
process.exit(err.exitCode);
|
|
7680
6897
|
}
|
|
7681
6898
|
if (debug) {
|
|
7682
|
-
|
|
6899
|
+
consola53.error(err);
|
|
7683
6900
|
} else {
|
|
7684
|
-
|
|
6901
|
+
consola53.error(err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err));
|
|
7685
6902
|
}
|
|
7686
6903
|
process.exit(1);
|
|
7687
6904
|
});
|