Skip to content

feat(pyrite): init as nixos host - #2497

Draft
cameronraysmith wants to merge 146 commits into
mainfrom
pyrite-baremetal-nixos
Draft

feat(pyrite): init as nixos host#2497
cameronraysmith wants to merge 146 commits into
mainfrom
pyrite-baremetal-nixos

Conversation

@cameronraysmith

Copy link
Copy Markdown
Owner

No description provided.

@cameronraysmith cameronraysmith self-assigned this Jul 19, 2026
…tall mechanics

Record that nixos-anywhere 1.13.0 defaults to disko's diskoScript, which composes _legacyDestroy then _create then _mount, closing the open question about the default --disko-mode and removing D8's create-only premise.
Change the pre-wipe primitive from sgdisk --zap-all plus wipefs -a to blkdiscard, since a zapped partition table leaves no lsblk children for disko's disk-deactivate.jq to reach zpool labelclear through, so the ZFS labels inside p2 survive and the next install reuses the old pool.
Add decision record D13 for the install mechanics: the nixos-anywhere version resolution through clan-core's nixpkgs, the four per-phase invocations, the default-phases and --update-hardware-config behavior, and the flake-float exposure.
Add a --phases disko verification step and a blockdev --getss check ahead of the irreversible install, and renumber the Phase 7 steps.
Replace Phase 8's nixos-anywhere source-acquisition task with a hold-the-flake-steady task.
Record the 4Kn dd bs=440 conjecture as unestablished rather than as rationale.
D14 adopts clan-core wifi clanService for wlp2s0 association and commits a dedicated fleet SSID credential as clan vars rather than the household network one.
Both reference clans follow an unstated origination test, committing secrets the repository originates and withholding secrets it borrows, and a household credential published to a public repository fails that test while a fleet-owned SSID passes it, which also makes the service hardcoded share = true correct rather than merely tolerated.
D14 records the counter-evidence rather than omitting it: a clan-core maintainer declines this service on four laptops of pyrite shape, and upstream share = true is undocumented behind a stub test.
D15 states hardware.enableRedistributableFirmware and hardware.cpu.intel.updateMicrocode explicitly, closing the path D5 closure-minimization examples open toward false on the machine only NIC, because the discriminating axis is redistributability rather than freeness.
Task 2.10 hand-written networkd unit is deleted because NetworkManager owns wlp2s0 and forces networking.useDHCP = false.
The claim that nixos-generate-config misdetects this wifi as b43 is struck as false, since nixos-generate-config.pl enumerates 14e4:43a3 explicitly and emits the enableRedistributableFirmware import.
Phase 5 gains a router prerequisite because the fleet SSID must broadcast before clan vars generate prompts for it.
D5 withdrew the skip-the-profile fallback on the ground that i915 reaches initrd only through the profile import chain, and that claim is false.
nixos/modules/module-list.nix:70 imports hardware/facter into every NixOS configuration unconditionally and graphics/default.nix:33 assigns boot.initrd.kernelModules from the report at plain priority, so cinnabar imports no nixos-hardware profile and carries virtio_pci by exactly that route today.
The fallback stays withdrawn on the SPI input path instead, where the profile is the sole supplier, because the facter keyboard module collects from usb_controller alone and this keyboard is SPI; the failure mode is therefore a passphrase prompt that renders and cannot be typed into rather than one that never appears.
The boot.initrd.kernelModules source enumeration is reframed from a closed count to an open set, since it was wrong at four, wrong at five, and an attempt to close it at six surfaced af_packet and zfs as well, and the mkForce invariant is strengthened rather than weakened by the sources not being reliably enumerable.
D16 affirms services.mbpfan.enable with aggressive = false, because mbpfan is gpl3 and in-tree and so sorts opposite to both D5 declines on the redistributability axis D5 itself establishes, and records that the same import also enables services.tlp.
An open risk records that nixos-hardware is absent from flake.lock until task 1.1, so every profile claim is measured at an unpinned upstream HEAD and needs four nix eval checks re-run once the lock is taken.
…or alone

