package main // How mesh-cli reaches the mesh now that the controller is unassigned (novox/hq issue 1126): through this machine's // console, the mesh MCP server the module node-tools serves on loopback (provision mcp-endpoint, ADR 0152: the // machine's login is the authority). mesh-cli calls `mesh_call` with a seat verb's or a module tool's address, and the // console puts the call on the bus, the way every agent session on the machine does. mesh-cli holds no credential and // keeps no bus account of its own. import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "strings" "time" ) // consoleFile is where the module writes the console's address, from the mcp-endpoint it requires; a variable so a // test can name its own. var consoleFile = "/etc/mesh-cli/console" // defaultConsole is the console's address when the file is not there yet: the port node-tools declares it serves. const defaultConsole = "http://127.0.0.1:4270/mcp" // callWithin bounds one call: the console's own bound on a call is 30 seconds, and a wait is longer. var callWithin = 2 * time.Minute // maxAnswer bounds what is read of the console's answer. const maxAnswer = 16 << 20 // consoleAddress is where the console listens, and where that was read from, for what is said when it does not answer. func consoleAddress() (string, string) { if v := strings.TrimSpace(os.Getenv("MESH_CONSOLE")); v != "" { return v, "MESH_CONSOLE" } if raw, err := os.ReadFile(consoleFile); err == nil { if v := strings.TrimSpace(string(raw)); v != "" && !strings.Contains(v, "${") { return v, consoleFile } } return defaultConsole, "the console's declared port (" + consoleFile + " is not there)" } // answer is what the console said of one call: its text, and whether the console marked it an error (a refusal, a // tool's failure, nothing serving, no answer in time). type answer struct { Text string IsError bool } // errNoConsole is a console that did not answer at all: nothing was called. var errNoConsole = errors.New("no console") // consoleTool asks the console one of its own tools (mesh_call, mesh_search, mesh_describe) and answers what it said. func consoleTool(ctx context.Context, tool string, arguments map[string]any) (answer, error) { url, from := consoleAddress() body, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": tool, "arguments": arguments}}) if err != nil { return answer{}, err } ctx, cancel := context.WithTimeout(ctx, callWithin) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return answer{}, fmt.Errorf("the console's address %q (from %s) is not usable: %v", url, from, err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") resp, err := http.DefaultClient.Do(req) if err != nil { if ctx.Err() != nil { return answer{}, ctx.Err() } return answer{}, fmt.Errorf("%w: nothing answers at %s (from %s), so nothing was called. The console is the "+ "module node-tools on this machine: `systemctl status` of its unit, or call from another machine: %v", errNoConsole, url, from, err) } defer resp.Body.Close() raw, err := io.ReadAll(io.LimitReader(resp.Body, maxAnswer)) if err != nil { return answer{}, fmt.Errorf("the console's answer could not be read: %v; the call may have run", err) } if resp.StatusCode != http.StatusOK { return answer{}, fmt.Errorf("the console at %s answered %s: %s", url, resp.Status, strings.TrimSpace(string(raw))) } return decodeAnswer(raw) } // decodeAnswer reads the console's JSON-RPC answer, given as JSON or as a server-sent event's data. func decodeAnswer(raw []byte) (answer, error) { payload := bytes.TrimSpace(raw) if !bytes.HasPrefix(payload, []byte("{")) { sc := bufio.NewScanner(bytes.NewReader(raw)) sc.Buffer(make([]byte, 0, 64<<10), maxAnswer) payload = nil for sc.Scan() { if d, ok := strings.CutPrefix(sc.Text(), "data:"); ok { payload = []byte(strings.TrimSpace(d)) } } } var rpc struct { Result *struct { Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` IsError bool `json:"isError"` } `json:"result"` Error *struct { Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(payload, &rpc); err != nil { return answer{}, fmt.Errorf("the console's answer is not one mesh-cli reads (%v); the call may have run", err) } if rpc.Error != nil { return answer{Text: rpc.Error.Message, IsError: true}, nil } if rpc.Result == nil { return answer{}, errors.New("the console answered nothing; the call may have run") } var parts []string for _, c := range rpc.Result.Content { if c.Type == "text" || c.Type == "" { parts = append(parts, c.Text) } } return answer{Text: strings.Join(parts, "\n"), IsError: rpc.Result.IsError}, nil } // meshCall calls one address through the console. func meshCall(ctx context.Context, address string, args map[string]any) (answer, error) { if args == nil { args = map[string]any{} } return consoleTool(ctx, "mesh_call", map[string]any{"address": address, "arguments": args}) }