GR8: a seat's verb is granted to its users only where its effective callers name them

Step 4 of a module's permissions granted every user of a seat every verb the seat
serves, so a kept-artifact call was composed for any module using a build seat. The
compiled Seat now carries each verb's effective callers (seatdef EffectiveCallers, the
class defaults), filled where the askers of its queues are, and a user is granted a
verb's call subject only where those callers name it: invokers that invoke it, its
module, or a seat it holds. A seat whose definitions are unread grants its users no verb.

Builds: design 47 G §610, §336, §271 (test 60), A-165, A-179

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 06:12:51 +01:00
parent eb45c3c67c
commit 5a7747f2bf
5 changed files with 95 additions and 1 deletions
@@ -67,3 +67,47 @@ func TestNamedAsks(t *testing.T) {
t.Errorf("the work queue's consumers: %d, want 4 (a main and a named worker for each of two queues)", len(consumers))
}
}
// Design 47 G §610, A-179 (test 60's case of §271): a user of a seat is granted a verb's call subject only where the verb's
// effective callers name it, so a kept-artifact call is composed for the module that invokes it and for no other user of
// the build seat; `module:` and `seat:` entries name theirs, and a seat whose definition is unread grants its users no verb.
func TestAVerbIsGrantedToItsEffectiveCallersAlone(t *testing.T) {
build := Seat{Name: "node-build-go", Scope: "node", Serves: []string{"kept-artifact", "freed", "run"},
VerbCallers: map[string][]string{
"kept-artifact": {"invokers", "persons", "sessions"},
"freed": {"module:a-checker"},
"run": {"seat:mesh-pipeline-runner"},
}}
user := func(module string, invokes []string, held ...string) Principal {
p := Principal{Kind: KindModule, Node: "one", Module: module, Uses: []Seat{build}, Invokes: invokes}
for _, h := range held {
p.Holds = append(p.Holds, Seat{Name: h})
}
return p
}
unread := build
unread.VerbCallers = nil
const base = "mesh.seat.node-build-go.tool."
for _, c := range []struct {
who Principal
want map[string]bool
}{
{user("an-invoker", []string{"seat:node-build-go.kept-artifact"}), map[string]bool{
base + "kept-artifact.ace": true, base + "freed.ace": false, base + "run.ace": false}},
{user("a-runner", nil, "mesh-pipeline-runner"), map[string]bool{
base + "kept-artifact.ace": false, base + "freed.ace": false, base + "run.ace": true}},
{user("a-checker", nil), map[string]bool{base + "kept-artifact.ace": false, base + "freed.ace": true}},
{Principal{Kind: KindModule, Node: "one", Module: "a-checker", Uses: []Seat{unread}}, map[string]bool{
base + "freed.ace": false, base + "kept-artifact.ace": false}},
} {
perms, err := PermissionsFor(c.who)
if err != nil {
t.Fatal(err)
}
for subject, want := range c.want {
if got := MayPublish(perms, subject); got != want {
t.Errorf("%s may publish %s: %v, want %v", c.who.Module, subject, got, want)
}
}
}
}
@@ -28,6 +28,7 @@ import (
sdkbc "git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/membership"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// A Kind is what a principal is, which decides the shape of its authority rather than its
@@ -107,6 +108,11 @@ type Seat struct {
// entries of the callers of the accepted kinds the definition puts on that queue (design 47 G §543, §600). The asker
// in a named ask's subject is the asking seat's name (A-163). Empty where the seat list is unread.
AcceptCallers map[string][]string
// VerbCallers are, by verb, the callers each verb the seat serves admits: its definition's effective list
// (seatdef.Verb.EffectiveCallers, the class defaults of design 47 G §610). A user of the seat is granted a verb's call
// subject only where that list names it (A-179), so the bus holds every other caller out. Empty where the seat list is
// unread, and then no verb of the seat is granted to its users.
VerbCallers map[string][]string
// HeldByTheEngine says every node's engine holds the seat on its own account (catalogue.Seat.HeldByTheEngine, novox/hq
// ADR 0563 §1): each of its events names the engine's machine as its last token (machineEvent).
HeldByTheEngine bool
@@ -1061,7 +1067,10 @@ func permissionsFor(p Principal) (Permissions, error) {
pub = append(pub, seatSubject(s, "accept", a))
}
for _, t := range s.Serves {
pub = append(pub, seatToolSubject(s, t, "*"))
// Only a verb whose effective callers name this module (design 47 G A-179, §610).
if callerOf(p, s, t) {
pub = append(pub, seatToolSubject(s, t, "*"))
}
}
}
@@ -1358,6 +1367,27 @@ func seatSubject(s Seat, kind, verb string) string {
return "mesh.seat." + s.Name + "." + kind + "." + verb
}
// callerOf says whether a verb's effective callers (Seat.VerbCallers) name the module principal p: `invokers` where its
// manifest invokes the verb (`seat:<seat>.<verb>`, or every tool by `*`), `module:<module>` naming it, or `seat:<seat>`
// naming a seat it holds (design 47 G §336, §610, A-179). `operator`, `persons` and `sessions` name no module.
func callerOf(p Principal, s Seat, verb string) bool {
for _, c := range s.VerbCallers[verb] {
switch {
case c == seatdef.CallerInvokers:
if slices.Contains(p.Invokes, "*") || slices.Contains(p.Invokes, "seat:"+s.Name+"."+verb) {
return true
}
case c == seatdef.CallerModulePrefix+p.Module:
return true
}
if seat, ok := strings.CutPrefix(c, seatdef.CallerSeatPrefix); ok &&
slices.ContainsFunc(p.Holds, func(h Seat) bool { return h.Name == seat }) {
return true
}
}
return false
}
// seatToolSubject is where a role's tool is asked. Mesh-wide for a mesh-scoped seat; a node-scoped
// seat carries the node it is asked of, because a flat subject would reach every machine's holder
// and the queue group would silently pick a winner (novox/hq ADR 0132, design 33 §4). A holder
@@ -177,3 +177,13 @@ func withAcceptCallers(askers map[string]map[string][]string, d *buscompose.Decl
}
}
}
// withVerbCallers gives each seat a declaration holds, uses or watches the effective callers of its verbs
// (Seat.VerbCallers, design 47 G §610, A-179), as withAcceptCallers gives the askers of its queues.
func withVerbCallers(callers map[string]map[string][]string, d *buscompose.Declared) {
for _, list := range []*[]buscompose.Seat{&d.Holds, &d.Uses, &d.Watches} {
for i := range *list {
(*list)[i].VerbCallers = callers[(*list)[i].Name]
}
}
}
@@ -110,6 +110,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
d.Bindings = in.Bindings[m.Module]
withEventSubjects(in.EventSubjects, &d)
withAcceptCallers(in.AcceptCallers, &d)
withVerbCallers(in.VerbCallers, &d)
return d
}
d := declare(m)
@@ -142,6 +143,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
withEventSubjects(in.EventSubjects, out.Engine)
withAcceptCallers(in.AcceptCallers, out.Engine)
withVerbCallers(in.VerbCallers, out.Engine)
}
return out, gaps
}
@@ -6,6 +6,8 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"mesh-grants/internal/buscompose"
)
// The sent stores a module reads follow the catalog graph (design 47 G §59, §124): the consumers of a module's events and the
@@ -34,5 +36,11 @@ func TestSentStoresRead(t *testing.T) {
if got := sentStoresRead(c.m, all, callers); !slices.Equal(got, c.want) {
t.Errorf("%s: %v, want %v", c.name, got, c.want)
}
} // The same callers reach each seat a declaration holds, uses or watches, from which its users' call subjects are
// composed (design 47 G A-179).
d := buscompose.Declared{Module: "bank", Uses: []buscompose.Seat{{Name: "orders-seat"}}, Holds: []buscompose.Seat{{Name: "billing"}}}
withVerbCallers(callers, &d)
if !slices.Equal(d.Uses[0].VerbCallers["place"], callers["orders-seat"]["place"]) || d.Holds[0].VerbCallers != nil {
t.Errorf("a used seat's verbs carry %v, a held seat no definition names %v", d.Uses[0].VerbCallers, d.Holds[0].VerbCallers)
}
}