ADR 0110 and 0111: a seat is a module assignment from a closed set, and a build source may live on the git seat

Seats have been doing two jobs and neither is written down. The mechanism ADR 0009 introduced is
enforced — a second holder is refused — but any well-formed name becomes a seat by being claimed,
and nothing can say which seats a mesh has or who holds them: holdings are assembled while planning
and discarded. The enumeration done while preparing this missed the control plane's own manifest,
because core modules' manifests live in its repository rather than the catalogue.

0110 closes the set. Each seat has a name, a scope, what occupying it delivers, and the record that
made it one; a claim outside the set is refused. A seat is held by a module assignment, and what the
mesh knows about the holder is what it knows about that assignment — nothing is stored beside it. A
seat may deliver a provision, and then its holder answers for it among several providers: pin, then
the holder, then the only provider, then refused. That keeps 0009's "refused, never guessed": the
seat is the choice made once, mesh-wide, instead of a pin per consumer node. The first set is the
eleven seats already claimed plus 0109's npm-package-registry, so nothing in use is refused.

Two concepts — seats for exclusion, a new word for consumable singulars — was rejected: both mean
"this mesh's one X", and the overview a person wants is one list.

0111 gives the mesh a git seat and makes a build source one of two explicit forms: a repository on
the seat's holder, recorded by its path and cloned from wherever the holder runs at build time; or
an external URL, recorded and cloned exactly as given. Recognising self-hosted sources by matching
URLs against the forge's address was rejected — it fails in the one case it exists for, after the
forge moves. Credentials for private repositories are left undecided and said so.

