← The specification

Repo: lemonfiber-plugins

Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page

Status: Accepted

The reviewed plugin catalogue. A register of where plugins are, and the revision of each one that a person read.

It registers rather than holds. A plugin’s manifest and its recordings belong to the repository that publishes them, and a second copy here would be a second answer to what does this plugin declare — with no way to tell which of the two an operator installed. So an entry is an origin and a reviewed revision, and everything else is read out of that revision when CI asks.

Implements: F5-R1–F5-R3, F5-R10, F5-R12


Why this is a separate repo

It meets ADR-0004’s test on every count: its change rate is other people’s rather than ours, its review style is reading a stranger’s data rather than reviewing our own code, and its release clock belongs to whoever contributed last. Holding it inside lemonfiber would mean a binary release every time somebody added a plugin, which is the coupling F1-R5 exists to prevent.

It is also the one repository in the org whose contributors are mostly not us, and that changes what its CI is for. Everywhere else, CI protects a maintainer from their own mistake. Here it is the first reviewer of an untrusted contribution, and it runs before a human spends any attention.

What’s in it

lemonfiber-plugins/
├── plugins/
│   └── <id>.toml            where it is, and the revision that was read
├── bundle/
│   ├── bundle.toml          the default bundle: the stack's tables and each first-party plugin, pinned
│   └── plugins/<id>/        a copy of each pinned plugin's tree, checked against its origin
├── registry/                the checks CI runs over an entry
├── targets.toml             the lemonfiber version the checks are held to
├── justfile
└── .github/
    ├── reader/              asks the release targets.toml names, copied from plugin-template
    └── workflows/

One file per plugin, and nothing in it that cannot be read as a diff (REPO-R60). No archives, no encoded blobs, no generated artefacts. That constraint is what makes human review possible at all, and it is the property F3-R10 asks for.

An entry is short on purpose: an origin and a commit. The revision is the whole of what review means here — a person read that tree, and moving the plugin forward is a pull request moving the commit, which is the same person reading again. It is also what makes F5-R7’s where it came from, at which revision answerable without the catalogue being where it came from.

The fixtures stay in the plugin’s own repository and CI reads them there, at the pinned revision, because the proofs it has to run (F5-R13) are the plugin’s proofs against the plugin’s recordings. Reading them rather than copying them is also the only way the two cannot drift.

What its CI does

Three things, in order, before a human looks:

  1. Fetch. The registered revision is fetched, and what is read out of it is data: the manifest, the recordings, and the release the plugin says it targets. Nothing in the registered repository is executed, here or ever (REPO-R62) — a catalogue that ran a stranger’s script to find out whether the stranger’s data was acceptable would be answering the question by doing the thing the question is about.
  2. Schema. Every manifest is validated against the schema the binary publishes (ARCH-R92).
  3. Proofs. Every declared proof runs against that plugin’s recorded fixtures, and a contribution with a proof that is neither passed nor failing as declared is refused (F5-R13).
  4. Reach. The declared pairs are checked statically — no undeclared flow, no address literal (ADR-0022).

All three run the same commands an author runs locally — the plain subcommands F3-R13 already requires — from a pinned lemonfiber release named in the repository (REPO-R56). A catalogue that validated against a different build than the operator runs would be vouching for something it had not tested.

Before a release reads what the bundled plugins declare, the repository names a full commit of the core’s main in the release’s place, and its checks run from that commit, built from source, as a first-party plugin’s own checks do (OPS-R89). The bundle the release embeds is checked by the build that release tags, which holds the commit. Once that release is published, the repository names it instead of the commit.

The review, and its honest limit

A human reads the diff and merges, which is possible precisely because a plugin is data. What that buys is stated carefully in F5: a schema that was checked, proofs that ran, a person who read it, and a signature tying what was fetched to what was reviewed.

What it does not buy is a reading of the image. A manifest is forty lines and a container image is not reviewable by anybody, here or anywhere — which is why the image is pinned by digest rather than vouched for (ADR-0023).

The reviewer is one person. security.md already names that as a structural cap, and an open contribution funnel through a single gate is this design’s likeliest practical failure. It is the reason F5-R4 keeps an operator’s own source on the same technical terms: the catalogue is the curated lane, not the only road.

Releases, and what a signature covers

The catalogue tags on its own clock and is not a stream the version train cuts (OPS-R59). No repos list in 70-operations/versions/ names it — it appears there only in satisfied_in, which tags nothing — and a plugin landing here moves no version number in this organisation.

A release is signed (F5-R3), and the signature covers exactly what was reviewed (REPO-R57). The signing capability is owed here before L1-R2 delivers it for release artefacts — one capability, two consumers, and the earlier consumer sets the date (ADR-0023).

