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
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user