diff --git a/02-DECISIONS/0091-a-mount-is-declared-three-ways.md b/02-DECISIONS/0091-a-mount-is-declared-three-ways.md new file mode 100644 index 0000000..c890497 --- /dev/null +++ b/02-DECISIONS/0091-a-mount-is-declared-three-ways.md @@ -0,0 +1,68 @@ +--- +topic: what runs on it +status: accepted +date: 2026-09-21 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0051-shared-data-is-the-operators.md +--- + +# 91. A mount is declared, and there are three things it can be + +## Context + +A container's bind mount whose source does not exist is created by the container runtime, as +root, with whatever mode it picks. So `owner` and `mode` — which exist so a module can say who +its data belongs to — never reach the directories that hold data, and the rule that keeps a +directory when a module goes away ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)) +does not cover them, because the mesh has never heard of them +([issue 026](../04-ISSUES/026-the-data-directories-are-not-declared/00-report.md)). Fourteen +such mounts were declared by hand; a check that every mount is declared was then written and +withdrawn, because it refused the builder: the builder mounts the container runtime's socket, +which is not its data, already exists, and belongs to the machine. Declaring it as the module's +own directory would be a lie the host would act on. + +## Considered Options + +1. **Declare the socket as a directory anyway.** Rejected: the host would create, own and + protect a path that is the machine's. +2. **A new manifest field** naming machine paths a module may mount. Rejected: the manifest + already says the module needs the container runtime, and a second field would say the same + thing in paths. +3. **Three declarations, one for each kind of path a mount can be.** Adopted. + +## Decision + +A container may not mount a path the module never declared, and a path is declared in one of +three ways, which are the three things a path can be: + +- **the module's own** — a directory or file resource, or where a secret, a grant or a + contribution lands. Created and owned by the mesh for this module, kept when the module goes; +- **the operator's** — an `accesses` entry ([ADR 0051](0051-shared-data-is-the-operators.md)): + pre-existing, shared, granted for use, never owned; +- **the machine's** — a facility a declared capability grants. `container-runtime` grants its + socket. The path exists, the machine owns it, and the capability is the declaration. + +A mount under a declared directory is declared. The check runs where the manifest is parsed, +and names the path and the three remedies. + +## Consequences + +Every directory that holds a module's data is one the mesh created with the module's owner and +mode, and one ADR 0030 protects. What got harder: a manifest borrowed from a compose file no +longer passes on the strength of its volume lines; each must say what kind of path it mounts. +The table of what a capability grants is small and in the catalogue's parser; a new capability +that grants a path adds a row. + +## How it is checked + +Manifest tests refuse an undeclared mount, accept one under a declared directory, accept one +the module accesses, and accept the runtime's socket with the capability and refuse it without. +A test parses every manifest in the catalogue beside the checkout and fails on any that breaks +the rule, so the catalogue cannot drift back. + +## References + +- [issue 026](../04-ISSUES/026-the-data-directories-are-not-declared/00-report.md) +- [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md), [ADR 0051](0051-shared-data-is-the-operators.md) +- [`03-DESIGN/01-to-be/18-building-a-module.md`](../03-DESIGN/01-to-be/18-building-a-module.md) diff --git a/02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md b/02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md new file mode 100644 index 0000000..7f8890a --- /dev/null +++ b/02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md @@ -0,0 +1,61 @@ +--- +topic: the tiers +status: accepted +date: 2026-09-21 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0085-a-secret-is-a-provision.md +--- + +# 92. An operator delivers a pair credential, and the mesh never replaces it + +## Context + +[ADR 0085](0085-a-secret-is-a-provision.md) names three species of secret and gives the vault +two of them: a module's own secret, which the mesh mints, and an operator-delivered secret — a +credential for something outside the mesh, which only a person can supply. Under 0085 a secret +from the vault is a pair credential between the consumer and the vault. The controller's one +command that takes a value from a person wrote only a module's own secret, sealed to one node. +Nothing could put a value into a pair, so the third species had no entry +([issue 070](../04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md)), and +an operator's credential could be held only as an own secret — un-audited, un-rotatable, the +gap 0085 opened to close. + +## Considered Options + +1. **A new verb on the pair.** Rejected: `secret accept` already means "a value a person + supplied, sealed on the way in, plaintext discarded"; a second verb would mean the same. +2. **`secret accept` grows a provider end.** Adopted. + +## Decision + +`secret accept --provider ` seals the supplied value to the +consumer's node, to the provider's node, and to the operator's key when the mesh has one, and +records the pair as `accepted`. Every pair credential now says where it came from: `made` or +`accepted`. + +An accepted pair is never replaced by a made one. When a sealing key at either end changes, the +mesh cannot re-seal a value it does not hold, so the read is refused and names the remedy — +accept it again. `rotate` refuses an accepted pair for the same reason: the mesh cannot make its +replacement, and deleting it would have the next read mint one, delivered and reported as +applied while failing to authenticate somewhere else entirely. Rotating an accepted credential +is accepting a new value. + +## Consequences + +A credential for something outside the mesh lives in the vault's ledger with the others, sealed +to both ends and recoverable by the operator. What got harder: a mesh whose node keys change +cannot heal an accepted pair by itself; a person is asked. That is the honest shape — the value +was never the mesh's to make. + +## How it is checked + +An inventory test accepts a value into a pair, reads it back twice unchanged with origin +`accepted`, asserts `rotate` refuses it naming the remedy while a made pair still rotates; +another changes a node's key and asserts the read is refused, then accepts again and reads. + +## References + +- [issue 070](../04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md), [issue 069](../04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md) +- [ADR 0085](0085-a-secret-is-a-provision.md) +- [`03-DESIGN/01-to-be/24-the-secrets-vault.md`](../03-DESIGN/01-to-be/24-the-secrets-vault.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 148d930..19afb37 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -112,6 +112,7 @@ python3 00-META/checks/index.py fail if stale - **0075** — [An artifact store is a provision; a package registry is a different one](0075-two-stores-and-which-provides-what.md) - **0078** — [The store and the broker are ordinary modules](0078-the-store-and-broker-are-modules.md) - **0079** — [The foundation seats are named after their servers](0079-the-foundation-seats-are-named-after-their-servers.md) +- **0092** — [An operator delivers a pair credential, and the mesh never replaces it](0092-an-operator-delivers-a-pair-credential.md) ### What runs on them, and how it gets there @@ -141,6 +142,7 @@ python3 00-META/checks/index.py fail if stale - **0084** — [Which provider serves a consumer, when the mesh runs more than one](0084-which-provider-serves-a-consumer.md) - **0085** — [A secret is a provision, and the vault is the module that provides it](0085-a-secret-is-a-provision.md) - **0087** — [A seeded file is created once, and what grows in it is not the mesh's](0087-a-seeded-file-is-created-once.md) +- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md) ### How it is built diff --git a/03-DESIGN/01-to-be/18-building-a-module.md b/03-DESIGN/01-to-be/18-building-a-module.md index 2cb0ab9..96e218a 100644 --- a/03-DESIGN/01-to-be/18-building-a-module.md +++ b/03-DESIGN/01-to-be/18-building-a-module.md @@ -7,6 +7,7 @@ code: - mesh-catalog modules/builder updated: 2026-09-21 decisions: + - 02-DECISIONS/0091-a-mount-is-declared-three-ways.md - 02-DECISIONS/0087-a-seeded-file-is-created-once.md - 02-DECISIONS/0040-what-a-module-is.md - 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md @@ -182,6 +183,16 @@ disagrees with it. | `computed` | marks a module the controller generates rather than an author writing | | `build.artifacts` | what it produces | +**A container mounts only what the manifest declares** +([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module +never declared is created by the container runtime as root, so the module's owner and mode never +reach its data and the rule that keeps data when a module goes away does not cover it. A path is +declared in one of three ways, for the three things a path can be: the module's own (a directory +or file resource, or where a secret, grant or contribution lands), the operator's (an `accesses` +entry), or the machine's (a facility a declared capability grants — `container-runtime` grants its +socket). *How it is checked:* the parser refuses an undeclared mount naming the path and the three +remedies, and a test parses every manifest in the catalogue beside the checkout. + ### What it builds | kind | is | diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index a865c05..98ec487 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -4,6 +4,7 @@ status: implemented code: [mesh-catalog, mesh-controller, mesh-host] updated: 2026-09-21 decisions: + - 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md - 02-DECISIONS/0085-a-secret-is-a-provision.md - 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md - 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md @@ -45,7 +46,12 @@ whose job it is to give it one."* A module that needs a secret for its own use requires a `secret` provision, exactly as it requires a database from the store. The vault generates the value — or takes custody of one an operator -delivered — and the credential belongs to the consumer↔vault pair. Because it is an ordinary pair +delivered, through `secret accept … --provider`, which seals it to both ends and records the pair +as accepted ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)) — and +the credential belongs to the consumer↔vault pair. An accepted pair is the one exception to what +follows: the mesh cannot make its replacement, so it is neither remade when a key changes nor +rotated; both are refused aloud, and accepting a new value is the rotation. *How it is checked:* +an inventory test accepts, reads back unchanged, and asserts the two refusals name the remedy. Because it is an ordinary pair credential, **everything already built for pair credentials applies to it unchanged**: it rotates with the one command that discards a credential and delivers both ends together, it is one secret per holder so rotating one touches nothing else, and *who holds this* is a query rather than an diff --git a/04-ISSUES/026-the-data-directories-are-not-declared/00-report.md b/04-ISSUES/026-the-data-directories-are-not-declared/00-report.md index 6afc292..5a4a68b 100644 --- a/04-ISSUES/026-the-data-directories-are-not-declared/00-report.md +++ b/04-ISSUES/026-the-data-directories-are-not-declared/00-report.md @@ -1,9 +1,9 @@ --- -status: located +status: resolved opened: 2026-09-01 located-in: [mesh-control] -fixed-by: partly — mesh-controller f5b03e1 declares the data directories; the gate refusing a container mount the module never declared (53eb000) was withdrawn in 83c6a2f and nothing replaces it -amended-design: +fixed-by: mesh-controller f5b03e1 (the fourteen declared); ADR 0091 and mesh-controller feat/multiple-fixes (the check, back, with the three declarations — the socket by capability, the operator's by accesses) +amended-design: 03-DESIGN/01-to-be/18-building-a-module.md --- # 026 — The data directories are mounted and never declared diff --git a/04-ISSUES/026-the-data-directories-are-not-declared/01-diagnosis.md b/04-ISSUES/026-the-data-directories-are-not-declared/01-diagnosis.md new file mode 100644 index 0000000..da9f65b --- /dev/null +++ b/04-ISSUES/026-the-data-directories-are-not-declared/01-diagnosis.md @@ -0,0 +1,14 @@ +# Diagnosis — 2026-09-21 + +1. The catalogue was read again for mounts no resource declares: twenty-four remained, in two + kinds only. Nine media modules mount the operator's library, and every one of those paths is + already in the module's `accesses` — the vocabulary [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) + gave exactly this. Four modules mount the container runtime's socket, and every one of them + declares the `container-runtime` capability. +2. So the field the report said would have to be invented already exists twice over, and the + check that was withdrawn needed only to read both: an access is a declared path, and a + capability declares the facility it grants. + +**Located in:** the catalogue's parser. The check is back, refuses an undeclared mount naming the +path and the three remedies, and a test parses the whole catalogue. Decided in +[ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md). diff --git a/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md b/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md index 5a2b7a2..5bd2f02 100644 --- a/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md +++ b/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/00-report.md @@ -1,9 +1,9 @@ --- -status: open +status: resolved opened: 2026-09-20 -located-in: [mesh-controller] -fixed-by: -amended-design: +located-in: [mesh-controller internal/inventory (secret), mesh-controller cmd/mesh-controller (secret accept)] +fixed-by: ADR 0092; mesh-controller feat/multiple-fixes (secret accept --provider; origin on the pair; remake and rotate refused) +amended-design: 03-DESIGN/01-to-be/24-the-secrets-vault.md --- # An operator cannot deliver a pair credential, so the vault's third species has no entry diff --git a/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/01-diagnosis.md b/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/01-diagnosis.md new file mode 100644 index 0000000..b0e12df --- /dev/null +++ b/04-ISSUES/070-an-operator-cannot-deliver-a-pair-credential/01-diagnosis.md @@ -0,0 +1,13 @@ +# Diagnosis — 2026-09-21 + +1. The three open questions, answered. It is `secret accept` growing a provider end, not a new + verb: the verb already means a value a person supplied, sealed on the way in. An accepted pair + refuses `rotate` — the mesh cannot make the replacement — and accepting a new value is the + rotation. The origin becomes a fact of every pair credential, `made` or `accepted`, so the + vault's ledger can say which a person supplied. +2. One consequence the report did not name: a pair credential is remade whenever either end's + sealing key changes, and an accepted one cannot be — the mesh does not hold the value. The + read is refused aloud with the remedy rather than quietly replaced by a minted one. + +**Located in:** the controller's pair-credential store and the `secret accept` command. Decided +in [ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md).