Files
Kai 07dd648b5f feat: scenarios for curator/memo-inbox/pi-grok, deploy and backup tooling
Scenarios
- memo-inbox: mirrored by copying; the live directory was not moved or modified
  and the service was not restarted. All four tracked files match byte for byte
  (pi-diff.sh reports SAME). Marked deploy = "mirror" so deploy-scenario.sh
  refuses --apply: applying a mirror would invert the direction of truth and
  could change a service in daily use.
- curator: target configuration, not yet deployed. .pi/SYSTEM.md replaces pi's
  coding-assistant prompt; durable role text is in .pi/APPEND_SYSTEM.md;
  profile.toml is the single source of truth for the launch contract.
- pi-grok: registered only. It is genuinely a coding agent, so the isolation
  baseline does not apply in full.

Corrections to the documentation, found by testing rather than by reading
- AGENTS.override.md does NOT block parent-directory context files; it only
  shadows its own directory. Verified: with an override file in the workspace, a
  marker in /tmp/AGENTS.md still reached the system prompt. The only effective
  switch is --no-context-files, so durable role text must live in
  .pi/APPEND_SYSTEM.md, which is a system-prompt file and unaffected by -nc.
  Verified end state: no coding-assistant framing, no pi-docs block, own
  identity and role text present, no parent pollution, only own skills/tools.
- PI_CODING_AGENT_DIR isolates settings/models/auth/trust/extensions/skills/
  prompts/themes under the agent directory -- stronger than the --no-* flags
  because it also repoints credentials -- but does NOT cover ~/.agents/skills.
  Measured: find-skills, modsearch and summarize still leak. So it complements
  --no-skills rather than replacing it.
- --append-system-prompt accepts a file path, which pi-grok relies on.
- cwd is what anchors .pi discovery: a probe that forgot cwd silently lost
  .pi/SYSTEM.md and kept the coding-assistant persona.

Tooling (all dry-run by default; none of them restarts a service)
- pi-diff.sh: compares tracked config against the live install in both
  directions, with a key-redacted comparison for models.json
- deploy-scenario.sh: installs a workspace and renders profile.toml into
  .pi/launch.json, then checks that every referenced path exists
- deploy-runtime.sh: renders models.json from its template, refusing placeholder
  or missing keys. Verified byte-identical to the live file
- pi-backup.sh / pi-restore.sh: archives outside the repo, sha256 manifest
  verified before any restore, live paths preserved rather than overwritten

Fixed while testing: pi-backup.sh compared the destination against the repo root
literally, so a relative --dest ./backups wrote credential archives into the work
tree. Now canonicalised with realpath; ./backups, an absolute in-repo path and
./docs/../backups are all refused.
2026-08-26 23:17:12 -07:00

