Files
multi-agent-mux/deploy/install.sh
T

544 lines
16 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ==============================================================================
# install.sh — Multi-Agent Mux (MAM) Orchestration Installer (Rev.2)
# ==============================================================================
set -euo pipefail
# --- Configuration & Defaults ---
TARGET_DIR=""
REFRESH="${MAM_REFRESH:-1}"
OVERWRITE_CUSTOM="${MAM_OVERWRITE_CUSTOM:-0}"
VENV_NAME=".venv"
MIN_PYTHON_VERSION="3.9"
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--force|--refresh-skills)
REFRESH=1
shift
;;
--no-refresh|--offline)
REFRESH=0
shift
;;
--overwrite-custom)
OVERWRITE_CUSTOM=1
shift
;;
-h|--help)
cat <<EOF
Usage: $0 [options] [target_dir]
Options:
-f, --force, --refresh-skills Fetch and refresh skills (now default)
--no-refresh, --offline Skip fetching latest assets if assets exist
--overwrite-custom Force overwrite user-modified skill files (backup still created)
-h, --help Show this help message
EOF
exit 0
;;
*)
if [ -z "$TARGET_DIR" ]; then
TARGET_DIR="$1"
else
echo "❌ Error: Unknown argument: $1" >&2
exit 1
fi
shift
;;
esac
done
if [ -z "$TARGET_DIR" ]; then
TARGET_DIR="$(pwd)"
fi
echo "===================================================================="
echo "⚡ Starting Multi-Agent Mux (MAM) Installation"
echo "📂 Target Workspace: $TARGET_DIR"
echo "===================================================================="
# --- 1. System Requirements Validation ---
echo "🔍 Checking system dependencies..."
check_cmd() {
local cmd="$1"
if ! command -v "$cmd" &>/dev/null; then
echo "❌ Error: '$cmd' is not installed. Please install it first." >&2
exit 1
fi
}
check_cmd herdr
check_cmd python3
PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
PYTHON_MAJOR="${MIN_PYTHON_VERSION%%.*}"
PYTHON_MINOR="${MIN_PYTHON_VERSION##*.}"
if python3 -c "import sys; exit(0 if sys.version_info >= ($PYTHON_MAJOR, $PYTHON_MINOR) else 1)"; then
echo "✅ Python $PYTHON_VERSION detected."
else
echo "❌ Error: Python version must be $MIN_PYTHON_VERSION or higher. Detected: $PYTHON_VERSION" >&2
exit 1
fi
if ! python3 -c "import yaml" &>/dev/null; then
echo "❌ Error: 'PyYAML' is not installed in system python3." >&2
exit 1
fi
echo "✅ PyYAML (system dependency) detected."
# --- 2. Workspace Setup ---
mkdir -p "$TARGET_DIR"
cd "$TARGET_DIR"
REPO_URL="${MAM_REPO_URL:-https://git.godopu.com/tmpl/multi-agent-mux.git}"
ARCHIVE_URL="${MAM_ARCHIVE_URL:-https://git.godopu.com/tmpl/multi-agent-mux/archive/main.tar.gz}"
check_assets_present() {
local dir="${1:-.}"
local core_files=(
".agents/skills/lib.sh"
".agents/skills/multi-agent-mux-create/scripts/create_session.sh"
".agents/skills/multi-agent-mux-delegate-job/scripts/registry.py"
".agents/skills/multi-agent-mux-status/scripts/status.sh"
".agents/skills/multi-agent-mux-loop/scripts/run_loop.sh"
)
for f in "${core_files[@]}"; do
if [ ! -f "$dir/$f" ]; then
return 1
fi
done
return 0
}
is_framework_owned() {
case "$1" in
.agents/skills/*) return 0 ;;
*) return 1 ;;
esac
}
# Fetch orchestration assets if REFRESH=1 or if core assets are missing.
if [ "$REFRESH" -eq 1 ] || [ "${MAM_SKIP_VENV:-0}" -eq 1 ] || ! check_assets_present "."; then
echo "📥 Staging orchestration assets from Gitea repository..."
STAGE_DIR="$(mktemp -d)"
trap 'rm -rf "$STAGE_DIR"' EXIT
FETCH_METHOD="archive"
if [ -d "$REPO_URL" ]; then
echo "🌐 Copying local working tree into a staging area..."
cp -R "$REPO_URL/." "$STAGE_DIR/"
FETCH_METHOD="local"
elif command -v git &>/dev/null; then
echo "🌐 Cloning repository (shallow) into a staging area..."
git clone --depth 1 "$REPO_URL" "$STAGE_DIR"
FETCH_METHOD="git"
elif command -v curl &>/dev/null; then
echo "🌐 Downloading and extracting archive into a staging area..."
curl -fsSL "$ARCHIVE_URL" | tar -xz --strip-components=1 -C "$STAGE_DIR" \
--exclude='*/.agents/reports/*' --exclude='*/.agents/references/*' \
--exclude='*/MESSAGING.md' --exclude='*/BOOTSTRAP.md' --exclude='*/BOOTSTRAP.ko.md' 2>/dev/null || true
FETCH_METHOD="archive"
else
echo "❌ Error: neither 'git' nor 'curl' is available to fetch the skills." >&2
exit 1
fi
if ! check_assets_present "$STAGE_DIR"; then
echo "❌ Error: fetched source is missing core runtime assets. Aborting." >&2
exit 1
fi
mkdir -p .mam
MANIFEST_FILE=".mam/install_manifest.txt"
touch "$MANIFEST_FILE"
# Migrate legacy root layout for remove.sh and update.sh into .mam_deploy/ if owned
mkdir -p .mam_deploy
for legacy_script in remove.sh update.sh; do
if [ -f "$legacy_script" ] && grep -Fqx "$legacy_script" "$MANIFEST_FILE" 2>/dev/null; then
mv -f "$legacy_script" ".mam_deploy/$legacy_script"
python3 -c '
import sys
path = sys.argv[1]
old_s = sys.argv[2]
new_s = sys.argv[3]
with open(path, "r") as f:
lines = f.readlines()
with open(path, "w") as f:
for line in lines:
if line.strip() == old_s:
f.write(new_s + "\n")
else:
f.write(line)
' "$MANIFEST_FILE" "$legacy_script" ".mam_deploy/$legacy_script" 2>/dev/null || true
fi
done
# Safe refresh & fingerprint checking logic
TS=$(date -u +%Y%m%dT%H%M%SZ)
PRESERVED_COUNT=0
MODIFIED_FILES=()
mkdir -p .agents
( cd "$STAGE_DIR/.agents" && find . -type f -print ) | while IFS= read -r rel; do
case "$rel" in
./reports/*|./references/*) continue ;;
*.tmp|*.log|*.pyc|*/__pycache__/*) continue ;;
esac
dest=".agents/${rel#./}"
mkdir -p "$(dirname "$dest")"
if is_framework_owned "$dest"; then
# 3-way check using python3 inline
STAGING_FILE="$STAGE_DIR/.agents/$rel"
ACTION=$(python3 - "$dest" "$STAGING_FILE" ".mam/asset_hashes.txt" "$OVERWRITE_CUSTOM" <<'PY'
import sys, hashlib, os
target_path, staging_path, hash_db_path, force_overwrite = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1"
def file_sha(path):
if not os.path.exists(path):
return None
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest()
target_sha = file_sha(target_path)
staging_sha = file_sha(staging_path)
if target_sha is None:
print("COPY_NEW")
sys.exit(0)
if target_sha == staging_sha:
print("NO_OP")
sys.exit(0)
db_sha = None
if os.path.exists(hash_db_path):
with open(hash_db_path, "r") as f:
for line in f:
parts = line.strip().split(None, 1)
if len(parts) == 2 and parts[1] == target_path:
db_sha = parts[0]
break
if db_sha is None:
print("BOOTSTRAP_OVERWRITE")
elif target_sha == db_sha:
print("UPDATE_UNMODIFIED")
else:
if force_overwrite:
print("FORCE_OVERWRITE_CUSTOM")
else:
print("PRESERVE_CUSTOM")
PY
)
case "$ACTION" in
COPY_NEW|UPDATE_UNMODIFIED|BOOTSTRAP_OVERWRITE|FORCE_OVERWRITE_CUSTOM)
if [ "$ACTION" = "BOOTSTRAP_OVERWRITE" ] || [ "$ACTION" = "FORCE_OVERWRITE_CUSTOM" ]; then
BACKUP_DIR=".mam/skill-backups/$TS/$(dirname "$dest")"
mkdir -p "$BACKUP_DIR"
cp "$dest" "$BACKUP_DIR/"
fi
cp -f "$STAGING_FILE" "$dest"
if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then
echo "$dest" >> "$MANIFEST_FILE"
fi
;;
PRESERVE_CUSTOM)
BACKUP_DIR=".mam/skill-backups/$TS/$(dirname "$dest")"
mkdir -p "$BACKUP_DIR"
cp "$dest" "$BACKUP_DIR/"
echo "PRESERVED:$dest"
;;
NO_OP)
if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then
echo "$dest" >> "$MANIFEST_FILE"
fi
;;
esac
elif [ ! -e "$dest" ]; then
cp "$STAGE_DIR/.agents/$rel" "$dest"
if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then
echo "$dest" >> "$MANIFEST_FILE"
fi
fi
done | while IFS= read -r line; do
if [[ "$line" == PRESERVED:* ]]; then
echo "️ Local modification detected: ${line#PRESERVED:}" >&2
fi
done
# Re-build asset_hashes.txt for all framework owned files
python3 - .mam/asset_hashes.txt <<'PY'
import os, hashlib, sys
hash_db_path = sys.argv[1]
hashes = []
for root, _, files in os.walk(".agents/skills"):
for file in files:
path = os.path.join(root, file)
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
hashes.append(f"{h.hexdigest()} {path}\n")
with open(hash_db_path, "w") as f:
f.writelines(sorted(hashes))
PY
# Copy root docs (R-2 essential set)
ROOT_DOCS="AGENTS.md"
if [ "${MAM_INSTALL_DOCS:-minimal}" = "full" ]; then
ROOT_DOCS="AGENTS.md MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md"
fi
for doc in $ROOT_DOCS; do
if [ -f "$STAGE_DIR/$doc" ] && [ ! -e "$doc" ]; then
cp "$STAGE_DIR/$doc" .
echo "$doc" >> "$MANIFEST_FILE"
fi
done
# Install remove.sh and update.sh into .mam_deploy/
mkdir -p .mam_deploy
if [ -f "$STAGE_DIR/deploy/remove.sh" ]; then
cp "$STAGE_DIR/deploy/remove.sh" .mam_deploy/remove.sh
chmod 0755 .mam_deploy/remove.sh
if ! grep -Fqx ".mam_deploy/remove.sh" "$MANIFEST_FILE" 2>/dev/null; then
echo ".mam_deploy/remove.sh" >> "$MANIFEST_FILE"
fi
fi
if [ -f "$STAGE_DIR/deploy/update.sh" ]; then
cp "$STAGE_DIR/deploy/update.sh" .mam_deploy/update.sh
chmod 0755 .mam_deploy/update.sh
if ! grep -Fqx ".mam_deploy/update.sh" "$MANIFEST_FILE" 2>/dev/null; then
echo ".mam_deploy/update.sh" >> "$MANIFEST_FILE"
fi
fi
if [ -f "$STAGE_DIR/.mam.env.example" ] && [ ! -e ".mam.env.example" ]; then
cp "$STAGE_DIR/.mam.env.example" .
echo ".mam.env.example" >> "$MANIFEST_FILE"
fi
if [ -f "$STAGE_DIR/deploy/INSTALL.md" ] && [ ! -e ".agents/INSTALL.md" ]; then
mkdir -p .agents
cp "$STAGE_DIR/deploy/INSTALL.md" .agents/INSTALL.md
echo ".agents/INSTALL.md" >> "$MANIFEST_FILE"
fi
# Record version stamp
STAGE_COMMIT="unknown"
if [ -d "$STAGE_DIR/.git" ]; then
STAGE_COMMIT=$(git -C "$STAGE_DIR" rev-parse HEAD 2>/dev/null || echo "unknown")
fi
cat <<EOF > .mam/version.txt
source=$REPO_URL
commit=$STAGE_COMMIT
fetched_at=$TS
method=$FETCH_METHOD
EOF
rm -rf "$STAGE_DIR"
trap - EXIT
echo "✅ Skills staged into workspace (user documents and custom configs preserved)."
fi
if ! check_assets_present "."; then
echo "❌ Error: Core runtime assets missing after setup." >&2
exit 1
fi
echo "✅ Orchestration skills present."
echo "📂 Ensuring metadata directory structure (.mam/)..."
mkdir -p .mam/jobs .mam/delegate_job_logs
if [ -O .mam ]; then
chmod 0700 .mam
fi
# R-4: Manage .gitignore block (never put .gitignore in manifest)
MAM_GI_START="# >>> MAM managed block (managed by install.sh — do not edit) >>>"
MAM_GI_END="# <<< MAM managed block <<<"
if [ "${MAM_SKIP_GITIGNORE:-0}" != "1" ]; then
GI_CREATED=0
[ -e .gitignore ] || { touch .gitignore; GI_CREATED=1; }
if ! grep -q '^gitignore_created=' .mam/install_state 2>/dev/null; then
echo "gitignore_created=$GI_CREATED" >> .mam/install_state
fi
python3 - .gitignore "$MAM_GI_START" "$MAM_GI_END" <<'PY'
import sys, os
gi_path, start_marker, end_marker = sys.argv[1], sys.argv[2], sys.argv[3]
block_lines = [
start_marker + "\n",
"/.venv/\n",
"/.mam/\n",
"/.mam_deploy/\n",
"/.mam.env\n",
"/.mam.env.*\n",
"!/.mam.env.example\n",
"/.cache/multi-agent-mux-monitor/\n",
"/.mam-skill-backup.*/\n",
"CURRENT_JOB.md\n",
end_marker + "\n"
]
lines = []
if os.path.exists(gi_path):
with open(gi_path, "r") as f:
lines = f.readlines()
new_lines = []
in_block = False
block_inserted = False
for line in lines:
if line.strip() == start_marker:
in_block = True
if not block_inserted:
new_lines.extend(block_lines)
block_inserted = True
continue
if line.strip() == end_marker:
in_block = False
continue
if not in_block:
new_lines.append(line)
if not block_inserted:
if new_lines and not new_lines[-1].endswith("\n"):
new_lines[-1] += "\n"
new_lines.extend(block_lines)
with open(gi_path, "w") as f:
f.writelines(new_lines)
PY
fi
# --- 3. Check Network File System (NFS) Warnings ---
echo "💾 Detecting file system mount type..."
if command -v df &>/dev/null && command -v mount &>/dev/null; then
MOUNTPOINT="$(df --output=target . 2>/dev/null | tail -1 || echo "")"
if [ -n "$MOUNTPOINT" ]; then
if mount | grep -q "$MOUNTPOINT.*nfs\|$MOUNTPOINT.*cifs\|$MOUNTPOINT.*fuse.sshfs"; then
echo "⚠️ WARNING: Target directory is on a network filesystem."
else
echo "✅ File system supports WAL (Local storage detected)."
fi
fi
fi
# --- 4. Python Virtual Environment Setup ---
if [ "${MAM_SKIP_VENV:-0}" != "1" ]; then
echo "🐍 Bootstrapping Python virtual environment (.venv)..."
if [ ! -d "$VENV_NAME" ]; then
python3 -m venv "$VENV_NAME"
echo "✅ Virtual environment created."
else
echo "️ Virtual environment (.venv) already exists. Skipping creation."
fi
source "$VENV_NAME"/bin/activate
pip install --upgrade pip
REQ_FILE=".agents/skills/multi-agent-mux-delegate-job/requirements.txt"
if [ -f "$REQ_FILE" ]; then
echo "📦 Installing backplane dependencies from $REQ_FILE..."
pip install -r "$REQ_FILE"
echo "✅ Dependencies installed successfully."
else
pip install "paho-mqtt>=2.0.0" pyyaml
fi
fi
# --- 5. Generate Environment Template ---
ENV_FILE=".mam.env"
ENV_EXAMPLE=".mam.env.example"
migrate_legacy_env() {
local manifest=".mam/install_manifest.txt"
[ -f ".mam.env" ] && return 0
[ -f ".env" ] || return 0
if [ "${MAM_LEGACY_ENV_OWNED:-0}" = "1" ] || { [ -f "$manifest" ] && grep -Fqx ".env" "$manifest" 2>/dev/null; }; then
mv -f ".env" ".mam.env"
chmod 0600 ".mam.env" 2>/dev/null || true
if [ -f "$manifest" ]; then
if grep -Fqx ".env" "$manifest" 2>/dev/null; then
python3 -c '
import sys
path = sys.argv[1]
with open(path, "r") as f:
lines = f.readlines()
with open(path, "w") as f:
for line in lines:
if line.strip() == ".env":
f.write(".mam.env\n")
else:
f.write(line)
' "$manifest" 2>/dev/null || true
else
echo ".mam.env" >> "$manifest"
fi
fi
echo "️ Legacy MAM config migrated: .env -> .mam.env"
else
echo "️ Existing .env left untouched (ownership unproven)."
echo " MAM will read it via the deprecated fallback."
echo " To migrate explicitly: deploy/generate-env.sh --migrate-legacy"
fi
}
migrate_legacy_env
if [ ! -f "$ENV_FILE" ] && [ ! -f ".env" ] && [ ! -f ".env.update-tmp" ]; then
if [ -f "$ENV_EXAMPLE" ]; then
echo "📝 Creating configuration from $ENV_EXAMPLE..."
cp "$ENV_EXAMPLE" "$ENV_FILE"
else
echo "📝 Creating default $ENV_FILE..."
touch "$ENV_FILE"
fi
cat <<EOF >> "$ENV_FILE"
# === Installer-applied active defaults ===
MQTT_BROKER=broker.hivemq.com
MQTT_PORT=1883
MQTT_TLS=0
MQTT_CLIENT_ID_PREFIX=mam-agent
HERDR_SERVER_NAME=default
EOF
chmod 0600 "$ENV_FILE"
echo "✅ Config file .mam.env initialized with chmod 0600."
mkdir -p .mam
touch .mam/install_manifest.txt
echo "$ENV_FILE" >> .mam/install_manifest.txt
else
if [ -f "$ENV_FILE" ]; then
echo "$ENV_FILE already exists. Skipping config override."
else
echo "️ Legacy environment detected. Preserved without shadowing."
fi
fi
echo "===================================================================="
echo "🎉 Installation complete!"
echo "✨ You can now run the status or monitor skills."
echo "💡 Hint: Try executing: .venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py list"
echo "===================================================================="