#!/bin/bash
#
# Volatclaw server installer.
#
# Server bootstrap flow (run on a fresh Ubuntu box):
#
# 1. As root, create the service user. Give it passwordless sudo ONLY for the
#    install (apt / Node / systemd units) — you REVOKE it in step 4 so a
#    compromised bot (which runs as this user) can't escalate to root:
#      useradd -m -s /bin/bash volatclaw
#      echo 'volatclaw ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/volatclaw
#      chmod 440 /etc/sudoers.d/volatclaw
#      su - volatclaw
#
# 2. As volatclaw, add your laptop's pubkey for direct SSH (optional):
#      mkdir -p ~/.ssh && chmod 700 ~/.ssh
#      vi ~/.ssh/authorized_keys      # paste laptop's ~/.ssh/id_rsa.pub
#      chmod 600 ~/.ssh/authorized_keys
#
# 3. Run the installer:
#      curl -fsSL https://install.volat.ai | bash
#
#    The script prompts interactively for the V_KEY, the RW PAT (bot repos),
#    and DOMAIN. To skip prompts, pass them as env vars:
#      curl -fsSL https://install.volat.ai | \
#        V_KEY=lic_xxx GITHUB_PAT_RW=ghp_yyy \
#        DOMAIN=conductor.example.com bash
#
#    DOMAIN is optional — Enter (or omit) for polling mode.
#
#    Steps 1-3 are automated end to end by scripts/provision-server.sh, run from
#    an operator laptop against a fresh box (`provision-server.sh root@<ip>`). It
#    also covers the case CI can't: it builds the release ON the box from a git ref
#    and hands it to this script via VOLAT_ARTIFACT (see below).
#
#    VOLAT_ARTIFACT=/path/to/volatclaw.tar.gz installs that release tarball instead
#    of downloading the signed one from the license server. The signature check is
#    skipped, because a box-built tarball has no signature — use it only for a
#    tarball you built yourself. The license key is still verified.
#
# 4. After confirming bots reply, REVOKE the service user's standing sudo so a
#    compromised bot can't escalate (deploys still work without it):
#      bash ~/volatclaw/scripts/harden-sudo.sh
#    Thereafter, run rare privileged ops (systemd unit changes) as root.
#
set -e

echo "=== Volatclaw Server Setup ==="

# The compiled build is fetched from the license server (license.volat.ai), which
# returns a short-lived signed Supabase download URL for a valid V_KEY. No
# source/dist repo access is needed on the customer box — only a license key.
LICENSE_SERVER="${LICENSE_SERVER:-https://license.volat.ai}"

# Refuse root — Claude Code refuses to run as root, which makes the agent
# subprocess exit 1. Run as a regular user with sudo privileges instead.
if [ "$(id -u)" = "0" ]; then
  cat >&2 <<'ROOTERR'
ERROR: Don't run setup.sh as root.

Claude Code (the agent SDK subprocess) refuses to run as root, so the
conductor would start but every bot reply would fail.

Create a non-root user and run the installer as them:

  useradd -m -s /bin/bash volatclaw
  usermod -aG sudo volatclaw
  su - volatclaw

  # Then run the curl|bash one-liner with your env vars
ROOTERR
  exit 1
fi

# Require passwordless sudo — curl|bash has no TTY, so a sudo prompt mid-script
# silently hangs forever.
if ! sudo -n true 2>/dev/null; then
  cat >&2 <<'NOSUDO'
ERROR: This installer needs passwordless sudo.

It runs apt-get, npm i -g, and writes the systemd unit. Piped to bash,
there's no TTY for a password prompt — sudo would hang silently.

As root:
  echo 'volatclaw ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/volatclaw
  chmod 440 /etc/sudoers.d/volatclaw

Then re-run the installer as the volatclaw user.
NOSUDO
  exit 1
fi

