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: `.`, or `.`; NodeScoped says a machine goes // before it, `/`. 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 `.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 p.Confined() { // The session's account cannot write the module's state, and must not rewrite its own record (ADR 0266). d.Agent, d.Log = p.Agent, JournalRecord+GuardJournalID } 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, "where the machine names an agent account of its own (novox/hq ADR 0266): every command and file edit of a " + "session that does not run as it, whatever it is, and no override lifts it", }, "agent_account": map[bool]string{true: p.Agent, false: "none of its own: agents run as the operator's account"}[p.Confined()], "override": OverrideVar + "=, 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), } }