| #!/usr/bin/env bash
|
| set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| OPEN_WEBUI_PORT="${OPEN_WEBUI_PORT:-8080}"
|
| OPEN_WEBUI_HOST="${OPEN_WEBUI_HOST:-127.0.0.1}"
|
| OPEN_WEBUI_NAME="${OPEN_WEBUI_NAME:-Hermes Agent WebUI}"
|
| OPEN_WEBUI_ENABLE_SIGNUP="${OPEN_WEBUI_ENABLE_SIGNUP:-true}"
|
| OPEN_WEBUI_ENABLE_SERVICE="${OPEN_WEBUI_ENABLE_SERVICE:-auto}"
|
| OPEN_WEBUI_VENV="${OPEN_WEBUI_VENV:-$HOME/.local/open-webui-venv}"
|
| OPEN_WEBUI_DATA_DIR="${OPEN_WEBUI_DATA_DIR:-$HOME/.local/share/open-webui/data}"
|
| HERMES_ENV_FILE="${HERMES_ENV_FILE:-$HOME/.hermes/.env}"
|
| HERMES_API_PORT="${HERMES_API_PORT:-8642}"
|
| HERMES_API_HOST="${HERMES_API_HOST:-127.0.0.1}"
|
| HERMES_API_CONNECT_HOST="${HERMES_API_CONNECT_HOST:-127.0.0.1}"
|
| HERMES_API_MODEL_NAME="${HERMES_API_MODEL_NAME:-Hermes Agent}"
|
| HERMES_API_BASE_URL="http://${HERMES_API_CONNECT_HOST}:${HERMES_API_PORT}/v1"
|
| LAUNCHER_PATH="$HOME/.local/bin/start-open-webui-hermes.sh"
|
| LOG_DIR="$HOME/.hermes/logs"
|
|
|
| log() {
|
| printf '[open-webui-bootstrap] %s\n' "$*"
|
| }
|
|
|
| require_cmd() {
|
| if ! command -v "$1" >/dev/null 2>&1; then
|
| echo "Missing required command: $1" >&2
|
| exit 1
|
| fi
|
| }
|
|
|
| choose_python() {
|
| if command -v python3.11 >/dev/null 2>&1; then
|
| echo python3.11
|
| elif command -v python3 >/dev/null 2>&1; then
|
| echo python3
|
| else
|
| echo "Python 3 is required." >&2
|
| exit 1
|
| fi
|
| }
|
|
|
| upsert_env() {
|
| local key="$1"
|
| local value="$2"
|
| local file="$3"
|
|
|
| mkdir -p "$(dirname "$file")"
|
| touch "$file"
|
|
|
| python3 - "$file" "$key" "$value" <<'PY'
|
| from pathlib import Path
|
| import sys
|
| path = Path(sys.argv[1])
|
| key = sys.argv[2]
|
| value = sys.argv[3]
|
| lines = path.read_text().splitlines() if path.exists() else []
|
| out = []
|
| seen = False
|
| for raw in lines:
|
| stripped = raw.strip()
|
| if stripped.startswith(f"{key}="):
|
| if not seen:
|
| out.append(f"{key}={value}")
|
| seen = True
|
| continue
|
| out.append(raw)
|
| if not seen:
|
| if out and out[-1] != "":
|
| out.append("")
|
| out.append(f"{key}={value}")
|
| path.write_text("\n".join(out).rstrip() + "\n")
|
| PY
|
| }
|
|
|
| get_env_value() {
|
| local key="$1"
|
| local file="$2"
|
| python3 - "$file" "$key" <<'PY'
|
| from pathlib import Path
|
| import sys
|
| path = Path(sys.argv[1])
|
| key = sys.argv[2]
|
| if not path.exists():
|
| raise SystemExit(0)
|
| for raw in path.read_text().splitlines():
|
| line = raw.strip()
|
| if line.startswith(f"{key}="):
|
| print(line.split("=", 1)[1])
|
| raise SystemExit(0)
|
| PY
|
| }
|
|
|
| generate_secret() {
|
| python3 - <<'PY'
|
| import secrets
|
| print(secrets.token_urlsafe(32))
|
| PY
|
| }
|
|
|
| shell_quote() {
|
| python3 - "$1" <<'PY'
|
| import shlex
|
| import sys
|
| print(shlex.quote(sys.argv[1]))
|
| PY
|
| }
|
|
|
| can_use_systemd_user() {
|
| [[ "$(uname -s)" == "Linux" ]] || return 1
|
| command -v systemctl >/dev/null 2>&1 || return 1
|
|
|
| local uid runtime_dir bus_path
|
| uid="$(id -u)"
|
| runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$uid}"
|
| bus_path="$runtime_dir/bus"
|
|
|
| if [[ -z "${XDG_RUNTIME_DIR:-}" && -d "$runtime_dir" ]]; then
|
| export XDG_RUNTIME_DIR="$runtime_dir"
|
| fi
|
| if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" && -S "$bus_path" ]]; then
|
| export DBUS_SESSION_BUS_ADDRESS="unix:path=$bus_path"
|
| fi
|
|
|
| systemctl --user show-environment >/dev/null 2>&1
|
| }
|
|
|
| install_macos_dependencies() {
|
| if [[ "$(uname -s)" == "Darwin" ]] && command -v brew >/dev/null 2>&1; then
|
| if ! command -v pandoc >/dev/null 2>&1; then
|
| log 'Installing pandoc with Homebrew (recommended by Open WebUI docs)...'
|
| brew install pandoc
|
| fi
|
| fi
|
| }
|
|
|
| install_open_webui() {
|
| local py
|
| py="$(choose_python)"
|
| log "Using Python interpreter: $py"
|
| "$py" -m venv "$OPEN_WEBUI_VENV"
|
|
|
| source "$OPEN_WEBUI_VENV/bin/activate"
|
| "$py" -m pip install --upgrade pip setuptools wheel
|
| "$py" -m pip install open-webui
|
| }
|
|
|
| write_launcher() {
|
| mkdir -p "$(dirname "$LAUNCHER_PATH")" "$OPEN_WEBUI_DATA_DIR" "$LOG_DIR"
|
|
|
| local quoted_data_dir quoted_name quoted_base_url quoted_host quoted_port quoted_venv
|
| quoted_data_dir="$(shell_quote "$OPEN_WEBUI_DATA_DIR")"
|
| quoted_name="$(shell_quote "$OPEN_WEBUI_NAME")"
|
| quoted_base_url="$(shell_quote "$HERMES_API_BASE_URL")"
|
| quoted_host="$(shell_quote "$OPEN_WEBUI_HOST")"
|
| quoted_port="$(shell_quote "$OPEN_WEBUI_PORT")"
|
| quoted_venv="$(shell_quote "$OPEN_WEBUI_VENV")"
|
|
|
| cat > "$LAUNCHER_PATH" <<EOF
|
| #!/usr/bin/env bash
|
| set -euo pipefail
|
| export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
| API_KEY=\$(python3 - <<'PY'
|
| from pathlib import Path
|
| p = Path.home()/'.hermes'/'.env'
|
| for raw in p.read_text().splitlines():
|
| line = raw.strip()
|
| if line.startswith('API_SERVER_KEY='):
|
| print(line.split('=', 1)[1])
|
| break
|
| PY
|
| )
|
| export DATA_DIR=${quoted_data_dir}
|
| export WEBUI_NAME=${quoted_name}
|
| export ENABLE_SIGNUP=${OPEN_WEBUI_ENABLE_SIGNUP}
|
| export ENABLE_PUBLIC_ACTIVE_USERS_COUNT=False
|
| export ENABLE_VERSION_UPDATE_CHECK=False
|
| export OPENAI_API_BASE_URL=${quoted_base_url}
|
| export OPENAI_API_KEY="\$API_KEY"
|
| export ENABLE_OPENAI_API=True
|
| export ENABLE_OLLAMA_API=False
|
| export OFFLINE_MODE=True
|
| export BYPASS_EMBEDDING_AND_RETRIEVAL=True
|
| export RAG_EMBEDDING_MODEL_AUTO_UPDATE=False
|
| export RAG_RERANKING_MODEL_AUTO_UPDATE=False
|
| export SCARF_NO_ANALYTICS=true
|
| export DO_NOT_TRACK=true
|
| export ANONYMIZED_TELEMETRY=false
|
| export HOST=${quoted_host}
|
| export PORT=${quoted_port}
|
| source ${quoted_venv}/bin/activate
|
| exec open-webui serve
|
| EOF
|
|
|
| chmod +x "$LAUNCHER_PATH"
|
| }
|
|
|
| ensure_env_permissions() {
|
| chmod 600 "$HERMES_ENV_FILE" 2>/dev/null || true
|
| }
|
|
|
| install_launchd_service() {
|
| local plist="$HOME/Library/LaunchAgents/ai.openwebui.hermes.plist"
|
| mkdir -p "$(dirname "$plist")"
|
| cat > "$plist" <<EOF
|
| <?xml version="1.0" encoding="UTF-8"?>
|
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
| <plist version="1.0">
|
| <dict>
|
| <key>Label</key>
|
| <string>ai.openwebui.hermes</string>
|
| <key>ProgramArguments</key>
|
| <array>
|
| <string>/bin/bash</string>
|
| <string>${LAUNCHER_PATH}</string>
|
| </array>
|
| <key>RunAtLoad</key>
|
| <true/>
|
| <key>KeepAlive</key>
|
| <true/>
|
| <key>WorkingDirectory</key>
|
| <string>${HOME}</string>
|
| <key>StandardOutPath</key>
|
| <string>${LOG_DIR}/openwebui.log</string>
|
| <key>StandardErrorPath</key>
|
| <string>${LOG_DIR}/openwebui.error.log</string>
|
| </dict>
|
| </plist>
|
| EOF
|
| launchctl bootout "gui/$(id -u)" "$plist" >/dev/null 2>&1 || true
|
| launchctl bootstrap "gui/$(id -u)" "$plist"
|
| launchctl enable "gui/$(id -u)/ai.openwebui.hermes"
|
| launchctl kickstart -k "gui/$(id -u)/ai.openwebui.hermes"
|
| }
|
|
|
| install_systemd_user_service() {
|
| require_cmd systemctl
|
| local unit_dir="$HOME/.config/systemd/user"
|
| local unit="$unit_dir/openwebui-hermes.service"
|
| mkdir -p "$unit_dir"
|
| cat > "$unit" <<EOF
|
| [Unit]
|
| Description=Open WebUI connected to Hermes Agent
|
| After=default.target
|
|
|
| [Service]
|
| Type=simple
|
| ExecStart=/bin/bash %h/.local/bin/start-open-webui-hermes.sh
|
| Restart=always
|
| RestartSec=3
|
| WorkingDirectory=%h
|
| StandardOutput=append:%h/.hermes/logs/openwebui.log
|
| StandardError=append:%h/.hermes/logs/openwebui.error.log
|
|
|
| [Install]
|
| WantedBy=default.target
|
| EOF
|
| systemctl --user daemon-reload
|
| systemctl --user enable --now openwebui-hermes.service
|
| }
|
|
|
| start_foreground_hint() {
|
| log "Launcher created at: ${LAUNCHER_PATH}"
|
| log "Start Open WebUI manually with: ${LAUNCHER_PATH}"
|
| }
|
|
|
| main() {
|
| require_cmd hermes
|
| require_cmd curl
|
| require_cmd python3
|
|
|
| install_macos_dependencies
|
|
|
| local api_key
|
| api_key="$(get_env_value API_SERVER_KEY "$HERMES_ENV_FILE")"
|
| if [[ -z "$api_key" ]]; then
|
| api_key="$(generate_secret)"
|
| fi
|
|
|
| log 'Ensuring Hermes API server is configured...'
|
| upsert_env API_SERVER_ENABLED true "$HERMES_ENV_FILE"
|
| upsert_env API_SERVER_HOST "$HERMES_API_HOST" "$HERMES_ENV_FILE"
|
| upsert_env API_SERVER_PORT "$HERMES_API_PORT" "$HERMES_ENV_FILE"
|
| upsert_env API_SERVER_MODEL_NAME "$HERMES_API_MODEL_NAME" "$HERMES_ENV_FILE"
|
| upsert_env API_SERVER_KEY "$api_key" "$HERMES_ENV_FILE"
|
| ensure_env_permissions
|
|
|
| log 'Restarting Hermes gateway so API server settings take effect...'
|
| hermes gateway restart >/dev/null 2>&1 || true
|
| sleep 4
|
| if ! curl -fsS "http://${HERMES_API_CONNECT_HOST}:${HERMES_API_PORT}/health" >/dev/null; then
|
| log 'Hermes API server did not answer on the first check. Trying to start gateway in the background...'
|
| nohup hermes gateway run >/dev/null 2>&1 &
|
| sleep 6
|
| fi
|
| curl -fsS "http://${HERMES_API_CONNECT_HOST}:${HERMES_API_PORT}/health" >/dev/null
|
|
|
| log 'Installing Open WebUI into a dedicated virtualenv...'
|
| install_open_webui
|
| write_launcher
|
|
|
| case "$OPEN_WEBUI_ENABLE_SERVICE" in
|
| true|auto)
|
| if [[ "$(uname -s)" == "Darwin" ]]; then
|
| install_launchd_service
|
| elif can_use_systemd_user; then
|
| install_systemd_user_service
|
| else
|
| log 'No usable user service manager detected; falling back to the launcher script.'
|
| start_foreground_hint
|
| fi
|
| ;;
|
| false)
|
| start_foreground_hint
|
| ;;
|
| *)
|
| echo "OPEN_WEBUI_ENABLE_SERVICE must be one of: auto, true, false" >&2
|
| exit 1
|
| ;;
|
| esac
|
|
|
| log "Done. Open WebUI should be available at: http://${OPEN_WEBUI_HOST}:${OPEN_WEBUI_PORT}"
|
| log "Hermes API endpoint: ${HERMES_API_BASE_URL}"
|
| log 'Important: Open WebUI persists connection settings after first launch. If you later save a wrong API key in the Admin UI, update/delete that connection there or reset its database.'
|
| }
|
|
|
| main "$@"
|
|
|