@monster-orange/den 0.4.21 → 0.4.23

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/docs/distributions.md +186 -42
  3. package/package.json +1 -1
  4. package/service/install.sh +213 -63
  5. package/src/shared/agents/den-claude.ts +13 -1
  6. package/src/shared/agents/guardrails/rotation-hook.ts +5 -0
  7. package/src/shared/agents/records.ts +6 -0
  8. package/src/shared/agents/rotate-settings.ts +309 -0
  9. package/src/shared/agents/rotation.ts +110 -5
  10. package/src/shared/agents/runtime.ts +6 -0
  11. package/src/shared/agents/session-name.ts +21 -1
  12. package/src/shared/den-config.ts +27 -12
  13. package/src/shared/launchd-session.ts +92 -0
  14. package/src/shared/sessions/client.ts +24 -0
  15. package/src/shared/sessions/host.ts +114 -5
  16. package/src/shared/sessions/types.ts +5 -0
  17. package/src/ui/cli/agent-ops.ts +469 -17
  18. package/src/ui/cli/agents-command.ts +200 -3
  19. package/src/ui/cli/doctor-command.ts +72 -0
  20. package/src/ui/cli/doctor-where.ts +203 -0
  21. package/src/ui/cli/index.ts +1 -1
  22. package/src/ui/cli/server-agent-ops.ts +2 -2
  23. package/src/ui/cli/service-command.ts +217 -11
  24. package/src/ui/cli/sessions-command.ts +46 -12
  25. package/src/ui/cli/web-command.ts +36 -5
  26. package/src/ui/cli/web-service.ts +160 -0
  27. package/src/ui/server/agent-routes.ts +149 -5
  28. package/src/ui/server/index.ts +17 -5
  29. package/src/ui/server/public/agents-page.js +316 -27
  30. package/src/ui/server/public/agents.html +62 -0
  31. package/src/ui/server/public/i18n/es.js +58 -2
  32. package/src/ui/server/public/i18n/ja.js +54 -2
  33. package/src/ui/server/public/settings-registry.js +204 -0
  34. package/src/ui/server/public/settings.html +43 -4
  35. package/src/ui/server/public/shell.js +56 -2
  36. package/src/ui/server/route-classes.ts +21 -0
  37. package/src/ui/server/session-routes.ts +27 -5
@@ -1,24 +1,42 @@
1
1
  #!/usr/bin/env bash
2
- # Run the web UI as a long-lived macOS service (a launchd LaunchAgent):
3
- # started at login, restarted if it exits, not tied to any terminal.
2
+ # Run the web UI and the session host as long-lived macOS launchd jobs:
3
+ # started at boot or login, restarted if they exit, not tied to a terminal.
4
4
  #
5
- # service/install.sh install (or refresh) and start it
6
- # service/install.sh uninstall stop it and remove the LaunchAgent
7
- # service/install.sh status launchd's view of it
8
- # service/install.sh print write the LaunchAgent plist to stdout only
5
+ # service/install.sh install (or refresh) and start them
6
+ # service/install.sh uninstall stop them and remove their plists
7
+ # service/install.sh status launchd's view of them
8
+ # service/install.sh print write the web plist to stdout only
9
+ # service/install.sh print-host write the session host plist to stdout only
10
+ #
11
+ # Two modes (D18), DEN_SERVICE_MODE:
12
+ # login (default) LaunchAgents in the desktop session (gui/<uid>): they
13
+ # start at login and stop at logout, with every agent.
14
+ # always LaunchDaemons that run as you (UserName): they start at boot and
15
+ # keep running across logout, so agents do too. One sudo writes
16
+ # and loads them. No login keychain, no GUI there: agents use the
17
+ # 1Password service-account token, headless browsers, and Full
18
+ # Disk Access for protected folders.
19
+ #
20
+ # The session host job runs DEN_SERVICE_HOST_EXE, a copy of node Den owns
21
+ # (<stateDir>/bin/<cli>-sessions-host): Full Disk Access is granted to that
22
+ # path, so it never goes to the shared node. A node change replaces the
23
+ # copy; the grant holds when the code signature's requirement is the same
24
+ # (an official node build), and install says to grant it again when not.
9
25
  #
