ADR 0054: add option E (a declared slug) and recommend it over A+B

Implementing A (bound the identity at 20) revealed the readable budget is node+module
<= 14 chars — so tight that the catalogue's own test names (workstation+keycloak, 25)
compact to an opaque hash. B's fallback would fire for the common case, not the rare
overflow, inverting A+B into mostly-opaque identities. Option E — an optional short
slug a module/node declares, preferred over the cleaned name — is the escape hatch B
wanted to be without the opacity: legible because a person chose it, and it makes an
early refusal palatable (refuse on the slug field, not the machine's name). B dropped;
E recommended over a bound of 20, composing with C later if needed.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-05 02:30:11 +02:00
parent 89b65dd4c0
commit af5c939d16
@@ -70,29 +70,54 @@ keeps meeting — but it directly contradicts 023 and ADR 0053: the identity wou
mesh's single derivation the two ends share, it would be a value one side invents and the other must mesh's single derivation the two ends share, it would be a value one side invents and the other must
be told. Listed for completeness; not recommended. be told. Listed for completeness; not recommended.
**E — A module (and a node) may declare a short slug; the identity is built from it.** The identity
becomes `mesh_<node-slug|node-name>_<module-slug|module-name>`: where a slug is declared it is used,
otherwise the cleaned name. A slug is a deliberately short, operator-chosen identifier — `kc` for
keycloak, `wkstn` for a workstation. It is optional: short names (`anchor`, `redis`) need none.
- *For:* this is the escape hatch B wanted to be, without the opacity. The name stays legible — a
provisioner can read `mesh_wkstn_kc` and know who is asking — because a person chose it, not a
hash function. And it makes an early refusal *palatable*: if even the slug-built identity overflows,
the refusal points at the slug, a field made for exactly this, rather than at the machine's name.
Both ends still derive it from one declared thing, so they agree by construction.
- *Against:* a new optional manifest field, and someone must pick the slug — but only for names that
would otherwise overflow, and picking a short legible identifier is a better job than being handed
a hash.
## What implementing A revealed
A was tried first. At `identityLimit = 20`, the readable budget is `mesh_` (5) + node + `_` + module
≤ 20, i.e. **node + module ≤ 14 characters** — far tighter than it looked. The catalogue's own
existing tests use `workstation`+`keycloak` (25), which compacts to `mesh_dbbc02f8dde34d3`; common
mesh names (`home-server`, `the-build-node`, `workstation`) blow the budget with any module. So B's
compact fallback would fire for the *common* case, not the rare overflow — which inverts A+B: most
identities would be opaque hashes. A alone (hard refusal at 20) would refuse most realistic names.
This is what moved the recommendation to E: the problem is not the limit, it is that the *readable
name* is the wrong source when it is long, and a slug is a better source than either a hash or a ban.
## Recommendation ## Recommendation
**A, with B held as the escape hatch.** Fixing the constant is what the code already says it meant **E, over a per-consumer bound (start with the global minimum, 20).** Build the identity from an
to do ("the shortest limit among the systems these names reach"), and moving the failure to an optional slug, keep it when it fits, and refuse at assignment with "declare or shorten `<module>`'s
honest assignment-time refusal is strictly better than a silent provision loop. Accept the naming slug" when it does not — no hash, no lost legibility, and the fix is a first-class field. Set the
budget first; it is a forcing function toward short node/module names, which the mesh benefits from bound to the true minimum (20) now; it needs no per-interface machinery to unblock S3, and a module
elsewhere. If the ≤14 budget proves too tight in real use, add B's compact fallback for the overflow that consumes S3 simply declares a short slug. Graduate to **C** (per-interface bounds) later if it
case only — keeping the legible name for everyone it fits. Reach for C only if per-interface bounds turns out that non-S3 consumers are paying for S3's limit often enough to mind — E and C compose:
turn out to matter beyond this one 20-vs-63 gap; it is the right shape but more machinery than the slugs are the mechanism, per-interface bounds refine where the ceiling sits. **B is dropped**: a
problem currently demands. declared slug is a strictly better escape hatch than an opaque hash. **D stays rejected.**
## Consequences (of A) ## Consequences (of E)
- `identityLimit` becomes 20; the comment stops claiming postgres is the shortest and names S3 as - A module manifest gains an optional `slug`; a node may carry one too. `ConsumerIdentity` prefers
the binding one. `CheckIdentity` refuses over-long names at `module add` / assignment, not at the slug over the cleaned name for each half. `identityLimit` becomes 20 (the true minimum), and
provision. `CheckIdentity` refuses at `module add` / assignment — now with a message naming the slug to set.
- Existing node/module names longer than the budget must be shortened before they can consume a - The common case stays legible; only names that overflow the budget need a slug, and what they get
provision — a migration cost paid once, surfaced as a clear refusal. is a name a person chose, not a hash.
- minio (04-ISSUES/010) is unblocked without any provider-specific mapping, and the minio - Existing modules/nodes whose names overflow declare a slug once — a migration cost paid as a clear
per-backend e2e (skipped, pending this) can run. refusal with an obvious remedy, not a silent hash or a silent truncation.
- **How it is checked:** the minio grant e2e — a consumer with a name inside the budget reaches its - minio (04-ISSUES/010) is unblocked: an S3 consumer declares a short slug and its access key fits.
bucket with the credential the mesh delivered — and a unit test that `CheckIdentity` refuses a - **How it is checked:** the minio grant e2e — a consumer whose (slugged) identity fits reaches its
name whose derived identity exceeds 20. bucket with the credential the mesh delivered — plus unit tests that a slug is preferred, that an
un-sluggable over-long identity is refused (naming the slug), and that two consumers never collide.
## References ## References