From d24a66f11a7c9f975134ededdec252b513dfdc53 Mon Sep 17 00:00:00 2001 From: dietrich ayala Date: Thu, 6 Aug 2026 23:05:32 +0200 Subject: [PATCH] make every restored path writable, not just the workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The restic backup set is four paths, and the read-only-file problem belongs to all of them: ~/.claude holds git repositories of its own, so its pack files are mode 0444 exactly as the workspace's object files are. Restoring with only ~/workspace made writable still aborted, on six pack files under ~/.claude. The drill's chmod list and the backup set are now two lists that must agree, so a guard in lib/common.test.sh extracts both and fails when they diverge — adding a path to the backup set without adding it here would otherwise reappear as a restore that dies partway through, months later, during a recovery. --- drill-restore.sh | 16 +++++++++++++--- flit-spec.md | 19 +++++++++++-------- lib/common.test.sh | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/drill-restore.sh b/drill-restore.sh index 4c6b8d8..f556cec 100755 --- a/drill-restore.sh +++ b/drill-restore.sh @@ -88,9 +88,19 @@ printf '%s\n' "$STORAGEBOX_KEY" \ # read-only file for writing fails even for its owner — regardless of who # runs the restore. The restic in use here predates the --overwrite mode # that would otherwise handle this, so write permission is restored on the -# target first instead. -drill_ssh "chmod -R u+w ~/workspace" \ - || die "could not make ~/workspace writable ahead of the restic restore" +# target first instead — for every path restic actually backs up, not just +# ~/workspace: a live run narrowed to ~/workspace still failed with errors +# under ~/.claude, which holds git repositories of its own. The list below +# IS the backup set, the same four arguments `restic backup` takes in +# server/bin/backup.sh (lib/common.test.sh guards the two lists against +# drifting apart). A path absent from the restored image — ~/.vim/undo, in +# particular, may never have been created — is skipped rather than failed; +# any other chmod failure still trips die() below. +drill_ssh 'for p in ~/workspace ~/.claude ~/.bash_history ~/.vim/undo; do + [ -e "$p" ] || continue + chmod -R u+w "$p" || exit 1 + done' \ + || die "could not make the backup set writable ahead of the restic restore" log "restoring latest restic snapshot" # Read fresh here rather than trust drill_precondition's check, which discards diff --git a/flit-spec.md b/flit-spec.md index 3103b7e..d767de8 100644 --- a/flit-spec.md +++ b/flit-spec.md @@ -1571,10 +1571,13 @@ restore worked. Buildability is the fixtures' job. 1. Create a CX53 from the most recent automatic backup — or from the golden snapshot when run with `--from-snapshot`, which is required at build time, before any automatic backup exists (§12.1). -2. Make `~/workspace` writable, then restore the restic snapshot onto it. git writes its object - files mode 0444, and a restore onto a host whose image already contains them must overwrite - those files — which fails on a read-only file even for its owner — so write permission is - restored first. +2. Make the whole restic backup set writable — `~/workspace`, `~/.claude`, `~/.bash_history`, + `~/.vim/undo`, the same four paths `server/bin/backup.sh` passes to `restic backup` + (`lib/common.test.sh` guards the two lists against drifting apart) — then restore the restic + snapshot onto it. git writes its object files mode 0444, and a restore onto a host whose image + already contains them must overwrite those files — which fails on a read-only file even for its + owner — so write permission is restored first. A path absent from the image (`~/.vim/undo`, in + particular) is skipped rather than failed. 3. Supply the `pass-cli` access token — it is §7.3 residue and is **not** in the restic backup set, so a restored host has none. The drill injects it from the vault session it is already running under (`pass-cli run`), which works because drills are laptop-initiated. It also @@ -1632,10 +1635,10 @@ works: 0. **Provision a replacement machine first.** These steps operate on a host that exists. Either create one from the most recent Hetzner backup, or run `bootstrap.sh` against a stock image. Both need the Hetzner API token, which needs step 1 — so do step 1 before this one. Whichever - path is taken, make `~/workspace` writable (`chmod -R u+w ~/workspace`) before any restic - restore run against it — git's object files are mode 0444, restoring onto a host that already - holds them requires overwriting them, and opening a read-only file for writing fails even for - its owner (§8.1). + path is taken, make the whole restic backup set writable (`~/workspace`, `~/.claude`, + `~/.bash_history`, `~/.vim/undo`) before any restic restore run against it — git's object files + are mode 0444, restoring onto a host that already holds them requires overwriting them, and + opening a read-only file for writing fails even for its owner (§8.1). 1. **Create a new `pass-cli` access token** in the Proton Pass UI, scoped read-only to the vault. Everything else depends on this. 2. **Write `/etc/$FLIT_NAME/secrets.env`** — restic repository password and the new token, mode diff --git a/lib/common.test.sh b/lib/common.test.sh index 111ae96..9d161cd 100755 --- a/lib/common.test.sh +++ b/lib/common.test.sh @@ -560,5 +560,20 @@ t "resolve_operator_key: several agent keys refuses rather than choosing" "1" "$ t "resolve_operator_key: refusal names the reason" "1" \ "$(grep -c 'the ssh agent holds several keys' <<<"$out")" +# ---- 18. drill-restore.sh's pre-restore `chmod -R u+w` list must name +# exactly the restic backup set — the literal path arguments server/bin/ +# backup.sh passes to `restic backup` — or a path added to one and not the +# other either goes unbacked-up or arrives read-only and fails the restore. +# Both lists are read out of the real files, not duplicated here, so this +# guard tracks either one changing without the other (same shape as the §13 +# rule 13 guard above). +backup_paths=$(grep -oE '(^|[[:space:]])"\$HOME/[^"]*"' server/bin/backup.sh \ + | sed -E 's/^[[:space:]]*//' | tr -d '"' | sed 's#^\$HOME/##' | sort) +chmod_line=$(grep -oE 'for p in [^;]*' drill-restore.sh) +chmod_paths=$(printf '%s\n' "$chmod_line" | sed -E 's/^for p in //' \ + | tr ' ' '\n' | sed 's#^~/##' | sort) +t "drill-restore.sh's chmod list matches server/bin/backup.sh's restic backup set" \ + "" "$(diff <(printf '%s\n' "$backup_paths") <(printf '%s\n' "$chmod_paths"))" + printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" [[ $FAIL -eq 0 ]] -- 2.51.2