Files
mesh-catalog/modules/claude-code/cmd/claude-code/mesh.go
T
jochen f72a435487
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
claude-code: read where it runs from the controller's JSON answer
nodesRunningMe looked for printed lines in what is a JSON list, so every register tool's "also on"
hint and every nodes: "all" named no machine.
2026-10-07 20:28:06 +02:00

328 lines
11 KiB
Go

package main
// What the mesh says about its own tools and machines, as the agent's instructions and the guard on its
// shell need it (novox/hq ADR 0245): every seat verb and module tool that says what shell command it
// replaces, and the mesh's machines by every name and address a session could reach one by.
//
// Asked of the controller — `tools` for the seats' verbs, `modules` for the modules' own tools, `nodes`
// and `node` for the machines — at start and every few minutes, kept in the module's state so a render
// never waits for the bus, and rendered whenever it changes. Nothing here is written by hand: a verb that
// gains a `replaces` in the records is in the next render's table and the guard's next refusal.
import (
"context"
"encoding/json"
"net"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
// Replacement is one verb or tool and the shell commands it is the mesh's way to do.
type Replacement struct {
// Address is how it is called: `<seat>.<verb>`, or `<module>.<tool>`; NodeScoped says a machine goes
// before it, `<node>/`.
Address string `json:"address"`
NodeScoped bool `json:"node_scoped,omitempty"`
Seat bool `json:"seat,omitempty"`
Does string `json:"does,omitempty"`
Replaces []string `json:"replaces"`
// On is where a module's tool runs; empty for a seat's verb, which every holder serves.
On []string `json:"on,omitempty"`
}
// Machine is one of the mesh's machines and the names and addresses it is reached by.
type Machine struct {
Name string `json:"name"`
Domains []string `json:"domains,omitempty"`
Addresses []string `json:"addresses,omitempty"`
}
// MeshTools is what the controller said, as kept in the module's state.
type MeshTools struct {
Replacements []Replacement `json:"replacements"`
Machines []Machine `json:"machines"`
// Asked is when the controller last answered.
Asked string `json:"asked,omitempty"`
// Refused is how often the guard here named each tool instead of a work-around, from its record: the
// table's order. Never kept — read from the record at each render.
Refused map[string]int `json:"-"`
}
// RefusedCounts are how often the guard's record named each tool, over its last lines.
func RefusedCounts(log string) map[string]int {
out := map[string]int{}
for _, e := range ReadGuardLog(log, 2000) {
for _, t := range e.Tools {
out[t]++
}
}
return out
}
// InternalSuffix is the mesh's own names' domain: every machine and service is `<name>.internal`
// (novox/hq ADR 0194), and a session reaches none of them by ssh.
const InternalSuffix = ".internal"
func (p Paths) meshTools() string { return filepath.Join(p.State, "mesh-tools.json") }
// ReadMeshTools is what was kept, or nil.
func ReadMeshTools(p Paths) *MeshTools {
var m MeshTools
if !readJSON(p.meshTools(), &m) {
return nil
}
return &m
}
// verbAnswer is a controller verb's answer: the command's printed output, and its JSON when it printed one.
func verbAnswer(raw json.RawMessage) json.RawMessage {
var r struct {
Output string `json:"output"`
Answer json.RawMessage `json:"answer"`
}
if json.Unmarshal(raw, &r) != nil {
return raw
}
if len(r.Answer) > 0 && string(r.Answer) != "null" {
return r.Answer
}
if r.Output != "" {
t := strings.TrimSpace(r.Output)
for _, open := range []string{"[", "{"} {
if strings.HasPrefix(t, open) {
return json.RawMessage(t)
}
if i := strings.Index(t, "\n"+open); i >= 0 {
return json.RawMessage(strings.TrimSpace(t[i+1:]))
}
}
return json.RawMessage(nil)
}
return raw
}
func outputOf(raw json.RawMessage) string {
var r struct {
Output string `json:"output"`
}
_ = json.Unmarshal(raw, &r)
return r.Output
}
var publicDomain = regexp.MustCompile(`(?m)^\s*public domain\s+(\S+)\s*$`)
// firstSentence is what a verb does, short: its description to the first full stop, at most 70 characters.
func firstSentence(s string) string {
s = strings.TrimSpace(s)
if i := strings.Index(s, ". "); i >= 0 {
s = s[:i]
}
s = strings.TrimSuffix(s, ".")
if i := strings.IndexAny(s, ":;("); i > 20 {
s = strings.TrimSpace(s[:i])
}
if r := []rune(s); len(r) > 70 {
s = strings.TrimSpace(string(r[:69])) + "…"
}
return s
}
// ReplacementsOf reads the controller's `tools` and `modules` answers: the seats' verbs first, in the
// records' order, then the modules' own tools by module and tool.
func ReplacementsOf(tools, modules json.RawMessage) []Replacement {
var out []Replacement
var seats struct {
Seats []struct {
Seat string `json:"seat"`
Scope string `json:"scope"`
Tools []struct {
Name string `json:"name"`
Description string `json:"description"`
Replaces []string `json:"replaces"`
} `json:"tools"`
} `json:"seats"`
}
if json.Unmarshal(tools, &seats) == nil {
for _, s := range seats.Seats {
for _, t := range s.Tools {
if len(t.Replaces) == 0 {
continue
}
out = append(out, Replacement{Address: s.Seat + "." + t.Name, NodeScoped: s.Scope == "node", Seat: true,
Does: firstSentence(t.Description), Replaces: t.Replaces})
}
}
}
var listed []struct {
Module string `json:"module"`
On []string `json:"on"`
Replaces map[string][]string `json:"replaces"`
}
if json.Unmarshal(modules, &listed) == nil {
sort.SliceStable(listed, func(i, j int) bool { return listed[i].Module < listed[j].Module })
for _, m := range listed {
names := make([]string, 0, len(m.Replaces))
for t := range m.Replaces {
names = append(names, t)
}
sort.Strings(names)
for _, t := range names {
if len(m.Replaces[t]) == 0 {
continue
}
out = append(out, Replacement{Address: m.Module + "." + t, NodeScoped: true, Replaces: m.Replaces[t], On: m.On,
Does: strings.ReplaceAll(strings.TrimPrefix(t, strings.ReplaceAll(m.Module, "-", "_")+"_"), "_", " ")})
}
}
}
return out
}
// lookup resolves one name, briefly: a name that does not resolve here is no address.
func lookup(name string) []string {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
addrs, _ := net.DefaultResolver.LookupHost(ctx, name)
return addrs
}
// AskMeshTools asks the controller now. A part that does not answer is left out and said in the error;
// what did answer is kept.
func AskMeshTools(ask Ask, resolve func(string) []string) (MeshTools, error) {
var m MeshTools
tools, terr := ask("seat:mesh-controller.tools", map[string]any{})
modules, merr := ask("seat:mesh-controller.modules", map[string]any{})
if terr != nil && merr != nil {
return m, terr
}
m.Replacements = ReplacementsOf(verbAnswer(tools), verbAnswer(modules))
nodesRaw, err := ask("seat:mesh-controller.nodes", map[string]any{})
if err != nil {
return m, err
}
var nodes []struct {
Name string `json:"name"`
}
_ = json.Unmarshal(verbAnswer(nodesRaw), &nodes)
for _, n := range nodes {
if n.Name == "" {
continue
}
machine := Machine{Name: n.Name, Domains: []string{n.Name + InternalSuffix}}
if shown, err := ask("seat:mesh-controller.node", map[string]any{"node": n.Name}); err == nil {
if d := publicDomain.FindStringSubmatch(outputOf(shown)); d != nil {
machine.Domains = append(machine.Domains, strings.ToLower(d[1]))
}
}
seen := map[string]bool{}
for _, name := range append([]string{n.Name}, machine.Domains...) {
for _, a := range resolve(name) {
if !seen[a] && !net.ParseIP(a).IsLoopback() {
seen[a] = true
machine.Addresses = append(machine.Addresses, a)
}
}
}
sort.Strings(machine.Addresses)
m.Machines = append(m.Machines, machine)
}
sort.Slice(m.Machines, func(i, j int) bool { return m.Machines[i].Name < m.Machines[j].Name })
return m, nil
}
// KeepMeshTools writes what was asked to the module's state when it differs from what was kept, and
// answers whether it did. When was asked is not a difference.
func KeepMeshTools(p Paths, m MeshTools) bool {
if was := ReadMeshTools(p); was != nil {
a, b := *was, m
a.Asked, b.Asked = "", ""
x, _ := json.Marshal(a)
y, _ := json.Marshal(b)
if string(x) == string(y) {
return false
}
}
m.Asked = time.Now().UTC().Format(time.RFC3339)
raw, _ := indented(m)
tmp := p.meshTools() + ".tmp"
if os.WriteFile(tmp, raw, 0o600) != nil {
return false
}
return os.Rename(tmp, p.meshTools()) == nil
}
// GuardDataOf is what the guard judges with on this machine.
func GuardDataOf(p Paths, m *MeshTools) GuardData {
d := GuardData{Node: p.Node, Log: filepath.Join(p.State, "guard.log"), Machines: []Machine{}, Replacements: []Replacement{}}
if m != nil {
d.Machines, d.Replacements = m.Machines, m.Replacements
}
return d
}
// guardFiles is the guard to place: this module's own executable and what it judges with. None where the
// executable cannot be named — then no hook is written, rather than one that cannot run.
func guardFiles(p Paths, m *MeshTools) *GuardFiles {
exe, err := os.Executable()
if err != nil {
return nil
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved
}
return &GuardFiles{Exe: exe, Data: GuardDataOf(p, m)}
}
// MeshToolsEvery is how often the controller is asked again.
const MeshToolsEvery = 10 * time.Minute
// refreshMeshTools asks the controller at start and every few minutes, and renders when what it said changed.
func refreshMeshTools(p Paths) {
for {
m, err := AskMeshTools(ask, lookup)
if err != nil {
say("asking the controller what each tool replaces: %v", err)
}
if (err == nil || len(m.Replacements) > 0) && KeepMeshTools(p, m) {
if _, err := RenderNow(p, writeManaged); err != nil {
say("rendering what the controller said of its tools: %v", err)
} else {
say("the mesh's tools changed: %d replace a command, %d machines; rendered", len(m.Replacements), len(m.Machines))
}
}
time.Sleep(MeshToolsEvery)
}
}
// GuardStatus is what claude_code_guard answers.
func GuardStatus(p Paths, last int) map[string]any {
m := ReadMeshTools(p)
d := GuardDataOf(p, m)
asked := "never: the controller has not answered yet"
if m != nil {
asked = m.Asked
}
return map[string]any{
"refuses": []string{
"ssh, scp, sftp, rsync, mosh or autossh to a mesh machine: its name, a name under its domains, any *" + InternalSuffix +
" name or one of its addresses, read as ssh reads it (ssh -G); a jump through one too. An ssh login as `" +
ForgeUser + "` is the forge's account and passes",
"writing /etc/hosts or /etc/resolv.conf, by the shell or the agent's Edit and Write",
"HOSTALIASES, named anywhere in a command",
"any command naming " + OverrideVar,
},
"override": OverrideVar + "=<why>, set in the operator's own shell before the session starts; read from the session's " +
"environment as it was started, recorded with why, and not honoured when it cannot be recorded",
"hook": GuardCommand(),
"asked": asked,
"machines": d.Machines,
"replacements": d.Replacements,
"instead_of": InsteadOf(m),
"record": ReadGuardLog(d.Log, last),
}
}