Let claude-code read its tools and machines from the bus, not the controller
The controller is not placed (ADR 0590), so its tools, modules, nodes and node verbs answer nothing. claude-code now reads the seats' verbs from the seats' contract, the placed modules' replaces from the indexer's manifests, and the machines and where modules run from mesh-placement's placement; the rendered instructions name the seats that own each job (issue 1126 B13). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -4,21 +4,27 @@ package main
|
||||
// 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.
|
||||
// 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"
|
||||
"regexp"
|
||||
"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.
|
||||
@@ -41,11 +47,11 @@ type Machine struct {
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
}
|
||||
|
||||
// MeshTools is what the controller said, as kept in the module's state.
|
||||
// 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 controller last answered.
|
||||
// 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.
|
||||
@@ -78,42 +84,120 @@ func ReadMeshTools(p Paths) *MeshTools {
|
||||
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"`
|
||||
// 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)
|
||||
}
|
||||
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:]))
|
||||
}
|
||||
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 json.RawMessage(nil)
|
||||
}
|
||||
return raw
|
||||
return read.All(), nil
|
||||
}
|
||||
|
||||
func outputOf(raw json.RawMessage) string {
|
||||
var r struct {
|
||||
Output string `json:"output"`
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
_ = json.Unmarshal(raw, &r)
|
||||
return r.Output
|
||||
for _, nodes := range out {
|
||||
sort.Strings(nodes)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var publicDomain = regexp.MustCompile(`(?m)^\s*public domain\s+(\S+)\s*$`)
|
||||
// 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 {
|
||||
@@ -131,52 +215,33 @@ func firstSentence(s string) string {
|
||||
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 {
|
||||
// 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
|
||||
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})
|
||||
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})
|
||||
}
|
||||
}
|
||||
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, "-", "_")+"_"), "_", " ")})
|
||||
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
|
||||
@@ -190,36 +255,24 @@ func lookup(name string) []string {
|
||||
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) {
|
||||
// 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
|
||||
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{})
|
||||
placed, err := placements(st)
|
||||
if err != nil {
|
||||
m.Replacements = ReplacementsOf(seats, 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]))
|
||||
}
|
||||
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{n.Name}, machine.Domains...) {
|
||||
for _, name := range append([]string{node}, machine.Domains...) {
|
||||
for _, a := range resolve(name) {
|
||||
if !seen[a] && !net.ParseIP(a).IsLoopback() {
|
||||
seen[a] = true
|
||||
@@ -231,7 +284,7 @@ func AskMeshTools(ask Ask, resolve func(string) []string) (MeshTools, error) {
|
||||
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
|
||||
return m, merr
|
||||
}
|
||||
|
||||
// KeepMeshTools writes what was asked to the module's state when it differs from what was kept, and
|
||||
@@ -281,19 +334,19 @@ func guardFiles(p Paths, m *MeshTools) *GuardFiles {
|
||||
return &GuardFiles{Exe: exe, Data: GuardDataOf(p, m), WaitsDir: WaitsDirPath(p)}
|
||||
}
|
||||
|
||||
// MeshToolsEvery is how often the controller is asked again.
|
||||
// MeshToolsEvery is how often the bus is read again.
|
||||
const MeshToolsEvery = 10 * time.Minute
|
||||
|
||||
// refreshMeshTools asks the controller at start and every few minutes, and renders when what it said changed.
|
||||
// refreshMeshTools reads the bus at start and every few minutes, and renders when what it said changed.
|
||||
func refreshMeshTools(p Paths) {
|
||||
for {
|
||||
m, err := AskMeshTools(ask, lookup)
|
||||
m, err := ReadMeshToolsFrom(busStates{}, catalogue.Seats(), lookup)
|
||||
if err != nil {
|
||||
say("asking the controller what each tool replaces: %v", err)
|
||||
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 controller said of its tools: %v", err)
|
||||
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))
|
||||
}
|
||||
@@ -306,7 +359,7 @@ func refreshMeshTools(p Paths) {
|
||||
func GuardStatus(p Paths, last int) map[string]any {
|
||||
m := ReadMeshTools(p)
|
||||
d := GuardDataOf(p, m)
|
||||
asked := "never: the controller has not answered yet"
|
||||
asked := "never: the bus has not been read whole yet"
|
||||
if m != nil {
|
||||
asked = m.Asked
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user