@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.9

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 (130) hide show
  1. package/out/analytics/dashboard/provider.js +13 -2
  2. package/out/analytics/dashboard/webview/panel.js +3 -2
  3. package/out/analytics/identity.js +0 -0
  4. package/out/analytics/service.js +23 -25
  5. package/out/analytics/sessionEvents.js +98 -23
  6. package/out/auth/authBaseUrl.js +59 -0
  7. package/out/auth/bootstrapApply.js +9 -10
  8. package/out/auth/brokerClient.js +48 -14
  9. package/out/auth/provider.js +8 -21
  10. package/out/auth/sessionStore.js +3 -40
  11. package/out/auth/signInFlow.js +2 -1
  12. package/out/auth-broker/config.js +50 -5
  13. package/out/auth-broker/server.js +122 -26
  14. package/out/auth-broker/store.js +25 -24
  15. package/out/authority/warrant.js +206 -0
  16. package/out/branding.js +27 -1
  17. package/out/cli/commands/conformance.js +249 -0
  18. package/out/cli/commands/data-plane.js +67 -0
  19. package/out/cli/commands/deploy.js +1 -0
  20. package/out/cli/commands/gateway.js +4 -0
  21. package/out/cli/commands/pack.js +182 -3
  22. package/out/cli/commands/sandbox.js +332 -0
  23. package/out/cli/commands/serve.js +875 -52
  24. package/out/cli/commands/store.js +12 -4
  25. package/out/cli/data-plane/gateway.js +28 -5
  26. package/out/cli/data-plane/index.js +17 -2
  27. package/out/cli/data-plane/registry.js +9 -0
  28. package/out/cli/data-plane/remote.js +106 -0
  29. package/out/cli/data-plane/sidecar.js +161 -0
  30. package/out/cli/deploy/layers/identity.js +85 -4
  31. package/out/cli/deploy-init.js +89 -24
  32. package/out/cli/deps.js +106 -2
  33. package/out/cli/idp-init.js +35 -0
  34. package/out/cli/index.js +155 -16
  35. package/out/cli/pack-install-ref.js +15 -0
  36. package/out/cli/pack-loader.js +8 -1
  37. package/out/cli/pack-runtime.js +61 -0
  38. package/out/cli/runtime.js +35 -1
  39. package/out/cli/serve-hardening.js +80 -3
  40. package/out/cli/utils.js +12 -1
  41. package/out/engines/base.js +32 -3
  42. package/out/engines/claude.js +33 -15
  43. package/out/engines/codex.js +110 -26
  44. package/out/engines/custom.js +8 -0
  45. package/out/evidence/sessionShip.js +610 -0
  46. package/out/extension.js +11 -2
  47. package/out/gateway/bridge.js +40 -2
  48. package/out/gateway/client.js +34 -1
  49. package/out/gateway/installSource.js +68 -0
  50. package/out/gateway/lifecycle.js +185 -51
  51. package/out/gateway/outboundPaths.js +4 -1
  52. package/out/gateway/secretsPreflight.js +90 -0
  53. package/out/gateway/service.js +7 -2
  54. package/out/headless/index.js +17 -1
  55. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  56. package/out/mcp.js +30 -0
  57. package/out/pack/caps.js +271 -0
  58. package/out/pack/governor.js +309 -0
  59. package/out/pack/lease-schema.js +270 -0
  60. package/out/pack/lease.js +640 -0
  61. package/out/pack/reconcile.js +217 -0
  62. package/out/pack/signature.js +839 -0
  63. package/out/remote/handleRemoteChat.js +44 -3
  64. package/out/remote/remoteTrust.js +277 -0
  65. package/out/remote/serveClient.js +52 -5
  66. package/out/runner/host.js +131 -0
  67. package/out/runner/protocol.js +108 -0
  68. package/out/runner/remoteEngine.js +130 -0
  69. package/out/runtime/interfaces/edit-tracker.js +2 -0
  70. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  71. package/out/sandbox/containerDriver.js +570 -0
  72. package/out/sandbox/processDriver.js +315 -0
  73. package/out/sandbox/sandboxRunner.js +191 -0
  74. package/out/sandbox/support.js +301 -0
  75. package/out/sandbox/types.js +55 -0
  76. package/out/scripts/agentide-litellm.sh +67 -5
  77. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  78. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
  79. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  80. package/out/scripts/analytics/setup-analytics.sh +93 -13
  81. package/out/scripts/analytics/start-analytics.sh +86 -14
  82. package/out/scripts/gateway/setup-gateway.sh +72 -32
  83. package/out/shared/models.js +11 -110
  84. package/out/spotlight/install.js +7 -1
  85. package/out/store/index.js +41 -10
  86. package/out/sync.js +3 -1
  87. package/out/test/analytics.test.js +162 -23
  88. package/out/test/auth-baseurl.test.js +132 -0
  89. package/out/test/auth-bootstrapApply.test.js +27 -6
  90. package/out/test/auth-broker-hardening.test.js +227 -0
  91. package/out/test/auth-broker.test.js +59 -9
  92. package/out/test/branding.test.js +13 -0
  93. package/out/test/caps-governor.test.js +455 -0
  94. package/out/test/codex-new-file-survival.test.js +119 -0
  95. package/out/test/configuration-scopes.test.js +131 -0
  96. package/out/test/data-plane.test.js +67 -0
  97. package/out/test/deploy-init.test.js +40 -0
  98. package/out/test/gateway-client.test.js +28 -0
  99. package/out/test/gateway-install-source.test.js +111 -0
  100. package/out/test/gateway-lifecycle.test.js +1 -1
  101. package/out/test/gateway-outbound.test.js +7 -0
  102. package/out/test/gateway-secrets-preflight.test.js +127 -0
  103. package/out/test/gateway-session-routing.test.js +18 -0
  104. package/out/test/governor-hardening.test.js +217 -0
  105. package/out/test/headless-sync.test.js +22 -0
  106. package/out/test/lease-signature.test.js +357 -0
  107. package/out/test/lease-trust-root.test.js +273 -0
  108. package/out/test/lease-validation.test.js +169 -0
  109. package/out/test/lease.test.js +114 -0
  110. package/out/test/mcp.test.js +27 -1
  111. package/out/test/pack-admission.test.js +216 -0
  112. package/out/test/pack-digest-parity.test.js +140 -0
  113. package/out/test/pack-reconcile.test.js +183 -0
  114. package/out/test/pack-signature-artifact.test.js +103 -0
  115. package/out/test/pack-signature.test.js +176 -0
  116. package/out/test/pack-trust-root.test.js +202 -0
  117. package/out/test/remote-trust.test.js +242 -0
  118. package/out/test/runner-protocol.test.js +249 -0
  119. package/out/test/sandbox-driver.test.js +553 -0
  120. package/out/test/sandbox-hardening.test.js +395 -0
  121. package/out/test/sandbox-runner.test.js +343 -0
  122. package/out/test/serve-hardening.test.js +45 -6
  123. package/out/test/serveClient.test.js +6 -5
  124. package/out/test/sessionShip.test.js +560 -0
  125. package/out/test/spotlightPublish.test.js +28 -0
  126. package/out/test/store-install-safe.test.js +69 -0
  127. package/out/test/tls-bypass.test.js +90 -0
  128. package/out/test/warrant.test.js +138 -0
  129. package/out/utils.js +46 -9
  130. package/package.json +1 -1
