From 5d69ad4f0b7fdafbf986abfdca849a23290ca81d Mon Sep 17 00:00:00 2001 From: Godopu Date: Wed, 24 Jun 2026 12:08:00 +0900 Subject: [PATCH] Harden remove.sh: fix fallback data-loss risk, prevent remove.sh clobbering, and ensure macOS compatibility --- deploy/install.sh | 14 ++++ deploy/remove.sh | 180 +++++++++++++++++++++++++++++++--------------- 2 files changed, 135 insertions(+), 59 deletions(-) diff --git a/deploy/install.sh b/deploy/install.sh index 4915c05..2371a0e 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -106,6 +106,11 @@ if ! check_assets_present "."; then exit 1 fi + # Create metadata directory and initialize manifest before copying + mkdir -p .mam + MANIFEST_FILE=".mam/install_manifest.txt" + touch "$MANIFEST_FILE" + # Copy ONLY runtime assets into the target, never overwriting an existing # target file. We merge per-file (POSIX find + an explicit "[ ! -e ]" guard) # instead of `cp -n`: `cp -n` is non-portable and now prints a deprecation @@ -117,6 +122,7 @@ if ! check_assets_present "."; then if [ ! -e "$dest" ]; then mkdir -p "$(dirname "$dest")" cp "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; } + echo "$dest" >> "$MANIFEST_FILE" fi done @@ -125,16 +131,19 @@ if ! check_assets_present "."; then for doc in AGENT.md AGENT.ko.md MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md INSTRUCTION.md; do if [ -f "$STAGE_DIR/$doc" ] && [ ! -e "$doc" ]; then cp "$STAGE_DIR/$doc" . || { echo "❌ Error: Failed to copy $doc" >&2; exit 1; } + echo "$doc" >> "$MANIFEST_FILE" fi done if [ -f "$STAGE_DIR/deploy/remove.sh" ] && [ ! -e "remove.sh" ]; then cp "$STAGE_DIR/deploy/remove.sh" remove.sh || { echo "❌ Error: Failed to copy remove.sh" >&2; exit 1; } chmod +x remove.sh + echo "remove.sh" >> "$MANIFEST_FILE" fi if [ -f "$STAGE_DIR/.env.example" ] && [ ! -e ".env.example" ]; then cp "$STAGE_DIR/.env.example" . || { echo "❌ Error: Failed to copy .env.example" >&2; exit 1; } + echo ".env.example" >> "$MANIFEST_FILE" fi rm -rf "$STAGE_DIR" @@ -225,6 +234,11 @@ TMUX_SERVER_NAME=default EOF chmod 0600 "$ENV_FILE" echo "✅ Config file .env initialized with chmod 0600." + + # Record the newly created .env in the manifest + mkdir -p .mam + touch .mam/install_manifest.txt + echo "$ENV_FILE" >> .mam/install_manifest.txt else echo "ℹ️ $ENV_FILE already exists. Skipping config override." fi diff --git a/deploy/remove.sh b/deploy/remove.sh index f52f254..c4bc6c4 100644 --- a/deploy/remove.sh +++ b/deploy/remove.sh @@ -3,12 +3,14 @@ # remove.sh — Multi-Agent Mux (MAM) Orchestration Uninstaller # ============================================================================== # Safely removes MAM orchestration skills, virtual environment, and metadata. -# Leaves pre-existing user configurations and files untouched. +# Leaves pre-existing user configurations and files untouched by reading +# the installation manifest (.mam/install_manifest.txt). # ============================================================================== set -euo pipefail TARGET_DIR="" FORCE=0 +PURGE_ENV=0 # Parse arguments while [[ $# -gt 0 ]]; do @@ -17,6 +19,10 @@ while [[ $# -gt 0 ]]; do FORCE=1 shift ;; + --purge-env) + PURGE_ENV=1 + shift + ;; *) TARGET_DIR="$1" shift @@ -40,33 +46,45 @@ fi cd "$TARGET_DIR" -# Check if there is anything to remove -assets_to_check=( - ".agents/skills/lib.sh" - ".agents/skills/multi-agent-mux-create" - ".agents/skills/multi-agent-mux-delegate-job" - ".agents/skills/multi-agent-mux-monitor" - ".agents/skills/multi-agent-mux-resume" - ".agents/skills/multi-agent-mux-status" - ".agents/skills/multi-agent-mux-stop" - ".venv" - ".mam" - "AGENT.md" - "AGENT.ko.md" - "MESSAGING.md" - "BOOTSTRAP.md" - "BOOTSTRAP.ko.md" - "INSTRUCTION.md" - ".env" -) +# 1. Non-interactive input safety guard (set -e read crash prevention) +if [ ! -t 0 ] && [ $FORCE -eq 0 ]; then + echo "❌ Error: Non-interactive terminal detected. Please run with -y/--yes/--force." >&2 + exit 1 +fi +# Check if there is anything to remove +MANIFEST_FILE=".mam/install_manifest.txt" any_exist=0 -for asset in "${assets_to_check[@]}"; do - if [ -e "$asset" ] || [ -h "$asset" ]; then - any_exist=1 - break - fi -done +manifest_files=() + +# Load the install manifest if it exists +if [ -f "$MANIFEST_FILE" ]; then + any_exist=1 + while IFS= read -r line; do + if [ -n "$line" ]; then + manifest_files+=("$line") + fi + done < "$MANIFEST_FILE" +else + # Fallback to the core MAM directories to check if any exist + fallback_assets=( + ".agents/skills/lib.sh" + ".agents/skills/multi-agent-mux-create" + ".agents/skills/multi-agent-mux-delegate-job" + ".agents/skills/multi-agent-mux-monitor" + ".agents/skills/multi-agent-mux-resume" + ".agents/skills/multi-agent-mux-status" + ".agents/skills/multi-agent-mux-stop" + ".venv" + ".mam" + ) + for asset in "${fallback_assets[@]}"; do + if [ -e "$asset" ] || [ -h "$asset" ]; then + any_exist=1 + break + fi + done +fi if [ $any_exist -eq 0 ]; then echo "ℹ️ No MAM assets detected in '$TARGET_DIR'. Nothing to do." @@ -78,7 +96,10 @@ if [ $FORCE -eq 0 ]; then echo "⚠️ WARNING: This will permanently remove the MAM orchestration skills, " echo " virtual environment (.venv), local metadata (.mam), and docs." echo " (Your own custom files inside .agents/ will NOT be touched)." - read -p "❓ Are you sure you want to proceed? [y/N]: " -r response + + if ! read -p "❓ Are you sure you want to proceed? [y/N]: " -r response; then + response="n" + fi if [[ ! "$response" =~ ^[yY](es)?$ ]]; then echo "❌ Uninstallation cancelled by user." exit 0 @@ -93,54 +114,95 @@ delete_asset() { fi } -# 1. Remove MAM skills -delete_asset ".agents/skills/lib.sh" -delete_asset ".agents/skills/multi-agent-mux-create" -delete_asset ".agents/skills/multi-agent-mux-delegate-job" -delete_asset ".agents/skills/multi-agent-mux-monitor" -delete_asset ".agents/skills/multi-agent-mux-resume" -delete_asset ".agents/skills/multi-agent-mux-status" -delete_asset ".agents/skills/multi-agent-mux-stop" +# 2. Uninstall files using the manifest if present +if [ ${#manifest_files[@]} -gt 0 ]; then + echo "📜 Manifest found. Reversing installer-created files..." + for f in ${manifest_files[@]+"${manifest_files[@]}"}; do + # Skip .env and remove.sh for now, they are handled separately + if [ "$f" = ".env" ] || [ "$f" = "remove.sh" ]; then + continue + fi + delete_asset "$f" + done + else + # Fallback: Delete MAM skills manually (only if manifest is missing) + echo "⚠️ No manifest found. Deleting standard MAM skills..." + delete_asset ".agents/skills/lib.sh" + delete_asset ".agents/skills/multi-agent-mux-create" + delete_asset ".agents/skills/multi-agent-mux-delegate-job" + delete_asset ".agents/skills/multi-agent-mux-monitor" + delete_asset ".agents/skills/multi-agent-mux-resume" + delete_asset ".agents/skills/multi-agent-mux-status" + delete_asset ".agents/skills/multi-agent-mux-stop" + fi -# Remove empty parent dirs under .agents to avoid littering -if [ -d ".agents/skills" ]; then - rmdir ".agents/skills" 2>/dev/null || true -fi +# 3. Clean up empty parent directories under .agents recursively to avoid littering if [ -d ".agents" ]; then - rmdir ".agents" 2>/dev/null || true + find .agents -depth -type d -exec rmdir {} + 2>/dev/null || true fi -# 2. Remove virtual environment & metadata +# 4. Remove virtual environment, monitor cache, and metadata database delete_asset ".venv" -delete_asset ".mam" +delete_asset ".cache/multi-agent-mux-monitor" +delete_asset ".mam" # Deletes manifest file too + +# 5. Clean up .env file (Only if created by installer, or forced with --purge-env) +# If .env is in manifest, it means MAM created it. +env_created_by_mam=0 +for f in ${manifest_files[@]+"${manifest_files[@]}"}; do + if [ "$f" = ".env" ]; then + env_created_by_mam=1 + break + fi +done -# 3. Remove configuration files (optionally keep or confirm .env) -# Since .env might contain custom keys, we confirm unless forced. if [ -f ".env" ]; then - if [ $FORCE -eq 1 ]; then + should_delete_env=0 + if [ $PURGE_ENV -eq 1 ]; then + should_delete_env=1 + elif [ $env_created_by_mam -eq 1 ]; then + # Even if MAM created it, ask or rename to backup to prevent loss of custom secrets + if [ $FORCE -eq 1 ]; then + should_delete_env=1 + else + if ! read -p "❓ MAM-created '.env' found. Delete it? (Saying No preserves it) [y/N]: " -r env_response; then + env_response="n" + fi + if [[ "$env_response" =~ ^[yY](es)?$ ]]; then + should_delete_env=1 + fi + fi + fi + + if [ $should_delete_env -eq 1 ]; then delete_asset ".env" else - read -p "❓ Do you want to remove '.env' configuration file? [y/N]: " -r response - if [[ "$response" =~ ^[yY](es)?$ ]]; then - delete_asset ".env" + if [ $env_created_by_mam -eq 1 ]; then + backup_name=".env.mam-backup" + if [ -e "$backup_name" ]; then + backup_name=".env.mam-backup.$(date +%Y%m%d%H%M%S)" + fi + echo "💾 Backing up .env configuration to $backup_name..." + mv ".env" "$backup_name" else - echo "ℹ️ Preserving .env configuration." + echo "ℹ️ Preserving user-owned .env configuration." fi fi fi -delete_asset ".env.example" -# 4. Remove runtime documentation -delete_asset "AGENT.md" -delete_asset "AGENT.ko.md" -delete_asset "MESSAGING.md" -delete_asset "BOOTSTRAP.md" -delete_asset "BOOTSTRAP.ko.md" -delete_asset "INSTRUCTION.md" +# 6. Remove uninstaller file itself (if we are in the target root) +# Simple check: only delete remove.sh if it is recorded in the manifest +remove_in_manifest=0 +for f in ${manifest_files[@]+"${manifest_files[@]}"}; do + if [ "$f" = "remove.sh" ]; then + remove_in_manifest=1 + break + fi +done -# Remove this uninstaller file itself (if we are in the target root) -if [ -f "remove.sh" ] && [ "$(realpath "remove.sh")" = "$(realpath "$0")" ]; then +if [ -f "remove.sh" ] && [ $remove_in_manifest -eq 1 ]; then echo "🗑️ Removing uninstaller: remove.sh" + # Self-delete is the final action rm -f "remove.sh" fi