fix9-8: the grants part composes the node-engine's grants for the seat mesh-recovery and for a module's catch-up tool

The node-engine of a node hears the kept events ended and restored through an ordered consumer, and calls the catch-up tool of a module placed on its node (its manifest's kept data name it); the verbs snapshot-kept, restore-kept and kept-points were granted already as the engine's invokes. No nodegrant mirror lives in mesh-delivery (it is mesh-host's).

Builds: design 47 G §406, §554, §631, A-108
This commit is contained in:
2026-11-12 01:13:03 +01:00
parent 90718ba7a3
commit 8fc1997b1b
6 changed files with 112 additions and 1 deletions
@@ -85,6 +85,17 @@ func engineGrants(p Principal) (pub, sub []string) {
sub = append(sub, k.Filter)
}
}
// And the kept events of other seats it follows (the recovery's ended and restored): each stream read through an ordered
// consumer made for the read, with no durable of its own, and the stream's own info (design 47 G §554, A-108).
for _, h := range p.EngineHears {
if !safeSubject.MatchString(h.Stream) {
continue
}
pub = append(pub, "$JS.API.STREAM.INFO."+h.Stream,
"$JS.API.CONSUMER.CREATE."+h.Stream, "$JS.API.CONSUMER.CREATE."+h.Stream+".>",
"$JS.API.CONSUMER.MSG.NEXT."+h.Stream+".*", "$JS.API.CONSUMER.INFO."+h.Stream+".*",
"$JS.API.CONSUMER.DELETE."+h.Stream+".*")
}
sub = append(sub, announcing(EngineSeat)...)
pub = append(pub, discovering()...)
tp, ts := seatTrafficGrants(SeatTrafficOf(occ.EngineAsker(p.Node), nil, p.Uses, nil))
@@ -124,6 +124,9 @@ type Principal struct {
// (engineGrants).
EngineEvents map[string]string
EngineKept []KeptEvent
// EngineHears are the kept events of other seats its engine consumes (design 47 G §554, A-108: the events ended and
// restored of the seat mesh-recovery), each stream followed through an ordered consumer made for the read.
EngineHears []Heard
// Ledgers are the local names of the ledgers its module asks for in `requires`, for a KindLedger principal alone
// (issue 1966 D5): its whole authority is publishing on those ledgers' histories and snapshots (LedgerPublishGrants).
Ledgers []string
@@ -0,0 +1,49 @@
package buscompose
import (
"testing"
)
// Design 47 G §406, §554, A-108: the node-engine of a node is granted to hear the seat mesh-recovery's kept events ended and
// restored through an ordered consumer, to ask the verbs snapshot-kept, restore-kept and kept-points of the seat, and to call
// the catch-up tool of a module placed on its node, on its own node's tool subject and no other module's tool.
func TestTheNodeEngineIsGrantedWhatItNeedsOfTheRecoverySeat(t *testing.T) {
kept := []KeptEvent{
{Seat: "mesh-recovery", Event: "ended", Stream: "SEAT_mesh-recovery_ended", Filter: "mesh.seat.mesh-recovery.event.ended.*.*"},
{Seat: "mesh-recovery", Event: "restored", Stream: "SEAT_mesh-recovery_restored", Filter: "mesh.seat.mesh-recovery.event.restored.*.*"},
}
records := Records{Nodes: []string{"one"}, Kept: kept,
Engine: &Declared{Module: EngineModule, Consumes: []string{"mesh-recovery.ended", "mesh-recovery.restored"},
Invokes: []string{"seat:mesh-recovery.snapshot-kept", "seat:mesh-recovery.restore-kept", "seat:mesh-recovery.kept-points"}},
Assigned: map[string][]Declared{"one": {{Module: "keeper", CatchUps: []string{"catch_up"}}, {Module: "plain"}}}}
users, err := Users(records)
if err != nil {
t.Fatal(err)
}
var node Principal
for _, u := range users {
if u.Kind == KindNode {
node = u
}
}
perms, err := PermissionsFor(node)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"$JS.API.CONSUMER.CREATE.SEAT_mesh-recovery_ended", "$JS.API.CONSUMER.CREATE.SEAT_mesh-recovery_ended.>",
"$JS.API.CONSUMER.MSG.NEXT.SEAT_mesh-recovery_ended.*", "$JS.API.CONSUMER.DELETE.SEAT_mesh-recovery_restored.*",
"$JS.API.STREAM.INFO.SEAT_mesh-recovery_restored",
"mesh.seat.mesh-recovery.tool.snapshot-kept", "mesh.seat.mesh-recovery.tool.restore-kept", "mesh.seat.mesh-recovery.tool.kept-points",
"mesh.mod.keeper.tool.catch_up.one",
} {
if !MayPublish(perms, want) {
t.Errorf("the node-engine of one may not publish %s", want)
}
}
for _, bad := range []string{"mesh.mod.plain.tool.catch_up.one", "mesh.mod.keeper.tool.other.one", "$JS.API.CONSUMER.CREATE.SEAT_mesh-recovery_set-aside"} {
if MayPublish(perms, bad) {
t.Errorf("the node-engine of one may publish %s", bad)
}
}
}
@@ -53,6 +53,9 @@ type Declared struct {
Watches []Seat
// Invokes are the tools it calls, `<module>.<tool>` or `*` (novox/hq ADR 0152).
Invokes []string
// CatchUps are the module's own tools that bring a kept data forward after a restore of it, as its manifest's kept data
// name them (design 47 G §406, §631, A-108): the node-engine on a node that places the module is granted to call each.
CatchUps []string
// Waits are the event families a session may wait on through the tool runner, the tool runner's module's
// alone (novox/hq ADR 0367 §2): its account subscribes them for a call, through no consumer.
Waits []Wait
@@ -223,9 +226,20 @@ func Users(r Records) ([]Principal, error) {
if r.Engine != nil {
engineHears, engineWatches, engineInvokes = r.Engine.Consumes, r.Engine.Watches, r.Engine.Invokes
}
// The node-engine's hearing of kept events of other seats (the recovery's ended and restored) and its call of each
// catch-up tool of a module placed on its node (design 47 G §406, §554, A-108), granted as the seat's grants name them.
_, engineKeptHears := SplitKept(r.Kept, engineHears)
engineInvokes = slices.Clone(engineInvokes)
for _, d := range r.Assigned[node] {
for _, tool := range d.CatchUps {
if t := d.Module + "." + tool; !slices.Contains(engineInvokes, t) {
engineInvokes = append(engineInvokes, t)
}
}
}
for _, login := range nodeLoginsOf(r, node) {
out = append(out, Principal{Kind: KindNode, Node: node, Login: login, Checks: checks, Runner: runner, Consumes: engineHears,
Watches: engineWatches, Invokes: engineInvokes, EngineEvents: r.EngineEvents, EngineKept: keptOfSeat(r.Kept, EngineSeat),
EngineHears: engineKeptHears, Watches: engineWatches, Invokes: engineInvokes, EngineEvents: r.EngineEvents, EngineKept: keptOfSeat(r.Kept, EngineSeat),
MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey),
AsksItsOwnSecrets: servesVerb(r, VaultOwn), AsksTheVaultToSign: servesVerb(r, VaultSign), Uses: asksSeatOf(r),
FollowsGrantsUserList: bus, ServedReads: servedOn(served, node), RotatesItsLogin: servesVerb(r, VaultRotateNode)})
@@ -0,0 +1,21 @@
package grants
import (
"slices"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Design 47 G §406, §631, A-108: the catch-up tools a manifest's kept data name are the module's own, each once, whichever
// kind of kept data names them.
func TestCatchUpsAreTheToolsTheManifestsKeptDataName(t *testing.T) {
m, err := catalogue.Decode([]byte(`{"module":"keeper","version":"1.0","state":[{"name":"seen","form":2,"catch-up":{"tool":"seen_again","arguments":{"all":true}}},"plain"],` +
`"logs":[{"name":"lines","form":1,"catch-up":"seen_again"}],"streams":[{"name":"ev","events":["a"],"max-age-days":1,"max-mib":1,"form":1,"catch-up":{"tool":"ev_again"}}]}`))
if err != nil {
t.Fatal(err)
}
if got := catchUpsOf(m); !slices.Equal(got, []string{"seen_again", "ev_again"}) && !slices.Equal(got, []string{"ev_again", "seen_again"}) {
t.Errorf("catchUpsOf = %v, want seen_again and ev_again once each", got)
}
}
@@ -156,6 +156,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
Ledgers: ledgersOf(m),
}
d.Loads = loadsABundle(m)
d.CatchUps = catchUpsOf(m)
if _, reads := m.OwnSecrets["broker"]; !reads {
d.NoAccount = true
}
@@ -608,3 +609,15 @@ func unionFields(dst, src reflect.Value) {
}
}
}
// catchUpsOf are the catch-up tools a manifest's kept data name, each once (design 47 G §406, §631): the module's own tools
// the node-engine calls after a restore of that data, which it is granted to call on its own node (A-108).
func catchUpsOf(m catalogue.Manifest) []string {
var tools []string
for _, k := range catalogue.KeptDeclarations(m) {
if k.CatchUp != nil && k.CatchUp.Tool != "" && !slices.Contains(tools, k.CatchUp.Tool) {
tools = append(tools, k.CatchUp.Tool)
}
}
return tools
}