ADR-0031: Installing a plugin and choosing a filler are web API writes; replacing a credential is not
Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page
Status: Accepted Date: 2026-09-28 Decided: 2026-09-28, by the maintainer, Wessel Verheij: with the yes to each of these writes being the offer its rehearsal answered, rather than a bare confirm. Amended: 2026-10-04 by ADR-0036, which lets the actions take every source the command takes. Amended: 2026-10-05 by ADR-0037, which makes minting and revoking an integration key the one credential write the web API takes.
Context
G1-R1 requires every action to be available from every surface, except where
it is intrinsically unsuited, and requires each exception to be documented.
ARCH-R48 bounds the web API from the other side: it exposes nothing the
command line cannot do.
The web API already documents one exception. /api/credentials lists every
credential the stack holds and no value, and the two things that can be asked
of a line of it, printing the value and replacing it, are not offered over that
door (the web API contract). Two
reasons are given. A value printed over HTTP passes through a browser’s cache,
any proxy between and the log each keeps. Replacing one is a write a request
could be forged into making against the credential the stack is working on.
Both acts stay at the terminal, in front of the person who typed the
confirmation.
Installing, updating and removing a plugin, and choosing which service fills a
capability, are consequential in the same way. An install adds a stranger’s
service to a working stack, and a choice decides what every consumer of a
capability reaches. In the core all four are command-line only, and F6-R15
and F4-R27 require them on the web API. The companion reaches a stack only
over the web API (N1-R16), so without them its plugin screen and its
contested-capability question (N5, N25) have nothing to act through. A contest
is refused until the operator chooses (F4-R8), and an operator who can read it
on the companion cannot answer it there.
So the question is whether these writes are exceptions in the sense the credential writes are.
Decision
They are not. Installing, updating and removing a plugin (F6-R15), and
choosing which service fills a capability with an optional reason (F4-R27,
F4-R29), are actions of the web API. Each takes what the command takes and
answers as the command answers. Each can be rehearsed over the API, writing
nothing (F6-R16, F4-R28). Printing and replacing a credential stay at the
terminal.
The line between them is what the request carries and what a forged one could do.
- No secret travels. An install names a source by its path on the stack’s machine, an update names a new version’s path, a removal names a plugin id, and a choice names a capability, a service and a reason. None of it is a secret, so nothing sensitive reaches a cache, a proxy or a log. A credential’s replacement carries the secret itself.
- A forged write can only choose among what is already there. An install reads a source already on the machine at the path named, and the manifest decides everything the install writes. The command takes no flag by which a request could ask for terms the manifest did not declare, and lemonfiber writes the container (ADR-0021). A choice is refused unless the service is one of this stack’s and claims the capability. A credential’s replacement installs a value that came from outside the machine, chosen by whoever sent it.
- Each is recorded and can be put back. Every change an install makes is
journalled (
F6-R2), removal reverses it (F6-R7), and a choice is one journalled setting thatundoputs back (F4-R10). The rehearsal states what each would do before it is agreed to (F6-R1,F4-R11). - They sit behind the guards every other write does. The per-run token or
session in a header (
ARCH-R52,ARCH-R76),OriginandHostchecked against the bound address (ARCH-R53), protection against cross-site request forgery (C6-R10), and the core’s refusal of anything a credential is not entitled to (G10-R3): a household member is granted a short list of reads and refused every other command, so none of these writes reaches one.uninstall,restoreandresetare already web API writes on those terms. The capability read that would show a client that answer (ARCH-R80) is not served yet; it reports the refusal and does not enforce it. - The yes is the offer that was read. Each of these is two requests with a
decision between them: a rehearsal states what the write would do, and the
write does it. Between the two, what was read can move. A manifest at the
named path can be replaced, a newer version can land, another plugin can
come to claim the capability, and a choice made meanwhile can settle the
contest another way. So each rehearsal answers an offer, a name built from
what it read. The write takes that offer as its yes, builds the name again
from what is there now, and refuses where the two differ, naming what moved.
A bare
confirmis not a yes to any of the four over the API. This is howrepair,restore,stop-seedinganduninstallalready take theirs. The command takes the same offer (ARCH-R48) (F6-R17,F4-R30).
Alternatives considered
| Option | Why it lost |
|---|---|
| Terminal only, like a credential’s replacement | Leaves N5 and N25 unanswerable, and leaves a contest the companion can show with nowhere to answer it. Neither of the reasons the credential exception rests on applies: no secret travels, and a forged request can only choose among what is already on the machine. |
| Read over the API, act at the terminal | Serves the question and not its answer. An operator told on their phone that a capability is contested and refused has been told about a broken link they cannot mend from there, which is the state F4-R8 exists to keep short. |
A bare confirm, with the journal as the safety net |
The journal puts a change back after it lands; it does not stop a yes from agreeing to something other than what was shown. A manifest replaced between the rehearsal and the yes would install terms nobody read, and a contest settled meanwhile would be overridden by a choice made against a wiring that no longer stands. Undoing afterwards is repair, not consent. Every other write that shows the operator something and then acts on the answer already takes the offer. |
| Install from a URL or a git source over the API | The command takes a path, and ARCH-R48 forbids the API taking more. A source fetched on a request’s say-so reaches the network on behalf of whoever sent the request, which is a different risk from reading a path already on the machine. |
Consequences
Positive
- The companion can install, update and remove a plugin, and answer a contest,
from wherever the operator is.
G1-R1holds for all four with no exception to document. - A choice carries the operator’s reason on both surfaces, so a choice read
later says what it was for (
F4-R29,N5). - A yes agrees to what the operator read and nothing else. A manifest, a version or a wiring that moved after the rehearsal is refused by name, not applied.
Negative
-
A request that passes the guards can install a plugin whose source is already on the machine. The guards are the same ones every other write relies on, so a weakness in them is a weakness for all of them.
-
Installing from a remote source is offered on neither surface, because the command takes only a path.
-
Each of the four becomes two requests with a name carried between them, so a client that sent a bare
confirmis refused and has to rehearse first.
Neutral
- The credential exception is unchanged, and this ADR states why it differs rather than moving it.
Revisit if
- The install command takes a source other than a path on the machine, such as
a git URL (
F5-R4). The web action would then fetch on a request’s say-so, and the second reason above no longer holds as written. - A plugin act or a choice gains an effect outside the machine, or one the journal cannot put back.
- A household credential becomes able to reach any of these writes.
Related
- F6 — the plugin lifecycle, and
F6-R14toF6-R17 - F4 — the capability vocabulary, and
F4-R26toF4-R30 - N5 — what connects to what, on the companion
- N25 — a plugin after it lands
- The web API contract — the credential exception, and the guards
- ADR-0021 — a plugin is data, and lemonfiber writes its container