Files
PlantAI-rag-new/tooling/crypto/client/install_plantai.sh
T
jerryandClaude Fable 5 27946edc2b P0/M1: 收编 encryption-test 工具链入 tooling/
- legacy_plantai_server_package/: phase_a–f + compile_* + bin/plantai_admin.py
  + tools/(audit/elf_visibility/smoke/build_delivery_manifest) 完整基线
- legacy_docker_packager/: protected 形态参考
- crypto/client/: plantai_core.py(AES-GCM 原语) + install_plantai.sh 参考
- secrets/provision 严格排除(gitignore),入 git 前已脱敏清理
- LEGACY_MAPPING.md 记录源与改造方向

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:59:29 +08:00

251 lines
9.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# PlantAI client-side installer. Safe to ship to any customer: this script
# and its bin/ folder contain no private key and no master-key logic, only
# the ability to collect a fingerprint and consume vendor-issued files.
#
# Usage:
# ./install_plantai.sh fingerprint
# -> collects this machine's hardware fingerprint and tells you where
# to send it to the vendor. Run this FIRST, before you have a
# license.
#
# ./install_plantai.sh install
# -> sets up the directory tree, installs the app + pre-encrypted
# assets from ./release/, and imports the vendor-issued
# license.json / asset.key.enc if you've placed the provision
# bundle at ./release/provision/ (or point PROVISION_BUNDLE at the
# .tar.gz the vendor sent you).
set -euo pipefail
umask 027 # Config files owner rwx, group rx; prevent world-readable by default
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for cfg in "/etc/plantai/install.env" "$SCRIPT_DIR/install.env"; do
[ -f "$cfg" ] && . "$cfg"
done
# ---------------------------------------------------------------------------
# Safety: validate that a path is safe to rm -rf
# ---------------------------------------------------------------------------
validate_path_under() {
local target="$1"
local root="$2"
local desc="${3:-path}"
[ -z "$target" ] && { echo "SECURITY: $desc is empty — refusing to rm -rf" >&2; exit 1; }
[ "$target" = "/" ] && { echo "SECURITY: $desc is '/' — refusing to rm -rf" >&2; exit 1; }
local resolved
resolved="$(cd "$(dirname "$target" 2>/dev/null || echo /)" 2>/dev/null && pwd)/$(basename "$target")" || true
local root_resolved
root_resolved="$(cd "$root" 2>/dev/null && pwd)" || root_resolved=""
if [ -n "$root_resolved" ] && [ -n "$resolved" ]; then
case "$resolved" in
"$root_resolved"|"$root_resolved/"*) ;;
*)
echo "SECURITY: $desc ($resolved) is NOT under expected root ($root_resolved) — refusing to rm -rf" >&2
exit 1
;;
esac
fi
[ "$target" = "$HOME" ] && { echo "SECURITY: $desc is \$HOME — refusing to rm -rf" >&2; exit 1; }
return 0
}
validate_version() {
local v="$1"
[ -z "$v" ] && { echo "SECURITY: version string is empty" >&2; exit 1; }
[[ "$v" =~ ^[A-Za-z0-9._-]+$ ]] || {
echo "SECURITY: version string '$v' contains illegal characters — only [A-Za-z0-9._-] allowed" >&2
exit 1
}
[[ "$v" == *".."* ]] && { echo "SECURITY: version string '$v' contains '..'" >&2; exit 1; }
return 0
}
default_root() {
if [ "$(id -u)" = "0" ] || [ -w "/opt" ]; then
echo "/opt/PlantAI"
else
echo "${HOME}/PlantAI"
fi
}
PLANTAI_ROOT="${PLANTAI_ROOT:-$(default_root)}"
RELEASE_SRC="${RELEASE_SRC:-$SCRIPT_DIR/release}"
DEVICE_ID="${DEVICE_ID:-$(hostname)}"
INSTALL_USER_SERVICE="${INSTALL_USER_SERVICE:-0}"
WEB_PORT="${PLANTAI_WEB_PORT:-7880}"
PYTHON="${PLANTAI_PYTHON:-python3}"
if [ -z "${BBAV5_CONDA_BASE:-}" ] && command -v conda >/dev/null 2>&1; then
BBAV5_CONDA_BASE="$(conda info --base 2>/dev/null || true)"
fi
BBAV5_CONDA_BASE="${BBAV5_CONDA_BASE:-}"
BBAV5_DEFAULT_ENV="${BBAV5_DEFAULT_ENV:-plantai}"
BBAV5_QUEUE_PREFIX="${BBAV5_QUEUE_PREFIX:-plantai_${WEB_PORT}}"
BBAV5_REDIS_URL="${BBAV5_REDIS_URL:-redis://localhost:6379/0}"
cmd_fingerprint() {
mkdir -p "$PLANTAI_ROOT/config"
"$PYTHON" "$SCRIPT_DIR/bin/plantai_client.py" collect-fingerprint \
--device-id "$DEVICE_ID" \
--root "$PLANTAI_ROOT" \
--output "$PLANTAI_ROOT/config/device_fingerprint.json"
echo
echo "Fingerprint written to: $PLANTAI_ROOT/config/device_fingerprint.json"
echo "Send this file to your vendor. They will send back a provision bundle"
echo "(license.json + asset.key.enc) -- place it at $RELEASE_SRC/provision/"
echo "or set PROVISION_BUNDLE=/path/to/plantai-provision-*.tar.gz, then run:"
echo " ./install_plantai.sh install"
}
cmd_install() {
need_file() { [ -e "$1" ] || { echo "Required path not found: $1" >&2; exit 1; }; }
need_file "$RELEASE_SRC/app"
need_file "$SCRIPT_DIR/bin/plantai_client.py"
need_file "$SCRIPT_DIR/bin/plantai_core.py"
need_file "$SCRIPT_DIR/bin/PlantAI-launcher"
VERSION="$(cat "$RELEASE_SRC/VERSION" 2>/dev/null || echo unknown)"
validate_version "$VERSION"
RELEASE_DIR="$PLANTAI_ROOT/releases/$VERSION"
APP_DIR="$RELEASE_DIR/app"
echo "[1/6] Create PlantAI directory tree: $PLANTAI_ROOT"
mkdir -p \
"$PLANTAI_ROOT/bin" "$PLANTAI_ROOT/config" "$PLANTAI_ROOT/assets" \
"$PLANTAI_ROOT/data/input" "$PLANTAI_ROOT/data/workspace" "$PLANTAI_ROOT/data/final" \
"$PLANTAI_ROOT/data/logs" "$PLANTAI_ROOT/data/memory_storage" "$PLANTAI_ROOT/data/reproducibility" \
"$PLANTAI_ROOT/var/cache" "$PLANTAI_ROOT/var/tmp" "$PLANTAI_ROOT/var/lock" "$PLANTAI_ROOT/var/audit" \
"$PLANTAI_ROOT/runtime" "$PLANTAI_ROOT/tools" "$RELEASE_DIR"
echo "[2/6] Install app code from release payload (already excludes models/prompts)"
validate_path_under "$APP_DIR" "$PLANTAI_ROOT/releases" "APP_DIR"
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR"
rsync -a --delete "$RELEASE_SRC/app/" "$APP_DIR/"
ln -sfn "$RELEASE_DIR" "$PLANTAI_ROOT/releases/current"
echo "[3/6] Install launcher + client tool"
install -m 0755 "$SCRIPT_DIR/bin/plantai_client.py" "$PLANTAI_ROOT/bin/plantai_client.py"
install -m 0644 "$SCRIPT_DIR/bin/plantai_core.py" "$PLANTAI_ROOT/bin/plantai_core.py"
install -m 0755 "$SCRIPT_DIR/bin/PlantAI-launcher" "$PLANTAI_ROOT/bin/PlantAI-launcher"
echo "[4/6] Install pre-encrypted assets + public key from release payload"
rsync -a "$RELEASE_SRC/assets/" "$PLANTAI_ROOT/assets/"
install -m 0644 "$RELEASE_SRC/config/supplier_public_key.pem" "$PLANTAI_ROOT/config/supplier_public_key.pem"
echo "[5/6] Import vendor-issued license + asset key, if available"
PROVISION_BUNDLE="${PROVISION_BUNDLE:-}"
if [ -n "$PROVISION_BUNDLE" ] && [ -f "$PROVISION_BUNDLE" ]; then
# Extract to a temp dir first so we can inspect filenames before
# installing to the real config directory.
PROV_TMP="$(mktemp -d "${TMPDIR:-/tmp}/plantai-provision.XXXXXX")"
tar -C "$PROV_TMP" -xzf "$PROVISION_BUNDLE" license.json asset.key.enc
for fn in license.json asset.key.enc; do
if [ ! -f "$PROV_TMP/$fn" ]; then
echo "ERROR: provision bundle is missing required file: $fn" >&2
validate_path_under "$PROV_TMP" "${TMPDIR:-/tmp}" "PROV_TMP"
rm -rf "$PROV_TMP"
exit 1
fi
if [ -L "$PROV_TMP/$fn" ]; then
echo "ERROR: provision bundle contains a symlink for $fn (unsafe)" >&2
validate_path_under "$PROV_TMP" "${TMPDIR:-/tmp}" "PROV_TMP"
rm -rf "$PROV_TMP"
exit 1
fi
done
install -m 0644 "$PROV_TMP/license.json" "$PLANTAI_ROOT/config/license.json"
install -m 0644 "$PROV_TMP/asset.key.enc" "$PLANTAI_ROOT/config/asset.key.enc"
validate_path_under "$PROV_TMP" "${TMPDIR:-/tmp}" "PROV_TMP"
rm -rf "$PROV_TMP"
elif [ -f "$RELEASE_SRC/provision/license.json" ] && [ -f "$RELEASE_SRC/provision/asset.key.enc" ]; then
install -m 0644 "$RELEASE_SRC/provision/license.json" "$PLANTAI_ROOT/config/license.json"
install -m 0644 "$RELEASE_SRC/provision/asset.key.enc" "$PLANTAI_ROOT/config/asset.key.enc"
else
echo " No license.json/asset.key.enc found yet."
echo " Run './install_plantai.sh fingerprint' first if you haven't, send it to"
echo " your vendor, then place their reply at $RELEASE_SRC/provision/ and re-run install."
fi
{
echo "PLANTAI_ROOT=$PLANTAI_ROOT"
echo "PLANTAI_RUNTIME=$PLANTAI_ROOT/runtime"
echo "PLANTAI_WEB_PORT=$WEB_PORT"
echo "PLANTAI_PYTHON=$PYTHON"
[ -n "$BBAV5_CONDA_BASE" ] && echo "BBAV5_CONDA_BASE=$BBAV5_CONDA_BASE"
echo "BBAV5_DEFAULT_ENV=$BBAV5_DEFAULT_ENV"
echo "BBAV5_QUEUE_PREFIX=$BBAV5_QUEUE_PREFIX"
echo "BBAV5_REDIS_URL=$BBAV5_REDIS_URL"
for name in \
NEO4J_URI NEO4J_USER NEO4J_PASSWORD QA_NEO4J_PASSWORD \
QDRANT_URL V15_QDRANT_COLLECTION V15_BGE_MODEL V15_OLLAMA_EMBED_URL \
RAG_PYTHON RAG_PORT QA_PYTHON QA_PORT \
QA_QDRANT_PORT QA_QDRANT_GRPC_PORT \
QA_NEO4J_HTTP_PORT QA_NEO4J_BOLT_PORT \
OLLAMA_PORT PLANTAI_RUNTIME_DATA \
QA_OLLAMA_GENERATION_MODEL QA_OLLAMA_EMBED_MODEL
do
if [ -n "${!name:-}" ]; then
printf '%s=%s\n' "$name" "${!name}"
fi
done
} > "$PLANTAI_ROOT/config/service.env"
chmod 600 "$PLANTAI_ROOT/config/service.env"
echo "[6/6] Create systemd user service file"
mkdir -p "$PLANTAI_ROOT/config/systemd"
cat > "$PLANTAI_ROOT/config/systemd/plantai.service" <<EOF
[Unit]
Description=PlantAI service
After=network.target
[Service]
Type=simple
WorkingDirectory=$APP_DIR
EnvironmentFile=$PLANTAI_ROOT/config/service.env
ExecStart=$PLANTAI_ROOT/bin/PlantAI-launcher
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
EOF
if [ "$INSTALL_USER_SERVICE" = "1" ]; then
mkdir -p "$HOME/.config/systemd/user"
cp "$PLANTAI_ROOT/config/systemd/plantai.service" "$HOME/.config/systemd/user/"
systemctl --user daemon-reload
systemctl --user enable plantai.service
echo "User service enabled. Start with: systemctl --user start plantai.service"
fi
if [ -f "$PLANTAI_ROOT/config/license.json" ] && [ -f "$PLANTAI_ROOT/config/asset.key.enc" ]; then
echo "Verifying install..."
PLANTAI_ROOT="$PLANTAI_ROOT" "$PLANTAI_ROOT/bin/PlantAI-launcher" --check-only
fi
echo "Done. PLANTAI_ROOT=$PLANTAI_ROOT"
}
cmd_tpm2_setup() {
echo "Setting up TPM 2.0 hardware-bound wrapping key..."
"$PYTHON" "$SCRIPT_DIR/bin/plantai_client.py" tpm2-setup --root "$PLANTAI_ROOT"
echo
echo "TPM setup complete. Now run:"
echo " ./install_plantai.sh fingerprint"
echo "to collect a new fingerprint that includes the TPM public key."
echo "Send that fingerprint to your vendor to get TPM-bound license + asset key."
}
case "${1:-}" in
fingerprint) cmd_fingerprint ;;
install) cmd_install ;;
tpm2-setup) cmd_tpm2_setup ;;
*) echo "Usage: $0 {fingerprint|install|tpm2-setup}" >&2; exit 1 ;;
esac