Call a module's tool or a seat verb on a named machine (issue 1470), part 3 of 6

This commit is contained in:
2026-10-20 20:04:47 +00:00
parent a51dc144a1
commit 4d4112a008
3 changed files with 123 additions and 3 deletions
+40 -2
View File
@@ -28,6 +28,26 @@ func (c *Conn) Call(key string, body any) (json.RawMessage, error) {
if err != nil {
return nil, err
}
return c.callOn(called, fallback, body)
}
// CallOn is Call to the tool or seat verb of one named machine (novox/hq ADR 0469 §3; issue 1470): the tool
// runner of that node alone answers, on mesh.mod.<module>.call.<tool>.<node>.<token> (a seat verb:
// mesh.seat.<seat>.call.<verb>.<node>.<token>), falling back to the subject that names no caller
// (mesh-tools node-tools/internal/bus/caller.go:15-19). node is one subject token: an empty one, or one
// holding '.', '*', '>' or white space, is refused before anything is published.
func (c *Conn) CallOn(node, key string, body any) (json.RawMessage, error) {
if err := checkNode(node); err != nil {
return nil, err
}
called, fallback, err := callSubjectsOn(key, node, c.callerToken())
if err != nil {
return nil, err
}
return c.callOn(called, fallback, body)
}
func (c *Conn) callOn(called, fallback string, body any) (json.RawMessage, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
@@ -46,18 +66,36 @@ func (c *Conn) callerToken() string { return strings.ReplaceAll(c.account.User,
// callSubjects is the caller-named subject key is called on, and its plain fallback (mesh-tools
// node-tools/internal/bus/caller.go CalledSubject, toolShape, calledPattern).
func callSubjects(key, token string) (called, fallback string, err error) {
return callSubjectsOn(key, "", token)
}
// checkNode refuses a node name that is not one subject token.
func checkNode(node string) error {
if node == "" || strings.ContainsAny(node, ".*> \t\r\n") {
return fmt.Errorf("%q is not a node name (one subject token: no '.', '*', '>' or space)", node)
}
return nil
}
// callSubjectsOn is callSubjects for one named node ("" names none): the node is a token between the tool and
// the caller on the called subject, and the last one of the fallback.
func callSubjectsOn(key, node, token string) (called, fallback string, err error) {
at := ""
if node != "" {
at = "." + node
}
if rest, ok := strings.CutPrefix(key, "seat:"); ok {
seat, verb, ok := strings.Cut(rest, ".")
if !ok || seat == "" || verb == "" {
return "", "", fmt.Errorf("%q names no seat and verb (seat:<seat>.<verb>)", key)
}
return "mesh.seat." + seat + ".call." + verb + "." + token, "mesh.seat." + seat + ".tool." + verb, nil
return "mesh.seat." + seat + ".call." + verb + at + "." + token, "mesh.seat." + seat + ".tool." + verb + at, nil
}
module, tool, ok := strings.Cut(key, ".")
if !ok || module == "" || tool == "" {
return "", "", fmt.Errorf("%q is not <module>.<tool>, and does not start seat:<seat>.<verb>", key)
}
return "mesh.mod." + module + ".call." + tool + "." + token, "mesh.mod." + module + ".tool." + tool, nil
return "mesh.mod." + module + ".call." + tool + at + "." + token, "mesh.mod." + module + ".tool." + tool + at, nil
}
// request asks one subject once and reads its reply ({result, error, node}); a reply naming an error is
+42
View File
@@ -235,3 +235,45 @@ func TestReleaseAcceptanceAProcessDoesExactlyItsBusDeclaration(t *testing.T) {
t.Error("subscribing a tool-call subject the process does not serve was not refused")
}
}
// 1470/AC4: two answerers of the same verb on the subjects of nodes ace and g14; CallOn naming g14 gets g14's
// answer, and ace's answerer hears nothing.
func TestCallOnReachesTheNamedNodeAlone(t *testing.T) {
url, account := grantedSendProcess(t)
adminNC, _ := adminJS(t, url)
aceHeard := make(chan struct{}, 1)
for _, node := range []string{"ace", "g14"} {
node := node
sub, err := adminNC.Subscribe("mesh.seat.operator-channel.call.tell."+node+".novox~mesh-mail~send", func(m *nats.Msg) {
if node == "ace" {
aceHeard <- struct{}{}
}
_ = m.Respond([]byte(`{"result":{"node":"` + node + `"}}`))
})
if err != nil {
t.Fatal(err)
}
defer func() { _ = sub.Unsubscribe() }()
}
if err := adminNC.Flush(); err != nil {
t.Fatal(err)
}
conn, err := Connect(account)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
result, err := conn.CallOn("g14", "seat:operator-channel.tell", map[string]any{})
if err != nil {
t.Fatalf("CallOn: %v", err)
}
if string(result) != `{"node":"g14"}` {
t.Fatalf("result: %s, want g14's answer", result)
}
select {
case <-aceHeard:
t.Error("ace's answerer heard a call named for g14")
default:
}
}
+41 -1
View File
@@ -1,6 +1,9 @@
package process
import "testing"
import (
"strings"
"testing"
)
// 1191/AC3: given a module tool key <module>.<tool>, when called, then the subject is
// mesh.mod.<module>.call.<tool>.<token> with the same fallback (mesh.mod.<module>.tool.<tool>).
@@ -39,3 +42,40 @@ func TestCallSubjectsRefusesAKeyNamingNeither(t *testing.T) {
}
}
}
// 1470/AC1: key mesh-build-go.fetch, node ace, caller ace.mesh-delivery-builder.
func TestCallSubjectsOnANodeForAModuleTool(t *testing.T) {
c := &Conn{account: Account{User: "ace.mesh-delivery-builder"}}
called, fallback, err := callSubjectsOn("mesh-build-go.fetch", "ace", c.callerToken())
if err != nil {
t.Fatal(err)
}
if called != "mesh.mod.mesh-build-go.call.fetch.ace.ace~mesh-delivery-builder" {
t.Errorf("called: %q", called)
}
if fallback != "mesh.mod.mesh-build-go.tool.fetch.ace" {
t.Errorf("fallback: %q", fallback)
}
}
// 1470/AC2: seat:node-files.read on g14.
func TestCallSubjectsOnANodeForASeatVerb(t *testing.T) {
called, fallback, err := callSubjectsOn("seat:node-files.read", "g14", "tok")
if err != nil {
t.Fatal(err)
}
if called != "mesh.seat.node-files.call.read.g14.tok" || fallback != "mesh.seat.node-files.tool.read.g14" {
t.Errorf("called %q, fallback %q", called, fallback)
}
}
// 1470/AC3: a node that is not one token is refused before anything is published (the Conn has no
// connection, so only the name check can answer with this error).
func TestCallOnRefusesANodeThatIsNotOneToken(t *testing.T) {
c := &Conn{}
for _, node := range []string{"", "a.b", "*", ">", "a b", "a\tb"} {
if _, err := c.CallOn(node, "mesh-build-go.fetch", nil); err == nil || !strings.Contains(err.Error(), "not a node name") {
t.Errorf("CallOn(%q) = %v, want the refusal of the name", node, err)
}
}
}