diff --git a/modules/mesh-grants/cmd/mesh-grants/main.go b/modules/mesh-grants/cmd/mesh-grants/main.go index d7d36e14..97161aab 100644 --- a/modules/mesh-grants/cmd/mesh-grants/main.go +++ b/modules/mesh-grants/cmd/mesh-grants/main.go @@ -40,6 +40,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/seatdef" "git.novox.be/novox/mesh-sdk/go/seatlist" "git.novox.be/novox/mesh-sdk/go/views" @@ -148,8 +149,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro }} f.Part.Bindings.Start(ctx, f.Part) // The seat list: read over the connection and the hub, given to catalogue at every change, and a pass composes again then. - gate := &seatGate{} - gate.follow(seatlist.ProcessBus(conn.Call, hub), f.Wake, logger.Printf) + gate := newSeatGate() + gate.Follow(seatlist.ProcessBus(conn.Call, hub), f.Wake, logger.Printf) f.Part.SeatList = gate f.Part.Say = sayCond fv.Start(ctx, f.Wake) @@ -220,7 +221,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err := hub.Start(ctx, js, account); err != nil { return err } - gate.eventsLive() + gate.EventsLive() hears, err := grants.OwnHears(ownStore{conn}, account.Node) if err != nil { return err @@ -292,3 +293,24 @@ func hearRunning(ctx context.Context, account process.Account, js jetstream.JetS hearingRunning.cc = cc return nil } + +// seatGate is the part's seat-list wiring (mesh-sdk seatlist.Part, ADR 0636 §1, §7) that also says the definitions the +// list holds, which the part makes each kept event's stream from. +type seatGate struct{ *seatlist.Part } + +// seatListAgainCallers may read the seat list again (ADR 0636 §7): every operator's session and the healers. A call through +// the mesh MCP server is proved only as its machine's tool runner (.node-tools, issue 627), so node-tools stands for an +// operator's session until a session is proved (issue 1966). +var seatListAgainCallers = []string{stdio.AnyPerson, "node-tools", "mesh-healers"} + +func newSeatGate() *seatGate { + return &seatGate{seatlist.NewPart("grants", seatListAgainCallers)} +} + +// Definitions are the seat definitions the reader holds; none before it has started. +func (g *seatGate) Definitions() map[string]seatdef.Definition { + if r := g.Reader(); r != nil { + return r.Definitions() + } + return nil +} diff --git a/modules/mesh-grants/cmd/mesh-grants/seatlist.go b/modules/mesh-grants/cmd/mesh-grants/seatlist.go deleted file mode 100644 index 7b30bb59..00000000 --- a/modules/mesh-grants/cmd/mesh-grants/seatlist.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "errors" - "mesh-grants/internal/grants" - "sync/atomic" - - stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/seatdef" - "git.novox.be/novox/mesh-sdk/go/seatlist" -) - -// The grants part composes from the seat list it reads (novox/hq ADR 0635 §1, ADR 0636 §1, D-30): through the seat -// mesh-catalog's verb seat-definitions and its two events, from the bootstrapped definitions until the seat has answered. What -// the reader holds is given to catalogue at every change, which composes every membership and grant, and a module that names a -// seat the list does not yet is held back, as a manifest waiting for a judge is, and what runs is composed from its running -// build's manifest. The process reads over its connection and the event hub; the tools' bundle over the runtime. - -// seatListTools are the reader's two tools (ADR 0636 §7): every operator's session and the healers may read again. A call -// through the mesh MCP server is proved only as its machine's tool runner (.node-tools, issue 627), so node-tools -// stands for an operator's session until a session is proved (issue 1966). -var seatListAgainCallers = []string{stdio.AnyPerson, "node-tools", "mesh-healers"} - -// seatGate is the reader the part gates by, set once it is started; before, it acts on nothing new. -type seatGate struct { - reader atomic.Pointer[seatlist.Reader] -} - -func (g *seatGate) MayAct(seats ...string) bool { - r := g.reader.Load() - return r != nil && r.MayAct(seats...) -} - -// Definitions are the seat definitions the reader holds: what the part makes each kept event's stream from. -func (g *seatGate) Definitions() map[string]seatdef.Definition { - if r := g.reader.Load(); r != nil { - return r.Definitions() - } - return nil -} - -// follow starts the reader over bus: what it holds is given to catalogue at once and at every change, and changed is called -// then (a pass composes again). -func (g *seatGate) follow(bus seatlist.Bus, changed func(), logf func(string, ...any)) *seatlist.Reader { - reader, err := seatlist.Start(seatlist.Config{Bus: bus, Bootstrap: seatlist.Bootstrapped(), - OnChange: func(seats []catalogue.Seat) { - catalogue.UseSeats(seats) - if changed != nil { - changed() - } - }}) - if err != nil { - logf("the grants part cannot follow its seat list, so it composes only the modules the bootstrapped seats name: %v", err) - } - if reader != nil { - catalogue.UseSeats(reader.Seats()) - g.reader.Store(reader) - } - return reader -} - -// eventsLive is called when the part's own consumer on EVENTS is heard. A seat that was silent at the part's start may have said it -// speaks before that consumer existed, which starts at the stream's end, so its event is never heard: while the list is unread the -// part reads it now (ADR 0636 §1), on a goroutine of its own so the pass is not held; the reader changes the catalogue and wakes -// the part when the seat answers, and stays unread, to be read at the seat's next event, when it does not. -func (g *seatGate) eventsLive() { - if r := g.reader.Load(); r != nil { - r.ReloadIfUnread() - } -} - -// tools are the two tools over whatever reader the gate holds at the call. -func (g *seatGate) tools() []stdio.Tool { - names := []string{grants.ToolSeatList, grants.ToolSeatListAgain} - shape := seatlist.Tools(&seatlist.Reader{}, names[0], names[1], seatListAgainCallers) - for i := range shape { - run := i - shape[i].Run = func(args map[string]any) (any, error) { - r := g.reader.Load() - if r == nil { - return nil, errors.New("the seat list reader has not started yet: the runtime has not served this module") - } - return seatlist.Tools(r, names[0], names[1], seatListAgainCallers)[run].Run(args) - } - } - return shape -} diff --git a/modules/mesh-grants/cmd/mesh-grants/tools.go b/modules/mesh-grants/cmd/mesh-grants/tools.go index 9d68a0a6..4500e291 100644 --- a/modules/mesh-grants/cmd/mesh-grants/tools.go +++ b/modules/mesh-grants/cmd/mesh-grants/tools.go @@ -266,14 +266,14 @@ func recordHandAct(what, why string) error { // serveTools serves the tools to the runtime until it stops. func serveTools() error { tv := &toolViews{} - gate := &seatGate{} + gate := newSeatGate() p := &grants.Part{Read: runtimeReader{views: tv}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}, SeatList: gate} // The reader asks through the runtime, which serves once Serve has its channel. go func() { <-stdio.Served() - gate.follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "mesh-grants: "+f+"\n", a...) }) + gate.Follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "mesh-grants: "+f+"\n", a...) }) }() - tools := append(toolsOf(p), gate.tools()...) + tools := append(toolsOf(p), gate.Tools()...) for i := range tools { run := tools[i].Run tools[i].Run = func(a map[string]any) (any, error) { tv.fresh(); return run(a) }