← The specification
ADR-0052: A curator's downloads complete into one folder, and the core tells each part what it holds
Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page
Status: Accepted Date: 2026-10-22 Decided: 2026-10-22, by the maintainer, Wessel Verheij:
- completed downloads go to
/data/downloads/complete/<category>, one folder per category that both download clients share, so one curator’s downloads land in one folder; download.usenetanddownload.torrenteach gainfile_under, which hands the client each category and its completed folder, andfiled, which reads that back; the seed asks every client this way, a bundled client through its in-process adapter and a plugin’s over its contract, keeps a folder the operator changed by hand in the client and reports it drifted; the extractor is handed the folder the client holds, asfiledreads it;- the core is also the asker of
dashboard.show: over the contract where the filler speaks it, and where the filler is the assembled Homepage, which speaks none, through an in-process adapter that writes Homepage’s own files, as a request gate speaking no contract is told through the files it reads; the page’s web search box takes its address from a setting; - an assembled part’s
[[secret]]whoseofnames another service is a credential that crosses: it is read from its holder each time and never copied into the secret store, and only a secret the operator enters is held there, as<service id>:<secret id>; - the core confirms a live-stream pass through its own pass service, the lemonfiber binary
run as a service of the project (
lemonfiber pass), joined to the guard alone and holding the pass key read-only, so the key never leaves lemonfiber’s own processes;apply a guardhands the guard that service’s address as where it asks about a pass; - a network’s subnet is derived from the network’s name alone, so one machine runs one lemonfiber stack, as a deliberate limit.
Amends:
- ADR-0006: the tree under
downloads/. Each client keeps its ownincomplete/, and both complete intodownloads/complete/<category>; - ADR-0041: §6. The core
asks
dashboard.showtoo; - ADR-0046:
§2. Of an assembled part’s secrets, only one the operator enters is held in the secret
store; one whose
ofnames another service is read from its holder; - ADR-0051:
§4. The core asks
dashboard.showas well asquality.sync,archive.extract,stream.guardandinvite.decline.
Context
Measured on 2026-10-22 against the core at 36f57306, lemonfiber-plugins at 116356c
and this specification at 8779e3d:
- A curator’s downloads land in two folders. ADR-0006’s tree gives each download
client a
complete/of its own,downloads/usenet/complete/<category>anddownloads/torrents/complete/<category>.archive.extract’swatchtakes one download folder per curator (ARCH-R306), so a curator taking both usenet and torrents has completed downloads in a folder the extractor is not told about. - No client is told where a category completes. The download contracts have no
operation naming a folder, and the core sets none on either client: SABnzbd files under
the category’s own name below its own complete directory, as its starting file leaves
dirempty, and qBittorrent keeps its defaults. A folder handed to the extractor can only be what the core assumes each client chose. - Nothing applies the dashboard a panel list. Homepage speaks no contract, no
in-process adapter applies it
dashboard.show’s panel list, and the core writes a plugin’s entries straight intoconfig/homepage/services.yaml. The page’s search box is fixed in its startingwidgets.yamlas DuckDuckGo. The request gate, which speaks no contract either, is told its routes by the core through the two files it reads (app/gating/configured.rs), behind the same port a contract-speaking gate is asked over. - Two secret rules disagree.
ARCH-R275holds every[[secret]]an assembled part’s plugin declares in the secret store, andARCH-R277holds that a credential crossing to a plugin is never copied there. Unpackerr’s and Recyclarr’s manifests declare the curators’ keys as[[secret]]withofnaming each curator, so both rules claim them. - The core cannot answer on the guard’s network.
ARCH-R291has the core confirm a pass at an endpoint on the guard’s network alone, and the core runs on the host and joins no network of the project, so no address on the guard’s networks is the core’s. - Subnets cannot be shared between two projects.
ARCH-R310chooses each network’s/29from its name, the same on every project, and the two fixed subnets are the same on every project too. A second project on the machine would ask the engine for subnets the first holds.
Decision
- A curator’s downloads complete into one folder. Each download client keeps its own
downloads/usenet/incomplete/ordownloads/torrents/incomplete/, and both file a completed download underdownloads/complete/<category>, so/data/downloads/complete/tvholds every completed television download whichever client fetched it.watchkeeps one folder per curator (ARCH-R306). - A download client is told its completed folders, and read back.
download.usenetanddownload.torrenteach gain two operations:file_under, handing the client each category and its completed folder, andfiled, answering every category and the completed folder the client holds for it. The seed asks every client this way, through its in-process adapter while the client is an assembled part and over its contract once a plugin fills the capability. A folder the operator changed by hand in the client is kept and reported drifted (D1-R3). The extractor is handed, for each curator, the folderfiledreads back for the curator’s category, not the one lemonfiber wrote (ARCH-R311). - The core asks the dashboard. The core applies the panel list to the filler of
dashboard.show: over the contract where the filler speaks it, and where the filler is the assembled Homepage, through an in-process adapter that writes the files Homepage reads and reads them back, as the request gate is told through its files. The page’s web search box sends a search to the address the core settingLEMONFIBER_DASHBOARD_SEARCHholds (ARCH-R308). - A crossing secret stays where it is held. An assembled part’s
[[secret]]whoseofnames another service is a credential that crosses (ARCH-R277): it is read from the store that holds it each time an entry naming it is written and is never copied into the secret store. A secret the operator enters is held as<service id>:<secret id>(ARCH-R275). - The core confirms a pass in a service of its own. The pass service is the lemonfiber
binary of the running release, run as a service of the project with
lemonfiber pass, on one network it shares with thestream.guardfiller alone, and holding the pass key read-only. The core issues passes and the pass service confirms them, so the key is held by lemonfiber’s processes and by nothing else.apply a guardhands the guard the pass service’s address as where it asks about a pass (ARCH-R291,ARCH-R303,ARCH-R312). - One machine runs one stack. Every subnet is derived from the network’s name alone,
so a second project on the machine names the subnets the first holds and the engine
refuses its networks. This is a limit lemonfiber keeps, not a fault it hides
(
ARCH-R310).
Consequences
- An extractor watches one folder per curator and sees every completed download the curator asked for, from either client.
- A client’s completed folders are a seeded value, read back and held to drift like every other, and a substitute client is told them over its contract on the same terms.
- A substitute dashboard is applied the same panel list as Homepage, and the core’s write
into
services.yamlbecomes the adapter’s. - The curators’ keys reach Unpackerr and Recyclarr from the store that holds them, so replacing a curator’s key regenerates both, and no stale copy sits in the secret store.
- The project holds a service that neither a plugin nor the bundle declares, the pass service, and a guard asks it over the network it shares with the guard alone.
- An operator who wants two stacks runs them on two machines.
Alternatives considered
| Option | Why it lost |
|---|---|
Two folders per curator in watch |
Every extractor would watch two trees for one curator, and the two clients’ choice of folder would stay a guess. |
| The folders written into each client’s starting files | A bundled client could be given them, a substitute could not, and nothing would read back a folder the operator changed. |
| The completed folder overwritten on every seed | It would undo an operator’s choice, which the seed keeps for every other value (D1-R3). |
| The core asking only a dashboard that speaks the contract | The core would go on writing Homepage’s files outside any port, and a substitute dashboard would be told nothing. |
| The search box fixed in the starting files | The operator could not change it without editing a file lemonfiber writes. |
| Every assembled secret copied into the store | Two copies of a curator’s key, and replacing it leaves the copy stale (ARCH-R277). |
| The core answering passes on a port published on the host | Any process on the machine could reach it, and the guard would need a route to the host. |
| The pass key given to the guard | The guard would stop being a stock filler of stream.guard, and a substitute guard would hold the key. |
| Subnets derived from the project’s name as well | Two projects would still draw from one range and could meet at one slot, so the limit would become a failure that strikes at random rather than a rule stated. |
Revisit when
- A client cannot file a category’s completed downloads under a folder it is told.
- A dashboard filler reads its configuration from somewhere no adapter can write.
- An operator needs two stacks on one machine.