Adopt the glossary's vocabulary in the mutable design docs
"control plane" -> controller and "substrate" -> foundation throughout 03-DESIGN, 00-META and the README, with 06-the-control-plane.md and 07-the-substrate.md renamed to 06-the-controller.md and 07-the-foundation.md. The immutable 02-DECISIONS records keep their original wording (and links to them are unchanged) — a term retired here may still appear there, which the glossary explains how to read. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
+1
-1
@@ -30,7 +30,7 @@ named, and nothing should be designed around a particular one existing.
|
||||
- **A hosted model provider** supplies the thinking for non-human agents, drawn from a
|
||||
shared pool of subscriptions — which is why budget pacing is a first-class concern.
|
||||
- **Long-lived user services** rather than an orchestrator. No cluster scheduler, no cloud
|
||||
control plane.
|
||||
controller.
|
||||
|
||||
Defaults, not mandates. A second model provider is anticipated by design; nothing in the
|
||||
domain may assume one vendor's credential lifecycle.
|
||||
|
||||
+2
-2
@@ -28,8 +28,8 @@ target, not the present.
|
||||
| Repository | Tier | Holds |
|
||||
|---|---|---|
|
||||
| `mesh-host` | 0 | **exists.** The node host — one statically linked binary, requiring nothing present ([ADR 0005](../02-DECISIONS/0005-the-node-host.md)) |
|
||||
| `mesh-substrate` | 1 | the four pinned services, as declarations |
|
||||
| `mesh-control` | 2 | **exists.** The control plane and its contexts — one of seven built ([ADR 0006](../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
|
||||
| `mesh-foundation` | 1 | the four pinned services, as declarations |
|
||||
| `mesh-control` | 2 | **exists.** The controller and its contexts — one of seven built ([ADR 0006](../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
|
||||
| `mesh-surfaces` | 3 | tools, web, cli |
|
||||
| `mesh-sdk` | — | the stable spine modules build against — the tool-serving harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile ([ADR 0039](../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md)). |
|
||||
| `mesh-lab` | — | **exists.** The lab — scenario lifecycle, networking, placement. Ships to nobody; runs on a workstation. |
|
||||
|
||||
@@ -92,7 +92,7 @@ What needs something *usable* retries, which is what both provisioners do and is
|
||||
anyway, because a dependency can restart long after everything was applied.
|
||||
|
||||
**The network was a real gap, and the first thing in Phase 1 that needed a decision.** Adding a
|
||||
shape widens what a compromised control plane can express, so
|
||||
shape widens what a compromised controller can express, so
|
||||
[ADR 0029](../../02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)
|
||||
records why this one is worth it: an `action` could create a network and **nothing could ever
|
||||
remove it**, because an action leaves no footprint the host can undo. The vocabulary is nine.
|
||||
@@ -106,7 +106,7 @@ not after.
|
||||
|
||||
*Done 2026-08-31. Worth recording because the task was not the one written down.*
|
||||
|
||||
**The control plane special-cases nothing.** `provides`, `requires`, `contributes` and `grants`
|
||||
**The controller special-cases nothing.** `provides`, `requires`, `contributes` and `grants`
|
||||
are name-agnostic — asking for a bucket needed no change to the mesh at all. What was missing was
|
||||
a provider, and the last step where something on the machine turns a delivered secret into a key
|
||||
that works. So "add an object-store provision" was never mesh work.
|
||||
@@ -200,7 +200,7 @@ losing something.
|
||||
|
||||
*2026-08-31.* **The old system's brain is switched off; its services keep running.**
|
||||
|
||||
Not a migration and not a period of dual control. The old control plane — provisioning, the
|
||||
Not a migration and not a period of dual control. The old controller — provisioning, the
|
||||
coordinator, the pipeline, the things that *decide* and *write* — is stopped. Every workload it
|
||||
was managing goes on running exactly as it is, because nothing is managing it. Then the new mesh
|
||||
takes ownership of them one at a time.
|
||||
@@ -217,7 +217,7 @@ stop having opinions.
|
||||
|
||||
**Disabled, not merely stopped**, and this is the part that is easy to get wrong: those units are
|
||||
enabled, so stopping them lasts until the machine reboots. A reboot mid-conversion would bring the
|
||||
old control plane back and it would resume regenerating managed files underneath the new one —
|
||||
old controller back and it would resume regenerating managed files underneath the new one —
|
||||
which is the one situation where two systems really would be fighting over the same machine.
|
||||
|
||||
**A service left running with nothing managing it is the safe state.** It has its data, its
|
||||
|
||||
@@ -40,13 +40,13 @@ not the first one built** ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
|
||||
|
||||
| | **Bootstrap scenario** | **Full scenario** |
|
||||
|---|---|---|
|
||||
| Contains | virtual machines, the host binary, a pinned substrate bundle | a complete mesh: forge, coordinator, delivery, modules |
|
||||
| Contains | virtual machines, the host binary, a pinned foundation bundle | a complete mesh: forge, coordinator, delivery, modules |
|
||||
| Verdict from | what the host reports about the state it reconciled | a pipeline result ending in verify |
|
||||
| Exercises | the node host and the substrate | the control plane and everything above it |
|
||||
| Exercises | the node host and the foundation | the controller and everything above it |
|
||||
| Exists to | **develop the mesh** | **test what runs on it** |
|
||||
|
||||
The bootstrap scenario is a **strict subset**: same virtualisation, same networking, same
|
||||
lifecycle — it simply stops before a control plane exists. Everything from *"Where this sits in
|
||||
lifecycle — it simply stops before a controller exists. Everything from *"Where this sits in
|
||||
the way work happens"* onward describes the full scenario, and applies once there is a
|
||||
coordinator to describe.
|
||||
|
||||
@@ -343,7 +343,7 @@ from the existing system has run against any of it yet.
|
||||
|
||||
---
|
||||
|
||||
## The substrate
|
||||
## The foundation
|
||||
|
||||
### A node is a system container
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ machines:
|
||||
|
||||
place:
|
||||
all: [host]
|
||||
anchor: [substrate]
|
||||
anchor: [foundation]
|
||||
|
||||
snapshot: raised
|
||||
```
|
||||
@@ -334,12 +334,12 @@ than a fork.
|
||||
# bootstrap — tiers 0 and 1
|
||||
place:
|
||||
all: [host]
|
||||
anchor: [substrate]
|
||||
anchor: [foundation]
|
||||
|
||||
# full — adds a control plane, a forge, and a module under test
|
||||
# full — adds a controller, a forge, and a module under test
|
||||
place:
|
||||
all: [host]
|
||||
anchor: [substrate, control, forge]
|
||||
anchor: [foundation, control, forge]
|
||||
module: a-web-service
|
||||
assert:
|
||||
- the service answers on its published name
|
||||
@@ -524,7 +524,7 @@ machines:
|
||||
|
||||
place:
|
||||
all: [host]
|
||||
anchor: [substrate]
|
||||
anchor: [foundation]
|
||||
|
||||
snapshot: raised
|
||||
```
|
||||
|
||||
@@ -47,10 +47,10 @@ mesh database, and it has no listening surface.
|
||||
|---|---|
|
||||
| `apply` | reconciling declared state on this machine |
|
||||
| `store` | local state, authoritative while disconnected |
|
||||
| `link` | the single outbound connection to the control plane |
|
||||
| `link` | the single outbound connection to the controller |
|
||||
| `profile` | what this machine can be asked to do |
|
||||
| `inventory` | what this machine is and has |
|
||||
| `substrate.lock` | the pinned tier-1 descriptor, appliable with no mesh present |
|
||||
| `foundation.lock` | the pinned tier-1 descriptor, appliable with no mesh present |
|
||||
|
||||
### apply
|
||||
|
||||
@@ -74,7 +74,7 @@ the machine in whatever state it reached, and nothing must claim otherwise.
|
||||
|
||||
### store
|
||||
|
||||
Local, and **authoritative while disconnected**. Not a cache of the control plane — the record
|
||||
Local, and **authoritative while disconnected**. Not a cache of the controller — the record
|
||||
of what this node has applied and what it currently holds.
|
||||
|
||||
This is structural rather than convenient: if disconnection is an ordinary situation rather
|
||||
@@ -84,7 +84,7 @@ not come back and ask what it is.
|
||||
|
||||
### link
|
||||
|
||||
The node's one connection to the control plane, and its security boundary
|
||||
The node's one connection to the controller, and its security boundary
|
||||
([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)).
|
||||
|
||||
It is the broker connection that already exists
|
||||
@@ -137,11 +137,11 @@ One behaviour, two sources
|
||||
|
||||
| Situation | Source |
|
||||
|---|---|
|
||||
| no mesh reachable | `substrate.lock` — the pinned bundle the host carries |
|
||||
| mesh reachable | the control plane, over the link |
|
||||
| no mesh reachable | `foundation.lock` — the pinned bundle the host carries |
|
||||
| mesh reachable | the controller, over the link |
|
||||
|
||||
**The first node is not a different kind of node.** It is a node whose mesh is not up yet. It
|
||||
applies the bundle it carries, the control plane comes up on top of it, and from that moment it
|
||||
applies the bundle it carries, the controller comes up on top of it, and from that moment it
|
||||
takes declarations like every other node. Its specialness is temporary and self-erasing.
|
||||
|
||||
**A joining node does the minimum to be reachable and nothing else** — an identity, an address,
|
||||
@@ -155,7 +155,7 @@ Settled by [ADR 0005](../../02-DECISIONS/0005-the-node-host.md).
|
||||
**JSON**, because the host has no dependencies to spend and the standard library carries no
|
||||
YAML. **An ordered list of typed resources**, each with a stable identity — the order is stated
|
||||
rather than derived, because deriving it would be the host deciding the thing most likely to
|
||||
differ from what the control plane intended.
|
||||
differ from what the controller intended.
|
||||
|
||||
**Unknown is refused, never skipped.** An unknown version, type or field refuses the whole
|
||||
declaration. A host that skipped what it did not understand would apply most of it and report
|
||||
@@ -173,16 +173,16 @@ without one, applying the bundle it carries, has nothing to check against.
|
||||
Staged so each stage is verifiable in the lab before the next exists.
|
||||
|
||||
**1 — profile and inventory.** The host runs on a machine, detects what it can do, and reports
|
||||
what it is. No control plane, no declarations, no network. Verifiable immediately: the lab's
|
||||
what it is. No controller, no declarations, no network. Verifiable immediately: the lab's
|
||||
`place:` gains its first implementation, and a raised scenario finally contains something.
|
||||
|
||||
**2 — apply, from the bundle.** The host applies `substrate.lock` with no mesh present. This is
|
||||
**2 — apply, from the bundle.** The host applies `foundation.lock` with no mesh present. This is
|
||||
the first node's path, and it is the claim the skeleton's Move 1 rests on and has never proved:
|
||||
that one host can raise the substrate alone.
|
||||
that one host can raise the foundation alone.
|
||||
|
||||
Raising the substrate uses **four** shapes — `package`, `container`, `service`, `action` —
|
||||
Raising the foundation uses **four** shapes — `package`, `container`, `service`, `action` —
|
||||
counted from the bundle that exists rather than reasoned about. `file` and `directory` are listed
|
||||
below because they are the cheapest to be sure of and a substrate that needed them would find them
|
||||
below because they are the cheapest to be sure of and a foundation that needed them would find them
|
||||
ready; the current bundle simply does not. **All of them are built:**
|
||||
|
||||
| | | |
|
||||
@@ -225,7 +225,7 @@ container runtime. All three were instead verified against a real machine — a
|
||||
labelled, replaced when its declaration changed, exec'd into and removed; an action that exits
|
||||
zero and satisfies nothing failing the apply. That is lab-installation work
|
||||
([`04-lab-installation.md`](04-lab-installation.md)) rather than a constraint on the design, but
|
||||
until it is done the substrate bootstrap has no end-to-end test.
|
||||
until it is done the foundation bootstrap has no end-to-end test.
|
||||
|
||||
**3 — link and store.** The node connects, receives declarations, and holds what it applied.
|
||||
|
||||
@@ -313,7 +313,7 @@ reported to be distinguishable from one that reported an empty list.*
|
||||
|
||||
## Open
|
||||
|
||||
- **Whether one host can raise the substrate alone.** Move 1 assumes it. Stage 2 tests it, and
|
||||
- **Whether one host can raise the foundation alone.** Move 1 assumes it. Stage 2 tests it, and
|
||||
if it is false the tier boundary moves.
|
||||
- **What the host carries versus what it finds.** It manages `wg`, `nft`, `pacman`, `docker`;
|
||||
it does not contain them, and how it obtains one it lacks is undecided —
|
||||
@@ -329,15 +329,15 @@ reported to be distinguishable from one that reported an empty list.*
|
||||
|
||||
## What was added to the vocabulary, and why each cost was worth paying
|
||||
|
||||
*Written 2026-08-30. Every addition widens what a compromised control plane can express, so the
|
||||
*Written 2026-08-30. Every addition widens what a compromised controller can express, so the
|
||||
count is asserted by a test and a change to it is a decision rather than a convenience.*
|
||||
|
||||
Four shapes raise the substrate. Five more exist because most of what a person installs is not a
|
||||
Four shapes raise the foundation. Five more exist because most of what a person installs is not a
|
||||
service:
|
||||
|
||||
| | why |
|
||||
|---|---|
|
||||
| **file**, **directory** | the substrate needs neither, and almost everything else does |
|
||||
| **file**, **directory** | the foundation needs neither, and almost everything else does |
|
||||
| **user** | a shell, a terminal, a chat client, a desktop are a package plus configuration **in somebody's home**. A mesh with no user owns `/etc` and nothing anybody looks at |
|
||||
| **archive** | a theme is hundreds of files. Inlining them makes every declaration enormous and rewrites all of them when one changes |
|
||||
| **network** | a module of several containers has to let them reach each other by name, and doing it with an action would create something nothing could ever remove ([ADR 0029](../../02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)) |
|
||||
|
||||
+23
-23
@@ -12,7 +12,7 @@ decisions:
|
||||
- 02-DECISIONS/0019-how-this-repository-works.md
|
||||
---
|
||||
|
||||
# The control plane
|
||||
# The controller
|
||||
|
||||
Tier 2. The term appears seventy-nine times across this repository and was defined nowhere,
|
||||
which is `how-we-build` §5 failing on this repository's own vocabulary.
|
||||
@@ -22,7 +22,7 @@ This document defines it. It does **not** design the contexts inside it; those a
|
||||
|
||||
## The definition
|
||||
|
||||
> **The control plane is everything that needs to know about more than one node.**
|
||||
> **The controller is everything that needs to know about more than one node.**
|
||||
|
||||
That is the whole test, and it is not arbitrary — it follows from
|
||||
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md). The host applies and
|
||||
@@ -32,11 +32,11 @@ exactly there:
|
||||
| Question | Whose |
|
||||
|---|---|
|
||||
| write this file, with this content, with this mode | the **host** — one machine |
|
||||
| which nodes should run the store | the **control plane** — needs every node |
|
||||
| which nodes should run the store | the **controller** — needs every node |
|
||||
| is this unit running | the **host** — one machine |
|
||||
| which peers belong in this node's overlay | the **control plane** — needs every node |
|
||||
| what does this machine have installed | the **host** reports; the control plane **records** |
|
||||
| has this node been unreachable for a week | the **control plane** — nobody else is watching |
|
||||
| which peers belong in this node's overlay | the **controller** — needs every node |
|
||||
| what does this machine have installed | the **host** reports; the controller **records** |
|
||||
| has this node been unreachable for a week | the **controller** — nobody else is watching |
|
||||
|
||||
A useful consequence: **anything a single machine could answer alone is not the control
|
||||
plane's.** If it needs no second node, putting it here is a mistake, and the tier rule will not
|
||||
@@ -67,7 +67,7 @@ something infrastructure*. `ai` is folded into `config`: a provider licence is a
|
||||
**`record` is an open question rather than an eighth entry.** Contexts integrate through it
|
||||
([ADR 0008](../../02-DECISIONS/0008-a-context-owns-its-store.md)), which makes it load-bearing,
|
||||
and [research 006](../../01-RESEARCH/006-mesh-from-scratch/skeleton.md) leaves *where it lives*
|
||||
unresolved — putting it in the substrate risks recreating the circularity the tier design just
|
||||
unresolved — putting it in the foundation risks recreating the circularity the tier design just
|
||||
removed. Listing it here would settle by naming what has not been settled by arguing.
|
||||
|
||||
**One of the seven is built.** `inventory` owns a database of that name and holds the node records;
|
||||
@@ -89,7 +89,7 @@ in front of them.
|
||||
The question this answers: **can a node write to the registry database?** No — and not "only
|
||||
through one node", which is the weaker arrangement it might be mistaken for.
|
||||
|
||||
> **No node holds a credential to any control-plane store, for writing or for reading.**
|
||||
> **No node holds a credential to any controller store, for writing or for reading.**
|
||||
|
||||
That is not a new rule here; it is four already taken, and it is worth seeing them together
|
||||
because each one alone reads like a detail:
|
||||
@@ -118,11 +118,11 @@ Reads work the same way in reverse — a node is *told*, in declarations. It nev
|
||||
|
||||
### Who actually consumes, and who writes
|
||||
|
||||
**The control plane is the consumer. There is one of it, and the context that owns the data does
|
||||
**The controller is the consumer. There is one of it, and the context that owns the data does
|
||||
the write.**
|
||||
|
||||
```
|
||||
node ──► broker ──► the control plane, consuming
|
||||
node ──► broker ──► the controller, consuming
|
||||
├─ a node reported what it applied ─► inventory writes the registry
|
||||
├─ a node reported health ─► observability writes its own store
|
||||
└─ a grant was requested ─► provisioning writes its own store
|
||||
@@ -139,9 +139,9 @@ the store it exclusively owns
|
||||
receiving half of what it expects* — which has happened, between a module's daemon and its
|
||||
capability server. With one consumer that class of fault cannot arise.
|
||||
|
||||
**And the broker is the buffer while the control plane is down.** Nodes go on publishing;
|
||||
messages queue; the control plane drains them when it returns. That is what makes
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)'s single control plane
|
||||
**And the broker is the buffer while the controller is down.** Nodes go on publishing;
|
||||
messages queue; the controller drains them when it returns. That is what makes
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)'s single controller
|
||||
tolerable — an outage delays the mesh's *knowledge* rather than losing it.
|
||||
|
||||
**With one consequence that must be bounded before it is discovered:** a queue with no limit
|
||||
@@ -177,7 +177,7 @@ volume genuinely argues against a relational store.
|
||||
node except through the host.
|
||||
- **Not a surface.** Tier 3 is how people and agents reach it. It has one interface; the
|
||||
surfaces are what speak to that interface.
|
||||
- **Not the substrate.** It *runs on* tier 1 — PostgreSQL, LavinMQ, an OCI registry
|
||||
- **Not the foundation.** It *runs on* tier 1 — PostgreSQL, LavinMQ, an OCI registry
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) — and cannot start without
|
||||
them, which is what makes them a lower tier.
|
||||
- **Not privileged on a node.** It has no more access to a machine than the declaration
|
||||
@@ -185,19 +185,19 @@ volume genuinely argues against a relational store.
|
||||
|
||||
## It is also a consumer
|
||||
|
||||
The property that makes tier 2 unlike the others: **the control plane has requirements of its
|
||||
The property that makes tier 2 unlike the others: **the controller has requirements of its
|
||||
own.** It needs a PostgreSQL database, an AMQP virtual host, and a bucket — the same things any
|
||||
module needs, granted the same way.
|
||||
|
||||
That is the circularity the tiers exist to resolve rather than hide: the control plane cannot
|
||||
That is the circularity the tiers exist to resolve rather than hide: the controller cannot
|
||||
provision its own database, because it is not running yet. So its **store** is raised from the
|
||||
bundle the host carries, before there is a control plane to ask
|
||||
bundle the host carries, before there is a controller to ask
|
||||
([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md),
|
||||
[research 011](../../01-RESEARCH/011-the-module-graph/worked-provider.md)).
|
||||
|
||||
Its virtual host and its bucket are **not** in the bundle — by the time they are wanted there is
|
||||
a control plane to grant them. Whether the bus must come first is
|
||||
[open](07-the-substrate.md#open), and it turns on whether these contexts talk to each other over
|
||||
a controller to grant them. Whether the bus must come first is
|
||||
[open](07-the-foundation.md#open), and it turns on whether these contexts talk to each other over
|
||||
it.
|
||||
|
||||
## Where it runs
|
||||
@@ -209,10 +209,10 @@ hosts, assigned to nodes by the same mechanism as everything else.
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)). The node is assigned,
|
||||
never elected — no promotion, no quorum, no split brain.
|
||||
|
||||
That is sound rather than merely cheap, because the design already tolerates the control plane
|
||||
That is sound rather than merely cheap, because the design already tolerates the controller
|
||||
being absent by construction: a node reconciles from **its own** store
|
||||
([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)) and
|
||||
never needed to ask anybody to hold the state it was last given. So the control plane being down
|
||||
never needed to ask anybody to hold the state it was last given. So the controller being down
|
||||
is not a new failure mode — it is
|
||||
[ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)'s ordinary disconnected
|
||||
situation, happening to every node at once. **What is lost is change, not operation.**
|
||||
@@ -226,13 +226,13 @@ every public name.
|
||||
- ~~**The contexts themselves.**~~ **Decided** — seven, by
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md).
|
||||
What remains open is narrower and named there: **where the record lives**, which research 006
|
||||
leaves unresolved because the substrate is the one place it must not go.
|
||||
leaves unresolved because the foundation is the one place it must not go.
|
||||
- **How far it may be split.** One deployable today. Splitting a context out costs the single
|
||||
interface a surface depends on
|
||||
([research 011](../../01-RESEARCH/011-the-module-graph/00-overview.md)).
|
||||
- ~~**How many run, and what a node does without one.**~~ **Resolved** by
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md). What remains is
|
||||
measurement: nothing reports how long the control plane has been unreachable, or how close a
|
||||
measurement: nothing reports how long the controller has been unreachable, or how close a
|
||||
certificate is to expiry — both needed for restore-not-failover to be a plan rather than a
|
||||
hope.
|
||||
- **What the interface is.** One interface is stated; its shape, and whether it is request,
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code:
|
||||
- mesh-host examples/substrate-first-node.lock
|
||||
- mesh-host examples/foundation-first-node.lock
|
||||
- mesh-host internal/apply
|
||||
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
|
||||
updated: 2026-08-31
|
||||
@@ -15,16 +15,16 @@ decisions:
|
||||
- 02-DECISIONS/0019-how-this-repository-works.md
|
||||
---
|
||||
|
||||
# The substrate
|
||||
# The foundation
|
||||
|
||||
Tier 1. Defined the same way [the control plane](06-the-control-plane.md) is, because the same
|
||||
Tier 1. Defined the same way [the controller](06-the-controller.md) is, because the same
|
||||
gap applied: the word was load-bearing and unpinned.
|
||||
|
||||
## The definition
|
||||
|
||||
> **The substrate is what the control plane consumes and cannot grant itself.**
|
||||
> **The foundation is what the controller consumes and cannot grant itself.**
|
||||
|
||||
Every module that needs a database asks the control plane's provisioning for one. The control
|
||||
Every module that needs a database asks the controller's provisioning for one. The control
|
||||
plane needs a database too — and it cannot ask itself, because it is not running yet. That
|
||||
circularity is not an awkwardness to work around; it *is* the definition. Anything on the wrong
|
||||
side of it must be raised some other way, and the other way is the bundle the host carries
|
||||
@@ -32,19 +32,19 @@ side of it must be raised some other way, and the other way is the bundle the ho
|
||||
|
||||
The test, applied:
|
||||
|
||||
| | control plane needs it | can it grant itself one? | |
|
||||
| | controller needs it | can it grant itself one? | |
|
||||
|---|---|---|---|
|
||||
| a relational store — **PostgreSQL** | its own state lives there | no — provisioning needs the store | **substrate** |
|
||||
| a message bus — **LavinMQ** | it reaches nodes over it ([ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md)) | no — it cannot grant itself a virtual host | **substrate** |
|
||||
| ~~an object store~~ | ~~artifacts and blobs it delivers~~ | — | **not substrate** — [ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md) |
|
||||
| ~~an image registry~~ | ~~images it delivers to nodes~~ | — | **not substrate** — needed to operate, not to start ([ADR 0033](../../02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md)) |
|
||||
| ~~an identity provider~~ | ~~only if it delegates authentication~~ | — | **not substrate** — it delegates to nothing ([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)) |
|
||||
| ingress — **Traefik** | not to start; only to be reached by name | — it grants itself one afterwards | **not substrate** ([ADR 0007](../../02-DECISIONS/0007-connectivity.md)) |
|
||||
| anything else the mesh hosts | no | — | not substrate |
|
||||
| a relational store — **PostgreSQL** | its own state lives there | no — provisioning needs the store | **foundation** |
|
||||
| a message bus — **LavinMQ** | it reaches nodes over it ([ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md)) | no — it cannot grant itself a virtual host | **foundation** |
|
||||
| ~~an object store~~ | ~~artifacts and blobs it delivers~~ | — | **not foundation** — [ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md) |
|
||||
| ~~an image registry~~ | ~~images it delivers to nodes~~ | — | **not foundation** — needed to operate, not to start ([ADR 0033](../../02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md)) |
|
||||
| ~~an identity provider~~ | ~~only if it delegates authentication~~ | — | **not foundation** — it delegates to nothing ([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)) |
|
||||
| ingress — **Traefik** | not to start; only to be reached by name | — it grants itself one afterwards | **not foundation** ([ADR 0007](../../02-DECISIONS/0007-connectivity.md)) |
|
||||
| anything else the mesh hosts | no | — | not foundation |
|
||||
|
||||
*The object-store row was wrong, and how it was wrong is worth keeping.* It answered *can it
|
||||
grant itself one* — no, it cannot grant itself a bucket — while assuming the first column. **The
|
||||
control plane does not need an object store**: it has no S3 client and never has, and artifacts
|
||||
controller does not need an object store**: it has no S3 client and never has, and artifacts
|
||||
reach nodes as content-addressed blobs in the registry. The row was inherited from the system being
|
||||
replaced, where an object store distributed module tarballs, and was never re-tested against the
|
||||
definition above it. *Both columns must be answered, and the second is true of almost any service.*
|
||||
@@ -60,76 +60,76 @@ does not record that the choice was ever made.
|
||||
|
||||
The dependency is on the **protocol**, not the product: AMQP for the bus, S3 for the object
|
||||
store, the OCI protocol for the registry. That is what keeps the naming safe rather than a
|
||||
commitment that cannot be revisited — replacing one is a substrate migration, not a redesign.
|
||||
commitment that cannot be revisited — replacing one is a foundation migration, not a redesign.
|
||||
The store is the exception, and the exception matters: the provisioning model uses databases,
|
||||
roles and schemas as PostgreSQL means them, so it is the one member that is not a swap.
|
||||
|
||||
## What that resolves
|
||||
|
||||
**Four or five?** [Research 006](../../01-RESEARCH/006-mesh-from-scratch/00-overview.md) asks
|
||||
whether the identity provider is a substrate service, and the test answers it *conditionally* —
|
||||
whether the identity provider is a foundation service, and the test answers it *conditionally* —
|
||||
which is the honest answer rather than a number.
|
||||
|
||||
- If the control plane **delegates** authentication, it cannot serve anybody before the provider
|
||||
exists, and it cannot grant itself a client. **Substrate.**
|
||||
- If the controller **delegates** authentication, it cannot serve anybody before the provider
|
||||
exists, and it cannot grant itself a client. **Foundation.**
|
||||
- If it **authenticates natively**, the provider is an ordinary hosted service like any other.
|
||||
**Not substrate.**
|
||||
**Not foundation.**
|
||||
|
||||
So the count follows from a design decision that has not been taken, and the record should say
|
||||
that rather than assert four.
|
||||
|
||||
**Why not "important infrastructure".** An identity provider, a mail server and an analytics
|
||||
service are all infrastructure by any ordinary reading, and none of them are substrate — the
|
||||
control plane starts and runs without them. *Important* is not the test; *the control plane
|
||||
service are all infrastructure by any ordinary reading, and none of them are foundation — the
|
||||
controller starts and runs without them. *Important* is not the test; *the controller
|
||||
cannot obtain it* is.
|
||||
|
||||
## What the substrate is not
|
||||
## What the foundation is not
|
||||
|
||||
- **Not tier 0.** The host raises the substrate; it is not part of it. The host carries the
|
||||
declaration that brings the substrate up, and depends on nothing.
|
||||
- **Not the control plane.** These are services with no knowledge of the mesh. A store does not
|
||||
- **Not tier 0.** The host raises the foundation; it is not part of it. The host carries the
|
||||
declaration that brings the foundation up, and depends on nothing.
|
||||
- **Not the controller.** These are services with no knowledge of the mesh. A store does not
|
||||
know what a node is.
|
||||
- **Not a place for logic.** The skeleton is explicit: tier 1 is *declarations only, no logic of
|
||||
its own.* A substrate service is an upstream image, pinned, with configuration.
|
||||
- **Not privileged.** The substrate is provisioned *from* by the control plane and grants
|
||||
its own.* A foundation service is an upstream image, pinned, with configuration.
|
||||
- **Not privileged.** The foundation is provisioned *from* by the controller and grants
|
||||
nothing on its own initiative.
|
||||
- **Not the mesh's supply of anything**
|
||||
([ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md)).
|
||||
A substrate service and a module of the same product are **different instances**. The mesh's own
|
||||
A foundation service and a module of the same product are **different instances**. The mesh's own
|
||||
PostgreSQL and a PostgreSQL a workload was given are two servers, and a node hosting both runs
|
||||
two containers — expected, not duplication to be tidied away.
|
||||
|
||||
The substrate is raised from the bundle before any mesh exists, so **it is not in the module
|
||||
The foundation is raised from the bundle before any mesh exists, so **it is not in the module
|
||||
graph**: a workload depending on it would depend on something the graph cannot see, cannot rotate
|
||||
a credential for, and cannot move. It would also put workload data in the store the control plane
|
||||
a credential for, and cannot move. It would also put workload data in the store the controller
|
||||
keeps its own state in, where a workload that fills a disk takes down the one thing needed to fix
|
||||
it.
|
||||
|
||||
## The pinned bundle
|
||||
|
||||
`substrate.lock` holds **what must exist before the control plane runs** — which is a smaller
|
||||
set than the substrate, and the difference is easy to miss. It is the only place in the mesh
|
||||
`foundation.lock` holds **what must exist before the controller runs** — which is a smaller
|
||||
set than the foundation, and the difference is easy to miss. It is the only place in the mesh
|
||||
where versions are pinned by hand rather than resolved.
|
||||
|
||||
Being substrate and being in the bundle are two different questions:
|
||||
Being foundation and being in the bundle are two different questions:
|
||||
|
||||
| | is it substrate? | must it precede the control plane? |
|
||||
| | is it foundation? | must it precede the controller? |
|
||||
|---|---|---|
|
||||
| PostgreSQL | yes — the control plane's own state lives in it | **yes** — there is nowhere to put that state otherwise |
|
||||
| LavinMQ | yes — it cannot grant itself a virtual host | **yes** — the control plane reaches a node only over the link, and the link is the broker ([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
|
||||
| PostgreSQL | yes — the controller's own state lives in it | **yes** — there is nowhere to put that state otherwise |
|
||||
| LavinMQ | yes — it cannot grant itself a virtual host | **yes** — the controller reaches a node only over the link, and the link is the broker ([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
|
||||
| the OCI registry | yes — it cannot grant itself a repository | no — the first node fetches upstream ([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
|
||||
|
||||
The registry is **substrate by role and ordinary by delivery**: by the time it is wanted there is
|
||||
a control plane, and it provisions it the way it provisions anything. That keeps the bundle small
|
||||
The registry is **foundation by role and ordinary by delivery**: by the time it is wanted there is
|
||||
a controller, and it provisions it the way it provisions anything. That keeps the bundle small
|
||||
enough for the review [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) requires — one
|
||||
substrate image until
|
||||
foundation image until
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) established that the
|
||||
broker has to precede the control plane, and two since.
|
||||
broker has to precede the controller, and two since.
|
||||
|
||||
*Corrected 2026-08-31, from counting what the bundle holds rather than reasoning about it.* **It
|
||||
carries three images, not two** — PostgreSQL, LavinMQ, and the control plane itself, which the
|
||||
sentence above had overlooked by counting only substrate services. The control plane is what the
|
||||
substrate exists to start, and it is in the bundle for the same reason they are: there is nothing
|
||||
carries three images, not two** — PostgreSQL, LavinMQ, and the controller itself, which the
|
||||
sentence above had overlooked by counting only foundation services. The controller is what the
|
||||
foundation exists to start, and it is in the bundle for the same reason they are: there is nothing
|
||||
to fetch it with yet. It also carries seven actions, a package and a service.
|
||||
|
||||
**Why pinned:** the bundle is applied when no mesh exists, so nothing can resolve a version, ask
|
||||
@@ -154,13 +154,13 @@ The order, from [research 011](../../01-RESEARCH/011-the-module-graph/worked-pro
|
||||
4 LavinMQ runs pulled by digest, from the bundle
|
||||
5 a virtual host, a credential, and actions, run locally
|
||||
a self-signed certificate
|
||||
6 the control plane starts and only now is there a mesh
|
||||
6 the controller starts and only now is there a mesh
|
||||
7 the registry, and everything else the ordinary path
|
||||
are provisioned
|
||||
```
|
||||
|
||||
**Steps 4 and 5 are why the bundle is not one image**
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)). The control plane cannot
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)). The controller cannot
|
||||
provision the broker, because provisioning means telling a host, and telling a host happens over
|
||||
the broker. The first node does not escape this by being local: it enrols the ordinary way, by
|
||||
dialling the broker at the address in its token.
|
||||
@@ -172,15 +172,15 @@ database* names a thing that will not exist
|
||||
database is a boundary a cross-context join cannot casually cross where a separate schema is not.
|
||||
|
||||
Only PostgreSQL is raised from the bundle, for the reason in *The pinned bundle* above — the
|
||||
rest of the substrate is wanted only once there is a control plane to provision it.
|
||||
rest of the foundation is wanted only once there is a controller to provision it.
|
||||
|
||||
**Step 0 is easy to leave out and it is where several things meet.** A substrate service is a
|
||||
**Step 0 is easy to leave out and it is where several things meet.** A foundation service is a
|
||||
container, so a container runtime must be working before anything else happens — and a runtime
|
||||
is a *package*, not a container.
|
||||
|
||||
**Which runtime is detected, not chosen**
|
||||
([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)): a machine that
|
||||
already has one keeps it. On a machine with none, the control plane names the package, because
|
||||
already has one keeps it. On a machine with none, the controller names the package, because
|
||||
what it is called differs per system. It is:
|
||||
|
||||
- what the host's capability detection already reports, and the first use of that report by
|
||||
@@ -191,7 +191,7 @@ what it is called differs per system. It is:
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md).
|
||||
|
||||
So the bootstrap uses four shapes: **package**, **container**, **service** and **action** —
|
||||
*counted from `substrate-first-node.lock`, which is the only bundle there is*. It had said six,
|
||||
*counted from `foundation-first-node.lock`, which is the only bundle there is*. It had said six,
|
||||
adding `file` and `directory`, which this bootstrap never asks for.
|
||||
|
||||
All four are built, as are the host's other five
|
||||
@@ -202,7 +202,7 @@ on the host any longer — which is the claim that mattered, and it was true eit
|
||||
the bootstrap rather than a service consumers use later. They are **actions** the bundle
|
||||
declares and the host runs
|
||||
([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)) — so the
|
||||
host's vocabulary grows by one shape rather than by one resource type per substrate service.
|
||||
host's vocabulary grows by one shape rather than by one resource type per foundation service.
|
||||
|
||||
## Open
|
||||
|
||||
@@ -210,12 +210,12 @@ host's vocabulary grows by one shape rather than by one resource type per substr
|
||||
[ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md): the control
|
||||
plane delegates authentication to nothing, so identity is an ordinary module. With the object
|
||||
store gone ([ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md))
|
||||
the substrate is three, and no member is conditional.
|
||||
- ~~**Whether the bus must precede the control plane.**~~ **Resolved** by
|
||||
the foundation is three, and no member is conditional.
|
||||
- ~~**Whether the bus must precede the controller.**~~ **Resolved** by
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) — it must, and the question as
|
||||
posed here could not have answered it. This asked whether the control plane's contexts talk to
|
||||
posed here could not have answered it. This asked whether the controller's contexts talk to
|
||||
each other over the bus; they do not, being one process, which under this framing would have
|
||||
kept LavinMQ out of the bundle. What decides it is how the control plane reaches a *node*, which
|
||||
kept LavinMQ out of the bundle. What decides it is how the controller reaches a *node*, which
|
||||
is only ever over the link.
|
||||
- **What issues the broker's certificate at bootstrap.** New, and created by the row above. A
|
||||
token pins the fingerprint a host must expect before it sends anything
|
||||
@@ -223,7 +223,7 @@ host's vocabulary grows by one shape rather than by one resource type per substr
|
||||
moment when there is no mesh to issue one and no public name to obtain one for. Self-signed and
|
||||
pinned is the shape that fits; how it is later replaced by the certificates in
|
||||
[`08-connectivity.md`](08-connectivity.md) is not decided.
|
||||
- **How a context added later gets its database.** By then there is a control plane — but one
|
||||
- **How a context added later gets its database.** By then there is a controller — but one
|
||||
holding a credential that can create databases holds more than what it exclusively owns
|
||||
([ADR 0008](../../02-DECISIONS/0008-a-context-owns-its-store.md)).
|
||||
- ~~**Whether the host can do step 2.**~~ **Resolved** by
|
||||
@@ -234,8 +234,8 @@ host's vocabulary grows by one shape rather than by one resource type per substr
|
||||
the module that provides one.
|
||||
- **Whether one host can raise all three.** The claim under stage 2 of
|
||||
[the node host](05-the-node-host.md), never proved. If it is false, the tier boundary moves.
|
||||
- **How the substrate is updated once a mesh exists.** Pinned by hand at bootstrap; afterwards
|
||||
the control plane could deliver it like anything else, and nothing says whether it does.
|
||||
- **How the foundation is updated once a mesh exists.** Pinned by hand at bootstrap; afterwards
|
||||
the controller could deliver it like anything else, and nothing says whether it does.
|
||||
|
||||
## Raised, and observed
|
||||
|
||||
@@ -243,7 +243,7 @@ host's vocabulary grows by one shape rather than by one resource type per substr
|
||||
|
||||
**It works, and what that means precisely:** a machine with a container runtime and nothing else
|
||||
applied the bundle its host carries and ended with a store, a database per context, those
|
||||
contexts' schemas, a broker holding a certificate it generated itself, and the control plane
|
||||
contexts' schemas, a broker holding a certificate it generated itself, and the controller
|
||||
serving on top of them. Eleven resources, one command, no mesh to ask anything of.
|
||||
|
||||
**Then it joined itself.** The same machine took a token, checked the broker against the
|
||||
@@ -259,7 +259,7 @@ of a database and pushed to over the broker. What arrived and what did not is th
|
||||
|---|---|
|
||||
| the password, in plain text | **on the machine only**, one file, mode 0600 |
|
||||
| in the declaration that crossed the broker | absent |
|
||||
| in the control plane's database | absent |
|
||||
| in the controller's database | absent |
|
||||
| in what the node reported back | absent |
|
||||
|
||||
**One fault, and it was in the joining.** The token did not say what the mesh calls the machine,
|
||||
@@ -21,25 +21,25 @@ decisions:
|
||||
|
||||
# Connectivity
|
||||
|
||||
One of [the control plane's](06-the-control-plane.md) ten contexts, and the one with the most
|
||||
One of [the controller's](06-the-controller.md) ten contexts, and the one with the most
|
||||
moving parts: **overlay, resolution, exposure, filtering, certificates.**
|
||||
|
||||
It is written as a whole because the five are one design. They share inputs, they must agree, and
|
||||
every one of them today is computed in a different place by a different module from a different
|
||||
copy of the same facts.
|
||||
|
||||
## Why it is control-plane work
|
||||
## Why it is controller work
|
||||
|
||||
Apply [the test](06-the-control-plane.md) — *everything that needs to know about more than one
|
||||
Apply [the test](06-the-controller.md) — *everything that needs to know about more than one
|
||||
node* — to each responsibility:
|
||||
|
||||
| | needs to know | whose |
|
||||
|---|---|---|
|
||||
| **overlay** — who peers with whom, at what address | **every node**, and which of them can be dialled | control plane |
|
||||
| **resolution** — which name is which node | **every node** | control plane |
|
||||
| **exposure** — which public name reaches which container | **which node is publicly reachable** ([ADR 0007](../../02-DECISIONS/0007-connectivity.md)) | control plane |
|
||||
| **filtering** — which port is open, to whom | what is assigned here, and the overlay's shape | control plane decides, host applies |
|
||||
| **certificates** — who may present which name | which name belongs to which node | control plane |
|
||||
| **overlay** — who peers with whom, at what address | **every node**, and which of them can be dialled | controller |
|
||||
| **resolution** — which name is which node | **every node** | controller |
|
||||
| **exposure** — which public name reaches which container | **which node is publicly reachable** ([ADR 0007](../../02-DECISIONS/0007-connectivity.md)) | controller |
|
||||
| **filtering** — which port is open, to whom | what is assigned here, and the overlay's shape | controller decides, host applies |
|
||||
| **certificates** — who may present which name | which name belongs to which node | controller |
|
||||
|
||||
**Not one of the five can be answered by a machine on its own.** That is the whole reason this is
|
||||
a context rather than a set of node-local modules — and it is exactly what the current
|
||||
@@ -57,7 +57,7 @@ exist; WireGuard, the resolver and the proxy are all *a container or a package,
|
||||
|
||||
It is also what removes the last two upward dependencies.
|
||||
[Research 006](../../01-RESEARCH/006-mesh-from-scratch/host-size.md) counted exactly two modules
|
||||
opening a direct connection to the control plane's database — `wireguard` and `traefik` — and
|
||||
opening a direct connection to the controller's database — `wireguard` and `traefik` — and
|
||||
they are the reason every node permanently holds a credential to it
|
||||
([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)). Both are connectivity
|
||||
modules. **Closing this context closes that set.**
|
||||
@@ -74,7 +74,7 @@ wanted the exception.
|
||||
|
||||
**What made it look unavoidable:** a peer list cannot be written in a manifest. It is derived from
|
||||
every other machine, so it differs on each one and changes when any of them changes. So the
|
||||
manifest says its resources are **computed** — it names something in the control plane that works
|
||||
manifest says its resources are **computed** — it names something in the controller that works
|
||||
them out per node — and it is a module in every other respect: assigned, resolved, configured by
|
||||
settings, and absent from a machine nobody gave it to.
|
||||
|
||||
@@ -108,7 +108,7 @@ claim, and the collision is refused by name.
|
||||
**And the proxy's half, which was the other module reaching into the database.** A web application
|
||||
requiring a reverse proxy has to say *which name, which port*, and there was nowhere to put it —
|
||||
`requires` says a thing must exist and never said what to do with it. A module now contributes to
|
||||
a requirement, the control plane collects every contribution on a node, and the provider is given
|
||||
a requirement, the controller collects every contribution on a node, and the provider is given
|
||||
them as a file at a path it named. It reloads when that file changes, by the same `restart-on` the
|
||||
private network needed when a peer list changed under a running interface.
|
||||
|
||||
@@ -177,7 +177,7 @@ which of those it may dial, and which must dial it.
|
||||
**Keys.** Each node generates its own keypair. **The private key never leaves the machine**; the
|
||||
public key is published to the mesh. This is already true and it is already right — it is
|
||||
[ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)'s *a node holds its own
|
||||
identity* applied to the overlay, and it means the control plane computes a graph it cannot
|
||||
identity* applied to the overlay, and it means the controller computes a graph it cannot
|
||||
itself impersonate.
|
||||
|
||||
**Shape: a hub, with direct peering between co-located nodes.**
|
||||
@@ -217,7 +217,7 @@ files were right, the services were up, and every node reported success.
|
||||
document's own warning, arriving in its implementation: *a more specific route to a dead
|
||||
endpoint blackholes; it does not fall back to the general one.*
|
||||
- **The container runtime closes the door the overlay needs.** Docker sets the FORWARD policy to
|
||||
DROP, so a hub with `ip_forward` enabled still carries nothing between its spokes. The substrate
|
||||
DROP, so a hub with `ip_forward` enabled still carries nothing between its spokes. The foundation
|
||||
at tier 1 silently breaks the network at tier 2, and nothing in either tier's state says so. The
|
||||
hub inserts its own rule above those chains and removes it on the way down.
|
||||
|
||||
@@ -287,7 +287,7 @@ node. What routes it once it arrives is a proxy's, and stays separate.
|
||||
**The mesh writes the data and runs no daemon.** One wildcard per machine, from the same set that
|
||||
writes the hosts file. A resolver is third-party software and runs *on* the mesh rather than being
|
||||
*of* it: the mesh has no business shipping one, choosing which one, or knowing its configuration
|
||||
language. Swapping dnsmasq for unbound changes that module and nothing in the control plane.
|
||||
language. Swapping dnsmasq for unbound changes that module and nothing in the controller.
|
||||
|
||||
**Two roles, two claims, because they are different things.** systemd-resolved cannot answer a
|
||||
wildcard at all — it routes the mesh's suffix to something that can. Treating serving and asking
|
||||
@@ -377,7 +377,7 @@ vocabulary — the mirror of a database grant, where the consumer supplies a tar
|
||||
name rather than supplying nothing and receiving credentials.
|
||||
|
||||
**A workload on an unreachable node is proxied by a reachable one, across the overlay.** Which is
|
||||
the case is a mesh-level fact, which is the fourth reason exposure is control-plane work.
|
||||
the case is a mesh-level fact, which is the fourth reason exposure is controller work.
|
||||
|
||||
### What was built
|
||||
|
||||
@@ -482,7 +482,7 @@ something:
|
||||
|
||||
| | why not |
|
||||
|---|---|
|
||||
| decide what the machine **forwards** | the container runtime writes its own forwarding rules and a second policy is consulted alongside them, so a drop here stops every container on the node — the control plane included. Nothing in a manifest says what a machine routes, so there is nothing to derive it from either |
|
||||
| decide what the machine **forwards** | the container runtime writes its own forwarding rules and a second policy is consulted alongside them, so a drop here stops every container on the node — the controller included. Nothing in a manifest says what a machine routes, so there is nothing to derive it from either |
|
||||
| **flush the ruleset** when loading | that empties every table on the machine, the runtime's among them. Only the mesh's own table is replaced, and it is declared empty first so the replacement works on a machine loading one for the first time |
|
||||
| carry a **command to load itself** | the link may not carry an action ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)). A service is declared to reflect the file instead, so replacing it restarts what loads it — the shape that rule leaves, used here for the first time for its real purpose |
|
||||
|
||||
@@ -549,7 +549,7 @@ defaults to the public authority's *production* endpoint. Two consequences, and
|
||||
worse than the lab problem that found it — every certificate experiment on a real node consumes
|
||||
production issuance quota, and a retry loop can exhaust it for a week.
|
||||
|
||||
**The mesh CA is not a bootstrap concern.** A joining node verifies the control plane against the
|
||||
**The mesh CA is not a bootstrap concern.** A joining node verifies the controller against the
|
||||
fingerprint in its token ([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)),
|
||||
so nothing needs the CA before membership. It certifies internal names afterwards, and that is
|
||||
all it does.
|
||||
|
||||
@@ -142,12 +142,12 @@ nox-mesh-host enrol --token <one-time token>
|
||||
|
||||
The token carries **four** things and is carried by a person
|
||||
([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)): the broker's
|
||||
address, the fingerprint to expect, **the control plane's signing identity**, and the right to
|
||||
address, the fingerprint to expect, **the controller's signing identity**, and the right to
|
||||
join once.
|
||||
|
||||
**The fourth is the one this document listed three of.** A node connects to the broker and takes
|
||||
instruction from the control plane behind it, and those are two different identities. Pinning only
|
||||
the broker would make the control plane's authority *transitive* — a compromised broker could then
|
||||
instruction from the controller behind it, and those are two different identities. Pinning only
|
||||
the broker would make the controller's authority *transitive* — a compromised broker could then
|
||||
forge declarations, which, since the host applies whatever the link delivers, is the whole machine.
|
||||
So the transport is verified once at connect, and **each declaration is verified by its signature,
|
||||
every time**.
|
||||
@@ -158,10 +158,10 @@ What happens, in order:
|
||||
2. it checks the broker's certificate against the pinned fingerprint — *before* sending anything;
|
||||
3. it presents the one-time secret **and its own public key**, which the mesh records;
|
||||
4. it reports its `profile` and `inventory` upward;
|
||||
5. the control plane decides what this machine should be, and sends a declaration;
|
||||
5. the controller decides what this machine should be, and sends a declaration;
|
||||
6. the host applies it, reads back, and reports.
|
||||
|
||||
**Step 4 is the one that is easy to miss and is what makes step 5 possible.** The control plane
|
||||
**Step 4 is the one that is easy to miss and is what makes step 5 possible.** The controller
|
||||
cannot decide what a machine should run without knowing what it *can* run — a graphical session,
|
||||
a container runtime, an architecture. The profile is not a diagnostic; it is the input.
|
||||
|
||||
@@ -209,7 +209,7 @@ channel, not about network reachability.** What it forbids is a listening thing
|
||||
instructions and changes the machine. A node being reachable on the overlay — the whole purpose
|
||||
of the overlay — is untouched by it, and so is a person opening a shell on it.
|
||||
|
||||
The distinction is *who can tell this machine what to be*: only the control plane, only over the
|
||||
The distinction is *who can tell this machine what to be*: only the controller, only over the
|
||||
link the node opened, only in declarations of known shape.
|
||||
|
||||
---
|
||||
@@ -219,18 +219,18 @@ link the node opened, only in declarations of known shape.
|
||||
The same path, with the mesh built in the middle of it.
|
||||
|
||||
```
|
||||
# 1 — raise the substrate and the control plane from the carried bundle
|
||||
# 1 — raise the foundation and the controller from the carried bundle
|
||||
nox-mesh-host reconcile
|
||||
|
||||
# 2 — the control plane now exists, and issues the first token
|
||||
# 2 — the controller now exists, and issues the first token
|
||||
mesh-control token issue
|
||||
|
||||
# 3 — the machine joins the mesh it just raised
|
||||
nox-mesh-host enrol --token <token>
|
||||
```
|
||||
|
||||
Step 1 is the bootstrap from [`07-the-substrate.md`](07-the-substrate.md): a container runtime,
|
||||
then PostgreSQL, then the database, then the schema, then the control plane. It needs no identity
|
||||
Step 1 is the bootstrap from [`07-the-foundation.md`](07-the-foundation.md): a container runtime,
|
||||
then PostgreSQL, then the database, then the schema, then the controller. It needs no identity
|
||||
because nothing is being asked of anyone — the host is applying a declaration it already
|
||||
carries, to the machine it is already on.
|
||||
|
||||
@@ -238,7 +238,7 @@ carries, to the machine it is already on.
|
||||
the bootstrap script never had. Its specialness lasted two commands.
|
||||
|
||||
**And enrolment is exercised on node one.** The path every other node depends on is walked
|
||||
immediately, against a control plane on the same machine, rather than being written and first
|
||||
immediately, against a controller on the same machine, rather than being written and first
|
||||
used months later on node two.
|
||||
|
||||
---
|
||||
@@ -265,7 +265,7 @@ closes — an authoritative local store, reconcile on start, *last heard from* r
|
||||
alarm — is what an episodic host needs, at a shorter period.
|
||||
|
||||
**It cannot be the first node**, and that is not a limitation to work around. Every step of
|
||||
raising a substrate is a `package`, a `container` or an `action` against one, and a partial host
|
||||
raising a foundation is a `package`, a `container` or an `action` against one, and a partial host
|
||||
refuses the first two. So `mesh-host-android bundle` returns a file that says so rather than an
|
||||
empty placeholder waiting to be filled in.
|
||||
|
||||
@@ -350,7 +350,7 @@ rest. The rule that exists to stop the host lying about what it did also makes i
|
||||
|
||||
## Updating what the node holds
|
||||
|
||||
An ordinary declaration. Someone assigns a module; the control plane recomputes what that node
|
||||
An ordinary declaration. Someone assigns a module; the controller recomputes what that node
|
||||
should be and sends it; the host applies the difference and removes what is no longer declared.
|
||||
|
||||
**Removal is not symmetric, and the asymmetry is the design:**
|
||||
@@ -410,7 +410,7 @@ one binary that has always been the same binary.
|
||||
|
||||
Two cases, and they are genuinely different.
|
||||
|
||||
**Graceful.** The control plane sends a final declaration that names nothing. The host removes
|
||||
**Graceful.** The controller sends a final declaration that names nothing. The host removes
|
||||
what it owns by the table above, reports, and drops its identity. The machine keeps the host
|
||||
installed and is back to `hosted`. Nothing is left behind that anybody has to remember.
|
||||
|
||||
@@ -638,7 +638,7 @@ lets a node verify a mesh it has never spoken to
|
||||
([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)). A token emailed,
|
||||
committed, or dropped in shared storage has lost the only property that makes it worth carrying.
|
||||
|
||||
**On the first node it comes from the control plane that was raised two commands ago**, which is
|
||||
**On the first node it comes from the controller that was raised two commands ago**, which is
|
||||
the same command against a mesh that is one machine old.
|
||||
|
||||
---
|
||||
|
||||
@@ -78,7 +78,7 @@ whenever anybody writes something reusable, which is constantly.
|
||||
|
||||
## Delivery is a comparison, not a pipeline
|
||||
|
||||
The control plane holds two facts and builds the difference:
|
||||
The controller holds two facts and builds the difference:
|
||||
|
||||
```
|
||||
what source exists ─┐
|
||||
@@ -94,7 +94,7 @@ That is the same shape the host uses on a machine, one layer up:
|
||||
|
||||
| | reconciles | against |
|
||||
|---|---|---|
|
||||
| the control plane | artifacts | source |
|
||||
| the controller | artifacts | source |
|
||||
| the host | machine state | declarations |
|
||||
|
||||
**There is no pipeline as a state machine.** No stage list something can be omitted from, and no
|
||||
@@ -178,9 +178,9 @@ Not aspirations — things without which the above does not work:
|
||||
same digest, a cascade would stop at the first module whose output did not move. Without them,
|
||||
one core-library commit redeploys the fleet with no behavioural change.
|
||||
- **How a module publishes its own types**, which differs per language.
|
||||
- **How the control plane upgrades itself.** It declares its own new version and the host applies
|
||||
- **How the controller upgrades itself.** It declares its own new version and the host applies
|
||||
it — but if the new one is broken, the thing that would fix it is the thing that is broken. The
|
||||
host has a launcher for exactly this; the control plane has nothing.
|
||||
host has a launcher for exactly this; the controller has nothing.
|
||||
|
||||
## What "behind" means, and what it used to mean
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ it is enough to freeze it.** A board that reads the provisioning tables directly
|
||||
breaks when provisioning changes its tables, and the change then gets weighed against the board.
|
||||
|
||||
**So a board reads through interfaces and holds nothing.** Everything on the mesh page above is
|
||||
already answerable by asking the control plane — what nodes exist, what each resolves to, what it
|
||||
already answerable by asking the controller — what nodes exist, what each resolves to, what it
|
||||
takes from elsewhere, which module came from which commit. A board that asks those questions is a
|
||||
client. A board that queries `inventory` is a second control plane with a worse contract.
|
||||
client. A board that queries `inventory` is a second controller with a worse contract.
|
||||
|
||||
**It stores nothing of its own.** No cache that can disagree, no table of "what the mesh looked
|
||||
like last time". If a question is slow to answer, the answer belongs in the context that owns it,
|
||||
@@ -63,7 +63,7 @@ calls the security boundary, and a login there would guard a room whose door is
|
||||
building. This one faces everybody.
|
||||
|
||||
**Which makes the identity provider the mesh's outermost gate.** The board is a presentation layer
|
||||
over the control plane and the control plane's networked surfaces can change the mesh
|
||||
over the controller and the controller's networked surfaces can change the mesh
|
||||
([ADR 0035](../../02-DECISIONS/0035-one-implementation-several-surfaces.md)), so **whoever that
|
||||
provider admits can assign modules, from anywhere.** Said flatly because it is easy to arrive at
|
||||
one reasonable step at a time and then be surprised by.
|
||||
|
||||
@@ -85,10 +85,10 @@ the worst possible moment.
|
||||
|
||||
## The builder runs on a node
|
||||
|
||||
**Not in the control plane, and this is the same boundary as everywhere else.** Building needs a
|
||||
container runtime and a working tree; what the control plane may send a machine is bounded by the
|
||||
**Not in the controller, and this is the same boundary as everywhere else.** Building needs a
|
||||
container runtime and a working tree; what the controller may send a machine is bounded by the
|
||||
declaration language ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)), and *run this build*
|
||||
is not in it. The alternative — the control plane holding a container socket — would make it the
|
||||
is not in it. The alternative — the controller holding a container socket — would make it the
|
||||
one component that can do anything on any machine, which is the property the whole design is
|
||||
arranged to avoid.
|
||||
|
||||
@@ -96,7 +96,7 @@ So the builder is a program a machine runs, given work over the broker like anyt
|
||||
its own credential and nothing more.
|
||||
|
||||
**A build is work, not state**, and that is why it does not travel as a declaration. Everything
|
||||
else the control plane sends a node is *what you should be*, reconciled forever. A build happens
|
||||
else the controller sends a node is *what you should be*, reconciled forever. A build happens
|
||||
once and is finished; as a declaration it would either rebuild on every reconcile or carry "and I
|
||||
already did this" — state about an event rather than about a machine.
|
||||
|
||||
@@ -203,7 +203,7 @@ at something. A build that failed before it knew what it was building keeps the
|
||||
is what a person goes and looks at.
|
||||
|
||||
Recording is idempotent on the correlation, because a result arrives twice — once as the answer to
|
||||
whoever asked and once on the exchange, where the control plane is also listening. Two rows would
|
||||
whoever asked and once on the exchange, where the controller is also listening. Two rows would
|
||||
show one build as two, and which is real is not answerable afterwards.
|
||||
|
||||
That is what a builds view reads, and until it existed there was nothing to read: a result was
|
||||
@@ -381,13 +381,13 @@ have a route and one does not:
|
||||
|
||||
| What | Why it cannot come through the loop | How it arrives |
|
||||
|---|---|---|
|
||||
| The control plane | It is what installs modules. Nothing can install it before it runs. | **Carried inside the installer** and published once there is a registry ([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)) |
|
||||
| The controller | It is what installs modules. Nothing can install it before it runs. | **Carried inside the installer** and published once there is a registry ([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)) |
|
||||
| The registry | It *is* where artifacts are delivered from. A store cannot be delivered through itself. | **Pulled from the public internet** — its image is an ordinary public one, never built ([`04-ISSUES/029`](../../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)) |
|
||||
| The builder | It is what builds. Nothing builds it before it runs. | Built at genesis by the init builder ([ADR 0070](../../02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md)) |
|
||||
| The catalogue | It owns the module graph, and nothing can be resolved or installed without it. | Built at genesis by the init builder ([ADR 0070](../../02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md)) |
|
||||
|
||||
**How they arrive is settled and not yet built.** The installer carries an init builder, which
|
||||
clones the source and builds the control plane, the catalogue and the builder before a mesh exists
|
||||
clones the source and builds the controller, the catalogue and the builder before a mesh exists
|
||||
to install anything. Two things about that are open and named in
|
||||
[ADR 0070](../../02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md): where the init builder
|
||||
clones from, given the forge normally runs on the mesh it would be rebuilding, and what it
|
||||
@@ -400,7 +400,7 @@ three. This section previously said the list was closed at three, which was writ
|
||||
catalogue had an owner and is corrected here rather than left to be reasoned from.
|
||||
|
||||
**And the answer for all four is now one mechanism, not four special cases.** Genesis carries an
|
||||
*init builder* and builds the core modules on the machine — control plane, catalogue and builder —
|
||||
*init builder* and builds the core modules on the machine — controller, catalogue and builder —
|
||||
rather than carrying a finished image of any of them. So the question is no longer "how does this
|
||||
one get here first?" asked once per component; it is answered once, by the thing that is carried
|
||||
being a builder rather than a result.
|
||||
@@ -422,7 +422,7 @@ the mesh's registry assigned, exactly like everything the builder produces. A re
|
||||
from a running mesh which of its images were carried, and that is the point: carrying is how the
|
||||
first copy arrives, not what it permanently is.
|
||||
|
||||
*Checked by the thing already checked at genesis: after installing, the running control plane is
|
||||
*Checked by the thing already checked at genesis: after installing, the running controller is
|
||||
pinned to a digest the mesh's own registry assigned, and not to the id of the image the installer
|
||||
carried. The same check applies to the builder and to the registry, and it is the same check —
|
||||
an image id where a registry digest belongs means the pivot did not finish.*
|
||||
|
||||
@@ -70,7 +70,7 @@ The mesh generated the password, sealed it to the machine that must accept it, a
|
||||
plaintext** — so it cannot tell a database to start accepting it. Something on that machine reads
|
||||
what the host wrote and makes it true.
|
||||
|
||||
That something is part of the module, not part of the control plane. **The control plane decides
|
||||
That something is part of the module, not part of the controller. **The controller decides
|
||||
and never touches a machine; a provisioner runs on the machine and touches it.** Two files, because
|
||||
the mesh could not compose a document containing a value it does not have:
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ names neither the licence nor the mesh.
|
||||
|
||||
**A key is read from a file or standard input, never an argument.** A key on a command line is a
|
||||
key in shell history and in every process listing taken while it ran. It is never echoed back:
|
||||
what is stored is unreadable by whoever holds it, the control plane included, and printing it
|
||||
what is stored is unreadable by whoever holds it, the controller included, and printing it
|
||||
would put the one copy that matters on a terminal.
|
||||
|
||||
## Refusing is felt, and that is the design working
|
||||
@@ -83,7 +83,7 @@ per machine, which is a step toward it and is not it.
|
||||
|
||||
*2026-08-31: this gap now has named consumers rather than hypothetical ones.*
|
||||
[ADR 0026](../../02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md) puts two sessions on the
|
||||
control-plane node — the node's own and the mesh's — each bound in its own right. See
|
||||
controller node — the node's own and the mesh's — each bound in its own right. See
|
||||
[`15-the-agent-session.md`](15-the-agent-session.md).
|
||||
|
||||
**And for sessions the gap is already closed, which was not obvious.** A binding is per module per
|
||||
@@ -172,12 +172,12 @@ it; the metric is vendor-defined, so no false common unit is forced. Anthropic b
|
||||
In the lab, on real machines, in the order a person would meet it: a consumer is refused with both
|
||||
candidates named; put on one and still refused because no key exists; the key is given on standard
|
||||
input and not echoed; the public half arrives saying it came from a record rather than a machine;
|
||||
the key arrives readable only by that machine — and it is **nowhere in the control plane's own
|
||||
the key arrives readable only by that machine — and it is **nowhere in the controller's own
|
||||
database**, nor in anything that crossed the broker.
|
||||
|
||||
For the generalisation ([ADR 0050](../../02-DECISIONS/0050-model-access-is-vendor-agnostic.md)): a
|
||||
second vendor — `anthropic-api-key`, static-key — is bound to a consumer and exercises the whole path
|
||||
with the carve-out switched off, its key sealed per node and absent from the control plane's database.
|
||||
with the carve-out switched off, its key sealed per node and absent from the controller's database.
|
||||
For a `refreshable-grant` licence, the refresh token is asserted to exist (encrypted) **only on the
|
||||
manager node**, to be **absent from every holder's delivery**, and the delivered credential to be
|
||||
access-token-only; a `static-key` licence stores no refresh token anywhere. A scenario with two
|
||||
|
||||
@@ -22,7 +22,7 @@ so, and the differences are few enough to list here:
|
||||
| **context root** | the node's | the mesh's |
|
||||
| **engram** | that node's | the mesh's |
|
||||
| **licence** | bound in its own right | bound in its own right |
|
||||
| **runs on** | that node | the node holding the control plane |
|
||||
| **runs on** | that node | the node holding the controller |
|
||||
| **how many** | one per node | one |
|
||||
|
||||
Everything below applies to both unless it says otherwise.
|
||||
@@ -64,9 +64,9 @@ as it reports anything else.
|
||||
**A node's session runs on that node**, and cannot be moved. Moved, one machine is answering as
|
||||
another (ADR 0004).
|
||||
|
||||
**The mesh's session runs on the node holding the control plane.** The reasoning is in ADR 0026
|
||||
**The mesh's session runs on the node holding the controller.** The reasoning is in ADR 0026
|
||||
and is worth carrying here because it is easy to get backwards: this is not *the important agent
|
||||
goes on the important machine*. It is that the control-plane node is already the one place
|
||||
goes on the important machine*. It is that the controller node is already the one place
|
||||
excepted from *compromise of a node is compromise of that node*, and an agent able to reach
|
||||
everything, placed anywhere else, would create a second such place.
|
||||
|
||||
@@ -102,7 +102,7 @@ machine*.
|
||||
|
||||
That is not sufficient here, and the shortfall is concrete rather than theoretical:
|
||||
|
||||
- the control-plane node hosts **two** sessions, which must be able to hold **different**
|
||||
- the controller node hosts **two** sessions, which must be able to hold **different**
|
||||
licences — a per-machine binding cannot express it at all;
|
||||
- *this node's session uses the personal licence, the mesh's uses the company one* is the ordinary
|
||||
case, not an exotic one.
|
||||
@@ -125,7 +125,7 @@ session a different agent from another, and memory is part of what makes it *tha
|
||||
and it is not a view over theirs. What the mesh has been asked, and what it worked out, is held
|
||||
in the mesh's root — not in the root of the node that happens to host it.
|
||||
|
||||
**That distinction is the point of putting it there.** The control-plane node runs two sessions
|
||||
**That distinction is the point of putting it there.** The controller node runs two sessions
|
||||
on one machine. If memory belonged to the machine rather than to the root, they would share it,
|
||||
and the mesh's recollection of a fortnight of questions would be indistinguishable from that
|
||||
node's own — which is the collision ADR 0026 exists to avoid, arriving through the back door.
|
||||
@@ -188,7 +188,7 @@ here so the shape is not rediscovered.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Two sessions on one node, and no ambiguity.** The control-plane node hosts its own node session
|
||||
**Two sessions on one node, and no ambiguity.** The controller node hosts its own node session
|
||||
and the mesh session. ADR 0004's *one per node* forbids ambiguity about who answers when a **node**
|
||||
is addressed; these answer to different addresses.
|
||||
|
||||
@@ -207,7 +207,7 @@ and these run in the lab on real machines:
|
||||
| Check | Defends |
|
||||
|---|---|
|
||||
| a node is asked something and its session answers | ADR 0004 |
|
||||
| the mesh is asked something and the mesh session answers, on the control-plane node | ADR 0026 |
|
||||
| the mesh is asked something and the mesh session answers, on the controller node | ADR 0026 |
|
||||
| both sessions on that node answer, to their own addresses, without ambiguity | ADR 0026 |
|
||||
| a session switched off replies saying so, rather than timing out | ADR 0004 |
|
||||
| a session whose engram was changed reports having applied it, like any declared file | ADR 0005 |
|
||||
|
||||
@@ -163,14 +163,14 @@ same module. There is one derivation here, and there should stay one.
|
||||
sealing is worth its inconvenience.
|
||||
|
||||
**An image store is a module, and was written up here as something the mesh does.** It was
|
||||
considered for the substrate and removed, because the test is not *can it grant itself one* —
|
||||
nearly anything passes that — but whether the control plane needs it before it can give its first
|
||||
considered for the foundation and removed, because the test is not *can it grant itself one* —
|
||||
nearly anything passes that — but whether the controller needs it before it can give its first
|
||||
instruction. It does not. So a registry somebody runs for their own images is the same module as
|
||||
the one the mesh runs for its own: it offers a place to push, and claims that role once per
|
||||
machine.
|
||||
|
||||
**A rule was enforced only at the far end.** A module may not declare an action, and the host
|
||||
refused one correctly — but the control plane accepted it into the catalogue, resolved it and
|
||||
refused one correctly — but the controller accepted it into the catalogue, resolved it and
|
||||
pushed it, so the refusal arrived on a machine with nothing tying it back to the manifest. The
|
||||
rule held; it was just unusable, which is the same shape as the network shape that cost five
|
||||
failing tests before anyone read the host's log. It is now refused where it is written.
|
||||
|
||||
@@ -30,7 +30,7 @@ rules cannot all hold at once, and it is resolved by a pivot
|
||||
|
||||
**Joining** happens on every machine after the first, and it is ordinary. A mesh exists, so it can
|
||||
be asked for a token and told what the machine should be. Joining installs the host and nothing
|
||||
else: no temporary anything, no substrate raised by hand, no registry.
|
||||
else: no temporary anything, no foundation raised by hand, no registry.
|
||||
|
||||
Confusing the two is what produced a procedure that only ever worked in a fixture. A bed that
|
||||
raises four machines the same way has not tested genesis at all — it has tested joining, four
|
||||
@@ -38,13 +38,13 @@ times, with the first one hand-fed.
|
||||
|
||||
## What changed, and what did not
|
||||
|
||||
*2026-09-13.* The installer carries a builder now, and builds the control plane it raises. Three
|
||||
*2026-09-13.* The installer carries a builder now, and builds the controller it raises. Three
|
||||
records settle it: [ADR 0070](../../02-DECISIONS/0070-the-catalogue-owns-the-module-graph.md) that
|
||||
genesis builds rather than carries, [ADR 0071](../../02-DECISIONS/0071-where-genesis-gets-its-source.md)
|
||||
where it clones from, and [ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md) how
|
||||
the builder arrives — which also records an argument that failed. It was put that a produced image
|
||||
must be published before anything can fetch it, so the registry would have to come up before the
|
||||
control plane. It does not: the machine that builds the image is the machine that runs it, and a
|
||||
controller. It does not: the machine that builds the image is the machine that runs it, and a
|
||||
local image is named by the digest of its own configuration exactly as a carried one is. **Building
|
||||
changes where the bytes came from, not where they are.**
|
||||
|
||||
@@ -53,7 +53,7 @@ written. What follows describes the program that exists.
|
||||
|
||||
## Genesis
|
||||
|
||||
The installer is a single program carrying **the builder** inside it — not the control plane
|
||||
The installer is a single program carrying **the builder** inside it — not the controller
|
||||
([ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md)). What cannot be fetched is
|
||||
the thing that does the fetching, so that is what is carried; everything else is made here.
|
||||
|
||||
@@ -62,12 +62,12 @@ It proceeds in one direction, and every step is safe to run again.
|
||||
**First it refuses to start if the machine is not ready.** A container runtime, the ability to
|
||||
write where it must write, the host binary where it expects it — and a repository and a commit to
|
||||
build from, because an installer told nothing would raise a store and a broker and then have
|
||||
nothing to raise a control plane from. A machine that is not ready is told what is missing rather
|
||||
nothing to raise a controller from. A machine that is not ready is told what is missing rather
|
||||
than half-changed.
|
||||
|
||||
**Then it loads the carried builder and builds the control plane with it**, from a repository on a
|
||||
**Then it loads the carried builder and builds the controller with it**, from a repository on a
|
||||
mesh that already exists and a named commit ([ADR 0071](../../02-DECISIONS/0071-where-genesis-gets-its-source.md)).
|
||||
This is the same repository and path every later rebuild of the control plane will use, so what
|
||||
This is the same repository and path every later rebuild of the controller will use, so what
|
||||
raises the mesh is the same thing that will maintain it.
|
||||
|
||||
**Then it describes what the machine will become.** The image it just made is named by the digest
|
||||
@@ -75,7 +75,7 @@ of its own configuration — content-addressed and unforgeable, and requiring no
|
||||
it. That is legal precisely where nothing could have served one, and it is why building here needs
|
||||
no registry: the machine that made the image is the machine that will run it.
|
||||
|
||||
**Then it raises the substrate and a temporary control plane, and waits for that control plane to
|
||||
**Then it raises the foundation and a temporary controller, and waits for that controller to
|
||||
answer.** At this point the machine is a mesh of one node with nothing joined to it.
|
||||
|
||||
**Then the machine joins the mesh it is itself running.** It enrols, and an agent runs on it. Being
|
||||
@@ -84,13 +84,13 @@ enrolling is itself the thing that makes a mesh hear from a machine.
|
||||
|
||||
**Then it installs a registry**, so the mesh has somewhere to keep its own images.
|
||||
|
||||
**Then it publishes the control plane's image to that registry**, which is the moment the image
|
||||
**Then it publishes the controller's image to that registry**, which is the moment the image
|
||||
first receives a digest assigned by something other than itself. This is the carrying step, and it
|
||||
is the same step for all three things the build loop cannot produce for itself — the control plane,
|
||||
is the same step for all three things the build loop cannot produce for itself — the controller,
|
||||
the registry, and the builder. The rule and its closed list are in
|
||||
[`12-a-module-repository`](12-a-module-repository.md#the-three-the-loop-cannot-build-and-there-are-only-three).
|
||||
|
||||
**Then it installs the control plane again, as an ordinary module pinned to that digest, and drops
|
||||
**Then it installs the controller again, as an ordinary module pinned to that digest, and drops
|
||||
the temporary one.** The pivot is complete: what raised the mesh is gone, and what runs is a module
|
||||
like any other. From here the mesh can build and roll out its own upgrades, including to the thing
|
||||
that runs it.
|
||||
@@ -98,7 +98,7 @@ that runs it.
|
||||
## After the pivot, and still part of installing
|
||||
|
||||
Genesis ends with a mesh of one that runs, and that is not the same as a mesh that works. What it
|
||||
has is a control plane, a store, a queue and a registry. What it cannot yet do is **produce
|
||||
has is a controller, a store, a queue and a registry. What it cannot yet do is **produce
|
||||
anything** — and almost every module in the catalogue is waiting to be produced, because a manifest
|
||||
names what its artifacts are and nothing has made them.
|
||||
|
||||
@@ -107,9 +107,9 @@ So installing continues:
|
||||
**The builder arrives, and installing is what brings it.** It is a module like any other and is
|
||||
assigned to a machine like any other, but it cannot be built by the thing it is — see
|
||||
[`12-a-module-repository`](12-a-module-repository.md#the-three-the-loop-cannot-build-and-there-are-only-three).
|
||||
So it is carried, and it is already here: it is what built the control plane. The last step of
|
||||
So it is carried, and it is already here: it is what built the controller. The last step of
|
||||
installing publishes it into the mesh's own registry, installs it as an ordinary module pinned to
|
||||
that digest, and issues it a broker account — the same two acts the control plane went through,
|
||||
that digest, and issues it a broker account — the same two acts the controller went through,
|
||||
plus the one thing only a builder needs. The account is issued before the machine is sent anything,
|
||||
because a builder that arrives without its credential starts, finds nothing it may read, and waits,
|
||||
which looks exactly like a builder with no work.
|
||||
@@ -121,12 +121,12 @@ publishes each artifact into the mesh's own registry, and hands back the module
|
||||
pinned and the commit recorded. The mesh records that, and from then on the module is described by
|
||||
something it made rather than by a placeholder.
|
||||
|
||||
**The control plane is built like the rest.** It was carried in and published once, which got the
|
||||
**The controller is built like the rest.** It was carried in and published once, which got the
|
||||
mesh running; building it from its own repository and path is what makes it upgradeable. The first
|
||||
time that happens is the moment the mesh stops depending on the installer for anything.
|
||||
|
||||
**And then the catalogue.** Every module with source of its own is built the same way. Until this
|
||||
has happened a mesh can install only what is public or carried, which is the substrate and little
|
||||
has happened a mesh can install only what is public or carried, which is the foundation and little
|
||||
else.
|
||||
|
||||
Only after all of that is the ordinary loop available: change a module's source, the mesh notices
|
||||
@@ -138,7 +138,7 @@ What remains after *that* belongs to somebody else: adding machines, and decidin
|
||||
|
||||
## Joining
|
||||
|
||||
A machine joins with the host binary and a token. It does not raise a substrate, does not install a
|
||||
A machine joins with the host binary and a token. It does not raise a foundation, does not install a
|
||||
registry, and is never enrolled twice. The mesh already knows how to tell a machine what to be;
|
||||
joining is the point at which a machine starts listening.
|
||||
|
||||
@@ -169,8 +169,8 @@ paragraphs above describing the catalogue being built are a thing somebody now t
|
||||
thing that cannot happen.
|
||||
|
||||
**A module's declaration still has to be copied onto the machine by hand.** The installer reads the
|
||||
registry's and the control plane's manifests from a checkout somebody put there. The control plane's
|
||||
now lives in the control plane's own repository, which the installer clones anyway, so this is a
|
||||
registry's and the controller's manifests from a checkout somebody put there. The controller's
|
||||
now lives in the controller's own repository, which the installer clones anyway, so this is a
|
||||
thing that can be removed rather than a thing that must be designed.
|
||||
|
||||
**A machine has no account for a registry that asks for one.** The mesh grants a consumer a
|
||||
@@ -198,6 +198,6 @@ the SDK inside `docker build`, which is slow and names a branch head rather than
|
||||
| An image is named exactly | A machine refuses a bundle naming an image by tag. The refusal is exercised, not assumed. |
|
||||
| The installer is what installed this | **Nothing.** See above. |
|
||||
| The builder can arrive on a fresh mesh | The installer carries it and installs it as its last step, and the genesis bed raises a machine by running the installer. A bed that raises one any other way fails its own acceptance check. |
|
||||
| The control plane a mesh runs is one it built | The genesis bed asserts the running control plane is pinned to a digest this mesh's own registry serves, for an image built from a named repository and commit — not one the installer carried. |
|
||||
| A core module is built rather than only carried | The control plane is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. |
|
||||
| The controller a mesh runs is one it built | The genesis bed asserts the running controller is pinned to a digest this mesh's own registry serves, for an image built from a named repository and commit — not one the installer carried. |
|
||||
| A core module is built rather than only carried | The controller is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. |
|
||||
| Installing produced a mesh that can produce | A module with source of its own is asked for and comes back pinned to a digest this mesh's registry assigned, not to a placeholder. **Done by hand on a raised machine, not yet by a bed** — it built the shared base and then a module naming that base. Nothing automated asserts it, which makes this the weakest check on this page. |
|
||||
|
||||
@@ -24,9 +24,9 @@ why the current model does not fit what a module is.
|
||||
Turning a module's source into artifacts the mesh can pin, publish and deliver — and saying
|
||||
truthfully what was produced and what it was produced against.
|
||||
|
||||
Everything else is somebody else's: *what* to build is the control plane's, *what a build means* is
|
||||
Everything else is somebody else's: *what* to build is the controller's, *what a build means* is
|
||||
the catalogue's ([ADR 0072](../../02-DECISIONS/0072-two-graphs-and-the-build-chain.md)), *where an artifact runs* is the
|
||||
control plane's again. The builder's whole responsibility is the middle.
|
||||
controller's again. The builder's whole responsibility is the middle.
|
||||
|
||||
## The language
|
||||
|
||||
@@ -130,7 +130,7 @@ change is one edit; with four it is four that must land together, and a mesh who
|
||||
about the envelope fails by ignoring messages rather than by failing to compile.
|
||||
|
||||
**So the contracts have to stop being expressed twice before they are expressed four times.** They
|
||||
are already: the manifest, declaration and link shapes exist as Go structs in the control plane and
|
||||
are already: the manifest, declaration and link shapes exist as Go structs in the controller and
|
||||
as TypeScript types in the SDK, kept in step by hand. Nobody has felt it because both live in one
|
||||
repository. A second *language* makes that drift; a specified envelope and schema that every SDK
|
||||
implements makes a second language an implementation rather than a translation.
|
||||
@@ -176,7 +176,7 @@ disagrees with it.
|
||||
| `certificate` | a certificate for a name it serves |
|
||||
| `grants` | credentials it must create for its consumers |
|
||||
| `filtering` | rules beyond its own ports |
|
||||
| `computed` | marks a module the control plane generates rather than an author writing |
|
||||
| `computed` | marks a module the controller generates rather than an author writing |
|
||||
| `build.artifacts` | what it produces |
|
||||
|
||||
### What it builds
|
||||
|
||||
@@ -58,7 +58,7 @@ mesh can issue anything. An implementation accepts both and must not treat the s
|
||||
- The connection **pins the fingerprint**. It does not trust a certificate authority, and it does
|
||||
not skip verification. A broker presenting a different certificate is refused, whatever else is
|
||||
true of it.
|
||||
- A scoped account **does not declare exchanges**. The substrate owns them; an account that may
|
||||
- A scoped account **does not declare exchanges**. The foundation owns them; an account that may
|
||||
declare one is an account that may create a parallel mesh by typo.
|
||||
- An implementation **declares its own queue** and nothing else.
|
||||
|
||||
@@ -142,7 +142,7 @@ A module's tools are its operator-facing surface.
|
||||
### Not yet true
|
||||
|
||||
The caller's half has no account. Until that is settled, the only thing that can ask a module a
|
||||
question is the substrate's bootstrap admin, which is not a protocol so much as a way in.
|
||||
question is the foundation's bootstrap admin, which is not a protocol so much as a way in.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ ingest/ingest.py emit("module.showcase.ingested", …) → events, emitti
|
||||
### Step 3 — the mesh does the rest
|
||||
|
||||
You do not write a Dockerfile, a unit file, or a port number. The builder picks the toolchain from
|
||||
the language, compiles each artifact alone, and publishes it. The control plane assigns the machine
|
||||
the language, compiles each artifact alone, and publishes it. The controller assigns the machine
|
||||
and the ports; the host writes the units.
|
||||
|
||||
### What it costs you to use four languages
|
||||
|
||||
@@ -26,9 +26,9 @@ happens*, in order, with each step's name as the installer prints it.
|
||||
|
||||
| | why |
|
||||
|---|---|
|
||||
| a container runtime | the substrate is containers, and the installer refuses without one |
|
||||
| a container runtime | the foundation is containers, and the installer refuses without one |
|
||||
| the host binary, where the installer expects it | it is what the machine becomes |
|
||||
| a repository and a commit to build from | the installer carries a builder, not a control plane, so it must be told what to make ([ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md)) |
|
||||
| a repository and a commit to build from | the installer carries a builder, not a controller, so it must be told what to make ([ADR 0073](../../02-DECISIONS/0073-the-installer-carries-a-builder.md)) |
|
||||
| a way out to the internet | the store, the broker and the registry are pulled from it |
|
||||
| a reachable git host, and a commit it serves | what is cloned is the trust anchor for everything this mesh will ever run ([ADR 0071](../../02-DECISIONS/0071-where-genesis-gets-its-source.md)) |
|
||||
| the address other machines will reach this one on | a token carries it verbatim; the installer refuses to guess |
|
||||
@@ -41,19 +41,19 @@ Twelve steps, run by one program, each safe to run again.
|
||||
|---|---|---|---|
|
||||
| 1 | `preflight` | everything above is checked | the machine is not half-changed by a missing prerequisite |
|
||||
| 2 | `load` | the carried builder image is loaded | the mesh has the one thing it cannot fetch — the thing that does the fetching |
|
||||
| 3 | `build` | the builder clones the named repository at the named commit and **builds the control plane** | what will run is something this mesh made and can make again |
|
||||
| 4 | `bundle` | the substrate template is written out, with the built control plane's id in place of the placeholder | the machine has a description of what it will become |
|
||||
| 5 | `apply` | store, broker, schemas, and a **temporary** control plane are raised | a mesh of one exists and answers |
|
||||
| 6 | `verify` | the control plane is asked, rather than assumed | it replies, and says it has no machines |
|
||||
| 3 | `build` | the builder clones the named repository at the named commit and **builds the controller** | what will run is something this mesh made and can make again |
|
||||
| 4 | `bundle` | the foundation template is written out, with the built controller's id in place of the placeholder | the machine has a description of what it will become |
|
||||
| 5 | `apply` | store, broker, schemas, and a **temporary** controller are raised | a mesh of one exists and answers |
|
||||
| 6 | `verify` | the controller is asked, rather than assumed | it replies, and says it has no machines |
|
||||
| 7 | `enrol` | the machine joins the mesh it is itself running | the mesh has one node, and an agent runs on it |
|
||||
| 8 | `registry` | the mesh's own artifact store is installed | there is somewhere to keep what this mesh makes |
|
||||
| 9 | `publish` | the control plane's image is pushed into it | the image has a digest something other than itself assigned |
|
||||
| 10 | `control-plane` | it is installed again, as an ordinary module pinned to that digest | what runs is a module like any other |
|
||||
| 11 | `retire` | the temporary control plane is dropped | **the pivot is complete** — what raised the mesh is gone |
|
||||
| 9 | `publish` | the controller's image is pushed into it | the image has a digest something other than itself assigned |
|
||||
| 10 | `controller` | it is installed again, as an ordinary module pinned to that digest | what runs is a module like any other |
|
||||
| 11 | `retire` | the temporary controller is dropped | **the pivot is complete** — what raised the mesh is gone |
|
||||
| 12 | `builder` | the carried builder is published, installed as a module, and issued a broker account | the mesh can produce |
|
||||
|
||||
**Steps 8 to 11 are the pivot** ([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)). Before
|
||||
them the control plane is something the installer put there; after them it is something the mesh
|
||||
them the controller is something the installer put there; after them it is something the mesh
|
||||
holds a record of and can upgrade. The account in step 12 is issued *before* the machine is sent
|
||||
anything, because a builder that arrives without its credential starts, finds nothing it may read,
|
||||
and waits — which looks exactly like a builder with no work.
|
||||
@@ -68,10 +68,10 @@ catalogue be missing from a test for weeks without anything complaining.
|
||||
| # | step | what happens | why it is here |
|
||||
|---|---|---|---|
|
||||
| 13 | the shared base is built | the toolchain and runtime every module with code of its own stands on | nothing else with code can be built until it exists |
|
||||
| 14 | a store module is built and run | a database **provider**, which the substrate's store is not | the substrate's store is the control plane's own memory, and offers nothing to anything |
|
||||
| 14 | a store module is built and run | a database **provider**, which the foundation's store is not | the foundation's store is the controller's own memory, and offers nothing to anything |
|
||||
| 15 | the catalogue is built and run | the module graph | without it the mesh cannot say what it holds, what a change reaches, or what must be rebuilt |
|
||||
| 16 | the catalogue asks for what it missed | the builds made before it existed are replayed | on a fresh mesh those are always the base, the store and the catalogue itself ([issue 050](../../04-ISSUES/050-the-catalogue-knows-nothing-built-before-it/00-report.md)) |
|
||||
| 17 | the control plane is rebuilt from its own repository | and rolled out through the module path | the moment the mesh stops depending on the installer for anything |
|
||||
| 17 | the controller is rebuilt from its own repository | and rolled out through the module path | the moment the mesh stops depending on the installer for anything |
|
||||
| 18 | `networking` is assigned **and the node placed** | a private network, and names | assigning installs the module; placing says where this machine is on it. Both, or the names file is written empty |
|
||||
| 19 | the packet filter is assigned | rules generated from what modules declared | until this, every rule the mesh computes has never been applied to anything |
|
||||
|
||||
@@ -129,9 +129,9 @@ two.
|
||||
|
||||
| # | module | provides | note |
|
||||
|---|---|---|---|
|
||||
| 1 | `postgres` | `postgres-database` | **the control plane's own records and every module's.** One server, not two |
|
||||
| 1 | `postgres` | `postgres-database` | **the controller's own records and every module's.** One server, not two |
|
||||
| 2 | `lavinmq` | `amqp` | the broker every node dials, and what modules are granted vhosts on |
|
||||
| 3 | `mesh-control` | *claims* `the-control-plane` | decides what runs where |
|
||||
| 3 | `mesh-control` | *claims* `the-controller` | decides what runs where |
|
||||
| 4 | `distribution` | `artifact-store` | what the mesh built, pinned by digest — the module is the software (Distribution), the provision is the job |
|
||||
| 5 | `builder` | — | turns source into artifacts |
|
||||
| 6 | `mesh-tools` | build inputs | the base everything with code compiles against. **Runs nowhere** |
|
||||
@@ -144,8 +144,8 @@ two.
|
||||
|
||||
### Why it is twelve and not thirteen
|
||||
|
||||
**The substrate's store and the `postgres` module are the same module.** They were two rows while the
|
||||
substrate was a different *kind* of thing: a store raised from a bundle cannot provide
|
||||
**The foundation's store and the `postgres` module are the same module.** They were two rows while the
|
||||
foundation was a different *kind* of thing: a store raised from a bundle cannot provide
|
||||
`postgres-database`, so anything wanting a database needed a second server. That is visible on any
|
||||
mesh built today — `mesh-store` and `postgres`, two containers, **the same image**.
|
||||
|
||||
@@ -156,20 +156,20 @@ The naming rule settles which name survives
|
||||
> interface *is* the protocol. **"database" is not a capability; the protocol is.** Never false
|
||||
> genericity: a name must not promise a swap the contract cannot deliver.
|
||||
|
||||
So there is no `store` module. The control plane is coupled to postgres — its own queries use
|
||||
So there is no `store` module. The controller is coupled to postgres — its own queries use
|
||||
`distinct on` and `on conflict`, which are not portable — and calling it `store` would advertise a
|
||||
swap that would fail the first time somebody tried it.
|
||||
|
||||
**The same applies to the broker**, with a different outcome: `amqp` *is* a protocol and more than
|
||||
one implementation speaks it, so `amqp` is a legitimate provision and `lavinmq` is one provider of
|
||||
it. The substrate's broker and the `lavinmq` module collapse the same way.
|
||||
it. The foundation's broker and the `lavinmq` module collapse the same way.
|
||||
|
||||
### What this costs, and it is the last specialty
|
||||
|
||||
Adopting the store and the broker as modules is [issue 051](../../04-ISSUES/051-the-mesh-cannot-update-what-it-depends-on/00-report.md),
|
||||
and it is the only part of this that has not been designed. The two hard parts:
|
||||
|
||||
- **upgrading a store the control plane is reading from** — a rollout where the thing being replaced
|
||||
- **upgrading a store the controller is reading from** — a rollout where the thing being replaced
|
||||
is the thing holding the record of the rollout
|
||||
- **upgrading a broker over the broker** — the instruction arrives on what it replaces, so the
|
||||
machine must finish without being able to report progress
|
||||
|
||||
@@ -34,7 +34,7 @@ after the phases that change its build path are in — not before.
|
||||
|
||||
## Phase 1 — the protocol is one thing, and correct *(mostly done: the drift was dead types)*
|
||||
|
||||
**Why here.** The Go control plane and the TypeScript SDK disagree about what a grant carries
|
||||
**Why here.** The Go controller and the TypeScript SDK disagree about what a grant carries
|
||||
(`consumer` is the module in one, the node in the other). That is exercised by the installer's own
|
||||
provisioning — the catalogue's database — so it belongs before more is built on it.
|
||||
|
||||
@@ -80,12 +80,12 @@ raises gitea and publishes the SDK before the base build. Those close together i
|
||||
a protocol that agrees, a registry to publish to. Issue 051.
|
||||
|
||||
- [ ] 3.1 the store is adopted — raised at genesis, then held as the `postgres` module; one server,
|
||||
the control plane's records and every module's database in it
|
||||
the controller's records and every module's database in it
|
||||
- [ ] 3.2 the broker is adopted — one `lavinmq`, the `/` vhost for the mesh bus, a vhost per
|
||||
consumer that requires `amqp`; the second server gone
|
||||
- [ ] 3.3 an upgrade of each, proven: a store the control plane reads from, a broker over the
|
||||
- [ ] 3.3 an upgrade of each, proven: a store the controller reads from, a broker over the
|
||||
broker, each with a stated window
|
||||
- [ ] 3.4 `status` can say the substrate is behind its source, which today it cannot form
|
||||
- [ ] 3.4 `status` can say the foundation is behind its source, which today it cannot form
|
||||
|
||||
**Done when.** A mesh built from bare metal runs one postgres and one lavinmq, and can upgrade
|
||||
either — so the twelve-module floor has no specialty left in it.
|
||||
|
||||
@@ -15,8 +15,8 @@ document is written and this one's status becomes `implemented`.
|
||||
| [`03-scenario-lifecycle.md`](03-scenario-lifecycle.md) | What happens to a scenario — raise, snapshot, restore, move, destroy | [ADR 0016](../../02-DECISIONS/0016-the-lab.md) |
|
||||
| [`04-lab-installation.md`](04-lab-installation.md) | Getting the lab onto a clean machine, and why it verifies capability rather than installation | [ADR 0010](../../02-DECISIONS/0010-delivery.md) |
|
||||
| [`05-the-node-host.md`](05-the-node-host.md) | Tier 0 — the one thing installed by hand, and the only thing that changes a machine | [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) |
|
||||
| [`06-the-control-plane.md`](06-the-control-plane.md) | Tier 2 — what the term means, and the test for what belongs in it | [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) |
|
||||
| [`07-the-substrate.md`](07-the-substrate.md) | Tier 1 — what the control plane consumes and cannot grant itself | [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [0048](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) |
|
||||
| [`06-the-controller.md`](06-the-controller.md) | Tier 2 — what the term means, and the test for what belongs in it | [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) |
|
||||
| [`07-the-foundation.md`](07-the-foundation.md) | Tier 1 — what the controller consumes and cannot grant itself | [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [0048](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) |
|
||||
| [`08-connectivity.md`](08-connectivity.md) | One context in full — overlay, resolution, exposure, filtering, certificates | [ADR 0007](../../02-DECISIONS/0007-connectivity.md), [0050](../../02-DECISIONS/0007-connectivity.md), [0051](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [0055](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) |
|
||||
| [`09-the-node-lifecycle.md`](09-the-node-lifecycle.md) | How a machine becomes a node, stays one, and stops being one | [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [0051](../../02-DECISIONS/0004-a-node-and-how-it-joins.md) |
|
||||
| [`10-delivery.md`](10-delivery.md) | Modules, the three edges, and how a change becomes a running thing | [ADR 0010](../../02-DECISIONS/0010-delivery.md), [0064](../../02-DECISIONS/0009-modules-and-the-graph.md), [0065](../../02-DECISIONS/0009-modules-and-the-graph.md) |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Novox HQ
|
||||
|
||||
The single source of truth for what Novox builds — what it **is**, what it is **becoming**,
|
||||
and why. Today that is almost entirely **Novox Mesh**, the substrate everything else runs on. Implementation lives in `modules/`; the reasoning behind it lives here.
|
||||
and why. Today that is almost entirely **Novox Mesh**, the foundation everything else runs on. Implementation lives in `modules/`; the reasoning behind it lives here.
|
||||
|
||||
## Structure
|
||||
|
||||
|
||||
Reference in New Issue
Block a user