# Collect inputs — env vars take precedence; otherwise prompt via /dev/tty
# (works under curl|bash, where stdin is the pipe but the controlling TTY is still around).
# /dev/tty exists as a device node even with no controlling terminal (ssh without
# -t, CI), and opening it then fails — so probe by opening, not with `-e`.
have_tty() { ( : </dev/tty ) 2>/dev/null; }
prompt_secret() { # var-name, prompt-text — reads char-by-char so we can echo *
  local val='' char
  if ! have_tty; then
    echo "ERROR: $1 not set and no TTY available for prompt." >&2
    exit 1
  fi
  printf '%s: ' "$2" > /dev/tty
  while IFS= read -rsn1 char < /dev/tty; do
    if [ -z "$char" ]; then
      break  # Enter
    elif [ "$char" = $'\x7f' ] || [ "$char" = $'\b' ]; then
      if [ -n "$val" ]; then
        val=${val%?}
        printf '\b \b' > /dev/tty
      fi
    else
      val+=$char
      printf '*' > /dev/tty
    fi
  done
  printf '\n' > /dev/tty
  printf -v "$1" '%s' "$val"
}
prompt_visible() { # var-name, prompt-text
  local val
  have_tty || return 0  # skip silently if no TTY (e.g., DOMAIN is optional)
  read -rp "$2: " val < /dev/tty
  printf -v "$1" '%s' "$val"
}

if [ -z "${V_KEY:-}" ]; then
  echo
  echo "Volatclaw license key (lic_...) — issued from $LICENSE_SERVER"
  prompt_secret V_KEY "  V_KEY"
fi
if [ -z "${GITHUB_PAT_RW:-}" ]; then
  echo
  echo "GitHub fine-grained PAT — Contents:Read+Write on bot repos"
  prompt_secret GITHUB_PAT_RW "  RW PAT"
fi
if [ -z "${DOMAIN:-}" ]; then
  echo
  echo "Webhook domain (e.g. conductor.customer.com) — empty for polling mode"
  prompt_visible DOMAIN "  DOMAIN"
fi

# STT provider — pick one and collect the matching API key. All providers use
# Whisper-family models with the same OpenAI-compatible API. OpenRouter is the
# cheapest paid route (whisper-large-v3-turbo via DeepInfra, ~$0.011/h, Groq as
# fallback) and needs no Groq account; Groq direct is free-tier only (8 h/day,
# no paid upgrade); OpenAI is the original ($0.36/h).
if [ -z "${STT_PROVIDER:-}" ]; then
  echo
  echo "Speech-to-text provider:"
  echo "  1) openai      — Whisper-1 (paid, \$0.36/h)"
  echo "  2) groq        — whisper-large-v3-turbo direct (free tier only, 8 h/day)"
  echo "  3) openrouter  — whisper-large-v3-turbo via OpenRouter (~\$0.011/h, recommended)"
  prompt_visible STT_CHOICE "  Choice [1-3, default 1]"
  case "$STT_CHOICE" in
    2|groq) STT_PROVIDER=groq ;;
    3|openrouter) STT_PROVIDER=openrouter ;;
    *) STT_PROVIDER=openai ;;
  esac
fi
case "$STT_PROVIDER" in
  openrouter)
    if [ -z "${OPENROUTER_API_KEY:-}" ] && have_tty; then
      echo
      echo "OpenRouter API key (openrouter.ai/keys)"
      prompt_secret OPENROUTER_API_KEY "  OPENROUTER_API_KEY"
    fi
    ;;
  groq)
    if [ -z "${GROQ_API_KEY:-}" ] && have_tty; then
      echo
      echo "Groq API key (free at console.groq.com)"
      prompt_secret GROQ_API_KEY "  GROQ_API_KEY"
    fi
    ;;
  openai)
    if [ -z "${OPENAI_API_KEY:-}" ] && have_tty; then
      echo
      echo "OpenAI API key"
      prompt_secret OPENAI_API_KEY "  OPENAI_API_KEY"
    fi
    ;;
  *)
    echo "ERROR: unknown STT_PROVIDER='$STT_PROVIDER' (expected: openai, groq, openrouter)" >&2
    exit 1
    ;;
esac
# The STT key is optional: all three providers geoblock some regions (RU/BY return
# 403 from every one of them), and a box there cannot use any key. Without one the
# server still boots — audio.ts logs a warning and only voice transcription fails.
case "$STT_PROVIDER" in
  openai)     STT_KEY_VALUE="${OPENAI_API_KEY:-}" ;;
  groq)       STT_KEY_VALUE="${GROQ_API_KEY:-}" ;;
  openrouter) STT_KEY_VALUE="${OPENROUTER_API_KEY:-}" ;;