D10 reverses: pyrite takes the tor server role through the unchanged nixos tag selector, so modules/clan/inventory/services/tor.nix is not touched by this change at all.
An onion service makes a machine with no stable address reachable from anywhere it has internet, which is worth more on a laptop behind NAT than on the five cloud hosts that already have a static IP, and a v3 onion address is network-independent by construction.
The not-a-relay refutation is strengthened with the block the earlier citation missed: nixos/modules/services/security/tor.nix:1298-1307 force-disables DirPort, ORPort, and PublishServerDescriptor outside the relay gate, which cinnabar confirms by evaluation.
An earlier revision declined the role and attributed that to a user preference the record does not carry; the question was raised twice, both times mischaracterised as enrolling the machine as a Tor relay, and never answered, so the withdrawal is recorded rather than designed around silently.
The precedent is one clan-core maintainer running this on four NixOS laptops through the identical selector and hand-adding a second onion service, and the decision records that n is one rather than presenting it as a survey.
proposal.md gains its Capabilities section, which could not be written until the wireless and tor questions were both answered; all three capabilities are new because openspec/specs/ does not exist.
Task 4.3 is deleted without renumbering, because design.md D5 and D14 cite tasks 4.7 and 4.4 by number and renumbering would silently falsify them.
…s, add D17

Audio stops being recorded as a binding decision at four sites and is argued on its merits with a revisit trigger in D11 form: the only out-of-tree module carrying the CS8409 amplifier init tops out below this repository pinned kernel, pinning the kernel backward is ruled out, and if mainline lands that init sound arrives on a routine kernel bump at no cost.
The record does not support the disposition the artifacts carried; the one recorded utterance states a firm constraint against pinning the kernel back and a hedged postpone, and the only assent covering audio endorses a framing with a later decision point rather than the permanence the artifacts asserted.
Hibernation loses the same unsourced token at two sites, where deferred alone is accurate and D6 logic is untouched; the three sourced uses naming the re-runnable install path are earned and stay.
D17 records that clan.core.deployment.requireExplicitUpdate is inherited rather than set, because the option is inert under this fleet update practice: its filter lives only in the implicit branch of clan_cli/machines/update.py, updates here name one machine per invocation, and nothing in the repository automates a bare or tag-scoped run that would reach it.
D17 carries a revisit trigger because a tag-scoped run is an implicit one, and records that setting the option fails silently rather than loudly.
Task 5.3 keeps the passphrase recording step and gives it a destination outside the repository plus a verification, because the false clause was no escrow rather than the step itself: the committed var is encrypted to exactly one human key, so the pool is lost only if that key is lost together with every copy held elsewhere, and the recording is what keeps the second term of that conjunction non-empty.
clan vars get appears in 5.3 as the verification and never as the recording, since reading back the var this step exists to duplicate would rest the pool on the single key whose loss is the failure mode.
plan.md's approach prose named Step 7.9 as the wipe-inclusive re-run that
converts "it installed" into "the install path works". Task 7.9 is the
cinnabar /pyrite.zt DNS record, a reversible edit; the re-run is 7.12.

The sentence was correct when written at 8aa678b6 — 7.9 was the re-run
then — and was orphaned when e1caf210 expanded Phase 7 from 9 tasks to 12
and updated plan.md's Task 7 step bodies without following through to the
prose. plan.md:3-4 directs agentic workers to implement from this file, so
the only statement of where the irreversibility proof lives pointed at a
DNS record, inside the one phase that wipes macOS with no fallback.

design.md:269 already says "Task 7.12 re-runs the install"; after this
plan.md and design.md agree.
plan.md's prose block froze at 8aa678b6 while four later commits updated
its task bodies and skipped it. Blame distributes 68 lines to 8aa678b6 and
none of the later commits touch lines 8, 24, or 26; c4ddc2f0 never opened
the file at all.

Drop the D1-D12 range rather than widen it to D1-D17: the range has gone
stale four times in one day, three of those in commits that edited this
very file, and the sentence's only job is to send the reader to design.md,
whose own ### D headings enumerate the set authoritatively. "The open
question" becomes "the open risks", matching design.md's actual section
header and its six subsections.

