package console import ( "encoding/json" "errors" "fmt" "regexp" "strings" "github.com/novox/mesh-tools/node-tools/internal/bus" ) // Tool is a tool as its module — or, for a role's tool, the mesh's records — describes it. type Tool struct { Module string Name string Description string Input json.RawMessage Seat bool Scope string Subjects []string } // Listing is what the mesh could say about its tools; silence is named, never dropped (design 34 §3). type Listing struct { Tools []Tool NotAnswering []string } // Seats is the seats and their verbs, so `.` resolves to the role. type Seats map[string]map[string]bool func seatsIn(l *Listing) Seats { out := Seats{} for _, t := range l.Tools { if !t.Seat { continue } if out[t.Module] == nil { out[t.Module] = map[string]bool{} } out[t.Module][t.Name] = true } return out } // toolKey is the key a call uses: a role's when the prefix is a seat declaring that verb. func toolKey(name string, seats Seats) string { if strings.HasPrefix(name, "seat:") { return name } dot := strings.Index(name, ".") if dot < 0 { return name } if seats[name[:dot]][name[dot+1:]] { return "seat:" + name } return name } // toolsOn is the flat catalogue (MESH_CONSOLE_FLAT=1): what announced itself on the bus, every // tool and seat verb, and the assignments with tools that did not (novox/hq ADR 0197). func toolsOn(conn *bus.Conn) (*Listing, error) { x, err := indexOn(conn) if err != nil { return nil, err } return x.Listing, nil } // callTool calls `.[@]`, on the subject the listing names for it when it names one. func callTool(conn *bus.Conn, key string, args any, seats Seats, l *Listing) (bus.Answered, error) { name, node, _ := strings.Cut(key, "@") if !strings.Contains(name, ".") { return bus.Answered{}, fmt.Errorf("%q does not name a tool: write ., as `mesh tools` lists "+ "them, or .@ for the instance on one machine", key) } resolved := toolKey(name, seats) if node != "" { resolved += "@" + node } return conn.Ask(resolved, args, subjectListed(name, node, l)) } func subjectListed(name, node string, l *Listing) string { if l == nil { return "" } dot := strings.Index(name, ".") module, tool := name[:dot], name[dot+1:] for _, t := range l.Tools { if t.Module == module && t.Name == tool && !t.Seat { if len(t.Subjects) == 0 { return "" } if node == "" { return t.Subjects[0] } for _, s := range t.Subjects { if strings.HasSuffix(s, "."+node) { return s } } return "" } } return "" } var ( noResponders = regexp.MustCompile(`(?i)no responders|503`) // The bus's own two refusals, by their whole phrase. "authorization" alone also matched a tool's own // answer that merely mentions the word — the runtime refusing a state value with an Authorization // header (novox/hq ADR 0201) read as "this account may not call", which sent the reader the wrong way. refused = regexp.MustCompile(`(?i)permissions violation|authorization violation`) timedOut = regexp.MustCompile(`(?i)timeout`) ) // whyItFailed says why a call failed, so the remedy is in the words. func whyItFailed(key string, err error) string { if err == nil { err = errors.New("failed") } msg := err.Error() switch { case noResponders.MatchString(msg): extra := "" if strings.HasPrefix(key, "seat:") { extra = ", or nothing holds that seat" } return "nothing serves " + key + ". The module may not be assigned to any machine, or it is down" + extra + " — `mesh tools` lists what answered." case refused.MatchString(msg): return "this account may not call " + key + ". What it may call was fixed when it was issued — a " + "person's by `operator issue`, the console's by its manifest." case timedOut.MatchString(msg): return key + " did not answer in time. Something is serving it, so this is the tool being slow " + "rather than absent." } return key + " failed: " + msg }