Skip to content

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.

PluginManifest

A whole plugin manifest.

FieldTypeRequiredWhat it is
askarray ofPluginAskno

The capabilities its services ask for.

claimarray ofClaimno

The core capabilities this plugin claims, and the probes each is shown by.

contributionarray ofContributionno

The rows it adds at the points lemonfiber publishes.

overridearray ofOverrideno

Every bundled thing it will change.

pluginPluginyes

Who the plugin is, and where it came from.

proofarray ofProofno

What must hold before it is installed.

recipearray ofRecipeno

The ordered calls that configure what it installed.

requiresRequiresornullno

What the plugin needs of lemonfiber.

schema_versionintegeruint32, ≥ 0yes

The manifest format generation.

secretarray ofSecretno

Every value it will hold.

servicearray ofPluginServiceno

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.

wiringarray ofWiringno

How the stack's own proxy and dashboard reach its services. One each, at most.

Capture

A value taken out of an answer.

FieldTypeRequiredWhat it is
fromstringyes

Where in the answer it is read from: an expectation key, or header.<name>.

namestringyes

What the value is called, for a later call to name.

originPluginValueOriginyes

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.

FieldTypeRequiredWhat it is
capabilitystringyes

A core name, which must also appear in the service's provides.

probearray ofClaimProbeno

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.

FieldTypeRequiredWhat it is
expectExpectyes

What the answer must be, within what the probe permits.

fixturestringyes

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.

idstringyes

Names a probe the capability declares.

requestPluginRequestornullno

Method and path on this plugin's own service. Exactly one of this and run, and this where the vocabulary declares the probe a request.

runPluginRunornullno

Arguments to the image's own entrypoint, run with no network. Exactly one of this and request, and this where the vocabulary declares the probe a run.

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.

FieldTypeRequiredWhat it is
actionstringornullno

What to do, in the imperative.

atstringyes

A point the build publishes. One it does not is refused by name.

categorystringornullno

Which family a check is narrowed to.

detailstringornullno

The technical half of a remedy, which must not lead.

expectExpectornullno

What the answer must be.

expectedarray ofPluginExpectedFailureno

Recordings the check fails on, each with the constraint that fails there and why.

fixturestringornullno

The recorded response the check is proved against.

forstringornullno

The check a remedy is for, which must be one this same plugin declared.

idstringyes

Namespaced with the declaring plugin's id, always.

requestPluginRequestornullno

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.

servicestringornullno

Which service the finding is about. Defaults to the plugin's own.

timeout_sintegerornulluint32, ≥ 0no

How long a check may run, within the bounds the point declares.

titlestringornullno

The one-line summary of what was checked.

whystringornullno

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.

FieldTypeRequiredWhat it is
body_starts_withstringornullno

What the body must begin with, where it is not JSON.

content_typestringornullno

A substring of the content type the answer was served as.

exitintegerornullint64no

The exit status a run must end with. Not sufficient alone.

jsonmap ofExpectedornullno

Places the answer must carry, each with the exact value it must hold.

json_array_minintegerornulluint64, ≥ 0no

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_leastmap ofintegerint64ornullno

Places the answer must carry, each with a number it must not be below.

json_has_keysarray ofstringornullno

Places the answer must carry, whatever they hold.

json_is_absentbooleanornullno

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 200 whether the API behind it exists or not, so what has to be said is that the body was not a document.

json_typesmap ofExpectedKindornullno

Places the answer must carry, each with the kind of value it must be.

statusintegerornulluint16, ≥ 0, ≤ 65535no

The status the answer must carry.

stdout_starts_withstringornullno

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.

FieldTypeRequiredWhat it is
askstringornullno

What the operator is asked, in one line, where the operator supplies it.

namestringyes

What the value is called, for a call to name.

ofstringornullno

The service whose credential it is, where the credential store holds it.

originPluginValueOriginyes

Whose value it is: the credential store, or the operator.

secretbooleanno

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.

FieldTypeRequiredWhat it is
idstringyes

Which bundled setting is changed.

whystringyes

What changing it is for.

Pair

One value, and the one destination it may reach.

FieldTypeRequiredWhat it is
releasestringornullno

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.

tostringyes

Where it may be carried.

valuestringyes

The value, by the name its capture or its input gives it.

Plugin

Who the plugin is, and where it came from.

FieldTypeRequiredWhat it is
descriptionstringyes

What it does for the operator.

formsarray ofstringyes

Which forms the service joins.

idstringyes

Unique, lowercase. The name it is installed and journalled under.

licensestringyes

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.

namestringyes

Human-facing.

upstreamstringyes

Project URL, so the operator can judge the thing rather than the wrapper.

versionstringyes

Semver. The plugin's own content version, moved by its author.

without_itstringyes

The consequence of its absence.

PluginAsk

One capability a plugin's service asks for.

FieldTypeRequiredWhat it is
capabilitystringyes

The core capability asked for.

eachbooleanno

Whether every service that fills it is reached rather than one.

nativestringornullno

The upstream's own API the service talks in, which the ask then reaches in place of the adapter fronting it.

