The derived-value record is renumbered a second time: the key-value-buckets record took 0201 while this waited to merge, as the bundles record took 0188 before it. Both times free when chosen, taken by the time it landed. cycle.py caught it; three repositories cite this record, so the number matters. 225 — a provisioner has not been able to read its grant secrets since 01:30, when a module's own code left its container and the files stayed root's. Four thousand refusals, each worded as patience, and two consumers unserved. Not from ADR 0202 or 0189, which landed hours later; dates in the report. 226 — the store's sweep stops at the first reference recorded with an address and collects nothing. A guard that cannot tell 'I will not ask about this' from 'it would not answer' stops the wrong amount of work. 227 — the photo app's admin client asks for the port the proxy holds. A module pinned months behind carries everything its branch gained, the first time anything makes it move.
153 lines
10 KiB
Markdown
153 lines
10 KiB
Markdown
---
|
|
topic: the mesh
|
|
status: accepted
|
|
date: 2026-10-02
|
|
deciders: jochen
|
|
reconstructed: false
|
|
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
|
---
|
|
|
|
# 202. A provider declares what it derives for each consumer, and the mesh tells both ends
|
|
|
|
> **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
|
|
|
|
An arrangement between a consumer and a provider is delivered entirely by the mesh. Where the
|
|
provider is, which port it answers on, what name the consumer must present, where its password
|
|
is — each arrives as a fact the consumer reads from its binding, or as `${bound:…}` filled into a
|
|
file before the declaration leaves the control plane. The provider invents none of it and hands
|
|
none of it back ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)).
|
|
|
|
One kind of value escapes that. Where the **provider names the resource** — a bucket, a database,
|
|
a vhost — the name is derived from the consumer, per consumer, and the mesh has no way to carry
|
|
it. `serves` is a literal block in the provider's definition: the same values for every consumer.
|
|
A provisioner's contract takes a provision and returns nothing. So a value the mesh's own rule
|
|
produced reaches neither end as a statement; it is recomputed at one end and transcribed at the
|
|
other.
|
|
|
|
The object store is the instance ([issue 124](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)).
|
|
Its provisioner normalises the login the mesh minted into a bucket name and creates, checks and
|
|
removes exactly that; the rule lives in twenty lines of the module's own TypeScript. Its three
|
|
consumers each write the answer into their own definition by hand. Two transcribed it correctly;
|
|
one named a predecessor's bucket, and would have authenticated successfully and been refused on
|
|
every object, which reads like a credential fault and is not one.
|
|
|
|
Even corrected, the transcriptions are wrong in a second way. Each is `mesh-<node>-<slug>`, so
|
|
each **names the machine the module happens to run on today** — a definition stating a fact about
|
|
one installation, which [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
|
forbids and whose check does not catch because the name is not a domain. Move any of the three to
|
|
another machine and its configuration points at a bucket its key cannot open.
|
|
|
|
The shape is not the object store's. A database provisioner that prefixed names, a queue provider
|
|
that scoped vhosts, any provider that derives a resource from who is asking: each forces the
|
|
consumer to reproduce somebody else's rule and keep it in agreement by hand.
|
|
|
|
## Decision
|
|
|
|
**1. A served value may name the consumer the mesh is serving.** A `serves` block, which is
|
|
literal today, may interpolate the mesh's own statement of who the consumer is:
|
|
|
|
- `${consumer:as}` — the identity the mesh minted for this consumer, exactly as the login it is
|
|
told to present ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md));
|
|
- `${consumer:as:dns}` — the same identity written as a DNS label.
|
|
|
|
Nothing else. **The mesh learns no protocol here; it spells its own name in an alphabet it already
|
|
knows.** The identity is the mesh's, minted by the mesh, already capped at twenty characters
|
|
because of what an S3 access key accepts; `dns` is that same name with its separator written `-`
|
|
instead of `_`, which is the whole of the difference between the mesh's identifier alphabet and
|
|
the one buckets, vhosts and hostnames use. A provider that needs a prefix or a suffix writes it
|
|
around the placeholder, because a served value is a string.
|
|
|
|
The rejected alternative is **the provider returning values from provisioning** — the natural
|
|
channel, since the provider is what derived them. It is rejected for three reasons, in order of
|
|
weight. It inverts the delivery the mesh is built on: a grant would carry data the provider wrote
|
|
rather than only data the mesh minted, and [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)
|
|
removed exactly that second path once already. It makes a consumer's declaration incomplete until
|
|
its provider's reconcile loop has run, so a consumer could not be composed before a provider
|
|
answered — a bootstrap order the mesh does not have and does not want. And it puts the rule where
|
|
nothing can check it: a value that arrives from a running process cannot be refused at resolution,
|
|
only discovered wrong later, which is the failure this record exists to end.
|
|
|
|
**2. The mesh resolves it once, per consumer, and tells both ends from the one resolution.** At the
|
|
moment a consumer's declaration is composed, the mesh knows exactly who the consumer is. There, and
|
|
only there, the placeholders are filled. The result reaches:
|
|
|
|
- the **consumer**, as the served facts in its binding file and as `${bound:<provision>:<key>}` in
|
|
any file it writes — unchanged mechanisms, carrying one more key;
|
|
- the **provider**, as `serves` on that consumer's entry in its contributions file, so the
|
|
provisioner is *told* the name rather than recomputing it.
|
|
|
|
**The provider stops deriving in code and starts declaring.** One statement, filled once, delivered
|
|
to both ends: the two cannot disagree, because there is no second computation to disagree with.
|
|
|
|
**3. A served value stays settled before it is per-consumer.** Settings still compose into `serves`
|
|
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)), and the consumer
|
|
placeholders are filled after that, so an operator may set a prefix and the mesh still derives the
|
|
rest. A `${consumer:…}` naming a fact or an alphabet the mesh does not have is refused when the
|
|
definition is parsed, with what it may say.
|
|
|
|
**4. A consumer may no longer name the resource its provider derives.** With the value delivered,
|
|
a literal in a consumer's definition is not merely redundant — it is the one thing that can
|
|
disagree with what the provider will actually create. The three object-store consumers lose their
|
|
hand-written bucket names in this change.
|
|
|
|
**5. A consumer that keeps several holders of one provision may not be served a derived value.**
|
|
Each holder gets its own login, `…_<local>` ([ADR 0094](0094-a-module-may-hold-several-secrets-from-one-provider.md)),
|
|
and a provider derives from the login — so it would make one resource per holder, while the
|
|
consumer's side has one binding and one `${bound:<provision>:<key>}`, both derived from the
|
|
un-suffixed identity. That is this record's own failure one case to the side, and just as quiet:
|
|
the consumer would authenticate and be refused on every object. Refused at resolution, naming
|
|
both ends. Lifting it means giving the consumer's side a local dimension, which is a decision and
|
|
not an omission.
|
|
|
|
## Consequences
|
|
|
|
- One more thing a definition may say, and one less thing a module may be wrong about. The
|
|
vocabulary grows by a placeholder; the catalogue loses three literals that named this
|
|
installation's control node.
|
|
- A provider's naming rule becomes readable in its definition instead of in its source. `minio`'s
|
|
`bucketFor` goes; the manifest says `"bucket": "${consumer:as:dns}"` and the provisioner uses
|
|
what it is given.
|
|
- A provider that already serves consumers keeps serving them: the derived value equals what the
|
|
code derived, so no bucket, database or login changes name. This is a change of **who says it**,
|
|
not of **what is said**.
|
|
- A refusal here fails **that machine's push**, naming the definition, and nothing else. That is
|
|
deliberate and is the opposite of a module quietly left out: a definition that transcribes
|
|
somebody else's rule is wrong everywhere, not just here, and the loud failure is in front of
|
|
whoever can fix it.
|
|
- The mesh now holds a rule in another system's alphabet — one rule, `dns`, stated once. A second
|
|
alphabet is a decision, not an addition: the cost of each is that the mesh must be right about
|
|
somebody else's naming, and that cost is only worth paying where the mesh already mints the name.
|
|
|
|
## How this is checked
|
|
|
|
- A served value naming an unknown fact or alphabet is refused at parse, with the list of what it
|
|
may say — tested on both halves of the message.
|
|
- Resolving a consumer whose provider derives a value puts that value in the consumer's binding
|
|
file, in its `${bound:…}` substitutions, and in the provider's contributions entry for that
|
|
consumer — one test asserting the three agree, because agreeing is the whole point.
|
|
- Two consumers of one provider on one machine get two different derived values, and neither gets
|
|
the other's.
|
|
- A consumer with several holders of a deriving provider is refused, with both ends named — the
|
|
test asserts the refusal, not merely that something failed.
|
|
- A catalogue-wide test refuses a consumer definition that writes a literal where its provider
|
|
derives: the provider's `serves` names the key, so the catalogue can say which definitions
|
|
transcribe one.
|
|
- `dns` is checked against the identity the mesh actually mints, not against an invented string:
|
|
the test derives an identity with `ConsumerIdentity` and asserts the label it becomes.
|
|
|
|
## References
|
|
|
|
- [issue 124 — a consumer cannot be told a value its provider derived for it](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)
|
|
- [ADR 0048 — a provider creates the credential the mesh minted, and seals nothing](0048-a-provider-creates-the-credential-the-mesh-minted.md)
|
|
- [ADR 0049 — a consumer's identity fits the tightest backend](0049-a-consumers-identity-fits-the-tightest-backend.md)
|
|
- [ADR 0174 — a node varies a module through settings and kept regions, never through an edit](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
|
|
- [ADR 0155 — a definition names no installation, and how that is checked](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
|
- [design 27 — a module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)
|