catalogue: compose a route's name from a label and its node's domain, and resolve it in-mesh

A public route used to carry its whole hostname as a literal in the module
manifest, so running the same catalogue against a different domain meant
overriding that literal on every routed module, per node. The mesh was, in
effect, holding a map of names to services: the one thing it should never hold,
because the subdomain is the operator's choice and the domain is the node's.

Compose instead. A route contribution carries a `label` (the subdomain); a node
carries its `public_domain` as node-level configuration; the mesh joins
`<label>.<public-domain>` and grants exactly that, interpreting neither half.
Held as a node property beside the node's other node-level facts (endpoint,
site, overlay address), not in a module's settings — the ADR calls it
node-level, and the settings table is keyed per module.

Additive, so an unmigrated catalogue keeps working: a contribution that still
carries a full `name` and no `label` passes through unchanged, and the catalogue
can migrate module by module. A labelled contribution on a node with no public
domain composes nothing, reading downstream as a route that named no host.

And propagate: each granted route name is published into internal resolution
mesh-wide, mapped to the node that serves it, alongside the `<node>.internal`
names every container already gets. So a container — and an internal ACME
validator, which cannot complete a challenge for a name it cannot reach —
resolves a routed name to the proxy that serves it. Name-agnostic throughout:
the mesh propagates whatever names it was told to serve and knows nothing about
what they mean.

novox/hq 02-DECISIONS/0056

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-09 23:27:35 +02:00
parent c147a26138
commit 232862315c
8 changed files with 448 additions and 5 deletions
+10 -2
View File
@@ -24,6 +24,10 @@ type Node struct {
// At is this machine's own name on the private network, empty if it is not on one. Needed to
// tell whether it can reach the node answering its requirements at all.
At string
// PublicDomain is the domain this node composes its routed names under, empty if it has none
// (novox/hq ADR 0056). A route contribution carries only a label — the subdomain — and the mesh
// joins <label>.<public-domain> to make the name it grants, interpreting neither half.
PublicDomain string
}
// World is what the rest of the mesh already has.
@@ -102,6 +106,10 @@ type Resolution struct {
// because a consumer bound to something answered on this same machine still has to be told
// where it is — the answer being local does not make the port guessable.
At string
// PublicDomain is the domain this node composes its routed names under, empty if it has none
// (novox/hq ADR 0056). Carried from the node so that composing <label>.<public-domain> for a
// route contribution needs no store lookup here — the join is a fact about this one machine.
PublicDomain string
// Modules in the order they were resolved: assigned first, then what they pulled in.
Modules []Manifest
@@ -504,8 +512,8 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
}
}
resolution := Resolution{Node: node.Name, At: node.At, Because: because, Needs: needs,
Unhostable: unhostable}
resolution := Resolution{Node: node.Name, At: node.At, PublicDomain: node.PublicDomain,
Because: because, Needs: needs, Unhostable: unhostable}
for _, n := range providersFirst(order, catalogue) {
resolution.Modules = append(resolution.Modules, catalogue[n])
}