191 lines
7.7 KiB
Shell
191 lines
7.7 KiB
Shell
|
|
#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||
|
|
echo "INSTALL_REQUIRES_ROOT" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
SOURCE_DIR="${1:-}"
|
||
|
|
if [[ -z "$SOURCE_DIR" || ! -f "$SOURCE_DIR/runtime/zhulan_cell.py" ]]; then
|
||
|
|
echo "SOURCE_DIR_INVALID" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
EXPECTED_HOST="${ZHULAN_EXPECTED_HOST:-VM-12-12-ubuntu}"
|
||
|
|
EXPECTED_DMI="${ZHULAN_EXPECTED_DMI:-7a500f2d-9aed-4b93-b3b8-59c87c65d031}"
|
||
|
|
ACTUAL_HOST="$(hostname)"
|
||
|
|
ACTUAL_DMI="$(tr '[:upper:]' '[:lower:]' </sys/class/dmi/id/product_uuid)"
|
||
|
|
if [[ "$ACTUAL_HOST" != "$EXPECTED_HOST" || "$ACTUAL_DMI" != "$EXPECTED_DMI" ]]; then
|
||
|
|
echo "TARGET_IDENTITY_MISMATCH host=$ACTUAL_HOST dmi=$ACTUAL_DMI" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
if ! command -v bwrap >/dev/null 2>&1 || ! command -v apparmor_parser >/dev/null 2>&1; then
|
||
|
|
apt-get update
|
||
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends apparmor bubblewrap
|
||
|
|
fi
|
||
|
|
|
||
|
|
INSTALL_ROOT="/opt/guanghu/zhulan-remote-cell"
|
||
|
|
STATE_ROOT="/var/lib/guanghu/zhulan-remote-cell"
|
||
|
|
WORKSPACE_ROOT="/srv/guanghu/zhulan-cell/workspaces"
|
||
|
|
CANDIDATE_ROOT="/srv/guanghu/zhulan-cell/candidates"
|
||
|
|
SECRET_ROOT="/etc/guanghu/secrets"
|
||
|
|
ENV_FILE="/etc/guanghu/zhulan-remote-cell.env"
|
||
|
|
SERVICE_FILE="/etc/systemd/system/zhulan-remote-cell.service"
|
||
|
|
EXECUTOR_SERVICE_FILE="/etc/systemd/system/zhulan-validation-executor.service"
|
||
|
|
APPARMOR_FILE="/etc/apparmor.d/zhulan-remote-cell-bwrap"
|
||
|
|
BACKUP_ROOT="/var/backups/guanghu/zhulan-remote-cell"
|
||
|
|
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||
|
|
|
||
|
|
getent passwd zhulan-runtime >/dev/null || {
|
||
|
|
echo "RUNTIME_USER_MUST_BE_PRECREATED" >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
install -d -o root -g root -m 0755 "$INSTALL_ROOT" "$BACKUP_ROOT"
|
||
|
|
install -d -o zhulan-runtime -g zhulan-runtime -m 0700 "$STATE_ROOT"
|
||
|
|
install -d -o zhulan-runtime -g zhulan-runtime -m 0700 "$WORKSPACE_ROOT"
|
||
|
|
install -d -o zhulan-runtime -g zhulan-runtime -m 0700 "$CANDIDATE_ROOT"
|
||
|
|
install -d -o root -g zhulan-runtime -m 0750 "$SECRET_ROOT"
|
||
|
|
|
||
|
|
if [[ -d "$INSTALL_ROOT/runtime" ]]; then
|
||
|
|
tar -C "$INSTALL_ROOT" -czf "$BACKUP_ROOT/runtime-$STAMP.tgz" runtime ui policy.json 2>/dev/null || true
|
||
|
|
fi
|
||
|
|
if [[ -f "$ENV_FILE" ]]; then
|
||
|
|
install -m 0600 "$ENV_FILE" "$BACKUP_ROOT/env-$STAMP"
|
||
|
|
fi
|
||
|
|
if [[ -f "$SERVICE_FILE" ]]; then
|
||
|
|
install -m 0644 "$SERVICE_FILE" "$BACKUP_ROOT/service-$STAMP"
|
||
|
|
fi
|
||
|
|
if [[ -f "$EXECUTOR_SERVICE_FILE" ]]; then
|
||
|
|
install -m 0644 "$EXECUTOR_SERVICE_FILE" "$BACKUP_ROOT/executor-service-$STAMP"
|
||
|
|
fi
|
||
|
|
if [[ -f "$APPARMOR_FILE" ]]; then
|
||
|
|
install -m 0644 "$APPARMOR_FILE" "$BACKUP_ROOT/apparmor-$STAMP"
|
||
|
|
fi
|
||
|
|
|
||
|
|
install -d -o root -g root -m 0755 "$INSTALL_ROOT/runtime" "$INSTALL_ROOT/ui" "$INSTALL_ROOT/contracts"
|
||
|
|
install -o root -g root -m 0755 "$SOURCE_DIR/runtime/zhulan_cell.py" "$INSTALL_ROOT/runtime/zhulan_cell.py"
|
||
|
|
install -o root -g root -m 0755 "$SOURCE_DIR/runtime/zhulan_code_gate.py" "$INSTALL_ROOT/runtime/zhulan_code_gate.py"
|
||
|
|
install -o root -g root -m 0755 "$SOURCE_DIR/runtime/zhulan_validation_executor.py" "$INSTALL_ROOT/runtime/zhulan_validation_executor.py"
|
||
|
|
# Keep the distro bwrap binary non-setuid. A dedicated group-executable copy
|
||
|
|
# lets AppArmor grant userns only to this runtime path instead of weakening the
|
||
|
|
# host-wide unprivileged-userns policy or opening /usr/bin/bwrap for all users.
|
||
|
|
install -o root -g zhulan-runtime -m 0750 /usr/bin/bwrap "$INSTALL_ROOT/runtime/zhulan-bwrap"
|
||
|
|
install -o root -g root -m 0644 "$SOURCE_DIR/ui/index.html" "$INSTALL_ROOT/ui/index.html"
|
||
|
|
install -o root -g root -m 0644 "$SOURCE_DIR/ui/styles.css" "$INSTALL_ROOT/ui/styles.css"
|
||
|
|
install -o root -g root -m 0644 "$SOURCE_DIR/ui/app.js" "$INSTALL_ROOT/ui/app.js"
|
||
|
|
install -o root -g root -m 0644 "$SOURCE_DIR/policy.example.json" "$INSTALL_ROOT/policy.json"
|
||
|
|
install -o root -g root -m 0644 "$SOURCE_DIR/README.md" "$INSTALL_ROOT/README.md"
|
||
|
|
|
||
|
|
if [[ -d "$SOURCE_DIR/contracts" ]]; then
|
||
|
|
find "$SOURCE_DIR/contracts" -maxdepth 1 -type f -print0 | while IFS= read -r -d '' file; do
|
||
|
|
install -o root -g root -m 0444 "$file" "$INSTALL_ROOT/contracts/$(basename "$file")"
|
||
|
|
done
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Install current Fifth Domain brains and Zhulan entry as read-only runtime
|
||
|
|
# contracts from the same exact repository checkout as this installer.
|
||
|
|
REPO_ROOT="$(cd "$SOURCE_DIR/../.." && pwd)"
|
||
|
|
SOURCE_COMMIT="${ZHULAN_SOURCE_COMMIT:-}"
|
||
|
|
if [[ -z "$SOURCE_COMMIT" ]] && git -C "$REPO_ROOT" rev-parse HEAD >/dev/null 2>&1; then
|
||
|
|
SOURCE_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)"
|
||
|
|
fi
|
||
|
|
if [[ ! "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
|
||
|
|
echo "SOURCE_COMMIT_REQUIRED" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
for relative in \
|
||
|
|
"skills/shared/guanghu-ui-brain/SKILL.md" \
|
||
|
|
"skills/shared/guanghu-ui-brain/BRAIN.hdlp" \
|
||
|
|
"skills/shared/guanghu-client-ui-brain/SKILL.md" \
|
||
|
|
"skills/shared/guanghu-client-ui-brain/BRAIN.hdlp" \
|
||
|
|
"光之湖/ICE-GL-ZL-001-铸澜/WAKE.hdlp" \
|
||
|
|
"光之湖/ICE-GL-ZL-001-铸澜/INDEX.hdlp" \
|
||
|
|
"光之湖/ICE-GL-ZL-001-铸澜/CURRENT.hdlp" \
|
||
|
|
"光之湖/ICE-GL-ZL-001-铸澜/PERMANENT-MEMORY-WRITE-PROTOCOL.hdlp"; do
|
||
|
|
source_file="$REPO_ROOT/$relative"
|
||
|
|
if [[ ! -f "$source_file" ]]; then
|
||
|
|
echo "CONTRACT_SOURCE_MISSING $relative" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
safe_name="$(printf '%s' "$relative" | tr '/ ' '__')"
|
||
|
|
install -o root -g root -m 0444 "$source_file" "$INSTALL_ROOT/contracts/$safe_name"
|
||
|
|
done
|
||
|
|
|
||
|
|
printf '%s\n' "$SOURCE_COMMIT" >"$INSTALL_ROOT/contracts/REPOSITORY-SOURCE-SHA"
|
||
|
|
chmod 0444 "$INSTALL_ROOT/contracts/REPOSITORY-SOURCE-SHA"
|
||
|
|
|
||
|
|
SECRET_FILE="$SECRET_ROOT/zhulan-remote-cell.secret"
|
||
|
|
if [[ ! -f "$SECRET_FILE" ]]; then
|
||
|
|
umask 0177
|
||
|
|
openssl rand -hex 48 >"$SECRET_FILE"
|
||
|
|
fi
|
||
|
|
chown root:zhulan-runtime "$SECRET_FILE"
|
||
|
|
chmod 0640 "$SECRET_FILE"
|
||
|
|
|
||
|
|
install -o root -g zhulan-runtime -m 0640 "$SOURCE_DIR/deploy/zhulan-remote-cell.env.example" "$ENV_FILE"
|
||
|
|
install -o root -g root -m 0644 "$SOURCE_DIR/deploy/zhulan-remote-cell.service" "$SERVICE_FILE"
|
||
|
|
install -o root -g root -m 0644 "$SOURCE_DIR/deploy/zhulan-validation-executor.service" "$EXECUTOR_SERVICE_FILE"
|
||
|
|
install -o root -g root -m 0644 "$SOURCE_DIR/deploy/zhulan-bwrap.apparmor" "$APPARMOR_FILE"
|
||
|
|
apparmor_parser -r "$APPARMOR_FILE"
|
||
|
|
|
||
|
|
systemctl daemon-reload
|
||
|
|
systemctl enable zhulan-validation-executor.service
|
||
|
|
systemctl restart zhulan-validation-executor.service
|
||
|
|
for _ in $(seq 1 30); do
|
||
|
|
if [[ -S /run/guanghu/zhulan-validation/executor.sock ]]; then
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
sleep 1
|
||
|
|
done
|
||
|
|
if [[ ! -S /run/guanghu/zhulan-validation/executor.sock ]]; then
|
||
|
|
systemctl status zhulan-validation-executor.service --no-pager >&2 || true
|
||
|
|
echo "VALIDATION_EXECUTOR_HEALTH_FAILED" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
systemctl enable zhulan-remote-cell.service
|
||
|
|
systemctl restart zhulan-remote-cell.service
|
||
|
|
|
||
|
|
for _ in $(seq 1 30); do
|
||
|
|
if curl --fail --silent --show-error --max-time 2 http://127.0.0.1:17631/health >"$STATE_ROOT/health.json.tmp"; then
|
||
|
|
mv "$STATE_ROOT/health.json.tmp" "$STATE_ROOT/health.json"
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
sleep 1
|
||
|
|
done
|
||
|
|
|
||
|
|
if [[ ! -s "$STATE_ROOT/health.json" ]]; then
|
||
|
|
systemctl status zhulan-remote-cell.service --no-pager >&2 || true
|
||
|
|
echo "RUNTIME_HEALTH_FAILED" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
SOURCE_SHA256="$(find \
|
||
|
|
"$INSTALL_ROOT/runtime" \
|
||
|
|
"$INSTALL_ROOT/ui" \
|
||
|
|
"$INSTALL_ROOT/contracts" \
|
||
|
|
"$INSTALL_ROOT/policy.json" \
|
||
|
|
"$INSTALL_ROOT/README.md" \
|
||
|
|
-type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')"
|
||
|
|
cat >"$STATE_ROOT/install-receipt.json.tmp" <<EOF
|
||
|
|
{
|
||
|
|
"schema": "guanghu.zhulan-runtime-install-receipt/v1",
|
||
|
|
"development_id": "DEV-20260813-005",
|
||
|
|
"runtime_node": "BS-SG-003",
|
||
|
|
"front_node_role": "BS-GZ-006_PROXY_ONLY",
|
||
|
|
"installed_at": "$STAMP",
|
||
|
|
"source_tree_hash": "$SOURCE_SHA256",
|
||
|
|
"source_commit": "$SOURCE_COMMIT",
|
||
|
|
"service": "zhulan-remote-cell.service",
|
||
|
|
"validation_executor": "zhulan-validation-executor.service",
|
||
|
|
"listener": "127.0.0.1:17631",
|
||
|
|
"health": "PASS"
|
||
|
|
}
|
||
|
|
EOF
|
||
|
|
chown zhulan-runtime:zhulan-runtime "$STATE_ROOT/install-receipt.json.tmp"
|
||
|
|
chmod 0600 "$STATE_ROOT/install-receipt.json.tmp"
|
||
|
|
mv "$STATE_ROOT/install-receipt.json.tmp" "$STATE_ROOT/install-receipt.json"
|
||
|
|
|
||
|
|
cat "$STATE_ROOT/install-receipt.json"
|