Item 5, area 8 (join 11): the grants part lets a caller of a marked verb hear its own calls' ends

A holder says a held call's end on its seat's gated-ended, kept last per subject for a day in SEAT_<seat>_gated-ended,
which the grants part makes from the definition seatdef.Resolve gives (A-264). The caller is granted hearing
gated-ended.<caller>.> under its own escaped account name, and an ordered consumer on that stream filtered to it, from its
invokes of the marked verb by name; a module or process that answers a marked verb is granted saying the event to any
caller. No process of mesh-delivery holds a marked verb, so no holder here takes NewGateChannel.

Builds: design 47 G §677, §700, A-264, A-273

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 12:24:48 +01:00
parent 4194e93a67
commit cdc6e1f521
7 changed files with 163 additions and 1 deletions
@@ -0,0 +1,54 @@
package buscompose
import (
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// **A held call's end is heard by its caller alone** (design 47 G §677; lead answer A-264): a holder says the end of a call of
// a marked verb on its seat's event `gated-ended`, subject `mesh.seat.<seat>.event.gated-ended.<caller>.<call>`, `<caller>` the
// account the bus proved escaped as a key's name is, kept last per subject for a day in the stream SEAT_<seat>_gated-ended
// (KeptEventsOf, from the definition seatdef.Resolve gives). The caller is granted hearing `gated-ended.<caller>.>` and no other
// caller's, from its invokes of the marked verb by name; it reads the stream through an ordered consumer filtered to its own
// subjects (mesh-sdk process awaitGated), made for the read. A process that answers a marked verb says the event to any caller.
// gatedEndedGrants are the grants of the principal user from the marked verbs (`seat:<seat>.<verb>`) it invokes by name and
// those it answers.
func gatedEndedGrants(user string, marked, invokes, answers []string) (pub, sub []string) {
caller := keys.EscapeName(user)
if user == "" {
return nil, nil
}
var heard, said []string
for _, v := range marked {
seat, _, ok := strings.Cut(strings.TrimPrefix(v, "seat:"), ".")
if !ok || !safeSubject.MatchString(seat) {
continue
}
if slices.Contains(invokes, v) {
heard = append(heard, seat)
}
if slices.Contains(answers, v) {
said = append(said, seat)
}
}
for _, seat := range unique(heard) {
stream := seatdef.KeptStream(seat, seatdef.Message{Name: seatdef.GatedEndedEvent})
own := GatedEndedSubject(seat, caller) + ".>"
sub = append(sub, own)
pub = append(pub, "$JS.API.STREAM.INFO."+stream, "$JS.API.CONSUMER.CREATE."+stream+".*."+own,
"$JS.API.CONSUMER.MSG.NEXT."+stream+".*", "$JS.API.CONSUMER.INFO."+stream+".*", "$JS.API.CONSUMER.DELETE."+stream+".*")
}
for _, seat := range unique(said) {
pub = append(pub, GatedEndedSubject(seat, "*")+".*")
}
return pub, sub
}
// GatedEndedSubject is the subject of the seat's gated-ended for one caller, its escaped name (keys.EscapeName) or `*`.
func GatedEndedSubject(seat, caller string) string {
return seatdef.EventSubject(seat, seatdef.GatedEndedEvent) + "." + caller
}
@@ -0,0 +1,55 @@
package buscompose
import (
"slices"
"testing"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// A held call's end is heard by its caller alone (design 47 G §677, A-264): the definition seatdef.Resolve gives a marked verb
// keeps gated-ended in SEAT_<seat>_gated-ended, last per subject for a day; a module's and a process's account that invoke the
// marked verb by name hear `gated-ended.<caller>.>` under their own escaped name and read the stream filtered to it, one that
// does not invoke it hears nothing, and a process that answers it says the event to any caller.
func TestAHeldCallsEndIsHeardByItsCallerAlone(t *testing.T) {
def, err := seatdef.Resolve(seatdef.Definition{Seat: "node-login-shell", Verbs: []seatdef.Verb{{Name: "execute", NeedsApproval: true}}}, nil)
if err != nil {
t.Fatal(err)
}
kept := KeptEventsOf(map[string]seatdef.Definition{def.Seat: def})
if len(kept) != 1 || kept[0].Stream != "SEAT_node-login-shell_gated-ended" || !kept[0].Last || kept[0].MaxAgeDays != 1 ||
kept[0].Filter != "mesh.seat.node-login-shell.event.gated-ended.*.*" {
t.Fatalf("kept = %+v, want the stream SEAT_node-login-shell_gated-ended, last per subject, one day", kept)
}
marked := []string{"seat:node-login-shell.execute"}
stream := "SEAT_node-login-shell_gated-ended"
caller, err := PermissionsFor(Principal{Kind: KindModule, Node: "a", Module: "shop", Invokes: marked, MarkedVerbs: marked})
if err != nil {
t.Fatal(err)
}
own := "mesh.seat.node-login-shell.event.gated-ended.a=2eshop.>"
if !slices.Contains(caller.Subscribe, own) || !slices.Contains(caller.Publish, "$JS.API.CONSUMER.CREATE."+stream+".*."+own) ||
!slices.Contains(caller.Publish, "$JS.API.CONSUMER.MSG.NEXT."+stream+".*") {
t.Errorf("the caller hears %v and reads %v, want its own ends alone", caller.Subscribe, caller.Publish)
}
other, _ := PermissionsFor(Principal{Kind: KindModule, Node: "a", Module: "bank", MarkedVerbs: marked})
for _, s := range append(other.Subscribe, other.Publish...) {
if slices.Contains([]string{own, "$JS.API.STREAM.INFO." + stream}, s) {
t.Errorf("a module that does not invoke the marked verb is granted %s", s)
}
}
proc, err := PermissionsFor(Principal{Kind: KindProcess, Node: "a", Module: "shop", Process: "run", Invokes: marked, MarkedVerbs: marked})
if err != nil {
t.Fatal(err)
}
if !slices.Contains(proc.Subscribe, "mesh.seat.node-login-shell.event.gated-ended.a=2eshop=2erun.>") {
t.Errorf("a process hears %v, want its own account's ends", proc.Subscribe)
}
holder, err := PermissionsFor(Principal{Kind: KindProcess, Node: "a", Module: "shell", Process: "run", AnswersSeat: marked, MarkedVerbs: marked})
if err != nil {
t.Fatal(err)
}
if !slices.Contains(holder.Publish, "mesh.seat.node-login-shell.event.gated-ended.*.*") || slices.Contains(holder.Subscribe, own) {
t.Errorf("the holder publishes %v, want gated-ended to any caller and no caller's hearing", holder.Publish)
}
}
@@ -218,6 +218,9 @@ type Principal struct {
Invokes []string
// AnswersSeat are the seat verbs a process answers, each seat:<seat>.<verb> (design 47 G A-109), of a process with a bus on its own account, else of the module's own (A-115).
AnswersSeat []string
// MarkedVerbs are the marked verbs of its module's declaration (Declared.MarkedVerbs): those of them it invokes by name it
// hears the ends of on gated-ended, and those it answers it says them on (design 47 G §677).
MarkedVerbs []string
// AsksIssued are, for the vault's holder, the tools it asks in its own name for a credential a provider's software
// issued, each `<module>.<tool>` (novox/hq ADR 0305, issue 723, vaultCalls): on the subject that names its caller,
@@ -1107,6 +1110,9 @@ func permissionsFor(p Principal) (Permissions, error) {
}
}
}
// 4a. The ends of its calls of a marked verb, on the seat's gated-ended, its own alone (design 47 G §677, A-264).
gp, gs := gatedEndedGrants(p.Username(), p.MarkedVerbs, p.Invokes, p.AnswersSeat)
pub, sub = append(pub, gp...), append(sub, gs...)
// 5. Its state, and the state of others it reads (novox/hq ADR 0201): every one read and
// watched, its own written too.
@@ -79,6 +79,11 @@ func processGrants(p Principal) (pub, sub []string, err error) {
}
sub = append(sub, answered...)
// 5c. The ends of its calls of a marked verb, heard on the seat's gated-ended, its own alone; and those of the marked verbs
// it answers, said to any caller (design 47 G §677, A-264).
gp, gs := gatedEndedGrants(p.Username(), p.MarkedVerbs, p.Invokes, p.AnswersSeat)
pub, sub = append(pub, gp...), append(sub, gs...)
// 6. The bus module's process (busstate.go, ADR 0595 §4, §5): every module's bus state it keeps, by name; its own
// consumer of what its module consumes; its module's own stream's events; and its conditions through the seats its
// module uses (the operator channel's accept.condition in its module's name).
@@ -115,6 +115,9 @@ type Declared struct {
// ServesOwn are the seat verbs of this module's processes that declare no bus, each seat:<seat>.<verb>: such a process
// runs on the module's own account, which is granted their subscribes (design 47 G A-115, A-127).
ServesOwn []string
// MarkedVerbs are the marked verbs (`seat:<seat>.<verb>`, definition-marked, design 47 G §677) the module or one of its
// processes invokes or answers: each caller hears its own calls' ends on the seat's gated-ended (gatedEndedGrants).
MarkedVerbs []string
// Ledgers are the local names of the ledgers it asks for in `requires` (catalogue.StorageLedger); each assignment of a
// module with any gets a ledger principal of its own (issue 1966 D5, ADR 0597 §9).
Ledgers []string
@@ -295,7 +298,7 @@ func Users(r Records) ([]Principal, error) {
out = append(out, Principal{
Kind: KindProcess, Node: node, Module: d.Module, Process: pr.Process,
Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks,
Invokes: pr.Invokes, AnswersSeat: pr.Serves, Removes: pr.Removes,
Invokes: pr.Invokes, AnswersSeat: pr.Serves, Removes: pr.Removes, MarkedVerbs: d.MarkedVerbs,
})
// The process of the module holding mesh-broker whose bus declares `keeps` keeps every module's bus state
// (busstate.go, ADR 0595 §4, ADR 0626 §1); `keeps` on any other module composes nothing (the judge refuses it,
@@ -341,6 +344,7 @@ func Users(r Records) ([]Principal, error) {
PerMachine: perMachineNames(d.State), ReadOnly: readOnlyNames(d.State), KeyedReads: d.KeyedReads,
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
Objects: objectNames(d.Objects), ReadsObjects: d.ReadsObjects, Bound: d.Bindings, AnswersSeat: d.ServesOwn,
MarkedVerbs: d.MarkedVerbs,
})
if d.HoldsRecovery {
out[len(out)-1].Recovers = r.RecoveryNames
@@ -184,6 +184,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
d.Marked = append(d.Marked, mark)
}
}
d.MarkedVerbs = markedNamed(d, seats)
d.CatchUps = catchUpsOf(m)
if _, reads := m.OwnSecrets["broker"]; !reads {
d.NoAccount = true
@@ -431,6 +432,27 @@ func markedVerbs(m catalogue.Manifest, s catalogue.SeatDeclaration) []string {
return out
}
// markedNamed are the verbs a seat definition marks (design 46 approval mark §2; design 47 G §677) that the declaration's module
// or one of its processes invokes by name or answers, each `seat:<seat>.<verb>`, sorted.
func markedNamed(d buscompose.Declared, seats map[string]catalogue.SeatDeclaration) []string {
named := slices.Concat(d.Invokes, d.ServesOwn)
for _, pr := range d.Processes {
named = append(named, slices.Concat(pr.Invokes, pr.Serves)...)
}
var out []string
for _, v := range named {
seat, verb, ok := catalogue.SeatInvoke(v)
if !ok {
continue
}
if i := slices.IndexFunc(seats[seat].Serves, func(s catalogue.Verb) bool { return s.Name == verb }); i >= 0 && seats[seat].Serves[i].NeedsApproval {
out = append(out, v)
}
}
slices.Sort(out)
return slices.Compact(out)
}
func asSeat(s catalogue.SeatDeclaration, declarer string) buscompose.Seat {
seat := buscompose.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}
@@ -44,3 +44,19 @@ func TestSentStoresRead(t *testing.T) {
t.Errorf("a used seat's verbs carry %v, a held seat no definition names %v", d.Uses[0].VerbCallers, d.Holds[0].VerbCallers)
}
}
// The marked verbs a declaration names (design 47 G §677): a definition-marked verb its module invokes or a process answers,
// and no unmarked verb.
func TestTheMarkedVerbsADeclarationNames(t *testing.T) {
seats := map[string]catalogue.SeatDeclaration{"node-login-shell": {Name: "node-login-shell",
Serves: []catalogue.Verb{{Name: "execute", NeedsApproval: true}, {Name: "list"}}}}
d := buscompose.Declared{Invokes: []string{"seat:node-login-shell.execute", "seat:node-login-shell.list", "shop.tool"},
Processes: []buscompose.ProcessPrincipal{{Serves: []string{"seat:node-login-shell.execute"}}}}
if got := markedNamed(d, seats); !slices.Equal(got, []string{"seat:node-login-shell.execute"}) {
t.Errorf("marked = %v", got)
}
m := catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-login-shell.execute"}}
if got := declaredFor(m, seats, nil).MarkedVerbs; !slices.Equal(got, []string{"seat:node-login-shell.execute"}) {
t.Errorf("the declaration carries %v, want the marked verb it invokes", got)
}
}