ADR 0051 — shared data is the operator's; a module is granted access (fixes issue 036) #26

Merged
jschoubben merged 2 commits from feat/adr-0051-shared-data-operator-owned into main 2026-09-05 20:29:45 +00:00
3 changed files with 171 additions and 4 deletions
@@ -0,0 +1,166 @@
---
topic: what runs on it
status: accepted
date: 2026-09-05
deciders: jochen
reconstructed: false
extends: 0030-data-outlives-the-mesh-that-declared-it.md
---
# 51. Shared data is the operator's, and a module is granted access to it
## Context
**Eight modules declared one filesystem as eight private ones.** The media stack — a library
server, the acquisition managers for films, series, music and books, a subtitle fetcher and two
download clients — shares directories on one machine: the download clients write into
`/services/media/downloads` and the managers read it; the managers write into the libraries and
the library server reads them. That sharing is the entire point of the stack. Yet each module
declared every shared directory it touched as its own `directory` resource, with an owner and a
mode. `/services/media/downloads` was written seven times, as seven private directories that
happen to be the same path.
**The resolver refuses exactly that, and is right to.** Two modules declaring one path on one node
are refused by name, with no exemption for identical content and no merge — because two owners of
one path is the class of fault this repository keeps recording ([04-ISSUES/036](../04-ISSUES/036-six-modules-own-what-they-must-share/00-report.md)).
So the stack as written refuses its own only sensible assignment: all of it on one machine,
sharing one filesystem. It passed today only because no test co-resolves any two of the eight. The
first machine assigned two of them together is where the refusal would have surfaced.
**The vocabulary had one word for two intentions, and this was already seen.**
[04-ISSUES/026](../04-ISSUES/026-the-data-directories-are-not-declared/00-report.md) found the
same gap from the other side and named it precisely: two kinds of mount are spelled identically —
*the directory my data lives in*, which the mesh creates and owns, and *a facility I was granted*,
which already exists and the mesh only reaches. That issue deferred inventing a field to tell them
apart, because doing so is a design decision and it declined to make one to get a check green. This
is that decision.
**A `directory` resource is owned, on every axis.** The host creates it, sets its owner and mode,
and removes it when it is empty and no longer declared — it *removes what it made and leaves what
it merely configured* ([ADR 0005](0005-the-node-host.md), [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)).
A shared media library is none of that. It existed before the mesh, several modules read and write
it at once, and losing it is the one failure that does not recover. It is not any module's
resource; it is the operator's, and a module only needs to be let at it.
## Considered Options
1. **Make the stack one module with several containers**, the way the mail system already is. The
issue raises it directly: is a set of modules that must share a filesystem really one module?
**Rejected.** The eight are independently assignable and independently useful — a person may run
the download client without the library server, or the film manager without the music one — and
folding them into a single module to express a shared directory would make *what a module is*
turn on an incidental filesystem contract. It also does not generalise: the next pipeline of
modules handing files to each other on one machine (an ingest folder, a spool, a drop directory)
would face the same wall and the same wrong remedy.
2. **One module owns the directories and the rest `require` them.** **Rejected**, and this is the
heart of the decision. Nobody owns shared operator data. The library predates the mesh and
outlives any one module, so making the library server or a manager its owner means unassigning
that module orphans everyone else's access — and the owner would set the owner and mode of a
tree it did not create. Ownership is the wrong relationship to model, because the true owner is
not a module at all.
3. **A flag on a directory resource** — `external: true`, or an owner of `operator`. **Rejected.**
It overloads one shape with a boolean that inverts every one of its semantics: created becomes
*must already exist*, owned becomes *touch nothing*, removed-when-empty becomes *never removed*.
That is the *two-kinds-spelled-identically* trap of 04-ISSUES/026 reintroduced with a single
quiet field — a reviewer reading `type: directory` would have to check one boolean elsewhere to
know whether the mesh owns the thing at all.
4. **A distinct `accesses` declaration, separate from resources.** **Adopted.**
## Decision
**Shared, pre-existing data is operator-owned and external. The mesh does not create it, does not
set its owner or mode, does not reconcile it and does not remove it.** A media library, a download
spool, an ingest directory is the operator's, and the mesh is a guest in it.
**A module declares that it needs *access* to such a path, not that it owns a resource there.** The
manifest field is `accesses`: a list of `{path, mode}`, where mode is `read` or `read-write` and
absent narrows to `read` — the safe default, because the danger with an access is being given more
than was meant, not less. A module's own configuration and state directories stay owned
`directory` resources; only the shared, pre-existing paths become accesses.
**The host mounts an accessed path and owns nothing about it.** It reaches the machine as a new
declaration shape, `access`, distinct from `directory`. The host confirms the path is present and
does nothing else — no create, no chown, no mode, no removal.
**An accessed path absent at apply time is refused, clearly, not created.** The mesh does not own
it, so conjuring it would be a lie the host then acts on — and specifically the lie 04-ISSUES/026
records, where a bind mount whose source does not exist is made by the container runtime as root
with the wrong ownership. The host says the operator must provide the path instead.
**Several modules accessing one path is normal, and never refused.** The duplicate-path refusal is
about *ownership*, not *use*: it applies to resources a module owns and to those alone. An access
is not a resource and never enters the check, so the eight-module stack co-resolves. What stays
refused is genuine rivalry — two modules owning one path — and the new contradiction it exposes: a
path one module owns while another merely accesses it, because that asserts both that the mesh owns
the directory and that the operator does.
This is a decision and not a patch because it settles *what a module may say about a path it did
not make*, which every co-located file-handoff in the catalogue now and later depends on — and
because it draws the ownership line [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)
started: the host owns what it made and keeps what it merely configured, and this adds the third
case it did not have a word for — what it neither made nor configured, and must not touch.
### How each claim is checked
- **The stack co-resolves.** A control-plane unit test assigns two modules that declare access to
one path on one node and asserts no refusal — the exact case the resolver refuses when the same
path is owned. The mirror test, two modules *owning* one path, still refuses, so the sharing
vocabulary does not weaken the rule it sits beside.
- **Ownership and access cannot both be claimed of one path.** A unit test asserts the resolver
refuses a path one module owns and another accesses, naming both.
- **Absent is refused, not created.** A host unit test applies an access to a path that does not
exist and asserts a clear refusal that names the operator, and that nothing was created.
- **Present is confirmed and nothing moves.** A host unit test applies an access to an existing
directory and asserts the apply reports no change and disturbs nothing.
- **Undeclaring never removes.** A host unit test drops a previously declared access and asserts
the operator's directory and its contents are left exactly as they were — the data-loss failure
[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) exists to prevent, on a directory the
mesh never made.
- **Every media manifest is corrected.** No `/services/media/*` path is an owned `directory`
resource in any of the eight; each is an `accesses` entry, and each module's own config and state
directories remain owned. Checked by the control-plane manifest parser, which now understands
`accesses` and refuses a malformed one.
## Consequences
**A shared filesystem between co-located modules now has a vocabulary**, and it is not the media
stack's alone: any pipeline handing files to a neighbour on one machine — an ingest directory, a
spool, a drop folder — says *I access this operator path* rather than *I own this directory*, and
several of them may say it of one path.
**Unassigning a module that reached shared data leaves the data.** Correct, and the same trade
[ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) made for owned directories: removing
data is a person's act, done knowingly, not a side effect of unassignment.
**The operator must provision the shared paths before the stack is applied**, and a machine that
lacks one is told plainly which. That is a real new obligation, and it is the right one: the mesh
cannot own what predates it, so it cannot create it either, and saying so at apply time beats a
directory conjured as root and a service that half-works.
**The host vocabulary grew by one shape**, which is a cost — every added shape widens what a
compromised control plane can express ([ADR 0005](0005-the-node-host.md)). It is a narrow one: an
`access` is confirmed by a stat and grants the host no new action. It earns its place by letting
the host refuse to create what it must not own, which no existing shape could say.
**A path can be both owned and accessed only by refusal.** If a future manifest declares one path
as an owned directory in one module and an access in another, the resolver refuses it rather than
guessing which is meant — the two assertions about who owns the data cannot both hold.
## References
- [04-ISSUES/036](../04-ISSUES/036-six-modules-own-what-they-must-share/00-report.md) — six (in
fact eight) modules own what they must share; the problem this resolves
- [04-ISSUES/026](../04-ISSUES/026-the-data-directories-are-not-declared/00-report.md) — the two
kinds of mount spelled identically, which deferred this field to a decision
- [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) — data outlives the mesh; the host
keeps what it did not make. This record adds the case it had no word for
- [ADR 0005](0005-the-node-host.md) — the host removes what it made and leaves what it merely
configured; the vocabulary is finite and every shape is a security decision
- [ADR 0040](0040-what-a-module-is.md) — what a module is; an access is a new thing a module may
say about the machine it lands on
- mesh-control `feat/shared-data-access`, mesh-catalog `feat/media-access-not-ownership`,
mesh-host `feat/mount-operator-owned` — the mechanism, the corrected manifests, and the host
shape
+1
View File
@@ -119,6 +119,7 @@ python3 00-META/checks/index.py fail if stale
- **0048** — [A provider creates the credential the mesh minted, and seals nothing](0048-a-provider-creates-the-credential-the-mesh-minted.md) - **0048** — [A provider creates the credential the mesh minted, and seals nothing](0048-a-provider-creates-the-credential-the-mesh-minted.md)
- **0049** — [A consumer's identity is bounded by the tightest backend that must accept it](0049-a-consumers-identity-fits-the-tightest-backend.md) - **0049** — [A consumer's identity is bounded by the tightest backend that must accept it](0049-a-consumers-identity-fits-the-tightest-backend.md)
- **0050** — [Model access is vendor-agnostic, and a vendor is an adapter](0050-model-access-is-vendor-agnostic.md) - **0050** — [Model access is vendor-agnostic, and a vendor is an adapter](0050-model-access-is-vendor-agnostic.md)
- **0051** — [Shared data is the operator's, and a module is granted access to it](0051-shared-data-is-the-operators.md)
### How it is built ### How it is built
@@ -1,9 +1,9 @@
--- ---
status: open status: located
opened: 2026-09-02 opened: 2026-09-02
located-in: [] located-in: [mesh-control, mesh-catalog, mesh-host]
fixed-by: fixed-by: 02-DECISIONS/0051-shared-data-is-the-operators.md
amended-design: amended-design: 02-DECISIONS/0051-shared-data-is-the-operators.md
--- ---
# 036 — Six modules own what they must share # 036 — Six modules own what they must share