kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 4d24e95c01735144146862e1c467e1bd0372d4a1
parent 3a404e67e47af514fb892a82c20393ff6b936005
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 11:51:38 -0700

doc: add SELFDIST.md self-distribution plan

Design for kit distributing itself: CalVer versioning in a VERSION file
(kit --version), make dist -> signed per-host fat .kpkg + .tar.gz via
kit's own pkg/cas, a signed kit-release channel index carrying per-artifact
mirror URLs, and an offline-first kit update with a pluggable curl/wget
fetch hook and an atomic single-root install layout.

Single per-user root ($KIT_HOME, default $XDG_DATA_HOME/kit) holds
everything in subdirs: versions/, current pointer, bin/ PATH links,
config/ (incl. trusted_keys), sysroots/, cache/. Embedded release-key set
with overlap-window rotation; libkit owns index format + CalVer compare,
driver owns policy. Indexed in doc/plan/README.md.

Diffstat:
Mdoc/plan/README.md | 1+
Adoc/plan/SELFDIST.md | 528+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 529 insertions(+), 0 deletions(-)

diff --git a/doc/plan/README.md b/doc/plan/README.md @@ -18,6 +18,7 @@ shrinks to whatever remains open (and is deleted once nothing remains open). | [ARM32.md](ARM32.md) | 32-bit ARM (`arm-none-eabi`, ARMv7-M/ARMv7E-M Thumb-2, Cortex-M3/M4/M7) freestanding backend: Phase 1 (walking skeleton) is landed; Phase 2 tracks the remaining ops, the -O1 known-frame path, 64-bit, atomics, TLS, and the `qemu-system-arm` cross-test lane. | [../ARCH.md](../ARCH.md), [../PORT.md](../PORT.md) | | [MCU.md](MCU.md) | Microcontroller development workflow for STM32 Cortex-M (ARMv7-M/E-M) and Espressif RISC-V (ESP32-C/H/P): the last-mile gaps over the working backends — `objcopy` ihex/srec, reusable startup/vector-table + starter linker script, the rv32 soft-float-default footgun, SDK interop, and on-target debug. Assessment stage; references ARM32.md for ARM-core follow-ons. ARMv6-M/Cortex-M0 and Xtensa are out of scope. | [ARM32.md](ARM32.md), [../RUNTIME.md](../RUNTIME.md), [LINKER.md](LINKER.md) | | [SYSROOTS.md](SYSROOTS.md) | Cross-compile sysroot packaging: minimal per-target stubs/headers/CRT objects distributed via `.kpkg` for the support set. Design complete, implementation not yet started. | — | +| [SELFDIST.md](SELFDIST.md) | Self-distribution: versioning kit (CalVer in `VERSION`, `kit --version`), `make dist` → signed per-host `.kpkg` + `.tar.gz` via kit's own pkg/cas, a signed channel index, and `kit update` (offline-first, pluggable curl/wget fetch, atomic versioned install root). Design, not yet built. | [../DISTRIBUTE.md](../DISTRIBUTE.md) | | [BUILD.md](BUILD.md) | A new content-addressed build coordinator (Bazel/Nix-style incremental builds layered on the CAS) — storage state machine, caching algorithm, recipe protocol. Design, not yet built. Distinct from `../BUILD.md` (kit's own Makefile build). | — (new subsystem) | | [TODO.md](TODO.md) | Open deferred fixes and code smells, plus terse backlog folded from retired plan docs (arch-backend parity, Wasm object backend, Windows x64 self-host, bootstrap breadth). Completed items are removed instead of checked off. A current backlog, not a roadmap. | — | diff --git a/doc/plan/SELFDIST.md b/doc/plan/SELFDIST.md @@ -0,0 +1,528 @@ +# Self-distribution (kit ships kit) + +This is the design and implementation plan for distributing the **kit toolchain +itself** — versioning it, building signed release artifacts for every supported +host, and updating an installed kit in place. It is the detailed design behind +the "release packaging: version, …, `kit install` default tool set" line in +[RELEASE.md](RELEASE.md). All design decisions below are settled; the remaining +work is implementation (see *Work items*). + +It deliberately reuses the signed content-addressed packaging subsystem already +in the tree (see [../DISTRIBUTE.md](../DISTRIBUTE.md)) rather than inventing a +second mechanism: a kit release is *just a signed kit package* whose payload +happens to be the toolchain. The asymmetry today is that `kit pkg` / `kit cas` +package **user** code, while kit's own `make dist` is an unsigned, unversioned, +native-host-only `tar czf` (`mk/dist.mk`). This plan closes that gap by +dogfooding the package pipeline onto kit's own release. + +## Goals + +- A single canonical **version** for kit, embedded in the binary + (`kit --version`) and carried as the package version of every release + artifact, with one committed source of truth. +- `make dist` (and a multi-target release driver) produce, per host triple, a + **signed** native `.kpkg` *and* a **signed** portable `.tar.gz`, both built + with kit's own pkg/cas tooling. +- A running kit can **verify and install** a newer kit with one command, against + a trust anchor it already carries, with no new trusted-byte surface beyond the + existing minisign/Ed25519 path. +- First acquisition (no kit yet) is verifiable with stock `minisign` and a + published public key — no chicken-and-egg. +- Reproducibility and the self-host byte-identity gate are preserved: nothing in + a release artifact depends on wall-clock time or build host. + +## Non-goals / deferred to later milestones + +- A built-in TLS/HTTP stack in libkit. Transport is untrusted; integrity comes + from the signature, so the first fetch implementation delegates to + `curl`/`wget`. A native minimal HTTP/1.1 fetcher can replace the hook later + (still no TLS needed). +- A dependency solver or multi-package release graph. A kit release is one + package; sysroots ([SYSROOTS.md](SYSROOTS.md)) are separate packages. +- Background/automatic updates or a daemon. `kit update` is explicit. +- Delta/binary-diff and thin/external-CAS packages. v1 ships **fat** per-host + packages; the thin shape + per-blob delta updates are a later evolution the + format already accommodates. +- `beta` / `nightly` channels. v1 is `stable` only; the index format reserves a + `channel` field so adding them is non-breaking. +- Apple Developer ID notarization. v1 documents quarantine removal (see + *macOS Gatekeeper*); notarization is a later milestone. + +## Relationship to existing subsystems + +| Concern | Where it lives | This plan | +|---|---|---| +| Content store, blobs, trees, signing, `.kpkg`/`.tar.gz` | [../DISTRIBUTE.md](../DISTRIBUTE.md), `src/dist/`, `include/kit/{cas,package}.h` | reused unchanged as the mechanism | +| Per-host build / cross-compile matrix | `scripts/hosted.sh`, [../PORT.md](../PORT.md) | drives the multi-target release build | +| Cross-compile sysroots (needed to build kit for a foreign host) | [SYSROOTS.md](SYSROOTS.md) | a build-time dependency of the release pipeline; its `~/.kit/sysroots` path becomes `$KIT_HOME/sysroots` under the single-root layout below | +| Release punchlist | [RELEASE.md](RELEASE.md) | this is the design for its "version/packaging" item | +| Install layout / tool symlinks | `driver/cmd/install.c` | extended with a versioned single-root install (`$KIT_HOME/bin` links) | + +libkit stays the **mechanism** (verify, unpack, materialize, hash, sign, plus +the new index format + version compare); the driver owns the **policy** (version +semantics, channel selection, trust anchor, fetch, atomic swap). This matches +the existing split where "argument parsing and trusted-keys path/pin policy stay +in the driver" (DISTRIBUTE.md). + +## Versioning + +### Source of truth: CalVer in `VERSION` + +A committed top-level `VERSION` file holds a CalVer string `YYYY.MINOR.PATCH`: + +``` +2026.6.0 +``` + +- `YYYY` — calendar year of the release series. +- `MINOR` — release within the year, incremented per shipped release. +- `PATCH` — fix release on a given `YYYY.MINOR`. + +CalVer signals cadence over API-compat promises, which fits a pre-1.0 +self-hosting toolchain better than semver. The file is the single source of +truth; nothing computes the version from git tags. (A matching annotated git tag +`v2026.6.0` is still created at release time for provenance, but it is *derived +from* `VERSION`, not the other way around.) + +### Build id and `kit --version` + +`kit --version` prints the version plus provenance: + +``` +$ kit --version +kit 2026.6.0 (a3f91c2, aarch64-macos) +``` + +- `2026.6.0` — contents of `VERSION`. +- `a3f91c2` — short git commit hash (the **build id**), injected at build time + (`unknown` / `<hash>-dirty` when git state is unavailable or dirty). +- `aarch64-macos` — the host triple this binary targets, known at compile time. + +Mechanism: the Makefile reads `VERSION` and the git short hash once and passes +them as `-DKIT_VERSION_STR=…` / `-DKIT_BUILD_ID=…` to a single small translation +unit `driver/version.c` exposing `kit_version_string()`, `kit_build_id()`, and +`kit_host_triple()`. Only that TU recompiles when `VERSION` or the commit +changes, so version churn does not rebuild the world. + +### `--version` everywhere + +`--version` / `version` routing slots into `kit_driver_main_ex` +(`driver/main.c`) alongside the existing `--help` / `help <tool>` handling. Every +**tool entry point** answers `--version` with the same banner, prefixed by the +tool name, so toolchain probes like `cc --version` work uniformly: + +``` +$ cc --version +kit cc 2026.6.0 (a3f91c2, aarch64-macos) +$ ld --version +kit ld 2026.6.0 (a3f91c2, aarch64-macos) +``` + +Compiler identity for configure scripts comes from predefined macros, not this +string, so a clear kit banner is correct and not a compatibility risk. + +### Reproducibility contract + +The self-host byte-identity gate compiles kit with kit from one tree and +compares stage2 vs stage3. To keep it intact: + +- The version comes from a **committed** constant, so both stages embed the same + bytes. +- The build id is injected **once** per `make` invocation (from `git rev-parse`) + and passed identically to every stage, so it does not differ stage-to-stage. +- **No** `__DATE__` / `__TIME__` anywhere in the binary. If a build date is ever + embedded it must honor `SOURCE_DATE_EPOCH`. +- Release builds assert a clean git tree. + +### Where the version flows + +`VERSION` → (1) the binary's `--version`; (2) the +channel index entries; (3) the git tag at release time. + +## Release artifacts (`make dist` → signed packages) + +### One package model, per-host fat artifacts + +A kit release is a package named `kit`, version `= cat VERSION`, produced **once +per host triple** as a self-contained **fat** package (everything embedded — no +external CAS). The package manifest's per-`[output]` `target` triple records the +host; the native host's output is flagged `default`. (The `kit-package 3` +manifest already models per-output target triples, a default flag, and +`exe`/`lib`/`data` artifact overlays — DISTRIBUTE.md — so no format change is +needed.) + +Per-host fat packages keep the model simple: each download is self-contained, +`kit update <file.kpkg>` works fully offline, and the download is only the +user's own host. Common headers/rt-source are duplicated across the ~dozen +server-side artifacts, but that storage is cheap; the thin/external-CAS shape +(cross-host dedup, delta updates) is a later evolution. + +Staging tree (what gets hashed into the package), aligned with today's +`mk/dist.mk` and `kit install`'s expectations: + +``` +kit/ + bin/kit the multi-call binary (exe) + lib/libkit.a static library (lib) + include/ public headers (data) + support/rt/include/ freestanding + kit rt headers (data) + support/rt/lib/ kit rt sources (data) + VERSION the release version (data) + README.txt, LICENSE.txt, NOTICE.txt (data) +``` + +### Both formats, both signed + +For each host triple the pipeline emits, all signed by the release key: + +``` +build/dist/ + kit-2026.6.0-aarch64-macos.kpkg native fat, signed (manifest+descriptor) + kit-2026.6.0-aarch64-macos.tar.gz portable, signed (embedded manifest.minisig) + kit-2026.6.0-aarch64-macos.tar.gz.minisig detached sig over the tarball bytes (bootstrap) + …one set per triple… + stable.index signed channel index (see below) + stable.index.minisig +``` + +- The `.kpkg` is the canonical update unit. +- The `.tar.gz` already carries the signed manifest internally + (`kit/package.manifest.minisig`), so it is signed by construction. The + **additional detached `.tar.gz.minisig`** over the whole archive lets a user + with only stock `minisign` (and no kit) verify before unpacking — the bootstrap + trust path. +- Both representations verify the same logical package/tree/blob content + (DISTRIBUTE.md determinism invariants), so a user may acquire either and end up + bit-identical. + +### `make dist` vs the release pipeline + +Two layers, so the everyday target needs no secret key: + +- **`make dist`** (rework of `mk/dist.mk`): native host only. Builds + `RELEASE=1`, stages the tree, and runs the in-tree pkg pipeline to emit the + signed `.kpkg` + `.tar.gz` + detached `.tar.gz.minisig` for the host triple. + Signing key is `KIT_SIGN_KEY=<path>` if set; **otherwise it signs with a + clearly-labeled, in-tree NON-RELEASE test keypair** (the same key the + end-to-end tests use) and prints a loud warning. Artifacts are therefore always + verifiable, and the full `make dist → verify → update` pipeline runs + hermetically in CI. +- **`scripts/release.sh`** (new): the full matrix. For each hosted triple in + `scripts/hosted.sh`'s support set it cross-builds the kit binary and the + per-target rt archives (leaning on the existing `kit-cross` / `windows_cross` + / bootstrap machinery and, for foreign libc, the sysroots of + [SYSROOTS.md](SYSROOTS.md)), stages, packages, and signs each with the **real** + release key (held in CI/secret, never in-tree), then emits and signs the + channel index. Freestanding targets have no host to run on and produce no kit + release artifact; the release set is the **hosted** triples only. + +Artifacts are named by the `scripts/hosted.sh` selector triples (e.g. +`aarch64-macos`, `x86_64-linux-gnu`) for one consistent naming scheme across the +build matrix, the index, and the file names. + +## Hosting and the mirror model + +Hosting is **not** a single fixed location. The signed channel index identifies +each artifact by its **content id** (package-id) and carries an *ordered list of +mirror URLs* for it; the fetcher tries them in order and accepts bytes only after +they match the signed content id and signature. Because identity is the hash, it +does not matter who serves the bytes — GitHub Releases, a CDN, an S3/R2 bucket, +or a private mirror are all interchangeable, and any subset can be listed. + +For v1 fat packages the "blob" is the whole `.kpkg` file. The same mechanism +generalizes to per-blob/per-chunk mirror lists when thin packages arrive later — +same principle, finer grain. GitHub Releases is a fine default first mirror +(durable URLs, free, signatures make its trust irrelevant), but the design pins +no host. + +## Manifest, signing, and trust anchor + +### The release key set + +Release artifacts are signed with a dedicated minisign (Ed25519) keypair whose +secret is held in CI/secret and never enters the tree. The binary embeds an +**array of trusted release public keys** (`driver/release_key.c`, +`static const KitReleaseKey kit_release_keys[]`); a signature from **any** +embedded key verifies an update. The public key(s) are also **published** out of +band for the stock-minisign bootstrap path. + +Verifying an update requires (a) the signature validates against an embedded (or +`--key`-supplied) key, **and** (b) the fetched package-id matches the channel +index entry, **and** (c) the version is acceptable per policy (below). The +existing `pkgid=` trusted-comment binding (DISTRIBUTE.md) ties the signature to +exact manifest bytes. + +### Key rotation: overlap windows + +Rotation needs no extra protocol — just overlapping embedded sets: + +``` +release N embeds {A, B}, signed by A → users now trust both A and B +release N+1 signed by B → trusted (users from N hold B) +release N+2 embeds {B, C}, signed by B → introduces C the same way +``` + +A user always validates a new artifact against a key their already-trusted kit +holds. Hard rotation after a key compromise falls back to the published +bootstrap key (re-acquire + verify with stock minisign). A data-driven +(TUF-style) key set in the index was considered and rejected as over-built for +v1. + +### Bootstrap trust (first install) + +No kit yet, so kit cannot verify itself. The published release key + the detached +`.tar.gz.minisig` let the user verify with stock minisign: + +``` +$ minisign -Vm kit-2026.6.0-aarch64-macos.tar.gz -P <published kit release pubkey> +$ tar xzf kit-2026.6.0-aarch64-macos.tar.gz && kit/bin/kit --version +``` + +From then on the installed kit's embedded anchor verifies all future updates. + +## Channel index format + +A small, signed, byte-stable text file per channel (`stable` for v1), in the +same canonical-text + detached-minisig idiom as the package manifest, and emitted +/ parsed by libkit (`src/dist/release.c`). Proposed `kit-release 1`: + +``` +kit-release 1 +channel = stable +version = 2026.6.0 +hash = blake2b-256 + +[host] +target = aarch64-macos +kpkg = <64-hex package-id of the .kpkg> +targz = <64-hex package-id of the .tar.gz> +size = 31457280 +url = https://github.com/<org>/kit/releases/download/v2026.6.0/kit-2026.6.0-aarch64-macos.kpkg +url = https://dl.kit.dev/2026.6.0/kit-2026.6.0-aarch64-macos.kpkg +url = https://mirror.example/kit/2026.6.0/kit-2026.6.0-aarch64-macos.kpkg + +[host] +target = x86_64-linux-gnu +… +``` + +- Signed by the release key; the signature's trusted comment binds the index id, + exactly like packages. +- Repeated `url` lines are an ordered **mirror list** for that host's `.kpkg`; + all are untrusted fetch hints (integrity = signed `kpkg` id + package + signature), so the index may be served over plain HTTP and mirrored freely. +- `kit update` resolves `(channel, host-triple)` → entry, fetches the kpkg from + the first reachable mirror, checks the fetched package-id equals the index's, + then runs full `kit_pkg_verify` against the embedded key. + +## Installation layout (single root) + +Everything kit manages for a user lives under **one self-contained root**, with +each concern in a subdir — kit never scatters files across separate +config/cache/data trees. The root defaults to `$XDG_DATA_HOME/kit` (i.e. +`~/.local/share/kit` when `XDG_DATA_HOME` is unset), so it is a single tree under +the conventional data path rather than a bare dotdir in `$HOME`; `$KIT_HOME` +overrides it outright. On Windows the default is `%LOCALAPPDATA%\kit`. + +``` +$KIT_HOME/ (default $XDG_DATA_HOME/kit ≡ ~/.local/share/kit) + versions/ + 2026.5.3/{bin,lib,include,support,VERSION,…} + 2026.6.0/{bin,lib,include,support,VERSION,…} + current -> versions/2026.6.0 atomic active-toolchain pointer + bin/ PATH links -> ../current/bin/kit + config/ + config tracked channel, retention, prefs + trusted_keys pkg trust anchors (kit pkg trust) + sysroots/ cross-compile sysroots (SYSROOTS.md) + cache/ + downloads/ fetched kpkgs / scratch +``` + +Consolidating `config/` and `cache/` *inside* the root is a deliberate deviation +from XDG's three-root split, in favor of one tree that is trivial to locate, back +up, or delete. The user puts `$KIT_HOME/bin` on `PATH` once (cargo/rustup style); +`kit install <dir>` still lays links into any other directory on demand. + +Two reconciliations of already-shipped/sketched design follow from the single +root: + +- **trusted_keys** moves from `~/.config/kit/trusted_keys` (shipped — see + DISTRIBUTE.md / `src/dist/trust.c`) to `$KIT_HOME/config/trusted_keys`. The + `$KIT_TRUSTED_KEYS` override is still honored, and the legacy path is read when + present so existing setups keep working. +- **sysroots** move from `~/.kit/sysroots` (SYSROOTS.md) to `$KIT_HOME/sysroots`. + +Self-update operates only on this user-writable root. A system-wide / root-owned +install (e.g. under `/usr/local`) is out of scope for self-update v1; there, +`kit update` reports that the install is not user-managed and points to +re-acquisition or the OS package manager. + +## Updating (`kit update`) + +### Flow and atomic swap + +1. Resolve target version (channel index, explicit `--version`, or local file) + and confirm it matches the running host triple. +2. If the target version is already installed under `versions/`, skip to the + flip (offline). Otherwise fetch the `.kpkg` (see fetch model). +3. `kit_pkg_verify` against the embedded (or `--key`) release key; confirm id + matches the index; confirm version policy. +4. `kit_pkg_unpack` into a temp dir under the data root, then rename into + `versions/<new>` (rename within a filesystem is atomic). +5. **Flip `current`** to the new version dir atomically (symlink swap on POSIX). +6. Refresh `$KIT_HOME/bin` links via the existing `kit install` logic so new + tools appear and removed ones go away. + +Because the live binary is reached through `current`, the swap does not mutate +the running image. **All** installed versions are retained by default +(`kit update --prune` removes the non-current ones), so any prior version is an +instant **offline** flip via `kit update --version <X>` or `--rollback`. + +Platform notes: +- **POSIX**: symlink flip is atomic; the running inode stays mapped during a swap. +- **Windows**: no default symlinks and a running `.exe` is locked. `current` is a + junction (or copy); install links are hard links (already the `kit install` + Windows default); a tool already running keeps its handle to the old file. + +### Fetch model: offline-first, pluggable, curl/wget first + +- **Offline-first**: `kit update <file.kpkg>` (or `--from <file>`) verifies and + installs a locally present package with **no network code**. This is the whole + update mechanism minus the download, and ships first. +- **Pluggable fetch hook**: networked `kit update` resolves the index and + downloads through a host fetch vtable in the driver env layer + (`driver/env/`), keeping libkit network-free. The **first implementation shells + out to `curl`/`wget`** (the driver already spawns external processes for + linkers), so kit ships no HTTP/TLS of its own. Because packages are signed, a + native fetcher (later) never needs TLS. + +### Version policy + +Default is **monotonic** for channel-driven updates: `kit update` refuses to move +to a version older than the running one. Intentional moves bypass the guard: +`kit update --version <X>` (flip to an installed version), `--rollback` (flip to +the immediately-previous installed version), and local-file installs are +deliberate and allowed (still fully verified); `--allow-downgrade` permits a +channel-driven downgrade. `--rollback` reverts the **active version only** — +channel tracking is untouched, and the command warns that the next +`kit update` will re-upgrade unless the user pins with `--version`. CalVer +compare is numeric per field (`YYYY`, then `MINOR`, then `PATCH`). + +### Command surface + +``` +kit update # latest on the tracked channel (default: stable) +kit update --version 2026.6.0 # specific version: offline flip if installed, else fetch +kit update <file.kpkg> # offline: verify + install a local package +kit update --from <file.kpkg> # explicit local-file form +kit update --check # report available vs installed; change nothing +kit update --rollback # flip to the previous installed version (warns re-upgrade) +kit update --list # list installed versions, mark current +kit update --prune # remove all non-current installed versions +kit update --key <pubkey> # verify against an explicit key (mirrors/forks) +kit update --allow-downgrade # permit a channel-driven downgrade +kit update --dry-run # print the plan; change nothing +``` + +## macOS Gatekeeper + +A downloaded Mach-O is quarantined and Gatekeeper blocks it on first run unless +notarized. v1 ships **without Apple notarization** and documents the one-time +removal: + +``` +$ xattr -dr com.apple.quarantine kit/bin/kit +$ kit/bin/kit --version +``` + +kit already ad-hoc-signs its own Mach-O output (required to run on Apple +Silicon), so the binary itself is valid code; this is purely the +download-quarantine prompt. Developer ID signing + notarization is a later +milestone once a signing identity exists. + +## Security / threat model + +- **Trust root**: the embedded release public-key set. Compromise of a release + secret key is the catastrophic case; mitigated by overlap-window rotation and + the out-of-band published key for hard recovery. +- **Untrusted transport / mirrors**: any mirror may tamper freely; a forged or + truncated download fails signature or package-id verification and is rejected + before any swap. +- **No partial install**: verification completes before the atomic `current` + flip; a crash mid-update leaves the old `current` intact. +- **Downgrade attack**: defended by the monotonic default on channel updates; an + attacker cannot push an older signed-but-vulnerable kit without an explicit + `--allow-downgrade`/`--version`. +- **Index substitution**: the index is signed and binds package-ids, so a swapped + index cannot redirect to an unsigned or mismatched package. + +## Code boundaries + +- **libkit (mechanism)** — the existing CAS/package/sign/verify/unpack path + (unchanged) plus a new `src/dist/release.c`: `kit-release` index emit/parse and + CalVer compare, byte-stable and hermetically tested in `test/dist` alongside + manifest/tree/kpkg. Surfaced via the dist subsystem (gating consistent with + `KIT_PKG_ENABLED`). +- **driver (policy)** — `driver/version.c` (version/build-id/triple), + `driver/release_key.c` (embedded key set), `driver/cmd/update.c` (channel + selection, install-root management, atomic swap, rollback/prune/list), and the + `driver/env/` fetch hook. Argument parsing and trust/path policy stay here, per + the existing split. + +## Work items + +### Versioning +- [ ] Add top-level `VERSION` (CalVer `YYYY.MINOR.PATCH`). +- [ ] Makefile: read `VERSION` + git short hash; inject `-DKIT_VERSION_STR` / + `-DKIT_BUILD_ID` into a new `driver/version.c` + (`kit_version_string`/`kit_build_id`/`kit_host_triple`). +- [ ] Wire `--version` / `version` into `kit_driver_main_ex` and every tool entry + point (shared `kit <tool> X.Y.Z (build, triple)` banner). +- [ ] Reproducibility: no `__DATE__`/`__TIME__`; build id injected once; + clean-tree assertion in release builds. Confirm the self-host byte-identity + gate still passes. + +### Release artifacts +- [ ] Rework `mk/dist.mk`: stage the tree (binary, libkit.a, headers, rt headers + + per-variant rt archives, `VERSION`, license), then emit signed fat + `.kpkg` + `.tar.gz` + detached `.tar.gz.minisig` for the native triple. + Key from `KIT_SIGN_KEY`, else the in-tree NON-RELEASE test key + loud + warning. +- [ ] Add the in-tree test keypair (clearly labeled) under `test/dist/keys/`; + use it for the hermetic end-to-end test and as the `make dist` fallback. +- [ ] `scripts/release.sh`: cross-build + package + sign every hosted triple in + `scripts/hosted.sh`'s support set with the real release key; emit + sign + the channel index. Document the sysroot dependency for foreign-libc targets. + +### Channel index + trust +- [ ] `src/dist/release.c` + tests: `kit-release 1` emit/parse (mirror `url` + lists) and CalVer compare. +- [ ] `driver/release_key.c`: embedded release public-key set; `--key` override; + overlap-window rotation. Generate the real release keypair offline (secret + kept out of tree) and embed its pubkey; publish it for bootstrap. + +### `kit update` +- [ ] `driver/cmd/update.c` (+ `KIT_TOOL_UPDATE_ENABLED`): offline verify+install + first; then channel/index resolution and the full command surface. +- [ ] Single-root install layout (`$KIT_HOME`, default `$XDG_DATA_HOME/kit`): + `versions/`, `current`, `bin/`, `config/`, `sysroots/`, `cache/`. Unpack → + atomic `current` flip → refresh `$KIT_HOME/bin` links; keep-all retention; + `--rollback` / `--list` / `--prune`; refuse self-update of a + non-user-managed install. +- [ ] Reconcile paths to the single root: `trusted_keys` → + `$KIT_HOME/config/trusted_keys` (honor `$KIT_TRUSTED_KEYS` + legacy + `~/.config/kit/trusted_keys`); update `src/dist/trust.c` + DISTRIBUTE.md. + Note the `$KIT_HOME/sysroots` move for the SYSROOTS.md implementation. +- [ ] Host fetch hook in `driver/env/`; first impl shells to `curl`/`wget`; + signature-gated, transport-agnostic, mirror-list fallback. +- [ ] Monotonic/downgrade policy and host-triple guard. + +### Tests +- [ ] Hermetic: `VERSION` parse + CalVer compare + index emit/parse byte-stability. +- [ ] End-to-end: `make dist` → `kit pkg verify --key test.pub` → + `kit update --key test.pub <file.kpkg>` into a temp `KIT_HOME`; assert + atomic swap, link refresh, offline `--version` flip, `--rollback`, `--prune`. +- [ ] Bootstrap: detached `.tar.gz.minisig` verifies with stock minisign. +- [ ] Tamper: flipped bytes / wrong key / channel-downgrade are all rejected + pre-swap.