From 220e3c72f5d24a3af26187780ceb149f61545bf0 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 5 Sep 2026 22:22:30 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200051=20(proposed)=20=E2=80=94=20shared=20?= =?UTF-8?q?data=20is=20the=20operator's?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves 04-ISSUES/036: eight media modules each declared the shared library and download directories as their own resources, and the resolver's duplicate-owner refusal — right in general — would refuse the stack's only sensible assignment the first time two landed on one node. The decision, from the operator: shared, pre-existing data is operator-owned and external. The mesh does not create, chown, reconcile or remove it. A module declares it needs access to such a path (read or read-write); the host mounts it and owns nothing. Several modules accessing one path is normal — the duplicate-path refusal is about ownership, not use. An accessed path absent at apply is refused clearly, not created. Extends ADR 0030: the third case the host had no word for, what it neither made nor configured and must not touch. Point 036's fixed-by/amended-design at the record; mark it located in mesh-control, mesh-catalog and mesh-host. Regenerate the decision index. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- .../0051-shared-data-is-the-operators.md | 166 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + .../00-report.md | 8 +- 3 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 02-DECISIONS/0051-shared-data-is-the-operators.md diff --git a/02-DECISIONS/0051-shared-data-is-the-operators.md b/02-DECISIONS/0051-shared-data-is-the-operators.md new file mode 100644 index 0000000..aa91752 --- /dev/null +++ b/02-DECISIONS/0051-shared-data-is-the-operators.md @@ -0,0 +1,166 @@ +--- +topic: what runs on it +status: proposed +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 diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index eff1d6f..ad02eb6 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -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) - **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) +- **0051** — [Shared data is the operator's, and a module is granted access to it](0051-shared-data-is-the-operators.md) *(proposed)* ### How it is built diff --git a/04-ISSUES/036-six-modules-own-what-they-must-share/00-report.md b/04-ISSUES/036-six-modules-own-what-they-must-share/00-report.md index 7513890..071d7df 100644 --- a/04-ISSUES/036-six-modules-own-what-they-must-share/00-report.md +++ b/04-ISSUES/036-six-modules-own-what-they-must-share/00-report.md @@ -1,9 +1,9 @@ --- -status: open +status: located opened: 2026-09-02 -located-in: [] -fixed-by: -amended-design: +located-in: [mesh-control, mesh-catalog, mesh-host] +fixed-by: 02-DECISIONS/0051-shared-data-is-the-operators.md +amended-design: 02-DECISIONS/0051-shared-data-is-the-operators.md --- # 036 — Six modules own what they must share