mesh-names, mesh-resolver and the names half of the overlay generators are gone.
They ran no software and could not be swapped for anything, which is the test of
whether something is a module at all — they existed because computed output
needed somewhere to live, and the control plane's only shape for output was a
module.
Now a module says where it wants what the mesh knows:
facts: { node-zones: /etc/mesh-resolver/nodes.conf }
and is given a file, under its own name, applied and removed like anything else
it declares. Two facts exist: node-names (a hosts file — exact names) and
node-zones (every machine as a wildcard, *.homer.internal is homer). Asking for
a fact the mesh does not compute is refused naming what would have worked,
because a daemon that starts and reads a file nobody wrote is a worse way to
find out.
The names ride with the network now: wireguard's manifest asks for node-names
into /etc/hosts, because being on the private network is what gives a machine a
name. networking no longer requires name-resolution — names are not a provision,
and the module that answered it ran nothing.
One behaviour inverted, deliberately: choosing another VPN used to drag
WireGuard in anyway, because only WireGuard provided the addressing the names
module required — the node-scope claim existed to at least make that loud. With
names as a fact there is nothing to drag in: tailscale assigned means tailscale,
alone. The claim still catches two VPNs assigned explicitly.
And a machine the mesh cannot place is left out of both files rather than named
at nothing: a name resolving to nothing hangs a connection, where an unknown
name fails at once and says so. In practice that is only ever a token issued and
not yet used — a machine that has announced itself has an address.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Example modules
Manifests, not programs. They are here because the contract is easier to read as something that works than as a description of something that would.
Third-party software runs on the mesh, not of it (novox/hq ADR 0001). dnsmasq is not the mesh's, and neither is systemd-resolved — what is the mesh's is the fact only it can know, which is which machines exist and where they are. So the mesh writes that to a file and these read it.
Resolving a service named under a machine
postgres.novox.internal, plex.ace.internal. The first label is the service and the rest is the
node, so anything under a node's name must resolve to that node and a proxy there routes by
the name it was asked for. That routing is a separate concern and stays separate.
Two roles, and they are genuinely different things:
| claims | ||
|---|---|---|
| serving | the-dns-port |
answers the wildcards — dnsmasq.json |
| asking | the-resolver-configuration |
decides what the machine asks — resolved-split-dns.json, resolv-conf.json |
systemd-resolved cannot serve a wildcard, so it is only ever an asking module: it routes the mesh's suffix to something that can. Treating the two roles as one would produce a module that cannot work, which is the mistake worth naming.
Assign one of each. Two of either is refused by the mesh rather than fought over on the machine:
resolved-split-dns and resolv-conf both claim "the-resolver-configuration", and only one thing may hold it per node
ADR 0009's table names that resource /etc/resolv.conf, which is what it is, the way it writes
the seat. A claim is a name in the catalogue's own form, so it is written as one.
Why neither needs to know the machine's address
Both would ordinarily need it — a resolver must bind somewhere, and a stub must be pointed somewhere — and a static manifest cannot know it.
Neither does, because both name things the mesh itself named: the private network's interface
is mesh0 on every machine, and the address a resolver listens on for the machine's own use is
127.0.0.54 on every machine. A name the mesh chose is a name a manifest can use.
Asking for a bucket
minio.json provides one, photos.json asks for one. Together they are the whole of an
edge, and they are here as a pair because that is the only way to see the halves line up:
| the provider says | the consumer says |
|---|---|
provides: s3-bucket |
requires: s3-bucket |
receives — where to be told who asked |
contributes: {bucket: photos} — what it wants |
grants — where their credentials land |
secrets — where to be given its key |
serves — port, scheme, region |
binds — where to be told all that |
The mesh adds the half neither can know: which machine the provider is on, and where it is on the private network. Neither manifest names an address, and that is what lets the same pair work on any mesh.
s3-bucket names the protocol, not the product (novox/hq ADR 0027). A
consumer's code is written against the S3 API, and swapping one store for another does not break
it — so the coupling is to S3. A database is the other case: an application is written against
PostgreSQL or against SQL Server, so those provisions name the engine.
What the pair is checked for. That the names match, that each side says where it wants to be
told, and that the consumer contributes the key the provisioner actually reads — bucket, not
name. Contributing name (which is what a database consumer contributes) resolves perfectly and
then fails on the machine with asked for a bucket and did not name it, which is a long way from
the manifest that caused it.
The provisioner that makes the credential true lives in
../objectstore-provisioner, and is proven against a real store in
the lab — including the assertion a database does not need, that a consumer cannot reach another
consumer's bucket. One store holds every bucket behind one endpoint, so that isolation is a
policy somebody wrote rather than a boundary the product has.