Files
the-nexus/mempalace/export_closets.sh
Claude (Opus 4.6) e957254b65
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
[claude] MemPalace × Evennia fleet memory scaffold (#1075) (#1088)
2026-04-07 14:12:38 +00:00

105 lines
3.6 KiB
Bash
Executable File

#!/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/"