#!/bin/bash # The private tier, end to end, against a local stand-in for everything remote. # # Nothing here touches the real endpoint, the real repos or the real secrets # manager, so it can be run as often as it takes and costs nothing when it # fails. What it does exercise is the real code: dotup's prompt loop, the real # credential splitting, chezmoi's real seven-question TUI, and the private # tier's real `dotsecrets` -- copied verbatim into the fake source tree, so it # is the shipping implementation being measured, not a rewrite of it. # # The first password is deliberately wrong. These credentials are asked for at # the very end of a run, so before the retry loop existed one typo meant redoing # the entire install. Getting it wrong on purpose is the only way to prove the # recovery path is there and that the URL and username survive the mistake. set -u fail() { echo "FAIL: $*"; exit 1; } ok() { echo " ok $*"; } umask 022 # modes below assume it; do not let the daemon's umask decide sudo apt-get update -qq && sudo apt-get install -y -qq git expect unzip >/dev/null 2>&1 echo " NOTE installed git by hand -- see ISSUE-1" # ---- public tier first: the private tier is a continuation, never a start ---- sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >/tmp/init.log 2>&1 \ || { tail -5 /tmp/init.log; fail "public tier init"; } export PATH="$HOME/.local/bin:$HOME/bin:$PATH" D=$HOME/.local/bin/dotup [ -x "$D" ] || fail "no dotup after the public apply" ok "public tier applied" # absence assertions BEFORE the private tier exists, so their later presence # means something for f in "$HOME/.config/zsh/secrets.zsh" "$HOME/.config/bitwarden/bws-token" \ "$HOME/.local/share/dotfiles-private"; do [ ! -e "$f" ] || fail "$f exists on a public-only machine" done ok "public-only machine carries no token, no secrets, no private source" # ---- a fake bws, so nothing reaches the network ----------------------------- # `ensure_bws` checks `have bws` first and `have` searches ~/.local/bin, so # putting the stub there is enough to keep the real download out of this run. # The real ensure_bws (pin, checksum, musl target) is a separate scenario -- # faking it here would only prove the fake works. mkdir -p "$HOME/.local/bin" cat > "$HOME/.local/bin/bws" <<'BWS' #!/bin/sh # Stand-in for the Bitwarden Secrets Manager CLI. Answers exactly the call # dotsecrets makes: `bws secret get -o env`, printing KEY=VALUE. # LAB_BWS_MODE bends it to drive the failure branches. case "${LAB_BWS_MODE:-ok}" in fail) exit 1 ;; wrong) printf 'SOMETHING_ELSE=x\n'; exit 0 ;; empty) printf 'LAB_ALPHA_API_KEY=\n'; exit 0 ;; esac [ "$1" = secret ] && [ "$2" = get ] || { echo "unsupported: $*" >&2; exit 2; } case "$3" in *0001) k=LAB_ALPHA_API_KEY ;; *0002) k=LAB_BRAVO_API_KEY ;; *0003) k=LAB_CHARLIE_API_KEY ;; *0004) k=LAB_DELTA_API_KEY ;; *0005) k=LAB_ECHO_API_KEY ;; *0006) k=LAB_FOXTROT_API_KEY ;; *0007) k=LAB_GOLF_API_KEY ;; *) echo "unknown id $3" >&2; exit 1 ;; esac printf '%s=lab-value-for-%s\n' "$k" "$k" BWS chmod 755 "$HOME/.local/bin/bws" ok "fake bws in place; this run reaches no network" # ---- tick the private rows, as the picker would ------------------------------ S=${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles mkdir -p "$S"; printf 'private/private-repo\nprivate/bws-secrets\n' > "$S/selected" # ---- drive it exactly as a person sitting at the keyboard would -------------- export E2E_NAME='Lab Tester' E2E_EMAIL='lab@example.invalid' timeout 420 expect -f - >/tmp/private.log 2>&1 <<'EXP' set timeout 120 log_user 1 spawn -noecho env PATH=$env(PATH) $env(HOME)/.local/bin/dotup private # A bare `expect -re {pat} {}` treats a timeout as "carry on", so a missed # prompt falls through silently and every later expect waits out its own # timeout, surfacing much later as an unattributed hang. Name what was missed. proc wait_for {pat what} { expect { -re $pat {} timeout { send_user "\nTIMEOUT: never saw $what\n"; exit 3 } eof { send_user "\nEOF before $what -- dotup exited early\n"; exit 4 } } } # dotup's own three prompts are plain `read` in cooked mode, so the text is the # whole signal. wait_for {Bootstrap URL:} "the bootstrap URL prompt" send -- "$env(BOOT_URL)\r" wait_for {Username:} "the username prompt" send -- "$env(BOOT_USER)\r" wait_for {Password:} "the password prompt" send -- "wrong-on-purpose\r" wait_for {wrong username or password} "the 401 message naming the actual fault" # The bracketed default is the proof that the URL and username were retained, # so only the password has to be retyped. wait_for {Bootstrap URL \[} "the retry prompt with the URL kept" send -- "\r" wait_for {Username \[} "the retry prompt with the username kept" send -- "\r" wait_for {Password:} "the retry password prompt" send -- "$env(BOOT_PW)\r" # chezmoi's seven are a full-screen TUI, and matching the prompt TEXT is not # enough. chezmoi writes the prompt while the tty is still in cooked mode and # only then switches to raw with TCSAFLUSH, which DISCARDS anything already # buffered. An answer sent on the text alone can land in that window and be # thrown away -- the field sits unsubmitted and the run burns its whole # timeout. `\033[?2004h` is bracketed-paste-on, emitted only after raw mode is # established, so waiting for it turns "probably ready" into "demonstrably # ready". Each prompt emits its own. proc ask {pat val} { wait_for $pat "chezmoi prompt $pat" wait_for "\033\\\[\\?2004h" "raw mode after $pat (the TUI never became ready)" send -- "$val\r" } ask {user\.name} "$env(E2E_NAME)" ask {user\.email} "$env(E2E_EMAIL)" ask {signing key} "" ask {WAN ssh} "" ask {LAN ssh} "" ask {WAN web} "" ask {LAN web} "" expect eof catch wait result exit [lindex $result 3] EXP rc=$? red() { sed -e "s|$BOOT_PW||g" -e "s|$BOOT_URL||g"; } case $rc in 124) red /dev/null; } [ -d "$HOME/.local/share/dotfiles-private" ] || fail "private source not cloned" case $(m "$HOME/.local/share/dotfiles-private") in *00) ;; *) fail "private source is mode $(m "$HOME/.local/share/dotfiles-private") — group/other can read it" ;; esac ok "private source cloned, go-rwx" [ "$(m "$HOME/.config/bitwarden/bws-token")" = 600 ] || fail "bws token mode $(m "$HOME/.config/bitwarden/bws-token"), want 600" ok "bws token written, mode 600" SEC=$HOME/.config/zsh/secrets.zsh [ -r "$SEC" ] || fail "secrets.zsh not generated" [ "$(m "$SEC")" = 600 ] || fail "secrets.zsh mode $(m "$SEC"), want 600" n=$(grep -c '^export ' "$SEC") [ "$n" -eq 8 ] || fail "secrets.zsh has $n exports, want 8 (7 secrets + 1 alias)" grep -q '^export LAB_GOLF_ALIAS_KEY=' "$SEC" || fail "the alias export is missing" ok "secrets.zsh mode 600, 8 exports, alias present" for p in "$HOME/.config/zsh:700" "$HOME/.ssh:700" "$HOME/.ssh/config:600" \ "$HOME/.config/zsh/local.zsh:600" "$HOME/.local/bin/dotsecrets:700"; do want=${p##*:}; path=${p%:*} [ "$(m "$path")" = "$want" ] || fail "$path is mode $(m "$path"), want $want" done ok "every private destination carries the mode it claims" # The credential must survive as a file and nowhere else. CRED=${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/private-credentials [ "$(m "$CRED")" = 600 ] || fail "credential file mode $(m "$CRED"), want 600" GC=$HOME/.local/share/dotfiles-private/.git/config grep -qE '://[^/@[:space:]]*:[^/@[:space:]]+@' "$GC" \ && fail "the clone's .git/config still carries the token" grep -qE '://[^/@[:space:]]*:[^/@[:space:]]+@' /tmp/private.log \ && fail "a credential-bearing URL appears in dotup's own output" ok "token is in the 600-mode credential file only — not in .git/config, not in the log" git -C "$HOME/.local/share/dotfiles-private" fetch -q 2>/dev/null \ || fail "a later fetch cannot authenticate — the credential helper did not survive" ok "a later fetch still authenticates from that file" # ---- the failure branches: yesterday's keys beat no keys -------------------- before=$(sha256sum "$SEC" | cut -d' ' -f1) for mode in fail wrong empty; do LAB_BWS_MODE=$mode "$HOME/.local/bin/dotsecrets" >/dev/null 2>&1 [ "$(sha256sum "$SEC" | cut -d' ' -f1)" = "$before" ] \ || fail "bws mode '$mode' modified secrets.zsh — it must be left alone on failure" done ls "$HOME/.config/zsh"/.secrets.zsh.* >/dev/null 2>&1 && fail "a temp file survived a failed refresh" ok "a failed refresh leaves secrets.zsh byte-identical and no temp file behind" # ---- GitHub reads stay anonymous https after the tier applies --------------- # The tier ships a git@github.com: rewrite for PUSHES. If it ever regresses to # a blanket insteadOf, every https clone on a keyless fresh box breaks -- the # first casualty was nvim's plugin bootstrap (live, 2026-08-22). # Plain `git config --get`, not --global: --global does not follow the # [include] seam by default, which made this pair pass vacuously. git config --get 'url.git@github.com:.insteadof' >/dev/null 2>&1 \ && fail "GitHub https READS are rewritten to ssh — a keyless box cannot clone plugins" git config --get 'url.git@github.com:.pushinsteadof' >/dev/null 2>&1 \ || fail "the GitHub push-over-ssh rewrite is gone entirely" ok "GitHub reads stay https; only pushes are rewritten to ssh" echo "PRIVATE TIER PASS"