Contract: plugin.toml — [[secret]] and [[override]]
Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page
Status: Accepted
Part of the plugin.toml contract: the secrets a plugin holds and
the bundled things it changes, each declared before it is held. The requirements this
page describes are listed with the rest of the contract’s, under
Requirements.
[[secret]] and [[override]] — declared before they are held
[[secret]]
id = "api-key"
of = "komga"
why = "Read the library counts the dashboard panel shows"
[[secret]]
id = "series-key"
of = "sonarr"
for = "subtitle.find"
why = "Read the series it finds subtitles for"
[[secret]]
id = "upstream-key"
of = "sabnzbd"
minted = true
why = "The key the adapter signs in to SABnzbd with"
[[override]]
id = "homepage.services"
why = "Add its own entry to the bundled dashboard"
F3-R17 fails validation on a secret captured but not declared, and F3-R18 on
a bundled thing changed but not declared. Both need somewhere to declare one.
A secret a service’s env names is declared here
too. Its value is one lemonfiber mints, one a recipe captures, a credential F14-R18 or
F14-R20 lets cross to the plugin, or, where none of those supplies it, one the operator
enters at install without it being shown. Whatever its source, it reaches the container only through the service’s
own secrets file (ARCH-R247). A value captured or entered is held in the secret store in
the plugin’s namespace, as <plugin id>:<secret id>. An assembled part’s plugin id is its
service’s, so the egress guard’s key is gluetun:wireguard-key whether the bundle
assembles the part or installs it. Of an assembled part’s secrets, only one the operator
enters is held in the store; one whose of names another service, as the curators’ keys
Unpackerr and Recyclarr declare do, is a credential that crosses (ARCH-R275).
A minted secret is lemonfiber’s to make. minted = true declares a credential the
plugin’s own service is given rather than one it hands out: lemonfiber makes the value
at install, before any of the plugin’s services first starts, as 32 lowercase hex
characters from the operating system’s random source, and holds it in the secret store
as <plugin id>:<secret id>. Nobody is asked for it and nothing shows it. It reaches the
service through its env ({ secret }) or a starting file,
and the adapter that fronts the service through its upstream. A minted secret’s of
names the plugin’s own service it is the credential of, and no recipe captures it. A part moved in place
keeps the credential its service already answers to: where lemonfiber reads it from the
service’s configuration before the move, the move files that value under the secret once
and mints nothing (ARCH-R315). plugin record and a proof mint a throwaway one for
their own container, as they take a throwaway copy of its starting files.
A credential that crosses is read where it is held. One F14-R18 or F14-R20 lets
cross is not copied into the secret store. lemonfiber reads it from the store that holds
it each time it writes an entry naming it, so replacing it regenerates every entry that
reads it, and no stale copy is left behind (ARCH-R277).
A third-party plugin is granted what crosses to it. A [[secret]] whose of names
another service declares for, the capability one of the plugin’s own services provides
that the credential is needed to fill. A third-party plugin declares at most one such
secret for each capability it fills; a manifest declaring two for one capability, or a
for naming a capability none of its services provides, is refused at install and
update, naming the secret (ARCH-R322). A first-party plugin may leave for out,
because F14-R18 lets the stack’s credentials cross to it whole.
What crosses to a third-party plugin is a grant, and the operator gives it. The install
or update reading names each credential that would cross as <setting>@<plugin>, with
the capability it is for and the service whose credential it is; the credential is named
by its setting, as the credentials read lists it, never by value. The act
takes the grant as another approval (--approve SONARR_API_KEY@comics), and refuses
unless every grant the reading names was approved. An update names every grant again
and takes none over unapproved: one the operator leaves out stops crossing when the
update is made. Each grant given or ended is journalled with the operation that gives or
ends it, by credential and plugin, so undoing that operation ends or restores the grant
(ARCH-R323). A granted credential crosses as one F14-R18 lets cross does: read where
it is held, into the plugin’s secrets file and into each connection an ask of the plugin
makes to the service whose credential it is.
A grant is revoked at the credentials surface. lemonfiber credentials revoke <plugin> <credential> and the credentials-revoke action end one grant: every entry and
connection of the plugin is written again without the credential, the plugin’s services
that read it restart, and the whole is journalled as one operation that undo reverses.
It is rehearsed and takes an offer as a plugin write does. The plugin stays installed,
and each capability the credential was for is reported unfilled for that reason; a
plugin and credential with no grant between them are refused, naming the grants the
plugin holds (ARCH-R324). The credentials read lists every grant beside the credential
it crosses: the plugin, the capability it is for and when it was approved, with no value
(ARCH-R325).
The journal holds a secret’s fingerprint, never its value. A secret’s change is
journalled with the stable fingerprint of the value before and after it, as
A7
references a credential, and never with the value, sealed or otherwise. The secret store
keeps each secret’s one previous value. Undoing the change restores it only while the
values held still match the fingerprints the entry recorded; otherwise the undo says the
secret must be replaced at the terminal, naming it (ARCH-R276).
What counts as capturing, and what counts as changing. A recipe is declarable before it is runnable and its block is where both verbs are written, so both rules are decided by reading the manifest rather than by watching one run:
| Declared | Held to |
|---|---|
[[secret]] |
every [[recipe.step]].capture |
[[override]] |
every [[recipe.step]].call whose method is not one that only asks and whose to is a service the stack ships |
A secret is matched on the captured value’s name and nothing else. of and
a capture’s origin both answer whose, and they answer it in different
vocabularies — one names a service, the other a kind of source — so holding them
to each other would be inventing a correspondence this contract does not state,
and refusing manifests for not keeping it. What both do name is the value. A
capture whose name no [[secret]] carries is refused, naming the value and its
origin.
Every captured value, rather than the ones that look like credentials. There is no field saying which is which and there should not be: a library id read back from a first-run flow is a value the plugin now holds and can carry somewhere, and a format in which the author decides what is worth declaring is one where the interesting cases are the ones nobody declared.
An override names an owner and what of theirs, and the owner is the half a
call can be held to by reading, because a path is a route on a service rather
than the name of a setting. Three destinations, and only one of them is this: a
call to one of the plugin’s own services changes what the plugin installed and
is what a recipe is for; a call to a DNS name outside the stack leaves the
machine and is declared as a host rather than as an override; a call to a
service the stack ships is refused, naming the call and the service, unless an
[[override]] names that owner. The verbs that change are written as the
complement of the ones that only ask, so a verb added to what a call may use is
a change its author has to declare rather than one nobody is told about.
So both blocks are absent from a manifest declaring no recipe, and are filled in
one that declares a recipe which captures or changes anything. An earlier
reading of this section — that both are in practice always absent until F8
arrives in 0.18.0 — held only while a recipe could not be written down at all.
They are in the format for the reason they always were: a rule that cannot be
stated for want of a field is not being enforced, and adding the field later
would make every manifest written against this version wrong.