What is reviewed is the register, so what a release signs is an index of it: each plugin id with its origin, the revision reviewed and the digest of its manifest there, and a serial each release raises above the one before. A release workflow here writes index.json, signs it with cosign using the key held as the CATALOGUE_SIGNING_KEY secret, and publishes both on the tag, and it fails rather than publish an index it could not sign (ADR-0034).

The bundle

The default bundle is the set of first-party plugins every stack is built from (ADR-0042). It lives here, in bundle/, and the core embeds it by pinning this repository as a submodule.

bundle/bundle.toml is the bundle contract at schema = 2 (REPO-R91): the stack’s profiles, forms, removed services, choices of filler and links by name, and each first-party plugin with its origin, the revision pinned, the digests of its manifest and its tree there, the capabilities it fills by default, whether lemonfiber assembles or installs it, the profile it sits in and the upstream’s last release. Where the plugin builds an image, the pin also names the release that tagged it.

It pins a revision rather than a tag because a manifest names its image by digest, and that digest reaches the plugin’s main through the pin pull request its release opens after the tag. A plugin’s main also pins images built from its own commits (OPS-R91); where the bundle names a release, the revision’s built images are tagged at it. A plugin that builds no image is never tagged, and its revision and tree are the whole pin (OPS-R86).

Beside it, bundle/plugins/<id>/ is the plugin’s tree at that revision, copied byte for byte at the paths the plugin’s repository holds them: its manifest, its starting files, its recordings and its conformance. This is the one place the catalogue holds a copy (REPO-R60). The binary compiles the bundle in and builds a stack offline, so it needs the trees themselves and not their addresses. CI holds each copy to its origin (REPO-R92): a copy that differs from the tree at the pinned revision is refused, so the copy and the origin are one answer.

The pin of a plugin that builds an image moves with the release train (OPS-R86), not with a contribution; the pin of one that builds none moves by a reviewed pull request here. The catalogue’s own releases and its register keep their own clock (OPS-R59). A pinned revision may target a commit of the core’s main rather than a release, and the train refuses to tag the core unless the core commit it tags holds that commit (OPS-R89).

What it never becomes

Not a runtime dependency. An installed plugin keeps working with this repository unreachable, and installing from a named source still works (F5-R10). So the catalogue publishes nothing that an installed plugin resolves while running (REPO-R58) — the moment it did, every operator’s stack would depend on this repository’s availability.

Not a service. No backend, no database, no state beyond the repository (REPO-R59). A catalogue that needed operating would be a second product, and it would be one this project has said it does not build.

Requirements

ID Requirement
REPO-R54 Withdrawn — carried to REPO-R60 when the catalogue became a register of origins rather than a copy of what it registers. The number is not reused.
REPO-R55 Withdrawn — carried to REPO-R61 for the same reason: the checks are made against a fetched revision rather than against a copy held here. The number is not reused.
REPO-R56 Those checks MUST run the same commands an author runs locally, from a lemonfiber release pinned and named in the repository, or, before a release reads what the bundled plugins declare, from a full commit of the core’s main named in its place and built from source; once that release is published, the repository MUST name it instead.
REPO-R57 A catalogue release MUST be signed, and what the signature covers MUST be exactly what was reviewed.
REPO-R58 The catalogue MUST NOT publish anything an installed plugin resolves at run time.
REPO-R59 The catalogue MUST hold no service, no database and no state beyond the repository itself.
REPO-R60 The catalogue MUST register each plugin as an origin and the revision of it that was reviewed, MUST hold no copy of a registered plugin’s manifest or recordings other than the bundle’s copies (REPO-R91), and MUST contain nothing that cannot be reviewed as a readable diff.
REPO-R61 Its CI MUST fetch each registered revision and, from the data in that revision alone, validate the manifest against the published schema, run every declared proof against the plugin’s recorded fixtures, and check its declared reach statically; a registration failing any of these MUST be refused before human review.
REPO-R62 The catalogue MUST NOT execute anything from a registered repository, and MUST read that repository as data only.
REPO-R63 Every catalogue release’s index MUST carry a serial higher than every earlier release’s, inside what the signature covers.
REPO-R91 The default bundle MUST be bundle/bundle.toml at schema = 2, as the bundle contract states it (ARCH-R265, ARCH-R266); and the catalogue MUST hold each pinned plugin’s tree, copied byte for byte from the pinned revision, at bundle/plugins/<id>/ (ARCH-R267).
REPO-R92 The catalogue’s CI MUST refuse a bundle in which a copy differs from the tree fetched from its origin at the pinned revision, a pinned manifest or tree digest is not its copy’s, a pin naming a release has a built image not tagged at it, the bundle fails the bundle contract’s validation, or a pinned plugin fails a check REPO-R61 holds a registration to.

Affected repos (GOV-R7): lemonfiber-plugins.