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.
141 lines
5.2 KiB
Bash
Executable File
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"
|