Skip to main content

max / alloy

Argue three absences and ship the staples the audit found Settles four positions left open by the 2026-07-29 feature audit. Updates: nothing automatic. No timer is enabled and no metadata is fetched, because a metadata check is an outbound request and the no-phone-home-before-consent principle takes no exception. The one nudge it does allow is local: the console reports how long since the last check, from a stored timestamp, and never whether an update exists. That half is not built yet; stale.rs stores no timestamp. Disk encryption: ratifies what already shipped rather than changing it. LUKS on unless declined, TPM-bound, passphrase plus an eight-word recovery phrase. Records the two things the code does not explain on its own: why enrollment happens after --block-setup tpm2-luks, and why block is ordered direct-first. Stated omissions: argues the graphical polkit agent (pkttyagent is already there), at-spi (shipping the bus with no reader behind it implies a capability nobody has tested), and NetworkManager-openvpn. Ships ripgrep, fd-find, bat, eza and p7zip, closing a contradiction this document was carrying: it argued for zoxide and direnv on the grounds a developer expects them, and shipped no rg. WireGuard needed adapting. NetworkManager-wireguard does not exist in Fedora 43 and neither does the -gnome variant; NetworkManager 1.54 speaks the protocol natively. What was actually missing is key generation, so this ships wireguard-tools for wg and wg-quick. All seven packages verified to resolve together against quay.io/fedora/fedora-bootc:43.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-05 16:16 UTC
Signed with PGP, not checked
Commit: 65e6bc5d8fce318ae5e5fa26cb927ee15dde0a94
Parent: 3b2ea82
2 files changed, +87 insertions, -0 deletions
@@ -512,6 +512,31 @@
512 512 # System introspection
513 513 bottom \
514 514 dua-cli \
515 + # Command-line staples. The 2026-07-29 feature audit caught the
516 + # contradiction: this image ships zoxide and direnv on the argument
517 + # that a developer expects them, and shipped no `rg`. These are the
518 + # rest of that same argument. All four are Rust, which is not the
519 + # reason they are here but does mean they carry no interpreter.
520 + #
521 + # fd-find installs the binary as `fd` on Fedora; the package name is
522 + # the one that differs from the command, which is the only trap in
523 + # this group.
524 + ripgrep \
525 + fd-find \
526 + bat \
527 + eza \
528 + # Archives. p7zip alone handles 7z; p7zip-plugins is what adds rar,
529 + # which is the format a person actually receives and cannot open.
530 + # Splitting them would ship the half nobody hits.
531 + p7zip \
532 + p7zip-plugins \
533 + # WireGuard. NetworkManager 1.54 speaks WireGuard natively, so there
534 + # is no plugin to install and none exists to install: the packages a
535 + # search suggests (NetworkManager-wireguard, and the -gnome variant)
536 + # are not in Fedora 43. What is missing without this package is key
537 + # generation, since `nmcli` imports a config but will not mint one.
538 + # wireguard-tools carries `wg` and `wg-quick`.
539 + wireguard-tools \
515 540 # Hardware health. The base ships nvme-cli, so NVMe wear and SMART
516 541 # were already readable, and nothing else was: no way to read ECC
517 542 # corrected-error counts, no SMART for SATA, no path to a BMC. That
@@ -14,6 +14,20 @@
14 14
15 15 Two things follow that are easier to read here than to discover. Building needs Linux with podman, real disk, and a long wall clock, so there is no path to an Alloy ISO from macOS or Windows. And an installed machine does not fetch updates: the timer stays disabled unless the medium named an update target, which only a development build does.
16 16
17 + ## Updates: **nothing automatic, and nothing checks on your behalf**
18 +
19 + **An installed Alloy never checks whether it is behind.** No timer is enabled, no metadata is fetched, and `alloy setup` has no update row. `alloy update` is the only path, and a person has to think of it. Decided 2026-08-05.
20 +
21 + **The reason is the no-phone-home-before-consent principle, applied without an exception.** It is the same rule that keeps tailscaled opt-in. An installed machine makes no outbound request its owner did not ask for, and update metadata is an outbound request like any other. A metadata-only check is a smaller commitment than an auto-apply, and it was considered separately and still declined: the smaller version of a thing the principle forbids is still the thing.
22 +
23 + **One nudge, and it is computed locally.** The console shows how long it has been since the last check: "last checked 34 days ago", or that it has never been checked. That is a stored timestamp from the last `alloy update` run subtracted from the clock, so it needs no network, contacts nothing, and reveals nothing. It is the one piece of the problem that can be solved without violating the principle, because the machine already knows when you last asked. What it cannot say, and must not imply, is whether an update exists; only that nobody has looked in a while.
24 +
25 + **The remaining cost is real and belongs here rather than in a footnote.** A machine can sit unpatched indefinitely, and the strongest thing it will ever say is that you have not checked recently. It cannot tell you a patch is waiting, because finding that out is the outbound request. Someone who installs Alloy and ignores the staleness line is running whatever they installed, including the security fixes they did not get. That is the price of the principle, and it is charged to the user, so the user is owed a plain statement of it before they install rather than a discovery afterward.
26 +
27 + `bootc-fetch-apply-updates.timer` and `fwupd-refresh.timer` both ship in the base image and neither is enabled by any preset, so this position costs no code. It is what the image already does, now written down. One consequence worth naming: `fwupd-refresh` being off means LVFS firmware metadata is stale too, on Framework hardware, which is the machine class Alloy claims to support. Firmware is checked when a person runs `fwupdmgr`, and not before.
28 +
29 + Rejected: **a staleness line that phones out to say something useful**, which is the same request wearing a local-looking label. The line reports the age of your last check and nothing about the world. **Enabling the metadata check by default**, which is the outbound request the principle guards against. **Auto-apply via `bootc-fetch-apply-updates.timer`**, which additionally stages updates and carries reboot semantics onto a machine whose owner did not consent to either. **Asking once at first boot**, the runner-up: it resolves the tension rather than picking a side, and `alloy setup` already exists as the place opt-in questions get asked, but it buys explicit consent by adding a screen to a first-boot flow that is meant to be short, and the answer it would collect is one `alloy update` already collects every time it is run.
30 +
17 31 ## Compositor
18 32
19 33 **Sway.** Mature, i3-style manual tiling (workspaces plus split/tabbed/stacked containers), Wayland, well-packaged on Fedora. The i3 model is the tiling model Alloy wants: predictable, workspace-based, no infinite scroll. C rather than Rust, accepted as a knowing trade: no mature Rust i3-style tiler exists, and the model outweighs toolkit purity here. The whole sway* ecosystem (swaylock, swayidle, swayosd, swaybar) fits behind it with zero glue.
@@ -221,6 +235,22 @@
221 235
222 236 What the console does instead is notice when the identity is absent and say so, in the shown-and-not-settable idiom the locale row above uses. That covers the part of a fresh install that actually hurts, which is not typing a key but failing to learn you needed one until gopass refuses to decrypt. [Chapter 10](manual/10-the-shell.md) is where the identity lives; [chapter 14](manual/14-troubleshooting.md) is the case where the machine is gone and the age identity and the disk-encryption recovery phrase are both wanted at once.
223 237
238 + ### Disk encryption: **LUKS by default, TPM-bound, with a recovery phrase**
239 +
240 + **Alloy encrypts unless it is told not to.** The installer's encryption step starts ticked. A laptop is the machine most likely to be lost or stolen and the least likely to be encrypted by someone who had to go find the option, so the default carries the decision and declining is the deliberate act. Ratified 2026-08-05; the code shipped before the position was written down, which is the gap this section closes.
241 +
242 + **Bound to the TPM, so an ordinary boot asks for nothing.** `bootc install` is passed `--block-setup tpm2-luks` and the volume unlocks against the TPM's measured state. That is what makes encryption survivable on a machine that reboots often, and what makes a headless build host possible at all: a box with no keyboard cannot answer a passphrase prompt, and an encrypted box that cannot boot unattended is a box nobody encrypts.
243 +
244 + **Two fallbacks, because a TPM binding breaks for legitimate reasons.** A firmware update, a boot-order change or a moved disk all change the measurement and the TPM stops unsealing. The passphrase collected at install is the first fallback. The second is an eight-word recovery phrase generated at install from the BIP-39 English list, 88 bits, shown once. The wordlist is chosen for properties that matter when a phrase is copied off a screen under stress: 2048 words so a word is exactly 11 bits, and every word unique in its first four characters, so a misread cannot silently land on a different valid word.
245 +
246 + **The installer enrolls both slots itself, because bootc leaves them empty.** `--block-setup tpm2-luks` runs `systemd-cryptenroll --wipe-slot=all`, which clears every keyslot it did not create. Enrolling the passphrase and the recovery phrase before that runs would enroll them into slots that are then wiped. The install path therefore enrolls after, and this ordering is the whole of why the code is more than a flag.
247 +
248 + **Declining does not silently encrypt.** `usr/lib/bootc/install/00-alloy.toml` sets `block = ["direct", "tpm2-luks"]` with `direct` first, so an install that passes no `--block-setup` gets an unencrypted disk rather than falling through to the encrypted path. The flag is passed only when the wizard's box is ticked.
249 +
250 + Rejected: **passphrase-only, no TPM**, which is the conventional answer and would mean no unattended reboot, so no headless build host and a laptop that prompts on every boot until its owner turns encryption off. **Off by default**, which is what most distros do and gets most users an unencrypted disk. **A TPM binding with no fallback**, which turns a firmware update into permanent data loss.
251 +
252 + Implementation lives in `crates/alloy/src/install.rs` (`Step::Encryption`) and `crates/alloy/src/recovery.rs`. The user-facing halves are [chapter 3](manual/03-installing.md) and [chapter 14](manual/14-troubleshooting.md).
253 +
224 254 ## Shell
225 255
226 256 **Nushell as the login shell; bash unchanged as `/bin/sh` and `/bin/bash`.** Full rationale, architecture, and interoperability rules in [SHELL.md](SHELL.md).
@@ -251,6 +281,32 @@
251 281
252 282 **The nu integration is Alloy-authored**, at [`usr/share/nushell/vendor/autoload/direnv.nu`](../usr/share/nushell/vendor/autoload/direnv.nu). direnv emits no nu init for the build-time trick used on starship and zoxide to call, so the hook is written against `direnv export json`, a supported export target. It registers a `pre_prompt` hook and a `direnv-reload` command for use by hand after editing an `.envrc`. PATH is applied apart from the rest of the diff, since nushell holds it as a list and direnv reports it colon-joined.
253 283
284 + ## Command-line staples
285 +
286 + **ripgrep, fd-find, bat, eza, and p7zip.** Added 2026-08-05, after the 2026-07-29 feature audit noticed this document arguing against itself: Alloy ships zoxide and direnv on the grounds that a developer expects them to be there, and shipped no `rg`. Either the argument holds or it does not, and it holds. A distro whose thesis is a curated default cannot leave the most-reached-for search tool to the user.
287 +
288 + The package name to know is `fd-find`, which installs the binary as `fd`. It is the only one in the group where the two differ.
289 +
290 + **p7zip comes with `p7zip-plugins`, which is the half that matters.** The base package handles 7z; the plugins package is what adds rar. Rar is the format a person receives rather than chooses, so shipping only the base would cover the case nobody hits and miss the case they do.
291 +
292 + Not shipped, and not an oversight: **the GNU coreutils replacements** (`sd`, `dust`, `procs`, `sk`). Alloy already ships `dua` for disk usage and `bottom` for process monitoring, and the rest are substitutions rather than additions. A curated default is worth something because it is short.
293 +
294 + ## VPN
295 +
296 + **WireGuard, through NetworkManager, with no plugin.** NetworkManager 1.54 speaks WireGuard natively, and the plugin packages a search turns up (`NetworkManager-wireguard` and its `-gnome` variant) do not exist in Fedora 43. What is missing without help is key generation: `nmcli` will import a peer config but will not mint one. `wireguard-tools` supplies `wg` and `wg-quick` and is the whole of what this needed.
297 +
298 + Rejected: **NetworkManager-openvpn**. It exists and it works, and it is still not here. OpenVPN is the protocol a person meets because an employer or a VPN vendor chose it, which makes it a compatibility obligation rather than a pick, and this document does not carry obligations it has not been handed. Anyone who needs it layers the package. The distinction from WireGuard is not preference: WireGuard needed no plugin at all, so shipping it cost one small package and no position.
299 +
300 + **Tailscale is the mesh answer and is separate.** It is opt-in at first boot and stays that way; see the `alloy setup` flow. It does not make WireGuard redundant, since a hand-configured peer and a managed mesh are different jobs.
301 +
302 + ## Accessibility: **not shipped, and that is not the same as supported**
303 +
304 + **at-spi is not in the image, and no screen reader is either.** Shipping the accessibility bus without a reader behind it would be the worse outcome: it would put the plumbing in place, let the capability appear in a feature list, and leave anyone who relies on it discovering at the point of use that nothing reads anything. An honest absence is better than an implied capability.
305 +
306 + The deeper reason is that Alloy is a terminal-first distro, and screen reader support in a TUI is not a package. It is a property of every view the console draws, and Alloy has not done that work or tested it. Claiming otherwise on the strength of one dependency would be a claim about software nobody has run with a reader.
307 +
308 + This is stated rather than argued away. It is a real limit on who can use Alloy, and it stays until someone does the work rather than installs the package.
309 +
254 310 ## Utility defaults: content viewers
255 311
256 312 Alloy ships defaults for the three content types users open constantly. Not shipping them cedes the choice to whatever a user's first web search returns (VLC for video, nomacs for images), which conflicts with the curated-defaults principle everywhere else.
@@ -335,6 +391,12 @@
335 391
336 392 Rejected: clipse (Go, a full TUI where a `cliphist list` pipe suffices), copyq (Qt, retained-mode GUI overkill).
337 393
394 + ### Privilege prompts: **pkttyagent, no graphical agent**
395 +
396 + **Alloy answers polkit prompts in a terminal.** `pkttyagent` ships in the polkit package, so the agent is already present and the Containerfile asserts it rather than assuming it. A graphical agent (`polkit-gnome`, `lxqt-policykit`, `mate-polkit`) is not installed and is not wanted: each is a GTK or Qt tray process running for the length of a session to serve a dialog that appears a few times a month, in a distro whose settings live in a TUI.
397 +
398 + The cost is honest: a GUI app that triggers a polkit action with no agent running gets a failure rather than a prompt. In practice the actions Alloy grants are reached from the console, where `pkttyagent` is exactly right, and `run0` goes through polkit's own PAM stack rather than through an agent at all.
399 +
338 400 ### Screen recorder: **wl-screenrec**
339 401
340 402 Rust, hardware-accelerated (VAAPI), small. CLI-driven, no config file.