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. // // Read from the bus at start and every few minutes (issue 1126 B13; the controller that answered this before is // not placed, novox/hq ADR 0590): the seats' verbs from the seats' contract (mesh-sdk catalogue), the modules' // own tools from the manifests the indexer keeps (`mesh-delivery-indexer.modules` and `.manifests`), and the // machines and where each module runs from the seat mesh-placement's `placement`. 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` is in the next render's table and the guard's next refusal. import ( "context" "encoding/json" "errors" "fmt" "net" "os" "path/filepath" "sort" "strings" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) // 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 bus said, as kept in the module's state. type MeshTools struct { Replacements []Replacement `json:"replacements"` Machines []Machine `json:"machines"` // Asked is when the bus was last read whole. 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 } // The states read, each declared in the manifest's reads. const ( PlacementRead = delivery.PlacementRead ModulesRead = delivery.IndexerModule + ".modules" ManifestsRead = delivery.IndexerModule + ".manifests" ) // States reads the module states the manifest declares in its reads: a state's keys, and one key's value. type States interface { Keys(read string) ([]string, error) Get(read, key string) ([]byte, bool, error) } // ModuleTools is one module's own tools that replace a command, as its manifest says (`replaces`), and where it runs. type ModuleTools struct { Module string On []string Replaces map[string][]string } // placements is every node's placement, from the seat mesh-placement's state. func placements(st States) (map[string]delivery.Placement, error) { keys, err := st.Keys(PlacementRead) if err != nil { return nil, fmt.Errorf("%s: %w", PlacementRead, err) } read := delivery.NewPlacementKeys() for _, k := range keys { v, found, err := st.Get(PlacementRead, k) if err != nil { return nil, fmt.Errorf("%s %s: %w", PlacementRead, k, err) } if found { _, _ = read.Put(k, v, 0, false) // a key that does not read places nothing } } return read.All(), nil } // runsOn is, by module, the nodes placement puts it on, sorted. func runsOn(placed map[string]delivery.Placement) map[string][]string { out := map[string][]string{} for node, p := range placed { for _, m := range p.Modules { out[m.Module] = append(out[m.Module], node) } } for _, nodes := range out { sort.Strings(nodes) } return out } // NodesRunning is the nodes placement puts module on. func NodesRunning(st States, module string) ([]string, error) { placed, err := placements(st) if err != nil { return nil, err } return runsOn(placed)[module], nil } // modulesTools is every placed module's own tools that replace a command, by module, from the manifest the indexer // keeps of it. A module whose manifest cannot be read is left out and said in the error. func modulesTools(st States, on map[string][]string) ([]ModuleTools, error) { var out []ModuleTools var problems []error modules := make([]string, 0, len(on)) for m := range on { modules = append(modules, m) } sort.Strings(modules) for _, module := range modules { key, err := delivery.ModulesKey(module) if err != nil { continue } raw, found, err := st.Get(ModulesRead, key) if err != nil || !found { if err != nil { problems = append(problems, fmt.Errorf("%s %s: %w", ModulesRead, module, err)) } continue } m, err := delivery.DecodeModule(raw) if err != nil || m.Manifest == "" { continue } mkey, err := delivery.ManifestsKey(m.Manifest) if err != nil { continue } raw, found, err = st.Get(ManifestsRead, mkey) if err != nil || !found { if err != nil { problems = append(problems, fmt.Errorf("%s of %s: %w", ManifestsRead, module, err)) } continue } kept, err := delivery.DecodeManifest(raw) if err != nil { problems = append(problems, fmt.Errorf("the manifest of %s: %w", module, err)) continue } var man struct { Replaces map[string][]string `json:"replaces"` } if json.Unmarshal([]byte(kept.JSON), &man) != nil || len(man.Replaces) == 0 { continue } out = append(out, ModuleTools{Module: module, On: on[module], Replaces: man.Replaces}) } return out, errors.Join(problems...) } // 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 is every seat verb and module tool that says it replaces a command: the seats' verbs first, in the // seats' order, then the modules' own tools by module and tool. func ReplacementsOf(seats []catalogue.Seat, modules []ModuleTools) []Replacement { var out []Replacement for _, s := range seats { for _, v := range s.Serves { if len(v.Replaces) == 0 { continue } out = append(out, Replacement{Address: s.Name + "." + v.Name, NodeScoped: s.Scope == catalogue.ScopeNode, Seat: true, Does: firstSentence(v.Description), Replaces: v.Replaces}) } } sorted := append([]ModuleTools(nil), modules...) sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Module < sorted[j].Module }) for _, m := range sorted { 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 } // ReadMeshToolsFrom reads the bus now: the seats' verbs, the placed modules' own tools, and every node placement names // with its domains and addresses. A part that cannot be read is left out and said in the error; what was read is kept. func ReadMeshToolsFrom(st States, seats []catalogue.Seat, resolve func(string) []string) (MeshTools, error) { var m MeshTools placed, err := placements(st) if err != nil { m.Replacements = ReplacementsOf(seats, nil) return m, err } modules, merr := modulesTools(st, runsOn(placed)) m.Replacements = ReplacementsOf(seats, modules) for node, p := range placed { machine := Machine{Name: node, Domains: []string{node + InternalSuffix}} if d := strings.ToLower(strings.TrimSpace(p.Decided.PublicDomain)); d != "" { machine.Domains = append(machine.Domains, d) } seen := map[string]bool{} for _, name := range append([]string{node}, 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, merr } // 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), WaitsDir: WaitsDirPath(p)} } // MeshToolsEvery is how often the bus is read again. const MeshToolsEvery = 10 * time.Minute // refreshMeshTools reads the bus at start and every few minutes, and renders when what it said changed. func refreshMeshTools(p Paths) { for { m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup) if err != nil { say("reading what each tool replaces and which machines the mesh has: %v", err) } if (err == nil || len(m.Replacements) > 0) && KeepMeshTools(p, m) { if _, err := RenderNow(p, writeManaged); err != nil { say("rendering what the bus said of the 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 bus has not been read whole 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), } }