The glossary's authority page still named the controller's seat the-controller in two entries, contradicting its own seat section after ADR 0079; issue 058's heading kept the pre-renumber 059; 055's fixed-by named branches that stop existing after merge (now merge commits/PRs) and its located-in listed file paths where the convention wants repos; 056's located-in named mesh-host, which received no fix, instead of mesh-catalog; and the design layer never said the one-store/one-broker property is enforced — 07-the-foundation and the installation table now state the seats. https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
179 lines
12 KiB
Markdown
179 lines
12 KiB
Markdown
---
|
|
layer: to-be
|
|
status: proposed
|
|
code:
|
|
- mesh-host internal/bootstrap
|
|
- mesh-host cmd/mesh-bootstrap
|
|
- mesh-lab test/integration/one-node-mesh.test.ts
|
|
updated: 2026-09-15
|
|
decisions:
|
|
- 02-DECISIONS/0067-genesis-is-a-pivot.md
|
|
- 02-DECISIONS/0073-the-installer-carries-a-builder.md
|
|
- 02-DECISIONS/0071-where-genesis-gets-its-source.md
|
|
- 02-DECISIONS/0014-no-npm-workspace.md
|
|
---
|
|
|
|
# The installation, in full
|
|
|
|
Every step from a machine with nothing to a mesh that maintains itself. Written out explicitly
|
|
because it is the procedure everything else depends on, and because the parts that do not exist
|
|
yet are easier to see beside the parts that do.
|
|
|
|
[`17-raising-a-mesh`](17-raising-a-mesh.md) argues *why* it is shaped this way. This says *what
|
|
happens*, in order, with each step's name as the installer prints it.
|
|
|
|
## What must be true before anything starts
|
|
|
|
| | why |
|
|
|---|---|
|
|
| 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 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 |
|
|
|
|
## Phase one — a machine becomes a mesh of one
|
|
|
|
Twelve steps, run by one program, each safe to run again.
|
|
|
|
| # | step | what happens | true afterwards |
|
|
|---|---|---|---|
|
|
| 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 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 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 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.
|
|
|
|
## Phase two — a mesh of one becomes a mesh that works
|
|
|
|
**Genesis ends with a mesh that runs, which is not the same as a mesh that works.** It has a control
|
|
plane, a store, a broker, a registry and a builder. It holds no module graph, has no private
|
|
network, no packet filter, and cannot resolve a name. Calling that "installed" is what let the
|
|
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 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 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 |
|
|
|
|
## Phase three — machines arrive
|
|
|
|
Only now. A machine joining a mesh that cannot build anything proves that enrolment works, which
|
|
was never the doubtful part.
|
|
|
|
| # | step | what happens |
|
|
|---|---|---|
|
|
| 20 | a node record is made and a token issued | one-time, carrying the broker's address and the fingerprint to pin |
|
|
| 21 | the machine enrols and runs its agent | being heard from once is not an agent running; both are checked |
|
|
| 22 | it is placed on the private network | or nothing can bind a consumer on it to a provider elsewhere |
|
|
| 23 | modules are assigned to it | and it pulls what it needs from the mesh's registry |
|
|
|
|
## What is not yet true
|
|
|
|
Stated plainly, because a procedure that implies otherwise is worse than none.
|
|
|
|
**Steps 13 to 19 are not the installer's.** They are things somebody types. The installer ends at
|
|
step 12, and everything that turns a running mesh into a working one is manual — which is why a
|
|
test had to be written to find out they were missing.
|
|
|
|
**Step 23 does not work for a second machine.** It has no account on the registry
|
|
([issue 042](../../04-ISSUES/042-nothing-gives-a-node-an-account-for-a-registry/00-report.md)) and
|
|
no reason to trust a registry serving plain HTTP over the network
|
|
([issue 048](../../04-ISSUES/048-nothing-makes-a-machine-trust-the-mesh-registry/00-report.md)).
|
|
Both are invisible on a mesh of one, where the registry is loopback.
|
|
|
|
**There is no private package registry**, and [ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md)
|
|
assumes one: a module consumes its dependencies, the mesh's own shared library included, from it.
|
|
At step 13 nothing has installed one, so the first build of the shared base resolves the SDK some
|
|
other way — today by a git URL, which is
|
|
[issue 053](../../04-ISSUES/053-the-sdk-is-pinned-twice-and-the-two-disagree/00-report.md).
|
|
|
|
**The installer does not install the host's unit, though one exists.** `mesh-host/packaging/` ships
|
|
`nox-mesh-host.service` and two companions; the installer declines to place them because a unit file
|
|
is a packaging decision. So an install that does nothing further leaves a machine whose containers
|
|
come back after a reboot and whose agent does not — it runs the right things and can no longer be
|
|
told anything.
|
|
|
|
**This is a gap in packaging, not in the mesh**, and the distinction matters: the lab's
|
|
`--host-in-background` says in its own help that it does not survive a reboot, so a lab run failing
|
|
this is the lab being honest rather than the mesh being broken. What is missing is the step that
|
|
puts the shipped unit on the machine.
|
|
|
|
**Nothing asserts a mesh was installed this way.** A claim that a machine was brought up by this
|
|
procedure cannot be contradicted by anything afterwards.
|
|
|
|
## What a finished mesh holds
|
|
|
|
**Twelve, and after the pivot none of them is a specialty.** Every row is a module the mesh built,
|
|
holds a version of, and can upgrade — which is the whole claim, and is not true today for the first
|
|
two.
|
|
|
|
| # | module | provides | note |
|
|
|---|---|---|---|
|
|
| 1 | `postgres` | `postgres-database` | **the controller's own records and every module's.** One server, not two — it *claims* the mesh-scoped `mesh-store` seat, so a second is refused |
|
|
| 2 | `lavinmq` | `amqp` | the broker every node dials, and what modules are granted vhosts on — *claims* `mesh-broker`, one per mesh |
|
|
| 3 | `mesh-controller` | *claims* `mesh-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** |
|
|
| 7 | `mesh-catalog` | the module graph | what is held, what a change reaches, what must be rebuilt |
|
|
| 8 | `networking` | `private-network`, naming | requirements only — assigning it brings `mesh-wireguard` and `mesh-names` |
|
|
| 9 | `dnsmasq` + one of `resolved-split-dns` / `resolv-conf` | `wildcard-resolution` | names that actually resolve, on top of `mesh-resolver`'s data |
|
|
| 10 | `step-ca` | `acme-ca` | certificates for `.internal` |
|
|
| 11 | `firewall` | *claims* `the-packet-filter` | rules generated from what modules declared |
|
|
| 12 | `gitea` | `package-registry` | where a module's dependencies come from ([ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md)), and the git host |
|
|
|
|
### Why it is twelve and not thirteen
|
|
|
|
**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**.
|
|
|
|
The naming rule settles which name survives
|
|
([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)):
|
|
|
|
> Where the consumer **speaks a protocol** — a database's wire protocol and query dialect — the
|
|
> 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 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 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 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
|
|
|
|
Both are operations with windows, and a machine rebooting inside one is an operator's problem during
|
|
an operation rather than a reason not to do it.
|