10
26
  # Works from Den's checkout and from a distribution, where it lives in
11
27
  # node_modules/@monster-orange/den/service/. The distribution root is
12
- # DEN_ROOT, else this checkout when it holds den.yml. The agent runs the
13
- # engine's server with the node found on PATH now, from the distribution
14
- # root; re-run install after changing node versions.
28
+ # DEN_ROOT, else this checkout when it holds den.yml.
15
29
  #
16
- # DEN_SERVICE_LABEL launchd label (default local.<dist dir name>.web)
17
- # DEN_SERVICE_LOGS log directory (default ~/Library/Logs/<dist dir name>)
18
- # DEN_SERVICE_NODE the node the agent runs (default: see pick_node)
30
+ # DEN_SERVICE_LABEL web job label (default local.<dist dir name>.web)
31
+ # DEN_SERVICE_HOST_LABEL host job label (default: the web label's .web → .sessions)
32
+ # DEN_SERVICE_LOGS log directory (default ~/Library/Logs/<dist dir name>)
33
+ # DEN_SERVICE_NODE the node the jobs run (default: see pick_node)
34
+ # DEN_SERVICE_HOST_EXE the host's node copy (default: none, the host runs that node)
19
35
  # DEN_SERVICE_READY_CHECK=external install does not print "running": the
20
36
  # caller (`<cli> service install`) checks the server
21
37
  # answers first.
38
+ # DEN_SERVICE_DAEMON_DIR, DEN_SERVICE_SUDO (tests) where LaunchDaemons go
39
+ # and what runs as root (default /Library/LaunchDaemons, sudo)
22
40
  set -euo pipefail
23
41
 
24
42
  HERE="$(cd "$(dirname "$0")" && pwd)"
@@ -34,9 +52,31 @@ fi
34
52
  [ -f "$DIST/den.yml" ] || { echo "✗ $DIST has no den.yml" >&2; exit 1; }
35
53
  NAME="$(basename "$DIST")"
36
54
  LABEL="${DEN_SERVICE_LABEL:-local.$NAME.web}"
55
+ HOST_LABEL="${DEN_SERVICE_HOST_LABEL:-${LABEL%.web}.sessions}"
37
56
  LOGS="${DEN_SERVICE_LOGS:-$HOME/Library/Logs/$NAME}"
38
- PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
39
- DOMAIN="gui/$(id -u)"
57
+ SVC_MODE="${DEN_SERVICE_MODE:-login}"
58
+ case "$SVC_MODE" in
59
+ login|always) ;;
60
+ *) echo "✗ DEN_SERVICE_MODE must be login or always, not \"$SVC_MODE\"" >&2; exit 2 ;;
61
+ esac
62
+ UID_N="$(id -u)"
63
+ USER_N="$(id -un)"
64
+ AGENT_DIR="$HOME/Library/LaunchAgents"
65
+ DAEMON_DIR="${DEN_SERVICE_DAEMON_DIR:-/Library/LaunchDaemons}"
66
+ SUDO="${DEN_SERVICE_SUDO-sudo}"
67
+ GUI="gui/$UID_N"
68
+
69
+ # The plist and domain for a label in a mode.
70
+ plist_for() { # $1 label $2 mode
71
+ if [ "$2" = always ]; then echo "$DAEMON_DIR/$1.plist"; else echo "$AGENT_DIR/$1.plist"; fi
72
+ }
73
+ domain_for() { if [ "$1" = always ]; then echo system; else echo "$GUI"; fi; }
74
+ other_mode() { if [ "$1" = always ]; then echo login; else echo always; fi; }
75
+ # Root runs it in the system domain (a LaunchDaemon); else as you.
76
+ as_mode() { # $1 mode, then the command
77
+ local m="$1"; shift
78
+ if [ "$m" = always ] && [ -n "$SUDO" ]; then "$SUDO" "$@"; else "$@"; fi
79
+ }
40
80
 
41
81
  # tsx: the nearest node_modules/tsx at or above the engine (hoisted installs).
