The mesh's knowledge is a fact a module asks for, not three modules

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
This commit is contained in:
2026-09-15 21:31:18 +02:00
parent 18ec632baa
commit fcdb065660
13 changed files with 348 additions and 344 deletions
+13 -67
View File
@@ -138,30 +138,6 @@ func (g *Generator) Resources(node string) ([]map[string]any, bool, error) {
// private network the peers would be written by something else and this would be unchanged.
type NameGenerator struct{ nodes []Node }
// NamesFor builds the name generator over the machines on the private network.
func NamesFor(nodes []Node) *NameGenerator { return &NameGenerator{nodes: nodes} }
// Resources is the one file.
func (g *NameGenerator) Resources(node string) ([]map[string]any, bool, error) {
var found bool
for _, n := range g.nodes {
if n.Name == node {
found = true
}
}
if !found {
return nil, false, nil
}
hosts, err := Hosts(g.nodes, node)
if err != nil {
return nil, false, err
}
return []map[string]any{{
"id": "mesh-names", "type": "file", "path": HostsPath,
"mode": "0644", "content": hosts,
}}, true, nil
}
// Nodes are the machines this generator was built over, so a caller can say who is on the network.
func (g *Generator) Nodes() []Node { return g.nodes }
@@ -179,55 +155,25 @@ func Manifest() map[string]any {
"version": "1",
"computed": Name,
"provides": []string{Requirement, Addressing},
"claims": []map[string]any{{"name": TheNetwork, "scope": "node"}},
}
}
// NamesManifest is the module that gives machines names on the private network.
//
// It requires the network rather than providing it, which is the whole reason it is separate: a
// name resolves to an address on the private wire, so having names without being on it would
// point every machine at somewhere it cannot reach.
func NamesManifest() map[string]any {
return map[string]any{
"module": Names,
"version": "1",
"computed": Names,
"provides": []string{Resolution},
"requires": []string{Addressing},
}
}
// ResolverManifest is what a resolver on this machine must know: every name under every machine.
//
// **It writes the data and runs no daemon.** A resolver is third-party software, and third-party
// software runs *on* the mesh rather than being *of* it
// ([ADR 0001](novox/hq)) — the mesh has no business shipping one, choosing which one, or knowing
// its configuration language. What only the mesh can know is which machines exist and where they
// are, so that is what it computes.
//
// So a module that runs a resolver requires what this provides, and reads one file. Swapping the
// daemon changes that module and nothing here.
//
// **Separate from names rather than part of them**, because a machine with no container runtime
// can still have a hosts file. Folding them together would take exact names away from a machine
// that cannot run a daemon, to give it a wildcard it cannot use either.
func ResolverManifest() map[string]any {
return map[string]any{
"module": Resolver,
"version": "1",
"computed": Resolver,
"requires": []string{Resolution},
"provides": []string{ResolverData},
// Being on the private network is what gives a machine a name, so the module that puts it
// there is what writes them. Asked for rather than generated by a module of its own: the
// mesh knows which machines exist and where; writing that into a hosts file is not a thing
// that needs a module to run nowhere.
"facts": map[string]string{"node-names": "/etc/hosts"},
"claims": []map[string]any{{"name": TheNetwork, "scope": "node"}},
}
}
// DomainManifest is the module that means "get the network working".
func DomainManifest() map[string]any {
return map[string]any{
"module": Domain,
"version": "1",
"requires": []string{Requirement, Resolution},
"module": Domain,
"version": "1",
// **Only the network now.** It used to require name-resolution as well, answered by a
// module that wrote a hosts file and ran nothing. Names are not a provision — they are a
// fact the mesh computes, and whatever puts a machine on the private network writes them,
// because a mesh name IS an address on that network.
"requires": []string{Requirement},
}
}