Decision 0034
The machine carries what it was built from
Architecture decision record 0034: The machine carries what it was built from
Context
ADR-0033 left the netinstall one step short of installing: the keeper installs a built system,
and on a live image nothing had built one. tacid install --yes refused without --system, and
the installation test handed it the system the test had built on the host, the way an offline
image would. A person with the netinstall on a stick had an installer that could lay out a disk,
write a genome, and then ask for something it had no way to make.
The obvious answer — the daemon runs nix build on the genome, on the live system — raised the
question of what that build reads. The genome’s flake names github:TacidOS/TacidOS as its one
input, and Tacid’s flake names nixpkgs and impermanence as its own. A build that fetches those
is a build that needs the network for its sources, before it needs it for a single package;
it is also a build whose lock file pins revisions the image never saw, so the system a machine
boots into is not, byte for byte, the one the image was built to install. The classic NixOS
image answers this by carrying a copy of the nixpkgs channel — five hundred megabytes for
nixos-install, a tool this image does not run — and an installed machine keeps a channel too.
Decision
Every machine keeps the sources it was built from, and a live image builds a first system from them.
tacid.core.inputsis an attribute set of input name to store path. Tacid’s flake fills it in for every machine it composes:tacidosis the flake’s own source, andnixpkgsandimpermanenceare its inputs. It is rendered into a small document in the store whose references keep those sources in the system’s closure; the daemon reads it asTACID_INPUTS.- On a live image, the daemon answers
genesis.build: it writes the genome it is handed into a fresh directory under its cache, buildsnixosConfigurations.thiswith every input pinned —--override-input tacidos path:…,--override-input tacidos/nixpkgs path:…, and so on — and returns the store path of the system, the lock file Nix wrote, and the end of the log. Then it removes the directory. A pinnedpath:input with its hash is a locked input in Nix’s eyes, so the lock names exactly what was built, and evaluates again with no network. - The lock travels with the answers to
genesis.install, and the keeper writes it beside the genome’sflake.nix. The installed machine’s first evaluation is the one that was built. genesis.buildis refused on an installed machine, whatever asks. A caller that could hand the daemon a flake to evaluate could read, through the evaluator, anything the daemon can read; on a live image there is nothing to read and a person at the console asking. It is refused to the agent’s own principal everywhere, because a first system is built for a person.- The live image switches the installation profile’s channel copy off. What it carries instead is the same nixpkgs, once, as a flake input the build actually uses.
tacid install --yeswithout--systemasks the daemon to build, then the keeper to install; with--systemit installs what it is given, as an offline image would.
The build runs on a thread of its own rather than under the agent’s lock: it is minutes of a process that is not the daemon, and a person watching an installer asks what is happening.
Consequences
- An installed machine’s closure carries about half a gigabyte of nixpkgs source it will read and never compile. That is the price of a machine that can rebuild itself with the network cable out, which the manifesto promises and a lock file full of GitHub references cannot keep: the day a garbage collection removed the sources a lock pointed at, an offline machine could not evaluate its own declaration. A source that is in the closure is a source that is not collected.
- The netinstall image carries the same sources instead of a channel copy, and is no larger for it. An offline image installs with no network at all: the sources are on the image, and every package the built system needs is on the image with it.
tacid updatemoves the genome’stacidospin to a channel revision, and from then on the lock names what the channel says; the new system’stacid.core.inputsnames the sources that build used, and the machine keeps those instead. The pins are for the first build; the channel is for every build after.- The daemon’s build path has been proven against a recording
nixthat asserts the exact argument vector, against a builder that checks what was written for it and where, and against a keeper that writes the lock into the genome. A real evaluation inside the installation test was thought to be an hour of software-emulated Nix, and the test handed the installer a built system for a while and said so. ADR-0037 then measured the machine’s own build at eleven minutes with every path in the store, and the installation test now lets the live image evaluate and assemble the machine’s first system itself, from the genome and the sources it carries: the host computes the same path from the same declaration and holds the machine to it, and the lock the build wrote is on the disk beside the flake. The first netinstall on hardware will be the first with a cache in front of it, and nothing else new. - What a person knows of a body that the installer cannot find out is brought to the genome at
birth:
tacid install --body <file>, given as often as there are files, writes each underbody/and imports it from the owner’s file, so the first evaluation — the one the live image builds and installs — reads it. The installation test needed this before any owner did: its word on the machine (the driver’s door, the virtual firmware, the constant version label) had been declared after the first boot, and three runs of the live-built installation in a row evaluated a genome without it, wanted a system no closure the host had built matched, and set out to compile it from source with no network. A declaration made after birth is a machine whose second generation is the first to differ from its first; brought at birth, the live image’s evaluation is the host’s, derivation for derivation.
Alternatives rejected
- Keep the channel copy and build with it. The channel is the input to
nixos-installandnixos-rebuild, neither of which a Tacid machine runs; the flake’s inputs are what its builds read. Carrying both is the same source twice. - Fetch the sources at build time, on the netinstall. A netinstall has a network; but a lock file that names revisions the image never carried is a first system that is not the one the image was tested to install, and an image that cannot install without GitHub being up.
- Pin only on the image, and let installed machines re-lock from the network. The GC trap above: a machine whose declaration stops evaluating when the network is out and the store has been cleaned, which is the failure this project exists to not have.