42
82
  find_tsx() {
@@ -76,20 +116,75 @@ pick_node() {
76
116
  echo "$node"
77
117
  }
78
118
 
79
- write_plist() { # $1 = output file
119
+ # The code signature's designated requirement, or "" (unsigned, no codesign).
120
+ requirement() { codesign -d -r- "$1" 2>&1 | sed -n 's/^designated => //p' || true; }
121
+
122
+ # The host's node copy (D18): refreshed when node changed. Prints the path
123
+ # the host job runs: the copy, or node itself when it cannot be copied.
124
+ # A node linked to libraries beside it (Homebrew's) does not run copied.
125
+ self_contained() {
126
+ ! { command -v otool >/dev/null 2>&1 && otool -L "$1" 2>/dev/null | grep -q '@rpath/\|/opt/homebrew/\|/usr/local/opt/'; }
127
+ }
128
+
129
+ host_exe() { # $1 node
130
+ local node="$1" exe="${DEN_SERVICE_HOST_EXE:-}" c src=""
131
+ if [ -z "$exe" ]; then echo "$node"; return; fi
132
+ # The node the jobs run, else (unless DEN_SERVICE_NODE names one) the
133
+ # PATH's: a version manager's is fine here, since the copy outlives it.
134
+ for c in "$node" $([ -z "${DEN_SERVICE_NODE:-}" ] && command -v node 2>/dev/null); do
135
+ if self_contained "$c"; then src="$c"; break; fi
136
+ done
137
+ if [ -z "$src" ]; then
138
+ echo "⚠ $node is not self-contained (it loads libraries beside it), so the session host" >&2
139
+ echo " runs it directly: grant Full Disk Access to $node, and again after each node" >&2
140
+ echo " upgrade. An official node build (nodejs.org, nvm) avoids that: set DEN_SERVICE_NODE." >&2
141
+ echo "$node"; return
142
+ fi
143
+ node="$src"
144
+ if [ -f "$exe" ] && cmp -s "$node" "$exe"; then echo "$exe"; return; fi
145
+ local before="" after
146
+ [ -f "$exe" ] && before="$(requirement "$exe")"
147
+ mkdir -p "$(dirname "$exe")"
148
+ chmod 700 "$(dirname "$exe")"
149
+ cp "$node" "$exe.new"
150
+ chmod 700 "$exe.new"
151
+ mv -f "$exe.new" "$exe"
152
+ after="$(requirement "$exe")"
153
+ case "$after" in
154
+ "" | cdhash*)
155
+ echo "⚠ $exe is a copy of $node, which has no stable code signature: Full Disk" >&2
156
+ echo " Access must be granted to it again after every node change. An official" >&2
157
+ echo " node build (nodejs.org, nvm) keeps the grant: set DEN_SERVICE_NODE." >&2 ;;
158
+ *)
159
+ if [ -n "$before" ] && [ "$before" != "$after" ]; then
160
+ echo "⚠ $exe changed signer: grant it Full Disk Access again" >&2
161
+ echo " (System Settings → Privacy & Security → Full Disk Access)." >&2
162
+ fi ;;
163
+ esac
164
+ echo "$exe"
165
+ }
166
+
167
+ write_plist() { # $1 = output file, $2 = web|host, $3 = mode
80
168
  NODE="$(pick_node "$MODE")"
81
169
  TSX="$(find_tsx)" || { echo "✗ tsx not installed: run npm ci in $DIST" >&2; exit 1; }
170
+ local exe="$NODE" label="$LABEL" log="web.log"
171
+ if [ "$2" = host ]; then
172
+ if [ "$MODE" = install ]; then exe="$(host_exe "$NODE")"; else exe="${DEN_SERVICE_HOST_EXE:-$NODE}"; fi
173
+ label="$HOST_LABEL"; log="sessions-host.log"
174
+ fi
82
175
  DOCKER_DIR="$(dirname "$(command -v docker || echo /usr/local/bin/docker)")"
83
176
  SVC_PATH="$(dirname "$NODE"):$DOCKER_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
84
- python3 -I - "$1" "$LABEL" "$NODE" "$TSX" "$ENGINE" "$DIST" "$SVC_PATH" "$HOME" "$LOGS" <<'PY'
177
+ python3 -I - "$1" "$2" "$3" "$label" "$exe" "$TSX" "$ENGINE" "$DIST" "$SVC_PATH" "$HOME" "$LOGS/$log" "$USER_N" <<'PY'
85
178
  import plistlib, sys
86
- out, label, node, tsx, engine, dist, path, home, logs = sys.argv[1:]
179
+ out, kind, mode, label, exe, tsx, engine, dist, path, home, log, user = sys.argv[1:]
180
+ entry = (f"{engine}/src/ui/server/index.ts", "--silent") if kind == "web" \
181
+ else (f"{engine}/src/ui/cli/index.ts", "sessions", "host", "--stand-by")
87
182
  data = {
88
183
  "Label": label,
89
- "ProgramArguments": [node, "--import", f"{tsx}/dist/loader.mjs",
90
- f"{engine}/src/ui/server/index.ts", "--silent"],
184
+ "ProgramArguments": [exe, "--import", f"{tsx}/dist/loader.mjs", *entry],
91
185
  "WorkingDirectory": dist,
92
- "EnvironmentVariables": {"PATH": path, "HOME": home,
186
+ "EnvironmentVariables": {"PATH": path, "HOME": home, "USER": user,
187
+ "LOGNAME": user,
93
188
  "DEN_ROOT": dist, "DEN_ENGINE_ROOT": engine},
94
189
  "RunAtLoad": True,
95
190
  "KeepAlive": True,
@@ -97,9 +192,12 @@ data = {
97
192
  # Interactive, not Background: macOS gives Background agents efficiency
98
193
  # cores and throttled I/O, and the UI stalled for seconds under load.
99
194
  "ProcessType": "Interactive",
100
- "StandardOutPath": f"{logs}/web.log",
101
- "StandardErrorPath": f"{logs}/web.log",
195
+ "StandardOutPath": log,
196
+ "StandardErrorPath": log,
102
197
  }
198
+ if mode == "always":
199
+ # A LaunchDaemon runs as root unless told otherwise: run as the person.
200
+ data["UserName"] = user
103
201
  if out == "-":
104
202
  sys.stdout.buffer.write(plistlib.dumps(data))
105
203
  else:
@@ -108,58 +206,110 @@ else:
108
206
  PY
109
207
  }
110
208
 
209
+ # Stop and remove a job (any mode), quietly when it is not there.
210
+ remove_job() { # $1 label $2 mode
211
+ local plist; plist="$(plist_for "$1" "$2")"
212
+ if [ "$2" = always ]; then
213
+ [ -f "$plist" ] || return 0
214
+ fi
215
+ as_mode "$2" launchctl bootout "$(domain_for "$2")/$1" 2>/dev/null || true
216
+ [ -f "$plist" ] && as_mode "$2" rm -f "$plist"
217
+ return 0
218
+ }
219
+
220
+ # Install and start one job in SVC_MODE.
221
+ load_job() { # $1 label $2 web|host
222
+ local label="$1" plist domain tmp
223
+ plist="$(plist_for "$label" "$SVC_MODE")"
224
+ domain="$(domain_for "$SVC_MODE")"
225
+ tmp="$(mktemp "${TMPDIR:-/tmp}/den-plist.XXXXXX")"
226
+ write_plist "$tmp" "$2" "$SVC_MODE"
227
+ plutil -lint "$tmp" >/dev/null
228
+ # The other mode's job is the same service: never both.
229
+ remove_job "$label" "$(other_mode "$SVC_MODE")"
230
+ as_mode "$SVC_MODE" launchctl bootout "$domain/$label" 2>/dev/null || true
231
+ # bootout returns before launchd has let go of the label; a bootstrap
232
+ # straight after fails ("Bootstrap failed: 5") and leaves NO service.
233
+ # Wait for it to go, then retry bootstrap a few times.
234
+ for _ in $(seq 1 50); do
235
+ as_mode "$SVC_MODE" launchctl print "$domain/$label" >/dev/null 2>&1 || break
236
+ sleep 0.2
237
+ done
238
+ if [ "$SVC_MODE" = always ]; then
239
+ as_mode always install -o root -g wheel -m 644 "$tmp" "$plist"
240
+ rm -f "$tmp"
241
+ else
242
+ mkdir -p "$(dirname "$plist")"
243
+ mv "$tmp" "$plist"
244
+ fi
245
+ local loaded=0
246
+ for _ in 1 2 3 4 5; do
247
+ if as_mode "$SVC_MODE" launchctl bootstrap "$domain" "$plist" 2>/dev/null; then loaded=1; break; fi
248
+ sleep 1
249
+ done
250
+ [ "$loaded" = 1 ] || { echo "✗ launchctl bootstrap failed for $label (plist: $plist)" >&2; exit 1; }
251
+ as_mode "$SVC_MODE" launchctl kickstart -k "$domain/$label"
252
+ }
253
+
254
+ # The job's state, as status shows it.
255
+ job_status() { # $1 label
256
+ local m plist domain svc_node out
257
+ for m in always login; do
258
+ plist="$(plist_for "$1" "$m")"
259
+ [ -f "$plist" ] || continue
260
+ domain="$(domain_for "$m")"
261
+ # The node it was installed with may be gone (a removed nvm version):
262
+ # launchd then restarts it forever with no useful message.
263
+ if command -v plutil >/dev/null 2>&1; then
264
+ svc_node="$(plutil -extract ProgramArguments.0 raw -o - "$plist" 2>/dev/null || true)"
265
+ if [ -n "$svc_node" ] && [ ! -x "$svc_node" ]; then
266
+ echo "✗ $1 runs $svc_node, which no longer exists: re-run service install" >&2
267
+ return 1
268
+ fi
269
+ fi
270
+ echo "$1 ($m: $domain)"
271
+ if out="$(launchctl print "$domain/$1" 2>/dev/null)"; then
272
+ printf '%s\n' "$out" | grep -E '^\s*(state|pid|last exit code|runs|spawn type)'
273
+ else
274
+ echo " plist present, job not loaded: $([ "$m" = always ] && echo "sudo ")launchctl bootstrap $domain $plist"
275
+ fi
276
+ return 0
277
+ done
278
+ # launchctl's own "Could not find service" is noise when it isn't loaded.
279
+ echo "$1 is not loaded"
280
+ }
281
+
111
282
  MODE="${1:-install}"
112
283
  case "$MODE" in
113
284
  install)
114
- mkdir -p "$LOGS" "$(dirname "$PLIST")"
115
- write_plist "$PLIST.new"
116
- plutil -lint "$PLIST.new" >/dev/null
117
- launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
118
- # bootout returns before launchd has let go of the label; a bootstrap
119
- # straight after fails ("Bootstrap failed: 5") and leaves NO service.
120
- # Wait for it to go, then retry bootstrap a few times.
121
- for _ in $(seq 1 50); do
122
- launchctl print "$DOMAIN/$LABEL" >/dev/null 2>&1 || break
123
- sleep 0.2
124
- done
125
- mv "$PLIST.new" "$PLIST"
126
- loaded=0
127
- for _ in 1 2 3 4 5; do
128
- if launchctl bootstrap "$DOMAIN" "$PLIST" 2>/dev/null; then loaded=1; break; fi
129
- sleep 1
130
- done
131
- [ "$loaded" = 1 ] || { echo "✗ launchctl bootstrap failed for $LABEL (plist: $PLIST)" >&2; exit 1; }
132
- launchctl kickstart -k "$DOMAIN/$LABEL"
285
+ mkdir -p "$LOGS"
286
+ if [ "$SVC_MODE" = always ] && [ -n "$SUDO" ]; then
287
+ echo "… $SVC_MODE: LaunchDaemons running as $USER_N need your admin password once (sudo)"
288
+ fi
289
+ load_job "$HOST_LABEL" host
290
+ load_job "$LABEL" web
133
291
  if [ "${DEN_SERVICE_READY_CHECK:-}" = external ]; then
134
- echo "… $LABEL started; waiting for it to answer"
292
+ echo "… $LABEL started ($SVC_MODE); waiting for it to answer"
135
293
  else
136
- echo "✓ $LABEL running (logs: $LOGS/web.log)"
294
+ echo "✓ $LABEL running ($SVC_MODE; logs: $LOGS/web.log)"
137
295
  fi
138
296
  ;;
139
297
  uninstall)