esac
if [ -z "$STT_KEY_VALUE" ]; then
  echo "WARNING: no API key for STT_PROVIDER=$STT_PROVIDER — voice messages will not be transcribed." >&2
  echo "         Add it to ~/volatclaw/.env later to enable them." >&2
fi

[ -n "$V_KEY" ] || { echo "ERROR: V_KEY is required" >&2; exit 1; }
[ -n "$GITHUB_PAT_RW" ] || { echo "ERROR: GITHUB_PAT_RW is required (for cloning bot repos)" >&2; exit 1; }

# Verify the license key resolves to a downloadable build before we touch disk.
echo "Verifying license key..."
art_status=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$LICENSE_SERVER/artifact" \
  -H 'content-type: application/json' -d "{\"licenseKey\":\"$V_KEY\"}")
if [ "$art_status" != "200" ]; then
  echo "ERROR: license key not accepted by $LICENSE_SERVER (HTTP $art_status)." >&2
  echo "       Check the key is correct and hasn't been revoked." >&2
  exit 1
fi
echo "  license key valid ✓"

# DOMAIN is optional — when set, configures webhook mode. Otherwise polling.
if [ -n "${DOMAIN:-}" ]; then
  echo "Webhook mode requested for domain: $DOMAIN"
  domain_ip=$(getent ahostsv4 "$DOMAIN" 2>/dev/null | awk 'NR==1{print $1}' || echo "")
  if [ -z "$domain_ip" ]; then
    echo "ERROR: $DOMAIN does not resolve. Check DNS." >&2
    exit 1
  fi
  echo "  $DOMAIN → $domain_ip"
else
  echo
  echo "  → polling mode (no DOMAIN set)"
fi

# Install git if missing
if ! command -v git &> /dev/null; then
  echo "Installing git..."
  sudo apt-get update
  sudo apt-get install -y git
fi

# Configure git credentials: the RW PAT for github.com (host-only). This is used
# only for cloning the customer's BOT repos — the volatclaw build itself comes
# from the license server, not git.
echo "Configuring git credentials..."
git config --global credential.helper store
umask 077
cat > ~/.git-credentials <<EOF
https://atanych:${GITHUB_PAT_RW}@github.com
EOF
chmod 600 ~/.git-credentials

# Install the EXACT nodejs.org official Node — NOT nodesource. The shipped artifact
# is V8 bytecode compiled in CI; V8 cached data is only loadable on the identical
# Node *build*, and CI uses the nodejs.org official build (actions/setup-node). A
# nodesource build of the same version produces incompatible bytecode. This MUST
# equal the version pinned in .github/workflows/build-dist.yml.
NODE_VERSION="${NODE_VERSION:-22.22.2}"
if [ "$(/usr/local/bin/node -v 2>/dev/null)" != "v$NODE_VERSION" ]; then
  echo "Installing Node.js $NODE_VERSION (nodejs.org official)..."
  curl -fsSL "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" -o /tmp/node.tar.xz
  sudo tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1
  rm -f /tmp/node.tar.xz
fi
export PATH="/usr/local/bin:$PATH"   # ensure the official node/npm win for the rest of this script
echo "Node.js $(/usr/local/bin/node -v) (nodejs.org official, /usr/local/bin)"

# Fetch the compiled build from the license server (Supabase-backed). The license
# key authorizes a short-lived signed download URL; the tarball is the source-free
# bytecode artifact + runtime files. node is present (installed above) to parse
# the JSON response.
mkdir -p ~/volatclaw
if [ -n "${VOLAT_ARTIFACT:-}" ]; then
  # Box-built release (provision-server.sh). Unsigned by construction — the
  # operator built it from a git ref on this box — so there is no signature to
  # check. The license key was already verified above.
  [ -f "$VOLAT_ARTIFACT" ] || { echo "ERROR: VOLAT_ARTIFACT=$VOLAT_ARTIFACT does not exist" >&2; exit 1; }
  echo "Installing local build $VOLAT_ARTIFACT (built on this box — no signature check)..."
  tar -xzf "$VOLAT_ARTIFACT" -C ~/volatclaw
