diff --git a/00-META/glossary.md b/00-META/glossary.md index 7d38f6d..4ffe66e 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -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 -- **seat** — a named position at a scope (node / site / mesh) with a **capacity**. A capacity-1 seat - is exclusive (one holder); a higher-capacity seat is a **bench** (several holders coexist). +- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a + **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 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 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 - and wires the two with an endpoint and a credential. This is separate from seats: a provision is - a service you offer, a seat is a slot you occupy. + and wires the two with an endpoint and a credential. A provision is a service you offer, a seat + 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 diff --git a/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md b/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md new file mode 100644 index 0000000..0d9e313 --- /dev/null +++ b/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md @@ -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::}` 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) diff --git a/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md b/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md new file mode 100644 index 0000000..ed7441a --- /dev/null +++ b/02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md @@ -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 +` 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 /` 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 ` 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` diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index bf98713..964231e 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -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) - **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) +- **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 @@ -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) - **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) +- **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 diff --git a/03-DESIGN/01-to-be/18-building-a-module.md b/03-DESIGN/01-to-be/18-building-a-module.md index 0076a36..5c77068 100644 --- a/03-DESIGN/01-to-be/18-building-a-module.md +++ b/03-DESIGN/01-to-be/18-building-a-module.md @@ -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 | diff --git a/03-DESIGN/01-to-be/23-choosing-a-provider.md b/03-DESIGN/01-to-be/23-choosing-a-provider.md index 50b3ed3..e3f36c0 100644 --- a/03-DESIGN/01-to-be/23-choosing-a-provider.md +++ b/03-DESIGN/01-to-be/23-choosing-a-provider.md @@ -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. diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md new file mode 100644 index 0000000..b809256 --- /dev/null +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -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. diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index 5c10294..bc88ea9 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -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