mesh-grants: create a module's object buckets and history streams, grant their readers read-only, and warn at 75 percent

ADR 0592 §5, §6, §9, §25 (the grants part's share by ADR 0593 §10): an object bucket and a stream that
keeps its history are created create-only with their caps and no age limit, refusing new data when full;
the owner alone writes its bucket and a module its grant names reads it; the part raises
mesh-grants.history-full once any passes 75 percent. Each engine also reads the indexer's manifest
verdicts by one key for compose-candidate.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 18:56:19 +01:00
parent d283efdc9f
commit 0b03b9c7c5
14 changed files with 444 additions and 21 deletions
@@ -33,7 +33,8 @@ func theBusNodeFollowsTheSealedListUnderTheComposedGrants(t *testing.T) {
"novox": {{Module: "nats", SnapshotsTheBus: true},
{Module: buscompose.GrantsModule, State: []buscompose.Bucket{{Module: buscompose.GrantsModule, Name: "user-list"}}}},
"ace": {{Module: "plex", Reads: []string{delivery.ComposedRead}},
{Module: "mesh-delivery-view", Reads: []string{delivery.ComposedRead}}},
{Module: "mesh-delivery-view", Reads: []string{delivery.ComposedRead}, ReadsObjects: []string{"mesh-knowledge_sections"}},
{Module: "mesh-knowledge", Objects: []buscompose.ObjectBucket{{Module: "mesh-knowledge", Name: "sections", MaxMiB: 1, Restricted: true}}}},
},
}
// The node-engine's state as the part's records carry it: every node's compositions (ADR 0593 §6).
@@ -216,6 +217,9 @@ func theBusNodeFollowsTheSealedListUnderTheComposedGrants(t *testing.T) {
if _, err := other.GetLastMsg("KV_"+buscompose.GrantsUserListBucket, "$KV."+buscompose.GrantsUserListBucket+"."+buscompose.GrantsUserListKey, nats.DirectGet()); err == nil {
t.Errorf("another node read the key: %v", err)
}
// A context of its own: the refusals above each wait out their bound.
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// ADR 0593 §6 (D14): the grants part creates each node's compositions; ace's engine writes its own and not novox's;
// the view, a named reader, finds every node's bucket by listing and reads it, writing nothing; plex reads none.
for _, node := range records.Nodes {
@@ -249,11 +253,48 @@ func theBusNodeFollowsTheSealedListUnderTheComposedGrants(t *testing.T) {
if _, err := composed.Get(ctx, "ace.node"); err != nil {
t.Errorf("the view reads ace's own composition: %v", err)
}
if _, err := composed.Put(ctx, "ace.node", []byte(`{}`)); err == nil {
refusedIn := func() context.Context { // a refused publish is answered by no one: it waits out a short bound
c, stop := context.WithTimeout(ctx, 2*time.Second)
t.Cleanup(stop)
return c
}
if _, err := composed.Put(refusedIn(), "ace.node", []byte(`{}`)); err == nil {
t.Error("the view wrote a composition")
}
plex, _ := dial("ace.plex").JetStream(nats.MaxWait(2 * time.Second))
if _, err := plex.GetLastMsg("KV_composed-ace", "$KV.composed-ace.ace.node", nats.DirectGet()); err == nil {
t.Error("plex, not a named reader, read a composition")
}
// ADR 0592 §5, §9: a module's own object bucket, created by the grants part, written (and an object replaced) by its
// owner alone, read by the module its grant names, read-only, and by no other.
sections := buscompose.ObjectBucket{Module: "mesh-knowledge", Name: "sections", MaxMiB: 1, Restricted: true}
if _, err := grants.CreateStream(ctx, buscompose.ObjectsStreamConfig(sections)); err != nil {
t.Fatalf("the grants part creates %s: %v", sections.Stream(), err)
}
owner, err := jetstream.New(dial("ace.mesh-knowledge"))
if err != nil {
t.Fatal(err)
}
ownStore, err := owner.ObjectStore(ctx, sections.Bucket())
if err != nil {
t.Fatalf("the owner binds its object bucket: %v", err)
}
for range 2 { // put twice: a second put of a name purges its old chunks
if _, err := ownStore.PutBytes(ctx, "s1", []byte("body")); err != nil {
t.Fatalf("the owner puts an object: %v", err)
}
}
readStore, err := view.ObjectStore(ctx, sections.Bucket())
if err != nil {
t.Fatalf("the granted reader binds the bucket: %v", err)
}
if got, err := readStore.GetBytes(ctx, "s1"); err != nil || string(got) != "body" {
t.Errorf("the granted reader reads %q, %v", got, err)
}
if _, err := readStore.PutBytes(refusedIn(), "s2", []byte("x")); err == nil {
t.Error("the granted reader wrote the bucket")
}
if _, err := plex.GetLastMsg(sections.Stream(), "$O."+sections.Bucket()+".M.x", nats.DirectGet()); err == nil {
t.Error("a module the grant does not name read the bucket")
}
}