Merge branch 'feat/47-G-fix11-a3h' into feat/47-G-join11

# Conflicts:
#	node-tools/internal/runtime/runtime.go
This commit is contained in:
2026-11-12 10:27:48 +01:00
4 changed files with 137 additions and 2 deletions
+10
View File
@@ -19,6 +19,8 @@ type Gate interface {
Unserved(module, fingerprint string, registered []string) []string
// Run is a call through the gate: run is called only when the call may run now, once.
Run(call approval.Call, run func() (any, error)) (any, error)
// Approvals is the seat verb `approvals` for a caller: the calls the gate holds, its acts and claims (A-265, A-271).
Approvals(caller string) (approval.Approvals, error)
}
// gatedRun is how a handler calls a tool: through the gate where there is one.
@@ -33,6 +35,14 @@ func (c *consumers) gated(call approval.Call, t launch.Tool, caller string) (any
return c.gate.Run(call, run)
}
// approvalsOf is how the runner answers a held seat's verb `approvals`: from its gate's record, nil without a gate.
func (r *runner) approvalsOf() func(caller string) (any, error) {
if r.events.gate == nil {
return nil
}
return func(caller string) (any, error) { return r.events.gate.Approvals(caller) }
}
// WithGate serves every call through the approval gate.
func WithGate(g Gate) Option { return func(c *consumers) { c.gate = g } }
@@ -0,0 +1,79 @@
package runtime
import (
"encoding/json"
"testing"
"git.novox.be/novox/mesh-sdk/go/approval"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"github.com/novox/mesh-tools/node-tools/internal/bus"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
// gateHolding is the runner's gate as far as the runtime reaches it: it records the calls it is run for and answers
// `approvals` from them.
type gateHolding struct{ calls []approval.Call }
func (g *gateHolding) Unserved(string, string, []string) []string { return nil }
func (g *gateHolding) Run(c approval.Call, run func() (any, error)) (any, error) {
g.calls = append(g.calls, c)
return run()
}
func (g *gateHolding) Approvals(caller string) (approval.Approvals, error) {
out := approval.Approvals{Calls: []approval.ListedCall{}, Acts: []approval.ListedAct{}, Claims: []string{}}
for i, c := range g.calls {
l := approval.ListedCall{Call: string(rune('a' + i)), Caller: c.Caller, Verb: c.Seat + "." + c.Name, State: "waiting"}
if caller == c.Caller {
l.Arguments = c.Args
}
out.Calls = append(out.Calls, l)
}
return out, nil
}
// **The tool runner answers a held seat's marked verb by caller and serves the seat's `approvals`** (design 47 G §677; lead
// answers A-265, A-271), with zsh's node-login-shell `execute` as the case: the marked verb reaches the gate with the caller
// its subject names, and `approvals` is answered from the runner's own record of the calls its gate holds, with the
// arguments for the call's own caller only, though no bundle names the verb.
func TestTheRunnerServesApprovalsForTheSeatsItsBundlesHold(t *testing.T) {
mesh := mt.New(t)
mesh.Issue(t, mt.MembershipOf("zsh", "anchor", true, map[string][]string{"node-login-shell": {"execute", seatdef.ApprovalsVerb}}))
nodeTools := connect(t, "node-tools", "anchor")
gate := &gateHolding{}
defined := WithSeatDefinitions(func() map[string]seatdef.Definition {
return map[string]seatdef.Definition{"node-login-shell": {Seat: "node-login-shell", Verbs: []seatdef.Verb{
{Name: "execute", NeedsApproval: true}, {Name: seatdef.ApprovalsVerb}}}}
})
stop, err := Run(nodeTools, []Served{{"zsh", []string{mt.Fixture("login-shell-execute.py")}}}, nil, func(string, ...any) {}, defined, WithGate(gate))
if err != nil {
t.Fatal(err)
}
defer stop()
asker, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "shanks", User: "shanks.node-tools", Password: "unused"})
if err != nil {
t.Fatal(err)
}
asker.Logf = func(string, ...any) {}
defer asker.Close()
mt.Until(t, func() error {
_, err := asker.Ask("seat:node-login-shell.execute@anchor", map[string]any{"command": "ls"}, "")
return err
})
if len(gate.calls) != 1 || gate.calls[0].Seat != "node-login-shell" || gate.calls[0].Name != "execute" || gate.calls[0].Caller != "shanks.node-tools" {
t.Fatalf("the marked verb reached the gate as %+v, want the seat's execute by shanks.node-tools", gate.calls)
}
var got approval.Approvals
mt.Until(t, func() error {
answered, err := asker.Ask("seat:node-login-shell.approvals@anchor", map[string]any{}, "")
if err == nil {
err = json.Unmarshal(answered.Result, &got)
}
return err
})
if len(got.Calls) != 1 || got.Calls[0].Caller != "shanks.node-tools" || got.Calls[0].Verb != "node-login-shell.execute" ||
string(got.Calls[0].Arguments) != `{"command":"ls"}` {
t.Fatalf("approvals answered %+v, want the one call the gate holds, with its caller's arguments", got)
}
}
+25 -2
View File
@@ -310,7 +310,8 @@ 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.events.admitted, r.events.chunked, r.fingerprintOf, logf)
stopSeats, again := serveSeats(conn, r.modules, r.registrations, r.events.gated, r.events.admitted, r.events.chunked, r.approvalsOf(),
r.fingerprintOf, logf)
r.seatsAgain = again
r.stops = append(r.stops, stopSeats)
@@ -662,6 +663,9 @@ func endpointsOf(conn *bus.Conn, own []registration, registrations []registratio
}
for _, v := range m.Seats {
t, ok := impl[v.Seat][v.Verb]
if v.Verb == seatdef.ApprovalsVerb && !ok {
t, ok = launch.Tool{Name: v.Verb, Description: "The calls this seat's holder holds for the operator's approval, its acts and its claims."}, true
}
if !ok || have[v.Subject] {
continue
}
@@ -717,7 +721,8 @@ func subjectsOf(conn *bus.Conn, module, tool string) []string {
// 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,
admitted func(seat, verb, caller string) error, chunked func(key string) bool, fingerprintOf func(string) string, logf func(string, ...any)) (func(), func()) {
admitted func(seat, verb, caller string) error, chunked func(key string) bool, approvals func(caller string) (any, error),
fingerprintOf func(string) string, logf func(string, ...any)) (func(), func()) {
var mu sync.Mutex
var stops []func()
stopped := false
@@ -751,6 +756,24 @@ func serveSeats(conn *bus.Conn, modules []string, current func() []registration,
continue
}
have[v.Subject] = true
// The seat's verb `approvals` is the gate's own, served wherever a marked verb is: the runner answers it from its
// record of the calls its gate holds, by caller, and no bundle names it (lead answers A-265, A-266, A-271).
if v.Verb == seatdef.ApprovalsVerb && approvals != nil {
seat := v.Seat
stop, err := conn.HandleSubjectCalled(v.Subject, func(caller string, _ json.RawMessage) (any, error) {
if err := admitted(seat, seatdef.ApprovalsVerb, caller); err != nil {
return nil, err
}
return approvals(caller)
})
if err != nil {
logf("[mesh-tools] cannot serve %s's approvals on %s: %v", seat, v.Subject, err)
continue
}
stops = append(stops, stop)
logf("[mesh-tools] serving %s's approvals on %s, the gate's own verb", seat, v.Subject)
continue
}
t, ok := impl[v.Seat][v.Verb]
if !ok {
logf("[mesh-tools] %s claims %s and implements no %s, which that seat promises; not served", module, v.Seat, v.Verb)
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
# The login shell's bundle as the seat node-login-shell has it: MCP over stdio, no SDK, one seat verb, execute, which says
# the command it was given and the caller the call's `_meta` named.
import json, sys
def say(m):
m["jsonrpc"] = "2.0"; sys.stdout.write(json.dumps(m) + "\n"); sys.stdout.flush()
TOOLS = [
{"name": "node-login-shell.execute", "description": "run a command", "inputSchema": {"type": "object", "properties": {"command": {"type": "string"}}}},
]
for line in sys.stdin:
req = json.loads(line); rid = req.get("id"); m = req.get("method"); p = req.get("params") or {}
if m == "initialize":
say({"id": rid, "result": {"protocolVersion": "2025-03-26", "capabilities": {"tools": {}}, "serverInfo": {"name": "zsh", "version": "1"}}})
elif m == "tools/list":
say({"id": rid, "result": {"tools": TOOLS}})
elif m == "tools/call":
meta = p.get("_meta") or {}
said = {"ran": (p.get("arguments") or {}).get("command", ""), "caller": meta.get("mesh/caller", "")}
say({"id": rid, "result": {"content": [{"type": "text", "text": json.dumps(said)}]}})
elif rid is not None:
say({"id": rid, "result": {}})