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:
- 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. - Schema. Every manifest is validated against the schema the binary publishes
(
ARCH-R92). - 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). - 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.
Related
- F5 The plugin catalogue — what this repository is the implementation of
- ADR-0023 — signing, and why it arrives before
L1owes it - ADR-0022 — the reach its CI checks
- releasing — the streams this is deliberately not one of