#!/usr/bin/env bash # export_closets.sh — Privacy-safe export of wizard closets for fleet sync. # # Exports ONLY closet (summary) files from a wizard's local MemPalace to # a bundle directory suitable for rsync to the shared Alpha fleet palace. # # POLICY: Raw drawers (full-text source content) NEVER leave the local VPS. # Only closets (compressed summaries) are exported. # # Usage: # ./mempalace/export_closets.sh [palace_dir] [export_dir] # # Defaults: # palace_dir — $MEMPALACE_DIR or /root/wizards/bezalel/.mempalace/palace # export_dir — /tmp/mempalace_export_closets # # After export, sync with: # rsync -avz --delete /tmp/mempalace_export_closets/ alpha:/var/lib/mempalace/fleet/bezalel/ # # Refs: #1083, #1075 set -euo pipefail PALACE_DIR="${1:-${MEMPALACE_DIR:-/root/wizards/bezalel/.mempalace/palace}}" EXPORT_DIR="${2:-/tmp/mempalace_export_closets}" WIZARD="${MEMPALACE_WING:-bezalel}" echo "[export_closets] Wizard: $WIZARD" echo "[export_closets] Palace: $PALACE_DIR" echo "[export_closets] Export: $EXPORT_DIR" if [[ ! -d "$PALACE_DIR" ]]; then echo "[export_closets] ERROR: palace not found: $PALACE_DIR" >&2 exit 1 fi # Validate closets-only policy: abort if any raw drawer files are present in export scope. # Closets are files named *.closet.json or stored under a closets/ subdirectory. # Raw drawers are everything else (*.drawer.json, *.md source files, etc.). DRAWER_COUNT=0 while IFS= read -r -d '' f; do # Raw drawer check: any .json file that is NOT a closet basename_f="$(basename "$f")" if [[ "$basename_f" == *.drawer.json ]]; then echo "[export_closets] POLICY VIOLATION: raw drawer found in export scope: $f" >&2 DRAWER_COUNT=$((DRAWER_COUNT + 1)) fi done < <(find "$PALACE_DIR" -type f -name "*.json" -print0 2>/dev/null) if [[ "$DRAWER_COUNT" -gt 0 ]]; then echo "[export_closets] ABORT: $DRAWER_COUNT raw drawer(s) detected. Only closets may be exported." >&2 echo "[export_closets] Run mempalace compress to generate closets before exporting." >&2 exit 1 fi # Also check for source_file metadata in closet JSON that would expose private paths. SOURCE_FILE_LEAKS=0 while IFS= read -r -d '' f; do if python3 -c " import json, sys try: data = json.load(open('$f')) drawers = data.get('drawers', []) if isinstance(data, dict) else [] for d in drawers: if 'source_file' in d and not d.get('closet', False): sys.exit(1) except Exception: pass sys.exit(0) " 2>/dev/null; then : else echo "[export_closets] POLICY VIOLATION: source_file metadata in non-closet: $f" >&2 SOURCE_FILE_LEAKS=$((SOURCE_FILE_LEAKS + 1)) fi done < <(find "$PALACE_DIR" -type f -name "*.closet.json" -print0 2>/dev/null) if [[ "$SOURCE_FILE_LEAKS" -gt 0 ]]; then echo "[export_closets] ABORT: $SOURCE_FILE_LEAKS file(s) contain private source_file paths." >&2 exit 1 fi # Collect closet files mkdir -p "$EXPORT_DIR/$WIZARD" CLOSET_COUNT=0 while IFS= read -r -d '' f; do rel_path="${f#$PALACE_DIR/}" dest="$EXPORT_DIR/$WIZARD/$rel_path" mkdir -p "$(dirname "$dest")" cp "$f" "$dest" CLOSET_COUNT=$((CLOSET_COUNT + 1)) done < <(find "$PALACE_DIR" -type f -name "*.closet.json" -print0 2>/dev/null) if [[ "$CLOSET_COUNT" -eq 0 ]]; then echo "[export_closets] WARNING: no closet files found in $PALACE_DIR" >&2 echo "[export_closets] Run 'mempalace compress' to generate closets from drawers." >&2 exit 0 fi echo "[export_closets] Exported $CLOSET_COUNT closet(s) to $EXPORT_DIR/$WIZARD/" echo "[export_closets] OK — ready for fleet sync." echo "" echo " rsync -avz --delete $EXPORT_DIR/$WIZARD/ alpha:/var/lib/mempalace/fleet/$WIZARD/"