cebb38b97a
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.
259 lines
12 KiB
Markdown
259 lines
12 KiB
Markdown
# dotfiles — public tier
|
|
|
|
Everything a machine needs to become a working machine, and nothing that makes
|
|
it *mine*. Clone it anonymously, apply it to a throwaway VM, and you get the
|
|
editor, the shell, the terminal and the tools. You do not get my name, my email,
|
|
my hosts, my agent config, or any credential — because none of that is in here.
|
|
|
|
The other half lives in a private repo you cannot clone without a password. That
|
|
split is the whole design, and it is structural rather than a policy: the gate
|
|
is *which repo a machine can obtain*, not a `.chezmoiignore` rule that could be
|
|
misconfigured once and leak.
|
|
|
|
Why the repo is shaped this way: **`DESIGN.md`**. How it got here: **`PLAN.md`**.
|
|
Both live in the working repo, not this one.
|
|
|
|
---
|
|
|
|
## Install
|
|
|
|
```sh
|
|
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply <this repo url>
|
|
~/.local/bin/dotup
|
|
```
|
|
|
|
Two commands. The first lays down the files and clones the six externals; the
|
|
second picks and installs the software. There is no third step, and nothing
|
|
here prompts for anything.
|
|
|
|
For a machine with nobody at the keyboard:
|
|
|
|
```sh
|
|
dotup --unattended # safe defaults, no UI, no prompt, no private tier
|
|
dotup --print # resolve everything and print the commands, install nothing
|
|
```
|
|
|
|
## What lands where
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `~/.zshrc` | Plain file, no template, no secrets. One guarded `source` is the entire secret surface. |
|
|
| `~/.zshenv`, `~/.p10k.zsh` | Shell environment and prompt |
|
|
| `~/.config/nvim/**` | LazyVim, 36 files. `lua/plugins/image.lua` is the fragile one — see DESIGN §3 before touching it. |
|
|
| `~/.config/{gh,ghostty,git,btop,herdr}` | Terminal and tooling config |
|
|
| `~/.tmux.conf` → `~/.tmux/.tmux.conf` | Symlink into the gpakosz external |
|
|
| `~/.gitconfig` | No `[user]`. See "the seams" below. |
|
|
| `~/.local/bin/{dotup,sysjournal}` | The installer and the journal CLI |
|
|
| `~/.local/share/dotup/packages.tsv` | The manifest `dotup` reads |
|
|
|
|
## The seams
|
|
|
|
Three files this tier *reads* and never writes. Each is absent on a public-only
|
|
machine, and each absence is a silent no-op rather than an error.
|
|
|
|
| File | Written by | Carries |
|
|
|---|---|---|
|
|
| `~/.config/git/config.local` | private tier | `[user]`, signing key, Gitea host rewrites |
|
|
| `~/.config/zsh/local.zsh` | private tier | aliases naming hosts you own |
|
|
| `~/.config/zsh/secrets.zsh` | `dotsecrets`, from bws | the seven API keys, mode 600, in no repo |
|
|
|
|
If `git commit` says it does not know who you are, the private tier has not been
|
|
applied. That is the correct complaint, not a bug.
|
|
|
|
## Externals
|
|
|
|
Six `git-repo` externals, none of which were declared anywhere before this repo
|
|
existed. That omission is why a clean bootstrap used to produce a broken login
|
|
shell: `.zshrc` sourced `$ZSH/oh-my-zsh.sh` with nothing having installed it.
|
|
|
|
`.tmux`, oh-my-zsh, powerlevel10k, zsh-autosuggestions, zsh-ai, tpm. The theme
|
|
and both plugins are nested under `.oh-my-zsh/custom/`, which is what
|
|
`$ZSH_CUSTOM` resolves to; chezmoi processes externals in path order, so the
|
|
parent is cloned first.
|
|
|
|
## dotup
|
|
|
|
One screen. Groups and packages are the same kind of row, both carry a tick box,
|
|
and every group opens into the packages underneath it. Group boxes are a
|
|
tri-state summary of their children, never an independent switch.
|
|
|
|
```
|
|
▸ [x] core 31/31 safe
|
|
▾ [~] media 2/3 safe
|
|
[x] ffmpeg apt ffmpeg
|
|
[ ] sox apt sox
|
|
[x] p7zip apt p7zip-full
|
|
▸ [ ] gpu 0/3 invasive kernel modules; a bad driver can bre…
|
|
▸ [ ] private 0/2 private one password, typed after the install…
|
|
|
|
space tick tab open ^t tick all shown ^a defaults ^x none enter install
|
|
```
|
|
|
|
**One toggle rule, everywhere.** Expand the row to the packages it covers; if
|
|
every one is on, turn them all off, otherwise turn them all on. That single rule
|
|
covers a group row, a package row, and a bulk toggle over a filtered set — type
|
|
`nvidia`, press `^t`, and exactly the three rows you can see flip.
|
|
|
|
**The risk model.** `safe` is pre-ticked; `gui` is pre-ticked only where there
|
|
is a display; `invasive` is never ticked for you, and its reason is on the row;
|
|
`private` needs a password you type after the install finishes. A group's flag
|
|
is derived as the worst flag among its children, so a group can never look
|
|
safer than something inside it.
|
|
|
|
**Dependencies tick themselves.** `@needs` closure runs in both directions:
|
|
ticking `xrdp` ticks the `desktop` group it is useless without, and unticking
|
|
`core/node` drops `codex`, `pi`, `mermaid-cli` and then `neovim`. The closure is
|
|
shown, not described — the counts move on the same keystroke.
|
|
|
|
### The manifest
|
|
|
|
Five tab-separated columns, greppable by hand. Anything that would have been a
|
|
sixth column is an `@` directive instead:
|
|
|
|
```tsv
|
|
@needs core/neovim core/imagemagick core/mermaid-cli
|
|
@spec agents/codex @openai/codex
|
|
core neovim safe -tarball neovim apt ships 0.9.5 — tarball to /opt/nvim
|
|
core fd safe fd-find fd binary is fdfind on ubuntu
|
|
```
|
|
|
|
A leading `-` in the apt or brew column means "not from this package manager":
|
|
`-tarball -npm -script -snap -deb -flatpak -uv -builtin -xcode`. A bare `-`
|
|
means unavailable there — and **Linux resolves apt-then-brew, not
|
|
apt-instead-of-brew**, because `omp`, `herdr` and `lazygit` have no apt package
|
|
at all. The installer goes further and probes `apt-cache` before it batches: a
|
|
name apt does not know moves to the brew column rather than failing the whole
|
|
batch.
|
|
|
|
`@spec` carries the argument a channel needs but the package name cannot supply
|
|
— the scoped npm name, the flatpak app id, the `.deb` source. It defaults to the
|
|
package name.
|
|
|
|
### Where fzf comes from
|
|
|
|
The picker cannot get fzf from the manifest: it needs fzf to *draw* the
|
|
manifest. So it brings its own copy into `~/.cache/dotup/` and invokes it by
|
|
absolute path. **`PATH` is never modified and `~/.local/bin` is never written**
|
|
— a tool that refuses to tick `invasive` for you has no business quietly
|
|
shadowing your distro's fzf for Ctrl-R and the oh-my-zsh plugin. Your own fzf
|
|
wins whenever it clears the floor; nothing is replaced merely for being old.
|
|
|
|
Floor is 0.44.0, which is exactly where verification stops rather than where
|
|
compatibility breaks: every release from 0.29 up parses every binding used here,
|
|
but cursor-on-reload can only be *measured* from 0.44.1, where fzf's `--listen`
|
|
API began reporting state.
|
|
|
|
### The private tier
|
|
|
|
Ticking a `private` row schedules work, it does not do it — `git`, `chezmoi` and
|
|
`bws` have to exist first, and a password typed at picker time would sit in
|
|
memory through ten minutes of package downloads. So it happens at the end, and
|
|
never without a human:
|
|
|
|
```
|
|
prompt: URL · username · password the address is in no repo
|
|
│
|
|
├── blank ─▶ done. public-only machine. nothing was asked for.
|
|
├── 401 ─▶ say so, leave the machine public-only
|
|
└── 200 ─▶ two-line blob ─┬─ private repo → chezmoi init --apply
|
|
└─ bws token → ~/.config/bitwarden/bws-token (600)
|
|
```
|
|
|
|
The endpoint returns **data, never a script**: two `KEY=VALUE` lines,
|
|
`PRIVATE_REPO_URL=` and `BWS_ACCESS_TOKEN=`. Compromising it discloses two
|
|
revocable read-only credentials rather than executing arbitrary code as you on
|
|
every machine you ever build. The password is spent, never stored, and never
|
|
reaches `argv` — `curl -K -` reads it from stdin, so it never reaches `ps`
|
|
either.
|
|
|
|
The clone URL that comes back carries a read-only token inline, and it gets the
|
|
same treatment. The token is split out into
|
|
`~/.config/dotfiles/private-credentials` (mode 600, git-credential-store
|
|
format); `chezmoi init` receives a clean URL, and the helper is written into the
|
|
clone's own config so a later `chezmoi update` still authenticates. Not printing
|
|
it was never sufficient: `/proc/<pid>/cmdline` is world-readable, so passing the
|
|
credential-bearing URL to `chezmoi init` disclosed the token to every account on
|
|
the box for the length of the clone, and then persisted it in `.git/config`
|
|
until the tree was deleted. The redaction that predates this only ever covered
|
|
the trace line.
|
|
|
|
`dotup --unattended` cannot reach any of this. It has nobody to type a password,
|
|
and it refuses `private` and `invasive` rows outright even if a stale state file
|
|
ticks them.
|
|
|
|
## Tests
|
|
|
|
```sh
|
|
sh .tests/test.sh # here
|
|
sh .tests/test.sh --docker # clean ubuntu:24.04
|
|
DOTUP_TEST_IMAGE=ubuntu:22.04 sh .tests/test.sh --docker
|
|
DOTUP_TEST_IMAGE=debian:12 sh .tests/test.sh --docker
|
|
```
|
|
|
|
101 assertions covering the toggle rule, the `@needs` closure both ways, the risk
|
|
model as invariants rather than prose, match confinement (two-sided: `--exact`
|
|
confines `nvidia` to three rows **and** fuzzy still over-matches, so removing
|
|
`--exact` fails loudly), the fzf preflight, channel resolution on both
|
|
platforms, the installer, and that dotup runs at all as a non-root user with no
|
|
`sudo` on `PATH`.
|
|
|
|
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 single package landing on the machine running the suite.
|
|
|
|
The version spread is the point of the containers — this box runs fzf 0.72,
|
|
24.04 ships 0.44.1, 22.04 ships 0.29 and bookworm 0.38.
|
|
|
|
The macOS half of the manifest has no CI anywhere, so a fake `uname` stands in
|
|
for it. That is not theatre: it caught the apt/brew fallback running in both
|
|
directions, which resolved Linux-only packages to `apt install davfs2` on a
|
|
machine that has never had apt. The fallback is one-directional now.
|
|
|
|
### End to end
|
|
|
|
The suite above installs nothing. `.tests/e2e.sh` does the opposite — a stock
|
|
`ubuntu:24.04` container, the public tier fetched from the anonymous URL, then
|
|
the private tier through a real bootstrap endpoint, then `bws`, then a rendered
|
|
`secrets.zsh`.
|
|
|
|
```sh
|
|
bash .tests/e2e.sh # prompts for endpoint URL, username, password
|
|
```
|
|
|
|
The password is never written to disk and never placed on a command line: it
|
|
reaches the container as `docker exec -e BOOT_PW` with a bare name, which
|
|
inherits from the calling shell. `-e BOOT_PW=…` would put it in docker's own
|
|
argv, and `/proc/<pid>/cmdline` is world-readable — the same bug this repo was
|
|
fixed to stop committing itself.
|
|
|
|
It asserts what the unit suite structurally cannot: that a machine which has
|
|
only ever seen the public repo can authenticate, clone the private tier, and end
|
|
up with **no credential in its logs and none in `.git/config`**, with the token
|
|
in a 600-mode credential file that a later `fetch` still authenticates from.
|
|
|
|
**Without an endpoint password**, `.tests/e2e-mock-endpoint.py` stands in for
|
|
Caddy — 200 with the blob, 401 on bad credentials, and a closed connection on
|
|
any other path, which is what `handle { abort }` does and why a retired route
|
|
reports `000` rather than `404`. Everything downstream stays real. That is how
|
|
the argv fix was verified, and running it that way immediately caught a rotation
|
|
script whose route regex had stopped matching.
|
|
|
|
## Rules this repo lives by
|
|
|
|
- **Zero credentials, forever.** `gitleaks detect` runs over the full history
|
|
with no allowlist. The private repo gets exactly one allowlisted path; this one
|
|
gets none, and that asymmetry is the point.
|
|
- **Grep the files this repo owns, not `$HOME`.** A wholesale scan of a finished
|
|
machine is not a test of this repo: `~/.npm-global` ends up holding an AWS SDK
|
|
whose type definitions contain a literal `AKIA…` and an Anthropic SDK whose
|
|
docs contain `ghp_…`. Scope the scan to `chezmoi managed`, and tighten `sk-` to
|
|
`sk-[A-Za-z0-9_-]{20,}` — the bare substring matches `ask-`, `task-`, `risk-`
|
|
and `disk-`, and one of this manifest's own npm packages is called
|
|
`rpiv-ask-user-question`.
|
|
- **No identity.** No name, email, hostname, private IP or absolute home path.
|
|
If something needs one, it belongs behind a seam.
|
|
- **`.chezmoiignore` is gitignore syntax.** A `#` mid-line becomes part of the
|
|
pattern and silently matches nothing. Every comment goes on its own line.
|
|
- **Nothing invasive without an action of yours.** Not "never", since a `@needs`
|
|
edge can pull one in — but never silently, and never unattended.
|