package console // The mesh's tools found by address, not announced whole (novox/hq ADR 0195, to-be 34 §3a). // // The console announces five tools. Everything the mesh answers is reached through them by one // address per layer: // // . a seat held once for the mesh — its holder answers // /. a seat held once per machine — that machine's holder answers // /. a module assigned to a machine — that assignment answers // . also, for a module whose instances are interchangeable (ADR 0160) // // A module that is not interchangeable is called with its machine or refused, naming the machines // it runs on: "whichever answers" is no answer for state a machine holds. // // Every discovery verb asks the mesh when it is called — kept a few seconds at most, never for a // session — so a tool that arrived a minute ago is found without the client reconnecting. import ( "encoding/json" "fmt" "sort" "strings" "sync" "time" "github.com/novox/mesh-tools/node-tools/internal/announce" "github.com/novox/mesh-tools/node-tools/internal/bus" ) // IndexKept is how long what the mesh answered is kept before it is asked again: long enough that // one agent turn's search, describe and call ask once, short enough that nothing goes stale. var IndexKept = 5 * time.Second // The five tools the console announces. Names of the API's kind — letters, digits, `_`, `-`. const ( verbOverview = "mesh_overview" verbMachine = "mesh_machine" verbSearch = "mesh_search" verbDescribe = "mesh_describe" verbCall = "mesh_call" ) // searchCap is how many matches a search answers before it says how many more there were. const searchCap = 25 const grammar = "Addresses: `.` for a seat held once for the mesh (e.g. `mesh-controller.nodes`); " + "`/.` for a seat every machine holds (e.g. `ace/node-packet-filter.rules`); " + "`/.` for a module on one machine (e.g. `novox/postgres.postgres_list_databases`); " + "and `.` also for a module whose instances are interchangeable." // discovery is the five tools as tools/list announces them. func discovery() []map[string]any { str := func(desc string) map[string]any { return map[string]any{"type": "string", "description": desc} } obj := func(props map[string]any, required ...string) map[string]any { s := map[string]any{"type": "object", "properties": props} if len(required) > 0 { s["required"] = required } return s } return []map[string]any{ {"name": verbOverview, "inputSchema": obj(map[string]any{}), "description": "The mesh at a glance: the seats it holds once for the whole mesh with their verbs, the seats " + "every machine holds, and its machines. Start here, then `mesh_machine` for one machine. " + grammar}, {"name": verbMachine, "inputSchema": obj(map[string]any{"node": str("the machine, as mesh_overview names it")}, "node"), "description": "One machine: the seats it holds with their verbs, and the modules assigned to it with their tools — " + "each with the address to describe or call it by. " + grammar}, {"name": verbSearch, "inputSchema": obj(map[string]any{"query": str("words to find in tool names and descriptions, e.g. `postgres databases`")}, "query"), "description": "Find tools anywhere in the mesh by words: every match's address and a line of what it does, across the " + "mesh's seats, the machines' seats and every module on every machine. " + grammar}, {"name": verbDescribe, "inputSchema": obj(map[string]any{"address": str("the tool's address")}, "address"), "description": "What one tool does and the arguments it takes, as a JSON schema. The machine is in the address, " + "never an argument. " + grammar}, {"name": verbCall, "inputSchema": obj(map[string]any{ "address": str("the tool's address"), "arguments": map[string]any{"type": "object", "description": "the tool's arguments, as mesh_describe gives its schema"}, }, "address"), "description": "Call one tool by its address with its arguments; the answer says which machine gave it. " + grammar}, } } func isDiscovery(name string) bool { switch name { case verbOverview, verbMachine, verbSearch, verbDescribe, verbCall: return true } return false } // seatInfo is a seat as the mesh's records define it, and who holds it where. type seatInfo struct { Seat string Scope string // "mesh" or "node" Verbs []Tool Holders []holder } type holder struct { Module string `json:"module"` Node string `json:"node"` } // moduleInfo is a module that answers tools: where it runs, whether any instance will do, and its // tools as one of its instances described them. type moduleInfo struct { Module string On []string Interchangeable bool Tools []Tool } // index is what the mesh answered about itself, at one moment. type index struct { Seats []seatInfo Machines []string Modules map[string]*moduleInfo NotAnswering []string Listing *Listing // the flat catalogue the call path resolves subjects with } func (x *index) seat(name string) *seatInfo { for i := range x.Seats { if x.Seats[i].Seat == name { return &x.Seats[i] } } return nil } func findTool(tools []Tool, name string) *Tool { for i := range tools { if tools[i].Name == name { return &tools[i] } } return nil } // controllerOutput is a controller seat verb's answer: the command's printed output. func controllerOutput(conn *bus.Conn, verb string) (string, error) { got, err := conn.Ask("seat:mesh-controller."+verb, map[string]any{}, "") if err != nil { return "", err } var r struct { Output string `json:"output"` OK *bool `json:"ok"` } if json.Unmarshal(got.Result, &r) != nil { return "", fmt.Errorf("mesh-controller.%s answered something that is not its output", verb) } if r.OK != nil && !*r.OK { return "", fmt.Errorf("mesh-controller.%s: %s", verb, strings.TrimSpace(r.Output)) } return r.Output, nil } // jsonIn is the JSON document a command printed, after any lines it said first: a seat verb runs // the controller's command, and a command may warn before it answers. func jsonIn(output string) string { t := strings.TrimSpace(output) if strings.HasPrefix(t, "{") || strings.HasPrefix(t, "[") { return t } for _, open := range []string{"\n{", "\n["} { if i := strings.Index(output, open); i >= 0 { return strings.TrimSpace(output[i+1:]) } } return "" } // recordedModule is a module as the controller's records hold it (`module list --json`): where the // mesh assigned it, and whether it declares tools — what should announce itself, and where. type recordedModule struct { Module string `json:"module"` On []string `json:"on"` Tools bool `json:"tools"` } // recordedMachine is a machine as the controller's records hold it (`node list --json`). type recordedMachine struct { Name string `json:"name"` } // indexOn asks the mesh what it holds (novox/hq ADR 0197): what answers, from every runtime's own // announcement on the bus — one `$SRV.INFO` request — and what should, from the controller's records // read as JSON. Nothing is inferred from a roster and nothing is parsed from print. func indexOn(conn *bus.Conn) (*index, error) { var wg sync.WaitGroup var nodesOut, modulesOut string wg.Add(2) go func() { defer wg.Done(); nodesOut, _ = controllerOutput(conn, "nodes") }() go func() { defer wg.Done(); modulesOut, _ = controllerOutput(conn, "modules") }() infos, err := announce.Gather(conn) wg.Wait() if err != nil { return nil, err } l := &Listing{Tools: []Tool{}, NotAnswering: []string{}} x := &index{Modules: map[string]*moduleInfo{}, Listing: l} announced := map[string]map[string]bool{} // module → node → announced something seats := map[string]*seatInfo{} toolAt := map[string]int{} // . or . → index in l.Tools machines := map[string]bool{} for _, info := range infos { for _, e := range announce.Endpoints(info) { if e.Node != "" { machines[e.Node] = true } if announced[e.Module] == nil { announced[e.Module] = map[string]bool{} } announced[e.Module][e.Node] = true switch e.Kind { case announce.KindSeat: st := seats[e.Seat] if st == nil { st = &seatInfo{Seat: e.Seat, Scope: e.Scope} seats[e.Seat] = st } if st.Scope != "node" && e.Scope == "node" { st.Scope = "node" } h := holder{Module: e.Module, Node: e.Node} if !containsHolder(st.Holders, h) { st.Holders = append(st.Holders, h) } key := e.Seat + "." + e.Tool if _, have := toolAt[key]; !have { toolAt[key] = len(l.Tools) t := Tool{Module: e.Seat, Name: e.Tool, Description: e.Description, Input: e.Schema, Seat: true, Scope: st.Scope} l.Tools = append(l.Tools, t) st.Verbs = append(st.Verbs, t) } case announce.KindTool: m := x.Modules[e.Module] if m == nil { m = &moduleInfo{Module: e.Module} x.Modules[e.Module] = m } if e.Node != "" && !contains(m.On, e.Node) { m.On = append(m.On, e.Node) } m.Interchangeable = m.Interchangeable || e.Interchangeable key := e.Module + "." + e.Tool i, have := toolAt[key] if !have { i = len(l.Tools) toolAt[key] = i l.Tools = append(l.Tools, Tool{Module: e.Module, Name: e.Tool, Description: e.Description, Input: e.Schema}) } if !contains(l.Tools[i].Subjects, e.Subject) { l.Tools[i].Subjects = append(l.Tools[i].Subjects, e.Subject) } } } } // A tool's subjects as a call looks them up: the plain one any instance answers first, then each // machine's. for i := range l.Tools { t := &l.Tools[i] if t.Seat { continue } plain := "mesh.mod." + t.Module + ".tool." + t.Name sort.SliceStable(t.Subjects, func(a, b int) bool { if (t.Subjects[a] == plain) != (t.Subjects[b] == plain) { return t.Subjects[a] == plain } return t.Subjects[a] < t.Subjects[b] }) } for _, t := range l.Tools { if !t.Seat { x.Modules[t.Module].Tools = append(x.Modules[t.Module].Tools, t) } } for _, m := range x.Modules { sort.Strings(m.On) } for _, st := range seats { sort.Slice(st.Holders, func(a, b int) bool { return st.Holders[a].Node < st.Holders[b].Node }) x.Seats = append(x.Seats, *st) } sort.Slice(x.Seats, func(i, j int) bool { return x.Seats[i].Seat < x.Seats[j].Seat }) sort.SliceStable(l.Tools, func(i, j int) bool { return l.Tools[i].Module+"."+l.Tools[i].Name < l.Tools[j].Module+"."+l.Tools[j].Name }) // What should have answered: every assignment of a module that declares tools. Silence is named; // a module with no tools is never a name here. var recorded []recordedModule if json.Unmarshal([]byte(jsonIn(modulesOut)), &recorded) == nil { for _, m := range recorded { if !m.Tools { continue } for _, n := range m.On { if !announced[m.Module][n] { l.NotAnswering = append(l.NotAnswering, m.Module+" on "+n) } } } } else { l.NotAnswering = append(l.NotAnswering, "mesh-controller (its records of the modules did not answer, so what is missing cannot be said)") } sort.Strings(l.NotAnswering) x.NotAnswering = l.NotAnswering var known []recordedMachine if json.Unmarshal([]byte(jsonIn(nodesOut)), &known) == nil { for _, n := range known { if n.Name != "" { machines[n.Name] = true } } } for n := range machines { x.Machines = append(x.Machines, n) } sort.Strings(x.Machines) return x, nil } func containsHolder(hs []holder, h holder) bool { for _, x := range hs { if x == h { return true } } return false } func (s *Surface) index() (*index, error) { s.mu.Lock() if s.idx != nil && time.Since(s.idxAt) <= IndexKept { x := s.idx s.mu.Unlock() return x, nil } s.mu.Unlock() x, err := indexOn(s.conn) if err != nil { return nil, err } s.mu.Lock() s.idx, s.idxAt = x, time.Now() s.mu.Unlock() return x, nil } // target is what an address resolves to. type target struct { Address string Key string // the call key the existing path takes: seat:.[@node] or .[@node] Name string // . or ., for the listing's subject lookup Node string Tool Tool Seat bool } // resolve turns an address into exactly one target, or says why it cannot. func resolve(x *index, address string) (target, error) { address = strings.TrimSpace(address) node, rest, hasNode := strings.Cut(address, "/") if !hasNode { rest, node = address, "" } dot := strings.Index(rest, ".") if dot <= 0 || dot == len(rest)-1 || strings.Contains(rest, "/") { return target{}, fmt.Errorf("%q is not an address. %s", address, grammar) } prefix, name := rest[:dot], rest[dot+1:] if s := x.seat(prefix); s != nil { verb := findTool(s.Verbs, name) if verb == nil { return target{}, fmt.Errorf("the seat %s has no verb %s; it has %s", prefix, name, toolNames(s.Verbs)) } if s.Scope == "node" { if node == "" { return target{}, fmt.Errorf("%s is held once per machine: write /%s — it is held on %s", prefix, rest, orNobody(nodesOf(s.Holders))) } return target{Address: node + "/" + rest, Key: "seat:" + rest + "@" + node, Name: rest, Node: node, Tool: *verb, Seat: true}, nil } if node != "" { return target{}, fmt.Errorf("%s is held once for the whole mesh: write %s, without a machine", prefix, rest) } return target{Address: rest, Key: "seat:" + rest, Name: rest, Tool: *verb, Seat: true}, nil } m := x.Modules[prefix] if m == nil { return target{}, fmt.Errorf("nothing in the mesh is called %s: no seat, and no module that answers tools. "+ "mesh_search finds a tool by words", prefix) } tool := findTool(m.Tools, name) if tool == nil { return target{}, fmt.Errorf("%s has no tool %s; it has %s", prefix, name, toolNames(m.Tools)) } if node == "" { if !m.Interchangeable { return target{}, fmt.Errorf("%s keeps state on each machine it runs on, so a call names the machine: "+ "write /%s — it runs on %s", prefix, rest, orNobody(m.On)) } return target{Address: rest, Key: rest, Name: rest, Tool: *tool}, nil } if len(m.On) > 0 && !contains(m.On, node) { return target{}, fmt.Errorf("%s does not run on %s; it runs on %s", prefix, node, orNobody(m.On)) } return target{Address: node + "/" + rest, Key: rest + "@" + node, Name: rest, Node: node, Tool: *tool}, nil } func contains(xs []string, s string) bool { for _, x := range xs { if x == s { return true } } return false } func toolNames(ts []Tool) string { names := make([]string, 0, len(ts)) for _, t := range ts { names = append(names, t.Name) } sort.Strings(names) return strings.Join(names, ", ") } func nodesOf(hs []holder) []string { var out []string for _, h := range hs { if h.Node != "" && !contains(out, h.Node) { out = append(out, h.Node) } } sort.Strings(out) return out } func orNobody(nodes []string) string { if len(nodes) == 0 { return "no machine the mesh knows of" } return strings.Join(nodes, ", ") } // firstLine is a description's first line, for a list. func firstLine(s string) string { s = strings.TrimSpace(s) if i := strings.IndexAny(s, "\n"); i >= 0 { s = s[:i] } if len(s) > 160 { s = s[:157] + "…" } return s } // schemaWithoutNode is a tool's schema as an agent passes it: the machine is in the address. func schemaWithoutNode(raw json.RawMessage) map[string]any { schema := asSchema(raw) out := map[string]any{} for k, v := range schema { out[k] = v } if p, ok := schema["properties"].(map[string]any); ok { props := map[string]any{} for k, v := range p { if k != "node" { props[k] = v } } out["properties"] = props } if r, ok := schema["required"].([]any); ok { var keep []any for _, k := range r { if k != "node" { keep = append(keep, k) } } if len(keep) == 0 { delete(out, "required") } else { out["required"] = keep } } return out } // answerText is a discovery verb's answer as MCP content: JSON, indented. func answerText(v any) map[string]any { var b strings.Builder enc := json.NewEncoder(&b) enc.SetEscapeHTML(false) // `/…` is read by an agent, not embedded in a page enc.SetIndent("", " ") _ = enc.Encode(v) return map[string]any{"content": []map[string]any{{"type": "text", "text": strings.TrimRight(b.String(), "\n")}}} } func failure(text string) map[string]any { return map[string]any{"content": []map[string]any{{"type": "text", "text": text}}, "isError": true} } // discover answers one of the five. func (s *Surface) discover(name string, args map[string]any) map[string]any { x, err := s.index() if err != nil { return failure("the mesh's discovery failed: " + err.Error()) } str := func(k string) string { v, _ := args[k].(string); return strings.TrimSpace(v) } switch name { case verbOverview: type verbLine struct { Address string `json:"address"` Description string `json:"description"` } var mesh, node []map[string]any for _, st := range x.Seats { var verbs []verbLine for _, v := range st.Verbs { addr := st.Seat + "." + v.Name if st.Scope == "node" { addr = "/" + addr } verbs = append(verbs, verbLine{addr, firstLine(v.Description)}) } entry := map[string]any{"seat": st.Seat, "verbs": verbs} if st.Scope == "node" { entry["held on"] = nodesOf(st.Holders) node = append(node, entry) } else { if h := nodesOf(st.Holders); len(h) > 0 { entry["held on"] = h } mesh = append(mesh, entry) } } modules := 0 tools := 0 for _, m := range x.Modules { modules++ tools += len(m.Tools) } return answerText(map[string]any{ "seats of the mesh": mesh, "seats every machine": node, "machines": x.Machines, "modules with tools": fmt.Sprintf("%d modules, %d tools — mesh_machine lists a machine's, mesh_search finds one", modules, tools), "not answering": x.NotAnswering, }) case verbMachine: node := str("node") if node == "" { return failure("mesh_machine needs `node`: one of " + orNobody(x.Machines)) } if !contains(x.Machines, node) { return failure(fmt.Sprintf("the mesh knows no machine %q; it has %s", node, orNobody(x.Machines))) } var seats []map[string]any for _, st := range x.Seats { if st.Scope != "node" || !contains(nodesOf(st.Holders), node) { continue } var verbs []string for _, v := range st.Verbs { verbs = append(verbs, node+"/"+st.Seat+"."+v.Name) } var by string for _, h := range st.Holders { if h.Node == node { by = h.Module } } seats = append(seats, map[string]any{"seat": st.Seat, "held by": by, "verbs": verbs}) } var modules []map[string]any names := make([]string, 0, len(x.Modules)) for n := range x.Modules { names = append(names, n) } sort.Strings(names) for _, n := range names { m := x.Modules[n] if !contains(m.On, node) { continue } var tools []map[string]string for _, t := range m.Tools { tools = append(tools, map[string]string{"address": node + "/" + m.Module + "." + t.Name, "does": firstLine(t.Description)}) } entry := map[string]any{"module": m.Module, "tools": tools} if m.Interchangeable { entry["interchangeable"] = "any instance answers . as well" } modules = append(modules, entry) } return answerText(map[string]any{"machine": node, "seats": seats, "modules": modules}) case verbSearch: query := strings.ToLower(str("query")) if query == "" { return failure("mesh_search needs `query`: words to find, e.g. `postgres databases`") } words := strings.Fields(query) type hit struct { Address string `json:"address"` Does string `json:"does"` Also string `json:"also,omitempty"` } var hits []hit matches := func(parts ...string) bool { hay := strings.ToLower(strings.Join(parts, " ")) for _, w := range words { if !strings.Contains(hay, w) { return false } } return true } for _, st := range x.Seats { for _, v := range st.Verbs { if !matches(st.Seat, v.Name, v.Description) { continue } if st.Scope == "node" { on := nodesOf(st.Holders) first := "" also := "" if len(on) > 0 { first = on[0] if len(on) > 1 { also = "also on " + strings.Join(on[1:], ", ") } } hits = append(hits, hit{first + "/" + st.Seat + "." + v.Name, firstLine(v.Description), also}) } else { hits = append(hits, hit{st.Seat + "." + v.Name, firstLine(v.Description), ""}) } } } names := make([]string, 0, len(x.Modules)) for n := range x.Modules { names = append(names, n) } sort.Strings(names) for _, n := range names { m := x.Modules[n] for _, t := range m.Tools { if !matches(m.Module, t.Name, t.Description) { continue } switch { case m.Interchangeable: hits = append(hits, hit{m.Module + "." + t.Name, firstLine(t.Description), "any instance; on " + orNobody(m.On)}) case len(m.On) == 0: hits = append(hits, hit{"/" + m.Module + "." + t.Name, firstLine(t.Description), "the mesh places it on no machine"}) default: also := "" if len(m.On) > 1 { also = "also on " + strings.Join(m.On[1:], ", ") } hits = append(hits, hit{m.On[0] + "/" + m.Module + "." + t.Name, firstLine(t.Description), also}) } } } out := map[string]any{"matches": hits} if len(hits) > searchCap { out["matches"] = hits[:searchCap] out["more"] = fmt.Sprintf("%d more; narrow the words", len(hits)-searchCap) } if len(hits) == 0 { out["matches"] = []hit{} out["hint"] = "nothing matched every word; try fewer words, or mesh_overview and mesh_machine to browse" } return answerText(out) case verbDescribe: t, err := resolve(x, str("address")) if err != nil { return failure(err.Error()) } description := t.Tool.Description if description == "" { description = t.Name } return answerText(map[string]any{"address": t.Address, "description": description, "arguments": schemaWithoutNode(t.Tool.Input)}) 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 } } } var got bus.Answered if t.Seat { got, err = s.conn.Ask(t.Key, callArgs, "") } else { got, err = callTool(s.conn, t.Key, callArgs, nil, x.Listing) } if err != nil { return failure(whyItFailed(t.Address, err)) } content := []map[string]any{{"type": "text", "text": pretty(got.Result)}} if got.Node != "" { content = append(content, map[string]any{"type": "text", "text": "answered by " + got.Node}) } return map[string]any{"content": content} } return failure("no discovery verb " + name) }