Derive a worker per kind for the build seats and grant their users to put asks, so a build node pulls delivery, check and recheck in order and a session cannot ask (issue 1887 A6, ADR 0525 §2)

This commit is contained in:
2026-10-23 15:54:45 +02:00
parent 9c4bf1f08b
commit 5ca4f14479
5 changed files with 67 additions and 4 deletions
+5 -1
View File
@@ -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{
+3
View File
@@ -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
+9 -2
View File
@@ -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_<SEAT>_<KIND>_worker`, filtered to `accept.<kind>` (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
+44
View File
@@ -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")
}
}
+6 -1
View File
@@ -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