servicestringornullno

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.

FieldTypeRequiredWhat it is
constraintPluginConstraintyes

The key of the expectation that fails on the recording, and one it carries.

fixturestringyes

The recording the assertion fails on. It may be the assertion's own fixture.

placestringornullno

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.

reasonstringyes

Why this recording is one the assertion fails on. Reported with the verdict every time.

verdictPluginDeclaredVerdictyes

fails, and nothing else: passing is what the expectation already says, and could-not-run is never excused.

PluginHealth

How to tell a plugin's service has started.

FieldTypeRequiredWhat it is
kindPluginHealthKindyes

Which kind of probe to use.

pathstringornullno

HTTP path, for http probes.

timeout_sintegerornulluint32, ≥ 0no

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.

FieldTypeRequiredWhat it is
equalsstringornullno

The text it must hold, exactly.

statusintegerornulluint16, ≥ 0, ≤ 65535no

The status that step must have answered.

stepstringornullno

The earlier step whose answer is asked about.

valuestringornullno

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.

FieldTypeRequiredWhat it is
everystringyes

How long it waits between two, written as whole seconds, 6s, and no longer than [LONGEST_WAIT].

timesintegeruint32, ≥ 0yes

How many times it is made again, at most: no more than [MOST_RETRIES].

untilPluginRecipeConditionyes

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.

FieldTypeRequiredWhat it is
acceptstringornullno

The one representation the answer is asked for, as a media type.

Absent where the service needs no asking, which is most of them.

methodstringyes

The HTTP method.

pathstringyes

The path on the service being asked.

PluginRun

A run of the image's own program, as a proof declares it.

FieldTypeRequiredWhat it is
argsarray ofstringyes

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_sintegerornulluint32, ≥ 0no

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.

FieldTypeRequiredWhat it is
asks_forstringornullno

What it asks for out there, in the same terms; never blank.

bindPluginBindornullno

Which tier every port it publishes is published on.

config_pathstringornullno

Where inside the container its one configuration directory is mounted, /config where absent.

config_robooleanno

Whether its configuration directory is mounted read-only.

criticalityPluginCriticalityyes

How much its absence costs.

digeststringyes

sha256:…. What actually runs.

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.

envmap ofServiceValueno

Its environment, each variable one of four typed kinds of value.

frontsstringornullno

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 speaks is.

healthPluginHealthornullno

How to tell the service has actually started.

idstringyes

Unique across the stack and every installed plugin.

imagestringyes

Registry path, without tag or digest.

listensintegerornulluint16, ≥ 0, ≤ 65535no

The port it answers on inside the stack's network, where lemonfiber and the services that ask reach it. Required where speaks is.

media_typesarray ofstringno

Which media types it handles, in the stack manifest's vocabulary.

mem_limit_mibintegerornulluint32, ≥ 0no

The most memory the container may hold, in MiB.

memory_mibintegerornulluint32, ≥ 0no

The memory it expects to need, in MiB, never written as a limit.

mountarray ofServiceMountno

Each further path mounted from its own mount tree.

namestringyes

Human-facing.

nativestringornullno

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.

networkServiceNetworkornullno

The capability whose filler's network it runs inside.

portintegerornulluint16, ≥ 0, ≤ 65535no

Where it is published on the host, absent for a service that publishes nothing.

postureServicePostureno

How tightly the container is held.

providesarray ofstringno

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.

publisharray ofServicePublishno

Each further port it publishes on its tier.

reachesstringornullno

Where it reaches outside the machine, in words an operator reads, as a stack service says it; declared with [Self::asks_for] or not at all.

replacesstringornullno

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.

shapePluginShapeornullno

The one privileged shape lemonfiber writes for it, from a closed set.

speaksarray ofstringno

Each capability contract it answers, as capability@major: the service is an adapter, and lemonfiber asks it over those contracts.

tagstringyes

The human-readable version the digest corresponds to. Recorded and shown; never resolved.

takes_databooleanno

Whether it takes the data mount.

tmpfsarray ofstringno

Absolute paths given scratch space in memory.

upstreammap ofServiceSecretOfno

Each field of the upstream.json lemonfiber writes for this adapter, to the secret it holds.

user_modeServiceUserModeornullno

Who the container runs as.

PluginShape

A privileged shape lemonfiber writes for a service.

One of these:

  • "egress-guard"

    NET_ADMIN and 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.

FieldTypeRequiredWhat it is
expectExpectyes

What the answer must be.

expectedarray ofPluginExpectedFailureno

Recordings this proof fails on, each with the constraint that fails there and why.

fixturestringornullno

A recorded response to run against where no instance exists.

idstringyes

Unique within the plugin. What a verdict is reported against.

requestPluginRequestornullno

Method and path. Exactly one of this and run.

runPluginRunornullno

Arguments to the image's own entrypoint, run with no network. Exactly one of this and request.

servicestringornullno

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.

titlestringyes

What it establishes, in one line.

whystringyes

Why this is worth asserting. A proof nobody can justify is one nobody will maintain.