140
- launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
141
- rm -f "$PLIST"
142
- echo "✓ $LABEL removed"
298
+ for m in login always; do
299
+ remove_job "$LABEL" "$m"
300
+ remove_job "$HOST_LABEL" "$m"
301
+ done
302
+ echo "✓ $LABEL and $HOST_LABEL removed"
143
303
  ;;
144
304
  print)
145
- write_plist -
305
+ write_plist - web "$SVC_MODE"
306
+ ;;
307
+ print-host)
308
+ write_plist - host "$SVC_MODE"
146
309
  ;;
147
310
  status)
148
- # The node the agent was installed with may be gone (a removed nvm
149
- # version): launchd then restarts it forever with no useful message.
150
- if [ -f "$PLIST" ] && command -v plutil >/dev/null 2>&1; then
151
- svc_node="$(plutil -extract ProgramArguments.0 raw -o - "$PLIST" 2>/dev/null || true)"
152
- if [ -n "$svc_node" ] && [ ! -x "$svc_node" ]; then
153
- echo "✗ $LABEL runs $svc_node, which no longer exists: re-run service install" >&2
154
- exit 1
155
- fi
156
- fi
157
- # launchctl's own "Could not find service" is noise when it isn't loaded.
158
- if out="$(launchctl print "$DOMAIN/$LABEL" 2>/dev/null)"; then
159
- printf '%s\n' "$out" | grep -E '^\s*(state|pid|last exit code|runs|spawn type)'
160
- else
161
- echo "$LABEL is not loaded"
162
- fi
311
+ job_status "$LABEL"
312
+ job_status "$HOST_LABEL"
163
313
  ;;