@@ -361,6 +361,53 @@ run_install() {
361
361
  fi
362
362
  }
363
363
 
364
+ # Ensure the proxy has an auth key without ever asking the user for one.
365
+ #
366
+ # The shipped config sets general_settings.master_key from ${LITELLM_MASTER_KEY} and the launcher
367
+ # refuses to start unauthenticated, so a first run with no key leaves LiteLLM dead with the real
368
+ # error buried in a log. Generate one, persist it to the env file the launcher already sources,
369
+ # and hand the same value to clients as AGENTHIPPO_LITELLM_API_KEY.
370
+ ensure_litellm_master_key() {
371
+ local env_file="${AGENT_HOME_DIR}/.env"
372
+
373
+ if [[ -f "$env_file" ]] && grep -q '^[[:space:]]*LITELLM_MASTER_KEY=' "$env_file"; then
374
+ log_success "LiteLLM master key already configured"
375
+ return 0
376
+ fi
377
+ # Respect a key exported by the caller rather than minting a second one.
378
+ if [[ -n "${LITELLM_MASTER_KEY:-}" ]]; then
379
+ log_success "Using LITELLM_MASTER_KEY from the environment"
380
+ return 0
381
+ fi
382
+
383
+ local key=""
384
+ if command -v openssl >/dev/null 2>&1; then
385
+ key="sk-$(openssl rand -hex 24)"
386
+ elif command -v python3 >/dev/null 2>&1; then
387
+ key="sk-$(python3 -c 'import secrets; print(secrets.token_hex(24))')"
388
+ else
389
+ log_warn "Neither openssl nor python3 found; cannot generate a LiteLLM master key"
390
+ log_info "Set one manually: echo 'LITELLM_MASTER_KEY=sk-...' >> ${env_file}"
391
+ return 1
392
+ fi
393
+
394
+ mkdir -p "${AGENT_HOME_DIR}"
395
+ touch "$env_file"
396
+ chmod 600 "$env_file" 2>/dev/null || true
397
+ # Keep a trailing newline so appends never join onto a previous line.
398
+ if [[ -s "$env_file" ]] && [[ -n "$(tail -c 1 "$env_file" 2>/dev/null)" ]]; then
399
+ echo "" >> "$env_file"
400
+ fi
401
+ {
402
+ echo "# Generated by setup-analytics — authenticates the local LiteLLM proxy."
403
+ echo "# Clients read the same value as AGENTHIPPO_LITELLM_API_KEY."
404
+ echo "LITELLM_MASTER_KEY=${key}"
405
+ echo "AGENTHIPPO_LITELLM_API_KEY=${key}"
406
+ } >> "$env_file"
407
+ export LITELLM_MASTER_KEY="$key"
408
+ log_success "Generated a LiteLLM master key (stored in ${env_file}, mode 600)"
409
+ }
410
+
364
411
  # Setup configuration
