From 7572627affde22796c8b24bb21517c1220e648cc Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 02:45:30 +0100 Subject: [PATCH] The console passes a seat verb's arguments on when it holds no definition of the seat (issue 1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A seat verb whose holder announced an empty schema was judged as taking nothing: shanks' tool runner refused "mesh-module-assigner.propose takes no argument changes, why — it takes nothing". The console now judges such a call by the verb's input in the seat's definition this runner holds; where it holds none (it waits for the seat list, holding only the bootstrapped definitions), the arguments go to the holder, which judges its own input, and mesh_describe says the definition is not held instead of showing an empty schema. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- node-tools/internal/console/address.go | 42 +++++++++++++++++++ .../internal/console/node_argument_test.go | 21 ++++++++++ 2 files changed, 63 insertions(+) diff --git a/node-tools/internal/console/address.go b/node-tools/internal/console/address.go index 7b884dd..15bac45 100644 --- a/node-tools/internal/console/address.go +++ b/node-tools/internal/console/address.go @@ -866,6 +866,11 @@ func argumentsAsSent(t target, given map[string]any) (map[string]any, error) { if !t.Seat { return args, nil } + t, held := seatDefinitionHeld(t) + if !held { + // Nothing here says what the verb takes: its holder judges the arguments (issue 1966). + return args, nil + } declared := map[string]bool{} var names []string if p, ok := schemaAsPassed(t)["properties"].(map[string]any); ok { @@ -893,6 +898,36 @@ func argumentsAsSent(t target, given map[string]any) (map[string]any, error) { return args, nil } +// seatDefinitionHeld is a seat verb's target with the input the console judges a call by, and whether anything here +// says what the verb takes (issue 1966): the schema its holder announced when it declares an argument, else the verb's +// input in the seat's definition this runner holds. Neither holds while the runner waits for the seat list holding +// only the bootstrapped definitions and the holder announced an empty schema: then a call's arguments are passed on to +// the holder, which judges its own input, and mesh_describe says the definition is not held. +func seatDefinitionHeld(t target) (target, bool) { + if p, ok := asSchema(t.Tool.Input)["properties"].(map[string]any); ok && len(p) > 0 { + return t, true + } + seat, ok := catalogue.SeatNamed(t.Tool.Module) + if !ok { + return t, false + } + for _, v := range seat.Serves { + if v.Name == t.Tool.Name && v.Input != nil { + if raw, err := json.Marshal(v.Input); err == nil { + t.Tool.Input = raw + } + } + } + return t, true +} + +// notHeld is what mesh_describe says of a seat verb whose definition this runner does not hold. +func notHeld(seat string) string { + return "the seat list this tool runner holds has no definition of " + seat + " yet (it holds the bootstrapped ones " + + "until it reads the list): what the verb takes is not known here, so the arguments of a call are passed to " + + "its holder, which judges them" +} + // 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) @@ -1092,7 +1127,14 @@ func (s *Surface) discover(ctx context.Context, name string, args map[string]any if description == "" { description = t.Name } + held := true + if t.Seat { + t, held = seatDefinitionHeld(t) + } described := map[string]any{"address": t.Address, "description": description, "arguments": schemaAsPassed(t)} + if !held { + described["arguments"] = notHeld(t.Tool.Module) + } key := t.Tool.Module + "." + t.Tool.Name if t.Seat { key = "seat:" + key diff --git a/node-tools/internal/console/node_argument_test.go b/node-tools/internal/console/node_argument_test.go index 0ea14b1..68ac20b 100644 --- a/node-tools/internal/console/node_argument_test.go +++ b/node-tools/internal/console/node_argument_test.go @@ -44,6 +44,27 @@ func TestAMeshSeatsNodeIsItsOwnArgument(t *testing.T) { t.Fatalf("another machine in the arguments than in the address was dropped: %v", err) } + // Issue 1966: a seat verb its holder announced with an empty schema is judged by the definition this runner holds; + // where it holds none (it waits for the seat list, holding the bootstrapped ones), the arguments go to the holder, + // which judges them, and mesh_describe says the definition is not held. + empty := json.RawMessage(`{"type":"object","properties":{}}`) + propose := target{Address: "mesh-module-assigner.propose", Seat: true, + Tool: Tool{Module: "mesh-module-assigner", Name: "propose", Seat: true, Scope: "mesh", Input: empty}} + if sent, err := argumentsAsSent(propose, map[string]any{"changes": []any{}, "why": "w"}); err != nil || sent["why"] != "w" { + t.Fatalf("a held definition's arguments were refused: %v %v", sent, err) + } + if _, err := argumentsAsSent(propose, map[string]any{"chnages": 1}); err == nil || !strings.Contains(err.Error(), "chnages") { + t.Fatalf("an argument the held definition does not declare was sent: %v", err) + } + unheld := target{Address: "mesh-not-yet-read.act", Seat: true, + Tool: Tool{Module: "mesh-not-yet-read", Name: "act", Seat: true, Scope: "mesh", Input: empty}} + if sent, err := argumentsAsSent(unheld, map[string]any{"changes": 1}); err != nil || sent["changes"] != 1 { + t.Fatalf("a seat whose definition is not held refused its arguments: %v %v", sent, err) + } + if _, held := seatDefinitionHeld(unheld); held || !strings.Contains(notHeld("mesh-not-yet-read"), "no definition of mesh-not-yet-read") { + t.Fatal("a definition not held is said to be held") + } + // 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 {