Files
OpenHands/docker/entrypoint.sh
T
cbeeee002e fix: inject runtime session key into index.html for published binary (#795)
* fix: inject runtime session key into index.html for published binary

The globally installed agent-canvas binary starts the agent-server with a
persisted session API key (~/.openhands/agent-canvas/session-api-key.txt)
as OH_SESSION_API_KEYS_0, making auth required. However, the pre-built
static frontend in the npm package has a different (or empty)
VITE_SESSION_API_KEY baked in at publish time, so every API request gets
401 Unauthorized.

Fix: static-server.mjs now accepts --session-api-key <key> and injects a
tiny bootstrap <script> before </head> in every index.html response. The
script seeds the key into localStorage['openhands-agent-server-config']
only if no key is already stored there, so explicit user overrides (via
Settings > Agent Server) are always preserved.

dev-with-automation.mjs and dev-static.mjs both pass
--session-api-key ${config.sessionApiKey} when spawning the static server,
so the runtime key is always available regardless of what was baked into
the bundle.

Tests: added 8 new cases to __tests__/scripts/static-server.test.ts
covering parseArgs, injection in direct and SPA-fallback index.html
responses, no injection for non-html assets, cache headers, and null key.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(docker): pass runtime session key to static-server so frontend can authenticate

The entrypoint computed EFFECTIVE_SESSION_KEY and forwarded it to the
agent-server (OH_SESSION_API_KEYS_0) and automation backends, but did not
pass it to the static-server. As a result the pre-built index.html served
with no session key injected, so every browser API call received 401.

Wire --session-api-key "$EFFECTIVE_SESSION_KEY" into the static-server
launch command so the runtime key is injected into index.html responses
(via the mechanism added in this branch to static-server.mjs).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address review suggestions on session key injection

- static-server.mjs: add comment clarifying replace() targets first
  </head> only; fall back to inserting before </body> when </head> is
  absent (avoids prepending before <!DOCTYPE html>)
- docker/entrypoint.sh: add comment documenting source of
  EFFECTIVE_SESSION_KEY before the static-server invocation
- static-server.test.ts: add test for </head>-absent fallback path
  confirming injection lands before </body>

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2026-05-27 11:12:23 -04:00

208 lines
9.3 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════════════════
# agent-canvas all-in-one entrypoint
#
# Starts three services:
# 1. Agent Server on port $AGENT_SERVER_PORT (default 18000)
# 2. Automation on port $AUTOMATION_PORT (default 18001)
# 3. Static server on port $PORT (default 8000)
# Routes /api/automation/* → automation, /api/* → agent-server,
# and serves the frontend static build for everything else.
#
# Environment variables:
# PORT – Unified entry point port (default: 8000)
# AGENT_SERVER_PORT – Internal agent-server port (default: 18000)
# AUTOMATION_PORT – Internal automation port (default: 18001)
# OH_SECRET_KEY – Secret key for settings encryption (auto-generated
# and persisted if not provided)
# OPENHANDS_AUTOMATION_API_KEY – Override automation backend auth key
# (defaults to session API key — both backends
# use the same `X-Session-API-Key` header)
# Any agent-server or automation env vars are passed through.
# ═══════════════════════════════════════════════════════════════════════════════
set -uo pipefail
log() { printf '[agent-canvas] %s\n' "$*"; }
log_error() { printf '[agent-canvas] ERROR: %s\n' "$*" >&2; }
# ── Load centralized defaults (generated from config/defaults.json at build) ─
# shellcheck source=/dev/null
if [ -f /opt/agent-canvas/defaults.env ]; then
# shellcheck disable=SC1091
. /opt/agent-canvas/defaults.env
fi
PORT="${PORT:-${CONFIG_PROXY_PORT:-8000}}"
AGENT_SERVER_PORT="${AGENT_SERVER_PORT:-${CONFIG_AGENT_SERVER_PORT:-18000}}"
AUTOMATION_PORT="${AUTOMATION_PORT:-${CONFIG_AUTOMATION_PORT:-18001}}"
# Persistence paths — keep settings, conversations, bash history under a
# single well-known directory that the VOLUME directive exposes.
OPENHANDS_DIR="${HOME}/.openhands"
STATE_DIR="${OPENHANDS_DIR}/${CONFIG_STATE_SUBDIR:-agent-canvas}"
export OH_PERSISTENCE_DIR="${OH_PERSISTENCE_DIR:-${OPENHANDS_DIR}}"
export OH_CONVERSATIONS_PATH="${OH_CONVERSATIONS_PATH:-${OPENHANDS_DIR}/${CONFIG_CONVERSATIONS:-agent-canvas/conversations}}"
export OH_BASH_EVENTS_DIR="${OH_BASH_EVENTS_DIR:-${OPENHANDS_DIR}/${CONFIG_BASH_EVENTS:-agent-canvas/bash_events}}"
# OH_SECRET_KEY is required for settings/secrets encryption. Without it the
# agent-server refuses to return encrypted secrets → conversation creation
# fails with a 503. Auto-generate and persist (just like the session API key)
# so the image never runs with a known default.
SECRET_KEY_FILE="${STATE_DIR}/secret-key.txt"
if [ -z "${OH_SECRET_KEY:-}" ]; then
if [ -f "$SECRET_KEY_FILE" ]; then
OH_SECRET_KEY="$(cat "$SECRET_KEY_FILE")"
else
OH_SECRET_KEY="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
mkdir -p "$(dirname "$SECRET_KEY_FILE")"
printf '%s' "$OH_SECRET_KEY" > "$SECRET_KEY_FILE"
chmod 600 "$SECRET_KEY_FILE"
log "Generated OH_SECRET_KEY (persisted to $SECRET_KEY_FILE)"
fi
fi
export OH_SECRET_KEY
# Session API key — generate one if not provided so the image doesn't run
# wide-open by default. Persisted so restarts reuse the same key.
SESSION_KEY_FILE="${STATE_DIR}/session-api-key.txt"
if [ -z "${OH_SESSION_API_KEYS_0:-}" ] && [ -z "${SESSION_API_KEY:-}" ]; then
if [ -f "$SESSION_KEY_FILE" ]; then
SESSION_API_KEY="$(cat "$SESSION_KEY_FILE")"
else
SESSION_API_KEY="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
mkdir -p "$(dirname "$SESSION_KEY_FILE")"
printf '%s' "$SESSION_API_KEY" > "$SESSION_KEY_FILE"
chmod 600 "$SESSION_KEY_FILE"
log "Generated session API key (persisted to $SESSION_KEY_FILE)"
fi
export OH_SESSION_API_KEYS_0="$SESSION_API_KEY"
fi
# Both backends share the same API key value and the same `X-Session-API-Key`
# header for authentication. Default OPENHANDS_AUTOMATION_API_KEY to the
# session key so a single credential secures the whole stack.
EFFECTIVE_SESSION_KEY="${OH_SESSION_API_KEYS_0:-${SESSION_API_KEY:-}}"
if [ -z "$EFFECTIVE_SESSION_KEY" ]; then
log "ERROR: No session API key available — cannot configure automation auth"
exit 1
fi
export OPENHANDS_AUTOMATION_API_KEY="${OPENHANDS_AUTOMATION_API_KEY:-${EFFECTIVE_SESSION_KEY}}"
export AUTOMATION_LOCAL_API_KEY="${AUTOMATION_LOCAL_API_KEY:-${EFFECTIVE_SESSION_KEY}}"
export AUTOMATION_AGENT_SERVER_API_KEY="${AUTOMATION_AGENT_SERVER_API_KEY:-${EFFECTIVE_SESSION_KEY}}"
# AGENT_SERVER_URL — needed by automation sandbox callbacks.
export AGENT_SERVER_URL="${AGENT_SERVER_URL:-http://127.0.0.1:${AGENT_SERVER_PORT}}"
# Make custom tools (e.g. canvas_ui_tool.py) importable by the agent-server
# via tool_module_qualnames. Matches what scripts/dev-safe.mjs does with
# OH_EXTRA_PYTHON_PATH: config.canvasToolsDir.
export OH_EXTRA_PYTHON_PATH="${OH_EXTRA_PYTHON_PATH:-/opt/agent-canvas/tools}"
# Track child PIDs so we can clean up on exit.
PIDS=()
cleanup() {
log "Shutting down..."
for pid in "${PIDS[@]}"; do
kill "$pid" 2>/dev/null || true
done
wait 2>/dev/null || true
}
trap cleanup EXIT SIGINT SIGTERM
# ── 1. Start Agent Server ────────────────────────────────────────────────────
log "Starting agent-server on port $AGENT_SERVER_PORT..."
if command -v openhands-agent-server >/dev/null 2>&1; then
# Binary build (production image)
openhands-agent-server --port "$AGENT_SERVER_PORT" &
elif [ -x /agent-server/.venv/bin/python ]; then
# Source build (development image)
/agent-server/.venv/bin/python -m openhands.agent_server --port "$AGENT_SERVER_PORT" &
else
log_error "Cannot find agent-server binary or source venv."
exit 1
fi
PIDS+=($!)
# ── 2. Start Automation Server ───────────────────────────────────────────────
log "Starting automation server on port $AUTOMATION_PORT..."
# Disable the automation's own frontend — agent-canvas provides the UI.
export AUTOMATION_FRONTEND_DIR=""
# Default to SQLite so the automation server works out of the box without
# an external PostgreSQL instance. Users can override AUTOMATION_DB_URL to
# point at a real Postgres for production deployments.
if [ -z "${AUTOMATION_DB_URL:-}" ]; then
AUTOMATION_DB_FILE="${OPENHANDS_DIR}/${CONFIG_AUTOMATION_DB:-automation/automations.db}"
mkdir -p "$(dirname "$AUTOMATION_DB_FILE")"
export AUTOMATION_DB_URL="sqlite+aiosqlite:///${AUTOMATION_DB_FILE}"
log "Using SQLite database: $AUTOMATION_DB_URL"
fi
# The automation server uses uvicorn. Set AUTOMATION_PORT via its CLI.
if command -v uvicorn >/dev/null 2>&1; then
uvicorn openhands.automation.app:app \
--host 0.0.0.0 \
--port "$AUTOMATION_PORT" &
PIDS+=($!)
elif python -c "import openhands.automation" 2>/dev/null; then
python -m uvicorn openhands.automation.app:app \
--host 0.0.0.0 \
--port "$AUTOMATION_PORT" &
PIDS+=($!)
else
log "WARNING: Automation server not found, skipping."
fi
# ── 3. Wait for backends to be ready ─────────────────────────────────────────
wait_for_port() {
local port=$1 name=$2 max_wait=${3:-30}
local elapsed=0
while ! (echo >/dev/tcp/127.0.0.1/"$port") 2>/dev/null; do
sleep 1
elapsed=$((elapsed + 1))
if [ "$elapsed" -ge "$max_wait" ]; then
log "WARNING: $name on port $port did not become ready within ${max_wait}s"
return 1
fi
done
log "$name is ready on port $port"
}
wait_for_port "$AGENT_SERVER_PORT" "Agent Server" 60 &
WAIT_PID1=$!
wait_for_port "$AUTOMATION_PORT" "Automation Server" 60 &
WAIT_PID2=$!
wait "$WAIT_PID1" "$WAIT_PID2"
# ── 4. Start static server (frontend + proxy) ────────────────────────────────
log "Starting frontend + proxy on port $PORT..."
# EFFECTIVE_SESSION_KEY is set above from ~/.openhands/agent-canvas/session-api-key.txt (or $SESSION_API_KEY)
node /opt/agent-canvas/static-server.mjs \
--port "$PORT" \
--host 0.0.0.0 \
--dir /opt/agent-canvas/frontend \
--session-api-key "$EFFECTIVE_SESSION_KEY" \
--route "/api/automation=http://127.0.0.1:${AUTOMATION_PORT}" \
--route "/api=http://127.0.0.1:${AGENT_SERVER_PORT}" \
--route "/server_info=http://127.0.0.1:${AGENT_SERVER_PORT}" \
--route "/sockets=http://127.0.0.1:${AGENT_SERVER_PORT}" \
--route "/alive=http://127.0.0.1:${AGENT_SERVER_PORT}" \
--route "/health=http://127.0.0.1:${AGENT_SERVER_PORT}" \
--route "/ready=http://127.0.0.1:${AGENT_SERVER_PORT}" \
--route "/docs=http://127.0.0.1:${AGENT_SERVER_PORT}" \
--route "/redoc=http://127.0.0.1:${AGENT_SERVER_PORT}" \
--route "/openapi.json=http://127.0.0.1:${AGENT_SERVER_PORT}" &
PIDS+=($!)
log "All services started. Unified entry point: http://0.0.0.0:${PORT}/"
# Wait for any child to exit. If one dies, the trap will clean up the rest.
wait -n "${PIDS[@]}" 2>/dev/null
EXIT_CODE=$?
log_error "A service exited with code $EXIT_CODE"
exit "$EXIT_CODE"