141 lines
5.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# Back up the live Pi installation and every scenario workspace.
#
# Archives are written OUTSIDE the repository by design. The precedent to avoid
# is hermes-agent-config, which committed hermes-secrets-*.tar.gz into its own
# working tree; had that tree ever been pushed, the credentials would have gone
# with it.
#
# Usage:
# scripts/pi-backup.sh # default destination
# scripts/pi-backup.sh --dest /path/to/dir
# scripts/pi-backup.sh --no-secrets # skip credential archives
# ---------------------------------------------------------------------------
# shellcheck source=lib/common.sh
. "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
DEST_ROOT="/mnt/truenas/multimedia/curator/backup/pi-agent-config"
WITH_SECRETS=1
while [ "$#" -gt 0 ]; do
case "$1" in
--dest) DEST_ROOT="${2:?--dest needs a path}"; shift 2 ;;
--no-secrets) WITH_SECRETS=0; shift ;;
-h|--help) sed -n '2,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "unknown argument: $1" ;;
esac
done
# Canonicalise before comparing: a relative path such as ./backups would
# otherwise slip past a literal prefix test and write credential archives into
# the working tree. (.gitignore would still stop them being committed, but the
# archives should not be there at all.)
DEST_ROOT="$(realpath -m -- "$DEST_ROOT")"
REPO_REAL="$(realpath -- "$REPO_ROOT")"
case "$DEST_ROOT" in
"$REPO_REAL"|"$REPO_REAL"/*)
die "refusing to write backups inside the repository
requested: $DEST_ROOT
repository: $REPO_REAL
Backups contain plaintext credentials and must live outside the work tree." ;;
esac
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
DEST="$DEST_ROOT/$STAMP"
install -d -m 700 "$DEST" || die "cannot create $DEST"
head1 "backup -> $DEST"
# --- 1. Pi agent directory, excluding sessions -----------------------------
if [ -d "$HOME/.pi/agent" ]; then
tar --exclude='agent/sessions' -czf "$DEST/pi-agent-runtime.tar.gz" -C "$HOME/.pi" agent
ok "pi-agent-runtime.tar.gz ${C_DIM}(contains plaintext apiKey)${C_OFF}"
chmod 600 "$DEST/pi-agent-runtime.tar.gz"
fi
# --- 2. Sessions, separately: bulky and lower value -----------------------
if [ -d "$HOME/.pi/agent/sessions" ]; then
tar -czf "$DEST/pi-global-sessions.tar.gz" -C "$HOME/.pi/agent" sessions
ok "pi-global-sessions.tar.gz"
fi
# --- 3. Each scenario workspace and session directory ---------------------
while IFS= read -r name; do
profile="$REPO_ROOT/scenarios/$name/profile.toml"
[ -f "$profile" ] || continue
workspace="$(toml_get "$profile" scenario workspace)"
session_dir="$(toml_get "$profile" scenario session_dir)"
if [ -n "$workspace" ] && [ -d "$workspace" ]; then
tar --exclude='.venv' --exclude='node_modules' --exclude='__pycache__' \
--exclude='.ccgram-uploads' \
-czf "$DEST/workspace-$name.tar.gz" \
-C "$(dirname "$workspace")" "$(basename "$workspace")"
ok "workspace-$name.tar.gz"
fi
if [ -n "$session_dir" ] && [ -d "$session_dir" ]; then
tar -czf "$DEST/sessions-$name.tar.gz" \
-C "$(dirname "$session_dir")" "$(basename "$session_dir")"
ok "sessions-$name.tar.gz"
fi
done < <(list_scenarios)
# --- 4. Credentials -------------------------------------------------------
if [ "$WITH_SECRETS" -eq 1 ]; then
STAGE="$(mktemp -d)"
found=0
while IFS= read -r f; do
[ -f "$f" ] || continue
cp -p "$f" "$STAGE/$(printf '%s' "$f" | tr '/' '_')"
found=1
done <<EOF
$HOME/.config/curator/curator.env
$HOME/.secrets/pi-memo-telegram.env
$HOME/.ccgram/.env
$REPO_ROOT/secrets/zenmux.env
EOF
if [ "$found" -eq 1 ]; then
tar -czf "$DEST/secrets.tar.gz" -C "$STAGE" .
chmod 600 "$DEST/secrets.tar.gz"
ok "secrets.tar.gz ${C_DIM}(mode 600 -- never commit)${C_OFF}"
fi
rm -rf "$STAGE"
else
info " ${C_DIM}secrets skipped (--no-secrets)${C_OFF}"
fi
# --- 5. systemd units and an environment freeze ---------------------------
STAGE="$(mktemp -d)"
cp -p "$HOME"/.config/systemd/user/*.service "$HOME"/.config/systemd/user/*.timer "$STAGE/" 2>/dev/null || true
systemctl --user list-units --all --no-pager --no-legend > "$STAGE/list-units.txt" 2>&1 || true
systemctl --user list-unit-files --no-pager --no-legend > "$STAGE/list-unit-files.txt" 2>&1 || true
tar -czf "$DEST/systemd-units.tar.gz" -C "$STAGE" .
rm -rf "$STAGE"
ok "systemd-units.tar.gz"
{
echo "# Environment freeze -- $(date -u +%FT%TZ)"
echo; echo "## pi"; pi --version 2>&1
echo; echo "## npm -g"; npm ls -g --depth=0 2>/dev/null
echo; echo "## node"; node --version
echo; echo "## python3"; python3 --version
echo; echo "## pi-agent-config HEAD"
git -C "$REPO_ROOT" log --oneline -1
git -C "$REPO_ROOT" rev-parse HEAD
} > "$DEST/environment-freeze.txt"
ok "environment-freeze.txt"
# --- 6. Manifest ----------------------------------------------------------
( cd "$DEST" && sha256sum ./*.tar.gz environment-freeze.txt > MANIFEST.sha256 )
ok "MANIFEST.sha256"
printf '\n'
( cd "$DEST" && sha256sum -c MANIFEST.sha256 >/dev/null 2>&1 ) \
&& ok "checksums verified" \
|| die "checksum verification FAILED -- do not rely on this backup"
info ""
info "Restore with: scripts/pi-restore.sh --from $DEST"