365
412
  setup_config() {
366
413
  log_step "Setting up configuration..."
@@ -370,6 +417,9 @@ setup_config() {
370
417
  mkdir -p "${ANALYTICS_DIR}/config"
371
418
  mkdir -p "${ANALYTICS_DIR}/traces"
372
419
 
420
+ # Before any early return below — the launcher exits non-zero without a key.
421
+ ensure_litellm_master_key || true
422
+
373
423
  # Copy OTEL Collector config if not present
374
424
  if [[ ! -f "${ANALYTICS_DIR}/config/otel-collector.yaml" ]]; then
375
425
  if [[ -f "${SCRIPT_DIR}/otel-collector.yaml" ]]; then
@@ -411,14 +461,9 @@ setup_config() {
411
461
  log_success "LiteLLM OTEL config already present (user-owned)"
412
462
  fi
413
463
 
414
- # Keep canonical template in sync with user edits when writable.
415
- if [[ -f "$source_config" ]] && ! cmp -s "$litellm_config" "$source_config"; then
416
- if cp "$litellm_config" "$source_config" 2>/dev/null; then
417
- log_success "Synced LiteLLM OTEL config back to source template"
418
- else
419
- log_info "Using user-owned LiteLLM config (source template is read-only)"
420
- fi
421
- fi
464
+ # Deliberately ONE-WAY (template -> user). Copying the user's config back over the shipped
465
+ # template wrote their edits into the installed extension / a git-tracked file, and model
466
+ # entries can carry a literal upstream `api_key` — a credential leak into source control.
422
467
  # Install LiteLLM hook for tool order sanitizer
423
468
  local hooks_target="${AGENT_HOME_DIR}/agentide_litellm_hooks_v1.py"
424
469
  local hooks_source=""
@@ -501,9 +546,34 @@ check_litellm() {
501
546
  fi
502
547
  }
503
548
 
504
- # Get LiteLLM PIDs
549
+ # Get LiteLLM PIDs.
550
+ #
551
+ # Prefer the PID the launcher recorded: `pgrep -f litellm` matches ANY command line containing
552
+ # the word, so on a shared or CI host it happily returns a colleague's proxy, an editor with the
553
+ # string in its argv, or a log tail — and stop_litellm SIGKILLs whatever it returns. The broad
554
+ # match stays only as a fallback when no PID file exists, and is scoped to a real proxy invocation.
505
555
  get_litellm_pids() {
506
- pgrep -f "litellm" 2>/dev/null || true
556
+ local pid_file="${AGENT_HOME_DIR}/litellm.pid"
557
+ if [[ -f "$pid_file" ]]; then
558
+ local recorded
559
+ recorded="$(head -n 1 "$pid_file" | tr -d '\r' | tr -d '[:space:]')"
560
+ if [[ "$recorded" =~ ^[0-9]+$ ]] && kill -0 "$recorded" 2>/dev/null; then
561
+ echo "$recorded"
562
+ return 0
563
+ fi
564
+ fi
565
+ # Fallback for an orphaned proxy with no PID file. Two filters, because "litellm --config"
566
+ # also appears inside the lifecycle wrapper's own script text: require the config invocation,
567
+ # then drop shell wrappers (and this script) so we never SIGKILL the thing that started us.
568
+ local pid cmd
569
+ for pid in $(pgrep -f "litellm.*--config" 2>/dev/null || true); do
570
+ [[ "$pid" == "$$" ]] && continue
571
+ cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)"
572
+ case "$cmd" in
573
+ */sh\ -c*|*/bash\ -c*|*/zsh\ -c*|sh\ -c*|bash\ -c*|zsh\ -c*) continue ;;
574
+ esac
575
+ echo "$pid"
576
+ done
507
577
  }
508
578
 
509
579
  # Stop LiteLLM gracefully
@@ -564,9 +634,12 @@ start_litellm_with_otel() {
564
634
  # Write PID file with OTEL flag + base URL for fast status checks
565
635
  local pid_file="${AGENT_HOME_DIR}/litellm.pid"
566
636
  local litellm_host="${LITELLM_HOST:-127.0.0.1}"
567
- local tls_enabled=true
568
- case "${LITELLM_ENABLE_TLS:-true}" in
569
- 0|false|no|off) tls_enabled=false ;;
637
+ # Must match agentide-litellm.sh, which defaults TLS OFF (port 4000). Defaulting to true here
638
+ # recorded https://…:4443 in the PID file while the proxy actually bound http://…:4000, and
639
+ # the extension then persisted that unreachable URL as the global litellm baseUrl.
640
+ local tls_enabled=false
641
+ case "${LITELLM_ENABLE_TLS:-false}" in
642
+ 1|true|yes|on) tls_enabled=true ;;
570
643
  esac
571
644
  local litellm_port="${LITELLM_PORT:-}"
572
645
  if [[ -z "$litellm_port" ]]; then
@@ -593,6 +666,13 @@ start_litellm_with_otel() {
593
666
  return 0
594
667
  else
595
668
  log_error "Failed to start LiteLLM. Check /tmp/litellm-otel.log"
669
+ # Surface the actual reason — the IDE shows this output, and "check a
670
+ # log file" is a dead end for non-technical users.
671
+ if [[ -f /tmp/litellm-otel.log ]]; then
672
+ echo ""
673
+ log_info "Last 10 lines of log:"
674
+ tail -10 /tmp/litellm-otel.log 2>/dev/null || true
675
+ fi
596
676
  return 1
597
677
  fi
598
678
  }
@@ -61,6 +61,25 @@ is_running() {
61
61
  return 1
62
62
  }
63
63
 
