Mark the uplink seat's verbs optional, the one mechanism, and keep every optional verb out of the seat row
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/node-uplink-verbs ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request

Staged duplicated Verb.Optional. Optional, though, was still seeded into the row,
where the mark is not kept: read back, the delivery seat's checks verb and these
would be required of every holder, and an older controller would require them
too. Stored leaves optional verbs out, and the working set takes them from the
compiled seat, still optional.
This commit is contained in:
jochen
2026-10-07 20:18:05 +02:00
parent be8a9fd00f
commit 3e9a38f81b
5 changed files with 76 additions and 52 deletions
+24 -30
View File
@@ -2,7 +2,6 @@ package catalogue
import (
"fmt"
"slices"
"sort"
"strings"
)
@@ -54,14 +53,6 @@ type Seat struct {
// ${contribution:<seat>:<kind>}. Compiled, never stored: like the protocol, it is the mesh's
// definition of the role, and the store's rows carry no column for it.
Receives []Receivable
// Staged names verbs of Serves that are promised and routed but not yet a condition of holding the
// seat: a verb added to a seat whose holders do not serve it yet. Holding requires every verb, and a
// claim may name only promised ones (ADR 0132), so a verb required at once would refuse every holder
// until the catalogue serves it — and the catalogue could not name it until this controller ran. Staged,
// the controller rolls out first, the holders serve the verb next, and a later change requires it.
// Compiled, never stored, and kept out of the stored row's serves: a controller older than the verb
// reads the row strictly and would require what no holder of its time serves.
Staged []string
// Decision is the record that made it a seat.
Decision string
}
@@ -294,9 +285,8 @@ var defaultSeats = append([]Seat{
// Its verbs say what the machine resolves through and over which links (novox/hq ADR 0241): the
// resolver file as it is and who wrote it, and every link with its addresses, routes and the resolvers
// the manager knows for it — the same for every holder, so a question about a machine's names is asked
// the same way whatever manages its network. Staged until both holders serve them.
{Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117",
Serves: uplinkVerbs(), Staged: []string{"resolvers", "links"}},
// the same way whatever manages its network. Optional until both holders serve them.
{Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117", Serves: uplinkVerbs()},
},
// The graphical session's roles (novox/hq ADR 0208), last because they are a workstation's.
graphicalSessionSeats()...)
@@ -353,14 +343,23 @@ func UseSeats(s []Seat) {
if d, known := byName[row.Name]; known {
row.Receives = d.Receives
row.Replicated = d.Replicated
// Staged verbs are never in the row: they come from the binary that promises them.
row.Staged = d.Staged
// Optional verbs are never in the row (Verb.Optional): they come from the binary that promises
// them, still optional; and a row kept from before the mark keeps none of its own required.
optional := map[string]Verb{}
for _, v := range d.Serves {
if v.Optional {
optional[v.Name] = v
}
}
have := map[string]bool{}
for _, v := range row.Serves {
for i, v := range row.Serves {
have[v.Name] = true
if o, ok := optional[v.Name]; ok {
row.Serves[i] = o
}
}
for _, v := range d.Serves {
if slices.Contains(d.Staged, v.Name) && !have[v.Name] {
if v.Optional && !have[v.Name] {
row.Serves = append(row.Serves, v)
}
}
@@ -503,7 +502,7 @@ func CanHold(m Manifest, seat Seat) error {
// **Serving the seat's tools is a condition of holding it** (novox/hq ADR 0132). A holder that
// does not answer what the role promises is every caller's timeout, found at registration and
// at handover instead, naming the verbs rather than the fact that something is missing.
if missing := unservedVerbs(claimed.ServesFor(m), seat.Required()); len(missing) > 0 {
if missing := unservedVerbs(claimed.ServesFor(m), seat.Serves); len(missing) > 0 {
return fmt.Errorf("%s claims %s but does not serve %s, which that seat's protocol promises "+
"(novox/hq ADR 0132) — a holder names every verb its seat declares, under the claim's "+
"serves or among its own tools",
@@ -617,20 +616,15 @@ func serviceManagerVerbs() []Verb {
}
}
// Required is the verbs a holder must serve: every one the seat promises but those still staged.
func (s Seat) Required() []Verb {
var out []Verb
// Stored is the seat as its row keeps it: its optional verbs left out (Verb.Optional).
func (s Seat) Stored() Seat {
var kept []Verb
for _, v := range s.Serves {
if !slices.Contains(s.Staged, v.Name) {
out = append(out, v)
if !v.Optional {
kept = append(kept, v)
}
}
return out
}
// Stored is the seat as its row keeps it: its staged verbs left out (see Staged).
func (s Seat) Stored() Seat {
s.Serves = s.Required()
s.Serves = kept
return s
}
@@ -642,11 +636,11 @@ func uplinkVerbs() []Verb {
"domains and options it lists, whether it is the file the mesh declares, and when it is not, who " +
"wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link " +
"in its place, a known writer running.",
Input: schema(nil, nil)},
Input: schema(nil, nil), Optional: true},
{Name: "links", Description: "Every network link on the machine: its state, its addresses, whether " +
"the default route leaves through it, and the resolvers and search domains the network manager " +
"knows for it — a VPN's tunnel included.",
Input: schema(nil, nil)},
Input: schema(nil, nil), Optional: true},
}
}
+40 -15
View File
@@ -6,8 +6,10 @@ import (
"testing"
)
// The node-uplink seat's verbs (novox/hq ADR 0241): promised and routed, staged — not yet a condition of
// The node-uplink seat's verbs (novox/hq ADR 0241): promised and routed, optional — not yet a condition of
// holding — so the holders of today still hold it, a holder may name them, and nothing it does not promise.
// And an optional verb is never stored in the seat's row (Verb.Optional): a controller that reads the row
// back must still find it optional, and one older than the verb must not find it at all.
func uplinkSeat(t *testing.T) Seat {
t.Helper()
@@ -25,9 +27,12 @@ func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) {
var names []string
for _, v := range seat.Serves {
names = append(names, v.Name)
if !v.Optional {
t.Errorf("%s is required already", v.Name)
}
}
if !slices.Equal(names, []string{"resolvers", "links"}) || len(seat.Required()) != 0 {
t.Fatalf("node-uplink promises %v and requires %v", names, seat.Required())
if !slices.Equal(names, []string{"resolvers", "links"}) {
t.Fatalf("node-uplink promises %v", names)
}
today := Manifest{Module: "networkmanager", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}}}
if err := CanHold(today, seat); err != nil {
@@ -45,24 +50,44 @@ func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) {
}
}
func TestAStagedVerbIsNeverStoredAndComesBackFromTheBinary(t *testing.T) {
func TestAnOptionalVerbIsNeverStoredAndComesBackOptionalFromTheBinary(t *testing.T) {
seat := uplinkSeat(t)
if stored := seat.Stored(); len(stored.Serves) != 0 {
t.Fatalf("the row would keep %v", stored.Serves)
}
before := seats
t.Cleanup(func() { seats = before })
// A row as the store keeps it: no protocol of its own. And one an operator gave a verb of its own.
optionalBoth := func(want int) {
t.Helper()
for _, s := range Seats() {
if s.Name != "node-uplink" {
continue
}
if len(s.Serves) != want {
t.Fatalf("the working set's node-uplink serves %+v", s.Serves)
}
holder := Manifest{Module: "networkmanager", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}}}
for _, v := range s.Serves {
if v.Name != "operators-own" {
if !v.Optional {
t.Fatalf("%s came back required", v.Name)
}
} else {
holder.Claims[0].Serves = []string{"operators-own"}
}
}
if err := CanHold(holder, s); err != nil {
t.Fatalf("a holder serving none of the optional verbs is refused: %v", err)
}
}
}
// A row as the store keeps it: no protocol of its own.
UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode}, {Name: "node-service-manager", Scope: ScopeNode}})
for _, s := range Seats() {
if s.Name == "node-uplink" && (len(s.Serves) != 2 || len(s.Staged) != 2) {
t.Fatalf("the working set's node-uplink is %+v", s)
}
}
optionalBoth(2)
// A row with a verb an operator added: it keeps it and gains the optional ones.
UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode, Serves: []Verb{{Name: "operators-own"}}}})
for _, s := range Seats() {
if s.Name == "node-uplink" && len(s.Serves) != 3 {
t.Fatalf("a row with a verb of its own lost or did not gain the staged ones: %+v", s.Serves)
}
}
optionalBoth(3)
// A row that stored the verb before it was marked (or by an older seeding): read back, still optional.
UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode, Serves: []Verb{{Name: "resolvers"}, {Name: "links"}}}})
optionalBoth(2)
}
+3 -1
View File
@@ -28,7 +28,9 @@ type Verb struct {
// does not serve the verb, the controller before it refusing the holder that does — and every check of
// the catalogue between the two would fail on a module nobody touched. Once every holder serves it,
// the mark is removed and the verb is a condition of holding like the rest. Never stored or said: the
// seat set's protocol is the compiled one.
// seat's row leaves an optional verb out (Seat.Stored) and the working set takes it, still optional,
// from the compiled seat (UseSeats) — the mark itself is not a column, so a stored verb would read back
// required, and a controller older than the verb would require it of every holder (novox/hq ADR 0241).
Optional bool `json:"-"`
}
+1 -1
View File
@@ -62,7 +62,7 @@ func (i *Inventory) Seats(ctx context.Context) ([]catalogue.Seat, error) {
func (i *Inventory) SeedSeats(ctx context.Context, defaults []catalogue.Seat) (int, error) {
var added int
for _, s := range defaults {
// A verb still staged is never stored (catalogue.Seat.Staged).
// An optional verb is never stored (catalogue.Verb.Optional).
s = s.Stored()
accepts, emits, serves, err := protocolJSON(s)
if err != nil {
+8 -5
View File
@@ -157,9 +157,10 @@ func TestTheControllersVerbsInTheRowAreTheBinarys(t *testing.T) {
}
}
// A verb still staged (novox/hq ADR 0241) is never in the store's row: a controller older than it reads
// the row and would require what no holder of its time serves.
func TestAStagedVerbIsNotSeeded(t *testing.T) {
// An optional verb (catalogue.Verb.Optional; novox/hq ADR 0241, ADR 0239) is never in the store's row: a
// controller older than it reads the row and would require what no holder of its time serves, and the mark
// itself is not stored, so the row would read back required.
func TestAnOptionalVerbIsNotSeeded(t *testing.T) {
inv := ForTest(t)
if _, err := inv.SeedSeats(t.Context(), catalogue.DefaultSeats()); err != nil {
t.Fatal(err)
@@ -173,8 +174,10 @@ func TestAStagedVerbIsNotSeeded(t *testing.T) {
t.Fatal(err)
}
for _, s := range got {
if s.Name == "node-uplink" && len(s.Serves) != 0 {
t.Fatalf("the row keeps %v", s.Serves)
for _, v := range s.Serves {
if (s.Name == "node-uplink") || (s.Name == catalogue.DeliverySeat && v.Name == "checks") {
t.Fatalf("%s's row keeps the optional %s", s.Name, v.Name)
}
}
}
}