Compare commits

...
Author SHA1 Message Date
jochen 3e9a38f81b 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.
2026-10-07 20:18:05 +02:00
jochen be8a9fd00f Ask the merge check again: the first run's suite hit its 30-minute bound on a loaded build machine
check-here at the same commit passed in the mesh's toolchain (merge-gate 4 of 4, repo-check PASS).
2026-10-07 20:16:16 +02:00
jochen 161e676b97 Give the uplink seat its verbs, staged until its holders serve them (hq ADR 0241)
What a machine resolves through and over which links had no tool: the
resolver file and its writer, and each link with its routes and resolvers,
are now verbs of node-uplink, the same whatever manages the network. A verb
required at once would refuse every holder before the catalogue could serve
it, and a stored one would be required by an older controller, so it is
staged: promised and routed, never stored, and not yet a condition of
holding.
2026-10-07 20:16:16 +02:00
5 changed files with 177 additions and 2 deletions
+54 -1
View File
@@ -281,7 +281,12 @@ var defaultSeats = append([]Seat{
// channel a fix could arrive on. A seat // 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: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117"}, //
// 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. 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. // The graphical session's roles (novox/hq ADR 0208), last because they are a workstation's.
graphicalSessionSeats()...) graphicalSessionSeats()...)
@@ -338,6 +343,26 @@ func UseSeats(s []Seat) {
if d, known := byName[row.Name]; known { if d, known := byName[row.Name]; known {
row.Receives = d.Receives row.Receives = d.Receives
row.Replicated = d.Replicated row.Replicated = d.Replicated
// 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 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 v.Optional && !have[v.Name] {
row.Serves = append(row.Serves, v)
}
}
} }
merged = append(merged, row) merged = append(merged, row)
} }
@@ -591,6 +616,34 @@ func serviceManagerVerbs() []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 !v.Optional {
kept = append(kept, v)
}
}
s.Serves = kept
return s
}
// uplinkVerbs is the contract every holder of node-uplink serves (novox/hq ADR 0241): read-only answers
// about what the machine resolves through and over which links, whatever program manages its network.
func uplinkVerbs() []Verb {
return []Verb{
{Name: "resolvers", Description: "The machine's resolver file as it is now: the resolvers, search " +
"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), 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), Optional: true},
}
}
// loginShellVerbs is the contract every holder of node-login-shell serves (novox/hq ADR 0176, ADR // loginShellVerbs is the contract every holder of node-login-shell serves (novox/hq ADR 0176, ADR
// 0204): one command, run the way the operator's own terminal would run it, bounded below the // 0204): one command, run the way the operator's own terminal would run it, bounded below the
// runtime's thirty-second call limit so a hung command answers rather than times the caller out. // runtime's thirty-second call limit so a hung command answers rather than times the caller out.
+93
View File
@@ -0,0 +1,93 @@
package catalogue
import (
"slices"
"strings"
"testing"
)
// 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()
for _, s := range DefaultSeats() {
if s.Name == "node-uplink" {
return s
}
}
t.Fatal("no node-uplink seat")
return Seat{}
}
func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) {
seat := uplinkSeat(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"}) {
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 {
t.Fatalf("a holder that serves no verb yet is refused: %v", err)
}
serving := today
serving.Claims = []Claim{{Name: "node-uplink", Scope: ScopeNode, Serves: []string{"resolvers", "links"}}}
if err := CanHold(serving, seat); err != nil {
t.Fatalf("a holder serving both is refused: %v", err)
}
typo := today
typo.Claims = []Claim{{Name: "node-uplink", Scope: ScopeNode, Serves: []string{"resolver"}}}
if err := CanHold(typo, seat); err == nil || !strings.Contains(err.Error(), "does not promise") {
t.Fatalf("a verb the seat does not promise was accepted: %v", err)
}
}
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 })
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}})
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"}}}})
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 // 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 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 // 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:"-"` Optional bool `json:"-"`
} }
+2
View File
@@ -62,6 +62,8 @@ func (i *Inventory) Seats(ctx context.Context) ([]catalogue.Seat, error) {
func (i *Inventory) SeedSeats(ctx context.Context, defaults []catalogue.Seat) (int, error) { func (i *Inventory) SeedSeats(ctx context.Context, defaults []catalogue.Seat) (int, error) {
var added int var added int
for _, s := range defaults { for _, s := range defaults {
// An optional verb is never stored (catalogue.Verb.Optional).
s = s.Stored()
accepts, emits, serves, err := protocolJSON(s) accepts, emits, serves, err := protocolJSON(s)
if err != nil { if err != nil {
return added, err return added, err
+25
View File
@@ -156,3 +156,28 @@ func TestTheControllersVerbsInTheRowAreTheBinarys(t *testing.T) {
t.Fatal("a verb this binary adds was not added") t.Fatal("a verb this binary adds was not added")
} }
} }
// 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)
}
// Seeded twice: the second widens, and must not add it either.
if _, err := inv.SeedSeats(t.Context(), catalogue.DefaultSeats()); err != nil {
t.Fatal(err)
}
got, err := inv.Seats(t.Context())
if err != nil {
t.Fatal(err)
}
for _, s := range got {
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)
}
}
}
}