else
echo "Fetching volatclaw build..."
build_resp=$(curl -fsS -X POST "$LICENSE_SERVER/artifact" \
  -H 'content-type: application/json' -d "{\"licenseKey\":\"$V_KEY\"}")
build_url=$(echo "$build_resp" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);if(!j.ok){console.error(j.error||'fetch failed');process.exit(1)}process.stdout.write(j.url||'')})")
build_sig_url=$(echo "$build_resp" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{process.stdout.write(JSON.parse(s).sigUrl||'')}catch{}})")
[ -n "$build_url" ] || { echo "ERROR: could not obtain artifact URL from $LICENSE_SERVER" >&2; exit 1; }
[ -n "$build_sig_url" ] || { echo "ERROR: license server returned no signature URL — refusing (build integrity unverifiable)." >&2; exit 1; }
curl -fsSL "$build_url" -o /tmp/volatclaw.tar.gz
curl -fsSL "$build_sig_url" -o /tmp/volatclaw.tar.gz.sig
# Verify the build signature against the EMBEDDED public key BEFORE extracting (fail-closed).
VOLAT_BUILD_PUBKEY="MCowBQYDK2VwAyEAG1tx2NhWarFF7Xgo0+75iAeYn1d6Cuxd++unmpO4nbk=" node -e '
  const crypto=require("crypto"),fs=require("fs");
  const pub=crypto.createPublicKey({key:Buffer.from(process.env.VOLAT_BUILD_PUBKEY,"base64"),format:"der",type:"spki"});
  const digest=crypto.createHash("sha256").update(fs.readFileSync("/tmp/volatclaw.tar.gz")).digest();
  const sig=Buffer.from(fs.readFileSync("/tmp/volatclaw.tar.gz.sig","utf8").trim(),"base64");
  process.exit(crypto.verify(null,digest,pub,sig)?0:1);
' || { echo "❌ BUILD SIGNATURE VERIFICATION FAILED — refusing to install (possible tampering)." >&2; rm -f /tmp/volatclaw.tar.gz /tmp/volatclaw.tar.gz.sig; exit 1; }
echo "✅ build signature verified"
tar -xzf /tmp/volatclaw.tar.gz -C ~/volatclaw
rm -f /tmp/volatclaw.tar.gz /tmp/volatclaw.tar.gz.sig
fi

cd ~/volatclaw

# Install dependencies. The artifact ships precompiled bytecode (dist/volatclaw.jsc)
# — no build/compile step here; nothing readable is on disk.
echo "Installing dependencies..."
npm install

# Per-bot UID isolation runs each agent as bot_<name> (≠ the install owner), so the
# install's shared, Landlock-allowlisted assets must be readable+traversable by other
# users. `npm install` here inherits the `umask 077` set above for ~/.git-credentials,
# which creates node_modules 700 → a UID-isolated agent fails with
# "exec .../claude: permission denied". Make the shared assets bot-readable. These
# trees hold no secrets (.env/.license/dist are separate, owner-only).
chmod -R o+rX node_modules skills bin 2>/dev/null || true

# CJK fonts for headless Chromium. Without these, agent-browser / stealth-browser
# screenshots and text extraction on Chinese / Japanese / Korean sites
# (autohome.com.cn, dongchedi, etc.) render as tofu boxes because fontconfig has
# no glyphs to fall back to. ~200MB on disk. Skip on re-runs so setup stays fast.
if ! dpkg -s fonts-noto-cjk >/dev/null 2>&1; then
  echo "Installing CJK fonts (Noto)..."
  sudo apt-get update
  sudo apt-get install -y fontconfig fonts-noto-cjk fonts-noto-cjk-extra
  fc-cache -f
fi

# Color emoji font for headless Chromium. Without it, HTML/PNG renders that use
# emoji (report headers, chat mock-ups, dashboards, PDF exports) fall back to
# monochrome boxes or invisible glyphs, and bot-side templates have to load
# Twemoji from Google Fonts with a `wait 2500` hack to let the webfont settle
# before screenshotting. One system font kills that dependency for every bot.
# ~5MB. Skip on re-runs so setup stays fast.
if ! dpkg -s fonts-noto-color-emoji >/dev/null 2>&1; then
  echo "Installing Noto Color Emoji..."
  sudo apt-get update
  sudo apt-get install -y fontconfig fonts-noto-color-emoji
  fc-cache -f