64
+ # OTLP receiver ports, read from the collector config the daemon will run with,
65
+ # so the preflight can never drift from what the collector actually binds.
66
+ # Falls back to the OTLP defaults if the config is missing or unparseable.
67
+ get_collector_ports() {
68
+ local ports=""
69
+ if [[ -f "${CONFIG_FILE}" ]]; then
70
+ ports=$(awk '
71
+ /^receivers:/ { in_receivers = 1; next }
72
+ /^[^[:space:]#]/ { in_receivers = 0 }
73
+ in_receivers && /endpoint:/ && match($0, /:[0-9]+[[:space:]]*$/) {
74
+ port = substr($0, RSTART + 1, RLENGTH - 1)
75
+ gsub(/[[:space:]]/, "", port)
76
+ print port
77
+ }
78
+ ' "${CONFIG_FILE}" 2>/dev/null | sort -un | tr '\n' ' ')
79
+ fi
80
+ echo "${ports:-4317 4318}"
81
+ }
82
+
64
83
  # Check if port is in use (returns 0 if in use)
65
84
  port_in_use() {
66
85
  local port=$1
@@ -100,12 +119,13 @@ find_orphan_otelcol() {
100
119
  # Kill orphan collector if user confirms
101
120
  handle_orphan_collector() {
102
121
  local orphan_pid=$1
122
+ local port="${2:-4317}"
103
123
  local orphan_cmdline=$(ps -p "$orphan_pid" -o command= 2>/dev/null || echo "unknown")
104
124
 
105
125
  log_warn "Found orphan OTEL Collector (PID: ${orphan_pid})"
106
126
  log_warn "Command: ${orphan_cmdline}"
107
127
 
108
- if [[ "${FORCE:-}" == "1" ]] || [[ "${1:-}" == "--force" ]]; then
128
+ if [[ "${FORCE:-}" == "1" ]]; then
109
129
  log_info "Killing orphan process..."
110
130
  kill "${orphan_pid}" 2>/dev/null || true
111
131
  sleep 1
@@ -115,7 +135,7 @@ handle_orphan_collector() {
115
135
  return 0
116
136
  fi
117
137
 
118
- log_error "Port 4317 is in use by an orphan collector."
138
+ log_error "Port ${port} is in use by an orphan collector."
119
139
  log_error "This may be a leftover from a previous installation."
120
140
  echo ""
121
141
  echo "To fix, run one of:"
@@ -125,6 +145,37 @@ handle_orphan_collector() {
125
145
  return 1
126
146
  }
127
147
 
148
+ # Explain, in plain language, who owns a port we need and how to free it.
149
+ # Docker publishes container ports through its own proxy process, so name the
150
+ # container (the thing the user can act on) rather than "com.docker.backend".
151
+ report_port_conflict() {
152
+ local port=$1
153
+ local owner="another program"
154
+ local fix="Free the port (check with: lsof -i :${port}), then run Setup Spotlight again."
155
+
156
+ if command -v lsof &>/dev/null; then
157
+ local line=$(lsof -nP -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null | awk 'NR==2 {print $1, $2}')
158
+ if [[ -n "$line" ]]; then
159
+ local cmd="${line% *}"
160
+ local pid="${line##* }"
161
+ owner="'${cmd}' (PID ${pid})"
162
+ fix="Stop that program (or run: kill ${pid}), then run Setup Spotlight again."
163
+ if [[ "$cmd" == com.docke* || "$cmd" == *docker* ]] && command -v docker &>/dev/null; then
164
+ local container=$(docker ps --filter "publish=${port}" --format '{{.Names}}' 2>/dev/null | head -1)
165
+ if [[ -n "$container" ]]; then
166
+ owner="Docker container '${container}'"
167
+ fix="Stop it with: docker stop ${container} then run Setup Spotlight again."
168
+ fi
169
+ fi
170
+ fi
171
+ fi
172
+
173
+ log_error "Cannot start Spotlight: port ${port} is already in use by ${owner}."
174
+ echo ""
175
+ echo " To fix: ${fix}"
176
+ echo ""
177
+ }
178
+
128
179
  # Start in foreground
129
180
  start_foreground() {
130
181
  local otelcol=$(find_otelcol)
@@ -149,12 +200,18 @@ start_daemon() {
149
200
 
150
201
  local otelcol=$(find_otelcol)
151
202
 
152
- # Check for port conflicts before starting
153
- if port_in_use 4317; then
203
+ # Check for port conflicts before starting. The collector needs every
204
+ # receiver port in its config — a conflict on any of them kills it at bind
205
+ # time with the real error buried in the log, so fail here with a clear message.
206
+ local port
207
+ for port in $(get_collector_ports); do
208
+ if ! port_in_use "$port"; then
209
+ continue
210
+ fi
154
211
  local orphan_pid=$(find_orphan_otelcol || true)
155
212
  if [[ -n "$orphan_pid" ]]; then
156
213
  if [[ "${FORCE:-}" == "1" ]]; then
157
- log_warn "Port 4317 in use by orphan process (PID: ${orphan_pid}). Killing..."
214
+ log_warn "Port ${port} in use by orphan process (PID: ${orphan_pid}). Killing..."
158
215
  kill "${orphan_pid}" 2>/dev/null || true
159
216
  sleep 1
160
217
  # Force kill if still running
@@ -162,16 +219,21 @@ start_daemon() {
162
219
  kill -9 "${orphan_pid}" 2>/dev/null || true
163
220
  sleep 1
164
221
  fi
222
+ # The orphan may not have been the port owner (e.g. a container
223
+ # holds it) — re-check before declaring the port free.
224
+ if port_in_use "$port"; then
225
+ report_port_conflict "$port"
226
+ exit 1
227
+ fi
165
228
  else
166
- handle_orphan_collector "$orphan_pid"
229
+ handle_orphan_collector "$orphan_pid" "$port"
167
230
  exit 1
168
231
  fi
169
232
  else
170
- log_error "Port 4317 is in use by another process."
171
- log_error "Check with: lsof -i :4317"
233
+ report_port_conflict "$port"
172
234
  exit 1
173
235
  fi
174
- fi
236
+ done
175
237
 
176
238
  # Ensure directories exist
177
239
  mkdir -p "${ANALYTICS_DIR}/traces"
@@ -260,12 +322,22 @@ show_status() {
260
322
  echo " kill ${orphan_pid} && $0 --daemon"
261
323
  echo " Or auto-fix with:"
262
324
  echo " FORCE=1 $0 --daemon"
263
- elif port_in_use 4317; then
264
- echo -e "OTEL Collector: ${RED}Port Conflict${NC}"
265
- echo -e " ${RED}⚠ Port 4317 in use by another process${NC}"
266
- echo " Check with: lsof -i :4317"
267
325
  else
268
- echo -e "OTEL Collector: ${RED}Stopped${NC}"
326
+ local conflict_port=""
327
+ local port
328
+ for port in $(get_collector_ports); do
329
+ if port_in_use "$port"; then
330
+ conflict_port=$port
331
+ break
332
+ fi
333
+ done
334
+ if [[ -n "$conflict_port" ]]; then
335
+ echo -e "OTEL Collector: ${RED}Port Conflict${NC}"
336
+ echo -e " ${RED}⚠ Port ${conflict_port} in use by another process${NC}"
337
+ echo " Check with: lsof -i :${conflict_port}"
338
+ else
339
+ echo -e "OTEL Collector: ${RED}Stopped${NC}"
340
+ fi
269
341
  fi
270
342
  fi
271
343
  echo ""
@@ -162,6 +162,32 @@ MANAGED_GATEWAY_WRAPPER_DIR="${HOME}/.local/bin"
162
162
  AGENT_ANYWHERE_PATH_MARKER_BEGIN="# >>> Agent Anywhere PATH >>>"
163
163
  AGENT_ANYWHERE_PATH_MARKER_END="# <<< Agent Anywhere PATH <<<"
164
164
  MANAGED_GATEWAY_WRAPPER_MARKER="# Managed by Agent Anywhere"
165
+ HIPPOCLAW_NPM_PACKAGE="@agenthippo-ai/hippoclaw"
166
+
167
+ # Global npm trees: scoped HippoClaw first, legacy openclaw fallback.
168
+ global_gateway_package_dir_candidates() {
169
+ local npm_root="${1:-}"
170
+ local lib_base="${2:-}"
171
+ if [[ -n "$npm_root" ]]; then
172
+ printf '%s\n' "$npm_root/@agenthippo-ai/hippoclaw" "$npm_root/openclaw"
173
+ fi
174
+ if [[ -n "$lib_base" ]]; then
175
+ printf '%s\n' "$lib_base/@agenthippo-ai/hippoclaw" "$lib_base/openclaw"
176
+ fi
177
+ }
178
+
179
+ remove_global_gateway_package_trees() {
180
+ local npm_root prefix pkg_dir
181
+ npm_root="$(npm root -g 2>/dev/null || true)"
182
+ prefix="$(npm prefix -g 2>/dev/null || true)"
183
+ while IFS= read -r pkg_dir; do
184
+ [[ -n "$pkg_dir" && -e "$pkg_dir" ]] || continue
185
+ rm -rf "$pkg_dir"
186
+ done < <(
187
+ global_gateway_package_dir_candidates "$npm_root" "${prefix:+$prefix/lib/node_modules}"
188
+ global_gateway_package_dir_candidates "" "$NPM_GLOBAL_PREFIX/lib/node_modules"
189
+ )
190
+ }
165
191
 
166
192
  add_path_entry() {
167
193
  local entry="$1"
@@ -514,15 +540,15 @@ resolve_managed_gateway_install() {
514
540
  if [[ -n "$current_node" ]]; then
515
541
  npm_root="$(npm root -g 2>/dev/null || true)"
516
542
  prefix="$(npm prefix -g 2>/dev/null || true)"
517
- package_root="${npm_root}/openclaw"
518
- if [[ -n "$npm_root" && -n "$prefix" && -d "$package_root" ]]; then
543
+ while IFS= read -r package_root; do
544
+ [[ -n "$npm_root" && -n "$prefix" && -d "$package_root" ]] || continue
519
545
  resolve_managed_gateway_install_from_root "$current_node" "$package_root" "${prefix}/bin" && return 0
520
- fi
546
+ done < <(global_gateway_package_dir_candidates "$npm_root" "${prefix:+$prefix/lib/node_modules}")
521
547
 
522
- package_root="${NPM_GLOBAL_PREFIX}/lib/node_modules/openclaw"
523
- if [[ -d "$package_root" ]]; then
548
+ while IFS= read -r package_root; do
549
+ [[ -d "$package_root" ]] || continue
524
550
  resolve_managed_gateway_install_from_root "$current_node" "$package_root" "${NPM_GLOBAL_PREFIX}/bin" && return 0
525
- fi
551
+ done < <(global_gateway_package_dir_candidates "" "$NPM_GLOBAL_PREFIX/lib/node_modules")
526
552
  fi
527
553
 
528
554
  local candidate_node candidate_dir install_dir
@@ -530,10 +556,10 @@ resolve_managed_gateway_install() {
530
556
  while IFS= read -r candidate_node; do
531
557
  candidate_dir="${candidate_node%/node}"
532
558
  install_dir="$(dirname "$candidate_dir")"
533
- package_root="${install_dir}/lib/node_modules/openclaw"
534
- if [[ -d "$package_root" ]]; then
559
+ while IFS= read -r package_root; do
560
+ [[ -d "$package_root" ]] || continue
535
561
  resolve_managed_gateway_install_from_root "$candidate_node" "$package_root" "$candidate_dir" && return 0
536
- fi
562
+ done < <(global_gateway_package_dir_candidates "" "$install_dir/lib/node_modules")
537
563
  done < <(find "$NODE_BOOTSTRAP_ROOT" -maxdepth 4 -type f -path '*/bin/node' 2>/dev/null | sort -r)
538
564
  return 1
539
565
  }
@@ -845,7 +871,7 @@ install_openclaw_from_remote_git_spec() {
845
871
  }
846
872
 
847
873
  local tgz
848
- tgz="$(ls -t "$work/src"/openclaw-*.tgz 2>/dev/null | head -1)"
874
+ tgz="$(ls -t "$work/src"/agenthippo-ai-hippoclaw-*.tgz "$work/src"/openclaw-*.tgz 2>/dev/null | head -1)"
849
875
  if [[ -z "$tgz" || ! -f "$tgz" ]]; then
850
876
  log_error "npm pack did not produce openclaw-*.tgz under clone"
851
877
  rm -rf "$work"
@@ -1047,11 +1073,15 @@ check_installed() {
1047
1073
  groot="$(resolve_managed_gateway_package_root_from_manifest || true)"
1048
1074
  if [[ -z "$groot" || ! -d "$groot" ]]; then
1049
1075
  npm_root="$(npm root -g 2>/dev/null || true)"
1050
- groot=""
1051
- [[ -n "$npm_root" ]] && groot="$npm_root/openclaw"
1052
- if [[ -z "$groot" || ! -d "$groot" ]]; then
1053
- groot="$NPM_GLOBAL_PREFIX/lib/node_modules/openclaw"
1054
- fi
1076
+ prefix="$(npm prefix -g 2>/dev/null || true)"
1077
+ while IFS= read -r candidate; do
1078
+ [[ -d "$candidate" ]] || continue
1079
+ groot="$candidate"
1080
+ break
1081
+ done < <(
1082
+ global_gateway_package_dir_candidates "$npm_root" "${prefix:+$prefix/lib/node_modules}"
1083
+ global_gateway_package_dir_candidates "" "$NPM_GLOBAL_PREFIX/lib/node_modules"
1084
+ )
1055
1085
  fi
1056
1086
  if [[ -n "$groot" && -d "$groot" ]]; then
1057
1087
  if [[ -f "$groot/dist/entry.mjs" || -f "$groot/dist/entry.js" ]]; then
@@ -1119,7 +1149,11 @@ check_uninstalled() {
1119
1149
  local groot=""
1120
1150
  groot="$(resolve_managed_gateway_package_root_from_manifest || true)"
1121
1151
  if [[ -z "$groot" || ! -d "$groot" ]]; then
1122
- groot="$NPM_GLOBAL_PREFIX/lib/node_modules/openclaw"
1152
+ while IFS= read -r candidate; do
1153
+ [[ -d "$candidate" ]] || continue
1154
+ groot="$candidate"
1155
+ break
1156
+ done < <(global_gateway_package_dir_candidates "" "$NPM_GLOBAL_PREFIX/lib/node_modules")
1123
1157
  fi
1124
1158
  if [[ -n "$groot" && -d "$groot" ]]; then
1125
1159
  log_warn "Managed npm openclaw package still present ($groot)"
@@ -1169,9 +1203,9 @@ install_gateway() {
1169
1203
 
1170
1204
  local _npm_root
1171
1205
  _npm_root="$(npm root -g 2>/dev/null || true)"
1172
- if [[ -n "$_npm_root" && -e "$_npm_root/openclaw" ]]; then
1173
- log_info "Removing existing global openclaw tree (avoids partial install / tar errors)..."
1174
- rm -rf "$_npm_root/openclaw"
1206
+ if [[ -n "$_npm_root" ]]; then
1207
+ log_info "Removing existing global gateway package trees (if present)..."
1208
+ remove_global_gateway_package_trees
1175
1209
  fi
1176
1210
 
1177
1211
  # Local directory: npm links to that path (stable). Remote github:/git+: use clone+pack+tgz
@@ -1270,8 +1304,13 @@ remove_managed_openclaw_artifacts() {
1270
1304
  local dir install_dir pkg_root
1271
1305
 
1272
1306
  pkg_root="$NPM_GLOBAL_PREFIX/lib/node_modules/openclaw"
1273
- if [[ -d "$pkg_root" ]]; then
1274
- log_info "Removing managed openclaw package from $pkg_root"
1307
+ if [[ -d "$NPM_GLOBAL_PREFIX/lib/node_modules/@agenthippo-ai/hippoclaw" ]]; then
1308
+ pkg_root="$NPM_GLOBAL_PREFIX/lib/node_modules/@agenthippo-ai/hippoclaw"
1309
+ elif [[ ! -d "$pkg_root" ]]; then
1310
+ pkg_root=""
1311
+ fi
1312
+ if [[ -n "$pkg_root" && -d "$pkg_root" ]]; then
1313
+ log_info "Removing managed gateway package from $pkg_root"
1275
1314
  rm -rf "$pkg_root"
1276
1315
  fi
1277
1316
  rm -f "$NPM_GLOBAL_PREFIX/bin/openclaw" "$NPM_GLOBAL_PREFIX/bin/hippoclaw" 2>/dev/null || true
@@ -1279,11 +1318,11 @@ remove_managed_openclaw_artifacts() {
1279
1318
  [[ -d "$NODE_BOOTSTRAP_ROOT" ]] || return 0
1280
1319
  while IFS= read -r dir; do
1281
1320
  install_dir="$(dirname "$dir")"
1282
- pkg_root="$install_dir/lib/node_modules/openclaw"
1283
- if [[ -d "$pkg_root" ]]; then
1284
- log_info "Removing managed openclaw package from $pkg_root"
1285
- rm -rf "$pkg_root"
1286
- fi
1321
+ while IFS= read -r candidate; do
1322
+ [[ -d "$candidate" ]] || continue
1323
+ log_info "Removing managed gateway package from $candidate"
1324
+ rm -rf "$candidate"
1325
+ done < <(global_gateway_package_dir_candidates "" "$install_dir/lib/node_modules")
1287
1326
  rm -f "$dir/openclaw" "$dir/hippoclaw" 2>/dev/null || true
1288
1327
  done < <(
1289
1328
  find "$NODE_BOOTSTRAP_ROOT" -maxdepth 4 -type f -path '*/bin/node' -print 2>/dev/null \
@@ -1306,12 +1345,13 @@ remove_resolved_cli_openclaw_install() {
1306
1345
  node_bin_dir="$(cd "$(dirname "$cli_bin")" 2>/dev/null && pwd)" || break
1307
1346
  # npm global layout: <prefix>/bin/<cli> and <prefix>/lib/node_modules/openclaw.
1308
1347
  prefix="$(dirname "$node_bin_dir")"
1309
- pkg_root="$prefix/lib/node_modules/openclaw"
1310
- if [[ -d "$pkg_root" ]]; then
1311
- log_info "Removing openclaw package tree resolved via CLI on PATH: $pkg_root"
1348
+ while IFS= read -r pkg_root; do
1349
+ [[ -d "$pkg_root" ]] || continue
1350
+ log_info "Removing gateway package tree resolved via CLI on PATH: $pkg_root"
1312
1351
  rm -rf "$pkg_root" 2>/dev/null || true
1313
1352
  removed_any=true
1314
- fi
1353
+ break
1354
+ done < <(global_gateway_package_dir_candidates "" "$prefix/lib/node_modules")
1315
1355
  rm -f "$node_bin_dir/openclaw" "$node_bin_dir/hippoclaw" 2>/dev/null || true
1316
1356
  # Bail if the same path still resolves (could not delete, e.g. permissions) to avoid looping.
1317
1357
  hash -r 2>/dev/null || true
@@ -1393,13 +1433,13 @@ uninstall_gateway() {
1393
1433
  log_error "npm not found"
1394
1434
  exit 1
1395
1435
  fi
1396
- npm uninstall -g openclaw 2>/dev/null || true
1436
+ npm uninstall -g @agenthippo-ai/hippoclaw openclaw 2>/dev/null || true
1397
1437
  remove_managed_gateway_wrapper_state
1398
1438
  cleanup_shell_path_setup
1399
1439
  # Also remove the install wherever the CLI actually resolves (nvm/other Node global trees),
1400
1440
  # not just the active node + managed prefix that `npm uninstall -g` above covers.
1401
1441
  remove_resolved_cli_openclaw_install
1402
- log_success "Ran: npm uninstall -g openclaw (global tree removed if present)"
1442
+ log_success "Ran: npm uninstall -g @agenthippo-ai/hippoclaw openclaw (global tree removed if present)"
1403
1443
  log_info "~/.openclaw was not removed (config, sessions, and credentials stay on disk)."
1404
1444
  local oc_after=""
1405
1445
  hash -r 2>/dev/null || true
@@ -30,72 +30,15 @@ exports.BUILTIN_MODELS = [
30
30
  contextWindow: 200000,
31
31
  maxOutputTokens: 8192,
32
32
  capabilities: ['chat', 'vision', 'function-calling', 'code'],
33
- description: 'AgentHippo automatically picks the best model for your request',
33
+ // The picker must say where the prompt goes: this route leaves the machine through
34
+ // AgentHippo's servers on the account JWT, NOT through any API key the user configured.
35
+ description: 'Managed gateway — routed through AgentHippo using your account, not your own API keys',
34
36
  recommended: true,
35
37
  },
36
- // ===========================================
37
- // OpenAI Models
38
- // ===========================================
39
- {
40
- id: 'gpt-4.1-nano',
41
- name: 'GPT-4.1 Nano',
42
- provider: 'openai',
43
- contextWindow: 128000,
44
- maxOutputTokens: 2048,
45
- capabilities: ['chat', 'vision', 'function-calling', 'code'],
46
- description: 'Lightweight GPT-4.1 option for quick completions',
47
- },
48
- {
49
- id: 'gpt-5.3-codex',
50
- name: 'GPT-5.3 Codex (ChatGPT)',
51
- provider: 'openai-codex',
52
- contextWindow: 256000,
53
- maxOutputTokens: 8192,
54
- capabilities: ['chat', 'code', 'function-calling'],
55
- description: 'Codex model authenticated via ChatGPT OAuth subscription',
56
- recommended: true,
57
- },
58
- // ===========================================
59
- // LiteLLM Proxy Models
60
- // These route through your LiteLLM deployment
61
- // ===========================================
62
- {
63
- id: 'litellm/gpt-4.1-nano',
64
- name: 'LiteLLM GPT-4.1 Nano',
65
- provider: 'litellm',
66
- contextWindow: 128000,
67
- maxOutputTokens: 2048,
68
- capabilities: ['chat', 'vision', 'function-calling', 'code'],
69
- description: 'GPT-4.1 Nano via LiteLLM proxy',
70
- },
71
- {
72
- id: 'litellm/gpt-5-nano',
73
- name: 'LiteLLM GPT-5 Nano',
74
- provider: 'litellm',
75
- contextWindow: 256000,
76
- maxOutputTokens: 8192,
77
- capabilities: ['chat', 'vision', 'function-calling', 'code'],
78
- description: 'GPT-5 Nano via LiteLLM proxy',
79
- },
80
- // Claude Models: Claude Code will auto translate models into dated aliases
81
- {
82
- id: 'claude-sonnet-4-5-20250929',
83
- name: 'Claude Sonnet 4.5 (SDK alias)',
84
- provider: 'litellm',
85
- contextWindow: 200000,
86
- maxOutputTokens: 8192,
87
- capabilities: ['chat', 'vision', 'artifacts', 'code'],
88
- description: 'Compatibility alias used by Claude Agent SDK (maps to Claude 3.5 Sonnet)',
89
- },
90
- {
91
- id: 'claude-haiku-4-5-20251001',
92
- name: 'Claude Haiku 4.5 20251001',
93
- provider: 'litellm',
94
- contextWindow: 200000,
95
- maxOutputTokens: 8192,
96
- capabilities: ['chat', 'vision', 'artifacts', 'code'],
97
- description: 'Compatibility alias used by Claude Agent SDK (maps to Claude 3 Haiku)',
98
- },
38
+ // LiteLLM models are NOT hardcoded here.
39
+ // They come from two dynamic sources:
40
+ // 1. Auto-discovery via LiteLLM's /model/info endpoint (require explicit enablement)
41
+ // 2. User-defined custom models in agentide.models.custom
99
42
  ];
100
43
  // -----------------------------------------------------------------------------
101
44
  // Registry Key Functions
@@ -178,58 +121,16 @@ function getActiveModels() {
178
121
  // Model Resolution (for CLI)
179
122
  // -----------------------------------------------------------------------------
180
123
  /**
181
- * Common model aliases that users might type.
182
- * Maps short names to full SDK-compatible model IDs.
183
- */
184
- const MODEL_ALIASES = {
185
- // Claude aliases
186
- 'claude-sonnet-4': 'claude-sonnet-4-5-20250929',
187
- 'claude-sonnet': 'claude-sonnet-4-5-20250929',
188
- 'sonnet': 'claude-sonnet-4-5-20250929',
189
- 'claude-haiku-4': 'claude-haiku-4-5-20251001',
190
- 'claude-haiku': 'claude-haiku-4-5-20251001',
191
- 'haiku': 'claude-haiku-4-5-20251001',
192
- 'claude-opus-4': 'claude-opus-4-20250514',
193
- 'claude-opus': 'claude-opus-4-20250514',
194
- 'opus': 'claude-opus-4-20250514',
195
- // OpenAI aliases (these work as-is with SDK, but include for consistency)
196
- 'gpt-4o': 'gpt-4o',
197
- 'gpt-4': 'gpt-4o',
198
- 'o3': 'o3',
199
- 'o1': 'o1',
200
- };
201
- /**
202
- * Resolve a model alias to a full model ID.
124
+ * Normalize a model id for CLI/headless use.
203
125
  *
204
- * Resolution order:
205
- * 1. Check if it's an exact match in BUILTIN_MODELS
206
- * 2. Check if it's a known alias
207
- * 3. Check if any model ID starts with the input (prefix match)
208
- * 4. Return as-is (let SDK handle it)
209
- *
210
- * @param input - User-provided model name (e.g., 'claude-sonnet-4', 'sonnet')
211
- * @returns Resolved model ID suitable for SDK
126
+ * Only canonicalizes ids that match a built-in catalog entry (currently `ah-auto`).
127
+ * All other ids are passed through unchanged for LiteLLM, pack specs, or provider SDKs.
212
128
  */
213
129
  function resolveModelAlias(input) {
214
130
  if (!input) {
215
131
  return input;
216
132
  }
217
133
  const normalized = input.toLowerCase().trim();
218
- // 1. Exact match in BUILTIN_MODELS
219
134
  const exactMatch = exports.BUILTIN_MODELS.find(m => m.id.toLowerCase() === normalized);
220
- if (exactMatch) {
221
- return exactMatch.id;
222
- }
223
- // 2. Known alias
224
- const aliasMatch = MODEL_ALIASES[normalized];
225
- if (aliasMatch) {
226
- return aliasMatch;
227
- }
228
- // 3. Prefix match (e.g., 'claude-sonnet-4' matches 'claude-sonnet-4-5-20250929')
229
- const prefixMatch = exports.BUILTIN_MODELS.find(m => m.id.toLowerCase().startsWith(normalized));
230
- if (prefixMatch) {
231
- return prefixMatch.id;
232
- }
233
- // 4. Return as-is
234
- return input;
135
+ return exactMatch?.id ?? input;
235
136
  }
@@ -194,7 +194,13 @@ async function installSpotlightViewFromFiles(files, options) {
194
194
  if (!INSTALLABLE_ROOT_FILES.has(relPath) && !INSTALLABLE_PREFIXES.some(prefix => relPath.startsWith(prefix))) {
195
195
  continue;
196
196
  }
197
- const destination = path.join(installDir, relPath);
197
+ // The prefix check above is on the raw string — "scripts/../../x" passes it. Resolve and
198
+ // require containment so a store bundle can never write outside its own install dir.
199
+ const resolvedInstallDir = path.resolve(installDir);
200
+ const destination = path.resolve(resolvedInstallDir, relPath);
201
+ if (!destination.startsWith(resolvedInstallDir + path.sep)) {
202
+ throw new Error(`Store bundle entry "${relPath}" escapes the install directory`);
203
+ }
198
204
  await fs_1.promises.mkdir(path.dirname(destination), { recursive: true });
199
205
  await fs_1.promises.writeFile(destination, content);
200
206
  }