The manifest
A plugin is one plugin.toml at the root of its repository or directory. A
source declaring two plugins is refused.
The reference below is not written here. It is read out of
contract/plugin-manifest.schema.json at the revision this site pins — the
schema the binary is generated from and validates against, and the one
lemonfiber plugin schema prints for your editor. It starts at the root,
PluginManifest, and every type a field names links to its own entry. The
normative prose, with the reasons, is
the plugin manifest contract.
What the reader refuses that a schema cannot say
Section titled “What the reader refuses that a schema cannot say”A manifest can match the schema and still be refused: an id with a character
that is not a lower-case letter, a digit or a hyphen, a port with no bind, a
hostname on a service nobody proxies, a [requires] naming something this
build does not offer. The rules, each with its reason, are the contract’s
validation, and
the fields a manifest has no room for at all — environment, kernel
capabilities, devices, privilege, a fragment of Compose — are
the fields that are absent, and why.
Neither is restated here, because the specification says the schema is the only
description of the format published to authors
(F10-R2), and
a second list is one more to fall behind.
The way to know what this build refuses is to ask it.
lemonfiber plugin claims <path> reads your
manifest the way install will, and reports every violation in one pass rather
than stopping at the first. A refusal is total: nothing of a refused manifest is
acted on, and PLUGIN-3
lists everything that was wrong with it.
Every field
Section titled “Every field”PluginManifest
A whole plugin manifest.
| Field | Type | Required | What it is |
|---|---|---|---|
ask | array ofPluginAsk | no | The capabilities its services ask for. |
claim | array ofClaim | no | The core capabilities this plugin claims, and the probes each is shown by. |
contribution | array ofContribution | no | The rows it adds at the points lemonfiber publishes. |
override | array ofOverride | no | Every bundled thing it will change. |
plugin | Plugin | yes | Who the plugin is, and where it came from. |
proof | array ofProof | no | What must hold before it is installed. |
recipe | array ofRecipe | no | The ordered calls that configure what it installed. |
requires | Requiresornull | no | What the plugin needs of lemonfiber. |
schema_version | integeruint32, ≥ 0 | yes | The manifest format generation. |
secret | array ofSecret | no | Every value it will hold. |
service | array ofPluginService | no | What runs. One or more. More than one because a plugin is often a thing and the thing beside it: Plex and the reader of its watch history are one install and one uninstall to an operator, and are two containers on two tiers with two criticalities. A format permitting one forces the author of the pair to choose the wider tier for both halves, or to publish two plugins an operator has to keep in step by hand. |
wiring | array ofWiring | no | How the stack's own proxy and dashboard reach its services. One each, at most. |
Capture
A value taken out of an answer.
| Field | Type | Required | What it is |
|---|---|---|---|
from | string | yes | Where in the answer it is read from: an expectation key, or |
name | string | yes | What the value is called, for a later call to name. |
origin | PluginValueOrigin | yes | Whose value it is: the service in this stack or the host outside it that answered. |
Claim
The core capability a plugin claims, and the probes it is demonstrated by.
The vocabulary owns what must be shown; this owns where to ask. A capability is one contract with many claimants, each answering at a path of its own, so the published probe declares the question and the statuses that answer it, and the binding declares the method, the path and the recorded response.
| Field | Type | Required | What it is |
|---|---|---|---|
capability | string | yes | A core name, which must also appear in the service's |
probe | array ofClaimProbe | no | One binding per probe the capability declares. Every one of them, exactly once. |
ClaimProbe
One probe of a claimed capability, bound to a request on this plugin's service or, where the vocabulary declares the probe a run, to a run of its image's own program.
| Field | Type | Required | What it is |
|---|---|---|---|
expect | Expect | yes | What the answer must be, within what the probe permits. |
fixture | string | yes | The recorded response. Required here where it is optional on a proof: a claim nobody can demonstrate without owning the service is a claim the catalogue's own checks cannot make. |
id | string | yes | Names a probe the capability declares. |
request | PluginRequestornull | no | Method and path on this plugin's own service. Exactly one of this and |
run | PluginRunornull | no | Arguments to the image's own entrypoint, run with no network. Exactly one of this and |
Contribution
A row in a register lemonfiber already runs.
The fields beyond at and id are the row its point declares, and the point is published — so the required set, the optional set, the closed sets and the bounds are read from extension-points.json rather than restated here. What this type fixes is that a contribution is declared in this block and nowhere else, and that it carries nothing outside the union of the rows the published points take.
| Field | Type | Required | What it is |
|---|---|---|---|
action | stringornull | no | What to do, in the imperative. |
at | string | yes | A point the build publishes. One it does not is refused by name. |
category | stringornull | no | Which family a check is narrowed to. |
detail | stringornull | no | The technical half of a remedy, which must not lead. |
expect | Expectornull | no | What the answer must be. |
expected | array ofPluginExpectedFailure | no | Recordings the check fails on, each with the constraint that fails there and why. |
fixture | stringornull | no | The recorded response the check is proved against. |
for | stringornull | no | The check a remedy is for, which must be one this same plugin declared. |
id | string | yes | Namespaced with the declaring plugin's id, always. |
request | PluginRequestornull | no | Method and path on the plugin's own service. There is no field for a host, so a check cannot be pointed at another service, at the machine, or off it. A plugin wanting to say something about a service it did not install is asking to speak for somebody else's software. |
service | stringornull | no | Which service the finding is about. Defaults to the plugin's own. |
timeout_s | integerornulluint32, ≥ 0 | no | How long a check may run, within the bounds the point declares. |
title | stringornull | no | The one-line summary of what was checked. |
why | stringornull | no | Why this is worth checking. |
Expect
What the answer has to be.
A status is a claim about the network path rather than about the service: Docker publishes a port by putting a proxy in front of it, and that proxy accepts a connection before knowing whether anything inside is listening. So a status alone is not evidence — except for a refusal or a redirect to HTTPS, answers no port proxy produces.
A key of the four key-wise constraints is a place rather than a name. A plain name is a top-level member, and one beginning with / is a JSON Pointer, extended with a step that picks an entry of a list by a field it holds. A flat name was enough while every service answered a flat object, and the only thing it could say about a service that nests its payload was that the envelope was there — which is a probe that passes by observing that something replied.
| Field | Type | Required | What it is |
|---|---|---|---|
body_starts_with | stringornull | no | What the body must begin with, where it is not JSON. |
content_type | stringornull | no | A substring of the content type the answer was served as. |
exit | integerornullint64 | no | The exit status a run must end with. Not sufficient alone. |
json | map ofExpectedornull | no | Places the answer must carry, each with the exact value it must hold. |
json_array_min | integerornulluint64, ≥ 0 | no | The answer read as an array, with at least this many entries. A catalogue is very often a list rather than an object, and none of the object-shaped constraints can say anything about one. |
json_at_least | map ofintegerint64ornull | no | Places the answer must carry, each with a number it must not be below. |
json_has_keys | array ofstringornull | no | Places the answer must carry, whatever they hold. |
json_is_absent | booleanornull | no | The answer did not parse as JSON at all. Which is what a service that serves its application shell for every path it does not implement answers — and the reason a status alone proves nothing against one: the shell comes back |
json_types | map ofExpectedKindornull | no | Places the answer must carry, each with the kind of value it must be. |
status | integerornulluint16, ≥ 0, ≤ 65535 | no | The status the answer must carry. |
stdout_starts_with | stringornull | no | What a run's standard output must begin with. |
Expected
Exactly what a place must hold.
Three kinds and no nesting: a flag that must be set, a number that must match, or a word. A value deeper than this is asking about a document rather than about a claim — and where the thing worth asserting is deeper in the answer, the key reaches it rather than the value growing to match.
It is booleanorintegerint64orstring
ExpectedKind
The kind of value a key must hold.
Five, and closed. A name outside them is one no runner could evaluate, and an assertion nothing evaluates is a proof that silently checks less than it says — which is worse than one that fails.
Named for what it is the kind of, rather than Kind, because this is published and whoever generates from it flattens every definition into one scope. Kind is the one name there a generator is certain to want for itself: every envelope this contract describes is keyed by its kind, so the union of them is a Kind too, and two of them in one module is a definition nothing can be compiled against.
One of these:
"bool"A true or a false.
"int"A whole number.
"str"A string.
"list"An array.
"dict"An object.
Input
A value a recipe brings in from outside an answer.
| Field | Type | Required | What it is |
|---|---|---|---|
ask | stringornull | no | What the operator is asked, in one line, where the operator supplies it. |
name | string | yes | What the value is called, for a call to name. |
of | stringornull | no | The service whose credential it is, where the credential store holds it. |
origin | PluginValueOrigin | yes | Whose value it is: the credential store, or the operator. |
secret | boolean | no | Whether what the operator supplies may be a secret, so a terminal takes it without showing it. |
Override
A bundled thing the plugin will change.
| Field | Type | Required | What it is |
|---|---|---|---|
id | string | yes | Which bundled setting is changed. |
why | string | yes | What changing it is for. |
Pair
One value, and the one destination it may reach.
| Field | Type | Required | What it is |
|---|---|---|---|
release | stringornull | no | Why a value captured from a service in this stack is carried to another destination, in the one sentence the operator is shown when asked to approve it. A capture is held to the service that answered it, and this is the one thing that lets it go elsewhere. Refused on a pair it frees nothing on. |
to | string | yes | Where it may be carried. |
value | string | yes | The value, by the name its capture or its input gives it. |
Plugin
Who the plugin is, and where it came from.
| Field | Type | Required | What it is |
|---|---|---|---|
description | string | yes | What it does for the operator. |
forms | array ofstring | yes | Which forms the service joins. |
id | string | yes | Unique, lowercase. The name it is installed and journalled under. |
license | string | yes | SPDX identifier. Recorded and shown rather than constrained. The bundled set is a curated list this project stands behind and is OSI-licensed throughout; a plugin is the operator's own choice, and refusing to install proprietary software on somebody else's machine would be the tool standing between an operator and their stack. A plugin whose licence is absent is refused; one whose licence is merely not open is installed and said so. |
name | string | yes | Human-facing. |
upstream | string | yes | Project URL, so the operator can judge the thing rather than the wrapper. |
version | string | yes | Semver. The plugin's own content version, moved by its author. |
without_it | string | yes | The consequence of its absence. |
PluginAsk
One capability a plugin's service asks for.
| Field | Type | Required | What it is |
|---|---|---|---|
capability | string | yes | The core capability asked for. |
each | boolean | no | Whether every service that fills it is reached rather than one. |
native | stringornull | no | The upstream's own API the service talks in, which the ask then reaches in place of the adapter fronting it. |
service | stringornull | no | The plugin's own service that asks; optional where the plugin declares one. |
PluginBind
Which tier a plugin's port is published on.
A tier, not an address. lemonfiber renders it to an address exactly as it does for a bundled service, so the two-tier policy stays a property of the system rather than a request the plugin makes.
One of these:
"loopback"Reachable only from the host.
"lan"Reachable from the local network.
PluginConstraint
A kind of constraint an expectation can put on a body.
The same vocabulary a proof's expectation and a contributed check's use, so that "a status alone is not evidence" is one rule rather than three. Read as well as written: a declaration that an assertion fails on a recording names the one of these that fails there.
One of these:
"status"The status alone.
"json"Keys carrying exactly these values.
"json_has_keys"Keys that must be present, whatever they hold.
"json_types"Keys that must hold a given kind of thing.
"json_at_least"Keys that must hold at least a given number.
"json_array_min"The answer read as an array, with at least a given number of entries.
"json_is_absent"The answer did not parse as JSON at all.
"content_type"The content type it is served as.
"body_starts_with"What the body must begin with.
"exit"The exit status a run ends with.
"stdout_starts_with"What a run's standard output begins with.
PluginCriticality
How much a plugin's service costs when it is absent.
critical means its failure has consequences outside the machine, which is the egress guard's alone: a service declares it only with shape = "egress-guard", and one that declares it without is refused naming the shape and the four it may declare.
One of these:
"critical"Its failure has consequences outside the machine.
"core"The form does not work without it.
"important"The form works, badly.
"enhancing"Makes things better.
"optional"Nice to have.
PluginDeclaredVerdict
The one verdict a declaration may name.
One of these:
"fails"The assertion fails on the recording, on the constraint the declaration names.
PluginExpectedFailure
A recording an assertion fails on, the one constraint of its expectation that fails there, and why.
For an assertion whose passing state nobody can record, such as a check that a server has an owner where claiming one needs an account the plugin's CI does not hold: the state it exists to find can be recorded, and the declaration says which constraint tells the two states apart. It changes the verdict on that recording and on nothing else — the live service and every other recording are held to the expectation as it is written.
It names a constraint rather than only a recording, because a recording that began failing for another reason — a truncated file, an error recorded by mistake — would otherwise read as failing as declared, excusing a failure nobody had looked at.
| Field | Type | Required | What it is |
|---|---|---|---|
constraint | PluginConstraint | yes | The key of the expectation that fails on the recording, and one it carries. |
fixture | string | yes | The recording the assertion fails on. It may be the assertion's own |
place | stringornull | no | Where within that constraint it fails, written exactly as the expectation writes it. Present for a key-wise constraint and absent for one about the whole answer. |
reason | string | yes | Why this recording is one the assertion fails on. Reported with the verdict every time. |
verdict | PluginDeclaredVerdict | yes |
|
PluginHealth
How to tell a plugin's service has started.
| Field | Type | Required | What it is |
|---|---|---|---|
kind | PluginHealthKind | yes | Which kind of probe to use. |
path | stringornull | no | HTTP path, for |
timeout_s | integerornulluint32, ≥ 0 | no | How long to wait before calling it unhealthy. |
PluginHealthKind
The kinds of health probe a plugin's service can declare.
One of these:
"http"Fetch a path on the service's port.
"tcp"Open a connection to the service's port.
"container"Trust the container's own reported state.
PluginRecipeCondition
What an earlier step or value must have come to.
One of two shapes, and a condition carrying neither or both is refused when the manifest is read: an earlier step answered a status, or an earlier value holds a text exactly.
| Field | Type | Required | What it is |
|---|---|---|---|
equals | stringornull | no | The text it must hold, exactly. |
status | integerornulluint16, ≥ 0, ≤ 65535 | no | The status that step must have answered. |
step | stringornull | no | The earlier step whose answer is asked about. |
value | stringornull | no | The earlier capture or input asked about. |
PluginRecipeOn
When a recipe runs.
One of these:
"install"During an install and an update, after the plugin's proofs and the stack's checks hold and before the install is recorded. What a recipe that says nothing does, because a first-run flow is what a recipe is for.
"demand"Only when the operator asks for it by name, with approvals of its own.
PluginRecipeRetry
Making a step again until its answer is the one wanted.
| Field | Type | Required | What it is |
|---|---|---|---|
every | string | yes | How long it waits between two, written as whole seconds, |
times | integeruint32, ≥ 0 | yes | How many times it is made again, at most: no more than [ |
until | PluginRecipeCondition | yes | What the answer must come to for it to stop: a status, or a value it captured. |
PluginRequest
What is asked, and where.
Three fields and no more, and the third is the one worth explaining. A service that answers XML unless a caller asks for JSON cannot satisfy a capability whose probe requires a JSON assertion, and until this field there was nowhere to ask: Plex answers text/xml at every path, including the one its health probe uses, unless the request carries Accept: application/json.
It is one media type and not a header map, and the difference is the point. A probe declares who it is asked as, and none on every guarded probe has meant what it says partly because nothing could be presented. A map of headers would make that a convention a reviewer has to hold — any service may name its credential header whatever it likes, so no list of refused names could ever be closed — where one named field keeps it a property of the format. A probe still cannot present anything, because there is nowhere to write it.
| Field | Type | Required | What it is |
|---|---|---|---|
accept | stringornull | no | The one representation the answer is asked for, as a media type. Absent where the service needs no asking, which is most of them. |
method | string | yes | The HTTP method. |
path | string | yes | The path on the service being asked. |
PluginRun
A run of the image's own program, as a proof declares it.
| Field | Type | Required | What it is |
|---|---|---|---|
args | array ofstring | yes | Each argument handed to the image's entrypoint, one string each: one to 64, each at most 4096 bytes and none holding a NUL. |
timeout_s | integerornulluint32, ≥ 0 | no | How long the run may take, from 1 to 120 seconds. Default 30. |
PluginService
What runs.
Declared in the stack manifest's service vocabulary, restricted to these fields. The restriction is the substance of it: the set of fields is the set of things a plugin may ask for, so "what can this plugin reach" is answerable from the format rather than from the instance.
| Field | Type | Required | What it is |
|---|---|---|---|
asks_for | stringornull | no | What it asks for out there, in the same terms; never blank. |
bind | PluginBindornull | no | Which tier every port it publishes is published on. |
config_path | stringornull | no | Where inside the container its one configuration directory is mounted, |
config_ro | boolean | no | Whether its configuration directory is mounted read-only. |
criticality | PluginCriticality | yes | How much its absence costs. |
digest | string | yes |
A tag is not a pin: it is a name the publisher can repoint, so the image reviewed and the image run can differ with nothing in the manifest changing. The digest is what lemonfiber writes into the generated entry. |
env | map ofServiceValue | no | Its environment, each variable one of four typed kinds of value. |
fronts | stringornull | no | The plugin's own service this adapter stands in front of: the upstream it answers its contracts for, whose pinned digest its conformance recordings are taken against. Required where |
health | PluginHealthornull | no | How to tell the service has actually started. |
id | string | yes | Unique across the stack and every installed plugin. |
image | string | yes | Registry path, without tag or digest. |
listens | integerornulluint16, ≥ 0, ≤ 65535 | no | The port it answers on inside the stack's network, where lemonfiber and the services that ask reach it. Required where |
media_types | array ofstring | no | Which media types it handles, in the stack manifest's vocabulary. |
mem_limit_mib | integerornulluint32, ≥ 0 | no | The most memory the container may hold, in MiB. |
memory_mib | integerornulluint32, ≥ 0 | no | The memory it expects to need, in MiB, never written as a limit. |
mount | array ofServiceMount | no | Each further path mounted from its own mount tree. |
name | string | yes | Human-facing. |
native | stringornull | no | The API this service answers the stack's other services in, by the name those services know it by: declared on the upstream an adapter of the same plugin fronts, where other services reach that upstream directly rather than through the adapter. |
network | ServiceNetworkornull | no | The capability whose filler's network it runs inside. |
port | integerornulluint16, ≥ 0, ≤ 65535 | no | Where it is published on the host, absent for a service that publishes nothing. |
posture | ServicePosture | no | How tightly the container is held. |
provides | array ofstring | no | The capabilities this service claims. A core name comes from the published vocabulary and means the contracted thing that vocabulary defines; a plugin's own must be namespaced with the plugin's id and is inert until something asks for it. |
publish | array ofServicePublish | no | Each further port it publishes on its tier. |
reaches | stringornull | no | Where it reaches outside the machine, in words an operator reads, as a stack service says it; declared with [ |
replaces | stringornull | no | The id of the stack's own service this service moves in place of, which is its own id: it keeps that service's container, configuration and data, and may take its port and the name the proxy answers for it. |
shape | PluginShapeornull | no | The one privileged shape lemonfiber writes for it, from a closed set. |
speaks | array ofstring | no | Each capability contract it answers, as |
tag | string | yes | The human-readable version the digest corresponds to. Recorded and shown; never resolved. |
takes_data | boolean | no | Whether it takes the data mount. |
tmpfs | array ofstring | no | Absolute paths given scratch space in memory. |
upstream | map ofServiceSecretOf | no | Each field of the |
user_mode | ServiceUserModeornull | no | Who the container runs as. |
PluginShape
A privileged shape lemonfiber writes for a service.
One of these:
"egress-guard"NET_ADMINand the tunnel device."render"The host's render devices and the group that owns them.
PluginValueOrigin
Where a value a recipe carries comes from.
Four, and no fifth: a service in this stack and a host outside it each answer a call, and lemonfiber's credential store and the operator each supply a value no call answered. Written by the manifest, and held to where the value really comes from.
One of these:
"stack-service"The answer of a call to a service in this stack.
"external-response"The answer of a call to a host outside it.
"credential-store"A credential lemonfiber already holds for a service.
"operator"A value the operator supplies when the recipe runs.
Proof
What must hold before a plugin is installed.
| Field | Type | Required | What it is |
|---|---|---|---|
expect | Expect | yes | What the answer must be. |
expected | array ofPluginExpectedFailure | no | Recordings this proof fails on, each with the constraint that fails there and why. |
fixture | stringornull | no | A recorded response to run against where no instance exists. |
id | string | yes | Unique within the plugin. What a verdict is reported against. |
request | PluginRequestornull | no | Method and path. Exactly one of this and |
run | PluginRunornull | no | Arguments to the image's own entrypoint, run with no network. Exactly one of this and |
service | stringornull | no | Which of this plugin's services is asked. Optional where the plugin declares one, required where it declares more. A proof gates the install of every service the plugin brings, and one that did not say which it was about would be checked against whichever the reader reached first — including its digest, which is what ties a recording to the image it came out of. |
title | string | yes | What it establishes, in one line. |
why | string | yes | Why this is worth asserting. A proof nobody can justify is one nobody will maintain. |
Recipe
One named flow of calls.
| Field | Type | Required | What it is |
|---|---|---|---|
id | string | yes | Unique within the plugin. |
input | array ofInput | no | The values it brings in from outside an answer: a credential lemonfiber holds, or one the operator supplies when it runs. |
on | PluginRecipeOn | no | When it runs: as part of installing the plugin, or when it is asked for. |
pair | array ofPair | no | Every value this flow could carry to every destination it could reach. A flow with no pair behind it fails validation before a call is made, which is what makes "what does this plugin tell whom" answerable from the manifest. |
step | array ofStep | no | The calls, in the order they are made. |
title | string | yes | What the flow accomplishes, in one line. |
why | string | yes | Why it is worth running. |
RecordedWith
What a recording gives a service in the operator's place: a constant reaching no third party, or, written "minted", a value made for that run alone.
It is stringorintegerint64orboolean
Requires
What the plugin needs of lemonfiber.
Named capabilities and never a version. A plugin written a year ago against a stack it has never met keeps working for exactly as long as the things it actually uses still exist, and when it stops working the message says which thing went — a version number cannot say that, because it conflates "older" with "missing something you needed".
| Field | Type | Required | What it is |
|---|---|---|---|
capabilities | array ofstring | no | What lemonfiber must offer for this plugin to be installable. |
Secret
A value a service holds, declared before anything captures or enters it.
| Field | Type | Required | What it is |
|---|---|---|---|
carried_from | SecretCarriedFromornull | no | Where the service a part moves in place of wrote this credential itself. |
for | stringornull | no | The capability of the plugin's own that a credential crossing to it is needed to fill. |
id | string | yes | What the value is, within its namespace. |
minted | SecretMinted | no | Whether lemonfiber makes the value at install, rather than anything capturing or entering it, and in what shape. |
of | string | yes | Whose credential it is. |
recorded_with | RecordedWithornull | no | What a recording gives the service in the operator's place, where only the operator holds the value and the service needs one to start. |
why | string | yes | What holding it is for. |
SecretCarriedFrom
Where a service wrote a credential itself: a file inside its container, the format it is written in and the dotted path to the value.
| Field | Type | Required | What it is |
|---|---|---|---|
file | string | yes | The file, as a path inside the service's container beneath its configuration directory. |
format | SecretFormat | yes | The format the file is written in. |
key | string | yes | The dotted path to the value in the file; for |
SecretFormat
A format a service writes its configuration in.
One of these:
"xml"XML.
"json"JSON.
"yaml"YAML.
"ini"INI.
SecretMinted
How a manifest writes [Minted]: true or false, or the shape it is made in.
It is booleanorSecretMintedShape
SecretMintedShape
A shape a minted value is made in, beyond a key.
One of these:
"password"A password: every character class a password rule asks for.
ServiceConstant
A value written exactly as declared.
It is stringorintegerint64orboolean
ServiceMount
A further path mounted from the service's own mount tree.
| Field | Type | Required | What it is |
|---|---|---|---|
from | string | yes | The relative path inside the mount tree. |
ro | boolean | no | Whether it is mounted read-only. |
to | string | yes | The absolute path inside the container. |
ServiceNetwork
The network a service runs inside instead of its own.
| Field | Type | Required | What it is |
|---|---|---|---|
via | string | yes | The capability whose filler's network it runs inside. |
ServicePosture
How tightly the container is held.
One of these:
"standard"Six kernel capabilities, no privilege gained, a bounded process count.
"confined"A read-only root and no kernel capability at all, besides.
ServicePublish
A further port a service publishes on its tier.
| Field | Type | Required | What it is |
|---|---|---|---|
listens | integerornulluint16, ≥ 0, ≤ 65535 | no | Where it answers inside the container, |
port | integeruint16, ≥ 0, ≤ 65535 | yes | Where it is published on the host. |
ServiceSecretOf
A secret the manifest declares.
| Field | Type | Required | What it is |
|---|---|---|---|
secret | string | yes | The declared secret's name. |
ServiceSetting
An operator setting, with what is written while none is recorded.
| Field | Type | Required | What it is |
|---|---|---|---|
default | ServiceConstant | yes | What is written while the operator has recorded no value. |
recorded_with | RecordedWithornull | no | What a recording gives the service in the operator's place, where only the operator holds the value and the service needs one to start. |
setting | string | yes | The setting's name. |
ServiceUrlOf
The address of a capability's filler.
| Field | Type | Required | What it is |
|---|---|---|---|
files | stringornull | no | The medium the filler asked for files. |
native | stringornull | no | The upstream's own API, where the ask reaches the upstream an adapter fronts. |
url_of | string | yes | The capability asked for. |
ServiceUserMode
Who the container runs as.
One of these:
"image"No user: the image decides.
"puid-env"PUIDandPGIDset to the operator's."uid"The operator's uid and gid as the container's user.
ServiceValue
One environment variable's value.
It is ServiceConstantorServiceSettingorServiceUrlOforServiceSecretOf
Step
One call in a flow.
| Field | Type | Required | What it is |
|---|---|---|---|
call | StepCall | yes | What is called, and where. |
capture | array ofCapture | no | The values taken out of the answer, for a later call to substitute. |
expect | Expectornull | no | What the answer must be for the flow to go on. |
id | string | yes | Unique within the recipe. |
retry | PluginRecipeRetryornull | no | How often the step is made again before its answer is taken as final. |
when | PluginRecipeConditionornull | no | What must hold for the step to be made; where it does not, the step is skipped. |
StepCall
What one step calls.
headers and body are where an earlier capture is substituted, and they are the whole of it. Without somewhere to put one, a recipe could name a destination and capture a value and had no way to carry the one to the other — which is every first-run flow there is: creating an account and reading back a token is worth nothing if the token cannot then be presented.
They are also what makes the set of flows a recipe could produce computable by reading it: every substitution inside a call is a flow from that value to that call's destination, and a call with nowhere to substitute into produces none.
| Field | Type | Required | What it is |
|---|---|---|---|
body | stringornull | no | The body the call carries. A value may substitute an earlier capture. |
headers | map ofstringornull | no | Headers the call carries. A value may substitute an earlier capture. |
method | string | yes | The HTTP method. |
path | string | yes | The path on it, written out. Never a substitution. |
to | string | yes | A service id in this stack, or a DNS name outside it. Never an address, and never a substitution. |
Wiring
How the stack's own services reach a plugin's service.
The tier governs, not the plugin. Only a lan service is proxied, because the bundled policy is that an admin surface does not get a hostname — and a plugin that could publish its own address could put an admin surface on the household network without touching anything the web-security checks inspect.
| Field | Type | Required | What it is |
|---|---|---|---|
dashboard_description | stringornull | no | The line beside its entry on the bundled dashboard. Defaults to the plugin's description. |
dashboard_group | stringornull | no | Which group on the bundled dashboard it appears under. Defaults to the group the stack uses for its tier. |
dashboard_hidden | boolean | no | Whether it has no entry on the bundled dashboard. |
dashboard_icon | stringornull | no | The icon its entry on the bundled dashboard shows, named from the dashboard's own icon set. Defaults to the service's id with |
hostname | stringornull | no | The label in front of the operator's domain. A single DNS label — not a name, an address or a port. Defaults to the service's id. |
service | stringornull | no | Which of this plugin's services it is about. Optional where the plugin declares one service, because there is nothing to choose between; required where it declares more, because a hostname is a fact about one service and a manifest that left it to be inferred would be inferring which of two the household reaches. |
A whole manifest
Section titled “A whole manifest”The template every plugin starts from, as its repository holds it at the revision this site pins. Its own comments explain each block; the repository is the plugin template.
#:schema https://github.com/lemonfiber/lemonfiber/releases/download/v0.16.0/plugin-manifest.schema.json# The template plugin. Copy this repository, replace the service, keep the shape.## Contract: https://github.com/lemonfiber/spec/blob/main/20-architecture/contracts/plugin-manifest.md## Everything lemonfiber does on a plugin's behalf comes from this file, and it# knows nothing about the plugin that is not declared here (ARCH-R83). Nothing in# it is executable, and nothing in it can be (F3-R1, F3-R6).## The service described here is real, and that is the point. F10-R7 asks for a# template that validates and proves *unmodified*, and a template whose proofs# are invented teaches an author to invent proofs. Every recording under# fixtures/ came off this image at this digest.
schema_version = 1
[plugin]id = "kavita"name = "Kavita"version = "1.0.0"description = "Reads comics, manga and ebooks in a browser, and remembers where everybody got to"without_it = "A folder of CBZ and EPUB files, opened one at a time with nothing keeping your place"upstream = "https://github.com/Kareadita/Kavita"license = "GPL-3.0-only"forms = ["library", "full"]
# The two forms that serve a library rather than acquire one. Neither `books`# nor `auto` is named: those acquire, and this acquires nothing.
[[service]]id = "kavita"name = "Kavita"image = "docker.io/jvmilazz0/kavita"# What actually runs (F3-R8, ARCH-R95). `tag` is the readable name for this# digest and is never resolved.digest = "sha256:b9c671586db2a6a688da3cb4b45f1319cca33b01e6e760c8bf3c19d60101bdf2"tag = "0.8.7"port = 5000# A household service: the point of it is the tablet on the sofa. The tier is# all this may say — the address is lemonfiber's to assign (ARCH-R88), and so is# whether it is reachable by name at all (ARCH-R104).bind = "lan"# Measured on a warm machine with the image already pulled: it runs its database# migrations before it opens its listener, and answered in 11s.health = { kind = "http", path = "/api/health", timeout_s = 90 }criticality = "enhancing"
takes_data = truemedia_types = ["comics", "books"]
# This image has no /config. It keeps its database, its covers and its settings# under /kavita/config, and a generated container mounting /config would mount a# directory the application never reads — installing cleanly, answering its# health probe, and losing everything the moment the container was replaced.config_path = "/kavita/config"
# What it can do (F4-R1). A core name comes from lemonfiber's published# vocabulary and is demonstrated by the [[claim]] below; a name of this plugin's# own is namespaced with the plugin's id (F4-R4) and is inert until something# asks for it.provides = [ "media.serve", "kavita:opds",]
# ─── The core capability, and the probes that demonstrate it ─────────────────## The vocabulary says what must be shown; this says where to ask it. A claim that# left a probe unbound, or bound one with an expectation weaker than the probe# permits, is refused naming the probe (ARCH-R109).
[[claim]]capability = "media.serve"
[[claim.probe]]id = "guarded"request = { method = "GET", path = "/api/library/libraries" }expect = { status = 401 }fixture = "fixtures/media-serve-guarded.json"
# `json_array_min = 0` is the assertion "the answer reads as an array", and# nothing about how long it is. That is deliberate: this service answers with a# list rather than an object, so none of the key-wise constraints can say anything# about it — and a probe demanding a *non-empty* list would refuse to install this# on the machine of somebody who has not copied their books over yet. What a check# may assert about the operator's own data, a probe may not.[[claim.probe]]id = "catalogue"request = { method = "GET", path = "/api/library/libraries" }expect = { status = 200, json_array_min = 0 }fixture = "fixtures/media-serve-catalogue.json"
# A list, and one entry, because wiring is a fact about a service rather than# about a plugin (ARCH-R127). A template declaring one service names none in its# entry; add `service = "..."` to each the day this declares two, because a# hostname left to be inferred is an inference about which one the household# reaches.[[wiring]]# A label in front of the operator's own domain, never an address — and a word# the household would go there for rather than the name of the software.hostname = "reader"dashboard_group = "Library"
[requires]capabilities = ["service.add", "service.health.http", "doctor.contribute"]
# ─── Proofs ──────────────────────────────────────────────────────────────────## What must hold before this is installed. Every one asserts a body: a status is# a claim about the network path and not about the service, because Docker# publishes a port by putting a proxy in front of it and that proxy accepts a# connection before knowing whether anything inside is listening (ARCH-R105).
[[proof]]id = "kavita.serves"title = "Kavita answers on its declared health path"fixture = "fixtures/api-health.json"why = """The health probe the manifest declares is what lifecycle waits on, so the firstproof is that the path in the manifest is a path this image serves. It answers`Ok` as plain text rather than JSON, so this asserts the content type and thefirst bytes — which is what asserting a body means where the body is not JSON."""request = { method = "GET", path = "/api/health" }expect = { status = 200, content_type = "text/plain", body_starts_with = "Ok" }
[[proof]]id = "kavita.status-is-not-an-answer"title = "An unimplemented path answers 200, so a status proves nothing here"fixture = "fixtures/api-unimplemented-path.json"why = """Asked for a path it does not implement, this service returns 200 and itssingle-page app. That is not a defect — it is how a client-routed applicationserves deep links — but it means the usual shorthand for "is it up" is worthlessagainst this image.
Recording it is what makes the shape of every other proof here reviewable ratherthan folklore, and turns a change in this behaviour into a red proof rather thana probe that has quietly gone blind."""request = { method = "GET", path = "/api/not-a-real-path" }expect = { status = 200, content_type = "text/HTML", json_is_absent = true, body_starts_with = "<!doctype html>" }
# ─── Contributions ───────────────────────────────────────────────────────────## A row in a register lemonfiber already runs, never an interpreter of its own# (F3-R26). The doctor already runs checks independently, bounds each one, keeps# `unverified` distinct from `pass`, and carries a remedy on anything that does# not pass. This is another row in that register, attributed to this plugin# wherever it appears.
[[contribution]]at = "doctor.check"id = "kavita:settings-guarded"title = "Kavita's server settings are not readable by the household network"category = "network"timeout_s = 10fixture = "fixtures/settings-guarded.json"why = """This service declares `bind = "lan"` and a hostname, which together put it infront of every device on the household network under a name they can remember.What makes that safe is Kavita refusing an unauthenticated read of its ownsettings, and nothing else here would notice if it stopped: the health probewould still pass and the reader would still work.
The `guarded` probe above asks a version of this question once, at install. Thisasks it every time the doctor runs, which is when it matters — an upgrade, areconfiguration or a reverse proxy in front of it are all ways for a service tostop refusing what it used to refuse."""request = { method = "GET", path = "/api/settings" }expect = { status = 401 }
[[contribution]]at = "doctor.remedy"id = "kavita:close-the-settings"for = "kavita:settings-guarded"action = "Stop Kavita and check what is in front of it before starting it again"why = """Kavita's settings are readable without signing in, which means anything on thehousehold network can read them — and on this network that is every phone,television and games console in the house."""detail = """The usual cause is something in front of the service rather than the serviceitself: a reverse proxy configured to strip the Authorization header, or asecond route to it that lemonfiber did not write. Kavita itself refuses thisread on a default install."""Read it for its shape rather than as something to install unchanged. Its
[requires] names service.add, service.health.http and doctor.contribute.
The lemonfiber this site pins offers doctor.contribute and nothing else, so
this build refuses the template as it stands. The disagreement is between the
template and the binary, and the fix belongs in one of those repositories.