Replace the Task 8 paragraph, which describes a Phase 8 that no longer
exists: e1caf210 closed that open question (it became D13) and rewrote
plan.md's own Task 8 heading and steps, stranding the paragraph that
described the deleted phase. plan.md:26 currently contradicts plan.md:88.

Add D14's wifi clanService to the Architecture summary and D16's mbpfan
line to Task 2, neither of which reached plan.md.
… from the installed one

The note reads as self-contradicting because it never distinguishes two
different kernels. 7.1.3 is the installer ISO's, which imports
latest-kernel.nix setting boot.kernelPackages = pkgs.linuxPackages_latest.
6.18.x is what an installed pyrite takes from the boot.kernelPackages
default. Both statements are true at once; only the framing was missing.

The pinned default is 6.18.37, not 6.18.38. Resolved the root flake's
nixpkgs outPath via nix eval — node nixpkgs_9, the 26.11pre tarball, not
catppuccin's bare nixpkgs node — and evaluated linuxPackages.kernel.version
there. 6.18.38 is the value in a stale ghq HEAD clone. design.md:110
already carries 6.18.37; the note was the lone outlier.

The conclusion is unaffected: 6.17 < 6.18.37 just as 6.17 < 6.18.38.

The two "binding decision" tokens are not a duplication — :94 declares and
:113 back-references one decision. Anchor :94 to where the decision lives
(design.md:145) rather than reconciling them.
D17 records a considered "set nothing" for
clan.core.deployment.requireExplicitUpdate. Its reasoning is sound and it
stands, but the record does not show the option being put to the user, and
the user's ruling was that it stands — which is not the same as having
chosen it.

D17 itself already says the fleet's omission "is answered by silence, which
is indistinguishable from having been missed". The same standard applies to
D17's own provenance, and the marker makes it honest without weakening the
decision.

The purpose decision is deliberately not marked: it was genuinely ratified
at 2026-07-16T15:37:50Z.
The premise was inverted. linear.yaml's comment, and design.md's open risk,
both assume 48b4123d589b is wrong for being a slug rather than a UUID and
that both forms resolve. Neither half holds.

`linear document list --project <slugId>` returns 9 documents; the same
project's UUID returns 0 nodes with exit 0 — silently. Tested against a
project that has documents, which is what makes the test severe; the
original "both resolve" test used pyrite, which has none, so "resolves to
empty" and "does not resolve" were indistinguishable.

The slugId is therefore correct and the value stays. Reconciling toward the
UUID, as the comment prescribes, would make the archive-time UPSERT title
scan miss, fall through to create, and duplicate every capability document
on every archive.

attempt_log outcome "deferred" is not in the enum (dropped|failed|posted).
The cause was Linear unavailable, which lifecycle.md:39 names as a
dropped-outcome degradation path.
… the user's awareness

"The user chose this knowingly" was not unsourced — the option that was
selected named these costs, and the alternative that lacked them stood
beside it. But sourced is not checkable. A reader holding this change can
verify nothing about anyone's state of mind, because the evidence for it
lives outside the artifacts; the technical comparison the claim rested on
is recoverable from the change's own record.

Both sites now rest on that comparison. ZFS native encryption forfeits a
second key and leaves dataset structure in the clear; LUKS offers keyslots
and metadata encryption, and D1's preceding bullet already records it as
considered and rejected. design.md:160 back-references that bullet, and
spec.md:149 adopts the "it is recorded that" register of its siblings at
147-148, stating a documentation requirement rather than a fact about the
world.

This is a wording repair, not a retraction. The keyslot facts around both
sentences are untouched and D4 is not reopened. design.md and spec.md move
together because striking one would desync design from spec.

