Name system seats by scope; let a module define its own (ADR 0121)

System seats are mesh-* (one, mesh-wide) or node-* (one per node). Renamed:
the-build-machine -> mesh-build-machine (+scope mesh), the-catalogue ->
mesh-catalog, the-dns-port -> node-dns-resolver, the-intrusion-prevention ->
node-intrusion-prevention, the-packet-filter -> node-packet-filter,
the-resolver-configuration -> node-resolver-config, the-uplink -> node-uplink.
Removed the-showcase from the set — it becomes the first module-defined seat.

A manifest may declare its own seats (DefinesSeats); a claim is a system seat,
a reserved mesh-*/node-* name the mesh does not define (refused), or a
module-defined seat valid only when the manifest declares it.

Deferred: the delivering registry seats (git, npm-package-registry,
the-artifact-store) and the-private-network (a scope + server/client model
change), per ADR 0121.
This commit is contained in:
2026-09-27 14:30:56 +02:00
parent a6d89e3f73
commit 1c56210530
5 changed files with 105 additions and 34 deletions
+4 -4
View File
@@ -35,13 +35,13 @@ func TestEverySeatIsListedIncludingTheOnesNobodyHolds(t *testing.T) {
func TestANodeSeatListsEveryMachineHoldingIt(t *testing.T) { func TestANodeSeatListsEveryMachineHoldingIt(t *testing.T) {
rows, _ := seatsHeld(catalogue.Seats(), []catalogue.Held{ rows, _ := seatsHeld(catalogue.Seats(), []catalogue.Held{
{Claim: "the-packet-filter", Scope: catalogue.ScopeNode, Node: "node2", Module: "nftables"}, {Claim: "node-packet-filter", Scope: catalogue.ScopeNode, Node: "node2", Module: "nftables"},
{Claim: "the-packet-filter", Scope: catalogue.ScopeNode, Node: "anchor", Module: "nftables"}, {Claim: "node-packet-filter", Scope: catalogue.ScopeNode, Node: "anchor", Module: "nftables"},
// Resolved twice, reported once: a machine is one holder however many passes saw it. // Resolved twice, reported once: a machine is one holder however many passes saw it.
{Claim: "the-packet-filter", Scope: catalogue.ScopeNode, Node: "anchor", Module: "nftables"}, {Claim: "node-packet-filter", Scope: catalogue.ScopeNode, Node: "anchor", Module: "nftables"},
}) })
for _, r := range rows { for _, r := range rows {
if r.Seat != "the-packet-filter" { if r.Seat != "node-packet-filter" {
continue continue
} }
if len(r.Holders) != 2 || r.Holders[0].Node != "anchor" || r.Holders[1].Node != "node2" { if len(r.Holders) != 2 || r.Holders[0].Node != "anchor" || r.Holders[1].Node != "node2" {
+8
View File
@@ -192,6 +192,14 @@ type Manifest struct {
// that every new module would force its predecessors to update. // that every new module would force its predecessors to update.
Claims []Claim `json:"claims,omitempty"` Claims []Claim `json:"claims,omitempty"`
// DefinesSeats are the seats this module defines for itself (novox/hq ADR 0121). The control
// plane defines the system seats — `mesh-*` and `node-*` — and a module may define its own,
// named outside that namespace, to coordinate its own instances: the mesh enforces
// one-holder-per-scope for it without knowing what it means. A module's declared seat is the
// only non-system name it may then claim; a claim to a name neither the mesh nor the module
// defines is refused.
DefinesSeats []Claim `json:"seats,omitempty"`
// Capabilities the machine must have. A different field from Requires because the remedy // Capabilities the machine must have. A different field from Requires because the remedy
// differs: a missing module can be assigned, and a missing capability means the wrong // differs: a missing module can be assigned, and a missing capability means the wrong
// machine. // machine.
@@ -170,7 +170,7 @@ func TestTwoThingsDecidingWhatAMachineAsksAreRefused(t *testing.T) {
if err == nil { if err == nil {
t.Fatal("resolv-conf and resolved-split-dns were both assigned to one machine") t.Fatal("resolv-conf and resolved-split-dns were both assigned to one machine")
} }
if !strings.Contains(err.Error(), "the-resolver-configuration") { if !strings.Contains(err.Error(), "node-resolver-config") {
t.Fatalf("the refusal does not say what was claimed: %v", err) t.Fatalf("the refusal does not say what was claimed: %v", err)
} }
} }
+67 -24
View File
@@ -37,23 +37,36 @@ var seats = []Seat{
{Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079"}, {Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079"},
{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "amqp", Decision: "novox/hq ADR 0079"}, {Name: "mesh-broker", Scope: ScopeMesh, Delivers: "amqp", Decision: "novox/hq ADR 0079"},
{Name: "the-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"}, {Name: "the-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"},
{Name: "the-catalogue", Scope: ScopeMesh, Decision: "novox/hq ADR 0110"}, {Name: "mesh-catalog", Scope: ScopeMesh, Decision: "novox/hq ADR 0121"},
// Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a
// delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight.
// They keep their names until that migration is done deliberately, apart from the node-* pass.
{Name: "npm-package-registry", Scope: ScopeMesh, Delivers: "npm-package-registry", Decision: "novox/hq ADR 0109"}, {Name: "npm-package-registry", Scope: ScopeMesh, Delivers: "npm-package-registry", Decision: "novox/hq ADR 0109"},
{Name: "git", Scope: ScopeMesh, Delivers: "git", Decision: "novox/hq ADR 0111"}, {Name: "git", Scope: ScopeMesh, Delivers: "git", Decision: "novox/hq ADR 0111"},
{Name: "the-build-machine", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, {Name: "mesh-build-machine", Scope: ScopeMesh, Decision: "novox/hq ADR 0121"},
{Name: "the-dns-port", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, {Name: "node-dns-resolver", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
{Name: "the-intrusion-prevention", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, {Name: "node-intrusion-prevention", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
{Name: "the-packet-filter", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, {Name: "node-packet-filter", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
// Deferred (novox/hq ADR 0121): renaming to mesh-private-network is a scope + server/client
// model change, not a rename, so it stays until that is built.
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, {Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
{Name: "the-resolver-configuration", Scope: ScopeNode, Decision: "novox/hq ADR 0110"}, {Name: "node-resolver-config", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
{Name: "the-showcase", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
// The program that manages the machine's own network. It delivers nothing: its holder only // The program that manages the machine's own network. It delivers nothing: its holder only
// keeps the manager and the mesh from contradicting each other — the resolver file left to the // keeps the manager and the mesh from contradicting each other — the resolver file left to the
// mesh, the private network's interface left alone — and never declares a link, an address or // mesh, the private network's interface left alone — and never declares a link, an address or
// a wireless network, because the link is the only channel a fix could arrive on. A seat // a wireless network, because the link is the only channel a fix could arrive on. A seat
// rather than a condition in the resolver's module, so a machine running two managers is // rather than a condition in the resolver's module, so a machine running two managers is
// refused at assignment instead of found by the resolver being rewritten (novox/hq ADR 0117). // refused at assignment instead of found by the resolver being rewritten (novox/hq ADR 0117).
{Name: "the-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117"}, {Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117"},
}
// A system seat name is the control plane's namespace: `mesh-*` for a mesh-wide role, `node-*` for
// a per-node one (novox/hq ADR 0121). A claim to a system name the mesh does not define is refused;
// any other name is a module's own to define and claim. Some of the mesh's own seats predate this
// convention and are not yet renamed (git, npm-package-registry, the-artifact-store,
// the-private-network) — those are in the set, so they resolve by name, not by prefix.
func isSystemSeatName(name string) bool {
return strings.HasPrefix(name, "mesh-") || strings.HasPrefix(name, "node-")
} }
// Seats is every seat the mesh defines, in reading order. // Seats is every seat the mesh defines, in reading order.
@@ -84,30 +97,60 @@ func SeatDelivering(provision string) (Seat, bool) {
return Seat{}, false return Seat{}, false
} }
// claimProblems is what is wrong with a manifest's claims against the set. // claimProblems is what is wrong with a manifest's claims and the seats it defines.
// //
// Three refusals, each naming the seat: a seat the mesh does not define, a seat claimed at another // A claim is one of three things (novox/hq ADR 0121): a **system seat** the control plane defines —
// scope, and a seat that delivers a provision claimed by a module that does not provide it — which // checked for scope and, if it delivers a provision, that the claimant provides it; a **system name
// would make the module the mesh's answer for something it cannot answer. // the mesh does not define** (`mesh-*`/`node-*`) — refused, because that namespace is the control
// plane's; or a **module-defined seat** — valid only when this manifest also declares it, since a
// module may coordinate its own instances through a seat of its own but may not invent one by
// claiming it. A module's own seat declaration may not sit in the system namespace or shadow a
// system seat.
func claimProblems(m Manifest) []string { func claimProblems(m Manifest) []string {
var problems []string var problems []string
for _, c := range m.Claims {
seat, known := SeatNamed(c.Name) defined := map[string]Claim{}
if !known { for _, d := range m.DefinesSeats {
if _, isSystem := SeatNamed(d.Name); isSystem || isSystemSeatName(d.Name) {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s claims %q, which is not a seat this mesh defines (novox/hq ADR 0110) — "+ "%s defines a seat %q in the mesh's own namespace; a module's seat is named outside "+
"the seats are: %s", m.Module, c.Name, seatNames())) "mesh-*/node-* (novox/hq ADR 0121)", m.Module, d.Name))
continue continue
} }
if c.At() != seat.Scope { defined[d.Name] = d
problems = append(problems, fmt.Sprintf( }
"%s claims %s at scope %q, and %s is a %s seat",
m.Module, c.Name, c.At(), c.Name, seat.Scope)) for _, c := range m.Claims {
if seat, known := SeatNamed(c.Name); known {
if c.At() != seat.Scope {
problems = append(problems, fmt.Sprintf(
"%s claims %s at scope %q, and %s is a %s seat",
m.Module, c.Name, c.At(), c.Name, seat.Scope))
}
if seat.Delivers != "" && !providesAt(m, seat.Delivers, seat.Scope) {
problems = append(problems, fmt.Sprintf(
"%s claims %s, whose holder answers for %q, and %s does not provide %q at %s scope",
m.Module, c.Name, seat.Delivers, m.Module, seat.Delivers, seat.Scope))
}
continue
} }
if seat.Delivers != "" && !providesAt(m, seat.Delivers, seat.Scope) { if isSystemSeatName(c.Name) {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s claims %s, whose holder answers for %q, and %s does not provide %q at %s scope", "%s claims %q, which is a seat in the mesh's own namespace (mesh-*/node-*) that it "+
m.Module, c.Name, seat.Delivers, m.Module, seat.Delivers, seat.Scope)) "does not define (novox/hq ADR 0121) — the seats are: %s", m.Module, c.Name, seatNames()))
continue
}
d, ours := defined[c.Name]
if !ours {
problems = append(problems, fmt.Sprintf(
"%s claims %q, which is not a seat this mesh defines and not one %s declares itself "+
"(novox/hq ADR 0121) — the seats are: %s", m.Module, c.Name, m.Module, seatNames()))
continue
}
if c.At() != d.At() {
problems = append(problems, fmt.Sprintf(
"%s claims its own seat %s at scope %q, having declared it at %q",
m.Module, c.Name, c.At(), d.At()))
} }
} }
return problems return problems
+25 -5
View File
@@ -44,8 +44,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
delivered[s.Delivers] = s.Name delivered[s.Delivers] = s.Name
} }
} }
if len(Seats()) != 15 { if len(Seats()) != 14 {
t.Errorf("the mesh defines %d seats rather than 15; the set is closed, so a change here is "+ t.Errorf("the mesh defines %d seats rather than 14; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames()) "a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
} }
} }
@@ -56,7 +56,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
// contradicting the mesh; a requirement resolving to it would make the manager the mesh's answer // contradicting the mesh; a requirement resolving to it would make the manager the mesh's answer
// for something, and the manager's link is the one thing the mesh must never be able to break. // for something, and the manager's link is the one thing the mesh must never be able to break.
func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) { func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) {
seat, known := SeatNamed("the-uplink") seat, known := SeatNamed("node-uplink")
if !known { if !known {
t.Fatalf("the uplink is not a seat; the seats are: %s", seatNames()) t.Fatalf("the uplink is not a seat; the seats are: %s", seatNames())
} }
@@ -64,7 +64,7 @@ func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) {
t.Fatalf("the uplink is %+v, not a node seat delivering nothing by ADR 0117", seat) t.Fatalf("the uplink is %+v, not a node seat delivering nothing by ADR 0117", seat)
} }
// And a manager's module can hold it without providing anything. // And a manager's module can hold it without providing anything.
raw := []byte(`{"module":"networkmanager","version":"1","claims":[{"name":"the-uplink","scope":"node"}]}`) raw := []byte(`{"module":"networkmanager","version":"1","claims":[{"name":"node-uplink","scope":"node"}]}`)
if _, err := ParseManifest(raw); err != nil { if _, err := ParseManifest(raw); err != nil {
t.Fatalf("a network manager's module could not hold the uplink: %v", err) t.Fatalf("a network manager's module could not hold the uplink: %v", err)
} }
@@ -83,11 +83,31 @@ func TestAClaimOnASeatTheMeshDoesNotDefineIsRefused(t *testing.T) {
t.Fatalf("the refusal does not say the seat is unknown: %v", err) t.Fatalf("the refusal does not say the seat is unknown: %v", err)
} }
// And it says what the seats are, because "no" without the list sends somebody reading code. // And it says what the seats are, because "no" without the list sends somebody reading code.
if !strings.Contains(err.Error(), "the-packet-filter") { if !strings.Contains(err.Error(), "node-packet-filter") {
t.Fatalf("the refusal does not list the seats: %v", err) t.Fatalf("the refusal does not list the seats: %v", err)
} }
} }
// A module may define its own seat and claim it — the mesh enforces exclusivity without knowing
// what it means (novox/hq ADR 0121). But it may not define one in the mesh's own namespace.
func TestAModuleDefinesAndClaimsItsOwnSeat(t *testing.T) {
ok := []byte(`{"module":"showcase","version":"1","seats":[{"name":"the-showcase","scope":"node"}],` +
`"claims":[{"name":"the-showcase","scope":"node"}]}`)
if _, err := ParseManifest(ok); err != nil {
t.Fatalf("a module could not define and claim its own seat: %v", err)
}
// Claiming a name it neither the mesh nor the module defines is still refused.
if _, err := ParseManifest(claimed(`[{"name":"the-anything","scope":"node"}]`)); err == nil {
t.Fatal("a module claimed a seat nobody defines")
}
// A module may not carve its seat out of the mesh's own namespace.
bad := []byte(`{"module":"x","version":"1","seats":[{"name":"node-mine","scope":"node"}],` +
`"claims":[{"name":"node-mine","scope":"node"}]}`)
if _, err := ParseManifest(bad); err == nil || !strings.Contains(err.Error(), "own namespace") {
t.Fatalf("a module defined a seat in the mesh's namespace and was not refused: %v", err)
}
}
func TestASeatClaimedAtAnotherScopeIsRefused(t *testing.T) { func TestASeatClaimedAtAnotherScopeIsRefused(t *testing.T) {
_, err := ParseManifest(claimed(`[{"name":"npm-package-registry","scope":"node"}]`)) _, err := ParseManifest(claimed(`[{"name":"npm-package-registry","scope":"node"}]`))
if err == nil { if err == nil {