diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index f54ed06e..807b85a6 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -2256,6 +2256,33 @@ func TestGrants(t *testing.T) { } }) + // What the part needs to hear the seat and read the list is never held by the unread list (ADR 0636 §1; the lead's ruling + // of 2026-11-05): its own module (its consumer on EVENTS, its grant of the seat's verb) and the module that holds the seat + // the list is read through, whatever other seats their manifests name. + t.Run("the seat list unread never holds the part's own module or the holder of the seat it is read through", func(t *testing.T) { + b := fixture() + own, err := os.ReadFile("module.json") + if err != nil { + t.Fatal(err) + } + b.manifestOf(grants.Module, string(own)) + b.manifestOf("cat-holder", `{"module":"cat-holder","version":"1","claims":[{"name":"mesh-catalog"}],"invokes":["seat:mesh-vault.account-issue"]}`) + b.assign("a", grants.Module) + b.assign("a", "cat-holder") + p := partOn(b) + p.SeatList = &seatGate{} // nothing is bootstrapped: only the exemption lets these two through + p.Compose() + refused := strings.Join(p.Last().Refused, "\n") + for _, module := range []string{grants.Module, "cat-holder"} { + if strings.Contains(refused, "manifest of "+module+":") { + t.Errorf("%s is held while the seat list is unread: %v", module, p.Last().Refused) + } + } + if !strings.Contains(refused, "manifest of mod-x: it waits for the seat list") { + t.Errorf("another module is not held: %v", p.Last().Refused) + } + }) + // DLV-G1, 1966/AC32-AC33 (novox/hq ADR 0595 §1, §7; design 25 § 12.2): the bus module's created writes the binding into // the module's membership on every machine it is assigned, one assigned later included; recreated rewrites it; dropped // removes it; before created, on a refused, or from an emitter holding no mesh-broker, none. diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 49afd3f6..7b7a6074 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -219,6 +220,14 @@ type SeatGate interface { MayAct(seats ...string) bool } +// readsTheList says whether a module is what the part needs to hear the seat and read the list: the part's own module (its +// consumer on EVENTS, its grant of the seat's verb and events) and the holder of the seat the list is read through. The unread +// list never holds these, whatever other seats their manifests name: held, the seat's event could not reach the part and +// the list would never be read (ADR 0636 §1). +func readsTheList(module string, m catalogue.Manifest) bool { + return module == Module || slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == meshcatalog.Seat }) +} + // SeatsOf are the seats a manifest claims or invokes, which the seat list must name before the part composes the module. func SeatsOf(m catalogue.Manifest) []string { var seats []string @@ -422,7 +431,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { if err == nil { m, err = cache.get(r, v.Manifest) } - if err == nil && gate != nil && !gate.MayAct(SeatsOf(m)...) { + if err == nil && gate != nil && !readsTheList(module, m) && !gate.MayAct(SeatsOf(m)...) { err = errors.New("it waits for the seat list: the list has not been read yet and the manifest names seats the bootstrapped list does not (novox/hq ADR 0636 §1)") } if err != nil {