Merge pull request 'ADR 0225: a consumer's identity is bounded by the provision it requires' (#127) from decision/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires into main

This commit was merged in pull request #127.
This commit is contained in:
2026-10-06 00:28:47 +00:00
6 changed files with 189 additions and 4 deletions
@@ -108,6 +108,14 @@ declared slug is a strictly better escape hatch than an opaque hash. **D stays r
## Consequences (of E)
> **The mechanism changed — 2026-10-06, by [ADR 0225](0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md).**
> Option C, named above as the later refinement, is taken: each offer states the longest identity its
> backend keeps, and a consumer is bounded by the provision it requires rather than by 20 everywhere.
> 20 stays the bound of the object store and of a provider that is told its consumers and does not
> say. The overflow is refused before merge by the catalogue check, and at composition the consumer
> is left out of its provider's grants and reported — never the provider's machine refused. What
> stands: the identity is said once, the slug is the remedy, nothing is hashed or truncated.
- A module manifest gains an optional `slug`; a node may carry one too. `ConsumerIdentity` prefers
the slug over the cleaned name for each half. `identityLimit` becomes 20 (the true minimum), and
`CheckIdentity` refuses at `module add` / assignment — now with a message naming the slug to set.
@@ -64,6 +64,13 @@ instead of `_`, which is the whole of the difference between the mesh's identifi
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 mechanism changed — 2026-10-06, by [ADR 0225](0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md).**
> The identity is no longer capped at twenty characters for every provision: each offer states the
> bound its backend keeps, and twenty is the bound of the object store and of a provider that does not
> say. What stands: the identity is still the mesh's, and `dns` is still that name with its separator
> written `-`. An offer serving `${consumer:as:dns}` now bounds its consumers at 63 or less, so the
> label still fits.
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
@@ -0,0 +1,148 @@
---
topic: what runs on it
status: accepted
date: 2026-10-06
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md
---
# 225. A consumer's identity is bounded by the provision it requires, judged before merge, and never refuses its provider
## Context
[ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) bounds every consumer's identity,
`mesh_<machine>_<slug-or-module>`, by the tightest backend anywhere in the mesh: an S3 access key's 20
characters. It chose that over per-interface bounds (its option C) because one constant unblocked the
object store, and named C as the refinement "if non-S3 consumers are paying for S3's limit often
enough to mind". [Issue 263](../04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md)
is that point. The operator: "the 20 character limit has bitten us multiple times".
What happened on 2026-10-06, and what it shows:
- **The bound applied where it meant nothing.** A change made the network-manager modules require the
mesh's resolver provision. The resolver mints no credential and keeps no name: its provider is not
even told who its consumers are (it receives nothing). `networkmanager`'s identity, 23 to 26
characters on real machine names, was refused all the same.
- **It was found late and far from its cause.** The catalogue's module check and the controller's
tests passed. ADR 0049 says the refusal comes at assignment; this was a new requirement on modules
already assigned, so no assignment saw it. It surfaced when the provider composed its grants.
- **It refused the wrong machine, wholly.** The controller judged the identity while composing the
*provider's* declaration, and one refusal there fails the whole composition. The anchor holds the
resolver, so the anchor — every module on it — could not be pushed until a slug was changed
elsewhere.
The catalogue's providers keep very different names. Read from each provider's code: the object store
keeps the identity as an access key (20) and a bucket name; PostgreSQL as a role and a database (63);
MongoDB as a database (63); SQL Server as a login and a database (128); the identity provider as a
client id (255); the forge as a user name (40); the mail server as a mailbox's local part (64); the
public DNS provider as a label (63); the cache, the vault, the message broker and the time-series
store as names with no limit worth stating. The resolver and both route providers keep no name of
their consumers at all.
## Considered Options
1. **Keep one bound, raise or lower it.** Any single number is wrong for most provisions: 20 refuses
a database consumer for an object store's key, 63 lets the object store fail at provision time
again ([issue 034](../04-ISSUES/034-mesh-login-exceeds-s3-access-key-limit/00-report.md)). Rejected —
it is the cause.
2. **Per-interface bounds in a mesh-wide table of provisions.** Puts the fact in the controller, which
would then know what an S3 key is. The mesh is name-agnostic about what a provider does with an
identity (`ConsumerIdentity`); a table would make it learn every backend. Rejected.
3. **Each offer states its own bound; the bound a consumer meets is that of the provision it
requires** (ADR 0049's option C, placed where [ADR 0202](0202-a-provider-declares-what-it-derives-for-each-consumer.md)
already places what a provider derives: in its own definition). Chosen.
4. **Derive a different identity per provision** (C's own "against": one consumer, several names).
Not needed: the identity stays one name, said once, and must fit every provision the module
requires. Only the *bound* is per provision. Rejected as unnecessary.
And for where the refusal lands:
5. **Keep refusing the provider's composition.** Rejected — it makes one consumer's name a reason
no push reaches a machine that did nothing wrong.
6. **Refuse the consumer's whole machine.** Right machine, still too wide, and still late. Not chosen;
the consumer is named instead, and refused in its own pull request (below).
## Decision
**1. An offer states the longest consumer identity its backend keeps.** In the provider's
definition, beside the provision: `identity` with a `max` and the words for what keeps it (`in`), so a
refusal can say "an S3 access key keeps 20"; `in` alone for a backend with no limit worth stating; or
`false` for a provision that keeps no name derived from its consumer. The bound applied to a consumer
is that of the provision it requires, from the offer of the module answering it — not the tightest
backend in the mesh.
**Unsaid, the bound follows from what the provider is told.** A provider that receives the provision,
or serves its consumers a value built from their identity, is told who each consumer is and may make
a name of it in a backend nobody measured: it keeps ADR 0049's 20. A provider told neither keeps
nothing of its consumers: no bound. A provider whose definition is not at hand is held to 20.
**2. An overflow is refused before merge.** The catalogue check (`module check`, and the controller's
test over the real catalogue) judges every module's identity, built on the longest machine name of
the mesh, against the bound of every provision it wants that a module in the catalogue offers — the
tightest where several offer it. The pull request that introduces an overflow — a new requirement, a
lowered bound, a longer module name — is the one that fails, naming the module and the longest slug
that would fit. The longest machine name is a parameter of the check; its default is the mesh's own
longest, raised in the same change that names a longer machine.
**3. One consumer's identity never refuses its provider's machine.** When the provider's declaration
is composed, a consumer whose identity overflows the bound is left out of the grants and returned
beside them; every other consumer is granted and the declaration composes. The consumer is named
where an operator looks: on the push and plan of the provider, on the plan of the consumer's own
machine, and in `status` (and its document), which does not call the mesh well while one stands.
**4. An identity served as a DNS label stays inside one.** The mesh writes an identity into a label
without truncating it ([ADR 0202](0202-a-provider-declares-what-it-derives-for-each-consumer.md)),
so an offer that serves `${consumer:as:dns}` must bound its consumers at 63 or less.
What does not change: the identity is still derived once and said to both ends (ADR 0049, issue 023);
the slug is still the remedy; truncation and hashing are still refused.
## Consequences
- A consumer of a database or the identity provider keeps a legible name on a long machine name; only
a consumer of the object store still needs a slug of a few characters. Requiring a keyless
provision costs nothing in name length.
- The catalogue's providers each state a bound in their offer, and the check reads it there. A new
provider that is told its consumers and says nothing keeps the old 20: safe, and visible in review.
- A consumer left out of the grants holds a binding and credential its provider never created. It
fails to authenticate; that is reported by name in `status` until a slug fixes it, rather than
hidden behind a machine nobody could push.
- The check's default machine-name length is a fact about one mesh carried in code. A mesh that
names a longer machine must raise it, or pass its own to `module check`; nothing reminds it to.
- Old controllers refuse the new field (manifests are parsed strictly), so the controller ships before
the catalogue that states bounds.
- ADR 0049's consequence "`identityLimit` becomes 20 … `CheckIdentity` refuses at `module add` /
assignment" now holds only for a provision that keeps 20, and is checked before merge and reported
at composition rather than refused at assignment. ADR 0049 carries a note saying so.
**How each rule is checked.**
- *Rule 1:* catalogue unit tests — an offer's stated bound is the one applied; an unstated one is 20
for a provider told its consumers and none for one told nothing; `false` is none; the field parses
strictly and a bound without `in`, or shorter than any identity, is refused when the definition is
parsed. Over the real catalogue: the resolver provision bounds nothing and the object store 20.
- *Rule 2:* `module check` refuses a module whose identity overflows what it requires, naming the
slug length, and passes a long name requiring a keyless provision; a test runs the same judgement
over every manifest in the real catalogue on the default machine-name length, so the catalogue's
own pull request fails on an overflow.
- *Rule 3:* a controller test against a real store reproduces the night it was found — the network
manager, no slug, on a six-character machine, requiring the resolver provision, beside a consumer
that overflows an object store: the provider's declaration composes, the network manager is granted,
the overflowing consumer is left out, named by the push, carried by `status` and its document, and
the mesh is not called well.
- *Rule 4:* a test that a DNS-label identity under a bound over 63 is refused when the definition is
parsed.
## References
- [Issue 263](../04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md) —
the observation.
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) — the bound this refines; its
option C.
- [ADR 0202](0202-a-provider-declares-what-it-derives-for-each-consumer.md) —
a provider declares what it derives; the bound is declared beside it.
- `mesh-controller` `internal/catalogue/identity.go` (`IdentityBound`, `CheckIdentityWithin`,
`IdentityProblems`, `Resolution.Overflowing`), `manifest.go` (`Offer.Identity`,
`IdentityBoundOf`), `cmd/mesh-controller/plan.go` (`grantsFor`), `check.go`, `status.go`.
- `mesh-catalog` — each provider's offer states its bound.
+1
View File
@@ -323,6 +323,7 @@ python3 00-META/checks/index.py fail if stale
- **0215** — [The machine's message bus is a node seat, and it is never restarted live](0215-the-machines-message-bus-is-a-node-seat-and-is-never-restarted-live.md)
- **0216** — [The agent's configuration is registered through its module, at three scopes, and served as one plugin](0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md)
- **0220** — [What a machine asks needs its uplink held, and the retired resolver pieces go](0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)
- **0225** — [A consumer's identity is bounded by the provision it requires, judged before merge, and never refuses its provider](0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md)
### How it is built
@@ -2,8 +2,9 @@
layer: to-be
status: in-progress
code: [mesh-controller internal/catalogue]
updated: 2026-10-02
updated: 2026-10-06
decisions:
- 02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
@@ -250,7 +251,11 @@ value in a container's environment is refused when the definition is parsed, wit
**A module is assigned at most once to a node**, and that pair is the assignment's identity
([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). Its directories,
containers, login, broker account and settings are keyed by it, as today, and a login still fits the
tightest backend ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)).
backend that keeps it ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)):
the bound is the one the offer of each provision it requires states, a keyless provision states none,
and an overflow is refused by the catalogue check before merge and left out of the provider's grants,
reported, at composition — never a refusal of the provider's machine
([ADR 0225](../../02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md)).
**A module may run on many nodes, and one assignment may hold a seat**
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md))). The definition
@@ -1,7 +1,7 @@
---
status: open
status: located
opened: 2026-10-06
located-in: []
located-in: [mesh-controller internal/catalogue, mesh-controller cmd/mesh-controller, mesh-catalog modules]
fixed-by:
amended-design:
---
@@ -52,6 +52,22 @@ the remedy. That has two costs:
3. **Never refuse a provider's whole declaration** for one consumer's identity. Leave that consumer's
grant out, say so in `status`, and keep the provider's machine pushable.
## Where it lives
- **The one bound** is `identityLimit` in the controller's `internal/catalogue/identity.go`, applied to
every consumer whatever it requires. Nothing in a provider's definition could say otherwise.
- **The refusal** was raised in `grantsFor` (`cmd/mesh-controller/plan.go`), while composing the
*provider's* declaration: an error there fails the whole composition, so the anchor could not be
pushed for a module on another machine. No assignment, module check or test judged it before.
- **The facts** are in each provider's code in the catalogue: the object store keeps the identity as
an access key (20), the databases as roles and database names (63, 128), the identity provider as a
client id (255); the resolver and the route providers keep no name of their consumers.
Decided in [ADR 0225](../../02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md),
which takes ADR 0049's option C and adds the two placements the issue asks for: each offer states its
bound, `module check` judges every identity on the longest machine name before merge, and a provider
leaves an overflowing consumer out of its grants and says so, in `status` too, instead of refusing.
## How it is checked (once fixed)
- A catalogue test: a module requiring a keyless provision composes with a long name.