Recipe

One named flow of calls.

FieldTypeRequiredWhat it is
idstringyes

Unique within the plugin.

inputarray ofInputno

The values it brings in from outside an answer: a credential lemonfiber holds, or one the operator supplies when it runs.

onPluginRecipeOnno

When it runs: as part of installing the plugin, or when it is asked for.

pairarray ofPairno

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.

steparray ofStepno

The calls, in the order they are made.

titlestringyes

What the flow accomplishes, in one line.

whystringyes

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".

FieldTypeRequiredWhat it is
capabilitiesarray ofstringno

What lemonfiber must offer for this plugin to be installable.

Secret

A value a service holds, declared before anything captures or enters it.

FieldTypeRequiredWhat it is
carried_fromSecretCarriedFromornullno

Where the service a part moves in place of wrote this credential itself.

forstringornullno

The capability of the plugin's own that a credential crossing to it is needed to fill.

idstringyes

What the value is, within its namespace.

mintedSecretMintedno

Whether lemonfiber makes the value at install, rather than anything capturing or entering it, and in what shape.

ofstringyes

Whose credential it is.

recorded_withRecordedWithornullno

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.

whystringyes

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.

FieldTypeRequiredWhat it is
filestringyes

The file, as a path inside the service's container beneath its configuration directory.

formatSecretFormatyes

The format the file is written in.

keystringyes

The dotted path to the value in the file; for ini, its section and entry.

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.

FieldTypeRequiredWhat it is
fromstringyes

The relative path inside the mount tree.

robooleanno

Whether it is mounted read-only.

tostringyes

The absolute path inside the container.

ServiceNetwork

The network a service runs inside instead of its own.

FieldTypeRequiredWhat it is
viastringyes

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.

FieldTypeRequiredWhat it is
listensintegerornulluint16, ≥ 0, ≤ 65535no

Where it answers inside the container, port where absent.

portintegeruint16, ≥ 0, ≤ 65535yes

Where it is published on the host.

ServiceSecretOf

A secret the manifest declares.

FieldTypeRequiredWhat it is
secretstringyes

The declared secret's name.

ServiceSetting

An operator setting, with what is written while none is recorded.

FieldTypeRequiredWhat it is
defaultServiceConstantyes

What is written while the operator has recorded no value.

recorded_withRecordedWithornullno

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.

settingstringyes

The setting's name.

ServiceUrlOf

The address of a capability's filler.

FieldTypeRequiredWhat it is
filesstringornullno

The medium the filler asked for files.

nativestringornullno

The upstream's own API, where the ask reaches the upstream an adapter fronts.

url_ofstringyes

The capability asked for.

ServiceUserMode

Who the container runs as.

One of these:

  • "image"

    No user: the image decides.

  • "puid-env"

    PUID and PGID set 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.

FieldTypeRequiredWhat it is
callStepCallyes

What is called, and where.

capturearray ofCaptureno

The values taken out of the answer, for a later call to substitute.

expectExpectornullno

What the answer must be for the flow to go on.

idstringyes

Unique within the recipe.

retryPluginRecipeRetryornullno

How often the step is made again before its answer is taken as final.

whenPluginRecipeConditionornullno

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.

FieldTypeRequiredWhat it is
bodystringornullno

The body the call carries. A value may substitute an earlier capture.

headersmap ofstringornullno

Headers the call carries. A value may substitute an earlier capture.

methodstringyes

The HTTP method.

pathstringyes

The path on it, written out. Never a substitution.

tostringyes

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.

FieldTypeRequiredWhat it is
dashboard_descriptionstringornullno

The line beside its entry on the bundled dashboard. Defaults to the plugin's description.

dashboard_groupstringornullno

Which group on the bundled dashboard it appears under. Defaults to the group the stack uses for its tier.

dashboard_hiddenbooleanno

Whether it has no entry on the bundled dashboard.

dashboard_iconstringornullno

The icon its entry on the bundled dashboard shows, named from the dashboard's own icon set. Defaults to the service's id with .png after it.

hostnamestringornullno

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.

servicestringornullno

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.

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.

plugin.toml, from 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 = true
media_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 first
proof 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 the
first 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 its
single-page app. That is not a defect — it is how a client-routed application
serves deep links — but it means the usual shorthand for "is it up" is worthless
against this image.
Recording it is what makes the shape of every other proof here reviewable rather
than folklore, and turns a change in this behaviour into a red proof rather than
a 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 = 10
fixture = "fixtures/settings-guarded.json"
why = """
This service declares `bind = "lan"` and a hostname, which together put it in
front 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 own
settings, and nothing else here would notice if it stopped: the health probe
would still pass and the reader would still work.
The `guarded` probe above asks a version of this question once, at install. This
asks it every time the doctor runs, which is when it matters — an upgrade, a
reconfiguration or a reverse proxy in front of it are all ways for a service to
stop 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 the
household 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 service
itself: a reverse proxy configured to strip the Authorization header, or a
second route to it that lemonfiber did not write. Kavita itself refuses this
read 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.