"pool layout" stays as an un-attributed ZFS fact: it was never part of the
comparison, and grounding it is out of scope here.
Stock nixos-facter 0.4.4 — the version this repository's pinned nixpkgs
ships — aborts on a MacBookPro14,1 with "unsupported bus type: Spi": Apple
attaches the touchpad over SPI and hwinfo's input scan has no mapping for
that bus, so the scan errors out. That blocks
`clan machines update-hardware-config` on pyrite, the fleet's first
bare-metal machine.

Override the nixpkgs nixos-facter attribute through this repo's overlay
accumulator, adding nix-community/nixos-facter PR #672 (issue #339, commit
4f27becd3b432eabf4f7faca7f14025c69684130) as a source patch. The commit
adds a mouseBusIDs mapping that classifies SPI and other buses instead of
aborting; it is unreleased as of 0.4.4.

Guarded to Linux because the fix branch exposes no darwin package and the
overlay applies fleet-wide, so darwin laptops keep stock 0.4.4. Self-retires
when a nixos-facter release carrying the fix reaches nixpkgs.

Built for x86_64-linux on the magnetite remote builder: the patch fetches
at the recorded hash, applies cleanly, and the package builds.
… task 7.15

The ratified purpose rests on pyrite being reachable for `clan machines
update` over ZeroTier after the install — the path the whole "no console PSK
forever" reasoning depends on. Every other Phase 7 step observes a machine on
the bench; 7.15 is the only one that exercises the path the fleet uses
afterward, run in D17's recorded bare form with no --target-host and no
--build-host.

It is appended after 7.14 rather than inserted because 7.12 destroys and
recreates the pool, so the machine that must be reachable is the one 7.12
leaves behind; the ZeroTier identity and the ssh host key survive that
re-install as committed vars, so 7.9's record and 7.10's entry stay valid
across it.

Severe by construction: it excludes the two ways the check goes green without
proving reachability — a --target-host root@<lan-ip> run that bypasses 7.9 and
the mesh (guarded by confirming getent hosts pyrite.zt returns the fddb:
address), and a same-revision run that moves no closure (guarded by deploying a
revision that changes pyrite's toplevel and confirming readlink
/run/current-system moved). That second guard is invisible on pyrite because
_update_nixos prints no diff where blackphos's darwin-rebuild did.

Adapted from the pre-renumber verified draft; citations re-checked against the
pinned clan-core (update.py:615, :637, :567-569) and the repo
(admins.nix:24-30, the blackphos zero-diff log).
…d framing

A register-only sweep removing five instances of embellished contraposition —
"not X but Y" that invents a belief to correct, strawman negations, and
inflated-reach metaphors. No decision, citation, number, or fact changes.

- proposal.md apple-laptop-hardware-support bullet: drop "rather than an
  improvised recovery"; the concrete USB-C-only trap stays stated in tasks.md
  6.7 and design.md.
- design.md postCreateHook discussion: drop "not a trick"; the
  lib/default.nix:476 and :507-511 citations are untouched.
- proposal.md Why: drop "not a demonstration", "more durably", and "the
  reusable half of the epic, inherited by every future physical machine". The
  terranix-cloud-only context, CAM-31 recon, encrypted-at-rest travel framing,
  and re-runnable-path-is-the-deliverable point all remain.
- design.md overview: drop "which is the more durable of the two"; the
  no-non-terranix-install-path reason is preserved.
- tasks.md 7.15: "the path the fleet uses forever after" becomes "the
  steady-state update path used for all subsequent updates".
… acceptance tasks

plan.md was committed before the graphical-desktop-session work landed, so it
carried no gnome/gdm/desktop/niri tokens while design.md, tasks.md, and
proposal.md all did. plan.md:3 declares itself the task-by-task driver for the
implementing session, so a stale plan would under-scope the desktop.

Five appends, no renumbering, authored from the design.md/tasks.md content:
- Goal: the local graphical desktop session, stock GNOME under GDM proven on
  this hardware from the installer ISO (mirrors Goal 5).
- Architecture: the two system options the host module sets.
- Task 2 Step 4: enable GDM+GNOME, system-level, no home-manager desktop
  config, no plymouth (D11 stands); folded into the host-module options step
  so the routing step stays last. See D19.
