- 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>
251 lines
9.9 KiB
Bash
Executable File
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
|