Commit Graph

7 Commits

Author SHA1 Message Date
bcherb2 2afdfd093b fix: accept either bootstrap URL shape, in dotup and in the harness
Three files each appended /bootstrap.env to a value whose shape nobody had
pinned down, and they did not agree:

  - the rotation scripts print the FULL file URL and say to store THAT in
    Bitwarden, so that is what gets pasted
  - dotup asked for the directory and appended /bootstrap.env itself
  - .tests/e2e.sh independently duplicated dotup's assumption

Pasting the saved value made the request .../bootstrap.env/bootstrap.env. The
404 tripped curl --fail and surfaced as "endpoint refused the credentials. The
machine stays public-only" -- blaming the password for a URL shape, which is
about the most expensive wrong error message this path could produce.

dotup and the wrapper now both strip a trailing slash and a trailing
/bootstrap.env before building the request, so either form works and the
existing Bitwarden entry needs no editing.

Four assertions cover all four shapes -- directory and file URL, each with and
without a trailing slash. They eval the normalisation lifted straight out of
dotup rather than a copy, and that binding was mutation-tested: deleting the
line from dotup makes them fail with exactly the production symptom,
https://h/r/bootstrap.env/bootstrap.env. 101 -> 105.

The second instance, in e2e.sh, was found only by running the harness with the
file URL instead of the directory. Every earlier run passed because it was fed
the shape that happened to be in a variable, not the shape a person has in a
password manager. Both shapes now run end to end and pass.

The normalisation exists in two places because the harness cannot source
dotup's internals. That is the same duplication that caused this, and only
dotup's copy is covered by the suite.
2026-08-17 22:52:11 -04:00
bcherb2 2a93bfd5b1 test: assert dotup's absolute path before touching PATH
The check was `export PATH="$HOME/.local/bin:$PATH"` followed by
`command -v dotup`, which proves the export worked and nothing else. It passed
on every run while a real operator on a fresh box hit
`dotup: command not found`.

~/.local/bin is put on PATH only by the .zshrc this tier ships, for a zsh that
dotup itself installs. So on any new machine the first invocation must be
~/.local/bin/dotup -- which the README already documents -- and bare `dotup`
cannot work until zsh is installed and logged into.

Assert `-x ~/.local/bin/dotup` first, then export for the rest of the script's
convenience. Reproduced the original failure in a clean root container before
changing anything, and re-ran the end-to-end after: PASS.
2026-08-17 22:30:55 -04:00
bcherb2 cebb38b97a test: move the end-to-end here, and fix the race that made it flaky
The e2e harness and its mock endpoint were living in the private repo, justified
by "they name the endpoint host". That was true when written and false two
commits later, once the hardcoded URL came out of the wrapper so the route would
live only in Bitwarden. Rechecked: all three name the endpoint zero times -- the
URL, username and password are supplied at runtime -- and the only host they
mention is this repo. They belong next to the dotup they exercise and the unit
suite that covers the rest of it.

The move surfaced a worse problem than the misplacement. The harness was flaky,
and an earlier PASS was partly luck.

chezmoi writes "git user.email?" with the tty still in cooked mode and only then
switches to raw mode -- with TCSAFLUSH, which discards whatever is already
buffered. Answering on the prompt TEXT races that switch. One run answered all
seven prompts; the next lost the Enter after user.email, leaving the field
unsubmitted. Every later expect then waited out its own timeout and the whole
thing surfaced at the 600s ceiling as an unattributed "a prompt went
unanswered".