- Task 7 Steps 8-10: mirror tasks.md 7.13/7.14/7.15 — the interactive GNOME
  login with is-active necessary-but-not-sufficient, the stage-1 prompt
  unperturbed by GDM, and the steady-state clan machines update as the
  travel-readiness acceptance.
- Out of scope: the niri Wayland session and its shell assembly, deferred to a
  reversible follow-up. See D19.
… the ledger

The T1 readiness gate fired: CAM-32's description was seeded from proposal.md
business content and its state advanced Backlog->Todo, both verified live.

Sync the openspec-linear-sync ledger in proposal.md frontmatter to match:
linear_story_state and last_synced_state to Todo, last_synced_at to the post
timestamp, and a `posted` attempt_log entry appended after the earlier
`dropped` one — so the ledger shows the offline drop followed by the realized
post rather than rewriting it. review_round and max_review_rounds are untouched;
a forward Backlog->Todo bind does not move the review counter.
The runbook jumped from `clan machines install` to post-install
verification, so an operator reading only this page never learned that
stage 1 asks for a FIDO2 PIN and a touch, that YubiKey-A must be seated,
or that a second boot with the token absent is what proves the
passphrase slot opens the container. Task 7.5 requires both paths and
states why: the passphrase is this machine's only recovery credential,
so an unverified fallback is indistinguishable from an absent one until
the token is lost. Carries 7.5's substance onto the page and makes both
boots an explicit precondition of the key-lifecycle section, which
removes YubiKey-A and leaves the passphrase as the sole authorizing
credential.
The runbook ordered the header capture ahead of the second-token
enrollment, inverting the normative SHALL in
specs/encrypted-zfs-root/spec.md — a backup is taken once the
container's keyslots reach their intended state — and the ordering
tasks.md 7.12a states. An operator working top to bottom completed a
whole capture-encrypt-transfer-upload-delete cycle against a two-slot
container and met the correction only as a trailing sentence, so a
restore would silently un-enroll YubiKey-B.

The reorder is within the key-lifecycle section rather than lifting
enrollment out to follow 'Verifying the install' directly: the section
intro establishes $part2 and the root-shell discipline both subsections
depend on, so moving one subsection out would strand it. Section heading
reordered to match. The buried re-take instruction is dropped in favour
of the retention rule already stated, and 7.12a is removed from the
re-take trigger list since it now precedes the first capture. Also adds
the third precondition the enrollment block claimed but never gave: the
first-boot section's two unlock paths, which is the gate tasks.md 7.12a
states.
…it its own criterion

Two defects in the blkdiscard fallback block.

The first: a prior revision dropped 'zpool labelclear -f $disk-part2' on
the ground that under a container p2 holds no ZFS labels to clear. That
is true of a future reinstall and false of the disk in front of the
operator today, which carries a live pre-D1 zroot (7.16). ZFS writes L2
and L3 at the tail of the vdev — vdev_label_offset
(module/zfs/vdev_label.c:163-171) places them in the last 512 KiB, since
vdev_label_t is 256 KiB (include/sys/vdev_impl.h:537-543) and
VDEV_LABEL_END_SIZE is two of them (:561) — so the 32 MiB head overwrite
cannot reach them and whole-disk wipefs does not touch
partition-interior offsets. disko recreates the layout at identical
offsets, zpool.nix:298's import succeeds, and the surviving-pool skip
fires on a fallback that reported success at every step.

Both disk states are served explicitly rather than picking one order
that is wrong half the time: labelclear and luksErase are each a no-op
in the other's state, the states are mutually exclusive, and the text
says so. labelclear goes first because it is the only step reaching the
tail, and its verifying 'zpool import' must run before the zap removes
the part2 node.

The second: the block pointed at the post-wipe verification as its pass
criterion, but arm 1a requires the disk's first 64 MiB to read as
zeroes, which this sequence never produces — it zeroes p2's head, not
the ESP region. A correctly-executed fallback therefore failed its own
criterion. Replaced with a fallback-specific one: zpool import empty,
wipefs -n empty, sgdisk -p announcing new in-memory GPT entries.
…able