fi

# LibreOffice (headless) for bots that convert docx/xlsx → pdf via
# `soffice --headless --convert-to pdf`. Writer + Calc cover the accountant/legal
# document workflows we've hit so far; full metapackage would also pull impress/
# draw/math/base which we don't need on a server. --no-install-recommends drops
# the Java runtime, GNOME icons, and print stack (~350MB installed vs ~500MB
# with recommends). fonts-liberation is metrically compatible with Times/Arial/
# Courier — without it, docx with those fonts falls back to a random substitute
# and the pagination shifts. Skip on re-runs so setup stays fast.
if ! dpkg -s libreoffice-writer >/dev/null 2>&1; then
  echo "Installing LibreOffice (headless) + fonts-liberation..."
  sudo apt-get update
  sudo apt-get install -y --no-install-recommends \
    libreoffice-writer libreoffice-calc fonts-liberation
fi

# python3-pip + python3-venv — python3 (3.12) ships with the base image but the
# pip installer and venv module are separate packages. Without them bots can't
# create per-bot venvs in their sandbox (`python3 -m venv tmp/venv`). ~15MB.
# Skip on re-runs so setup stays fast.
if ! dpkg -s python3-pip >/dev/null 2>&1; then
  echo "Installing python3-pip + python3-venv..."
  sudo apt-get update
  sudo apt-get install -y python3-pip python3-venv
fi

# acl (setfacl) — the bot provisioner sets a default ACL g:bots:rw on the shared
# ~/.claude so the ONE Claude credential stays readable by every bot UID after a
# token refresh rewrites it 0600. Without this package a refresh locks every bot
# out ("Not logged in") — demo, 2026-09-25. provision-bot-uid.sh and
# install-bot-ops.sh also install it; listed here so a new server gets it up front.
if ! dpkg -s acl >/dev/null 2>&1; then
  echo "Installing acl (setfacl)..."
  sudo apt-get update
  sudo apt-get install -y acl
fi

# python-pptx — PPTX generation library used across client bots (accountant /
# consultant workflows that hand out slides). Pure Python, ~1MB. Installed
# system-wide via --break-system-packages because Ubuntu 24.04 enforces PEP 668
# on the base Python but there's no Debian package for it. Skip on re-runs.
if ! python3 -c "import pptx" >/dev/null 2>&1; then
  echo "Installing python-pptx (system-wide, --break-system-packages)..."
  sudo pip3 install --break-system-packages python-pptx
fi

# Install agent-browser (needs sudo for global install into /usr/lib/node_modules).
# Deliberately NO `agent-browser install`: it downloads Chrome-for-Testing into
# ~/.agent-browser/browsers/, which agent-browser then prefers — and on Ubuntu
# 24.04 (apparmor_restrict_unprivileged_userns=1) only /opt/google/chrome/chrome is
# AppArmor-blessed for user namespaces, so every launch dies with "No usable
# sandbox". agent-browser falls back to system Chrome, installed just below.
echo "Installing agent-browser..."
sudo npm i -g agent-browser
rm -rf ~/.agent-browser/browsers/chrome-* 2>/dev/null || true

# Install stealth-browser CLI — same shape (global `stealth-browser` on PATH).
# `sudo npm link` links the in-tree bin (./bin/stealth-browser.mjs) into the
# global prefix; using sudo matches how agent-browser was installed above and
# avoids permission errors on the system prefix.
echo "Linking stealth-browser..."
sudo npm link
# Install real Chrome for Patchright (system channel — best stealth). Usually a
# no-op (Chrome is already present via the OS package).
npx patchright install chrome || true
if [ ! -x /opt/google/chrome/chrome ]; then
  echo "WARNING: system Chrome (/opt/google/chrome/chrome) is missing — agent-browser has no" >&2
  echo "         sandbox-capable browser. Install it: sudo apt-get install -y google-chrome-stable" >&2
