From 5108c28e2241e7d6bced132fa5437cb56cd76846 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 15:31:32 +0100 Subject: [PATCH 1/2] The node-engine composes seat passes with the SDK's one rule instead of its own copy (issue 1966, ADR 0660) Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- internal/compose/resolution/seat_pass.go | 74 ----------------- internal/compose/resolution/seat_pass_test.go | 80 ------------------- internal/compose/rest.go | 6 +- internal/compose/rest_test.go | 39 ++++++++- 4 files changed, 40 insertions(+), 159 deletions(-) delete mode 100644 internal/compose/resolution/seat_pass.go delete mode 100644 internal/compose/resolution/seat_pass_test.go diff --git a/internal/compose/resolution/seat_pass.go b/internal/compose/resolution/seat_pass.go deleted file mode 100644 index 5f80658..0000000 --- a/internal/compose/resolution/seat_pass.go +++ /dev/null @@ -1,74 +0,0 @@ -package resolution - -import ( - "fmt" - "sort" - - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// SeatsToPassOn reads the mesh for seats held once whose holder on record has left (novox/hq ADR 0449 §4). -// -// **A holder on record that is assigned on no machine holds nothing, and a seat that answers nothing is -// the day a delivery removed its module.** Of such a seat, exactly one assignment in the mesh able to hold -// it (it claims the seat at its scope and CanHold accepts it) takes it over: that is a pass, and the caller -// records it and writes the hand act. With no such assignment the record stays; with two or more nothing is -// recorded and the answer says so, naming both and the `seat --to` that settles it. A holder still assigned -// anywhere changes nothing: the other claimants stand beside it, eligible and silent (ADR 0131). Only mesh -// seats held once are read: a replicated seat's holders are added by an act (ADR 0223), a kinded bench is held -// by its claims, and a seat with several holders on record is a store that disagrees with the mesh (refused -// by the resolver). -func SeatsToPassOn(holdings []mcat.Held, assignments []mcat.Assignment) ([]mcat.SeatPass, []string) { - assigned := map[string]bool{} - for _, a := range assignments { - assigned[a.Manifest.Module] = true - } - bySeat := map[string][]mcat.Held{} - var order []string - for _, h := range holdings { - s, ok := mcat.SeatNamed(h.Claim) - if !ok || s.Replicated || s.Scope != mcat.ScopeMesh || h.Scope != mcat.ScopeMesh || mcat.KindedBenches[s.Name] { - continue - } - if _, seen := bySeat[s.Name]; !seen { - order = append(order, s.Name) - } - bySeat[s.Name] = append(bySeat[s.Name], h) - } - sort.Strings(order) - - var passes []mcat.SeatPass - var said []string - for _, name := range order { - recs := bySeat[name] - if len(recs) != 1 || assigned[recs[0].Module] { - continue - } - seat, _ := mcat.SeatNamed(name) - var claimants []mcat.Held - for _, a := range assignments { - if mcat.CanHold(a.Manifest, seat) != nil { - continue - } - claimants = append(claimants, mcat.Held{Claim: seat.Name, Scope: seat.Scope, Node: a.Node, Module: a.Manifest.Module}) - } - sort.Slice(claimants, func(i, j int) bool { - if claimants[i].Node != claimants[j].Node { - return claimants[i].Node < claimants[j].Node - } - return claimants[i].Module < claimants[j].Module - }) - switch len(claimants) { - case 0: - case 1: - passes = append(passes, mcat.SeatPass{Seat: seat, From: recs[0], To: claimants[0]}) - default: - said = append(said, fmt.Sprintf( - "%s on %s is on record as the holder of %q and is assigned nowhere, and %s on %s and %s on %s both claim it "+ - "— nothing is recorded; `seat %s --to /` records the one that holds it", - recs[0].Module, recs[0].Node, seat.Name, claimants[0].Module, claimants[0].Node, - claimants[1].Module, claimants[1].Node, seat.Name)) - } - } - return passes, said -} diff --git a/internal/compose/resolution/seat_pass_test.go b/internal/compose/resolution/seat_pass_test.go deleted file mode 100644 index 347defd..0000000 --- a/internal/compose/resolution/seat_pass_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package resolution - -import ( - "strings" - "testing" - - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -func onRecord(node, module string) []mcat.Held { - return []mcat.Held{{Claim: "mesh-broker", Scope: mcat.ScopeMesh, Node: node, Module: module}} -} - -// 1317/AC1 -func TestASeatWhoseHolderIsAssignedNowhereIsPassedToItsOnlyClaimant(t *testing.T) { - busSeatDelivering(t, "mesh-bus") - passes, said := SeatsToPassOn(onRecord("novox", "old-broker"), - []mcat.Assignment{{Node: "novox", Manifest: newBroker()}}) - if len(said) != 0 || len(passes) != 1 { - t.Fatalf("the only claimant was not given the seat: passes=%v said=%v", passes, said) - } - p := passes[0] - if p.Seat.Name != "mesh-broker" || p.From.Module != "old-broker" || p.From.Node != "novox" || - p.To.Module != "new-broker" || p.To.Node != "novox" { - t.Fatalf("the pass does not name the seat, the former holder and the new one: %+v", p) - } -} - -// 1317/AC2 -func TestWithTwoClaimantsAndTheHolderGoneNothingIsPassedAndTheRefusalNamesBoth(t *testing.T) { - busSeatDelivering(t, "mesh-bus") - passes, said := SeatsToPassOn(onRecord("novox", "gone"), []mcat.Assignment{ - {Node: "novox", Manifest: newBroker()}, {Node: "ace", Manifest: oldBroker()}}) - if len(passes) != 0 || len(said) != 1 { - t.Fatalf("two claimants: passes=%v said=%v", passes, said) - } - for _, want := range []string{"old-broker", "new-broker", "seat mesh-broker --to"} { - if !strings.Contains(said[0], want) { - t.Fatalf("the refusal does not say %q: %s", want, said[0]) - } - } -} - -// 1317/AC3 -func TestAHolderStillAssignedSomewhereChangesNothing(t *testing.T) { - busSeatDelivering(t, "mesh-bus") - // Assigned on another machine than the record names: still assigned somewhere. - passes, said := SeatsToPassOn(onRecord("novox", "old-broker"), []mcat.Assignment{ - {Node: "ace", Manifest: oldBroker()}, {Node: "novox", Manifest: newBroker()}}) - if len(passes) != 0 || len(said) != 0 { - t.Fatalf("a holder still assigned lost its seat: passes=%v said=%v", passes, said) - } -} - -// 1317/AC4 -func TestWithNoClaimantTheRecordStays(t *testing.T) { - busSeatDelivering(t, "mesh-bus") - passes, said := SeatsToPassOn(onRecord("novox", "old-broker"), []mcat.Assignment{ - {Node: "novox", Manifest: mcat.Manifest{Module: "other"}}}) - if len(passes) != 0 || len(said) != 0 { - t.Fatalf("with no claimant: passes=%v said=%v", passes, said) - } - // A claimant that cannot hold the seat (it does not provide what the seat delivers) is no claimant. - bad := mcat.Manifest{Module: "no-bus", Claims: []mcat.Claim{{Name: "mesh-broker", Scope: mcat.ScopeMesh}}} - passes, said = SeatsToPassOn(onRecord("novox", "old-broker"), []mcat.Assignment{{Node: "novox", Manifest: bad}}) - if len(passes) != 0 || len(said) != 0 { - t.Fatalf("a module that cannot hold the seat was passed it: passes=%v said=%v", passes, said) - } -} - -// A replicated seat is never passed: its holders are added by an act (ADR 0223). -func TestAReplicatedSeatIsNeverPassed(t *testing.T) { - record := []mcat.Held{{Claim: "mesh-dns-resolver", Scope: mcat.ScopeMesh, Node: "novox", Module: "gone"}} - resolver := mcat.Manifest{Module: "resolver", Provides: []mcat.Offer{{Name: "wildcard-resolution", Scope: mcat.ScopeMesh}}, - Claims: []mcat.Claim{{Name: "mesh-dns-resolver", Scope: mcat.ScopeMesh}}} - passes, _ := SeatsToPassOn(record, []mcat.Assignment{{Node: "novox", Manifest: resolver}}) - if len(passes) != 0 { - t.Fatalf("a replicated seat was passed: %v", passes) - } -} diff --git a/internal/compose/rest.go b/internal/compose/rest.go index c2e4708..002820e 100644 --- a/internal/compose/rest.go +++ b/internal/compose/rest.go @@ -179,7 +179,7 @@ func (c *composer) theRestOfTheMeshNow(exclude string) (catalogue.World, Passes, // assigned nowhere and which has exactly one claimant able to hold it (novox/hq ADR 0449 clause 4), and answers the // holdings as they are after it, with the passes. Nothing is recorded. func passSeatsOn(holdings []catalogue.Held, assignments []catalogue.Assignment) ([]catalogue.Held, Passes) { - passes, said := resolution.SeatsToPassOn(holdings, assignments) + passes, said := catalogue.SeatsToPassOn(holdings, assignments) out := Passes{Said: said} for _, p := range passes { out.Passes = append(out.Passes, p) @@ -197,7 +197,7 @@ func passSeatsOn(holdings []catalogue.Held, assignments []catalogue.Assignment) } // assignmentsOf are the assignments of every machine, as the passing of a seat reads them. A module assigned and -// missing from the shelf is still assigned: an assignment with no claims, so its holder is not taken for gone. +// missing from the shelf is still assigned: an assignment marked unread, so its holder is not taken for gone. func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment { var out []catalogue.Assignment names := make([]string, 0, len(assigned)) @@ -211,7 +211,7 @@ func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]st if !ok { m = catalogue.Manifest{Module: module} } - out = append(out, catalogue.Assignment{Node: node, Manifest: m}) + out = append(out, catalogue.Assignment{Node: node, Manifest: m, Unread: !ok}) } } return out diff --git a/internal/compose/rest_test.go b/internal/compose/rest_test.go index d0b0d2b..952cc3c 100644 --- a/internal/compose/rest_test.go +++ b/internal/compose/rest_test.go @@ -6,7 +6,6 @@ import ( "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "git.novox.be/novox/mesh-sdk/go/catalogue" - "github.com/novox/mesh-host/internal/compose/resolution" ) // objects and files are the provider and consumer of mesh-controller's facts_test.go aMeshWithSecrets (cmd/mesh-controller at @@ -186,7 +185,7 @@ func TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone(t *testing.T) { withSeat(t) as := assignmentsOf(map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")}, map[string][]string{"novox": {"gate", "old-broker"}}) - passes, _ := resolution.SeatsToPassOn( + passes, _ := catalogue.SeatsToPassOn( []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}, as) if len(passes) != 0 { t.Fatalf("a holder missing from the shelf lost its seat: %+v", passes) @@ -230,3 +229,39 @@ func TestEachNodeIsResolvedWithItsOwnPinsInTheRestOfTheMesh(t *testing.T) { t.Fatalf("a pin to a provider that does not answer did not leave the consumer out: %v %v", left, err) } } + +// ADR 0660: a holder on record that is still assigned, whose manifest no longer claims the seat, has left it; the seat is +// composed with the one claimant, the pass is returned and said, and Input's holdings (the record) are not written. +func TestAHolderWhoseManifestDroppedTheSeatGoesToItsOnlyClaimant(t *testing.T) { + withSeat(t) + mk := func(claimants ...string) Input { + cat := map[string]catalogue.Manifest{"old-broker": {Module: "old-broker"}} + assigned := []string{"old-broker"} + for _, c := range claimants { + cat[c] = aBrokerClaiming(c) + assigned = append(assigned, c) + } + return Input{Catalogue: cat, + Nodes: []Node{{Name: "novox", Place: Place{Site: "here"}, Assigned: assigned}}, + Holdings: []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker", Site: "here"}}} + } + in := mk("gate") + w, passes, _, err := TheRestOfTheMesh(in, "") + if err != nil || len(passes.Passes) != 1 || passes.Passes[0].To.Module != "gate" || passes.Passes[0].From.Module != "old-broker" { + t.Fatalf("%v %+v", err, passes) + } + if len(w.Holdings) != 1 || w.Holdings[0].Module != "gate" { + t.Fatalf("the composition does not hold the pass: %+v", w.Holdings) + } + unchanged(t, func() Input { return mk("gate") }, func(in Input) { + if _, _, _, err := TheRestOfTheMesh(in, ""); err != nil { + t.Fatal(err) + } + }) + _, passes, _, err = TheRestOfTheMesh(mk("gate", "other"), "") + said := strings.Join(passes.Said, "\n") + if err != nil || len(passes.Passes) != 0 || !strings.Contains(said, "gate") || !strings.Contains(said, "other") || + !strings.Contains(said, "seat mesh-broker --to /") { + t.Fatalf("two claimants: %v %+v", err, passes) + } +} -- 2.54.0 From 60f413f9bc6ac7868663cef884a9f6f0331516fa Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 15:44:00 +0100 Subject: [PATCH 2/2] The node-engine reads each assignment by the build its node keeps and says a seat pass in its answer (issue 1966, ADR 0660) Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- internal/compose/node.go | 5 +++- internal/compose/rest.go | 47 ++++++++++++++++++++++++++--------- internal/compose/rest_test.go | 16 +++++++++++- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/internal/compose/node.go b/internal/compose/node.go index 9238ed0..bfd0408 100644 --- a/internal/compose/node.go +++ b/internal/compose/node.go @@ -60,7 +60,8 @@ type Declared struct { ForeseenPorts []string // CoreBoxesWanted are the core credentials whose box the send must ask the vault for and keep. CoreBoxesWanted []CoreBoxWanted - // Passes are the seats the mesh would pass on, for the caller to record, and Said the lines found while composing. + // Passes are the seats composed as passed to their only claimant (ADR 0660), recorded by nobody here, and Said the lines found + // while composing, the passes among them. Passes Passes Said []string } @@ -228,6 +229,8 @@ func (c *composer) declaration(nodeName string, mode Mode) (Declared, error) { return Declared{}, err } out.Passes = passes + // A pass is composed, never recorded (ADR 0660 §4): the answer says it, with what two claimants leave unsettled. + out.Said = append(out.Said, passes.Lines()...) for _, l := range left { out.Said = append(out.Said, l.String()) } diff --git a/internal/compose/rest.go b/internal/compose/rest.go index 002820e..7a073a5 100644 --- a/internal/compose/rest.go +++ b/internal/compose/rest.go @@ -25,9 +25,10 @@ func unresolvable(err error) bool { return errors.As(err, &n) } -// Passes is what a composition found that the mesh must record, and did not: each seat held once whose holder on record -// is assigned nowhere that exactly one claimant takes over (novox/hq ADR 0449 clause 4), and the lines that say what was -// found. The caller records a pass and writes its hand act; this package does neither. +// Passes is what a composition found and composed with: each seat held once whose holder on record has left it (assigned +// nowhere, or none of its assignments' manifests claims the seat) that exactly one claimant takes over (novox/hq ADR 0449 +// clause 4, widened by ADR 0660), and the lines that say what was found. A pass is composed and said, never recorded: neither +// this package nor its caller writes the holdings (ADR 0660 clause 4). type Passes struct { // Passes are the seats to pass, in the order they were found. Passes []catalogue.SeatPass @@ -92,8 +93,8 @@ func (c *composer) theRestOfTheMeshNow(exclude string) (catalogue.World, Passes, pins map[string]catalogue.Chosen } var others []candidate - // Every machine's assignments, before the holdings are read by any resolution (novox/hq ADR 0449 clause 4): a seat - // held once whose holder on record is assigned on no machine passes to its only claimant here. + // Every machine's assignments, before the holdings are read by any resolution (novox/hq ADR 0449 clause 4, ADR 0660): a + // seat held once whose holder on record has left it passes to its only claimant here, composed and not recorded. everyAssigned := map[string][]string{} for _, n := range nodes { if len(n.Assigned) > 0 { @@ -101,7 +102,7 @@ func (c *composer) theRestOfTheMeshNow(exclude string) (catalogue.World, Passes, } } var passes Passes - holdings, passes = passSeatsOn(holdings, assignmentsOf(shelf, everyAssigned)) + holdings, passes = passSeatsOn(holdings, c.assignmentsOf(shelf, everyAssigned)) for _, n := range nodes { if n.Name == exclude { continue @@ -175,8 +176,8 @@ func (c *composer) theRestOfTheMeshNow(exclude string) (catalogue.World, Passes, return world, passes, left, nil } -// passSeatsOn finds, in the composition that finds it, the new holder of each seat held once whose holder on record is -// assigned nowhere and which has exactly one claimant able to hold it (novox/hq ADR 0449 clause 4), and answers the +// passSeatsOn finds, in the composition that finds it, the new holder of each seat held once whose holder on record has left +// it and which has exactly one claimant able to hold it (catalogue.SeatsToPassOn, novox/hq ADR 0660), and answers the // holdings as they are after it, with the passes. Nothing is recorded. func passSeatsOn(holdings []catalogue.Held, assignments []catalogue.Assignment) ([]catalogue.Held, Passes) { passes, said := catalogue.SeatsToPassOn(holdings, assignments) @@ -196,9 +197,11 @@ func passSeatsOn(holdings []catalogue.Held, assignments []catalogue.Assignment) return holdings, out } -// assignmentsOf are the assignments of every machine, as the passing of a seat reads them. A module assigned and -// missing from the shelf is still assigned: an assignment marked unread, so its holder is not taken for gone. -func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment { +// assignmentsOf are the assignments of every machine, as the passing of a seat reads them (novox/hq ADR 0660 clause 2): each +// with the manifest of the build its node runs where the Input holds it (the node's Kept: what it runs of a module kept at +// that build), else the trunk's on the shelf, which for the composing node's own modules is already the build they are +// wanted at. A module assigned and missing from both is still assigned, marked unread, so its holder is not taken for gone. +func (c *composer) assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment { var out []catalogue.Assignment names := make([]string, 0, len(assigned)) for node := range assigned { @@ -206,8 +209,17 @@ func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]st } sort.Strings(names) for _, node := range names { + var kept map[string]catalogue.Manifest + for _, n := range c.in.Nodes { + if n.Name == node { + kept = n.Kept + } + } for _, module := range assigned[node] { - m, ok := shelf[module] + m, ok := kept[module] + if !ok { + m, ok = shelf[module] + } if !ok { m = catalogue.Manifest{Module: module} } @@ -217,6 +229,17 @@ func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]st return out } +// Lines are what the passes say, one each, then what the finding itself says: a pass is composed so and recorded by nobody +// (ADR 0660 clause 4). +func (p Passes) Lines() []string { + var out []string + for _, x := range p.Passes { + out = append(out, fmt.Sprintf("%s: %s on %s is on record as its holder and holds it no more; composed as held by %s on %s, not recorded", + x.Seat.Name, x.From.Module, x.From.Node, x.To.Module, x.To.Node)) + } + return append(out, p.Said...) +} + // servedOnNode is what a module on a node tells a consumer of one of its provisions, with THAT node's ports on it: the // port the node was given over the one the mesh assigned over the manifest's own, settled with the node's settings // layers. The one derivation for every reader of a provider's address. diff --git a/internal/compose/rest_test.go b/internal/compose/rest_test.go index 952cc3c..63e6d94 100644 --- a/internal/compose/rest_test.go +++ b/internal/compose/rest_test.go @@ -183,7 +183,7 @@ func TestNoSeatIsPassedForTwoClaimantsAnAssignedHolderOrNoClaimant(t *testing.T) // seat_pass_test.go TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone. func TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone(t *testing.T) { withSeat(t) - as := assignmentsOf(map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")}, + as := (&composer{}).assignmentsOf(map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")}, map[string][]string{"novox": {"gate", "old-broker"}}) passes, _ := catalogue.SeatsToPassOn( []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}, as) @@ -264,4 +264,18 @@ func TestAHolderWhoseManifestDroppedTheSeatGoesToItsOnlyClaimant(t *testing.T) { !strings.Contains(said, "seat mesh-broker --to /") { t.Fatalf("two claimants: %v %+v", err, passes) } + // The build the holder's node runs still claims the seat, though the trunk dropped it: the holder keeps it (ADR 0660 §2). + running := mk("gate") + running.Nodes[0].Kept = map[string]catalogue.Manifest{"old-broker": aBrokerClaiming("old-broker")} + _, passes, _, err = TheRestOfTheMesh(running, "") + if err != nil || len(passes.Passes) != 0 || len(passes.Said) != 0 { + t.Fatalf("a holder whose running build claims the seat lost it: %v %+v", err, passes) + } + // And a pass is said in the composition's answer, not recorded. + var d Declared + d, _, err = Compose(mk("gate"), "novox") + said = strings.Join(d.Said, "\n") + if err != nil || !strings.Contains(said, "mesh-broker: old-broker on novox") || !strings.Contains(said, "held by gate on novox, not recorded") { + t.Fatalf("the answer does not say the pass: %v %q", err, said) + } } -- 2.54.0