Files
mesh-catalog/modules/hosts/cmd/hosts-tools/main.go
T
jochen 076455ec78 hosts in Go, with the machine's own name in its block (ADR 0199)
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.
2026-10-05 20:42:58 +02:00

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")) }),
}
}