Ctrl K
ComunidadGuías…

entrypoint-webui.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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