The builder-gate rationale said the closure falls back to the installer
ISO; 'Build host behaviour' said it falls back to stibnite's Rosetta
builder. Both were stated unconditionally and they contradict, and the
first was the sole rationale for a hard pre-wipe gate.

Resolved against pinned nixos-anywhere 1.13.0 rather than either
sentence. clan passes no --build-on unless asked
(clan_lib/machines/install.py:193-194), so the default 'auto' applies,
and checkBuildLocally (src/nixos-anywhere.sh:611-652) decides by
attempting a trivial x86_64-linux derivation on the admin box: success
gives 'local', failure gives 'remote', which in nixos-anywhere means the
target machine. Both original sentences describe real outcomes of that
probe under different builder availability, which is why neither is
simply deleted. stibnite's nix-rosetta-builder does advertise
x86_64-linux (modules/machines/darwin/stibnite/default.nix:205-207) but
is stopped by default, so off the mesh with it stopped the fallback is
the ISO. Gate retained, consequence restated, and the reader is told to
start the Rosetta builder rather than reason past the gate.
`jj git push -b` pushes where the bookmark points and does not advance
it. pyrite-baremetal-nixos sits well behind the chain tip, so an
operator running the tree-transfer step as written publishes a revision
predating the review fixes and clones it onto pyrite as the tree the VM
test validates. The disko.nix and flake.lock hashes cannot catch this —
a stale bookmark can carry a matching layout — so a commit-id equality
against `jj log -r @-` is added alongside them as the criterion that
can. Applied to both the runbook and tasks.md, which carried the same
push without the bookmark move.
…in the restore block

The pre-install secrets gate was labelled 'host: stibnite' but written as
a bash heredoc with a trailing 'echo "exit=$?"'. stibnite's interactive
shell is fish, which supports neither <<'SH' nor $?, so the block errors
on paste and the result line reports nothing; in bash it reported the
wrapper's status rather than the gate's. Rewritten as a file the operator
saves and runs with an explicit bash, exit-status line dropped, and the
PASS line named as the sole affirmative signal — the heredoc's one real
service, keeping the loop's 'exit 1' from closing the operator's shell,
is preserved by the saved-script form.

Separately, the header-restore block is labelled as runnable in a rescue
environment but referenced $part2 from an assignment in a different
block on the installed system. Repeated inline, as the enrollment block
already does.
…try check

The runbook states that /dev/nvme0n1 is a kernel enumeration order
rather than an identity, that it resolves on stibnite to a different
disk, and that a measurement taken against it is not evidence about the
disk even when read-only — while tasks.md 7.2 and 7.4 specified exactly
that name. Rewritten to the _1 by-id path. 7.2's recorded result keeps
its factual provenance: the measurement really was taken against
/dev/nvme0n1, so the record says so and the re-confirmation the task
already required is now bound to the by-id path.

The runbook's 'Verifying the install' carried no geometry check at all
though it claims to carry 7.4, so 7.4's 'sgdisk -p' is added as check 1
and the rest renumbered. The keyslot check now uses $part2 like its
siblings rather than the by-partlabel name, with the equivalence stated
so tasks.md 7.6's form is not read as a different device.

Also corrects specs/bare-metal-install-path 'at least one device' to
'exactly one', which the next line of the same scenario, D25, tasks.md
7.2a and the runbook all already said. A reader taking the requirement
text as the criterion proceeds with two tokens seated, --fido2-device=auto
fails to resolve, and disko exits under set -efux after luksFormat.
…d disko comment

disko.nix cited '7.12's reinstall' as the last occasion to influence
root-dataset creation; D29 deleted 7.12 and tasks.md records the
numbering gap where it stood, leaving this comment the last artifact
asserting a second install exists. Now names 7.3's destroy-and-recreate.

The prompt-count reasoning pointed at the 'ykman fido info' gate 'above'
when it is thirty lines below. The revocation block lacked the 'in a root
shell (sudo -i)' annotation every sibling block carries, though the
section states every systemd-cryptenroll call needs root and that
$part2 does not survive a change of shell.

