Files
mesh-sdk/go/calladdress/calladdress.go
T
jschoubben 49b307a46d Route a call key naming its machine after '@', and refuse a malformed key before publishing
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
2026-11-02 11:22:50 +01:00

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
}