The tools were TypeScript; the mesh's modules are Go. The write to /etc/hosts is now staged and moved into place rather than written over the live file.
82 lines
3.0 KiB
Go
82 lines
3.0 KiB
Go
// hosts-tools (novox/hq ADR 0199): the hosts file's tools. One binary, launched by the machine's tool
|
|
// runtime and speaking MCP to it over stdio through the Go SDK (ADR 0193, ADR 0198): the
|
|
// node-hosts-file seat's three verbs — the file's lines with whose each is, add an operator's line,
|
|
// remove one. They change the machine's file and nothing else; the controller holds none of it.
|
|
//
|
|
// stdout is the MCP channel; everything this module says, it says on stderr.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// Seat is the role this module holds.
|
|
const Seat = "node-hosts-file"
|
|
|
|
func main() {
|
|
if err := stdio.Serve("", tools(HostsFile{Path: HostsPath, Run: execRunner})); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[hosts] %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func str(description string) map[string]any {
|
|
return map[string]any{"type": "string", "description": description}
|
|
}
|
|
|
|
func arg(a map[string]any, k string) string {
|
|
v, _ := a[k].(string)
|
|
return strings.TrimSpace(v)
|
|
}
|
|
|
|
var separators = regexp.MustCompile(`[\s,]+`)
|
|
|
|
// namesArg is the names given, separated by spaces or commas — or, from a caller that sends a list,
|
|
// the list.
|
|
func namesArg(a map[string]any) []string {
|
|
var raw []string
|
|
switch v := a["names"].(type) {
|
|
case string:
|
|
raw = separators.Split(v, -1)
|
|
case []any:
|
|
for _, n := range v {
|
|
if s, ok := n.(string); ok {
|
|
raw = append(raw, separators.Split(s, -1)...)
|
|
}
|
|
}
|
|
}
|
|
names := []string{}
|
|
for _, n := range raw {
|
|
if n != "" {
|
|
names = append(names, n)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's
|
|
// subject, as <node>/node-hosts-file.<verb>.
|
|
func verb(name, description string, input map[string]any, run func(a map[string]any) (any, error)) stdio.Tool {
|
|
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input, Run: run}
|
|
}
|
|
|
|
func tools(h HostsFile) []stdio.Tool {
|
|
ctx := context.Background()
|
|
return []stdio.Tool{
|
|
verb("entries", "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.",
|
|
nil, func(map[string]any) (any, error) { return h.Entries() }),
|
|
verb("add", "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's. Nothing changes when they are already there.",
|
|
map[string]any{"address": str("the IPv4 or IPv6 address"), "names": str("the names for it, separated by spaces")},
|
|
func(a map[string]any) (any, error) { return h.Add(ctx, arg(a, "address"), namesArg(a)) }),
|
|
verb("remove", "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.",
|
|
map[string]any{"name": str("a host name, or an address to remove every line of")},
|
|
func(a map[string]any) (any, error) { return h.Remove(ctx, arg(a, "name")) }),
|
|
}
|
|
}
|