164
- *) echo "usage: $0 [install|uninstall|status|print]" >&2; exit 2 ;;
314
+ *) echo "usage: $0 [install|uninstall|status|print|print-host]" >&2; exit 2 ;;
165
315
  esac
@@ -455,6 +455,11 @@ export function firstPromptArg(text: string | undefined): string[] {
455
455
  return [t.startsWith("-") ? `Prompt: ${t}` : t];
456
456
  }
457
457
 
458
+ /** D24: `--name <label>`, so peers tell Den's agents apart; or nothing. */
459
+ function nameArg(label: string | undefined): string[] {
460
+ return label ? ["--name", label] : [];
461
+ }
462
+
458
463
  const TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
459
464
 
460
465
  /**
@@ -503,6 +508,7 @@ export const denClaude: Runtime = {
503
508
  bin,
504
509
  "--session-id",
505
510
  id,
511
+ ...nameArg(input.sessionLabel),
506
512
  ...c.argv,
507
513
  ...firstPromptArg(input.firstPrompt),
508
514
  ],
@@ -517,7 +523,13 @@ export const denClaude: Runtime = {
517
523
  const bin = binFor(input);
518
524
  const c = common(input);
519
525
  return {
520
- argv: [bin, "--resume", input.resumeToken, ...c.argv],
526
+ argv: [
527
+ bin,
528
+ "--resume",
529
+ input.resumeToken,
530
+ ...nameArg(input.sessionLabel),
531
+ ...c.argv,
532
+ ],
521
533
  env: {},
522
534
  files: c.files,
523
535
  warnings: c.warnings,
@@ -59,6 +59,11 @@ export function rotationHookOutput(
59
59
  now: Date = new Date(),
60
60
  ): string {
61
61
  if (!input || typeof input !== "object") return "";
62
+ // D28: the limit in force is the one Den keeps in rotation.json (it
63
+ // follows the settings); the launch's --after is the first one.
64
+ const stored = readRotation(args.files);
65
+ if (stored && stored.after !== args.after)
66
+ args = { ...args, after: stored.after };
62
67
  const i = input as Record<string, unknown>;
63
68
  const event = i.hook_event_name;
64
69
  const conversation =
@@ -86,6 +86,12 @@ export interface AgentRecord {
86
86
  displayName?: string;
87
87
  /** When displayName was given (ISO 8601): the latest rename wins (AG-4b). */
88
88
  displayNameAt?: string;
89
+ /**
90
+ * D24: the name Den last gave its runtime session (Claude Code's
91
+ * `--name`, peerSessionName), so reading it back is never taken for a
92
+ * rename.
93
+ */
94
+ peerName?: string;
89
95
  /**
90
96
  * The model version it runs (AG-12.3): "latest" (the model's newest), or
91
97
  * a pinned version. Absent: before AG-12, `model` as given.