Installer contract
What an installer built on K does for every way a request can arrive, every machine it can land on, and every way it can end. It reads three things, presence, the world and the product's answer, and everything else follows from them. K's transaction is code and is fixed. This contract wraps it, and is advisory.
Still settling. K ships the transaction, bootstrapStable, the lock and the receipts; the rest is the installer's. The reference implementation is raft-computer-installer. Two shipped installers, Codex's and the current Raft one, are what this page was checked against; see prior art.
The problem
A request comes from the shell, from the application, or from a remote authority. Someone is there to answer a question, or nobody is. The machine has nothing installed, or an installation K manages, or one mid-upgrade, or one that predates K, or one that cannot start. Only the shell can reach a machine whose application is down, and a repair, unlike an upgrade, cannot be rolled back. Most installers answer the common cells of that space and improvise the rest, and the improvised cells are where machines get stuck.
Three things the installer reads
Every run starts by reading three things, in this order, and acts only on what it read. Nothing else is guessed.
- Presence, from the environment and the terminal. Attended means a person can be asked and can see the result. Decided once, before anything else: an explicit
CIor*_NON_INTERACTIVE=1means unattended; otherwise a terminal means attended: stdin or/dev/tty, the console on Windows. It decides who consents and whether anything is asked. - The world, from the installation on disk. Five of them: fresh, nothing installed; adopted, a binary installed before K; managed, a stable slot K keeps; upgrading, unfinished K work in the journal; broken, records K cannot read or cannot settle. Read after unfinished work is settled, so upgrading is never mistaken for anything else. It decides which action runs: install, adopt and upgrade, upgrade, or repair.
- The product's answer, from the product itself, to two questions. Is a service running? Yes when a live process of the installed binary answers the probe, or at least exists. What must the user still do? A step such as "run app login", or nothing; set up means nothing. Asked after the world is read and before anything is stopped, and remembered on disk so recovery answers the same way. Running decides how the readback is taken; the next step decides what happens after an install and what the line says.
Who can do what, where
The interface
What a person or a script sees. Names are the reference implementation's; the shape is the contract.
curl -fsSL https://example.com/install.sh | sh # attended: resolve, show, ask
curl -fsSL https://example.com/install.sh | sh -s -- --channel alpha # attended, another channel
CI=1 curl -fsSL https://example.com/install.sh | sh # unattended: the channel's current release, no questions
curl -fsSL https://example.com/install.sh | sh -s -- --version 2.3.0 --yes # this version, no questions
curl -fsSL https://example.com/install.sh | sh -s -- repair --version 2.3.0 --yes # only a broken machine accepts this
irm https://example.com/install.ps1 | iex # Windows: the same, from PowerShell
| Command | Meaning |
|---|---|
install, upgrade (default) | Bring this machine to one exact version. What that means is decided by the world: install, adopt then upgrade, or upgrade. Broken is refused, or offered repair when attended. |
repair | Ask for repair explicitly. Held unless the machine is broken. |
status | The world and the live service. The last receipt is not a live observation. |
| Option | Meaning |
|---|---|
--version V | The exact version. Default: the channel's current release, resolved through the release authority. |
--channel C | Which channel to resolve. Attended, the resolved version is shown and asked about. |
--yes | Skip the question when attended. Unattended runs never ask; running one is the consent. |
--allow-downgrade | Intend an older target. Without it, an older target is held. This is also how a bad release is left: name the version that worked. |
| Environment | Meaning |
|---|---|
CI, <PRODUCT>_NON_INTERACTIVE=1 | Unattended, whatever the terminal says: no questions. |
<PRODUCT>_HOME | The product's state root. K's state lives under it. |
<PRODUCT>_INSTALL_DIR | Where the binary on PATH is published. |
<PRODUCT>_RELEASE_BASE | The byte store: <version>/manifest.json and the artifacts it names. |
<PRODUCT>_HANDS_ORIGIN | The release authority a channel is resolved through. |
<PRODUCT>_INSTALLER_VERSION, <PRODUCT>_INSTALLER_RELEASE_BASE | Which installer the bootstrap fetches, and from where. The bootstrap pins one; it never resolves "latest". |
Every run prints one line on stdout and exits 0, 1 or 2. What was settled first, if anything, goes to stderr. Details go to the receipt file, not the terminal.
The rules
Request
- Every entry hands the installer the same request: an id, an exact version and recorded consent. It selects nothing else: no code, no commands, no URLs.
- A channel is resolved to one version before anything changes, and the receipt records that version, never the channel. Attended, the person sees the version and answers; unattended, the channel's current release is the target and the invocation is the consent.
- Every run has an operation id, and the same id with the same target replays the first receipt and touches nothing. The id is the installer's: a launcher that must be able to repeat a request names it through the environment, a person never sees it. A new attempt is a new id.
Presence
- Presence is decided once, before anything else, as defined above. Nothing later re-decides it: a run that started attended may ask at any point, a run that started unattended never does.
- An entry may collect consent attended and then run the installer unattended.
self upgradedoes exactly this: it asks in the application, then launches the same bootstrap non-interactively with the version and the consent in the request. - Unattended, nothing prompts. Running the installer is the consent to whatever the machine needs, repair included, and the receipt records who ran it and what was done;
--yesonly matters attended, where it skips the question. Putting the install directory onPATHis part of installing and happens either way; the line says so. Another manager's copy is never removed.
Lock and settle
- One installer per state directory, under K's lock. Another live owner is held, exit 2. Liveness is decided by the lock protocol, never by age.
- Every run finishes unfinished K work first, whatever was asked, and reports what it settled alongside its own outcome. This is recovery, not repair; it needs no network and no consent.
- K's journal and receipts are its working memory for one transaction. Nothing outside that transaction depends on them, so they are never the user's problem: records K cannot read, or cannot settle, make the machine broken, and broken is repaired in the same run. A K that once wrote something a later K cannot read must not strand the machine, and there is no exit code for "come back later".
Worlds
- After settling, the installer reads the machine once: a readable stable slot is managed; a service installed before K is adopted by seeding stable from its running executable; unresolved recovery, a corrupt slot or a dead lock owner is broken; nothing is fresh.
- Managed and adopted are K's transaction, with rollback. Fresh is verify, seed stable, publish, self-check, then the product's first setup when attended: no transaction, no rollback, nothing to restore. Broken is repaired the same way, and the run says so.
- An installation owned by another manager, such as a package manager, is held, exit 2. The line says where it is and how to remove it. The installer never removes it.
- A target older than what is installed is held, exit 2, unless the request says the downgrade is intended. Refusing to downgrade is not success.
The product's answer
- Running is read before anything is stopped, and remembered for recovery. A running service is stopped and must come back as a live service. A service that was not running is not started by an upgrade; the candidate is run for its version instead. The user's choice to have it stopped is kept.
- The next step is read from the product without a running service, so it works on a machine nobody has logged into. Not set up and attended, a fresh install runs first setup and then starts. Not set up and unattended, nothing is started and the line names the step. An upgrade never runs first setup; it repeats the step if the product still names one.
- First setup runs on the terminal, not on stdin: under
curl | shthe shell has consumed the script by the time the installer runs, so stdin is at its end, and the product is given the terminal to ask on. A setup that is declined or fails leaves a usable installation, not a failed one. - Neither answer changes what is verified: bytes, platform, self-reported version, then a process of the new bytes. They change only which process answers, and what the line says.
- The product's own commands are asked first, and the installer decides for itself when they are not enough. A service that does not answer but whose process exists counts as running. A stop that returns while the process lives is finished by the installer: terminate, wait, then kill. The rule is identity, not pid: a process is touched only when its executable is provably the installed binary, never by a bare pid or a name, and the receipt says it was done.
Verify
- The release authority and the byte store are different trust domains. When both describe the target, their sha256 and size must agree before any download.
- Bytes are verified against the manifest, after decompression if compressed, and again after staging. The staged executable must match the host platform and must report the requested version when asked. Any mismatch fails before any change, exit 1.
- Downloads fail before anything changes; recovery needs none. Credentials for a private source come from the environment; attended, missing ones are asked for.
Repair
- Repair is what a broken machine gets, and only a broken machine: a failed upgrade never escalates to it, and on a machine that can be upgraded a repair request is held. Attended, it is asked about once like any other action, naming what will be quarantined. Unattended, running the installer is the consent, as for everything else.
- Settle first. Never under a live lock. Quarantine K's state by one atomic rename; delete nothing; never touch data outside the slots. An unresponsive service is stopped the same way as anywhere else: by the product's command, then by the installer, by identity.
- Install and probe exactly as an upgrade would, then
bootstrapStable, so the next upgrade is a normal transaction. No rollback; the receipt says repaired, never promoted, and names the quarantine, the version and the approver. What repair must keep is the normative repair boundary.
Report
- Success is read back from a process of the new bytes, never inferred. An accepted request, a finished download, a replaced file: none of these is success.
- What the readback reaches depends on what was running. A service that was running is stopped and must come back as a live service reporting the requested version, a pid and a new start id. Where nothing runs, because nobody has set the product up yet, the candidate itself is run and must answer as the requested version; every run is a new process, so the start id is new. The line says which:
Upgraded 2.2.1 → 2.3.0. It is running.againstUpgraded 2.2.1 → 2.3.0. Next: run app login. - Installed means usable, not running: the bytes are in place and answer as the version. Running is claimed only when the service was started and read back, after first setup or because it was running before. The product names the next step when there is one, and the line repeats it.
- Every run ends in one printed line and one exit code: 0 upgraded, installed or up to date, 1 failed or rolled back, 2 not done. The line says what happened, what did not change, and what to do next, in the user's words.
- The line is the same attended and unattended. Only who acts on it differs: a person, or the caller. The installer never retries the same target on its own.
What each outcome prints
| Outcome | The line | Attended adds | Unattended adds |
|---|---|---|---|
| Unfinished work found | An earlier upgrade was interrupted; the previous version was put back. then continue | Both lines are printed. | |
| Channel, not a version | Upgrade 2.2.1 → 2.3.0? [y/N] | Resolve, show, ask; the answer goes into the receipt. | Resolve and proceed. If the authority cannot be reached: exit 1 Could not upgrade: the release server could not be reached. Nothing changed. |
| Up to date 0 | 2.3.0 is already installed. Nothing to do. | ||
| Installed 0 | Installed 2.3.0. Set up and running. | First setup ran on the terminal, then start and readback. | Installed 2.3.0. Next: run app login. |
| Promoted 0 | Upgraded 2.2.1 → 2.3.0. It is running. | Only after the live readback. | Same. Where nothing was running: Upgraded 2.2.1 → 2.3.0. and it stays stopped; the next step follows when the product is not set up. |
| Failed before any change 1 | Could not upgrade: <reason>. Nothing changed; 2.2.1 is still running. | Never retry the same target on its own. | |
| Rolled back 1 | 2.3.0 did not start correctly, so 2.2.1 was put back and is running. Where nothing was running: 2.3.0 did not check out correctly, so 2.2.1 was put back. | Never retry the same target on its own. | |
| Held 2 | Not done: <reason>. Nothing changed. plus what to do, when there is something | Reasons in the user's words: installed by another manager, older than what is installed, declined, another installer running. | The caller decides. No retry. |
| Broken: records K cannot read or settle | Reinstalled 2.3.0. The previous installation was kept at <path>. then as Installed | Ask first: The current installation cannot be recovered (<reason>). Reinstall 2.3.0 fresh? The old installation is kept aside, but this cannot be undone. [y/N] | Reinstall, and say where the old installation went. |
| Same id again | Upgraded 2.2.1 → 2.3.0 earlier. It is running. | No lifecycle effects. | Same. |
The line is in the user's words. Slots, journals, receipts, operation ids and quarantines are how the installer works, not what it says; they live in the receipt file. A path is fine when the user may need it, such as where an old installation was kept.