fi

# Stage Patchright's patched Chromium + headless shell into a SHARED, agent-readable
# cache. Under per-bot UID isolation each agent's XDG_CACHE_HOME points at its own
# (empty) per-bot cache, so a browser installed in volatclaw's ~/.cache is invisible
# to it. Installing under node_modules/.cache (Landlock read-allowlist, made
# world-readable) lets every bot's stealth-browser find Chromium; agent.ts sets
# PLAYWRIGHT_BROWSERS_PATH to this dir when present. No sudo needed.
export PLAYWRIGHT_BROWSERS_PATH="$PWD/node_modules/.cache/ms-playwright"
# NEVER fatal (`|| true`) — a browser-install hiccup must not abort setup (a missing
# stealth browser only degrades one skill). Same guard as update.sh.
npx patchright install chromium chromium-headless-shell || true
chmod -R o+rX node_modules/.cache 2>/dev/null || true

# Native Claude Code CLI for the OPERATOR (interactive `claude`: the OAuth login
# the bots share, debugging). The bots never use it — they run the binary bundled
# with the Agent SDK (resolveClaudeExecutable in src/agent-isolation.ts), pinned by
# the lockfile and wrapped by the Landlock sandbox. This one auto-updates, which is
# fine for a human tool and exactly why the runtime doesn't use it. Non-fatal: a
# failed download (e.g. a region block) only costs the convenience command.
if [ ! -x "$HOME/.local/bin/claude" ]; then
  echo "Installing Claude Code CLI (native, for interactive use)..."
  curl -fsSL https://claude.ai/install.sh | bash || echo "WARNING: Claude Code CLI install failed — run the bundled one: $HOME/volatclaw/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64/claude" >&2
fi
grep -q 'HOME/.local/bin' "$HOME/.bashrc" 2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"

# Create bots and tmp directories
mkdir -p bots tmp

# Bot agents run as bot_<name> (not this user) and must TRAVERSE the install root
# to reach their own bots/<name>/ folder and the shared node_modules/skills/bin.
# The `umask 077` above (for ~/.git-credentials) creates ~/volatclaw and bots/ as
# 0700, so every turn died in the wrapper — "mkdir …/bots: permission denied",
# "exec …/claude: permission denied" (rf box, 2026-09-27). Grant traverse ONLY
# (o+x, no read): bots can walk the path but not list the install root, and
# dist/, scripts/, .env stay owner-only.
chmod o+x "$HOME" "$HOME/volatclaw" "$HOME/volatclaw/bots"

# Set up .env if not exists
if [ ! -f .env ]; then
  cp .env.example .env
  echo ""
  echo ">>> Edit ~/volatclaw/.env with your API keys"
fi

# Persist the license key into .env (update.sh reads it to fetch future builds).
if grep -qE '^V_KEY=' .env; then
  sed -i.bak "s|^V_KEY=.*|V_KEY=$V_KEY|" .env
else
  echo "V_KEY=$V_KEY" >> .env
fi
rm -f .env.bak

# Persist STT provider + the matching API key into .env. sed assumes the keys
# already exist in .env (they're in .env.example) — re-runs are safe.
sed -i.bak "s|^STT_PROVIDER=.*|STT_PROVIDER=$STT_PROVIDER|" .env
case "$STT_PROVIDER" in
  groq)       sed -i.bak "s|^GROQ_API_KEY=.*|GROQ_API_KEY=$GROQ_API_KEY|" .env ;;
  openai)     sed -i.bak "s|^OPENAI_API_KEY=.*|OPENAI_API_KEY=$OPENAI_API_KEY|" .env ;;
  openrouter) sed -i.bak "s|^OPENROUTER_API_KEY=.*|OPENROUTER_API_KEY=$OPENROUTER_API_KEY|" .env ;;
esac
rm -f .env.bak

# Apply webhook config when DOMAIN is set (idempotent — safe on re-runs).
if [ -n "${DOMAIN:-}" ]; then
  sed -i.bak \
    -e "s|^MODE=.*|MODE=webhook|" \
    -e "s|^WEBHOOK_BASE_URL=.*|WEBHOOK_BASE_URL=https://$DOMAIN|" \
    -e "s|^PORT=.*|PORT=80|" \
    .env
  rm -f .env.bak
  echo "Configured .env for webhook mode at https://$DOMAIN (PORT=80)"