The prerequisites section made the USB-C keyboard a hard gate and said
nothing about the FIDO2 token, though YubiKey-A must be seated through
the whole disko phase and its identity was not established until four
hundred lines later. Named there now, with the reason it cannot wait.

The past-tense APFS observation is scoped to the 2026-07-19 run inside
its own sentence and points forward to the correction, rather than
relying on a reader reaching it fifty lines later.
7.2b carried the same bash-heredoc-plus-$? gate the runbook did, so the
two artifacts would otherwise disagree on how the gate is invoked.
Rewritten to the saved-script form, exit-status line dropped, and the
paragraph crediting the heredoc for shell survival re-attributed to the
saved-script form that now provides it.
…equisite

The runbook's ten PIN mentions all concerned whether a PIN is set on the
token; none concerned the operator knowing it. The prompt lands on stibnite
mid-disko, after the wipe, so an operator who cannot answer it is left with
a formatted container and no fallback OS. Name the PIN beside the token and
the keyboard, state that nothing in the repository records it, and give the
ykman retry count as the budget for wrong entries.
…rst boot around it

clan passes --no-reboot only when asked, so nixos-anywhere runs its default
reboot phase and pyrite reboots itself six seconds after the install phase.
The first-boot section read as if the operator initiated that boot, and told
the operator to remove the installer SSD before powering the machine on --
unperformable, since the ISO is the running root until the reboot fires.
Wording chosen to leave the boot-device outcome undiagnosed rather than
asserting which device the firmware picks: both outcomes get an action.
The runbook asserted an unbounded prompt timeout in three places; design.md
explicitly declines to assert it, recording that systemd-cryptsetup's query
timeout under this configuration was not verified and that boot.zfs.password
Timeout = 0 no longer applies. Scoped the runbook to the design's non-claim
rather than discharging it, since discharging needs the built initrd on the
machine the install has not yet produced. The keyboard gate is unaffected:
it stands harder under a bounded wait than under an infinite one.
…sphrase prompt

The token-absent boot exists solely to prove the only recovery credential,
and it opens with half a minute of silence and then a message that reads as
an abort. An operator who power-cycles there leaves the fallback unproven.
Verified in systemd's src/cryptsetup/cryptsetup.c: the 30 s default token
timeout, the EAGAIN return, and the main loop's clearing of the fido2 auto
flag on retry, which is what raises the passphrase prompt.
…belclear account

spec.md, design.md D8, and tasks.md 6.1 forbade the zpool labelclear the
runbook restored, on the ground that under a LUKS container p2 holds no ZFS
labels to clear. That is true of a later reinstall and false of the disk in
front of the operator, which carries a live pre-D1 zroot with tail labels.
Amended all three to the two-state account: labelclear is the live arm today,
luksErase on a reinstall, and the states are mutually exclusive. Safety of
running both re-derived from zpool_clear_label rather than asserted -- it
skips any label slot that does not unpack to a valid guid and known state,
so on a LUKS-bearing p2 it writes nothing and cannot pre-empt luksErase.
Both still said an off-mesh install falls back to the Rosetta builder,
'always available' and 'does not block'. The runbook establishes the
opposite: the builder is stopped by default, checkBuildLocally's probe
therefore fails, and buildOn=remote puts the build on the just-wiped
installer ISO over WiFi. Read after a failed pre-wipe gate, the old
wording invites exactly the reasoning-past-the-gate the runbook forbids.
Rebind $part2 in the revocation block, which is the block most likely to be
run months later in a fresh shell and was the only one still relying on an
inherited assignment. Drop tasks.md 7.2b's pointer at a heredoc the next
paragraph says was removed. Replace the two stale relative pointers with
positional-independent forms -- the enrollment section by name rather than
'four hundred lines below', and the 32 MiB overwrite by its position in the
fallback block rather than 'below', which the labelclear insertion inverted.
Named forms chosen over corrected distances because this document has been
reordered four times and every positional reference is a latent defect.
… rev

