diff --git a/02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md b/02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md similarity index 93% rename from 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md rename to 02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md index edbb84c..9d1b1a3 100644 --- a/02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md +++ b/02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md @@ -7,11 +7,14 @@ reconstructed: false extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md --- -# 201. A provider declares what it derives for each consumer, and the mesh tells both ends +# 202. A provider declares what it derives for each consumer, and the mesh tells both ends -> Written as 0188 on 2026-10-02 and renumbered to 0201 on 2026-10-04: the record of the bundles -> refactor took 0188 on main while this one waited in a pull request, and the mesh's own code now -> cites that one. Only the number moved; the decision is the one taken on the 2nd. +> **Written as 0188 on 2026-10-02, renumbered to 0201, and to 0202 on 2026-10-04.** Twice, for the +> same reason twice: the bundles refactor took 0188 while this waited in a pull request, and the +> key-value-buckets record took 0201 while this waited again. Both times the number was free when +> it was chosen and taken by the time this merged. Only the number moved; the decision is the one +> taken on the 2nd. The check that refuses two records sharing a number is what caught it, both +> times — a number is how a record is cited, and three repositories cite this one. ## Context diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 1d582bd..9f2880c 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -190,7 +190,7 @@ python3 00-META/checks/index.py fail if stale - **0187** — [A dead tracker is not the machine's failure](0187-a-dead-tracker-is-not-the-machines-failure.md) - **0189** — [The store keeps what the records name, and a maintenance step holds its writers still](0189-the-store-keeps-what-the-records-name.md) - **0190** — [A seat's work is shared by its holders, and building is the first such role](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md) -- **0201** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0201-a-provider-declares-what-it-derives-for-each-consumer.md) +- **0202** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0202-a-provider-declares-what-it-derives-for-each-consumer.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md index f99903d..b5841c8 100644 --- a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md +++ b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md @@ -14,7 +14,7 @@ decisions: - 02-DECISIONS/0084-which-provider-serves-a-consumer.md - 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md - 02-DECISIONS/0038-the-mesh-assigns-the-port.md - - 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md + - 02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md --- # 27 — A module requires, the mesh resolves @@ -208,7 +208,7 @@ the placeholder allows: the definition says which values reach which requirement does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it. *A provider says once what it derives for each consumer (2026-10-02, -[ADR 0201](../../02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md), +[ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md), [issue 124](../../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)):* where a provider **names the resource** it gives each consumer — a bucket, a database, a vhost — the name is derived per consumer, and a literal `serves` block could not carry it. A served value may @@ -221,7 +221,7 @@ its binding's served facts and as `${bound::}` in any file it wr as `derived` on that consumer's entry in its contributions file, so its provisioner is told the name rather than recomputing it. A consumer that writes the derived value into its own definition instead of asking for it is refused, naming the placeholder to use. *How it is checked:* the unit tests in -ADR 0201's "how this is checked", each run against the unchanged controller first. +ADR 0202's "how this is checked", each run against the unchanged controller first. ## How a definition reads what was resolved diff --git a/04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md b/04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md index 8c8b543..3cb04d0 100644 --- a/04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md +++ b/04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md @@ -2,7 +2,7 @@ status: resolved opened: 2026-09-26 located-in: [mesh-controller internal/catalogue, mesh-sdk src/provisioner, mesh-catalog modules/minio] -fixed-by: 02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md +fixed-by: 02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md --- @@ -64,7 +64,7 @@ compares it to what the provider will actually create. The one wrong instance wa bucket in its own configuration against the one the provider would create is a check that could exist today, for any interface, without the mechanism above. -## Answered, 2026-10-02 — [ADR 0201](../../02-DECISIONS/0201-a-provider-declares-what-it-derives-for-each-consumer.md) +## Answered, 2026-10-02 — [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md) The channel is the provider's own `serves` block, which may now name the consumer the mesh is serving: `${consumer:as}` and `${consumer:as:dns}`. The mesh fills it once, where it knows who the diff --git a/04-ISSUES/225-a-provisioner-cannot-read-the-grant-secrets-since-its-code-left-the-container/00-report.md b/04-ISSUES/225-a-provisioner-cannot-read-the-grant-secrets-since-its-code-left-the-container/00-report.md new file mode 100644 index 0000000..16435c0 --- /dev/null +++ b/04-ISSUES/225-a-provisioner-cannot-read-the-grant-secrets-since-its-code-left-the-container/00-report.md @@ -0,0 +1,75 @@ +--- +status: open +opened: 2026-10-04 +located-in: [mesh-host internal/apply, mesh-controller internal/catalogue] +fixed-by: +amended-design: +--- + +# 225 — A provisioner cannot read the grant secrets since its code left the container, and every consumer of it is unserved + +## What was observed + +On the control machine, 2026-10-04, found while looking at why one app was restarting: + +``` +[mongodb] [provisioner:mongodb-database] mesh_novox_photos: secret not readable yet + (/var/lib/mongodb/grants/novox.photos.secret): + Error: EACCES: permission denied, open '/var/lib/mongodb/grants/novox.photos.secret' +``` + +**4330 times, every five seconds, since 01:30:20.** The consequence is not a log line: the +provisioner never reads the password, so it never creates the user, so the consumer never +connects — + +``` +UserNotFound: Could not find user "mesh_novox_photos" for db "admin" +``` + +— and the app crash-loops. Two consumers on this machine are in that state. + +## Why + +The grant secrets are what the mesh seals for each consumer and the host unseals beside the +provider's contributions file. They are written `-rw------- root root`, which was right while a +module's own code ran in a container as root. + +[ADR 0198](../../02-DECISIONS/0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md) +moved a module's long-running code out of its container and under the node's runtime, which runs +as the operator's account. The provisioner is now that account; the secret is still root's. The +timestamps say it exactly: the files are dated 2026-09-26, the first refusal is 01:30:20 on the +day the runtime rolled. + +**Nothing reports it.** The machine applies cleanly and reads as current; the provisioner says +`secret not readable yet`, whose wording is for a real and ordinary race on the first pass — the +host has not written the file yet — and which is indistinguishable, in the log, from a permanent +refusal. Four thousand occurrences of a message that means "wait a moment" is the shape to +recognise. + +## Why it matters beyond this instance + +This is every provider that provisions. The grant secret is the one file the sdk's harness reads +for every consumer, so a provider that cannot read it serves nobody — and says so only in a line +that reads like patience. + +It is also the general question the runtime move leaves: **what the mesh seals for a module is +owned for the shape that module's code used to have.** Each module whose code moved is a module +whose files may now be unreadable to it, and ownership is the mesh's to state, not the module's +to work around. + +## What this is not + +Not caused by [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md) +or [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md), which landed two +to three hours after the first refusal. Those rebuilt the two affected consumers, which recreated +their containers and made a silent fault visible as a restarting one. The dates are above. + +## Open questions + +- Who owns a grant secret now — the module's account, as `secrets-owner` already says for a + module's own secrets? Then the host writes it so, and this is a one-line statement in the + declaration rather than a convention. +- Should `secret not readable yet` stop saying "yet" after the first few passes? A message that + is right once and wrong four thousand times is a message that hides its own meaning. +- Which other modules' files did the runtime move leave behind? The sweep is the same question + for every path the mesh writes for a module: directories, bundles, received files. diff --git a/04-ISSUES/226-the-stores-sweep-stops-at-the-first-reference-recorded-with-an-address/00-report.md b/04-ISSUES/226-the-stores-sweep-stops-at-the-first-reference-recorded-with-an-address/00-report.md new file mode 100644 index 0000000..ca4c401 --- /dev/null +++ b/04-ISSUES/226-the-stores-sweep-stops-at-the-first-reference-recorded-with-an-address/00-report.md @@ -0,0 +1,58 @@ +--- +status: open +opened: 2026-10-04 +located-in: [mesh-controller cmd/mesh-controller/collect.go, mesh-controller internal/inventory/collection.go] +fixed-by: +amended-design: +--- + +# 226 — The store's sweep stops at the first reference recorded with an address, so it collects nothing at all + +## What was observed + +The first live run of [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)'s +sweep, 2026-10-04, printed on every build: + +``` +the artifact store kept 127.0.0.1:5100/mesh-tools/build@sha256:0de48cd3…, so nothing more was + asked of it: 127.0.0.1:5100/mesh-tools/build@sha256:0de48cd3… is not a reference into the + mesh's artifact store +1681 more to collect; the next build asks again +``` + +Nothing is collected, and nothing ever will be. The store holds 1681 artifacts the mesh no longer +keeps and the feature that exists to remove them is inert. + +## Why + +Two correct decisions meeting badly. + +**A reference recorded before references were kept without an address** is +`127.0.0.1:5100/@sha256:…` rather than `artifact-store://@sha256:…` +([04-ISSUES/102](../102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)). +`LetGo` rightly refuses to compose a delete for a reference whose shape it does not recognise — +that refusal is what keeps the sweep from reaching something that is not the mesh's. + +**The sweep stops at the first refusal**, because "a store that refuses one refuses all of them" +— deletion disabled, the store down, the network gone — and pushing through would mean a hundred +identical failures in front of whoever was building something. That reasoning is right for the +store refusing. It is wrong for *this* record being unreadable. + +So one old record, early in the oldest-first order, halts the whole sweep for ever. + +## Why it matters beyond this instance + +**A guard that cannot tell "I will not ask about this" from "it would not answer" stops the wrong +amount of work.** The two deserve opposite responses: skip one, abandon the other. Collapsing +them into "an error" is how a bounded, cautious loop becomes a loop that does nothing — and it +reports the right number while doing it, which is what made it look healthy. + +## What a fix has to settle + +- A reference the sweep cannot address is **skipped, and the sweep goes on** — it is a fact about + that record, not about the store. +- `Recorded()` already normalises the old form to the kept one, and is what the rest of the mesh + uses for exactly these references. The sweep should normalise before asking rather than refuse. +- Only a refusal *by the store* ends a sweep. +- **How it is checked:** a sweep over records holding one address-recorded reference and one kept + one collects the second; a sweep against a store that refuses stops at the first. diff --git a/04-ISSUES/227-the-photo-apps-admin-client-asks-for-the-port-the-proxy-holds/00-report.md b/04-ISSUES/227-the-photo-apps-admin-client-asks-for-the-port-the-proxy-holds/00-report.md new file mode 100644 index 0000000..906b001 --- /dev/null +++ b/04-ISSUES/227-the-photo-apps-admin-client-asks-for-the-port-the-proxy-holds/00-report.md @@ -0,0 +1,44 @@ +--- +status: open +opened: 2026-10-04 +located-in: [mesh-catalog modules/photos] +fixed-by: +amended-design: +--- + +# 227 — The photo app's admin client asks for port 80, which the reverse proxy holds, so it cannot start + +## What was observed + +Applying the control machine, 2026-10-04: + +``` +applying "photos.admin-client": starting container photos-admin-client: + failed to bind host port 0.0.0.0:80/tcp: address already in use +``` + +Port 80 on that machine belongs to the reverse proxy (`mesh-route-proxy`, confirmed with `ss`), +which is the whole arrangement: the proxy holds the public ports and every module is reached +through it. A module that publishes 80 itself can never start beside it. + +Everything else on the machine applied; this one resource fails every pass. + +## How it surfaced + +`photos` had been pinned at a commit from 2026-09-28 and was rebuilt to `main` on 2026-10-04 — +forced by [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)'s +refusal of its transcribed bucket name. The admin client is one of the changes that came with the +rest of `main`. The rebuild did not create the conflict; it delivered it. + +**A module pinned months behind carries whatever its branch gained, all at once, the first time +something makes it move.** That is the cost of a pin, and it is paid in full rather than +gradually. + +## What a fix has to settle + +- Which port the admin client should ask for, or whether it should be reached through the proxy + like everything else and publish nothing. +- Whether a module declaring a port the machine's proxy already holds should be refused when it + is composed, rather than failing on the machine every pass. The mesh assigns ports + ([ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md)); a fixed 80 beside a proxy is + a statement it could check.