fi

# Dashboard access is Telegram-login only (no shared password). Remind the
# operator to list themselves so the fleet-wide views (/metrics, /inventory)
# are reachable; a stale METRICS_PASSWORD from an older install is inert.
if grep -qE '^METRICS_PASSWORD=' .env; then
  sed -i.bak '/^METRICS_PASSWORD=/d' .env
  rm -f .env.bak
  echo "Removed METRICS_PASSWORD from .env (dashboard Basic Auth no longer exists)."
fi
if grep -qE '^DASHBOARD_OPERATORS=$' .env; then
  echo "NOTE: DASHBOARD_OPERATORS is empty — add your Telegram user ID to it to reach /metrics and /inventory."
fi

# Harden: pin ptrace scope so one bot's agent can't read a sibling bot's process
# memory/env (/proc/<pid>/environ, mem, maps). All bot agents run as the same OS
# user under one conductor, so cross-bot snoop is blocked only by YAMA: scope=1
# ("restricted") limits ptrace-read to a process's own descendants, and sibling
# agents are NOT descendants of each other. This is the Ubuntu default, but we pin
# it via a sysctl.d drop-in so a re-imaged or misconfigured box can't silently
# weaken cross-bot isolation. We never LOWER a stricter value (2 = admin-only is
# preserved). Chrome/agent-browser crash handling works under scope=1 (crashpad
# uses PR_SET_PTRACER), so this doesn't break the browser.
echo "Hardening ptrace scope (cross-bot /proc isolation)..."
if [ -e /proc/sys/kernel/yama/ptrace_scope ]; then
  cur=$(cat /proc/sys/kernel/yama/ptrace_scope 2>/dev/null || echo 1)
  want=$cur
  if [ "${want:-0}" -lt 1 ]; then want=1; fi
  sudo tee /etc/sysctl.d/60-volatclaw-hardening.conf >/dev/null <<SYSCTL
# Managed by volatclaw setup.sh — restrict ptrace so sibling bot agents can't read
# each other's process memory/env (/proc/<pid>/environ, mem, maps). Do not lower below 1.
kernel.yama.ptrace_scope = $want
SYSCTL
  sudo chmod 0644 /etc/sysctl.d/60-volatclaw-hardening.conf
  sudo sysctl -w kernel.yama.ptrace_scope="$want" >/dev/null || true
  echo "  kernel.yama.ptrace_scope = $(cat /proc/sys/kernel/yama/ptrace_scope) (pinned via /etc/sysctl.d/60-volatclaw-hardening.conf)"
else
  echo "  (YAMA LSM not present on this kernel — skipping ptrace_scope pin)"
fi

# Network hardening: SSH key-only auth, fail2ban, journald cap, GRUB fallback
# menu, and a deny-by-default firewall. The logic lives in harden-server.sh so
# the same baseline can be applied standalone to an existing server without this
# file and that one drifting apart. DOMAIN is passed through so the firewall
# knows whether :80 is needed at all, and whether to pin it to Cloudflare.
echo "Applying network hardening..."
sudo DOMAIN="${DOMAIN:-}" SKIP_FIREWALL="${SKIP_FIREWALL:-0}" \
  bash "$HOME/volatclaw/scripts/harden-server.sh"
# harden-server.sh arms a 5-minute deadman that disables ufw unless cancelled.
# On a fresh install SSH is allow-listed and we have just used it, so cancel it
# here rather than leaving the operator to notice the warning.
sudo systemctl stop ufw-deadman.timer 2>/dev/null || true

# Install systemd service. The unit-file heredoc lives in install-volatclaw-unit.sh
# so the same content is used here and on standalone runs against existing servers.
echo "Installing systemd service..."
"$HOME/volatclaw/scripts/install-volatclaw-unit.sh"

# Restart trigger: a path unit watches $HOME/volatclaw/bots/.restart-request.json.
# The /restart command writes that file; systemd then runs `systemctl restart
# volatclaw` (oneshot) so the bot doesn't have to kill its own process mid-reply.
# The unit-file heredoc lives in install-restart-unit.sh so the same content is
# used here and on standalone runs against existing servers.
"$HOME/volatclaw/scripts/install-restart-unit.sh"