The runbook cited clan_lib/machines/install.py at two different revisions:
the --no-reboot, phase-list, and --build-on citations were keyed to a local
clone's HEAD c266bcd93, while the --extra-files and --disk-encryption-keys
citations were keyed to the flake.lock rev d332b69. Under no single revision
were install.py:162-168 = --extra-files and install.py:162-163 = --no-reboot
both true, so a reader following either citation read the other.

Renumber all five sites to d332b69 and state the convention once in the
provenance paragraph, alongside the existing nixos-anywhere 1.13.0 pin.
…st boot

The reboot phase fires the first boot about six seconds after the install
phase, unattended, and the stage-1 prompt's timeout under this configuration
is unmeasured. The disko phase already requires the operator at the machine;
say that the requirement does not end there.
…harged

design.md stated three pre-install obligations that neither tasks.md nor the
runbook imposed, so all three would have been silently skipped. The first is
covered by task 3.11's VM test and the second is harmless by D24. The third
is accepted as a residual rather than turned into a task: the crypttab
generator produces the unit at boot, so it is not a file in the image and
the directory listing the paragraph assumes does not answer it.
…sites

ykman prints a WARNING and an ERROR line about PC/SC before its real output,
because pcscd runs on neither the installer ISO nor the installed system and
the CCID transport is therefore unavailable. The runbook tells the operator
to treat gates as hard stops, so an ERROR line immediately above the correct
answer invites a false abort on a check that passed. Annotate the pre-wipe
gate and the second-token enrollment, and record the shared AAGUID where the
indistinguishability of the two tokens is already argued.
pyrite was the only machine in the fleet with no explicit
services.openssh.settings.MaxAuthTries, so it took the OpenSSH
compiled-in default of 6, while cinnabar, magnetite, electrum, galena,
scheelite and the four darwin hosts all set 20.

The default has not yet caused a failure because OpenSSH promotes an
identity that is both an IdentityFile and agent-held to the front of the
offer list, and ~/.ssh/id_ed25519 is both: a live ssh -v pyrite.zt
enumerates 19 candidates but sends exactly one "Offering public key"
before "Server accepts key". That promotion stops applying once the key
no longer exists on disk, which is the direction the intended posture
goes: agent-held keys with no physical copy on the machine.

srvos is not the source of this setting on any host; every host that has
it sets it explicitly. The deliberate omission of srvos from pyrite is
unrelated to this change.
@cameronraysmith
cameronraysmith force-pushed the pyrite-baremetal-nixos branch from f786d41 to 02f2bc4 Compare July 30, 2026 19:39
…rator

The committed artifact was minted on 2026-07-18 by the pre-D27 generator,
`xkcdpass --numwords 6 --random-delimiters --case random > $out/key`, and was
never regenerated after nkotuxksspqo (2026-07-19) added `| tr -d "\n"` and the
layout-stable hyphen delimiter. The generator declares no `validation`, so
`validationHash` is null and clan's currency check is omitted entirely; a re-run
treated the stale artifact as current.

Both failure modes D27 identifies were live in the artifact rather than the
script. A trailing newline is handed to `cryptsetup luksAddKey` verbatim, so
slot 1 would have been enrolled as passphrase-plus-newline while
`systemd-ask-password` strips the terminating newline from what is typed --
the slot exists and opens for nobody. Random punctuation additionally may not
map at the initrd console. With `enrollRecovery = false` that slot is the only
non-token credential, and it is the credential 7.12a needs in order to enroll
the second YubiKey at all.

Verified on the regenerated artifact by boolean checks that emit no
secret-derived characters: no trailing newline, hyphen-only delimiters,
five hyphens for six words, all printable ASCII.

Regenerated with `CLAN_NO_COMMIT=1 clan vars generate pyrite --generator zfs
--regenerate`. `--regenerate` is load-bearing: without it `full_closure` is
false, `all_missing_closure` filters out generators that already exist, and the
command reports success while changing nothing. `CLAN_NO_COMMIT=1` returns from
`commit_files` before any git invocation, leaving the development join
untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant