Files
Panama/tests/setup/user-content-contract
T
Gabriel Brown 89761a7da3 Keep the personal half of the desktop in one place, and ask before installing it
Agent instructions, skills, SSH host aliases and expansion triggers are worth
having identical on every machine one person owns, and belong in none of the
shared configuration. They live in user/ now, with a manifest saying where each
piece goes and a link-user stage that puts it there.

That stage does nothing unless the machine said yes. Somebody who clones Panama
to try the desktop keeps their own ~/.claude/CLAUDE.md exactly where it was;
the question names the destinations and defaults to no. Anything displaced goes
to config/old rather than being deleted.

~/.claude/CLAUDE.md and ~/.codex/AGENTS.md were byte-identical copies of one
file, which is the drift this exists to prevent.

Also adds the vitals toggles for the battery and Claude usage readouts, which
had preferences and no way to reach them.
2026-08-22 08:54:43 -04:00

172 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# The personal half of the repository.
#
# user/ is somebody's actual agent instructions and SSH host aliases, tracked in
# a repository other people are meant to clone. That is only defensible under
# three rules, and this pins all three:
#
# 1. Nothing is linked on a machine that did not say yes. A stranger who runs
# ./install and answers the default gets their own ~/.claude/CLAUDE.md left
# exactly where it was.
# 2. Nothing already in a destination is destroyed. It moves to config/old/,
# the same promise link-dotfiles makes.
# 3. No secrets. The directory is world-readable to anyone who finds the
# repository, so a private key or token committed here is a disclosure and
# not a mistake to be caught in review.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
linker="$repo_dir/setup/scripts/link-user"
manifest="$repo_dir/user/manifest"
interview="$repo_dir/setup/scripts/interview"
installer="$repo_dir/install"
findings=()
note() { findings+=("$1"); }
[[ -x "$linker" ]] || { printf 'user content contract: %s is not executable\n' "$linker" >&2; exit 1; }
[[ -r "$manifest" ]] || { printf 'user content contract: no manifest at %s\n' "$manifest" >&2; exit 1; }
# ── 3. No secrets ───────────────────────────────────────────────────────────
#
# Checked first, because it is the one failure that cannot be undone by fixing
# the code: a key that reached a commit is a key that must be rotated.
if grep -rlqE 'BEGIN [A-Z ]*PRIVATE KEY' "$repo_dir/user" 2>/dev/null; then
note 'a private key is committed under user/'
fi
if grep -rlqE 'sk-ant-[A-Za-z0-9]|ghp_[A-Za-z0-9]{20}|xox[baprs]-[A-Za-z0-9]' "$repo_dir/user" 2>/dev/null; then
note 'something that looks like an API token is committed under user/'
fi
while read -r candidate; do
note "user/ carries a credentials file: ${candidate#"$repo_dir"/}"
done < <(find "$repo_dir/user" -type f \( -name 'id_*' -o -name '*.pem' -o -name '.credentials.json' \) 2>/dev/null)
# ── Every manifest line is usable ───────────────────────────────────────────
while read -r kind source destination; do
[[ -e "$repo_dir/user/$source" ]] \
|| note "the manifest points at $source, which is not in user/"
[[ "$kind" == link || "$kind" == copy ]] \
|| note "the manifest uses an unknown kind: $kind"
[[ "$destination" == '~/'* ]] \
|| note "the manifest destination $destination is not under the home directory"
done < <(grep -vE '^\s*(#|$)' "$manifest")
# ── The behaviour, against a fake home ──────────────────────────────────────
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
# A checkout of its own, so the test never links anything into the real home and
# never moves a real file into the real config/old.
checkout="$work/Panama"
mkdir -p "$checkout/setup/scripts" "$checkout/user/agents"
cp "$linker" "$checkout/setup/scripts/link-user"
printf 'tracked instructions\n' >"$checkout/user/agents/AGENTS.md"
mkdir -p "$checkout/user/agents/skills/example"
printf 'a skill\n' >"$checkout/user/agents/skills/example/SKILL.md"
printf 'copied once\n' >"$checkout/user/plain.txt"
cat >"$checkout/user/manifest" <<'FIXTURE'
# a comment, and a blank line follow
link agents/AGENTS.md ~/.claude/CLAUDE.md
link agents/skills ~/.agents/skills
copy plain.txt ~/.config/plain.txt
link missing.txt ~/.config/missing.txt
FIXTURE
home="$work/home"
run() {
HOME="$home" XDG_STATE_HOME="$work/state" PANAMA_PATH="$checkout" \
"$checkout/setup/scripts/link-user" "$@" >"$work/log" 2>&1
}
# ── 1. Silence unless asked ─────────────────────────────────────────────────
mkdir -p "$home/.claude"
printf 'somebody else instructions\n' >"$home/.claude/CLAUDE.md"
run
[[ "$(cat "$home/.claude/CLAUDE.md")" == "somebody else instructions" ]] \
|| note 'personal content was linked on a machine that was never asked'
[[ -e "$home/.agents/skills" ]] \
&& note 'skills were linked on a machine that was never asked'
PANAMA_USER_CONTENT=no run
[[ "$(cat "$home/.claude/CLAUDE.md")" == "somebody else instructions" ]] \
|| note 'personal content was linked on a machine that answered no'
# ── 2. Saying yes links, and keeps what was there ───────────────────────────
PANAMA_USER_CONTENT=yes run
[[ -L "$home/.claude/CLAUDE.md" ]] \
|| note 'CLAUDE.md was not replaced with a symlink into the checkout'
[[ "$(cat "$home/.claude/CLAUDE.md")" == "tracked instructions" ]] \
|| note 'the CLAUDE.md link does not resolve to the tracked file'
[[ -L "$home/.agents/skills" && -f "$home/.agents/skills/example/SKILL.md" ]] \
|| note 'the skills directory was not linked as a directory'
[[ -f "$home/.config/plain.txt" && ! -L "$home/.config/plain.txt" ]] \
|| note 'a copy entry was linked rather than copied'
if ! grep -rq 'somebody else instructions' "$checkout/config/old" 2>/dev/null; then
note 'the file that was already there was not preserved in config/old'
fi
# A source the manifest names but the checkout does not have is skipped, not
# fatal: half a manifest applied is better than none, and the log says which.
[[ -e "$home/.config/missing.txt" ]] \
&& note 'a manifest entry with no source produced a destination anyway'
grep -q 'missing.txt' "$work/log" \
|| note 'a skipped manifest entry was not reported'
# ── The answer sticks ───────────────────────────────────────────────────────
#
# So that `panama upgrade` on a configured machine relinks without an interview,
# and a machine that said no stays quiet forever.
printf 'edited by hand\n' >"$home/.config/plain.txt"
rm -f "$home/.claude/CLAUDE.md"
run # no PANAMA_USER_CONTENT this time
[[ -L "$home/.claude/CLAUDE.md" ]] \
|| note 'the recorded answer was not remembered, so an upgrade would not relink'
[[ "$(cat "$home/.config/plain.txt")" == "edited by hand" ]] \
|| note 'a copy entry overwrote a file the machine had edited'
# Relinking twice must not bury the previous run's symlink in config/old.
before="$(find "$checkout/config/old" -type f 2>/dev/null | wc -l)"
run
after="$(find "$checkout/config/old" -type f 2>/dev/null | wc -l)"
[[ "$before" == "$after" ]] \
|| note 'relinking backed up its own symlink, so config/old grows every run'
# ── Wiring ──────────────────────────────────────────────────────────────────
grep -q 'link-user' "$installer" \
|| note 'link-user is not in the installer STAGES list, so it never runs'
grep -q 'PANAMA_USER_CONTENT' "$installer" \
|| note 'the installer does not export the personal-content answer'
grep -q 'PANAMA_USER_CONTENT' "$interview" \
|| note 'the interview never asks about personal content'
# Order matters: link-user must land the tracked espanso identity before
# setup-identity would seed one from the interview answers.
python3 - "$installer" <<'PY' || note 'link-user does not run before setup-identity'
import re, sys
line = next(l for l in open(sys.argv[1], encoding="utf-8") if l.startswith("STAGES="))
stages = re.findall(r"[\w-]+", line)
if stages.index("link-user") > stages.index("setup-identity"):
raise SystemExit(1)
PY
if (( ${#findings[@]} > 0 )); then
printf 'user content contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'user content contract: PASS\n'