Merge pull request 'Console: keep a mesh seat's node argument, and refuse what a call would drop (hq issue 244)' (#14) from fix/console-keeps-a-mesh-seats-node into main
mesh/delivery held for a person: merged without a passing check: only a person decides that it goes on

This commit was merged in pull request #14.
This commit is contained in:
2026-10-05 22:46:47 +00:00
2 changed files with 122 additions and 9 deletions
+70 -9
View File
@@ -552,7 +552,71 @@ func firstLine(s string) string {
return s
}
// schemaWithoutNode is a tool's schema as an agent passes it: the machine is in the address.
// schemaAsPassed is a tool's schema as an agent passes it at this address. Where the address names
// the machine, `node` is taken out: the machine is in the address. Where it does not — a seat held
// once for the mesh, or an interchangeable module — `node` is the tool's own argument, and is kept.
//
// **Taking it out everywhere made the mesh's own verbs lie** (novox/hq issue 244): the controller's
// `push`, `plan`, `assign`, `pin` and `settings` take the machine they act on as `node`, and were
// described without it — `push` as taking nothing at all, so a push naming one machine arrived as a
// push of every machine behind.
func schemaAsPassed(t target) map[string]any {
if t.Node == "" {
return asSchema(t.Tool.Input)
}
return schemaWithoutNode(t.Tool.Input)
}
// argumentsAsSent are a call's arguments as the tool at this address receives them, or why the call
// is refused. **Nothing given is dropped without a word** (novox/hq issue 244):
// - where the address names the machine, a `node` naming the same machine is redundant and taken
// out; one naming another machine is refused — which of the two was meant is not guessed;
// - where it does not, `node` is the tool's own argument and goes to it like any other;
// - a seat's schema is the mesh's record of the verb, so an argument a seat's verb does not
// declare is refused here, naming it, rather than sent to be passed over.
func argumentsAsSent(t target, given map[string]any) (map[string]any, error) {
args := map[string]any{}
for k, v := range given {
args[k] = v
}
if n, has := args["node"]; has && t.Node != "" {
if named, _ := n.(string); strings.TrimSpace(named) != t.Node {
return nil, fmt.Errorf("%s names the machine %s in its address and %v in its arguments: say it once, in the address",
t.Address, t.Node, n)
}
delete(args, "node")
}
if !t.Seat {
return args, nil
}
declared := map[string]bool{}
var names []string
if p, ok := schemaAsPassed(t)["properties"].(map[string]any); ok {
for k := range p {
declared[k] = true
names = append(names, k)
}
}
sort.Strings(names)
var strangers []string
for k := range args {
if !declared[k] {
strangers = append(strangers, k)
}
}
if len(strangers) > 0 {
sort.Strings(strangers)
takes := "nothing"
if len(names) > 0 {
takes = strings.Join(names, ", ")
}
return nil, fmt.Errorf("%s takes no argument %s — it takes %s (mesh_describe %s); nothing was sent",
t.Address, strings.Join(strangers, ", "), takes, t.Address)
}
return args, nil
}
// schemaWithoutNode is a tool's schema with `node` taken out, for an address that names the machine.
func schemaWithoutNode(raw json.RawMessage) map[string]any {
schema := asSchema(raw)
out := map[string]any{}
@@ -805,20 +869,17 @@ func (s *Surface) discover(name string, args map[string]any) map[string]any {
description = t.Name
}
return answerText(map[string]any{"address": t.Address, "description": description,
"arguments": schemaWithoutNode(t.Tool.Input)})
"arguments": schemaAsPassed(t)})
case verbCall:
t, err := resolve(x, str("address"))
if err != nil {
return failure(err.Error())
}
callArgs := map[string]any{}
if a, ok := args["arguments"].(map[string]any); ok {
for k, v := range a {
if k != "node" {
callArgs[k] = v
}
}
given, _ := args["arguments"].(map[string]any)
callArgs, err := argumentsAsSent(t, given)
if err != nil {
return failure(err.Error())
}
var got bus.Answered
if t.Seat {
@@ -0,0 +1,52 @@
package console
import (
"encoding/json"
"strings"
"testing"
)
// novox/hq issue 244: `node` is taken out only where the address names the machine. A seat held once
// for the mesh takes the machine it acts on as `node`, and is described and called with it — on
// 2026-10-05 the controller's push was described as taking nothing, and a push naming one machine
// ran as a push of every machine behind.
func TestAMeshSeatsNodeIsItsOwnArgument(t *testing.T) {
push := Tool{Module: "mesh-controller", Name: "push", Seat: true, Scope: "mesh",
Input: json.RawMessage(`{"type":"object","properties":{"node":{"type":"string"},"behind":{"type":"string"}}}`)}
mesh := target{Address: "mesh-controller.push", Tool: push, Seat: true}
props := schemaAsPassed(mesh)["properties"].(map[string]any)
if _, has := props["node"]; !has {
t.Fatalf("a mesh seat's verb was described without its node: %v", props)
}
sent, err := argumentsAsSent(mesh, map[string]any{"node": "g1"})
if err != nil || sent["node"] != "g1" {
t.Fatalf("a mesh seat's verb was called without the machine it was given: %v %v", sent, err)
}
// An argument the seat's verb does not declare is refused, naming it, never sent to be dropped.
if _, err := argumentsAsSent(mesh, map[string]any{"machine": "g1"}); err == nil || !strings.Contains(err.Error(), "machine") {
t.Fatalf("an undeclared argument was sent: %v", err)
}
// A machine's seat: the address names the machine, so `node` is out of the schema; given the same
// machine again it is taken out, given another it is refused.
shelf := Tool{Module: "node-shelf", Name: "list", Seat: true, Scope: "node",
Input: json.RawMessage(`{"type":"object","properties":{"limit":{"type":"string"}}}`)}
onDesk := target{Address: "desk/node-shelf.list", Node: "desk", Tool: shelf, Seat: true}
if _, has := schemaAsPassed(onDesk)["properties"].(map[string]any)["node"]; has {
t.Fatal("a machine's seat was described with a node the address already names")
}
if sent, err := argumentsAsSent(onDesk, map[string]any{"node": "desk", "limit": "3"}); err != nil || sent["node"] != nil || sent["limit"] != "3" {
t.Fatalf("the same machine named twice: %v %v", sent, err)
}
if _, err := argumentsAsSent(onDesk, map[string]any{"node": "bench"}); err == nil || !strings.Contains(err.Error(), "bench") {
t.Fatalf("another machine in the arguments than in the address was dropped: %v", err)
}
// A module's tool is its own: what it takes beyond node is the module's to judge.
beta := target{Address: "desk/beta.three", Node: "desk", Tool: Tool{Module: "beta", Name: "three"}}
if sent, err := argumentsAsSent(beta, map[string]any{"node": "desk", "verbose": true}); err != nil || sent["verbose"] != true || sent["node"] != nil {
t.Fatalf("a module's tool: %v %v", sent, err)
}
}