#!/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 <&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 < .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 <> "$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 "===================================================================="