mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 14:58:39 +08:00
* 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>
208 lines
9.3 KiB
Bash
208 lines
9.3 KiB
Bash
#!/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"
|