Two fixes:

  - each answer now waits for \033[?2004h, bracketed-paste-on, which the TUI
    emits only AFTER raw mode is established. "Probably ready" becomes
    "demonstrably ready", and each prompt emits its own, so it is per-answer.
  - a bare `expect -re {pat} {...}` treats timeout as "carry on", which is what
    turned one lost keystroke into a ten-minute mystery. Prompts now fail
    immediately naming which one was missed, and distinguish EOF (dotup exited
    early) from timeout.

Also moved red(), the redactor, above the run. It was defined below the new
early-abort path that calls it, so the one branch that most needs redaction
would have hit an undefined function.

Verified by running it twice end to end against a mock endpoint, both passing
identically, including the three assertions the argv fix exists for.

README: the suite is 101 assertions, not 81, and the end-to-end is documented.
2026-08-17 22:14:35 -04:00
bcherb2 63bed1d32d fix: keep the gitea token out of chezmoi init's argv and .git/config
The endpoint hands back a clone URL with the read-only token inline, and that
URL went straight onto `chezmoi init`'s command line. Two durable exposures
followed, neither of which the existing redaction touched -- it only kept the
token out of the trace line:

  - /proc/<pid>/cmdline is world-readable, so any account on the box could read
    the live token for as long as the clone ran
  - the resulting .git/config recorded the credential-bearing remote and kept
    it until the tree was deleted

Split the credential out of the URL before anything executes. The token goes to
$STATE/private-credentials at 600 in git-credential-store format; the URL that
reaches argv and .git/config is clean. The helper is passed via GIT_CONFIG_*
for the clone and then written into the clone's own config, so a later
`chezmoi update` still authenticates without the token being stored.

Verified: clone succeeds with the clean URL, .git/config contains no token, and
a subsequent fetch authenticates from the credential file alone. Suite 101/101.
2026-08-17 20:21:39 -04:00
bcherb2 400bd9b9f1 fix: install bws from the private tier, not the manifest
The previous commit put the bws install on the private/bws-secrets row as
a tarball channel. That row can never install anything: selected_packages
drops every row flagged `private`, which is the invariant that makes
--unattended safe to run. The container proved it — unzip installed, bws
did not, and the row still could not deliver on its promise.

Nor can it be a safe row: the defaults preset ticks every safe package, so
that would put a Bitwarden binary and a 12 MB GitHub download on every
throwaway public VM, for a tool those machines have no credential to use.

So ensure_bws lives in dotup and is called from cmd_private immediately
after the token is written. Nothing installs bws unless something is about
to hand it a token, and the manifest keeps its invariant.

core/unzip stays a real package with the @needs edge — the release is a
.zip and 24.04 minimal has no unzip.
2026-08-17 11:20:45 -04:00
bcherb2 90bca39396 fix: install bws, isolate the private config, keep credentials out of logs
Three defects the private tier could not survive a fresh machine with.

bws was never installed. The private/bws-secrets row promised seven API
keys and shipped no way to fetch them: dotsecrets shells out to `bws
secret get`, and nothing put that binary on the box. Added as a tarball
channel with checksum verification, musl rather than gnu so it does not
pin a glibc newer than an older LTS carries, plus core/unzip as a real
@needs dependency since the release is a .zip and 24.04 minimal has no
unzip. The releases API needs filtering by tag prefix: sdk-sm is a
monorepo and `latest` usually points at a python SDK, not bws.

Both tiers rendered their config to ~/.config/chezmoi/chezmoi.toml, so
re-running the public installer overwrote the private config and took its
seven promptStringOnce answers with it. Silently -- the templates degrade
politely when data is missing, so the symptom was `git commit` not knowing
who you are, days later. The private tier now renders to private.toml and
the cmp alias carries the matching -c.

`run` echoes its argv to stderr, and the private init argv ends in
https://user:TOKEN@host -- into scrollback, any `dotup 2>log`, and any
agent transcript. Traced through redact_url instead, in both the live and
dry-run branches. The clone is also chmod -R go-rwx afterwards: it lands
at the caller umask, and .git/config stores that same credential URL.

Tests: 90 passing, 9 new covering all three.
2026-08-17 11:15:48 -04:00
bcherb2 b487b0e855 feat: public dotfiles tier — no credential, no identity, one installer
Fresh history. This is the repo a throwaway VM clones anonymously: it brings a
machine to a working baseline and carries nothing that makes it mine.

56 files. 50 land in $HOME, 3 are chezmoi metadata, 2 are repo documentation,
1 is the manifest, and a 15-file test harness stays behind in .tests/.

What did not travel, and why:

  encrypted_private_bws-token.age   a real credential; age is dropped entirely
  .chezmoidata/bws.toml             env-var -> secret-id map; belongs with the
                                    tier that can use it
  SECRETS.md                        documentation of the rules, not config
  finish-setup.sh.tmpl              superseded by dotup
  nvim/init.lua.backup              dead file
  dot_claude/**, dot_codex/**,      120 files of agent config, private tier
  dot_pi/**

De-identified rather than dropped:

  .gitconfig   [user], the GitHub ssh rewrite and both Gitea host rewrites are
               identity, not configuration. They move behind an [include] of
               ~/.config/git/config.local, which the private tier writes. Git
               treats a missing include as a no-op, so a public-only machine
               reads the file and stops.
  .zshrc       the two gitea aliases carried a personal domain and a LAN IP.
               They move behind a guarded source of ~/.config/zsh/local.zsh,
               the sibling of the secrets.zsh seam phase 2 established.
  nvim         a commented-out LM Studio endpoint naming a LAN address.
  ghostty      a stale auto-generated header naming an absolute home directory.

Newly captured, never tracked before: ~/.zshenv, ~/.config/gh/config.yml. The
former sourced ~/.cargo/env unguarded, so every zsh on a machine without rustup
printed an error -- the same shape as the unguarded oh-my-zsh source phase 2
fixed. It is guarded now.

.chezmoiexternal.toml grows from one entry to six. oh-my-zsh, powerlevel10k,
zsh-autosuggestions, zsh-ai and tpm were hand-installed and declared nowhere,
which is why `chezmoi init --apply` on a clean box produced a .zshrc that broke
the shell it configures. The theme and both plugins nest under
.oh-my-zsh/custom/, which is what $ZSH_CUSTOM resolves to.

dotup gains an install engine. It resolves each selected package to a channel
(apt, brew, npm, uv, snap, deb, flatpak, tarball, script, builtin) through one
function every consumer reads, probes apt-cache before batching so a name apt
does not know moves to brew instead of failing all thirty, and retries
individually if a batch still fails -- which earned its keep on the first real
container run, where mermaid-cli's puppeteer dependency failed and the other
twelve npm packages installed anyway. --unattended computes safe defaults fresh
from the manifest rather than inheriting a state file, and refuses private and
invasive rows outright even when a stale state file ticks them.

The manifest gains @spec, a second directive kind alongside @needs, carrying the
argument a channel needs but a package name cannot supply -- the scoped npm
name, the flatpak app id, the .deb source. The TSV stays five columns wide.

Three bugs the container runs found, all fixed here:

  * `apt install nodejs` gives you node WITHOUT npm on Ubuntu, so all thirteen
    npm packages failed on a fresh box. The manifest asks apt for both names.
  * A tool installed a moment ago is not on this process's PATH -- uv lands in
    ~/.local/bin, npm -g honours the ~/.npmrc prefix, linuxbrew is outside a
    non-login PATH. Resolved by looking in the places we just wrote to, never by
    exporting a modified PATH.
  * `A || { B && C; }` is one || list, so when `command -v sudo` failed the list
    failed and `set -e` killed dotup at load. On a non-root machine with no
    sudo it died before printing anything. There is a regression test.

.zshenv and .p10k.zsh are marked private_. Both are shell code the login shell
executes and both applied at 664, group-writable. Third occurrence of the class
of bug phase 1 found on .pi/agent/auth.json and phase 2 found on .zshrc; the
first one found on purpose rather than by accident.

Verification: 81 assertions, 81/81 on this box and in ubuntu:24.04, ubuntu:22.04
and debian:12. The installer is driven against a directory of fake package
managers that record what they were asked to do and install nothing, so the
engine is exercised end to end without a package landing on the test machine.
`gitleaks detect` over the full history and the working tree: no leaks found,
with no allowlist and no .gitleaks.toml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:24:41 -04:00