The lease write asked seat:node-service-manager.stop@novox-be through Conn.Call, which published the '@' inside the subject: nobody served it and the call timed out after 30 s with nothing said (issue 774 notes 84-87). Every key now parses in one place, calladdress: Call routes '@node' as CallOn does, and stdio Ask refuses a key the runtime could not route, before anything is sent. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
109 lines
3.6 KiB
Go
109 lines
3.6 KiB
Go
// Package calladdress reads the key a module calls a tool or a seat verb by, as a module's invokes names it, with the
|
|
// machine it is asked of after '@': `seat:<seat>.<verb>[@<node>]` or `<module>.<tool>[@<node>]` (novox/hq ADR 0159,
|
|
// ADR 0469 §3). Every caller in the SDK parses its key here before it publishes anything, so a malformed key fails at
|
|
// once and says why, never as a call nobody serves and a timeout (novox/hq issue 774, the lease write's stop asked on
|
|
// `...call.stop@novox-be...`).
|
|
package calladdress
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Address is a parsed key: a seat and its verb, or a module and its tool, and the machine named, if any.
|
|
type Address struct {
|
|
Seat, Verb string
|
|
Module, Tool string
|
|
Node string
|
|
}
|
|
|
|
// Key is the address as a module's invokes names it, without the machine.
|
|
func (a Address) Key() string {
|
|
if a.Seat != "" {
|
|
return "seat:" + a.Seat + "." + a.Verb
|
|
}
|
|
return a.Module + "." + a.Tool
|
|
}
|
|
|
|
// Parse reads a key, refusing one with more than one '@', a node that is not one subject token, or a seat, verb, module
|
|
// or tool that holds a wildcard, white space or (but a tool) a dot.
|
|
func Parse(key string) (Address, error) {
|
|
name, node, err := cutNode(key)
|
|
if err != nil {
|
|
return Address{}, err
|
|
}
|
|
if rest, ok := strings.CutPrefix(name, "seat:"); ok {
|
|
seat, verb, ok := strings.Cut(rest, ".")
|
|
if !ok || seat == "" || verb == "" {
|
|
return Address{}, fmt.Errorf("%q names no seat and verb (seat:<seat>.<verb>)", key)
|
|
}
|
|
if err := tokens(key, seat, verb); err != nil {
|
|
return Address{}, err
|
|
}
|
|
return Address{Seat: seat, Verb: verb, Node: node}, nil
|
|
}
|
|
module, tool, ok := strings.Cut(name, ".")
|
|
if !ok || module == "" || tool == "" {
|
|
return Address{}, fmt.Errorf("%q is not <module>.<tool>, and does not start seat:<seat>.<verb>", key)
|
|
}
|
|
if err := tokens(key, module); err != nil {
|
|
return Address{}, err
|
|
}
|
|
if err := checkTool(key, tool); err != nil {
|
|
return Address{}, err
|
|
}
|
|
return Address{Module: module, Tool: tool, Node: node}, nil
|
|
}
|
|
|
|
// CheckOwn is Parse for a caller that may also name a tool of its own module by its bare name (the runtime's mesh/ask).
|
|
func CheckOwn(key string) error {
|
|
name, _, err := cutNode(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.HasPrefix(name, "seat:") && !strings.Contains(name, ".") {
|
|
return tokens(key, name)
|
|
}
|
|
_, err = Parse(key)
|
|
return err
|
|
}
|
|
|
|
// CheckNode refuses a node name that is not one subject token.
|
|
func CheckNode(node string) error {
|
|
if node == "" || strings.ContainsAny(node, ".*>@ \t\r\n") {
|
|
return fmt.Errorf("%q is not a node name (one subject token: no '.', '*', '>', '@' or space)", node)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cutNode(key string) (name, node string, err error) {
|
|
name, node, at := strings.Cut(key, "@")
|
|
if !at {
|
|
return key, "", nil
|
|
}
|
|
if strings.Contains(node, "@") {
|
|
return "", "", fmt.Errorf("%q holds more than one '@': a key names at most one machine", key)
|
|
}
|
|
if err := CheckNode(node); err != nil {
|
|
return "", "", fmt.Errorf("%q: %w", key, err)
|
|
}
|
|
return name, node, nil
|
|
}
|
|
|
|
func tokens(key string, parts ...string) error {
|
|
for _, p := range parts {
|
|
if p == "" || strings.ContainsAny(p, ".*> \t\r\n") {
|
|
return fmt.Errorf("%q: %q is not one subject token (no '.', '*', '>' or space)", key, p)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkTool refuses a tool name with a wildcard or white space; a dot is kept, as the tool runner serves it.
|
|
func checkTool(key, tool string) error {
|
|
if strings.ContainsAny(tool, "*> \t\r\n") || strings.HasPrefix(tool, ".") || strings.HasSuffix(tool, ".") {
|
|
return fmt.Errorf("%q: %q is not one subject token (no '*', '>' or space)", key, tool)
|
|
}
|
|
return nil
|
|
}
|