# Scoped service access for the non-sudo service user: a polkit rule so it can
# start/stop/restart/reload ONLY its own units without sudo, plus systemd-journal
# membership so it can read its logs. Idempotent; keeps the user non-sudo.
echo "Granting volatclaw scoped service control + log access..."
sudo "$HOME/volatclaw/scripts/install-volatclaw-access.sh"

# Narrow NOPASSWD entries for per-bot provisioning + deletion. Copies the three
# ops scripts to /usr/local/sbin (root:root 0755) and writes
# /etc/sudoers.d/volatclaw-bot-ops. Required so David (the ops bot) can create /
# delete bots after the standing sudo is revoked below — the alternative would be
# leaving standing sudo in place, which defeats the isolation model. Idempotent.
echo "Installing narrow bot-ops sudoers entries..."
sudo "$HOME/volatclaw/scripts/install-bot-ops.sh"

# Schedule daily auto-push of bot repos at 02:00. Idempotent: any existing
# crontab line that mentions push-bots.sh is removed before we add ours.
echo "Scheduling daily push-bots cron at 02:00..."
push_cron="0 2 * * * $HOME/volatclaw/scripts/push-bots.sh >> $HOME/volatclaw/tmp/push-bots.log 2>&1"
( crontab -l 2>/dev/null | grep -v 'volatclaw/scripts/push-bots.sh'; echo "$push_cron" ) | crontab -

# Revoke the service user's standing NOPASSWD sudo. Every step above that needed
# sudo has run; everything after this point (deploys via update.sh, /restart,
# per-bot provisioning via the narrow sudoers entries installed just above) is
# designed to work without it. Idempotent (no-op on already-hardened boxes).
echo "Hardening: revoking standing sudo for the volatclaw service user..."
bash "$HOME/volatclaw/scripts/harden-sudo.sh"

# unattended-upgrades INSTALLS security updates but never reboots, so kernel and
# libc fixes sit staged and inactive indefinitely — "0 pending security updates"
# looks reassuring while the running kernel quietly falls months behind. One
# server accumulated 10 kernel versions over 19 weeks of uptime this way. That
# matters here specifically: bot agents are exposed to prompt injection and their
# confinement (Landlock + per-bot UIDs) is kernel-enforced, so a local privesc CVE
# in a stale kernel undermines the whole isolation model. Surface it loudly rather
# than auto-rebooting, which would restart a live chat service unattended.
if [ -f /var/run/reboot-required ]; then
  echo ""
  echo "⚠️  REBOOT REQUIRED — staged kernel/libc updates are not running yet."
  echo "    running kernel: $(uname -r)"
  echo "    Reboot when convenient:  sudo reboot"
  echo "    Re-check any time with:  ls /var/run/reboot-required"
fi

echo ""
echo "=== Setup complete (sudo revoked — you're now a non-sudo user) ==="
echo "1. Edit ~/volatclaw/.env (API keys + admin DATABASE_URL)"
echo "2. Clone bot repos via HTTPS: git clone https://github.com/atanych/<bot>.git ~/volatclaw/bots/<bot>"
echo "3. Provision per-bot isolation (UID + DB) for each cloned bot — REQUIRED, not optional:"
echo "   UID isolation is mandatory and fail-closed; an unprovisioned bot cannot run a turn."
echo "     sudo -n /usr/local/sbin/volat-provision-bot    <bot>"
echo "     sudo -n /usr/local/sbin/volat-provision-bot-db <bot>"
echo "   (David's create-bot flow does this automatically for bots it scaffolds.)"
if [ -n "${DOMAIN:-}" ]; then
  echo "4. For each bot: scripts/set-webhook.sh <bot> (registers TG webhook + generates secret)"
  echo "5. journalctl -u volatclaw -f (watch logs)"
else
  echo "4. journalctl -u volatclaw -f (watch logs)"
fi
echo ""
echo "🔒 Rare privileged ops (systemd unit edits, kernel-level changes) must be run as ROOT."
