Open-WebUI runs in its own container with no WebGUI session, and nginx applies auth_request to everything it serves, so it cannot call the plugin API. Same shape as the arr webhook listener: node outside nginx, shared secret, one read-only verb. Serves chunks rather than answers because the caller already has a model loaded.
168 lines
7.2 KiB
Bash
Executable File
168 lines
7.2 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ━━━ AI Retrieval Server ━━━
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ==============================================================================================
|
|
# Starts ai_serve.js, the retrieval-only HTTP bridge that lets Open-WebUI — which runs in its
|
|
# own container and cannot see Varaverk's filesystem — search the AI index for grounding
|
|
# context.
|
|
#
|
|
# ==============================================================================================
|
|
#
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
# Mirrors Arrs_Stack/start_webhook_listener.sh, because it solves the same problem: a caller
|
|
# outside Unraid's nginx needs to reach Varaverk, and nginx applies auth_request to everything
|
|
# it serves. Run from ARRAY_START_SCRIPTS and exec's node, so the process this script becomes
|
|
# is the server itself — no PID file to go stale.
|
|
#
|
|
# Retrieval only. Generation stays in ai_query.sh; Open-WebUI has its own model loaded and
|
|
# generates from the chunks this returns.
|
|
#
|
|
# ==============================================================================================
|
|
#
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
# Every precondition is checked before exec, not after. A failure at array start should name
|
|
# its cause in the log rather than surface as an exec error once the setup has already run.
|
|
#
|
|
# Disabled is a first-class state. AI_HTTP_PORT=0 or AI_ENABLED=false exits 0 without warning,
|
|
# so a host that does not want the bridge is not a host reporting a failed start script.
|
|
#
|
|
# The index is not built here. This serves an index; ai_index.sh creates one. A server that
|
|
# silently indexed on boot would turn a restart into an unbounded embedding run.
|
|
#
|
|
# ==============================================================================================
|
|
#
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Root Required
|
|
# Writes the generated secret back into master.conf and logs under /var/log/varaverk.
|
|
#
|
|
# Refuses to Serve a Missing Index
|
|
# The endpoint would answer every query with a retrieval error and Open-WebUI would render
|
|
# it as "nothing found" — indistinguishable from a corpus that genuinely lacks the answer.
|
|
# Exits with a message naming ai_index.sh instead.
|
|
#
|
|
# Secret Generated Once, and Verified Persisted
|
|
# An unset AI_HTTP_SECRET is generated with openssl and written to master.conf. If the
|
|
# write-back cannot be confirmed the start is failed: a secret that exists only in this
|
|
# process changes on every restart, silently breaking the tool registered in Open-WebUI.
|
|
# Same failure mode, and same guard, as the webhook listener's secret.
|
|
#
|
|
# Single Instance
|
|
# acquire_lock "continuous" — an array stop/start without a reboot leaves the old node
|
|
# process holding the port, and a second bind would fail with EADDRINUSE and log a false
|
|
# failure against array_started.sh.
|
|
#
|
|
# Binds Only Where It Must
|
|
# Defaults to 0.0.0.0 because the caller is a container on the docker bridge and cannot
|
|
# reach a loopback-bound socket. That is the reason the secret exists, and why the served
|
|
# surface is a single read-only verb over documentation already in git.
|
|
#
|
|
# ==============================================================================================
|
|
#
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
# AI_ENABLED master.conf — false exits without starting
|
|
# AI_HTTP_PORT master.conf — 0 disables the bridge
|
|
# AI_HTTP_SECRET master.conf — auto-generated on first start if empty
|
|
# AI_HTTP_BIND master.conf — bind address, default 0.0.0.0
|
|
# AI_INDEX_DB master.conf — index served
|
|
# <HOST>_OLLAMA_URL host conf — embedding endpoint
|
|
# <HOST>_OLLAMA_EMBED_MODEL host conf — embedding model
|
|
#
|
|
# ==============================================================================================
|
|
#
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
# start_ai_server.sh exec's the server in the foreground; run from ARRAY_START_SCRIPTS
|
|
#
|
|
# ==============================================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ECOSYSTEM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
source "$ECOSYSTEM_ROOT/load_config.sh"
|
|
|
|
if [[ "$EUID" -ne 0 ]]; then
|
|
error "Must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${AI_ENABLED:-false}" != "true" ]]; then
|
|
echo "[ai_serve] AI_ENABLED is not true — retrieval bridge disabled"
|
|
exit 0
|
|
fi
|
|
|
|
[[ "${AI_HTTP_PORT:-0}" -eq 0 ]] && {
|
|
echo "[ai_serve] AI_HTTP_PORT=0 — retrieval bridge disabled"
|
|
exit 0
|
|
}
|
|
|
|
if ! command -v node >/dev/null 2>&1; then
|
|
error "node not found — required to run ai_serve.js"
|
|
notify "AI retrieval bridge failed to start on $(hostname) — node not installed" \
|
|
"AI Retrieval" "warning"
|
|
exit 1
|
|
fi
|
|
|
|
detect_hosts
|
|
|
|
_url_var="${MY_ID}_OLLAMA_URL"
|
|
_emb_var="${MY_ID}_OLLAMA_EMBED_MODEL"
|
|
OLLAMA_URL="${!_url_var:-}"
|
|
EMBED_MODEL="${!_emb_var:-nomic-embed-text}"
|
|
DB="${AI_INDEX_DB:-${DATA_DIR}/ai_index.db}"
|
|
|
|
if [[ -z "$OLLAMA_URL" ]]; then
|
|
error "${MY_ID}_OLLAMA_URL is empty — cannot embed queries"
|
|
exit 1
|
|
fi
|
|
|
|
# Serving an absent index answers every question with a retrieval error, which Open-WebUI
|
|
# renders as "nothing found" — the same thing an empty corpus looks like.
|
|
if [[ ! -f "$DB" ]]; then
|
|
error "No index at $DB — run AI/ai_index.sh first"
|
|
notify "AI retrieval bridge not started on $(hostname) — index missing" \
|
|
"AI Retrieval" "warning"
|
|
exit 1
|
|
fi
|
|
|
|
acquire_lock "continuous"
|
|
|
|
if [[ -z "${AI_HTTP_SECRET:-}" ]]; then
|
|
if ! command -v openssl >/dev/null 2>&1; then
|
|
error "openssl not found — cannot generate AI_HTTP_SECRET"
|
|
exit 1
|
|
fi
|
|
|
|
GENERATED=$(openssl rand -hex 32)
|
|
MASTER_CONF="$ECOSYSTEM_ROOT/Configurations/master.conf"
|
|
sed -i "s/AI_HTTP_SECRET=\"\"/AI_HTTP_SECRET=\"$GENERATED\"/" "$MASTER_CONF"
|
|
AI_HTTP_SECRET="$GENERATED"
|
|
|
|
# A secret held only in this process would differ on the next start, silently breaking
|
|
# the tool already registered in Open-WebUI. Same guard as the webhook listener.
|
|
if ! grep -q "AI_HTTP_SECRET=\"$GENERATED\"" "$MASTER_CONF" 2>/dev/null; then
|
|
error "Generated AI_HTTP_SECRET but could not persist it to $MASTER_CONF"
|
|
error "Set AI_HTTP_SECRET manually — a non-persisted secret changes on every restart"
|
|
notify "AI retrieval secret not persisted on $(hostname)" "AI Retrieval" "warning"
|
|
exit 1
|
|
fi
|
|
|
|
echo "[ai_serve] Generated AI_HTTP_SECRET — add it to the Open-WebUI tool"
|
|
fi
|
|
|
|
mkdir -p /var/log/varaverk
|
|
|
|
exec node "$ECOSYSTEM_ROOT/AI/ai_serve.js" \
|
|
"$AI_HTTP_PORT" "$AI_HTTP_SECRET" "${AI_HTTP_BIND:-0.0.0.0}" \
|
|
"$DB" "$OLLAMA_URL" "$EMBED_MODEL" \
|
|
>> /var/log/varaverk/ai_serve.log 2>&1
|