DLV-G1, DLV-W1: the grants part and the watchman hear mesh-broker's events on the seat's subject; the keeper is granted them

The lead's ruling of 13:0x (registry milestone): mesh-broker's events are mesh.seat.mesh-broker.event.<event> on the
shared events stream like every seat's. The grants part and the watchman consume mesh-broker.<event> and no longer hear
the bus module's own stream, which is its history for its tool. The keeper's process account is composed the seat's
event subjects, and the acknowledgement of a snapshot's chunks of each entry it keeps, which ADR 0626 §1 grants and the
composition lacked; a row holds it to no subscription of a restricted stream's subjects and no consumer of one.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 13:05:27 +01:00
parent 20a4a79993
commit 8a78e00bb0
14 changed files with 105 additions and 55 deletions
@@ -17,8 +17,9 @@ import (
)
// **What the watchman consumes** (its manifest's `consumes`; novox/hq ADR 0595 §4, design 25 § 12.2; DLV-W1): the indexer's
// manifest-accepted, on its durable consumer on EVENTS, and the bus module's created and refused, on its durable consumer
// on the bus module's own stream, which its membership names (Hears). Each consumer is the one the grants part makes; one
// manifest-accepted and the seat mesh-broker's created and refused, all on its durable consumer on EVENTS (the seat's
// events on the seat's subjects like every seat's: the lead's ruling, registry milestone 13:0x); beside it any module
// stream its membership names (Hears). Each consumer is the one the grants part makes; one
// not there yet is opened when the watchman's membership next changes, and one opened is never opened twice. An event is
// acknowledged once handed to the watchman, so one given while it was down is heard when it is back.
@@ -29,8 +30,8 @@ const (
eventRefused = "refused"
)
// consumed hands one consumed event to the watchman, by its subject: manifest-accepted from EVENTS, created and refused
// from the bus module's stream. Anything else is left.
// consumed hands one consumed event to the watchman, by its subject: manifest-accepted, and the seat mesh-broker's created
// and refused. Anything else is left.
func consumed(w *watch.Watchman, subject string, body []byte, logf func(string, ...any)) {
switch {
case strings.HasSuffix(subject, ".event."+eventAccepted) && strings.HasPrefix(subject, "mesh.mod."):
@@ -110,8 +111,8 @@ func hearConsumed(ctx context.Context, js jetstream.JetStream, cred process.Acco
}()
}
// brokerSubject is a subject the bus module's created or refused arrives on: its own stream's, or the seat mesh-broker's
// (where the seat's definition puts it; only its holder publishes there).
// brokerSubject is a subject the bus module's created or refused arrives on: the seat mesh-broker's, where only its holder
// publishes. The bus module's own stream is its history for its tool, never heard here.
func brokerSubject(subject string) bool {
return strings.HasPrefix(subject, "mesh.stream.") || strings.HasPrefix(subject, "mesh.seat."+catalogue.BrokerSeat+".event.")
return strings.HasPrefix(subject, "mesh.seat."+catalogue.BrokerSeat+".event.")
}
@@ -52,7 +52,12 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6
out := &findingsOut{findings: map[string]bool{}}
w := watch.New(out, time.Now)
say := func(string, ...any) {}
// The bus module's own stream is its history, never heard here: only the seat's subject (ruling 13:0x).
consumed(w, "mesh.stream.nats.event.refused", []byte(`{"module":"m","local":"l","why":"no room"}`), say)
if len(out.findings) != 0 {
t.Fatalf("a refused on the bus module's own stream reached %v", out.findings)
}
consumed(w, "mesh.seat.mesh-broker.event.refused", []byte(`{"module":"m","local":"l","why":"no room"}`), say)
if !out.findings[watch.FindingKey(watch.ScopeModule, "m", watch.KindBusStateRefused+"-l")] {
t.Fatalf("refused reached no finding: %v", out.findings)
}
@@ -60,7 +65,7 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6
if len(out.findings) != 0 {
t.Fatalf("an accepted manifest no longer asking for l left %v", out.findings)
}
consumed(w, "mesh.stream.nats.event.refused", []byte(`{"module":"m","local":"l"}`), say)
consumed(w, "mesh.seat.mesh-broker.event.refused", []byte(`{"module":"m","local":"l"}`), say)
consumed(w, "mesh.seat.mesh-broker.event.created", []byte(`{"module":"m","local-name":"l"}`), say)
if len(out.findings) != 0 {
t.Fatalf("created left %v", out.findings)
@@ -103,7 +103,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6
t.Errorf("tools = %v", m.Tools)
}
// DLV-W1 (ADR 0595 §4): it hears the accepted verdicts and the bus module's created and refused, durably.
if !reflect.DeepEqual(m.Consumes, []string{"mesh-delivery-indexer.manifest-accepted", "nats.created", "nats.refused"}) {
if !reflect.DeepEqual(m.Consumes, []string{"mesh-delivery-indexer.manifest-accepted", "mesh-broker.created", "mesh-broker.refused"}) {
t.Errorf("consumes = %v", m.Consumes)
}
for _, k := range []string{"emits", "context", "items", "provides"} {
+2 -2
View File
@@ -31,8 +31,8 @@
],
"consumes": [
"mesh-delivery-indexer.manifest-accepted",
"nats.created",
"nats.refused"
"mesh-broker.created",
"mesh-broker.refused"
],
"state": [
{ "name": "presence", "history": 20 },
@@ -9,6 +9,7 @@ import (
"io"
"os"
"path/filepath"
"slices"
"strings"
"sync/atomic"
"testing"
@@ -231,19 +232,16 @@ func TestTheGrantsPartWhole(t *testing.T) {
if firstViewAsks.Load() < int32(len(grants.FirstViews)) {
t.Errorf("the part composed with %d first-view asks, want one of each of the %d", firstViewAsks.Load(), len(grants.FirstViews))
}
// DLV-G1, 1966/AC34 (novox/hq ADR 0595 §1, §7): the bus module's created, said on its own stream, reaches the part on
// its durable consumer there, and the binding it carries is in the module's membership; dropped takes it out.
until("the part's durable consumer on the bus module's stream", func() bool {
e, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a."+grants.Module)
// DLV-G1, 1966/AC34 (novox/hq ADR 0595 §1, §7): the bus module's created, said on the seat mesh-broker's subject on
// EVENTS like every seat's event (the lead's ruling, registry milestone 13:0x), reaches the part on its durable consumer
// there, and the binding it carries is in the module's membership; dropped takes it out.
until("the part's durable consumer on EVENTS hearing mesh-broker's created", func() bool {
cons, err := js.Consumer(ctx, "EVENTS", "a_"+grants.Module)
if err != nil {
return false
}
m, err := membership.DecodeGrantsMembership(e.Value())
if err != nil || len(m.Hears) == 0 {
return false
}
_, err = js.Consumer(ctx, m.Hears[0].Stream, m.Hears[0].Consumer)
return err == nil
info, err := cons.Info(ctx)
return err == nil && slices.Contains(info.Config.FilterSubjects, "mesh.seat.mesh-broker.event.created")
})
bindingOf := func() []membership.BusBinding {
e, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c")
@@ -255,7 +253,7 @@ func TestTheGrantsPartWhole(t *testing.T) {
}
sayBroker := func(event string, bind membership.BusBinding) {
body, _ := json.Marshal(grants.BrokerEvent{V: 1, Module: "mod-c", Local: "kept", Storage: "key-value", Template: "state", Binding: bind})
if _, err := js.Publish(ctx, "mesh.stream.nats.event."+event, body); err != nil {
if _, err := js.Publish(ctx, "mesh.seat.mesh-broker.event."+event, body); err != nil {
t.Fatal(err)
}
}
+4 -3
View File
@@ -2238,7 +2238,8 @@ func TestGrants(t *testing.T) {
return changed
}
first := membership.BusBinding{Local: "records", Storage: "key-value", Template: "state", Bucket: "mod-x_records"}
for _, event := range []string{"mesh.stream.nats.event.refused", "mesh.stream.mod-y.event.created", "mesh.stream.nats.event.drifted"} {
for _, event := range []string{"mesh.seat.mesh-broker.event.refused", "mesh.stream.mod-y.event.created",
"mesh.seat.mesh-broker.event.drifted", "mesh.stream.nats.event.created", "mesh.mod.nats.event.created", "nats.created"} {
if hear(event, first) {
t.Errorf("%s changed the bindings", event)
}
@@ -2253,7 +2254,7 @@ func TestGrants(t *testing.T) {
if !slices.Contains(heard.consumerCreates, "THINGS.b_mod-y") {
t.Errorf("the consumer of a heard module stream was not created: %v", heard.consumerCreates)
}
if !hear("mesh.stream.nats.event.created", first) || hear("mesh.stream.nats.event.created", first) {
if !hear("mesh.seat.mesh-broker.event.created", first) || hear("mesh.seat.mesh-broker.event.created", first) {
t.Fatal("created: not kept once")
}
b.assign("b", "mod-x")
@@ -2268,7 +2269,7 @@ func TestGrants(t *testing.T) {
}
again := first
again.Bucket = "mod-x_records-2"
hear("mesh.stream.nats.event.recreated", again)
hear("mesh.seat.mesh-broker.event.recreated", again)
p.Compose()
if got := bindingsOn("b.mod-x"); len(got) != 1 || got[0].Bucket != "mod-x_records-2" {
t.Errorf("after recreated: %+v", got)
@@ -1069,12 +1069,14 @@ func aLedgerIsWrittenByItsLedgerAccountAlone(t *testing.T) {
func theBusModulesProcessKeepsBusState(t *testing.T) {
r := Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": {
{Module: "node-tools", ToolRunner: true},
{Module: "nats", SnapshotsTheBus: true, Holds: []Seat{{Name: catalogue.BrokerSeat, Serves: BrokerVerbs()}},
{Module: "nats", SnapshotsTheBus: true, Holds: []Seat{{Name: catalogue.BrokerSeat, Serves: BrokerVerbs(),
Emits: []string{"created", "refused"}}},
Consumes: []string{"mesh-delivery-indexer.manifest-accepted"}, Uses: []Seat{{Name: "operator-channel", Accepts: []string{"condition"}, ByCaller: []string{"condition"}}},
Streams: []ModuleStream{{Module: "nats", Name: "BROKER_EVENTS", Events: []string{"created", "refused"}, MaxAgeDays: 30, MaxMiB: 256}},
Processes: []ProcessPrincipal{{Process: "keeper", State: []string{"asked"}, Keeps: true}, {Process: "other"},
{Process: "nats-tools", Tools: true, Removes: []string{catalogue.RemovesEngineActs}}}},
{Module: "mod-x", Processes: []ProcessPrincipal{{Process: "sneak", Keeps: true}}},
{Module: "mod-x", Processes: []ProcessPrincipal{{Process: "sneak", Keeps: true}},
Streams: []ModuleStream{{Module: "mod-x", Name: "SECRETS", Events: []string{"sealed"}, Restricted: true}}},
{Module: "admin-ish", Invokes: []string{"*"}},
}},
BusState: map[string][]catalogue.BusStateEntry{"mod-x": {
@@ -1132,11 +1134,39 @@ func theBusModulesProcessKeepsBusState(t *testing.T) {
t.Error("the bus module's process answers what it was asked")
}
for _, s := range []string{"$JS.API.CONSUMER.MSG.NEXT.EVENTS.a_nats_keeper", "$JS.ACK.EVENTS.a_nats_keeper.x",
"mesh.stream.nats.event.created", "mesh.seat.operator-channel.accept.condition.nats"} {
"mesh.stream.nats.event.created", "mesh.seat.operator-channel.accept.condition.nats",
// The seat's events on the seat's subjects, as every seat's (the lead's ruling, registry milestone 13:0x), and the
// acknowledgement of a snapshot's chunks of an entry it keeps (ADR 0626 §1).
"mesh.seat.mesh-broker.event.created", "mesh.seat.mesh-broker.event.refused", "$JS.SNAPSHOT.ACK.KV_mod-x_records.x.1.0"} {
if !MayPublish(keeper, s) {
t.Errorf("the bus module's process may not publish %s", s)
}
}
for _, other := range []string{"a.nats.other", "a.mod-x.sneak"} {
if MayPublish(perms[other], "mesh.seat.mesh-broker.event.created") {
t.Errorf("%s, which is no keeper of the bus, says mesh-broker's created", other)
}
}
// ADR 0626, How it is checked: no subscription to a restricted stream's subjects, nor a consumer of it or of any
// stream it keeps; a snapshot's chunks come to its own inbox alone, and only of a stream it keeps.
for _, s := range []string{"mesh.stream.mod-x.event.sealed", "mesh.stream.mod-x.event.>", "mesh.bus.mod-x.records.x"} {
if MaySubscribe(keeper, s) {
t.Errorf("the bus module's process hears %s", s)
}
}
for _, s := range []string{"$JS.API.CONSUMER.CREATE.SECRETS.x.y", "$JS.API.CONSUMER.CREATE.KV_mod-x_records.x",
"$JS.API.CONSUMER.MSG.NEXT.SECRETS.x", "$JS.API.DIRECT.GET.SECRETS", "$JS.API.STREAM.SNAPSHOT.SECRETS",
"$JS.SNAPSHOT.ACK.SECRETS.x.1.0", "$JS.API.STREAM.MSG.GET.KV_mod-x_records"} {
if MayPublish(keeper, s) {
t.Errorf("the bus module's process may publish %s", s)
}
}
for _, s := range keeper.Subscribe {
if !strings.HasPrefix(s, "_INBOX.a.nats.keeper.") && !strings.HasPrefix(s, "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_nats_") &&
!strings.HasPrefix(s, "mesh.seat.operator-channel.") {
t.Errorf("the bus module's process subscribes %s, beyond its inbox, its own state's creation and its conditions' answers", s)
}
}
made := false
for _, mc := range AccountConsumers(users) {
if mc.Consumer.Stream == "EVENTS" && mc.Consumer.Name == "a_nats_keeper" &&
@@ -144,13 +144,34 @@ func BusStateNames(asked map[string][]catalogue.BusStateEntry, bound map[string]
}
// busStateKeeperGrants are what the bus module's process publishes to keep the bus state named: each stream's create,
// update, information, deletion and snapshot, and the stream list it finds orphans in.
// update, information, deletion and snapshot with the acknowledgement of the snapshot's chunks (ADR 0626 §1; the server
// waits on `$JS.SNAPSHOT.ACK.<stream>.…` past its window), and the stream list it finds orphans in. Never a consumer of
// one, nor any stream's own subjects: a snapshot carries an entry's data to the keeper's inbox alone, and the keeper
// hears no message of it (ADR 0626 §1, restricted streams included).
func busStateKeeperGrants(names []string) []string {
out := []string{"$JS.API.STREAM.NAMES", "$JS.API.STREAM.LIST"}
for _, n := range names {
for _, op := range []string{"CREATE", "UPDATE", "INFO", "DELETE", "SNAPSHOT"} {
out = append(out, "$JS.API.STREAM."+op+"."+n)
}
out = append(out, "$JS.SNAPSHOT.ACK."+n+".>")
}
return out
}
// brokerSeatEvents are the subjects of the seat mesh-broker's events, from the held seat's definition: what the bus
// module's keeper says them on, like every seat's holder (the lead's ruling, registry milestone 13:0x).
func brokerSeatEvents(holds []Seat) []string {
var out []string
for _, s := range holds {
if s.Name != catalogue.BrokerSeat {
continue
}
for _, e := range s.Emits {
if safeSubject.MatchString(e) {
out = append(out, seatSubject(s, "event", e))
}
}
}
return out
}
@@ -114,6 +114,10 @@ type Principal struct {
// §4, §5), and KeepsBusState are the streams it defines, every module's bus state by its name.
KeepsBus bool
KeepsBusState []string
// SaysSeatEvents are the subjects of the seat mesh-broker's events the bus module's keeper says them on (the lead's
// ruling, registry milestone 13:0x): `mesh.seat.mesh-broker.event.<event>` for each event the seat's definition
// names, as a holder's own account would be granted them; set on the keeper alone.
SaysSeatEvents []string
// Process is the process resource's id, for a KindProcess principal alone (novox/hq ADR 0436 §2):
// Username and inbox are <node>.<module>.<process id>.
Process string
@@ -61,6 +61,7 @@ func processGrants(p Principal) (pub, sub []string, err error) {
pub = append(pub, keeperConsumerGrants(c)...)
}
pub = append(pub, ownStreamGrants(p.Module, p.Streams)...)
pub = append(pub, p.SaysSeatEvents...)
tp, ts := seatTrafficGrants(SeatTrafficOf(p.Module, nil, p.Uses, nil))
pub, sub = append(pub, tp...), append(sub, ts...)
}
@@ -249,6 +249,7 @@ func Users(r Records) ([]Principal, error) {
k := &out[len(out)-1]
k.KeepsBus, k.KeepsBusState = true, BusStateNames(r.BusState, r.Bound)
k.Consumes, k.Streams = d.Consumes, d.Streams
k.SaysSeatEvents = brokerSeatEvents(d.Holds)
}
if d.SnapshotsTheBus && len(pr.Removes) > 0 {
out[len(out)-1].Streams = d.Streams
@@ -61,28 +61,13 @@ type Bound struct {
Bindings map[string]membership.BusBinding `json:"bindings"`
}
// brokerEvent is the local name of an event heard from the holder of mesh-broker, by its name or its bus subject, and
// whether it is one; an emitter that holds no mesh-broker seat is never one.
// brokerEvent is the local name of an event heard from the holder of mesh-broker, by its bus subject, and whether it is
// one: the seat's own subject alone (`mesh.seat.mesh-broker.event.<event>`, the lead's ruling, registry milestone 13:0x),
// where only its holder publishes, and only while a module holds the seat. The bus module's own stream is its history
// for its tool, never what the part hears.
func brokerEvent(event string, brokers map[string]bool) (string, bool) {
emitter, name := "", ""
switch {
case strings.HasPrefix(event, "mesh.seat."+catalogue.BrokerSeat+".event."):
// The seat's own subject: only its holder publishes there, so the emitter is the holder.
name = strings.TrimPrefix(event, "mesh.seat."+catalogue.BrokerSeat+".event.")
for b := range brokers {
emitter = b
}
case buscompose.OnAModuleStream(event):
// mesh.stream.<module>.event.<event>
rest := strings.TrimPrefix(event, "mesh.stream.")
emitter, name, _ = strings.Cut(rest, ".event.")
case strings.HasPrefix(event, "mesh.mod."):
rest := strings.TrimPrefix(event, "mesh.mod.")
emitter, name, _ = strings.Cut(rest, ".event.")
default:
emitter, name, _ = strings.Cut(event, ".")
}
if !brokers[emitter] {
name, onTheSeat := strings.CutPrefix(event, "mesh.seat."+catalogue.BrokerSeat+".event.")
if !onTheSeat || len(brokers) == 0 {
return "", false
}
switch name {
@@ -7,6 +7,7 @@ import (
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/health"
"mesh-grants/internal/buscompose"
)
@@ -39,8 +40,10 @@ func FromKeeper(event string) bool {
const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
// Consumes is every event the part hears, as its manifest's consumes name them.
var Consumes = append([]string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, BusModule + "." + BrokerCreated,
BusModule + "." + BrokerDropped, BusModule + "." + BrokerRecreated}, viewsEvents()...)
// The seat mesh-broker's created, recreated and dropped are heard as every seat's event, `<seat>.<event>` on EVENTS (the
// lead's ruling, registry milestone 13:0x), never on the bus module's own stream.
var Consumes = append([]string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, catalogue.BrokerSeat + "." + BrokerCreated,
catalogue.BrokerSeat + "." + BrokerDropped, catalogue.BrokerSeat + "." + BrokerRecreated}, viewsEvents()...)
// viewsEvents are the owners' events the part follows its first views by: `<owner>.views.<state>`, but the verdicts by
// the indexer's manifest-accepted and manifest-refused (VerdictEvents, ADR 0622 §2).
+3 -3
View File
@@ -14,9 +14,9 @@
"mesh-conditions.condition-raised",
"mesh-conditions.condition-cleared",
"mesh-vault.account-issued",
"nats.created",
"nats.dropped",
"nats.recreated",
"mesh-broker.created",
"mesh-broker.dropped",
"mesh-broker.recreated",
"mesh-delivery-assigner.views.placement",
"mesh-delivery-assigner.views.wants",
"mesh-delivery-indexer.views.manifests",