docs: the install-variant tree, for tomorrow's decision

Enumerates the forty-seven prompts the two scripts actually ask and sorts them
into branches, consents and values — the distinction that decides what a
generated leaf can remove. Four real branches (OS, role, tailnet state, which
half), fourteen consents that let a leaf omit a section entirely, and a set of
values that must stay prompts because baking them in would mean publishing
somebody's hostname.

Names the two things that need deciding rather than deciding them:

Whether a leaf strips dead code or sets constants and calls the base. They are
different artifacts and the plan rests on which one is meant — the first is what
makes it auditable by being short, the second is what keeps it maintainable.

And the combinatorics: 4 OS x 3 roles x 3 tailnet states is 36 leaves before
consents, so the tree cannot be the full product. Publishing a few opinionated
leaves keeps the static-file-anyone-can-diff property; generating on demand does
not, which is the property per-leaf scripts existed for.

Also notes that --unattended and a generated leaf are the same mechanism seen
twice, and that install_config's existing behaviour — keep the user's file when
there is no tty — is the conservatism every unattended answer needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 03:58:32 +00:00
co-authored by Claude Opus 5
parent cd209483e3
commit d7d64cd6d6
+99
View File
@@ -0,0 +1,99 @@
# The install page, and the scripts behind it
**Status: for discussion, 2026-08-13.** Nothing here is built. It exists so tomorrow's conversation
is about real branches rather than sketched ones — every question below is one the scripts already
ask today.
## The shape agreed
- One **source** — the interactive scripts as they are.
- A **build script** that compiles them into single files, because `curl | bash` cannot fetch libs.
- The build emits **one script per leaf** of the question tree, not one script with pre-seeded
answers. A person auditing before running reads only their own path.
- Verification is of the **generator**, once: anyone regenerates the leaves from source and diffs
them against what is published. One thing to trust rather than N.
---
## The questions that actually exist
Forty-seven prompts across the two scripts. Almost none of them should become a branch — the
distinction that matters is:
**A branch** changes which *code* runs. Removing it makes a script genuinely shorter.
**A value** changes a *string*. Removing it makes a script no shorter — it just moves the answer
from a prompt to a constant.
**A consent** is a yes/no about doing a step at all. These are the interesting middle: pre-answering
one lets the build delete the section entirely.
### Branches — these change what code exists
| question | answers | what it eliminates |
| --- | --- | --- |
| operating system | macOS · Debian/Ubuntu · Arch · Fedora | 17 of 26 machine-setup sections on macOS; the whole `case $PM` ladder collapses to one arm |
| machine role | homelab · vps · dev | swap, ballast, earlyoom, sleep/suspend, boot-hang, static addressing — each is role-gated today |
| tailnet | already connected · set one up · none | the entire Tailscale section, its four sub-options and the offscale explanation |
| which half | machine + officer · officer only · machine only | one of the two scripts disappears |
### Consents — pre-answering deletes a section
Docker · fail2ban · unattended-upgrades · Neovim · agent CLIs · shell config · firewall · SSH
hardening · DNS · swap · ballast · earlyoom · inotify · boot-on-start.
Fourteen sections that a leaf script can simply not contain.
### Values — never a branch
Username · install path · git name and email · port · public URL · Postgres connection · timezone ·
locale · LAN CIDR · swap size · swappiness.
These stay as prompts even in a generated script, or arrive as environment variables. Baking them
into a published file would mean publishing somebody's hostname.
---
## Where this collides with `--unattended`
`--unattended` and a generated leaf are the *same mechanism seen twice*: both are "answer these in
advance". The difference is only whether the answer is baked in at build time or supplied at run
time.
Worth deciding tomorrow whether a leaf script is literally `base.sh --unattended` with a header of
constants, or whether the build truly strips the dead branches. The second is what makes it
auditable-by-being-short; the first is what makes it maintainable. **They are not the same artifact,
and the whole plan rests on which one we mean.**
One thing that already exists and should be preserved either way: with no tty, `install_config`
keeps the user's file rather than replacing it. Every unattended answer needs to be conservative in
that same way, and that is a property of each prompt, not of the flag.
---
## The combinatorics
4 OS × 3 roles × 3 tailnet states = **36 leaves** before any consent is considered, and consents
multiply it past anything anyone would publish.
So the tree the install page walks cannot be the full product. Two ways out, to choose between:
1. **Publish a few opinionated leaves** — "Ubuntu VPS, new tailnet", "macOS dev machine", "Ubuntu
homelab, existing tailnet" — and send everything else to the full interactive script.
2. **Generate on demand** — the page composes the leaf when the questions are answered. Stronger, but
the artifact is no longer a static file anyone can diff against the repo, which costs the
verification property the whole design was for.
My inclination is (1), because (2) quietly trades away the thing that made per-leaf scripts worth
building. But it is a real trade and it is yours.
---
## Open, for tomorrow
- Does a leaf strip dead code, or set constants and call the base?
- How many leaves get published, and what happens to the rest?
- Does the install page show the script before running it? It should — that is the moment auditing
is cheap and nobody will do it afterwards.
- The report from `install-report.md` names a script commit. A generated leaf needs to name the
source commit it was generated from, or the report cannot be checked against anything.