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; product installers that adopt it share these observable behaviors.
This contract defines the shared behavior of product installers built on K. K implements the transaction, bootstrap, lock, receipts and quarantine primitives. Each product supplies its release source, paths, lifecycle commands, health evidence and setup flow; those adapters do not change the outcomes required here. The runnable Rust example demonstrates the K boundary, not a complete installer. Implementation and verification status must be reported separately from this contract.
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.
The model
A person uses an entry. The entry hands the installer a request. The installer serves it with K's transaction and reports back through the entry.
- Entries
- The only things people use: the install script (
install.sh,install.ps1), the product's own commands, such asapp self upgrade, and the product's remote upgrade, requested by a remote authority such as the product's web console. An entry turns what the person asked for into a request. - Request
- The same from every entry: an operation id, an exact version and recorded consent.
- Installer
- What every entry launches, through a bootstrap that fetches and verifies it. It serves one request and exits: it reads presence, settles unfinished work, reads the world and the product's answer, installs, upgrades or repairs, reads the result back, reports and cleans up. It is how entries get their work done, an implementation detail a person never needs to know exists.
- K
- The transaction inside the installer: the lock, the journal, two slots, receipts and quarantine. An upgrade ends promoted, rolled back or unresolved, and the next run settles whatever an interrupted one left.
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
The install script, as a person or a script sees it. Commands and environment names below illustrate product bindings, not interfaces built into the K crate. A product supplies working defaults and documents the options it exposes; it need not expose every installation path or release endpoint as a setting. Fixed product bindings must obey the same rules below, including consent, immutable release selection, verification and recovery.
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
curl -fsSL https://example.com/install.sh | CI=1 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. The world decides what that means: install, adopt then upgrade, upgrade, or, on a broken machine, repair. A healthy machine never receives repair. |
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. |
--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>_HANDS_ORIGIN | The release authority's origin. |
<PRODUCT>_INSTALLER_VERSION | For the product's developers: which installer release the bootstrap fetches. The bootstrap freezes one immutable installer release before it fetches any file; it may resolve a channel once to do so, and every later fetch uses that release. |
The human CLI prints one outcome line on stdout and exits 0, 1, 2 or 3 as defined below. Prompts, progress and what was settled first go to stderr. Details go to the receipt file. An explicit machine interface may emit structured output carrying the same outcome and operation exit code; a service supervisor transport must not turn an unresolved operation into success.
The rules
Voice
- Everything a person reads is about the product, from the bootstrap's first progress line to the outcome line and whatever the product shows for a remote upgrade. It names the product's version and things the person already knows, such as the command they ran, the file on their
PATHand their service. How the work is done stays out of it: the installer and its name, K's slots, journals, receipts and quarantines, release and operation ids, a channel the person did not choose, and codes. A bootstrap printsDownloading app 2.3.0..., not the name and release of the installer it fetches first. A path is shown only when the person needs it to act. - Every outcome other than success carries two things, kept apart: a reason in the user's words and a stable code for software. The receipt records both, and so does any structured output. A line never contains a code; software never parses a line.
- The outcome line says what happened, what did not change and, when there is something to do, one next step the person can run as written. The next step is always an entry, never the installer. The line is the same attended and unattended; only who acts on it differs, a person or the caller.
- When something stands in the way, the line says what it is in the user's terms and that nothing changed. What the request already implies is decided by the entry (see Presence and consent) and never handed back as a chore ("remove it, or set
APP_INSTALL_DIR").
Entries
- An entry finishes what it can on its own, and asks a person to act only when that does not settle it. Its run settles unfinished work before it acts (see Lock and settle), and a run that ends unresolved is recovered and tried once more before the entry reports.
- A product that launched a remote upgrade and finds no terminal receipt for it recovers it on its next start. A request K never started is recorded as failed before any change, because the machine still runs what it ran.
- Beyond that one retry, nothing is tried again on its own. A held, failed or rolled-back request is settled, and trying again is the person's or the caller's decision.
Request
- A request selects nothing beyond its id, version and consent: no code, no commands, no URLs.
- Without
--version, the target is the channel's current release. The channel is resolved to one version through the release authority before anything changes, and the receipt records that version, never the channel. - Every run has an operation id. The same id with the same target replays its retained terminal receipt without installation or service lifecycle effects: a replay is history, and
statusis the live observation. A new attempt is a new id. The id is the installer's: an entry that must be able to repeat a request names it through the environment.
Presence and consent
- Presence is never re-decided: a run that started attended may ask at any point, and a run that started unattended never asks.
- Attended, the run asks once before anything changes, naming what will change: the version, or a repair and what it will set aside. The answer goes into the receipt.
--yesskips the question. - Unattended, nothing prompts. The request is the consent to whatever the machine needs, repair included, and the receipt records who ran it and what was done.
- An entry may collect consent attended and hand the installer an unattended request.
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. - A request carries the consents implied by what the person asked for, and nobody is asked about them. The install script is run by someone who wants the product on this machine, so its request consents to replacing what occupies the product's path (see Worlds). The product's commands and its remote upgrade act on an installation the installer made, so they never meet an occupant. Putting the install directory on
PATHis part of installing, attended or not; the line says so.
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. A dead owner alone is not corruption: its lock is reclaimed by the same protocol, unfinished work is settled, and then the installation is inspected.
- Every invocation,
statusincluded, settles unfinished K work before it selects a new action. Recovery needs no network and no consent. - Recovery that cannot settle leaves the machine broken, and the exit code stays 3 until a real repair succeeds. A failed or declined repair, or a missing network, never hides it behind exit 0, 1 or 2.
Worlds
- The installer reads the machine once: a readable stable slot is managed; a usable binary installed before K is adopted, by verifying and seeding its existing bytes, whether running or stopped; unresolved recovery or corrupt installation state 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 by an install, upgrade or repair request (see Repair), and the line says so;
statusreports it and repairs nothing. - An installation owned by another manager, such as a package manager, is held, exit 2. The line names the manager and where the installation is, and the next step is that manager's own update: that manager is the entry for it. The installer never removes it.
- A fresh install can find the product's path occupied: something the installer did not put there and no manager owns, such as a launcher script, a link, or a program that is not this product. It is almost always an earlier way of running the product. With the request's consent (see Presence and consent), it is moved aside under a name the line gives, never deleted; the install proceeds as fresh, and the line says how to put it back. A request without that consent is held, exit 2, and the line says what is there. A usable binary of the product is not an occupant: it is adopted.
- 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
- What was running comes back; what was not is not started. 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, and 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 the service. Not set up and unattended, nothing is started. An upgrade never runs first setup. Whenever the product still names a step, the line repeats it.
- 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.
- Upgrade does not require the old product to implement the current status or lifecycle interface. Missing or broken old interfaces are a reason for installer-controlled handover, not a prerequisite the user must fix first. The installed executable and its running mode are determined from operating-system identity, and that mode is preserved across interruption. The replacement is verified through its supported contract before success is reported.
- The product's own commands are asked first, with finite deadlines. A stop succeeds when the identified processes have exited, not when the stop command returns zero. The installer decides for itself when those commands are not enough. 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's answer names one immutable release: the exact version, and the size and sha256 of every file it serves, compressed or not. Nothing else describes a release.
- Bytes are verified against that answer, 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. 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 or rolled-back upgrade is already settled and never escalates to it; a repair request on a machine that can be upgraded is held.
- Repair starts settled, and never while another live owner holds the lock. It moves K's damaged state aside by one atomic rename while repair is unresolved, never deletes the only remaining recovery input, and never touches application data. An unresponsive service is stopped as anywhere else: by the product's command, then by the installer, by identity.
- It installs and reads back exactly as an upgrade would, then calls
storage::bootstrap_stable, so the next upgrade is a normal transaction. There is 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.
- A service that was running must come back as a live service reporting the requested version, a pid and a new start id. Where nothing was running, the candidate itself is run and must answer as the requested version; every run is a new process, so its 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.
- Every run has one operation exit code: 0 upgraded, installed, repaired or up to date; 1 failed or rolled back, with recovery settled; 2 held before a new action; 3 recovery unresolved.
Cleanup
- Cleanup is the installer's. After a durable terminal receipt, and never before a repair's replacement is verified, it removes what it no longer needs: obsolete installer-owned executable copies, download caches and temporary quarantine directories. It keeps the current installation and only the small records needed for replay and attribution, not complete old installations for hypothetical debugging.
- Keeping a receipt does not require keeping its executable payload. This contract introduces no receipt expiry, and deleting a record never lets its request execute again.
- Like recovery, cleanup keeps no record of its own. The next invocation derives what is outstanding from the terminal receipt, the active operation and K's own state, even if no cleanup was ever recorded, and retries it silently; a replay of the same id may retry it too.
- Cleanup never causes a reinstall, a restart or a question. Its failure changes no verified result and asks no one to act. It never cleans up another active operation, and never deletes product credentials, attachments, configuration or user files.
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 | Resolved to one version first. If the release authority cannot be reached, exit 1: Could not upgrade: the release server could not be reached. Nothing changed. | Show and ask: Upgrade 2.2.1 → 2.3.0? [y/N] The answer goes into the receipt. | The channel's current release, without a question. |
| 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. |
| Fresh, path occupied 0 | Installed 2.3.0. The previous ~/.local/bin/app is now ~/.local/bin/app.previous; to put it back, move it over ~/.local/bin/app. | No question about it: the install script's request consents. | Same. |
| Promoted 0 | Upgraded 2.2.1 → 2.3.0. It is running. only after the live readback. 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 the next step, when there is one. Reasons in the user's words: installed by another manager, older than what is installed, declined, another install or upgrade in progress, something at the product's path the request does not consent to replace (~/.local/bin/app is a script that runs app from ~/src/app). | The caller decides. No retry. | |
| Broken: records K cannot read or settle | Reinstalled 2.3.0. then as Installed | Ask first: The current installation cannot be recovered (<reason>). Reinstall 2.3.0 fresh? This replaces the unrecoverable installation; application data is preserved. [y/N] | No question: the request is the consent. |
| Recovery unresolved 3 | The interrupted upgrade could not be recovered. Run the install command again to repair it. | Repair may proceed once the person agrees, as for Broken; exit 3 if it does not succeed. | Repair may proceed without a question; exit 3 if it does not succeed. |
| Same id again | Upgraded 2.2.1 → 2.3.0 earlier. Current service health was not checked. | No lifecycle effects. | Same. |