The Index Contract¶
Status: draft (2026-07-05), distilled from ADR-0033 as amended by ADR-0037 (subscribe + import) and ADR-0038 (single-file package), plus ADR-0031/0034/0039 and the 2026-07-04 design session (§11, §13, §16, §20–22). This is contract #2 of the ecosystem: what a shareable, cacheable, offline collection of apps is, and how trust travels with it. Contract #1 is the App contract. The governing principle:
DDIs carry trust. Indexes carry discovery.
Naming of files and directories below is proposed — see Open items.
What an index is¶
An index is a signed catalog plus the payload files it vouches for — a collection of images from whoever, indexed. There is no "repository" in this ecosystem.
- An index is a filesystem layout, not a service. Anything that holds or serves files is one: an HTTPS directory, a USB stick, a local folder, an SSH host, another device's cache. Sneakernet is the same contract as HTTP.
- Identity is the index key, not the URL. The signing key is the index; URLs are just locations of it. The same index may exist at many locations (origin server, mirror, stick, a friend's cache) — all equally authentic.
- Precision rule: "the index" names the whole published thing (catalog + payload); "the index file" names a signed catalog document, when the distinction matters.
Layout (proposed)¶
<index-root>/
index.json ← the manifest: identity, classes, delegation
index.json.p7s ← signature by the index key
keys/
<fingerprint>.crt ← index cert + delegated build certs (PEM)
apps/
<arch>/
catalog.json ← per-arch app catalog (latest only)
catalog.json.p7s
<name>/<version>/
<name>_<version>_<arch>.raw ← the package: ONE file (ADR-0038)
bases/
<arch>/
catalog.json
catalog.json.p7s
<name>/<version>/...
A package is a single .raw (ADR-0038): its metadata is the embedded
manifest, extracted at add time strictly after verification. There is no
standalone metadata file and no .zip.
- Two artifact classes:
apps/andbases/(ADR-0008, layered design). An index may serve either or both; the manifest declares which. - All references inside the index are relative paths. Never absolute URLs — this is what makes a verbatim copy work at any address (refines ADR-0010; the same-origin property becomes structural).
- Versions coexist structurally (
<name>/<version>/); the catalog names exactly one latest per name (UAPI.10 maximum). Retention of old version directories is the index holder's housekeeping, not contract. - Artifact naming per the app contract:
<name>_<version>_<arch>.raw+ metadata.jsonsidecar.
The manifest¶
The root index.json is the index's identity document, signed by the index
key:
| Field | Meaning |
|---|---|
spec_version |
contract version of this document |
name |
human-readable display name (the local alias is the consumer's, not this) |
classes |
which of apps / bases this index serves, with their relative paths |
keys[] |
the delegation set: every cert this index accepts, each with key_id (SHA-256 of DER cert, lowercase hex), role (index / build), relative cert_path, active flag |
key_rotation |
monotonic counter bumped on key set changes (ADR-0007) |
version |
monotonic index version, bumped on every publish (see Trust) |
published_at |
unixtime set at signing; cross-checked against version — any mismatch = bail (see Trust) |
Delegation-by-inclusion: the signed keys[] list is the trust
statement — "I, the index holder, vouch for these builders." No per-cert
countersignatures. Key roles are positional in this list; certs themselves
carry no markings (resolves Q-B5).
Per-arch catalogs¶
Each catalog.json is signed by the index key and lists the latest
version of each name for that arch and class:
| Entry field | Meaning |
|---|---|
name, version, arch |
identity (version = UAPI.10; version is the DDI version the author assigns — inner software versions are untracked by design) |
path |
relative path to the .raw |
sha256, size |
of the .raw — download-integrity anchor |
signing_key_id |
which build cert signed this image |
added_at |
unixtime the DDI was published into this index (the second signing moment: DDI signed at build, index at publish) |
runtime |
portable / nspawn (from the app contract declaration) |
base |
base lineage (name + sysext_level) — what get uses to resolve the closure |
description |
search field, projected from the embedded manifest — search is index-file text matching only, never opens DDIs |
A type field (apps / bases) guards against class confusion.
Trust¶
- Trust rides the build signature, never the index signature (ADR-0037). The index signature provides catalog integrity and anti-freeze for subscribers — an index is a collection anyone can copy DDIs into; its signature is the signer's curation bookkeeping, not a vouching mechanism.
- Two gates. get-gate: signed catalog +
sha256match — download integrity. up-gate: systemd verifies the build signature embedded in the DDI at every start (app contract §2) against the accepted-cert store. The two-signature cocktail: build with your key, be published under anyone's index key. - Monotonicity (anti-rollback/freeze): every signed manifest/catalog
carries the pair
generation(monotonic counter) +built_at(unixtime at signing), bumped together on every publish. The two must always move together — any disagreement is "iffy, bail out": equal generation with a different timestamp → refuse (two catalogs claiming the same generation); newer generation with older timestamp → refuse; older generation with newer timestamp → refuse. Consumers remember the highest consistent pair per index identity and refuse anything older. Already-cached content stays usable; only the catalog is downgrade-protected. The counter is the machine rule (no clock trust); the timestamp doubles as the human staleness display. Monotonicity applies to subscriptions only — imports (ADR-0037) consume no foreign catalog, so the stale-catalog override question is moot. Field naming per §20/§21 rulings: the counter is the index'sversion; "generation" is dead. - Presence ≠ authenticity. A partial copy may carry the full signed
catalog while holding a subset of payload;
getof an absent entry fails loudly. The catalog promises hashes are authentic, not that files are present. - Consumer-side storage: pinned index keys and delegated build certs are
stored per the VOA hierarchy (UAPI.11;
repository-metadataandimagepurposes, per-index$context) — ADR-0034. The up-gate additionally requires the delegated build certs in/etc/verity.d/until systemd consumes VOA (T99); that store is flat, so per-index scoping is enforced at the get-gate plus runtime state (an image that bypassed a trustedgetis refused atup).
Subscribe and import (ADR-0037)¶
Content reaches a consumer by exactly two paths:
- Subscribe.
get <alias>/<name>against a signed index at any location — https, ssh, orfile://on a stick if its maker signed an index for it. The subscriber gets catalog integrity + the anti-freeze pair. Never trust. - Import. Raw DDI files from anywhere (a directory, a stick, a friend's collection). Each file's build signature is verified against certs the consumer accepts — the signer's cert travels in the PKCS7 and is shown at import; accepting a new builder is an explicit act. Verified DDIs are added to the consumer's own index, re-signed with their own key. Selective and merge-friendly by construction.
Every index is always signed. There is no "local" exemption: an index lives on a disk, and a disk may be a USB drive moved to another machine — at which point it is an external index. Sneakernet chains re-curate at every hop; content trust rides the build signatures end to end.
Resolution¶
- Addressing is explicit:
get <alias>/<name>— the alias is a local, consumer-chosen name bound to a pinned index key atindex trust. No bare names, no default index. getfetches the latest version only (UAPI.10 max in the catalog); there is no downgrade-via-get. Local retention and on-disk revert are consumer policy, outside this contract.getresolves the closure: eachstackentry is satisfied from an installed/cached base or fetched (bases class) — from any trusted index that serves it; if the closure cannot be satisfied, the wholegetfails loudly and nothing half-usable lands in the cache.
Authoring¶
A conformant author (reference implementation: buildctl index …):
init— explicit ceremony: create the layout, generate the index keypair, write + sign the manifest. Identity keys are never created as a side effect.add <artifact>— the ADR-0038 sequence: probe the GPT (type UUIDs), verify the build signature against the accepted certs (unknown builders refused — accepting a new builder's cert is a separate explicit act), verity-verify, then extract the embedded manifest; derive the destination path from the verified identity (filenames are untrusted — files may be renamed to convention); project the entry fields into the catalog; re-sign; bump the indexversion+ timestamp.update— full rescan/repair of a local tree, re-sign.- All operations are local-filesystem-only. Transport to wherever the
index lives is the operator's tool (
scp,rsync, a stick). The index key never leaves the author's host (ADR-0009's surviving core). - Catalog hygiene (description/publisher/license present) is enforced at
add— the published floor is the index holder's standard, above the app contract's build floor. - Updates must be atomic per signed file (write + sign to temp, rename), so a reader never observes an unsigned or half-written catalog.
Consuming¶
index trust <url|path>— the explicit ceremony: fetch the manifest, show identity + fingerprint, pin the index key, install the delegated build certs. Trust publishers, not media: a known identity at a new location (a friend's stick) needs no new ceremony; an unknown identity always does.index list— trusted identities, their aliases, known locations, staleness (age of newest seen catalog).index drop <alias>— untrust; refuses while apps from that index remain provisioned; non-destructive once none remain (everything is re-get-able after a future re-trust).
Open items (for review)¶
- File names (proposed, unconfirmed): root manifest
index.json, per-archcatalog.json. (Class dirsapps/+bases/confirmed 2026-07-05.) Related and parked: the "index" noun itself may get a better name, together with the unnamed distribute tool (ADR-0039). - ~~
.ziptransport bundle~~ — RESOLVED: dropped (payload already compressed inside the DDI). - ~~Anti-rollback shape~~ — RESOLVED:
version+published_atcross-checked pair; subscriptions only; the stale-catalog--forcequestion is moot under ADR-0037 (imports consume no foreign catalog). - ~~Catalog search fields~~ — RESOLVED:
name+descriptionmatching; freeform tags rejected; categories killed entirely (2026-07-06, §24 — no added value). Search is index-file-only. - ~~
up --getacross tools~~ — RESOLVED by ADR-0040 (one binary, legs as namespaces): the composition flags work naturally within one tool.