From b088bdaaef706662aaabffcf7080e017ec50064e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 18:43:40 +0100 Subject: [PATCH] Names N2a: a callers list names a seat, `seat:`, resolved by the part's holder function A tool's callers list named modules ("node-tools", "mesh-healers"). A-295: an entry `seat:` allows a module caller whose module holds that seat, as a holder function the part is given answers it from what it last heard of the assigner's seat-holders view, so no call is made per check. VerbCallers.AllowedWith and seatlist.RefusalWith/ToolsWith take the function; Allowed, Refusal and Tools keep their form (no seat entry allows anyone without one); a seatlist.Part is given it by Holders. Builds: design 47 G A-210, A-295 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/caller_test.go | 16 ++++++++++++++++ go/callers.go | 19 +++++++++++++++++-- go/seatlist/part.go | 16 ++++++++++++++-- go/seatlist/seatlist.go | 15 +++++++++++++-- go/seatlist/seatlist_test.go | 13 +++++++++++++ 5 files changed, 73 insertions(+), 6 deletions(-) diff --git a/go/caller_test.go b/go/caller_test.go index 72e90b22..01b645f4 100644 --- a/go/caller_test.go +++ b/go/caller_test.go @@ -140,6 +140,22 @@ func TestACallerInTheArgumentsIsNeverTheCaller(t *testing.T) { t.Errorf("%s by %q allowed: %v, want %v", c.verb, c.caller, got, c.want) } } + // A-295: an entry `seat:` allows the modules the part's holder function answers for the seat, and + // nobody without holders. + seats := VerbCallers{"again": {AnyPerson, "seat:node-tool-runner", "seat:mesh-healers"}} + holders := func(seat string) []string { + return map[string][]string{"node-tool-runner": {"node-tools"}, "mesh-healers": {"mesh-delivery-watchman"}}[seat] + } + for _, c := range []struct { + caller string + holders func(string) []string + want bool + }{{"ace.node-tools", holders, true}, {"g14.mesh-delivery-watchman", holders, true}, {"ace.mesh-build-go", holders, false}, + {"person.jo", holders, true}, {"ace.node-tools", nil, false}} { + if got := seats.AllowedWith("again", c.caller, c.holders) == nil; got != c.want { + t.Errorf("again by %q (holders %v) allowed: %v, want %v", c.caller, c.holders != nil, got, c.want) + } + } if _, err := ParseVerbCallers(`{"mirror":["x"]}`, []string{"publish"}, nil); err == nil { t.Error("a list naming a verb the holder does not serve was read") } diff --git a/go/callers.go b/go/callers.go index 2e0f5aab..e502200f 100644 --- a/go/callers.go +++ b/go/callers.go @@ -80,14 +80,29 @@ func ParseVerbCallers(raw string, verbs []string, defaults VerbCallers) (VerbCal return c, nil } +// CallerSeatPrefix opens an entry naming a seat, `seat:`: every module holding it, so a list names no module (design 47 +// G A-210, A-295). +const CallerSeatPrefix = "seat:" + // Allowed answers nil when caller, as the runtime proved it, may call verb; else the refusal naming both. An unproven -// caller is never allowed, not even by AnyCaller. -func (c VerbCallers) Allowed(verb, caller string) error { +// caller is never allowed, not even by AnyCaller. An entry `seat:` allows no one here (AllowedWith). +func (c VerbCallers) Allowed(verb, caller string) error { return c.AllowedWith(verb, caller, nil) } + +// AllowedWith is Allowed whose entries `seat:` allow a module caller whose module holders answers for that seat: +// the part's holder function, which answers from what it last heard of the assigner's seat-holders view (design 47 G A-295), +// so no call is made per check. A nil holders, or a seat it answers none for, allows no one by that entry. +func (c VerbCallers) AllowedWith(verb, caller string, holders func(seat string) []string) error { if caller == "" || caller == Unproven { return fmt.Errorf("%s: refused: the request carries no caller the tool runner proved", verb) } person := strings.HasPrefix(caller, "person.") for _, entry := range c[verb] { + if seat, ok := strings.CutPrefix(entry, CallerSeatPrefix); ok { + if !person && holders != nil && slices.Contains(holders(seat), ModuleOf(caller)) { + return nil + } + continue + } switch entry { case AnyCaller: return nil diff --git a/go/seatlist/part.go b/go/seatlist/part.go index 00a4d6ab..3d564798 100644 --- a/go/seatlist/part.go +++ b/go/seatlist/part.go @@ -21,6 +21,18 @@ type Part struct { prefix string callers []string reader atomic.Pointer[Reader] + holders atomic.Pointer[func(seat string) []string] +} + +// Holders gives the part the function that answers a seat's holders, by which its callers' entries `seat:` are +// resolved when a call is checked (design 47 G A-295): the part's follower of the assigner's seat-holders view. +func (p *Part) Holders(h func(seat string) []string) { p.holders.Store(&h) } + +func (p *Part) holdersOf(seat string) []string { + if h := p.holders.Load(); h != nil && *h != nil { + return (*h)(seat) + } + return nil } // NewPart is the wiring of a part whose tools are named `_seat_list` and `_seat_list_again`, the second @@ -112,7 +124,7 @@ func (p *Part) ToolNames() (read, again string) { // reader has not started. func (p *Part) Tools() []stdio.Tool { read, again := p.ToolNames() - shape := Tools(&Reader{}, read, again, p.callers) + shape := ToolsWith(&Reader{}, read, again, p.callers, p.holdersOf) for i := range shape { run := i shape[i].Run = func(args map[string]any) (any, error) { @@ -120,7 +132,7 @@ func (p *Part) Tools() []stdio.Tool { if r == nil { return nil, errors.New("the seat list reader has not started yet: the runtime has not served this module") } - return Tools(r, read, again, p.callers)[run].Run(args) + return ToolsWith(r, read, again, p.callers, p.holdersOf)[run].Run(args) } } return shape diff --git a/go/seatlist/seatlist.go b/go/seatlist/seatlist.go index db1048b9..78499c88 100644 --- a/go/seatlist/seatlist.go +++ b/go/seatlist/seatlist.go @@ -470,6 +470,11 @@ func (r *Reader) changed() { // answers only to the callers the host's list names (the operator and the healers). The host names the tools and the list; // again records nothing but the read it asked. func Tools(r *Reader, read, again string, callers []string) []stdio.Tool { + return ToolsWith(r, read, again, callers, nil) +} + +// ToolsWith is Tools whose callers' entries `seat:` are resolved by holders (RefusalWith, design 47 G A-295). +func ToolsWith(r *Reader, read, again string, callers []string, holders func(seat string) []string) []stdio.Tool { return []stdio.Tool{ {Name: read, Description: "The seat list this part holds (novox/hq ADR 0636): each seat's definition with its source (trunk or bootstrap) and " + @@ -484,7 +489,7 @@ func Tools(r *Reader, read, again string, callers []string) []stdio.Tool { "and answer what it holds afterwards. Answered only to the operator and the healers.", Input: map[string]any{"type": "object", "properties": map[string]any{}}, Run: func(args map[string]any) (any, error) { - if err := Refusal(again, callers, stdio.CallerOf(args)); err != nil { + if err := RefusalWith(again, callers, stdio.CallerOf(args), holders); err != nil { return nil, err } err := r.Reload() @@ -502,7 +507,13 @@ func Tools(r *Reader, read, again string, callers []string) []stdio.Tool { // callers, the entries of a verb's list as VerbCallers reads them, so stdio.AnyPerson names every operator's session // the runtime proved (person.) and a module name its instances (ADR 0636 §7). func Refusal(tool string, callers []string, caller string) error { - if err := (stdio.VerbCallers{tool: callers}).Allowed(tool, caller); err != nil { + return RefusalWith(tool, callers, caller, nil) +} + +// RefusalWith is Refusal whose entries `seat:` allow the modules holders answers for the seat (stdio.VerbCallers. +// AllowedWith, design 47 G A-295). +func RefusalWith(tool string, callers []string, caller string, holders func(seat string) []string) error { + if err := (stdio.VerbCallers{tool: callers}).AllowedWith(tool, caller, holders); err != nil { return fmt.Errorf("%s is refused to %q: it answers only %s (novox/hq ADR 0636 §7)", tool, caller, strings.Join(callers, ", ")) } return nil diff --git a/go/seatlist/seatlist_test.go b/go/seatlist/seatlist_test.go index 9382bf08..da318d15 100644 --- a/go/seatlist/seatlist_test.go +++ b/go/seatlist/seatlist_test.go @@ -284,6 +284,19 @@ func TestTheTwoToolsReadTheHeldListAndReadItAgainForTheirCallers(t *testing.T) { if _, err := part.Tools()[1].Run(map[string]any{}); err == nil || !strings.Contains(err.Error(), "it answers only operator, mesh-healers") { t.Fatalf("again without a proven caller: %v", err) } + // A-295: the part's list names seats, `seat:`, resolved by the holder function it is given when a call is checked. + bySeat := []string{stdio.AnyPerson, "seat:mesh-healers"} + if err := RefusalWith("seat_list_again", bySeat, "ace.mesh-delivery-watchman", part.holdersOf); err == nil { + t.Fatal("a seat entry allowed a module before the part was given its holders") + } + part.Holders(func(seat string) []string { + return map[string][]string{"mesh-healers": {"mesh-delivery-watchman"}}[seat] + }) + for caller, allowed := range map[string]bool{"ace.mesh-delivery-watchman": true, "ace.mesh-build-go": false, "person.jo": true} { + if err := RefusalWith("seat_list_again", bySeat, caller, part.holdersOf); (err == nil) != allowed { + t.Errorf("again by %s through the seat entry: %v, want allowed %v", caller, err, allowed) + } + } owner2.table["node-c"] = def("node-c", "node", "put") if err := owner2.say(ChangedEvent, "node-c", SourceTrunk); err != nil || len(woken) == 0 { t.Fatalf("a change did not wake the part: %v", err)