entrypoint-webui.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/sh
set -e
# ── Pre-flight: verify volume mount ──────────────────────────
# Without a volume mount, the vault key and all data are lost on restart.
PREFLIGHT_FILE="$HOME/.lynox/.volume-check"
if printf 'ok' > "$PREFLIGHT_FILE" 2>/dev/null && [ -f "$PREFLIGHT_FILE" ]; then
rm -f "$PREFLIGHT_FILE"
else
echo "" >&2
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >&2
echo " WARNING: Volume mount not detected or not writable!" >&2
echo " Data will be LOST when this container restarts." >&2
echo "" >&2
echo " Add to your docker run command:" >&2
echo " -v ~/.lynox:/home/lynox/.lynox" >&2
echo "" >&2
echo " Or in docker-compose.yml:" >&2
echo " volumes:" >&2
echo " - \${HOME}/.lynox:/home/lynox/.lynox" >&2
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >&2
echo "" >&2
fi
# Where the vault key comes from, for the engine to report (`LYNOX_VAULT_KEY_SOURCE`, read in
# src/core/vault-key-source.ts): env | volume-env | minted | none. Set here whatever the
# environment said, so a value from outside cannot claim a source.
if [ -n "${LYNOX_VAULT_KEY:-}" ]; then
KEY_SOURCE=env
else
KEY_SOURCE=none
fi
# Load vault key from .env if it exists
ENV_FILE="$HOME/.lynox/.env"
if [ -z "${LYNOX_VAULT_KEY:-}" ] && [ -f "$ENV_FILE" ] && [ ! -L "$ENV_FILE" ]; then
FILE_PERMS=$(stat -c '%a' "$ENV_FILE" 2>/dev/null || stat -f '%Lp' "$ENV_FILE" 2>/dev/null || echo "unknown")
case "$FILE_PERMS" in
600|400)
VAULT_KEY=$(grep '^LYNOX_VAULT_KEY=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
if [ -n "$VAULT_KEY" ]; then
export LYNOX_VAULT_KEY="$VAULT_KEY"
KEY_SOURCE=volume-env
fi
;;
esac
fi
# Auto-generate access token if not set
TOKEN_FILE="$HOME/.lynox/.access-token"
if [ -z "${LYNOX_HTTP_SECRET:-}" ]; then
# Reuse persisted token from previous run (survives container restarts)
if [ -f "$TOKEN_FILE" ]; then
LYNOX_HTTP_SECRET=$(cat "$TOKEN_FILE")
export LYNOX_HTTP_SECRET
echo " Access token loaded from volume (not shown in logs)." >&2
echo " Retrieve: cat ~/.lynox/.access-token (on the host)" >&2
echo " with the documented \${HOME}/.lynox volume mount" >&2
echo "" >&2
else
LYNOX_HTTP_SECRET=$(node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")
export LYNOX_HTTP_SECRET
# Persist to volume so it survives container restarts
mkdir -p "$HOME/.lynox"
printf '%s' "$LYNOX_HTTP_SECRET" > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
# POSIX-compatible 8-char preview — entrypoint runs under dash (`/bin/sh` of the
# node:22-slim image), which does not support bash's ${VAR:0:8} substring
# expansion (was emitting `Bad substitution` on every fresh boot, leaving
# users with no token preview at all — pre-HN-launch P0 fix).
TOKEN_PREVIEW=$(printf '%s' "$LYNOX_HTTP_SECRET" | head -c 8)
echo "" >&2
echo "========================================" >&2
echo " Access Token (enter in browser):" >&2
echo " ${TOKEN_PREVIEW}..." >&2
echo "========================================" >&2
echo " Stored in volume — same token on every restart." >&2
echo " Full token: cat ~/.lynox/.access-token (on the host)" >&2
echo " with the documented \${HOME}/.lynox volume mount" >&2
echo " Override: set LYNOX_HTTP_SECRET in .env or docker run" >&2
echo "" >&2
fi
fi
# Auto-generate vault key if not set (persist to volume). A new key is generated only where
# it cannot replace or sit beside one that data was encrypted under:
# - not when the data dir already holds a vault. The engine then decides as it does without
# this script: it reads the key from $ENV_FILE if it can, then the data dir's vault.key, and
# never generates a key over an existing vault (`ensureVaultKey`).
# - not into a .env that is not a regular file with mode 600, or that names LYNOX_VAULT_KEY
# outside a comment line although the reader above took no key from it (an empty value, an
# indented line, a byte-order mark, a space before `=`). The start stops instead. A second key line would split the readers: the installer reads the first key line
# and takes an empty one for "no key" (`readVaultKeyFromRecoveryFile`), so it would write a
# new key over the one appended here.
# - the key is appended, so other lines in the file stay.
# The data dir follows the engine's rule (`lynoxDirFor`): LYNOX_DATA_DIR, then LYNOX_DIR, an
# empty value counting as unset.
DATA_DIR="${LYNOX_DATA_DIR:-${LYNOX_DIR:-$HOME/.lynox}}"
if [ -z "${LYNOX_VAULT_KEY:-}" ] && [ -e "$DATA_DIR/vault.db" ]; then
if [ ! -e "$DATA_DIR/vault.key" ]; then
echo "" >&2
echo " WARNING: this start found no vault key, and $DATA_DIR/vault.db already exists." >&2
echo " No new key was generated: it could not open what is encrypted there." >&2
echo " If lynox then logs \"Encrypted vault found but LYNOX_VAULT_KEY is not set\" or" >&2
echo " \"History: encrypted data found but no encryption key set\", or your chats, saved" >&2
echo " connections and API keys are missing, set LYNOX_VAULT_KEY to the key this install was" >&2
echo " created with (docker run -e, or the compose .env), or put it in $ENV_FILE with mode 600." >&2
echo "" >&2
fi
elif [ -z "${LYNOX_VAULT_KEY:-}" ]; then
ENV_REFUSED=""
if [ -L "$ENV_FILE" ]; then
ENV_REFUSED="it is a symbolic link"
elif [ -e "$ENV_FILE" ] && [ ! -f "$ENV_FILE" ]; then
ENV_REFUSED="it is not a regular file"
elif [ -e "$ENV_FILE" ]; then
case "$(stat -c '%a' "$ENV_FILE" 2>/dev/null || stat -f '%Lp' "$ENV_FILE" 2>/dev/null || echo unknown)" in
600)
# Any mention outside a comment counts, not only the shapes a reader takes a key from:
# the three readers (this script, the engine, the installer) differ in what they trim.
# -a: a NUL byte anywhere would make grep treat the file as binary and print nothing.
if grep -a -v '^[[:space:]]*#' "$ENV_FILE" 2>/dev/null | grep -a -q 'LYNOX_VAULT_KEY'; then
ENV_REFUSED="it names LYNOX_VAULT_KEY in a line this start cannot read a key from"
fi
;;
400) ENV_REFUSED="it is read-only (mode 400), and this start read no key from it" ;;
*) ENV_REFUSED="its mode is not 600 or 400, so it was not read" ;;
esac
fi
if [ -n "$ENV_REFUSED" ]; then
echo "" >&2
echo " ERROR: no vault key is set, and no new key can be written to $ENV_FILE: $ENV_REFUSED." >&2
echo " A new key there could replace or sit beside the one your data is encrypted under, so" >&2
echo " lynox does not start. Set LYNOX_VAULT_KEY, or make the file a regular file with mode 600" >&2
echo " (chmod 600) that holds the key on one line: LYNOX_VAULT_KEY=<key>." >&2
echo "" >&2
exit 1
fi
LYNOX_VAULT_KEY=$(node -e "process.stdout.write(require('crypto').randomBytes(48).toString('base64'))")
export LYNOX_VAULT_KEY
KEY_SOURCE=minted
mkdir -p "$HOME/.lynox"
# A new file is created with mode 600; an existing one is appended to only at mode 600.
# The leading newline keeps the key off a last line that has none (`FOO=bar` would become
# `FOO=barLYNOX_VAULT_KEY=…`, which no reader takes a key from).
(umask 077 && printf '\nLYNOX_VAULT_KEY=%s\n' "$LYNOX_VAULT_KEY" >> "$ENV_FILE")
echo " Vault key generated and saved to volume."
echo " Save it to a password manager: Settings → Config → Security"
echo ""
else
echo " Vault key active — data is encrypted."
echo ""
fi
# Check ANTHROPIC_API_KEY (warn but don't exit — browse mode still works)
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
CONFIG_FILE="$HOME/.lynox/config.json"
if [ ! -f "$CONFIG_FILE" ] || ! grep -q '"api_key"' "$CONFIG_FILE" 2>/dev/null; then
echo "" >&2
echo " Warning: ANTHROPIC_API_KEY not set" >&2
echo " AI responses disabled — browse mode only" >&2
echo " Set via Web UI Settings or: docker run -e ANTHROPIC_API_KEY=sk-ant-... ..." >&2
echo "" >&2
fi
fi
# Pre-create + warn-if-unwritable for the data + cache subdirs the engine
# expects. The mkdir is important because docker-compose mounts a tmpfs at
# /home/lynox/.cache, which masks the Dockerfile's build-time directory
# creation — without the mkdir here the subsequent Xenova ONNX cache write
# fails with ENOENT on the first web_research call. The writability check
# stays as a defensive signal in case a misconfigured host-volume mount
# strips owner.
for dir in "$HOME/.lynox" "$HOME/.cache/huggingface"; do
mkdir -p "$dir" 2>/dev/null || true
if [ ! -w "$dir" ] 2>/dev/null; then
echo "Warning: $dir is not writable by user $(id -u). Data persistence may fail." >&2
fi
done
# Detect ownership drift inside ~/.lynox/. The base image's default user used
# to be `node` (uid 1000) before we switched to `lynox` (uid 1001). Volumes
# persisted across that switch keep their old ownership, which makes file-
# write tools (api_setup refine, artifact save, …) fail silently — the
# directory is writable so the dir-check above passes, but individual file
# rewrites inside it return EACCES. Surface the diagnosis so the operator
# knows the one-liner fix instead of debugging "refine doesn't work".
CURRENT_UID=$(id -u)
# The container may run as any host uid (the installer sets `user:` to the host user's), and most
# have no name in the image. `id -un` then fails, and under `set -e` a failed command substitution
# in an assignment ends the script: the container restarted in a loop before the engine started.
CURRENT_USER=$(id -un 2>/dev/null) || CURRENT_USER="(no name)"
if [ -d "$HOME/.lynox" ]; then
# Limit to the first few mismatched paths so the log stays readable; the
# operator only needs to know *that* there are wrong-owned files, not the
# full list. Errors from `find` (e.g. unreadable subdirs) are silenced.
WRONG_OWNED=$(find "$HOME/.lynox" -mindepth 1 -not -uid "$CURRENT_UID" 2>/dev/null | head -3)
if [ -n "$WRONG_OWNED" ]; then
echo "" >&2
echo " WARNING: files in $HOME/.lynox are owned by another user." >&2
echo " The container runs as '$CURRENT_USER' (uid $CURRENT_UID), but some files were created" >&2
echo " by an older image version under a different uid. Examples:" >&2
echo "$WRONG_OWNED" | sed 's/^/ /' >&2
echo "" >&2
echo " Symptom: api_setup refine, artifact save, and other in-place rewrites fail with EACCES." >&2
echo " Fix from the host (one-time):" >&2
echo " docker compose stop lynox" >&2
echo " chown -R \$(docker exec lynox id -u):\$(docker exec lynox id -g) <host-volume-path>" >&2
echo " docker compose start lynox" >&2
echo "" >&2
fi
fi
# SvelteKit CSRF: ORIGIN must match the browser's Origin header on form POSTs.
# Behind a reverse proxy / tunnel, the browser sends the public URL as Origin,
# but the server sees localhost — causing a CSRF mismatch (403).
if [ -z "${ORIGIN:-}" ]; then
if [ -n "${LYNOX_ALLOWED_ORIGINS:-}" ]; then
# Use the first allowed origin (comma-separated list) as the CSRF origin
export ORIGIN="${LYNOX_ALLOWED_ORIGINS%%,*}"
elif [ -n "${LYNOX_TLS_CERT:-}" ]; then
export ORIGIN="https://localhost:${LYNOX_HTTP_PORT:-3000}"
else
export ORIGIN="http://localhost:${LYNOX_HTTP_PORT:-3000}"
fi
fi
export LYNOX_VAULT_KEY_SOURCE="$KEY_SOURCE"
# Single process: Engine auto-loads Web UI handler from /app/web-ui/handler.js
echo "Starting lynox on port ${LYNOX_HTTP_PORT:-3000}..."
echo " Phone access: Settings → Mobile Access → scan QR code" >&2
exec node /app/dist/index.js --http-api