Design: new to-be 26 (the seats); 23 gains the seat step in resolution; 18's source entry names
the two forms; the glossary's seat and provision entries say where they meet. 0109 is carried from
its own branch so every link here resolves.
This commit is contained in:
jochen
2026-09-25 20:33:14 +02:00
parent 59c93dcfe4
commit dbe100ca96
8 changed files with 403 additions and 9 deletions
+3 -2
View File
@@ -5,8 +5,9 @@ code:
- mesh-controller cmd/mesh-builder
- mesh-controller internal/builder
- mesh-catalog modules/builder
updated: 2026-09-21
updated: 2026-09-25
decisions:
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
- 02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md
- 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
- 02-DECISIONS/0091-a-mount-is-declared-three-ways.md
@@ -38,7 +39,7 @@ controller's again. The builder's whole responsibility is the middle.
| term | is |
|---|---|
| **source** | a repository, a path within it, and a ref — resolved to one commit ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)) |
| **source** | a repository, a path within it, and a ref — resolved to one commit ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)). The repository is either on the forge holding the `git` seat, recorded by its path there and cloned from wherever that forge runs at build time, or external, recorded and cloned exactly as given ([ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md)) |
| **recipe** | how *one* artifact is produced from that source |
| **toolchain** | what a recipe runs inside — a compiler, a runtime, the SDK |
| **artifact** | what a recipe produced, named by the digest of its content |
+11 -3
View File
@@ -2,10 +2,11 @@
layer: to-be
status: designed
code: []
updated: 2026-09-20
updated: 2026-09-25
decisions:
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
- 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
---
# 23 — Choosing a provider
@@ -50,9 +51,16 @@ provider on a different node. That coupling is exactly what may not be guessed,
names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is
to that provider and not to whichever one is nearest.
**A seat names the mesh's one provider of a kind.** Where a seat delivers the provision, its holder
answers for it when several providers exist and the consumer named none. That is not picking: the
choice was made once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer
coupled to particular contents has said so.
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
named, and none is co-located, the requirement is unsatisfiable and is refused with the candidates
shown — the same stance
named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused
with the candidates shown — the same stance
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took
against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer
delivered quietly costs more than a refusal.
+125
View File
@@ -0,0 +1,125 @@
---
layer: to-be
status: in-progress
code:
- mesh-controller internal/catalogue/seats.go
- mesh-controller internal/catalogue/resolve.go
- mesh-controller cmd/mesh-controller/seats.go
- mesh-controller cmd/mesh-controller/build.go
- mesh-catalog modules/gitea/module.json
updated: 2026-09-25
decisions:
- 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
---
# 26 — The seats
**What a mesh can have one of, and who fills each.** A seat is a named role at a scope, taken by a
module assignment. The mesh defines which seats exist. Occupying one may deliver a provision, and the
list of seats with their holders is the quickest answer to "what is in this mesh".
## What a seat is
A seat has four properties, fixed by the mesh rather than by any module:
| property | is |
|---|---|
| name | what a manifest claims, and what a person reads in the list |
| scope | node, site or mesh: where there may be only one holder |
| delivers | the provision its holder answers for, or nothing |
| decision | the record that made it a seat |
**A module assignment holds a seat by claiming it.** The claim is the manifest's `claims`, and it is
satisfied by assigning the module somewhere. The seat is not a second record beside the assignment.
It points at the assignment, and everything the mesh knows about the holder is what it knows about
that assignment: the node, the node's settings for the module, and what the module serves.
**The set is closed.** A claim naming a seat the mesh does not define is refused, and so is a claim at
the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the host's
vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry nobody
argued for is an entry nobody can explain.
## The set
| seat | scope | delivers | typically held by |
|---|---|---|---|
| `mesh-controller` | mesh | — | the controller |
| `mesh-store` | mesh | `postgres-database` | the store |
| `mesh-broker` | mesh | `amqp` | the broker |
| `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
| `the-catalogue` | mesh | — | the catalogue |
| `npm-package-registry` | mesh | `npm-package-registry` | the forge |
| `git` | mesh | `git` | the forge |
| `the-build-machine` | node | — | a builder |
| `the-dns-port` | node | — | the local resolver |
| `the-intrusion-prevention` | node | — | an intrusion-prevention service |
| `the-packet-filter` | node | — | the packet filter |
| `the-resolver-configuration` | node | — | whichever of the alternative resolver configurations is chosen |
| `the-showcase` | node | — | the showcase module |
The control plane holds this set in code, and a test asserts both its size and that every entry names
the record that made it a seat. This document follows the code, not the reverse. If the two disagree,
the test has been changed without this table, and the table is what is wrong.
## A seat that delivers a provision
A seat that delivers a provision may only be held by a module that provides it, at the seat's scope.
A mesh seat delivers a mesh-scoped provision.
**Its holder answers for that provision.** When a requirement for it has more than one provider in the
mesh, the control plane takes, in order:
1. the provider the consumer's node was pinned to, because a consumer coupled to one provider's
contents has said so ([23 — Choosing a provider](23-choosing-a-provider.md));
2. the holder of the seat;
3. the only provider, when there is one;
4. otherwise nothing, and the requirement is refused with the candidates named.
So a second provider can run beside the holder and harm nothing. The forge holds
`npm-package-registry`. An npm proxy may provide the same provision on another machine, and a module
requiring an npm registry is still served by the forge, without anybody pinning it. Moving the role
to the proxy is moving the seat: unassign the claim from one, assign it to the other, and every
consumer follows.
**What a consumer receives is a grant**, the same as for any provision: where the provider answers,
what it serves, and a credential where one is minted. A consumer never reads the seat directly. The
one exception is the control plane itself, which reaches the store and the broker through a narrow
seat placeholder because it made them before any module existed and cannot be their consumer.
## A seat that delivers nothing
Most node seats deliver nothing. They say which module is this machine's packet filter, or which of
two alternative resolver configurations it runs, and a second holder is refused. That is the whole of
their job, and it is a real one: it is the mesh saying what a machine is, in words a person can read.
## The overview
The control plane lists every seat in the set with its scope, what it delivers, and each holder as a
node and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no
forge", and not a fault.
Holdings are derived from assignments whenever they are asked for, never stored. The list is always
what the mesh is running, because it is computed from the same thing that decides what the mesh runs.
## The git seat, and where a build comes from
A module is built from a repository, a path and a ref. The repository is one of two things, and the
mesh records which:
| form | means | recorded as |
|---|---|---|
| on the `git` seat | a repository on the forge that holds the seat | its path on the forge, and the seat |
| external | a repository anywhere else, a public forge for instance | its URL, exactly as given |
For a repository on the seat, the control plane composes the clone URL at the moment of building,
from where the holder runs and the scheme and port it serves for `git`. The recorded source never
contains an address, so moving the forge changes nothing that was recorded. The build machine is not
told the difference: it receives a URL either way.
With the seat unheld, a build from the seat is refused and says why. External builds carry on.
**Not yet designed:** a credential for cloning a private repository. The mesh's own repositories are
public. The natural place for a clone credential is the `git` provision's grant, and that is a
decision still to take.
+1
View File
@@ -34,6 +34,7 @@ document is written and this one's status becomes `implemented`.
| [`22-the-work-ahead.md`](22-the-work-ahead.md) | Everything decided and not yet built, in dependency order, each phase ending at a run | [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md), [ADR 0075](../../02-DECISIONS/0075-two-stores-and-which-provides-what.md), [ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md) |
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
| [`26-the-seats.md`](26-the-seats.md) | What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
## Not yet written