A-165 callers: the tool runner refuses a seat verb's caller outside its effective callers before the bundle

Where serveSeats answers a seat's verb it reads the verb from the runner's seat list (seatdef's
EffectiveCallers): an unproven caller is refused, and every caller while the seat's definition is unread,
names no such verb or opens it to none; a proven caller is among them, since the grants part composes the
call subject only for the effective callers. A module's tool on the subject naming no caller is unchanged.

Builds: design 47 G §610; lead answers A-165, A-179

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:49:57 +01:00
parent afc327392e
commit e7e20e3d13
4 changed files with 86 additions and 9 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ func main() {
Build: os.Getenv(runtime.FingerprintWord), Hear: hearOne(conn)})
// A failed read or write of the state approvals is made again at the next membership change the runner hears (A-157).
conn.OnMembership(func(bus.Membership) { go gate.StateAgain() })
opts = append(opts, runtime.WithGate(gate))
opts = append(opts, runtime.WithGate(gate), runtime.WithSeatDefinitions(definitionsOf(seatList)))
// A run still unrecorded is asked again at each start and when the hand-act seat's holder is heard again (lead answer A-157).
if stopBounds, err := gate.WatchBounds(); err != nil {
log.Printf("[mesh-tools] the gate's bound keys are not watched (%v): an unanswered hand-act is said in the log only", err)
+35 -4
View File
@@ -2,9 +2,12 @@ package runtime
import (
"encoding/json"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-tools/node-tools/internal/bus"
@@ -24,7 +27,7 @@ func TestACallOnTheCallerNamedSubjectReachesTheModuleWithItsCaller(t *testing.T)
mesh := mt.New(t)
mesh.Issue(t, mt.MembershipOf("ledger", "anchor", true, map[string][]string{"node-desk": {"who"}}))
nodeTools := connect(t, "node-tools", "anchor")
stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {})
stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {}, deskDefined)
if err != nil {
t.Fatal(err)
}
@@ -62,13 +65,20 @@ func TestACallOnTheCallerNamedSubjectReachesTheModuleWithItsCaller(t *testing.T)
}
}
// The subject that names no caller is still served, for one release (retired by hq issue 464), and says so:
// the module is named no caller, whatever the body claims.
// deskDefined is the seat list's definition of the fixture seat node-desk, its verb who taking its class's default.
var deskDefined = WithSeatDefinitions(func() map[string]seatdef.Definition {
return map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who"}}}}
})
// The subject that names no caller is still served for a module's tool, for one release (retired by hq issue 464), and says
// so: the module is named no caller, whatever the body claims. A seat's verb refuses it before the bundle runs (design 47 G
// §610; lead answers A-165, A-179), and refuses every caller while the seat's definition is unread, names no such verb or
// opens it to none.
func TestTheSubjectThatNamesNoCallerStillWorksAndNamesNone(t *testing.T) {
mesh := mt.New(t)
mesh.Issue(t, mt.MembershipOf("ledger", "anchor", true, map[string][]string{"node-desk": {"who"}}))
nodeTools := connect(t, "node-tools", "anchor")
stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {})
stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {}, deskDefined)
if err != nil {
t.Fatal(err)
}
@@ -91,8 +101,29 @@ func TestTheSubjectThatNamesNoCallerStillWorksAndNamesNone(t *testing.T) {
}
return err
})
if strings.HasPrefix(subject, "mesh.seat.") {
if !strings.Contains(reply.Error, "node-desk.who takes no unproven caller") || reply.Result.Call != "" {
t.Errorf("%s: an unproven caller of a seat's verb was answered %+v (error %q), want refused before the bundle", subject, reply.Result, reply.Error)
}
continue
}
if reply.Error != "" || reply.Result.Caller != "" {
t.Errorf("%s: the module was named caller %q (error %q), want none", subject, reply.Result.Caller, reply.Error)
}
}
for _, c := range []struct {
defs map[string]seatdef.Definition
says string
}{
{nil, "is not yet read"},
{map[string]seatdef.Definition{"node-desk": {Seat: "node-desk"}}, "names no verb who"},
{map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who", Callers: &seatdef.Callers{List: []string{}}}}}}, "open to no caller"},
{map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who", Class: seatdef.ClassAct}}}}, ""},
} {
defs := c.defs
err := (&consumers{definitions: func() map[string]seatdef.Definition { return defs }}).admitted("node-desk", "who", "shanks.node-tools")
if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) {
t.Errorf("a proven caller with the definitions %v: %v, want %q", c.defs, err, c.says)
}
}
}
+38
View File
@@ -1,6 +1,10 @@
package runtime
import (
"fmt"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"github.com/novox/mesh-tools/node-tools/internal/approval"
"github.com/novox/mesh-tools/node-tools/internal/launch"
)
@@ -33,6 +37,40 @@ func WithGate(g Gate) Option { return func(c *consumers) { c.gate = g } }
var _ Gate = (*approval.Gate)(nil)
// WithSeatDefinitions refuses, where a seat's verb is answered, a caller outside the verb's effective callers, before the
// bundle runs (design 47 G §610; lead answers A-165, A-179): defs are the seat definitions the runner's seat list holds.
// Without it a verb is answered for every caller, as before.
func WithSeatDefinitions(defs func() map[string]seatdef.Definition) Option {
return func(c *consumers) { c.definitions = defs }
}
// admitted says why a seat's verb is refused to a caller, nil when it is answered: an unproven caller is refused, and so is
// every caller while the seat's definition is not read, names no such verb, or the verb's effective callers are empty; a
// proven caller is among them, since the grants part composes a verb's call subject only for its effective callers (A-179).
func (c *consumers) admitted(seat, verb, caller string) error {
if c.definitions == nil {
return nil
}
if caller == "" {
return fmt.Errorf("Not run: %s.%s takes no unproven caller: call it on its subject that names its caller, "+
"mesh.seat.%s.call.%s.<caller>", seat, verb, seat, verb)
}
def, ok := c.definitions()[seat]
if !ok {
return fmt.Errorf("Not run: the definition of the seat %s is not yet read by this tool runner's seat list, so no caller "+
"of %s.%s is admitted", seat, seat, verb)
}
for _, v := range def.Verbs {
if v.Name == verb {
if len(v.EffectiveCallers()) == 0 {
return fmt.Errorf("Not run: %s.%s is open to no caller", seat, verb)
}
return nil
}
}
return fmt.Errorf("Not run: the seat %s's definition names no verb %s, so no caller is admitted", seat, verb)
}
// unservedMarks is, of the module's marked tools, those its bundle's registrations under its own name do not register.
func (r *runner) unservedMarks(module string, regs []launch.Registration) []string {
if r.events.gate == nil {
+12 -4
View File
@@ -16,6 +16,8 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"github.com/novox/mesh-tools/node-tools/internal/announce"
"github.com/novox/mesh-tools/node-tools/internal/approval"
"github.com/novox/mesh-tools/node-tools/internal/bus"
@@ -304,7 +306,7 @@ func Run(conn *bus.Conn, served []Served, envs map[string]map[string]string, log
}
logf("%s", line)
conn.OnMembership(func(bus.Membership) { go func() { r.refreshRepairs(); conn.Flush() }() })
stopSeats, again := serveSeats(conn, r.modules, r.registrations, r.events.gated, r.fingerprintOf, logf)
stopSeats, again := serveSeats(conn, r.modules, r.registrations, r.events.gated, r.events.admitted, r.fingerprintOf, logf)
r.seatsAgain = again
r.stops = append(r.stops, stopSeats)
@@ -707,8 +709,8 @@ func subjectsOf(conn *bus.Conn, module, tool string) []string {
// the tool of the same name registered under the seat's name (ADR 0159, 0160) — and serves again
// whenever a membership changes, and when again is called because a bundle was loaded again. Whether
// this machine holds the seat is the bus's to decide. It answers its stop and again.
func serveSeats(conn *bus.Conn, modules []string, current func() []registration, gated gatedRun, fingerprintOf func(string) string,
logf func(string, ...any)) (func(), func()) {
func serveSeats(conn *bus.Conn, modules []string, current func() []registration, gated gatedRun,
admitted func(seat, verb, caller string) error, fingerprintOf func(string) string, logf func(string, ...any)) (func(), func()) {
var mu sync.Mutex
var stops []func()
stopped := false
@@ -747,9 +749,13 @@ func serveSeats(conn *bus.Conn, modules []string, current func() []registration,
logf("[mesh-tools] %s claims %s and implements no %s, which that seat promises; not served", module, v.Seat, v.Verb)
continue
}
// Through the approval gate, before the bundle (design 46 7-approval-mark §20).
// Its callers checked, then through the approval gate, before the bundle (design 47 G §610, A-179; design 46
// 7-approval-mark §20).
module, v := module, v
stop, err := conn.HandleSubjectCalled(v.Subject, func(caller string, body json.RawMessage) (any, error) {
if err := admitted(v.Seat, v.Verb, caller); err != nil {
return nil, err
}
return gated(approval.Call{Address: v.Seat + "." + v.Verb, Module: module, Seat: v.Seat, Name: v.Verb,
Fingerprint: fingerprintOf(module), Caller: caller, Args: argsOf(body), Description: t.Description}, t, caller)
})
@@ -794,6 +800,8 @@ type consumers struct {
sessions sessionTable
// gate is the approval gate every call is served through (gate.go); nil serves every call as before.
gate Gate
// definitions are the seat definitions a seat verb's callers are read from (gate.go); nil admits every caller.
definitions func() map[string]seatdef.Definition
}
type moduleEvents struct {