@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.
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +3 -2
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +23 -25
- package/out/analytics/sessionEvents.js +98 -23
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +48 -14
- package/out/auth/provider.js +8 -21
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +50 -5
- package/out/auth-broker/server.js +122 -26
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +67 -0
- package/out/cli/commands/deploy.js +1 -0
- package/out/cli/commands/gateway.js +4 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +332 -0
- package/out/cli/commands/serve.js +875 -52
- package/out/cli/commands/store.js +12 -4
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +17 -2
- package/out/cli/data-plane/registry.js +9 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +161 -0
- package/out/cli/deploy/layers/identity.js +85 -4
- package/out/cli/deploy-init.js +89 -24
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +35 -0
- package/out/cli/index.js +155 -16
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +8 -1
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +35 -1
- package/out/cli/serve-hardening.js +80 -3
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +33 -15
- package/out/engines/codex.js +110 -26
- package/out/engines/custom.js +8 -0
- package/out/evidence/sessionShip.js +610 -0
- package/out/extension.js +11 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +17 -1
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/mcp.js +30 -0
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +309 -0
- package/out/pack/lease-schema.js +270 -0
- package/out/pack/lease.js +640 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +131 -0
- package/out/runner/protocol.js +108 -0
- package/out/runner/remoteEngine.js +130 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +570 -0
- package/out/sandbox/processDriver.js +315 -0
- package/out/sandbox/sandboxRunner.js +191 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
- package/out/scripts/analytics/setup-analytics.ps1 +37 -13
- package/out/scripts/analytics/setup-analytics.sh +93 -13
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +11 -110
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +162 -23
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +59 -9
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +455 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane.test.js +67 -0
- package/out/test/deploy-init.test.js +40 -0
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +217 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +169 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/mcp.test.js +27 -1
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +395 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/serve-hardening.test.js +45 -6
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +560 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +46 -9
- 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
|
-
#
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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" ]]
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
171
|
-
log_error "Check with: lsof -i :4317"
|
|
233
|
+
report_port_conflict "$port"
|
|
172
234
|
exit 1
|
|
173
235
|
fi
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
518
|
-
|
|
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
|
-
|
|
546
|
+
done < <(global_gateway_package_dir_candidates "$npm_root" "${prefix:+$prefix/lib/node_modules}")
|
|
521
547
|
|
|
522
|
-
package_root
|
|
523
|
-
|
|
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
|
-
|
|
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
|
|
534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
groot="$
|
|
1054
|
-
|
|
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
|
-
|
|
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"
|
|
1173
|
-
log_info "Removing existing global
|
|
1174
|
-
|
|
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 "$
|
|
1274
|
-
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
log_info "Removing managed
|
|
1285
|
-
rm -rf "$
|
|
1286
|
-
|
|
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
|
|
1310
|
-
|
|
1311
|
-
log_info "Removing
|
|
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
|
-
|
|
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
|
package/out/shared/models.js
CHANGED
|
@@ -30,72 +30,15 @@ exports.BUILTIN_MODELS = [
|
|
|
30
30
|
contextWindow: 200000,
|
|
31
31
|
maxOutputTokens: 8192,
|
|
32
32
|
capabilities: ['chat', 'vision', 'function-calling', 'code'],
|
|
33
|
-
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
*
|
|
205
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/out/spotlight/install.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|