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:
+11
-4
@@ -44,15 +44,22 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
|
|
||||||
## How modules relate to the mesh
|
## How modules relate to the mesh
|
||||||
|
|
||||||
- **seat** — a named position at a scope (node / site / mesh) with a **capacity**. A capacity-1 seat
|
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a
|
||||||
is exclusive (one holder); a higher-capacity seat is a **bench** (several holders coexist).
|
**closed set** the mesh defines: a claim naming a seat outside the set is refused. A seat may
|
||||||
|
**deliver a provision**, and its holder is then the mesh's answer for it when several modules
|
||||||
|
provide it ([ADR 0110](../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)).
|
||||||
|
The set, with who holds each seat, is the overview of what a mesh has
|
||||||
|
([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)). A seat has a **capacity**: a
|
||||||
|
capacity-1 seat is exclusive (one holder); a higher-capacity seat is a **bench** (several holders
|
||||||
|
coexist).
|
||||||
- **claim** — a module taking a spot on a seat. `claims: [{name, scope}]` in a manifest. A
|
- **claim** — a module taking a spot on a seat. `claims: [{name, scope}]` in a manifest. A
|
||||||
mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is
|
mesh-scoped exclusive claim is how the mesh says "there is one of me". A foundation seat is
|
||||||
named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim
|
named after the server it guards: the `mesh-controller`, `postgres` and `lavinmq` modules claim
|
||||||
the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)).
|
the `mesh-controller`, `mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)).
|
||||||
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
|
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
|
||||||
and wires the two with an endpoint and a credential. This is separate from seats: a provision is
|
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat
|
||||||
a service you offer, a seat is a slot you occupy.
|
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
||||||
|
what makes a module *the* provider of it.
|
||||||
|
|
||||||
## How this page is kept
|
## How this page is kept
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-25
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 0009-modules-and-the-graph.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 110. A seat is a module assignment from a closed set, and it may deliver a provision
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0009](0009-modules-and-the-graph.md) introduced claims: a module declares something
|
||||||
|
singular, at a scope, and a second holder is refused. [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)
|
||||||
|
named the foundation's three after their servers. That mechanism is enforced and works. What it
|
||||||
|
means has drifted, and three things are now true of it that no record says.
|
||||||
|
|
||||||
|
**Any well-formed name becomes a seat by being claimed.** The control plane's manifest check
|
||||||
|
refuses a claim only for a malformed name or an unknown scope. Nothing says which seats a mesh has.
|
||||||
|
The names in use were each invented by the module that claims them: `the-showcase`,
|
||||||
|
`the-build-machine`, `the-intrusion-prevention`.
|
||||||
|
|
||||||
|
**Nothing can say what a mesh has, or who fills it.** There is no seat table and no command that
|
||||||
|
lists seats. Holdings are assembled while planning, one node at a time, and discarded afterwards.
|
||||||
|
The only way to answer "which seats does this mesh have, and which module holds each" is to read
|
||||||
|
every manifest in two repositories, because the core modules' manifests moved into the control
|
||||||
|
plane's own repository ([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)). While this record
|
||||||
|
was being prepared, that enumeration was done once by hand, and it missed the control plane's own
|
||||||
|
manifest: eleven claims were reported where there are twelve.
|
||||||
|
|
||||||
|
**Some seats are the mesh's one of something that others consume, and nothing uses that fact.**
|
||||||
|
Of the twelve claims, four are held by a module that provides something consumers require:
|
||||||
|
`mesh-store` (`postgres-database`, eleven consumers), `mesh-broker` (`amqp`), `the-artifact-store`
|
||||||
|
(`artifact-store`) and `the-dns-port`. The rest deliver nothing to anybody, and are still
|
||||||
|
meaningful: they say which module is this mesh's packet filter, or resolver configuration.
|
||||||
|
Meanwhile a requirement for a mesh-scoped provision with more than one provider is refused until a
|
||||||
|
person pins, **per consumer node**, which provider to use. [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)
|
||||||
|
anticipates exactly that case — gitea and verdaccio both answering npm — and under today's
|
||||||
|
resolution it would mean a pin on every machine that builds anything.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. Leave seats as free-form exclusion, and keep provisions as the only thing that delivers.**
|
||||||
|
Rejected. The overview stays unanswerable, and a second provider of anything costs a pin per
|
||||||
|
consumer node — the decision "gitea is our npm registry" made again on every machine.
|
||||||
|
|
||||||
|
**2. Two concepts: seats for exclusion, and a new word for the mesh's one consumable thing.**
|
||||||
|
Rejected. Both mean "this mesh's one X". Every existing claim would first have to be classified
|
||||||
|
into one or the other, and the overview a person wants is one list, not two.
|
||||||
|
|
||||||
|
**3. A seat is a module assignment from a closed set, and occupying it may deliver a provision.**
|
||||||
|
Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The mesh defines a closed set of seats.** Each entry has a name, a scope, what occupying it
|
||||||
|
delivers (if anything), and the decision that made it a seat. A claim naming a seat outside the set
|
||||||
|
is refused, and so is a claim at a scope other than the seat's. Adding a seat is a decision, for the
|
||||||
|
same reason adding a shape to the host's vocabulary is one: the set is what a person reads to learn
|
||||||
|
what a mesh can have, and a name added without an argument is a name nobody can explain later.
|
||||||
|
|
||||||
|
**A seat is held by a module assignment.** What the mesh knows about a seat's holder is what it knows
|
||||||
|
about that assignment: its node, the node's settings for it, and what it serves. Holdings are not
|
||||||
|
stored separately. The seat points at an assignment, and a second record of the same fact would be a
|
||||||
|
second thing to disagree with the first.
|
||||||
|
|
||||||
|
**A seat may deliver a provision, and then its holder answers for it.** A seat that delivers a
|
||||||
|
provision can only be held by a module that provides it, at the seat's scope, and a claim that does
|
||||||
|
not is refused. When several providers answer a requirement for that provision, resolution takes, in
|
||||||
|
order:
|
||||||
|
|
||||||
|
1. a provider the consumer's node was **pinned** to — a consumer coupled to one provider's
|
||||||
|
contents, as [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md) already allows;
|
||||||
|
2. **the holder of the seat** that delivers it;
|
||||||
|
3. the **only** provider, when there is one;
|
||||||
|
4. otherwise, refused with the candidates named, as now.
|
||||||
|
|
||||||
|
This keeps [ADR 0009](0009-modules-and-the-graph.md)'s rule that a requirement with several answers is
|
||||||
|
never guessed. The seat is not a guess. It is the choice made once, mesh-wide, by assigning the holder,
|
||||||
|
instead of once per consumer by pinning. A second provider may run beside the holder, and whatever
|
||||||
|
requires the provision still resolves to the holder without anybody naming it.
|
||||||
|
|
||||||
|
**Seats are also informational.** The control plane lists every seat in the set, what it delivers,
|
||||||
|
and which assignment holds it, including seats nobody holds. An unheld seat is an answer, "this mesh
|
||||||
|
has no X", not an error.
|
||||||
|
|
||||||
|
**The first set is the eleven seats already claimed, plus one.** Twelve claims are in use, and they
|
||||||
|
name eleven seats because two alternative modules claim `the-resolver-configuration`. This record
|
||||||
|
admits every seat the catalogue and the control plane claim today, so no module is refused by it:
|
||||||
|
|
||||||
|
| seat | scope | delivers | held today by | made a seat by |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `mesh-controller` | mesh | — | `mesh-controller` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
||||||
|
| `mesh-store` | mesh | `postgres-database` | `postgres` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
||||||
|
| `mesh-broker` | mesh | `amqp` | `lavinmq` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
|
||||||
|
| `the-artifact-store` | mesh | `artifact-store` | `distribution` | [0075](0075-two-stores-and-which-provides-what.md) |
|
||||||
|
| `the-catalogue` | mesh | — | `mesh-catalog` | this record |
|
||||||
|
| `npm-package-registry` | mesh | `npm-package-registry` | `gitea` | [0109](0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
||||||
|
| `the-build-machine` | node | — | `builder` | this record |
|
||||||
|
| `the-dns-port` | node | — | `dnsmasq` | this record |
|
||||||
|
| `the-intrusion-prevention` | node | — | `fail2ban` | this record |
|
||||||
|
| `the-packet-filter` | node | — | `nftables` | this record |
|
||||||
|
| `the-resolver-configuration` | node | — | `resolv-conf` or `resolved-split-dns` | this record |
|
||||||
|
| `the-showcase` | node | — | `showcase` | this record |
|
||||||
|
|
||||||
|
`npm-package-registry` is the one addition. It is [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md)'s
|
||||||
|
seat, named after the provision it delivers, as 0079 named the foundation's seats after what they are.
|
||||||
|
gitea claims it. verdaccio provides the same provision and claims nothing, so it is the second
|
||||||
|
provider this record exists to make harmless.
|
||||||
|
|
||||||
|
`the-dns-port` is listed as delivering nothing, although `dnsmasq` provides `wildcard-resolution`.
|
||||||
|
That provision is node-scoped and answered on the machine, so no preference between providers
|
||||||
|
arises. Whether the seat should say it delivers it is left for when a second resolver makes the
|
||||||
|
question real.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The control plane carries the set in code. A test asserts its size, and that every entry names the
|
||||||
|
record that made it a seat, so changing the set means finding the argument rather than a number.
|
||||||
|
This is the pattern the host's vocabulary test already follows.
|
||||||
|
- Manifest validation refuses an unknown seat, a seat claimed at the wrong scope, and a
|
||||||
|
provision-delivering seat claimed by a module that does not provide the provision. The three
|
||||||
|
refusals name the seat and the set.
|
||||||
|
- Resolution prefers the seat's holder among several providers, after a pin. A provider record
|
||||||
|
gains the module it came from, because two modules on one node could otherwise not be told apart
|
||||||
|
as holder and non-holder.
|
||||||
|
- A `seats` command lists the set with each seat's holders, derived from assignments.
|
||||||
|
- gitea claims `npm-package-registry`. The catalogue's `package-registry` becomes
|
||||||
|
`npm-package-registry` throughout, per [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md).
|
||||||
|
- **What got harder:** a module wanting a new singular role can no longer invent a name. It needs a
|
||||||
|
record. That is the point, and it costs one record per seat.
|
||||||
|
- **Not changed:** `${seat:<seat>:<port>}` stays as it is. It exists so the control plane can reach a
|
||||||
|
foundation it made before any module existed, and it cannot be a consumer. A module that needs
|
||||||
|
something from a seat's holder requires the provision the seat delivers, and receives it the way
|
||||||
|
any provision is received: through a grant.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The set is closed, and every entry names its decision | A control-plane unit test asserts the set's size and a non-empty decision for every entry. |
|
||||||
|
| A claim outside the set is refused | Manifest-validation tests for an unknown seat, the wrong scope, and a delivering seat whose claimant does not provide. |
|
||||||
|
| Every module in use claims a seat in the set | A control-plane test parses every catalogue manifest and fails on any refused claim. The lab's beds read the same manifests ([ADR 0089](0089-a-bed-reads-the-catalogue-it-proves.md)). |
|
||||||
|
| The holder answers among several providers | Resolution tests: two providers with the seat held, two with a pin overriding the seat, two with the seat unheld (refused). |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0009](0009-modules-and-the-graph.md): claims, scopes, and "refused, never guessed"
|
||||||
|
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md): a seat named after what it is
|
||||||
|
- [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md): the per-ecosystem registry seats
|
||||||
|
- [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md): pins, co-location and refusal
|
||||||
|
- `mesh-controller internal/catalogue/resolve.go` (`checkClaims`, the brokered branch of `Resolve`),
|
||||||
|
`internal/catalogue/manifest.go` (claim validation), `cmd/mesh-controller/plan.go` (holdings)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
topic: building it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-25
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 0069-a-module-is-a-repository-and-a-path.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 111. A build source is on the mesh's git seat, or it is an external repository
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0069](0069-a-module-is-a-repository-and-a-path.md) made a module a repository, a path and a
|
||||||
|
ref, and the control plane records all three against the module so it can rebuild it and say when
|
||||||
|
its source has moved ahead. **The repository is recorded exactly as a person typed it.** `build
|
||||||
|
<repository>` hands the string to a build machine, which runs `git clone` on it, and the same string
|
||||||
|
becomes the module's recorded source.
|
||||||
|
|
||||||
|
**So a self-hosted forge's address is written into every module built from it.** The mesh runs its
|
||||||
|
own forge, and most of what it builds lives there. Every one of those modules carries the forge's
|
||||||
|
scheme, host and port in its recorded source. Move the forge to another machine, or change the port
|
||||||
|
it is published on, and every recorded source is stale at once. Nothing notices until a rebuild fails
|
||||||
|
to clone.
|
||||||
|
|
||||||
|
**And nothing names the mesh's git at all.** gitea serves git over HTTP and over SSH, and the mesh's
|
||||||
|
vocabulary contains neither. No provision, no `serves`, no seat, as the forge survey
|
||||||
|
([research 013](../01-RESEARCH/013-the-forge-and-the-registries/the-survey.md)) found. The only trace
|
||||||
|
is a label on its public route, which the mesh is explicitly not meant to interpret.
|
||||||
|
|
||||||
|
**External repositories are ordinary, and must stay so.** An application the mesh hosts may live on a
|
||||||
|
public forge. Building it from its URL works today and must keep working unchanged.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. Keep recording literal URLs.** Rejected. It is the problem: the forge's address copied into
|
||||||
|
every module built from it.
|
||||||
|
|
||||||
|
**2. Recognise a self-hosted source by matching its URL against the forge's current address.**
|
||||||
|
Rejected. It infers the kind of source from the shape of a string, and the inference fails in the
|
||||||
|
one case it exists for: after the forge moves, old URLs no longer match anything.
|
||||||
|
|
||||||
|
**3. Two explicit forms: a repository on the holder of the `git` seat, or an external URL.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The mesh has a `git` seat.** It is mesh-scoped and delivers the `git` provision, per
|
||||||
|
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md). Its holder provides `git`,
|
||||||
|
serving how a repository on it is cloned: the scheme and the port. gitea claims it.
|
||||||
|
|
||||||
|
**A source is on the git seat, or it is external, and the mesh records which.**
|
||||||
|
|
||||||
|
- `build --self <owner>/<repository>` builds from a repository on the seat's holder. The recorded
|
||||||
|
source is the repository's path on that holder, and the seat it is on. **It never contains an
|
||||||
|
address.** At the moment of building, the control plane composes the clone URL from where the
|
||||||
|
holder runs and what it serves for `git`, so a moved forge changes nothing recorded.
|
||||||
|
- `build <url>` is unchanged: an external repository, recorded and cloned exactly as given. GitHub
|
||||||
|
and GitLab are the ordinary cases.
|
||||||
|
|
||||||
|
**An unheld seat refuses self-hosted builds and nothing else.** With nobody holding `git`, `build
|
||||||
|
--self` is refused, naming the seat and saying what would hold it. External builds are unaffected. A
|
||||||
|
mesh without a forge of its own builds from external repositories only, and says so rather than
|
||||||
|
failing to clone.
|
||||||
|
|
||||||
|
**The build machine is not told the difference.** It receives a URL either way. Composing the URL is
|
||||||
|
the control plane's job, because only the control plane knows where the seat's holder runs.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The control plane's inventory gains a column saying which seat a source is on. It is empty for
|
||||||
|
every module recorded before this, which is correct: they were all recorded as literal URLs.
|
||||||
|
- `build`, `build --behind` and `build --dry-run` resolve a seat source before asking a builder. The
|
||||||
|
recorded source keeps the seat form; the build log keeps the URL that was actually cloned, because
|
||||||
|
that is what happened.
|
||||||
|
- gitea claims `git` and provides it, serving HTTP clone on its web port.
|
||||||
|
- **Not decided: credentials for private repositories.** The mesh's own repositories are public, and
|
||||||
|
clone without one. A private repository still works only if the build machine's own git
|
||||||
|
configuration authenticates, exactly as before. Delivering a clone credential through the `git`
|
||||||
|
provision's grant is the obvious next step, and it is its own decision.
|
||||||
|
- **Not changed:** modules already recorded from the forge keep their literal URLs until they are
|
||||||
|
rebuilt with `--self`. Rewriting them in place would be the URL-matching this record rejects.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A seat source records no address | A control-plane test resolves a seat source and asserts the recorded repository is the path alone. |
|
||||||
|
| The URL comes from the holder | A test composes the clone URL from a holder's node address and served `git` scheme and port, and a second where the port was moved on the node. |
|
||||||
|
| An unheld seat refuses self-hosted builds only | A test with no holder: `--self` is refused naming the seat; an external URL passes through unchanged. |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0069](0069-a-module-is-a-repository-and-a-path.md): a module is a repository, a path and a ref
|
||||||
|
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md): seats, and a seat delivering a provision
|
||||||
|
- [Research 013](../01-RESEARCH/013-the-forge-and-the-registries/the-survey.md): git is served and declared nowhere
|
||||||
|
- `mesh-controller cmd/mesh-controller/build.go`, `internal/builder/builder.go`
|
||||||
@@ -156,6 +156,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0087** — [A seeded file is created once, and what grows in it is not the mesh's](0087-a-seeded-file-is-created-once.md)
|
- **0087** — [A seeded file is created once, and what grows in it is not the mesh's](0087-a-seeded-file-is-created-once.md)
|
||||||
- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md)
|
- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md)
|
||||||
- **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md)
|
- **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md)
|
||||||
|
- **0110** — [A seat is a module assignment from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
@@ -175,6 +176,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0096** — [An upstream image is copied between registries, never through a machine's image store](0096-an-upstream-image-is-copied-between-registries.md)
|
- **0096** — [An upstream image is copied between registries, never through a machine's image store](0096-an-upstream-image-is-copied-between-registries.md)
|
||||||
- **0097** — [A vendor image is a declared build input, and a recipe fetches nothing undeclared](0097-a-vendor-image-is-a-declared-build-input.md)
|
- **0097** — [A vendor image is a declared build input, and a recipe fetches nothing undeclared](0097-a-vendor-image-is-a-declared-build-input.md)
|
||||||
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
|
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
|
||||||
|
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
|
||||||
|
|
||||||
### How it is checked
|
### How it is checked
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ code:
|
|||||||
- mesh-controller cmd/mesh-builder
|
- mesh-controller cmd/mesh-builder
|
||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-catalog modules/builder
|
- mesh-catalog modules/builder
|
||||||
updated: 2026-09-21
|
updated: 2026-09-25
|
||||||
decisions:
|
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/0097-a-vendor-image-is-a-declared-build-input.md
|
||||||
- 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
|
- 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
|
||||||
- 02-DECISIONS/0091-a-mount-is-declared-three-ways.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 |
|
| 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 |
|
| **recipe** | how *one* artifact is produced from that source |
|
||||||
| **toolchain** | what a recipe runs inside — a compiler, a runtime, the SDK |
|
| **toolchain** | what a recipe runs inside — a compiler, a runtime, the SDK |
|
||||||
| **artifact** | what a recipe produced, named by the digest of its content |
|
| **artifact** | what a recipe produced, named by the digest of its content |
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: designed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-09-20
|
updated: 2026-09-25
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
||||||
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.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
|
# 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
|
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.
|
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
|
**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
|
named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused
|
||||||
shown — the same stance
|
with the candidates shown — the same stance
|
||||||
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took
|
[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
|
against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer
|
||||||
delivered quietly costs more than a refusal.
|
delivered quietly costs more than a refusal.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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) |
|
| [`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) |
|
| [`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) |
|
| [`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
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user