From 5ca4f144790eff6d97510483b16ddbcd5f00ddfc Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 15:54:45 +0200 Subject: [PATCH] =?UTF-8?q?Derive=20a=20worker=20per=20kind=20for=20the=20?= =?UTF-8?q?build=20seats=20and=20grant=20their=20users=20to=20put=20asks,?= =?UTF-8?q?=20so=20a=20build=20node=20pulls=20delivery,=20check=20and=20re?= =?UTF-8?q?check=20in=20order=20and=20a=20session=20cannot=20ask=20(issue?= =?UTF-8?q?=201887=20A6,=20ADR=200525=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/broker/derived.go | 6 +++- internal/broker/nats.go | 3 ++ internal/broker/seattraffic.go | 11 ++++++-- internal/broker/seattraffic_test.go | 44 +++++++++++++++++++++++++++++ internal/inventory/busrecords.go | 7 ++++- 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/internal/broker/derived.go b/internal/broker/derived.go index 6aa1eaf1..7046af76 100644 --- a/internal/broker/derived.go +++ b/internal/broker/derived.go @@ -120,6 +120,8 @@ type DeclaredSeat struct { // Serves are the verbs the holder answers, request and reply. Serves []string RetainSeconds int + // PulledByKind is Seat's: a worker per accepted kind, derived from the holders' seat traffic, not here. + PulledByKind bool } // ConsumerFor is the durable consumer a module's declarations imply, or false when it subscribes @@ -258,7 +260,9 @@ func ConsumersOf(users []Principal) []ModuleConsumer { // redelivery: nothing is delivered that nobody asked for. A long build keeps its own ask alive // (stillWorking); the ack wait is for a holder that died. func HolderConsumerFor(node, module string, seat DeclaredSeat) (Consumer, bool) { - if len(seat.Accepts) == 0 { + if len(seat.Accepts) == 0 || seat.PulledByKind { + // A seat pulled by kind has a worker per kind, which its holders' seat traffic derives (SeatTrafficObjects): one + // worker filtered on every accept would overlap them on the work queue. return Consumer{}, false } return Consumer{ diff --git a/internal/broker/nats.go b/internal/broker/nats.go index a6980a5d..dae430a1 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -92,6 +92,9 @@ type Seat struct { // DeclaredBy is the module that declares the seat. On a kinded bench it alone submits work to a kind // and answers its proofs (novox/hq ADR 0259 §8): the router, not any user or watcher of the bench. DeclaredBy string + // PulledByKind says each accepted kind is a worker of its own on the seat's queue, filtered to that kind, which + // every holder pulls in the order Accepts lists them: the build seats' (novox/hq ADR 0525 §2, ADR 0536 §4). + PulledByKind bool } // A Principal is one user of the bus. Its permissions are derived from what it declares and diff --git a/internal/broker/seattraffic.go b/internal/broker/seattraffic.go index ff797e1f..be2852c4 100644 --- a/internal/broker/seattraffic.go +++ b/internal/broker/seattraffic.go @@ -61,7 +61,14 @@ func SeatTrafficOf(module string, holds, uses, watches []Seat) SeatTraffic { continue } } - if len(s.Accepts) > 0 { + if len(s.Accepts) > 0 && s.PulledByKind { + // One worker per accepted kind, which every holder pulls in the order of Accepts (ADR 0525 §4, ADR 0536 §4): + // `SEAT___worker`, filtered to `accept.` (mesh-sdk delivery.AskWorker). + for _, a := range s.Accepts { + t.Workers = append(t.Workers, Worker{Stream: seatStreamName(s.Name), Consumer: WorkerName(s.Name, a), + Filter: "mesh.seat." + s.Name + ".accept." + a}) + } + } else if len(s.Accepts) > 0 { stream := seatStreamName(s.Name) filter := "mesh.seat." + s.Name + ".accept.>" if kind != "" { @@ -156,7 +163,7 @@ func seatTrafficGrants(t SeatTraffic) (pub, sub []string) { // isNewTraffic says whether a seat carries any of the rules above, so a seat that carries none is // composed exactly as before them. func (s Seat) isNewTraffic() bool { - return s.Kinded || len(s.ByCaller) > 0 || len(s.Proofs) > 0 || len(s.Records) > 0 + return s.Kinded || len(s.ByCaller) > 0 || len(s.Proofs) > 0 || len(s.Records) > 0 || s.PulledByKind } // plainVerbs is a seat's accepts or emits with those the rules above compose taken out: a verb named by diff --git a/internal/broker/seattraffic_test.go b/internal/broker/seattraffic_test.go index 08110088..26295f02 100644 --- a/internal/broker/seattraffic_test.go +++ b/internal/broker/seattraffic_test.go @@ -388,3 +388,47 @@ func TestVerifiedSenderNeedsTheChannelsAndTheRoutersMachinesRootFree(t *testing. t.Error("no router placed, and verified-sender kept") } } + +func buildSeat() Seat { + return Seat{Name: "node-build-go", Scope: "node", Accepts: []string{"delivery", "check", "recheck"}, + Serves: []string{"current", "pause", "resume"}, PulledByKind: true} +} + +// 1887/A6, ADR 0525 §2: the builder, which uses the build seat, may put an ask on each kind; a session's account may +// not; the seat's holder pulls a worker per kind and no catch-all worker is made beside them. +func TestTheBuilderMayAskABuildSeatAndASessionMayNot(t *testing.T) { + builder := perms(t, Principal{Kind: KindModule, Node: "novox", Module: "mesh-delivery-builder", Uses: []Seat{buildSeat()}}) + for _, kind := range []string{"delivery", "check", "recheck"} { + if s := "mesh.seat.node-build-go.accept." + kind; !allowed(builder.Publish, s) { + t.Errorf("the builder may not publish %s", s) + } + } + session := perms(t, Principal{Kind: KindPerson, Module: "jochen", Invokes: []string{"mesh-controller.status"}}) + if allowed(session.Publish, "mesh.seat.node-build-go.accept.delivery") { + t.Error("a session's account may put an ask") + } + holds := []Seat{buildSeat()} + traffic := SeatTrafficOf("mesh-build-go", holds, nil, nil) + var workers []string + for _, w := range traffic.Workers { + workers = append(workers, w.Consumer+" "+w.Filter) + } + // Sorted: the order a holder pulls them in is its own (mesh-sdk delivery.AskKinds). + want := "SEAT_NODE_BUILD_GO_CHECK_worker mesh.seat.node-build-go.accept.check," + + "SEAT_NODE_BUILD_GO_DELIVERY_worker mesh.seat.node-build-go.accept.delivery," + + "SEAT_NODE_BUILD_GO_RECHECK_worker mesh.seat.node-build-go.accept.recheck" + if strings.Join(workers, ",") != want { + t.Errorf("workers = %v, want one per kind", workers) + } + pub, _ := seatTrafficGrants(traffic) + if !allowed(pub, "$JS.API.CONSUMER.MSG.NEXT.SEAT_NODE_BUILD_GO.SEAT_NODE_BUILD_GO_CHECK_worker") { + t.Error("the holder may not pull the check worker") + } + _, consumers := SeatTrafficObjects([]Principal{{Kind: KindModule, Module: "mesh-build-go", Holds: holds}}) + if len(consumers) != 3 { + t.Errorf("consumers = %+v, want the three kinds' workers", consumers) + } + if _, made := HolderConsumerFor("shanks", "mesh-build-go", DeclaredSeat{Name: "node-build-go", Accepts: buildSeat().Accepts, PulledByKind: true}); made { + t.Error("a catch-all worker would overlap the kinds' workers on the work queue") + } +} diff --git a/internal/inventory/busrecords.go b/internal/inventory/busrecords.go index de25fb28..9ca930fb 100644 --- a/internal/inventory/busrecords.go +++ b/internal/inventory/busrecords.go @@ -356,6 +356,11 @@ func asSeat(s catalogue.SeatDeclaration, declarer string) broker.Seat { seat := broker.Seat{Name: s.Name, Scope: s.Scope, Accepts: s.Accepts, Emits: s.Emits, Serves: catalogue.VerbNames(s.Serves), Kinded: s.Kinded, ByCaller: s.ByCaller, Proofs: s.Proofs, DeclaredBy: declarer} + // A build seat's kinds are each a worker of its own, pulled in their order (novox/hq ADR 0525 §2; issue 1887 A6): the + // mesh's definition of the role, compiled, never stored. + if own, ok := catalogue.SeatNamed(s.Name); ok && own.PulledByKind { + seat.PulledByKind = true + } // A seat's records are its declaring module's buckets, named as the bus holds them (ADR 0259 §3). for _, r := range s.Records { if declarer != "" { @@ -371,7 +376,7 @@ func MeshSeats() []broker.DeclaredSeat { var out []broker.DeclaredSeat for _, s := range catalogue.SeatsWithAProtocol() { out = append(out, broker.DeclaredSeat{ - Name: s.Name, Accepts: s.Accepts, Emits: s.Emits, Serves: catalogue.VerbNames(s.Serves), + Name: s.Name, Accepts: s.Accepts, Emits: s.Emits, Serves: catalogue.VerbNames(s.Serves), PulledByKind: s.PulledByKind, }) } return out