Skip to content

Commit 8cd539c

Browse files
docs(adr): ADR-0012 nix bootstrap wiring + ADR-0013 per-user nix.conf (#53)
M4 entry ADRs answering the two architectural questions rolled forward from the M3 retrospective: - ADR-0012: Wire LiveNixInstaller from a dedicated `pearlite bootstrap` subcommand, not auto-phase-0 of `apply`. Apply halts at preflight with NIX_NOT_INSTALLED when nix is missing and any host needs Home Manager. Per-host SHA pin lives in the Nickel config under `nix.installer.expected_sha256`. - ADR-0013: `~/.config/nix/nix.conf` stays HM territory; Pearlite writes only the system-wide `/etc/nix/nix.conf` minimum during bootstrap. No `[[managed.nix_conf]]` block; HM's `nix.settings` is the documented per-user surface. Index updated in docs/adr/README.md. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cba4c11 commit 8cd539c

3 files changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# ADR-0012: Nix bootstrap wiring as a dedicated subcommand
2+
3+
**Date:** 2026-04-29
4+
**Status:** Accepted
5+
**Supersedes:**
6+
**Superseded by:**
7+
8+
## Context
9+
10+
M3 PR #45 shipped `LiveNixInstaller` — the Determinate Nix installer
11+
wrapper governed by ADR-004 — with full hash-pin defence and a
12+
`MockNixInstaller` for tests. The adapter is complete in isolation,
13+
but no place in the engine or CLI invokes it. Without a caller, the
14+
M3 retrospective rolled both the apply-time wiring and
15+
`vm-09-nix-bootstrap.sh` into M4 W1.
16+
17+
Two open questions block the wiring:
18+
19+
1. **Where does the expected SHA-256 come from?** The Determinate
20+
installer ships under a moving URL. ADR-004 mandates the hash-pin;
21+
the question is whose responsibility the pin is. Options:
22+
- Per-host Nickel config: `nix.installer.expected_sha256 = "…"`.
23+
- Top-level repo-wide `bootstrap.toml` next to `state.toml`.
24+
- Baked-in pinned hash refreshed at every Pearlite release.
25+
26+
2. **When does the installer run?** Two shapes:
27+
- Auto phase 0.0 of `pearlite apply` — the installer becomes part
28+
of the seven-phase pipeline, runs implicitly when nix is missing
29+
and any host has `home_manager.enabled = true`.
30+
- Dedicated `pearlite bootstrap` subcommand — runs explicitly,
31+
once, per host. `pearlite apply` halts with a preflight error
32+
if nix is missing.
33+
34+
The risk with auto-phase-0 is the snapper rollback story: the
35+
installer touches `/nix` (and on btrfs hosts, `/nix` lives on its own
36+
subvolume by Determinate's design) plus `/etc/profile.d/nix.sh`,
37+
`/etc/nix/nix.conf`, and a systemd unit. None of those land inside
38+
the snapper subvolume that wraps `/`. A pre-apply snapper snapshot
39+
would not capture the installer's effects, breaking the
40+
"apply is reversible via `pearlite rollback`" mental model embedded
41+
in PRD §11 and CLAUDE.md hard invariant 9.
42+
43+
## Decision
44+
45+
1. **Run the installer from a dedicated `pearlite bootstrap`
46+
subcommand**, not as auto-phase-0 of `apply`. Apply stays a pure
47+
declarative diff-and-execute over snapshotted state; bootstrap is
48+
the explicit, one-shot side-effect on the host that establishes
49+
the prerequisites apply expects.
50+
2. **The expected SHA-256 lives in the per-host Nickel config** under
51+
`nix.installer.expected_sha256`. Rationale: hosts already declare
52+
nix presence (`home_manager.enabled`); the SHA pin sits next to
53+
the consumer that needs it. A central `bootstrap.toml` decouples
54+
the pin from the host that uses it; a baked-in hash forces a
55+
Pearlite release for every Determinate version bump. Per-host is
56+
the right granularity.
57+
3. **`pearlite apply` halts at preflight (exit 2) with
58+
`error.code = NIX_NOT_INSTALLED`** when any host has
59+
`home_manager.enabled = true` and `nix --version` fails. The
60+
`error.hint` is the literal `pearlite bootstrap` command. This
61+
keeps apply pure-declarative; bootstrap is the one-shot
62+
side-effect.
63+
4. **Bootstrap state isn't recorded in `state.toml`.** Nix presence
64+
is a runtime fact (queried via `nix --version`), not a managed
65+
declaration. Re-bootstrapping is a no-op via the `nix --version`
66+
short-circuit already shipped in `LiveNixInstaller`. The
67+
`[[managed.*]]` blocks in state.toml are reserved for things
68+
Pearlite owns and might remove on prune; the nix daemon is
69+
neither.
70+
5. **`vm-09-nix-bootstrap.sh`** exercises the bootstrap command
71+
end-to-end on a clean image (no nix present) and re-runs to
72+
verify the short-circuit. The scenario is gated behind
73+
`PEARLITE_VM_TEST=1` like the other VM scenarios.
74+
75+
## Consequences
76+
77+
- **One new CLI subcommand** (`pearlite bootstrap`) and one new
78+
preflight check in `apply`. The subcommand is `read + write`
79+
capability per CLAUDE.md's tag system; `destructive` would imply a
80+
rollback path the side-effect doesn't have.
81+
- **`LiveNixInstaller` finally has a caller.** Gains an integration
82+
test wired through `pearlite-cli`'s bootstrap renderer.
83+
- **Bootstrap output bypasses snapper.** Consequence of choosing a
84+
dedicated subcommand: if the installer corrupts the host, the user
85+
recovers via Determinate's own uninstall path, not
86+
`pearlite rollback`. Documented in the bootstrap subcommand's help
87+
text and in the M4 W1 retro callout.
88+
- **Per-host SHA pin requires per-host edits.** When Determinate
89+
releases a new installer version, every host file needs its
90+
`expected_sha256` bumped. Acceptable — the cadence is "one bump per
91+
Pearlite-relevant Determinate release", which historically runs at
92+
~quarterly intervals. The CLI's error message names the expected
93+
vs. observed SHA verbatim so operators can paste the correct value.
94+
- **No `bootstrap.toml`.** The repo-root config-file count stays at
95+
`state.toml` only. Future state may grow more (e.g. fleet config in
96+
v1.1), but bootstrap doesn't earn a new file.
97+
98+
## References
99+
100+
- ADR-004 — Determinate Nix installer over the official one
101+
(Plan §13.4).
102+
- M3 retrospective `docs/retrospectives/M3.md` §"What didn't" and
103+
§"Actions for M4" item 1.
104+
- PRD §8.2 — seven-phase apply (no bootstrap phase added).
105+
- PRD §11 + CLAUDE.md hard invariant 9 — apply reversibility via
106+
snapper.

docs/adr/0013-per-user-nix-conf.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# ADR-0013: Per-user `nix.conf` is left to Home Manager
2+
3+
**Date:** 2026-04-29
4+
**Status:** Accepted
5+
**Supersedes:**
6+
**Superseded by:**
7+
8+
## Context
9+
10+
Home Manager's `switch` requires `experimental-features =
11+
nix-command flakes` to be enabled at the daemon or per-user level.
12+
Without it, `nix profile`-style invocations that HM uses internally
13+
fail with `error: experimental Nix feature 'nix-command' is disabled`.
14+
15+
Plan §7.4 W1 listed "per-user `nix.conf` handling" alongside the
16+
`runuser` drop wrapper, but M3 only shipped runuser. The M3
17+
retrospective marked nix.conf provisioning `[~]` and called for an
18+
ADR before any code lands.
19+
20+
Three approaches were considered:
21+
22+
1. **Pearlite writes `~/.config/nix/nix.conf`** from a declared
23+
block in the per-host Nickel config — e.g.
24+
`users.alice.nix.conf.experimental_features =
25+
["nix-command", "flakes"]`. Pearlite owns the file; HM doesn't
26+
touch it.
27+
2. **Pearlite leaves `~/.config/nix/nix.conf` alone.** HM already
28+
exposes a `nix.settings` block that compiles down to a per-user
29+
nix.conf; the operator declares `experimental-features` there.
30+
Pearlite establishes the system-wide minimum (`/etc/nix/nix.conf`)
31+
during bootstrap and stays out of the per-user file's way.
32+
3. **Hybrid.** Pearlite writes a baseline `experimental-features =
33+
nix-command flakes` to per-user nix.conf if the file is missing
34+
or doesn't contain that line; otherwise leaves it alone.
35+
36+
The hybrid option's "if missing" branch is the kind of magic that
37+
breaks operator mental model: HM's `nix.settings` block silently
38+
loses ground to a Pearlite-owned line that the operator didn't
39+
declare. Ownership questions emerge: if HM and Pearlite both want to
40+
write the same key, which wins? Three-way merges of generated config
41+
are exactly the muddle ADR-001 (Nickel for human config, TOML for
42+
machine state) was written to avoid.
43+
44+
## Decision
45+
46+
**Adopt option 2: Pearlite does not manage per-user nix.conf.**
47+
48+
Concretely:
49+
50+
1. **`pearlite bootstrap` (per ADR-0012) writes
51+
`/etc/nix/nix.conf`** with `experimental-features = nix-command
52+
flakes` and `auto-optimise-store = true` (the Determinate
53+
defaults). This is the system-wide minimum that lets HM's first
54+
`switch` succeed. The write is idempotent: if the keys are
55+
already present and match, the file isn't touched.
56+
2. **`~/.config/nix/nix.conf` is HM territory.** Operators who want
57+
per-user overrides declare them in the HM `home.nix` via the
58+
`nix.settings` Nickel block. HM compiles that into the per-user
59+
file; the file's contents are owned by HM's generation, not by
60+
Pearlite.
61+
3. **No `[[managed.nix_conf]]` block in `state.toml`.** Per-user
62+
nix.conf isn't a Pearlite-managed file. The system-wide
63+
`/etc/nix/nix.conf` is touched only by `bootstrap`, which
64+
doesn't record state (per ADR-0012 decision 4). If Pearlite later
65+
needs to verify the system-wide file has its expected contents,
66+
that's a preflight concern, not a `state.toml` row.
67+
4. **The host config's documentation**`docs/book/host-config.md`
68+
when it lands in M6 — points operators at HM's `nix.settings`
69+
for any per-user nix configuration questions. The cross-reference
70+
is "if you want to set a nix.conf option for user X, declare it
71+
in their HM config; Pearlite does not provide a separate path."
72+
73+
## Consequences
74+
75+
- **No new managed file.** `state.toml`'s `[[managed.*]]` blocks
76+
stay at the M3 set: pacman, cargo, services, config_files,
77+
user_env. The config-file inventory in PRD §7.3 is unchanged.
78+
- **One write site for `/etc/nix/nix.conf`.** Inside
79+
`pearlite bootstrap`. Idempotent (read-then-write-if-different),
80+
no `[[managed.config_files]]` entry, no diff-time involvement.
81+
- **HM's `nix.settings` is the documented per-user surface.**
82+
Pearlite's docs, error hints, and bootstrap output all cross-link
83+
to the HM option. Operators who try to declare a Pearlite-side
84+
per-user nix block get an explicit error pointing at HM.
85+
- **No competing source of truth.** The "two systems writing the
86+
same file" problem (HM and Pearlite both managing
87+
`~/.config/nix/nix.conf`) is structurally eliminated. The
88+
`experimental-features` line that bootstrap establishes
89+
system-wide is sufficient for HM's first switch; per-user
90+
refinements happen exclusively through HM.
91+
- **No fleet-wide knob for per-user nix settings.** v1.0 ships
92+
without this. v1.1's fleet mode (PRD §17.1) may revisit if real
93+
operations show a need; that revision supersedes this ADR.
94+
- **Cost of holding.** Operators who prefer to manage everything
95+
through Pearlite's host file (rather than HM's `home.nix`) lose
96+
one knob. That preference is a v1.1+ topic; v1.0 commits to the
97+
Nickel-host-file + HM-home.nix split as the canonical division.
98+
99+
## References
100+
101+
- ADR-001 — Nickel for human config, TOML for machine state
102+
(Plan §13.1).
103+
- ADR-0012 — Nix bootstrap wiring (this ADR's bootstrap-side
104+
partner).
105+
- M3 retrospective `docs/retrospectives/M3.md` §"What didn't"
106+
and §"Actions for M4" item 3.
107+
- Plan §7.4 W1 — original "per-user nix.conf" task.
108+
- Home Manager docs — `nix.settings` option (linked from M6 book
109+
chapter when written).

docs/adr/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ under Plan §9.6 but not gating any milestone exit.
2828
| [ADR-0009](./0009-cachyos-ci-runner.md) | CachyOS-fidelity CI runner choice | M2 W1 |
2929
| [ADR-0010](./0010-plan-file-schema-stability.md) | `--plan-file` schema-stability rules | M2 W3 |
3030
| [ADR-0011](./0011-prune-drift-threshold.md) | `apply --prune` drift threshold | M2 W3 |
31+
| [ADR-0012](./0012-nix-bootstrap-wiring.md) | Nix bootstrap wiring as a dedicated subcommand | M4 W1 |
32+
| [ADR-0013](./0013-per-user-nix-conf.md) | Per-user `nix.conf` is left to Home Manager | M4 W1 |

0 commit comments

Comments
 (0)