S7: the assigner's and the grants part's seat-list again answers a proved operator's session
mesh/delivery merged: source.main moved to novox/mesh-delivery@8d9683d844ec
mesh/delivery-group group fix/1966-seat-list-unread merged: every member is merged
mesh/check checking b1343147e80f on 8d9683d844ec | novox/mesh-delivery#197 in group fix/1966-seat-list-unread

Their seat_list_again answered person.<name> and the healers, but every call
through the mesh MCP server is proved as its machine's tool runner (issue
627), so no operator's session could ask it; node-tools now stands for that
session, the kind met a second time in the node-engine (issue 1966). The
assigner's silent stand-in keeps the bus's retry bound short, since the
seat list's reader now asks again while its ask reached no one.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-10 00:59:07 +01:00
parent 8b03cc036e
commit b1343147e8
5 changed files with 35 additions and 3 deletions
@@ -19,6 +19,7 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/seatlist"
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient" mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
@@ -252,6 +253,15 @@ func TestTheSeatsVerbs(t *testing.T) {
}) })
} }
s.seatList = nil s.seatList = nil
t.Run("1966: the seat list's again tool answers an operator's session through the mesh MCP server", func(t *testing.T) {
// Such a session is proved as its machine's tool runner (issue 627); the healers too, no other module.
for caller, refused := range map[string]bool{"person.jochen": false, "shanks.node-tools": false, "ace.mesh-healers": false,
"ace.messenger": true} {
if err := seatlist.Refusal(seatListAgainTool, seatListAgainCallers, caller); (err != nil) != refused {
t.Errorf("%s: refused=%v, err %v", caller, refused, err)
}
}
})
t.Run("1953/D4-D6: the seat mesh-wants' upgrade and bus", wantsVerbs) t.Run("1953/D4-D6: the seat mesh-wants' upgrade and bus", wantsVerbs)
t.Run("1955/L7 (ADR 0571 §6): rule 7 reads the node's own composition; an unplaced node is said", func(t *testing.T) { t.Run("1955/L7 (ADR 0571 §6): rule 7 reads the node's own composition; an unplaced node is said", func(t *testing.T) {
w := newWorld() w := newWorld()
@@ -25,7 +25,9 @@ const (
seatListAgainTool = "assigner_seat_list_again" seatListAgainTool = "assigner_seat_list_again"
) )
var seatListAgainCallers = []string{stdio.AnyPerson, "mesh-healers"} // A call through the mesh MCP server is proved only as its machine's tool runner (<node>.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 a runtime of the assigner judges by, set once it is started; before, the list counts as unread. // seatGate is the reader a runtime of the assigner judges by, set once it is started; before, the list counts as unread.
type seatGate struct { type seatGate struct {
@@ -5,7 +5,9 @@ import (
"errors" "errors"
"sync" "sync"
"testing" "testing"
"time"
"git.novox.be/novox/mesh-sdk/go/busretry"
"git.novox.be/novox/mesh-sdk/go/seatdef" "git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/seatlist" "git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest" "git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
@@ -50,6 +52,11 @@ func (standInCatalog) Subscribe(string, func(string, json.RawMessage) error) err
// (catalogue.UseSeats), as a host's does, for the tests after it too: the list is the same everywhere. // (catalogue.UseSeats), as a host's does, for the tests after it too: the list is the same everywhere.
func gateOver(t *testing.T, silent bool) *seatGate { func gateOver(t *testing.T, silent bool) *seatGate {
t.Helper() t.Helper()
if silent {
// A silent seat reached no one: its reader asks again within the bus's retry bound (issue 1966), which a test
// that waits for it to stay unread keeps short.
t.Cleanup(busretry.Waits(20*time.Millisecond, time.Millisecond, 5*time.Millisecond))
}
g := &seatGate{} g := &seatGate{}
r := g.follow(standInCatalog{silent: silent}, nil, t.Logf) r := g.follow(standInCatalog{silent: silent}, nil, t.Logf)
if r == nil { if r == nil {
@@ -31,6 +31,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/membership" "git.novox.be/novox/mesh-sdk/go/membership"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seal" "git.novox.be/novox/mesh-sdk/go/seal"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views" "git.novox.be/novox/mesh-sdk/go/views"
"mesh-grants/internal/grants" "mesh-grants/internal/grants"
@@ -321,6 +322,16 @@ func TestTheGrantsPartWhole(t *testing.T) {
t.Errorf("the bucket %s: %v, want none", bucket, err) t.Errorf("the bucket %s: %v, want none", bucket, err)
} }
} }
// Issue 1966: the seat list's again tool answers an operator's session, which the mesh MCP server proves as its
// machine's tool runner (issue 627), and the healers; no other module.
t.Run("the seat list's again tool answers an operator's session", func(t *testing.T) {
for caller, refused := range map[string]bool{"person.jochen": false, "shanks.node-tools": false, "ace.mesh-healers": false,
"ace.messenger": true} {
if err := seatlist.Refusal(grants.ToolSeatListAgain, seatListAgainCallers, caller); (err != nil) != refused {
t.Errorf("%s: refused=%v, err %v", caller, refused, err)
}
}
})
// ADR 0640 §5: the real additions change the subjects and the filters and nothing else, and recreate nothing. // ADR 0640 §5: the real additions change the subjects and the filters and nothing else, and recreate nothing.
t.Run("adding subjects and filters changes only those", func(t *testing.T) { t.Run("adding subjects and filters changes only those", func(t *testing.T) {
stream, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "ADDS", Subjects: []string{"a.>"}, MaxAge: time.Hour, MaxMsgs: 100, stream, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "ADDS", Subjects: []string{"a.>"}, MaxAge: time.Hour, MaxMsgs: 100,
@@ -16,8 +16,10 @@ import (
// 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 // 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. // 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. // seatListTools are the reader's two tools (ADR 0636 §7): every operator's session and the healers may read again. A call
var seatListAgainCallers = []string{stdio.AnyPerson, "mesh-healers"} // through the mesh MCP server is proved only as its machine's tool runner (<node>.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. // seatGate is the reader the part gates by, set once it is started; before, it acts on nothing new.
type seatGate struct { type seatGate struct {