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

622 lines
19 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"
".agents/hooks.json"
".agents/hooks/loop_delegation_guard.sh"
)
for f in "${core_files[@]}"; do
if [ ! -f "$dir/$f" ]; then
return 1
fi
done
return 0
}
# 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
OWNERSHIP_LIB="$STAGE_DIR/deploy/lib_ownership.sh"
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/*' 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 [ ! -f "$OWNERSHIP_LIB" ]; then
echo "❌ Error: missing $OWNERSHIP_LIB; refusing to install with unknown ownership rules." >&2
exit 1
fi
# shellcheck source=deploy/lib_ownership.sh
. "$OWNERSHIP_LIB"
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)
FRAMEWORK_LEDGER=".mam/.framework_ledger.tmp"
: > "$FRAMEWORK_LEDGER"
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
STAGING_FILE="$STAGE_DIR/.agents/$rel"
printf '%s\t%s\n' "$STAGING_FILE" "$dest" >> "$FRAMEWORK_LEDGER"
IS_REGISTRY=0
if is_registry_file "$dest"; then IS_REGISTRY=1; fi
ACTION=$(python3 - "$dest" "$STAGING_FILE" ".mam/asset_hashes.txt" "$OVERWRITE_CUSTOM" "$IS_REGISTRY" <<'PY'
import sys, hashlib, os
target_path, staging_path, hash_db_path, force_overwrite, is_registry = (
sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1", sys.argv[5] == "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")
elif is_registry:
print("MERGE_REGISTRY")
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
;;
MERGE_REGISTRY)
BACKUP_DIR=".mam/skill-backups/$TS/$(dirname "$dest")"
mkdir -p "$BACKUP_DIR"
cp "$dest" "$BACKUP_DIR/"
MERGE_MSG=$(python3 - "$dest" "$STAGING_FILE" ".mam/base/$dest" <<'MERGEREG'
import json, os, sys
target_path, upstream_path, base_path = sys.argv[1], sys.argv[2], sys.argv[3]
def load(path):
if not os.path.exists(path):
return None
try:
with open(path, encoding="utf-8") as f:
d = json.load(f)
return d if isinstance(d, dict) else None
except Exception:
return None
target, upstream, base = load(target_path), load(upstream_path), load(base_path)
if target is None or upstream is None:
print("unmergeable (not a JSON object) -- kept your file untouched")
sys.exit(0)
merged = dict(target)
added, updated, conflicts, retired = [], [], [], []
if base is None:
for k, v in upstream.items():
if k not in merged:
merged[k] = v
added.append(k)
else:
for k, v in upstream.items():
if k not in base:
if k not in merged:
merged[k] = v
added.append(k)
elif merged[k] != v:
conflicts.append(k)
elif k not in merged:
added.append(k)
merged[k] = v
elif merged[k] == base[k]:
if merged[k] != v:
merged[k] = v
updated.append(k)
elif merged[k] != v:
conflicts.append(k)
for k in base:
if k not in upstream and k in merged and merged[k] == base[k]:
del merged[k]
retired.append(k)
if merged != target:
tmp = target_path + ".merge.tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(merged, f, indent=2)
f.write("\n")
os.replace(tmp, target_path)
bits = []
for label, keys in (("added", added), ("updated", updated),
("retired", retired), ("kept yours", conflicts)):
if keys:
bits.append("%s %s" % (label, ", ".join(sorted(keys))))
print("; ".join(bits) if bits else "already in sync")
MERGEREG
) || MERGE_MSG="merge failed -- kept your file untouched"
echo "MERGED:$dest:$MERGE_MSG"
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
elif [[ "$line" == MERGED:* ]]; then
_mam_rest="${line#MERGED:}"
echo "🔀 Registry merged: ${_mam_rest%%:*} (${_mam_rest#*:})" >&2
fi
done
# 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" ]; then
mkdir -p .agents
cp -f "$STAGE_DIR/deploy/INSTALL.md" .agents/INSTALL.md
printf '%s\t%s\n' "$STAGE_DIR/deploy/INSTALL.md" ".agents/INSTALL.md" \
>> "$FRAMEWORK_LEDGER"
if ! grep -Fqx ".agents/INSTALL.md" "$MANIFEST_FILE" 2>/dev/null; then
echo ".agents/INSTALL.md" >> "$MANIFEST_FILE"
fi
fi
# Re-build asset_hashes.txt from the UPSTREAM copy of every framework-owned file
python3 - .mam/asset_hashes.txt "$FRAMEWORK_LEDGER" <<'HASHDB'
import os, hashlib, sys
hash_db_path, ledger_path = sys.argv[1], sys.argv[2]
def sha(path):
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest()
hashes = {}
with open(ledger_path) as f:
for line in f:
line = line.rstrip("\n")
if "\t" not in line:
continue
upstream, dest = line.split("\t", 1)
if os.path.exists(upstream):
hashes[dest] = sha(upstream)
with open(hash_db_path, "w") as f:
f.writelines(sorted("%s %s\n" % (h, d) for d, h in hashes.items()))
HASHDB
# Snapshot registry base from upstream
while IFS="$(printf '\t')" read -r _up _dest; do
[ -n "${_dest:-}" ] || continue
is_registry_file "$_dest" || continue
[ -f "$_up" ] || continue
mkdir -p ".mam/base/$(dirname "$_dest")"
cp -f "$_up" ".mam/base/$_dest"
done < "$FRAMEWORK_LEDGER"
rm -f "$FRAMEWORK_LEDGER"
# 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."
# --- 3. Python Virtual Environment (.venv) Setup ---
if [ "${MAM_SKIP_VENV:-0}" -eq 1 ]; then
echo "⏩ Skipping virtualenv setup (MAM_SKIP_VENV=1)."
else
echo "🐍 Bootstrapping Python virtual environment ($VENV_NAME)..."
if [ ! -d "$VENV_NAME" ]; then
python3 -m venv "$VENV_NAME"
echo "✅ Created virtualenv at $VENV_NAME."
else
echo "️ Virtualenv ($VENV_NAME) already exists. Reusing."
fi
echo "📦 Installing/upgrading core dependencies inside $VENV_NAME..."
# shellcheck disable=SC1091
source "$VENV_NAME/bin/activate"
python -m pip install --upgrade pip -q
REQ_FILE=".agents/skills/multi-agent-mux-delegate-job/requirements.txt"
if [ -f "$REQ_FILE" ]; then
pip install -r "$REQ_FILE" -q
echo "✅ Installed dependencies from $REQ_FILE."
else
pip install "paho-mqtt>=2.0.0" pyyaml -q
echo "✅ Installed default fallback dependencies (paho-mqtt, pyyaml)."
fi
deactivate
fi
# --- 4. Environment Configuration (.mam.env) ---
echo "⚙️ Configuring environment variables..."
MAM_ENV=".mam.env"
LEGACY_ENV=".env"
if [ -f "$LEGACY_ENV" ] && [ ! -f "$MAM_ENV" ]; then
echo "📦 Migrating existing $LEGACY_ENV to $MAM_ENV..."
cp "$LEGACY_ENV" "$MAM_ENV"
fi
if [ ! -f "$MAM_ENV" ]; then
echo "📝 Initializing $MAM_ENV with default orchestration configuration..."
MAM_CLIENT_PREFIX="mam-agent"
MAM_PORT="1883"
cat <<EOF > "$MAM_ENV"
# ==============================================================================
# Multi-Agent Mux (MAM) Environment Configuration
# ==============================================================================
# Generated by install.sh at $(date -u +'%Y-%m-%dT%H:%M:%SZ')
MQTT_BROKER=localhost
MQTT_PORT=$MAM_PORT
MQTT_CLIENT_ID_PREFIX=$MAM_CLIENT_PREFIX
MQTT_RETRY_INTERVAL=2
MQTT_MAX_RETRIES=5
HERDR_SERVER_NAME=multi-agent-mux
HERDR_SOCKET_PATH=$HOME/.config/herdr/herdr.sock
MAM_LOG_LEVEL=INFO
MAM_EVENT_RETENTION_DAYS=7
EOF
echo "$MAM_ENV" >> "$MANIFEST_FILE"
echo "✅ Initialized $MAM_ENV."
else
echo "$MAM_ENV already exists. Preserving existing settings."
fi
# Ensure gitignore handles MAM isolation files (R-4: managed block)
GITIGNORE=".gitignore"
MAM_GI_START="# >>> MAM managed block (managed by install.sh — do not edit) >>>"
MAM_GI_END="# <<< MAM managed block <<<"
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
if [ "${MAM_SKIP_GITIGNORE:-0}" -eq 0 ]; then
python3 -c '
import os
if os.path.exists(".mam/install_state"):
with open(".mam/install_state", "a") as f:
f.write("gitignore_created=1\n")
else:
os.makedirs(".mam", exist_ok=True)
with open(".mam/install_state", "w") as f:
f.write("gitignore_created=1\n")
'
fi
echo "===================================================================="
echo "🎉 Multi-Agent Mux (MAM) Installation Completed Successfully!"
echo "===================================================================="
cat <<EOF
--------------------------------------------------------------------------------
💡 Next Steps:
1. Initialize a new isolated session:
$ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \\
--workspace "$TARGET_DIR" --agent claude --role developer --isolate
2. Monitor live agent states:
$ bash .agents/skills/multi-agent-mux-status/scripts/status.sh
3. Update installed skills in the future:
$ bash .mam_deploy/update.sh
--------------------------------------------------------------------------------
EOF