Files
mesh-catalog/modules/claude-code/cmd/claude-code/config.go
T
jschoubben 657cb2b6c8 A section registered before the controller's terminal took over, or set there, is replaced or removed by a proposal the operator approves
It could not be changed at all: propose refused a name set there and never replaced one. The ask now says plainly that
it replaces or removes the registered section and binds its text's digest; on Approve every node renders the new text
in the place of the old (a removal leaves it out); a replacement keeps the section's own heading form (issue 340 note 14).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 22:14:55 +01:00

1198 lines
45 KiB
Go

package main
// The agent's configuration, registered through this module at three scopes (novox/hq ADR 0216, to-be 36 §8).
//
// Every registration is one key in the module's `config` state (ADR 0201):
//
// mesh.<kind>.<name> every node running the agent
// node.<node>.<kind>.<name> one node — a list of nodes is one key each
// home.<node>.<kind>.<name> the operator account's own agent directory on one node
//
// Every instance watches the state and takes what applies to it. What it takes lands in one place per kind,
// the one place the vendor honours for it:
//
// skill, agent, command, output-style the plugin `nox-mesh`, in a marketplace in the managed directory
// (at the home scope: the home's own directories)
// instructions sections of the managed instruction file (home: a rule file)
//
// **What a session runs or obeys is not registered here** (novox/hq issue 340). A hook runs a command, and the
// managed settings say what runs without asking, in every session on every node the registration reaches, as
// whoever starts it — the operator, whose account holds root. The tool runner acts for any caller of the mesh MCP
// server and is not told who called, so a registration through these tools could come from an agent confined on
// another node. Hooks, permissions and the rest of the managed settings are the operator's, set at the
// controller's terminal in this module's `managed_settings` setting (the controller refuses that key through a
// verb); the kinds `hook` and `settings` are refused at registration and never taken from the state, and an
// item that would run something itself — a skill's or a command's shell lines, a hook, a server or a mode in its
// front matter, tools it pre-approves — is refused too. What may still be registered is text a session reads.
//
// Tool servers keep their own state and file (`servers`, the managed tool-server file): the exclusive file
// would block a plugin's.
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"time"
"gopkg.in/yaml.v3"
)
// Plugin is the plugin's name, and its marketplace's: what its items are called in a session, `nox-mesh:<name>`.
const Plugin = "nox-mesh"
// MarketplaceDir is where the marketplace is written, inside the managed directory.
const MarketplaceDir = "marketplace"
// MaxItemBytes is the most one registration may carry, its files included: well under the bus's message limit.
const MaxItemBytes = 256 << 10
// The kinds a registration may be.
const (
KindSkill = "skill"
KindAgent = "agent"
KindCommand = "command"
KindHook = "hook"
KindOutputStyle = "output-style"
KindInstructions = "instructions"
KindSettings = "settings"
)
// Kinds is every kind, in the order a list shows them.
var Kinds = []string{KindSkill, KindAgent, KindCommand, KindHook, KindOutputStyle, KindInstructions, KindSettings}
// The scopes.
const (
ScopeMesh = "mesh"
ScopeNode = "node"
ScopeHome = "home"
)
// SettingsName is the one name a settings registration has: a scope holds one set of settings.
const SettingsName = "settings"
var itemName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
// nodeName is what a node may be called in a key.
var nodeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
// AtTheTerminal is how a hook, a permission rule or any other managed setting is set now (novox/hq issue 340):
// in this module's `managed_settings` setting, at the controller's terminal, which refuses it through a verb.
const AtTheTerminal = "mesh-cli settings set claude-code '{\"managed_settings\": {...}}' [--node <node>], " +
"typed by the operator at the controller's terminal: mesh-cli (nox) on the control-node (novox/hq ADR 0272)"
// RunsInASession says why a kind is never registered through this module: what it holds runs in, or is obeyed
// by, every session it reaches, as whoever starts that session (novox/hq issue 340).
func RunsInASession(kind string) string {
what := map[string]string{
KindHook: "a hook runs its command in every session it reaches",
KindSettings: "the managed settings say what runs in every session without asking — permissions, hooks, the status line, the environment",
}[kind]
return what + ", as whoever starts that session — the operator, whose account holds root — and the tool runner is " +
"not told who called it, so an agent on another node could register one. It is set at the controller's " +
"terminal only: " + AtTheTerminal + " (novox/hq issue 340)"
}
// runsFrontMatter are the front matter keys of a skill, a subagent or a command that make it run something
// itself or loosen what runs (novox/hq issue 340): a hook, a tool server, a permission mode, tools it
// pre-approves. Compared lower-case with - and _ taken out.
var runsFrontMatter = map[string]string{
"hooks": "hooks run a command when the item is used",
"mcpservers": "a tool server starts a program",
"permissionmode": "a permission mode changes what runs without asking",
"allowedtools": "allowed-tools lets the tools it names run without asking",
}
// shellLine is a skill's or a command's shell line, run when it is loaded and its output put in the prompt:
// !`command` inline, or a fenced block opened with ```!.
var shellLine = regexp.MustCompile("!`[^`\n]+`|(?m)^[ \t]*```!")
// fileRef is a file the agent may read into the prompt when it loads a skill or a command: `@` and a path, at the
// start of a line or after a space, a bracket or a quote. Any such word counts, a path without a dot or a slash
// (`@Makefile`) as much as one with: refused unless it is one of the item's own files, rather than guessed at.
var fileRef = regexp.MustCompile(`(?:^|[\s(\[{"'<])@([^\s` + "`" + `"')\]}>]+)`)
// runsProblem says why an item's text would run something in a session by itself, or "": the shell lines the
// agent runs when it loads a skill or a command, the front matter keys that start, run or pre-approve, front
// matter the module cannot read for certain, and a file outside the item that the agent would read in.
func runsProblem(it Item) string {
if it.Kind != KindSkill && it.Kind != KindAgent && it.Kind != KindCommand {
return ""
}
rels := make([]string, 0, len(it.Files))
for rel := range it.Files {
rels = append(rels, rel)
}
sort.Strings(rels)
for _, rel := range rels {
if !strings.HasSuffix(rel, ".md") {
continue // a script beside a skill runs only when the agent runs it, through the session's own rules
}
content := strings.TrimPrefix(it.Files[rel], "\ufeff")
if shellLine.MatchString(content) {
return fmt.Sprintf("%s holds a shell line (!`…` or ```!), which the agent runs when it loads the %s, "+
"before any permission is asked; nothing registered through this module runs by itself in a "+
"session (novox/hq issue 340)", rel, it.Kind)
}
keys, problem := frontMatterKeys(content)
if problem != "" {
return fmt.Sprintf("%s: %s, so what it asks of a session cannot be judged; it is refused rather than "+
"guessed at (novox/hq issue 340)", rel, problem)
}
for _, key := range keys {
norm := strings.ToLower(strings.NewReplacer("-", "", "_", "").Replace(key))
if why, ok := runsFrontMatter[norm]; ok {
return fmt.Sprintf("%s says %q in its front matter: %s. Nothing registered through this module "+
"runs or loosens what runs in a session; that is the operator's, at the controller's "+
"terminal (novox/hq issue 340)", rel, key, why)
}
}
if it.Kind == KindSkill || it.Kind == KindCommand {
for _, m := range fileRef.FindAllStringSubmatch(content, -1) {
ref := strings.TrimRight(m[1], ".,;:!?")
if ref == "" {
continue
}
if _, own := it.Files[ref]; !own || pathProblem(ref) != "" {
return fmt.Sprintf("%s refers to @%s, a file outside the %s that the agent reads into its "+
"prompt when it loads it; a registered item refers only to its own files (novox/hq issue 340)",
rel, ref, it.Kind)
}
}
}
}
return ""
}
// frontMatterKeys are the top-level keys of a markdown file's front matter, read as YAML: the block between a
// first line `---` (after a byte-order mark) and the next. A block that does not close, does not parse, or is not
// a mapping is a problem: the agent's own reader may take from it what this one cannot see.
func frontMatterKeys(content string) ([]string, string) {
content = strings.ReplaceAll(strings.TrimPrefix(content, "\ufeff"), "\r\n", "\n")
lines := strings.Split(content, "\n")
first := 0
for first < len(lines) && strings.TrimSpace(lines[first]) == "" {
first++
}
if first == len(lines) || strings.TrimSpace(lines[first]) != "---" {
return nil, ""
}
end := -1
for i := first + 1; i < len(lines); i++ {
if t := strings.TrimSpace(lines[i]); t == "---" || t == "..." {
end = i
break
}
}
if end < 0 {
return nil, "its front matter does not close"
}
block := strings.Join(lines[first+1:end], "\n")
if strings.TrimSpace(block) == "" {
return nil, ""
}
var node yaml.Node
if err := yaml.Unmarshal([]byte(block), &node); err != nil {
return nil, "its front matter is not YAML (" + err.Error() + ")"
}
var top map[string]any
if err := node.Decode(&top); err != nil {
return nil, "its front matter is not a mapping of keys (" + err.Error() + ")"
}
keys := make([]string, 0, len(top))
for k := range top {
keys = append(keys, k)
}
sort.Strings(keys)
return keys, ""
}
// Item is one registration, as the state keeps it.
type Item struct {
Kind string `json:"kind"`
Name string `json:"name"`
Scope string `json:"scope"`
// Files are the item's files by path relative to the item: a skill's SKILL.md and whatever sits beside
// it; for the one-file kinds, `<name>.md`; a hook's scripts.
Files map[string]string `json:"files,omitempty"`
// A hook's event, matcher and command. In the command, ${HOOK_DIR} is the directory its files are in.
Event string `json:"event,omitempty"`
Matcher string `json:"matcher,omitempty"`
Command string `json:"command,omitempty"`
// Settings, for the settings kind: keys of the vendor's settings file.
Settings map[string]any `json:"settings,omitempty"`
// Where it was registered from, and when.
By string `json:"by,omitempty"`
At string `json:"at,omitempty"`
}
// Key is where an item lives in the state, for one node (ignored at the mesh scope).
func (it Item) Key(node string) string {
if it.Scope == ScopeMesh {
return ScopeMesh + "." + it.Kind + "." + it.Name
}
return it.Scope + "." + node + "." + it.Kind + "." + it.Name
}
// ParseKey reads a key back: its scope, the node it is for ("" at the mesh scope), the kind and the name.
func ParseKey(key string) (scope, node, kind, name string, ok bool) {
parts := strings.Split(key, ".")
switch {
case len(parts) == 3 && parts[0] == ScopeMesh:
return ScopeMesh, "", parts[1], parts[2], true
case len(parts) == 4 && (parts[0] == ScopeNode || parts[0] == ScopeHome):
return parts[0], parts[1], parts[2], parts[3], true
}
return "", "", "", "", false
}
func knownKind(k string) bool {
for _, x := range Kinds {
if x == k {
return true
}
}
return false
}
// Problem says why an item cannot be registered, or "".
func (it Item) Problem() string {
if problem := it.shapeProblem(); problem != "" {
return problem
}
return runsProblem(it)
}
// shapeProblem says why an item is not well formed, or "": its kind, scope, name, paths, files and size, without
// judging what its text asks of a session — which the operator's own text, set at the terminal, may.
func (it Item) shapeProblem() string {
if !knownKind(it.Kind) {
return fmt.Sprintf("%q is not a kind: %s", it.Kind, strings.Join(Kinds, ", "))
}
if it.Scope != ScopeMesh && it.Scope != ScopeNode && it.Scope != ScopeHome {
return fmt.Sprintf("%q is not a scope: mesh, node or home", it.Scope)
}
if it.Kind == KindHook || it.Kind == KindSettings {
return RunsInASession(it.Kind)
}
if !itemName.MatchString(it.Name) {
return fmt.Sprintf("%q is not a name: lower-case letters, digits and -, at most 64", it.Name)
}
for rel := range it.Files {
if problem := pathProblem(rel); problem != "" {
return problem
}
for other := range it.Files {
if strings.HasPrefix(other, rel+"/") {
return fmt.Sprintf("%q is a file and also the directory of %q", rel, other)
}
}
}
switch it.Kind {
case KindSkill:
if _, ok := it.Files["SKILL.md"]; !ok {
return "a skill needs a SKILL.md"
}
case KindAgent, KindCommand, KindOutputStyle, KindInstructions:
if len(it.Files) != 1 || strings.TrimSpace(it.Files[it.Name+".md"]) == "" {
return "this kind is one file, " + it.Name + ".md, with content"
}
}
if n := it.size(); n > MaxItemBytes {
return fmt.Sprintf("%d bytes: more than the %d one registration may carry", n, MaxItemBytes)
}
return ""
}
// pathProblem says why a file's path is not one inside the item, or "": relative, slash-separated, every
// segment a real name — never empty, `.` or `..`.
func pathProblem(rel string) string {
if rel == "" || path.IsAbs(rel) || strings.ContainsAny(rel, "\\\x00") {
return fmt.Sprintf("%q is not a path inside the item", rel)
}
for _, seg := range strings.Split(rel, "/") {
if seg == "" || seg == "." || seg == ".." {
return fmt.Sprintf("%q is not a path inside the item", rel)
}
}
return ""
}
func (it Item) size() int {
raw, _ := json.Marshal(it)
return len(raw)
}
// ---- the view -------------------------------------------------------------------------------------
// ConfigView is what this node takes from the `config` state: every mesh item, and the node and home items
// for this node — kept in memory from the watch and written through to the module's own file, so the
// managed directory renders without the bus.
type ConfigView struct {
p Paths
mu sync.Mutex
items map[string]Item
}
// NewConfigView is the view as last written through, or empty.
func NewConfigView(p Paths) *ConfigView {
var kept map[string]Item
_ = readJSON(p.config(), &kept)
return &ConfigView{p: p, items: Taken(kept)}
}
// Taken is what of the items kept in the module's own file may be rendered: each judged again as the state's watch
// judges it, so what was written through before this module refused some kinds — a hook, settings, an item that
// runs something by itself — is never rendered (novox/hq issue 340).
func Taken(kept map[string]Item) map[string]Item {
out := map[string]Item{}
for key, it := range kept {
_, node, _, _, ok := ParseKey(key)
if ok && it.Problem() == "" && it.Key(node) == key {
out[key] = it
}
}
return out
}
// Applies says whether a key is this node's to take.
func (v *ConfigView) Applies(key string) bool {
scope, node, _, _, ok := ParseKey(key)
return ok && (scope == ScopeMesh || node == v.p.Node)
}
// Take takes one change, and answers whether what applies to this node changed.
func (v *ConfigView) Take(key, op string, item *Item) bool {
if !v.Applies(key) {
return false
}
_, node, _, _, _ := ParseKey(key)
v.mu.Lock()
defer v.mu.Unlock()
// Only an item that is what its key says: a key decides which nodes take an item, the item where it
// lands, and the two must agree — a mesh key holding a home item would land in every home.
if op == "put" && item != nil && item.Problem() == "" && item.Key(node) == key {
v.items[key] = *item
} else {
delete(v.items, key)
}
return v.writeThroughLocked()
}
// Prune drops what the view holds and the state no longer does: a registration removed while this node
// was away is never handed over by the watch, which hands over what is there, not what went.
func (v *ConfigView) Prune(present []string) bool {
keep := map[string]bool{}
for _, k := range present {
keep[k] = true
}
v.mu.Lock()
defer v.mu.Unlock()
for k := range v.items {
if !keep[k] {
delete(v.items, k)
}
}
return v.writeThroughLocked()
}
// Items is what applies here, by key.
func (v *ConfigView) Items() map[string]Item {
v.mu.Lock()
defer v.mu.Unlock()
out := make(map[string]Item, len(v.items))
for k, it := range v.items {
out[k] = it
}
return out
}
// writeThroughLocked writes the view to its file, with v.mu held: the snapshot and the write are one step, so
// an older snapshot never lands after a newer one.
func (v *ConfigView) writeThroughLocked() bool {
now, _ := indented(v.items)
before, _ := fsys.ReadFile(v.p.config())
if string(before) == string(now) {
return false
}
_ = fsys.WriteFile(v.p.config(), now, 0o600)
return true
}
// Config is what applies to this node, sorted for rendering.
type Config struct {
Mesh, Node, Home []Item
// Terminal is the shared text the operator set at the controller's terminal, in this module's settings
// (novox/hq issue 340): laid over what was registered for the mesh and the node, for every session here.
Terminal []Item
// Approved is the sections the operator approved through the operator channel (novox/hq ADR 0274), as this
// node proved and keeps them, that reach this node.
Approved []ApprovedSection
}
// ConfigOf sorts the items that apply here by scope, each scope by kind and name.
func ConfigOf(items map[string]Item) Config {
var c Config
for _, it := range items {
switch it.Scope {
case ScopeMesh:
c.Mesh = append(c.Mesh, it)
case ScopeNode:
c.Node = append(c.Node, it)
case ScopeHome:
c.Home = append(c.Home, it)
}
}
for _, list := range [][]Item{c.Mesh, c.Node, c.Home} {
sort.Slice(list, func(i, j int) bool {
if list[i].Kind != list[j].Kind {
return list[i].Kind < list[j].Kind
}
return list[i].Name < list[j].Name
})
}
return c
}
// ---- rendering --------------------------------------------------------------------------------------
// PluginFile is one file of the marketplace: its content and whether it is run — or, for a binary, the
// file it is copied from (the guard's, which is this module's own executable).
type PluginFile struct {
Content string
Executable bool
From string `json:",omitempty"`
}
// GuardDir is where the guard on the agent's shell lives in the plugin (novox/hq ADR 0245): its binary and
// what it is given. Outside hooks/, so no registered hook's name can be it.
const GuardDir = "guard"
// GuardFiles are the guard as the plugin carries it: the binary to copy, what it judges with, and — the same
// binary again — the waits directory the SessionStart and Stop hooks are given (novox/hq ADR 0489, issues
// 1744, 1743). Empty WaitsDir places no such hook.
type GuardFiles struct {
Exe string
Data GuardData
WaitsDir string
}
// GuardMatcher is what the guard is asked about: every tool. Its own rules judge the shell and the agent's file
// edits and let every other tool through; the rule of the account agents run as (novox/hq ADR 0266) refuses
// every tool of a session running as another, reading and the mesh's tools included, so that session can do
// nothing at all.
const GuardMatcher = "*"
// GuardCommand is the hook's command line: by its path under the managed directory, root's — never the
// copy the agent may keep of a plugin, which the session could change.
func GuardCommand() string {
dir := filepath.Join(ManagedDir, MarketplaceDir, "plugins", Plugin, GuardDir)
return `"` + filepath.Join(dir, "guard") + `" guard "` + filepath.Join(dir, "guard.json") + `"`
}
// SessionStartCommand is the plugin's SessionStart hook: the guard's own binary, told the session's open
// waits live under waitsDir (novox/hq ADR 0489 §3, issue 1744).
func SessionStartCommand(waitsDir string) string {
dir := filepath.Join(ManagedDir, MarketplaceDir, "plugins", Plugin, GuardDir)
return `"` + filepath.Join(dir, "guard") + `" session-start "` + waitsDir + `"`
}
// StopCommand is the plugin's Stop hook: the guard's own binary, refusing to end the turn while a wait under
// waitsDir is open and unwatched, or past its bound and untold (novox/hq ADR 0489 §3-§6, issue 1743).
func StopCommand(waitsDir string) string {
dir := filepath.Join(ManagedDir, MarketplaceDir, "plugins", Plugin, GuardDir)
return `"` + filepath.Join(dir, "guard") + `" stop "` + waitsDir + `"`
}
// Marketplace is the marketplace directory's whole content, by path inside it. A node item of a name laid
// over a mesh item of the same kind and name: the node's wins.
func Marketplace(c Config) map[string]PluginFile { return MarketplaceWith(c, nil) }
// MarketplaceWith is the marketplace with the guard on the agent's shell, when there is one to place: its
// hook first, before any registered one.
func MarketplaceWith(c Config, guard *GuardFiles) map[string]PluginFile {
root := "plugins/" + Plugin + "/"
out := map[string]PluginFile{
".claude-plugin/marketplace.json": {Content: jsonFile(map[string]any{
"name": Plugin,
"owner": map[string]any{"name": "the mesh"},
"plugins": []any{map[string]any{"name": Plugin, "source": "./plugins/" + Plugin,
"description": "What the mesh registered for the agent: written by the claude-code module, never by hand."}},
})},
root + ".claude-plugin/plugin.json": {Content: jsonFile(map[string]any{
"name": Plugin, "version": "1.0.0",
"description": "What the mesh registered for the agent: written by the claude-code module, never by hand.",
"author": map[string]any{"name": "the mesh"},
})},
}
chosen := map[string]Item{}
for _, layer := range [][]Item{c.Mesh, c.Node, c.Terminal} {
for _, it := range layer {
chosen[it.Kind+"/"+it.Name] = it
}
}
keys := make([]string, 0, len(chosen))
for k := range chosen {
keys = append(keys, k)
}
sort.Strings(keys)
hooks := map[string][]any{}
if guard != nil && guard.Exe != "" {
out[root+GuardDir+"/guard"] = PluginFile{From: guard.Exe, Executable: true}
out[root+GuardDir+"/guard.json"] = PluginFile{Content: jsonFile(guard.Data)}
hooks["PreToolUse"] = append(hooks["PreToolUse"], map[string]any{"matcher": GuardMatcher,
"hooks": []any{map[string]any{"type": "command", "command": GuardCommand(), "timeout": 15}}})
if guard.WaitsDir != "" {
// A session's wait list (novox/hq ADR 0489): the SessionStart hook names the session's id and its open
// waits (issue 1744); the Stop hook keeps the turn open while one is unwatched or untold (issue 1743).
hooks["SessionStart"] = append(hooks["SessionStart"], map[string]any{"matcher": "startup|resume|clear|compact",
"hooks": []any{map[string]any{"type": "command", "command": SessionStartCommand(guard.WaitsDir), "timeout": 10}}})
hooks["Stop"] = append(hooks["Stop"], map[string]any{
"hooks": []any{map[string]any{"type": "command", "command": StopCommand(guard.WaitsDir), "timeout": 5}}})
}
}
for _, k := range keys {
it := chosen[k]
switch it.Kind {
case KindSkill:
for rel, content := range it.Files {
out[root+"skills/"+it.Name+"/"+rel] = PluginFile{Content: content, Executable: isScript(rel, content)}
}
case KindAgent:
out[root+"agents/"+it.Name+".md"] = PluginFile{Content: it.Files[it.Name+".md"]}
case KindCommand:
out[root+"commands/"+it.Name+".md"] = PluginFile{Content: it.Files[it.Name+".md"]}
case KindOutputStyle:
out[root+"output-styles/"+it.Name+".md"] = PluginFile{Content: it.Files[it.Name+".md"]}
}
// A hook is never rendered from a registration (novox/hq issue 340): the plugin's hooks are the mesh's own
// guard alone, and any other is the operator's, in the managed settings set at the controller's terminal.
}
if len(hooks) > 0 {
out[root+"hooks/hooks.json"] = PluginFile{Content: jsonFile(map[string]any{"hooks": hooks})}
}
return out
}
func isScript(rel, content string) bool {
return strings.HasPrefix(content, "#!") || strings.HasSuffix(rel, ".sh")
}
// MarketplaceKeys are the two managed settings keys that name the marketplace and enable the plugin.
func MarketplaceKeys() map[string]any {
return map[string]any{
"extraKnownMarketplaces": map[string]any{Plugin: map[string]any{
"source": map[string]any{"source": "directory", "path": filepath.Join(ManagedDir, MarketplaceDir)}}},
"enabledPlugins": map[string]any{Plugin + "@" + Plugin: true},
}
}
// mergeSettings lays b over a: objects are merged key by key, lists are joined without repeats (a
// permission rule or an auto-mode rule added for a node adds to the mesh's), and anything else is b's.
func mergeSettings(a, b map[string]any) map[string]any {
out := map[string]any{}
for k, v := range a {
out[k] = v
}
for k, v := range b {
switch nb := v.(type) {
case map[string]any:
if na, ok := out[k].(map[string]any); ok {
out[k] = mergeSettings(na, nb)
continue
}
case []any:
if la, ok := out[k].([]any); ok {
joined := append([]any{}, la...)
seen := map[string]bool{}
for _, x := range la {
raw, _ := json.Marshal(x)
seen[string(raw)] = true
}
for _, x := range nb {
raw, _ := json.Marshal(x)
if !seen[string(raw)] {
seen[string(raw)] = true
joined = append(joined, x)
}
}
out[k] = joined
continue
}
}
out[k] = v
}
return out
}
// InstructionSections is what the managed instruction file adds after the mesh's own text: the sections
// registered for the mesh, then those for this node.
func InstructionSections(c Config) string {
var b strings.Builder
// The approved sections that replace or remove a registered one, by its name and the digest of its text, and the
// proposals already placed where the registered section stood.
over, placed := map[string]ApprovedSection{}, map[string]bool{}
for _, s := range c.Approved {
if s.Over != "" {
over[s.Name+"|"+s.Over] = s
}
}
registered := "Registered through the `claude-code` module before that moved to the controller's terminal; change them there, in the module's `instructions` setting."
for _, part := range []struct {
title, note string
items []Item
}{{"Instructions for every node", registered, c.Mesh}, {"Instructions for this node", registered, c.Node},
{"Instructions set at the controller's terminal", "Set in the `claude-code` module's `instructions` setting, at the controller's terminal.", c.Terminal}} {
var sections []Item
for _, it := range part.items {
if it.Kind == KindInstructions {
sections = append(sections, it)
}
}
if len(sections) == 0 {
continue
}
fmt.Fprintf(&b, "\n## %s\n\n%s\n", part.title, part.note)
for _, it := range sections {
text := it.Files[it.Name+".md"]
if s, ok := over[it.Name+"|"+textDigest(text)]; ok {
// What the operator approved over this registered section stands in its place, or leaves it out.
placed[s.Proposal] = true
if s.Removes {
continue
}
fmt.Fprintf(&b, "\n### %s\n\n*Replaced on the operator's approval (proposed from %s; approved %s).*\n\n%s\n", it.Name, s.From,
s.Approved.UTC().Format("2006-01-02 15:04 UTC"), strings.TrimSpace(s.Text))
continue
}
fmt.Fprintf(&b, "\n### %s\n\n%s\n", it.Name, strings.TrimSpace(text))
}
}
var approved []ApprovedSection
for _, s := range c.Approved {
if !s.Removes && !placed[s.Proposal] {
approved = append(approved, s)
}
}
if len(approved) > 0 {
b.WriteString("\n## Instructions the operator approved\n\nProposed by an agent through the `claude-code` module and " +
"approved by the operator on a channel that proves who answered (novox/hq ADR 0274). Changed or removed only the " +
"same way: `claude_code_instructions_propose`.\n")
for _, s := range approved {
fmt.Fprintf(&b, "\n### %s\n\n*Proposed from %s; approved %s.*\n\n%s\n", s.Name, s.From,
s.Approved.UTC().Format("2006-01-02 15:04 UTC"), strings.TrimSpace(s.Text))
}
}
return b.String()
}
// HomeFiles is what the home scope places in the operator account's agent directory, by path relative to
// that directory.
func HomeFiles(c Config) map[string]string {
out := map[string]string{}
for _, it := range c.Home {
switch it.Kind {
case KindSkill:
for rel, content := range it.Files {
out["skills/"+it.Name+"/"+rel] = content
}
case KindAgent:
out["agents/"+it.Name+".md"] = it.Files[it.Name+".md"]
case KindCommand:
out["commands/"+it.Name+".md"] = it.Files[it.Name+".md"]
case KindOutputStyle:
out["output-styles/"+it.Name+".md"] = it.Files[it.Name+".md"]
case KindInstructions:
out["rules/"+it.Name+".md"] = it.Files[it.Name+".md"]
}
}
return out
}
// ---- the home ---------------------------------------------------------------------------------------
// Placed is what this module placed in the home, by path relative to the agent directory, with the digest
// of what it wrote (ADR 0182: the mesh owns what it places, and only that).
type Placed map[string]string
func digest(s string) string {
sum := sha256.Sum256([]byte(s))
return hex.EncodeToString(sum[:])
}
// deletedByHand marks, in the record, a path the mesh placed and the person then deleted: their choice,
// kept until the item is unregistered.
const deletedByHand = "deleted-by-hand"
// PlaceHome brings the home in line with what the home scope wants (ADR 0182): it writes what is wanted and
// absent, or its own; it never writes a path the person made, nor through a directory that is a symbolic
// link; it removes what it placed and is no longer wanted, unless the person changed it since; and a file it
// placed that the person deleted stays deleted until the item is unregistered. Answers what it did and what
// it left alone, and why.
func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
return placeHomeIn(filepath.Join(p.Home, ".claude"), p.placed(), want)
}
// placeHomeIn is PlaceHome for one agent directory and its own record of what was placed there.
func placeHomeIn(dir, record string, want map[string]string) (done []string, left []string) {
var placed Placed
if !readJSON(record, &placed) {
placed = Placed{}
}
paths := make([]string, 0, len(want))
for rel := range want {
paths = append(paths, rel)
}
sort.Strings(paths)
for _, rel := range paths {
full := filepath.Join(dir, filepath.FromSlash(rel))
content := want[rel]
if why := linkedParent(dir, rel); why != "" {
left = append(left, rel+": "+why+", left alone")
continue
}
current, err := fsys.ReadFile(full)
exists := err == nil
ours, wasPlaced := placed[rel]
switch {
case !exists && wasPlaced && ours == deletedByHand:
continue
case !exists && wasPlaced:
placed[rel] = deletedByHand
left = append(left, rel+": deleted by hand, left deleted until the item is unregistered")
continue
case exists && !wasPlaced:
left = append(left, rel+": the person's own, left alone")
continue
case exists && ours == deletedByHand:
left = append(left, rel+": made again by hand after the mesh's was deleted, left alone")
continue
case exists && string(current) == content:
continue
case exists && digest(string(current)) != ours:
left = append(left, rel+": changed by hand since it was placed, left alone")
continue
}
if err := fsys.MkdirAll(filepath.Dir(full), 0o755); err != nil {
left = append(left, rel+": "+err.Error())
continue
}
mode := os.FileMode(0o644)
if isScript(rel, content) {
mode = 0o755
}
if err := fsys.WriteFile(full, []byte(content), mode); err != nil {
left = append(left, rel+": "+err.Error())
continue
}
placed[rel] = digest(content)
done = append(done, rel+": placed")
}
for rel, ours := range placed {
if _, wanted := want[rel]; wanted {
continue
}
full := filepath.Join(dir, filepath.FromSlash(rel))
if ours == deletedByHand {
delete(placed, rel)
continue
}
if why := linkedParent(dir, rel); why != "" {
left = append(left, rel+": no longer registered, but "+why+", left alone")
continue
}
current, err := fsys.ReadFile(full)
if err == nil && digest(string(current)) != ours {
left = append(left, rel+": no longer registered, but changed by hand, left alone")
delete(placed, rel)
continue
}
if err := fsys.Remove(full); err != nil && !os.IsNotExist(err) {
left = append(left, rel+": no longer registered, and could not be removed: "+err.Error())
continue
}
// Up to the kind's own directory, never it: `skills/<name>` goes when empty, `skills` stays.
removeEmptyParents(filepath.Join(dir, strings.SplitN(rel, "/", 2)[0]), filepath.Dir(full))
delete(placed, rel)
done = append(done, rel+": removed")
}
raw, _ := indented(placed)
if err := fsys.WriteFile(record, raw, 0o600); err != nil {
left = append(left, "the record of what was placed could not be saved: "+err.Error())
}
return done, left
}
// linkedParent says, when a directory between the agent directory and a file is a symbolic link, which one:
// writing through it would write wherever it points.
func linkedParent(dir, rel string) string {
parts := strings.Split(rel, "/")
at := dir
for _, seg := range parts[:len(parts)-1] {
at = filepath.Join(at, seg)
if info, err := fsys.Lstat(at); err == nil && info.Mode()&os.ModeSymlink != 0 {
return at + " is a symbolic link"
}
}
return ""
}
// removeEmptyParents removes empty directories from dir up to, not including, root.
func removeEmptyParents(root, dir string) {
for dir != root && strings.HasPrefix(dir, root+string(filepath.Separator)) {
if err := fsys.Remove(dir); err != nil {
return
}
dir = filepath.Dir(dir)
}
}
// HomeConflict says whether registering an item at the home scope here would collide with something the
// person made: "" when it would not.
func HomeConflict(p Paths, it Item) string {
var placed Placed
_ = readJSON(p.placed(), &placed)
for rel := range HomeFiles(Config{Home: []Item{it}}) {
if _, ours := placed[rel]; ours {
continue
}
if _, err := fsys.Stat(filepath.Join(p.Home, ".claude", filepath.FromSlash(rel))); err == nil {
return fmt.Sprintf("%s already exists in this home and the mesh did not place it; pick another name, or import it", rel)
}
}
return ""
}
// ---- what the module did not place ------------------------------------------------------------------
// HomeItem is one item found in the home.
type HomeItem struct {
Kind string `json:"kind"`
Name string `json:"name"`
Path string `json:"path"`
Placed bool `json:"placedByTheMesh"`
Notes []string `json:"notes,omitempty"`
}
var mcpToolRef = regexp.MustCompile(`mcp__([A-Za-z0-9_-]+)__[A-Za-z0-9_-]+`)
// HomeItems lists the home's skills, subagents, commands, output styles, rule files and its own memory (CLAUDE.md), says which the mesh
// placed, and notes those that share a name with a mesh item or call a tool server that is not loaded here.
func HomeItems(p Paths, c Config, loaded Servers) []HomeItem {
dir := filepath.Join(p.Home, ".claude")
var placed Placed
_ = readJSON(p.placed(), &placed)
inPlugin := map[string]bool{}
for _, layer := range [][]Item{c.Mesh, c.Node, c.Terminal} {
for _, it := range layer {
inPlugin[it.Kind+"/"+it.Name] = true
}
}
var out []HomeItem
add := func(kind, name, rel, body string) {
h := HomeItem{Kind: kind, Name: name, Path: filepath.Join(dir, filepath.FromSlash(rel))}
_, h.Placed = placed[rel]
if inPlugin[kind+"/"+name] {
h.Notes = append(h.Notes, "the mesh also registers a "+kind+" of this name, offered as "+Plugin+":"+name)
}
seen := map[string]bool{}
for _, m := range mcpToolRef.FindAllStringSubmatch(body, -1) {
if server := m[1]; !seen[server] && server != meshEntry && loaded[server] == nil {
seen[server] = true
h.Notes = append(h.Notes, "calls tools of `"+server+"`, a tool server not loaded on this node")
}
}
out = append(out, h)
}
if entries, err := fsys.ReadDir(filepath.Join(dir, "skills")); err == nil {
for _, e := range entries {
if !e.IsDir() {
continue
}
body, err := fsys.ReadFile(filepath.Join(dir, "skills", e.Name(), "SKILL.md"))
if err != nil {
continue // the vendor's own synced folders carry none at their top
}
add(KindSkill, e.Name(), "skills/"+e.Name()+"/SKILL.md", string(body))
}
}
for kind, sub := range map[string]string{KindAgent: "agents", KindCommand: "commands", KindOutputStyle: "output-styles", KindInstructions: "rules"} {
entries, err := fsys.ReadDir(filepath.Join(dir, sub))
if err != nil {
continue
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
body, _ := fsys.ReadFile(filepath.Join(dir, sub, e.Name()))
add(kind, strings.TrimSuffix(e.Name(), ".md"), sub+"/"+e.Name(), string(body))
}
}
if body, err := fsys.ReadFile(filepath.Join(dir, "CLAUDE.md")); err == nil {
add(KindMemory, memoryName, "CLAUDE.md", string(body))
}
sort.Slice(out, func(i, j int) bool {
if out[i].Kind != out[j].Kind {
return out[i].Kind < out[j].Kind
}
return out[i].Name < out[j].Name
})
return out
}
// ImportFromHome reads one item of this node's home as an item to register. A rule file becomes instructions.
func ImportFromHome(p Paths, kind, name string) (Item, error) {
dir := filepath.Join(p.Home, ".claude")
it := Item{Kind: kind, Name: name, Files: map[string]string{}}
switch kind {
case KindSkill:
root := filepath.Join(dir, "skills", name)
err := fsys.WalkDir(root, func(full string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
rel, _ := filepath.Rel(root, full)
raw, err := fsys.ReadFile(full)
if err != nil {
return err
}
it.Files[filepath.ToSlash(rel)] = string(raw)
return nil
})
if err != nil {
return Item{}, fmt.Errorf("the skill %s in this home: %w", name, err)
}
case KindAgent, KindCommand, KindOutputStyle, KindInstructions:
sub := map[string]string{KindAgent: "agents", KindCommand: "commands", KindOutputStyle: "output-styles", KindInstructions: "rules"}[kind]
raw, err := fsys.ReadFile(filepath.Join(dir, sub, name+".md"))
if err != nil {
return Item{}, fmt.Errorf("the %s %s in this home: %w", kind, name, err)
}
it.Files[name+".md"] = string(raw)
default:
return Item{}, fmt.Errorf("a %s is not imported from a home", kind)
}
return it, nil
}
// ---- registering ------------------------------------------------------------------------------------
// ConfigState is the `config` state as this module reaches it through the runtime.
type ConfigState interface {
Put(key string, value any) error
Delete(key string) error
Keys() ([]string, error)
Get(key string) (json.RawMessage, bool, error)
}
// SharedTextAtTheTerminal is why text for every node or some is not registered through this module (novox/hq issue
// 340, the operator's decision of 2026-10-09): an instruction section, an output style, a skill, a command or a
// subagent at the mesh or node scope reaches every session on those nodes as the mesh's own word, and the tool
// runner cannot tell an agent from the operator. Until a surface of the operator's own updates it, it is set at
// the controller's terminal, in this module's settings; a person's own home stays theirs to register into.
func SharedTextAtTheTerminal(kind string) string {
key := map[string]string{KindSkill: "skills", KindAgent: "agents", KindCommand: "commands",
KindOutputStyle: "output_styles", KindInstructions: "instructions"}[kind]
return "a " + kind + " for every node or some reaches every session there as the mesh's own word, and this tool " +
"cannot tell an agent from the operator. It is set at the controller's terminal only, under `" + key +
"` in this module's settings, its text as a list of lines, on the control-node: mesh-cli settings set claude-code " +
"'{\"" + key + "\": {\"<name>\": [\"line\", …]}}' [--node <node>] (novox/hq issue 340)." +
proposeInstead(kind) + " The home scope, " +
"one account's own directory, is still registered here. Nothing was registered"
}
// proposeInstead is, for an instruction section, the way an agent may still bring one to every node: proposed for
// the operator's approval (novox/hq ADR 0274).
func proposeInstead(kind string) string {
if kind != KindInstructions {
return ""
}
return " An agent may instead propose it for the operator's approval with claude_code_instructions_propose: it " +
"becomes active only when the operator approves it on a channel that proves who answered (novox/hq ADR 0274)."
}
// Register puts an item (or, with unregister, removes it) at its scope: at the mesh scope one key, at the
// node and home scopes one key per node — this node when none is given. Taken into this node's view at once,
// so the answer says what it did here.
func Register(p Paths, it Item, nodes []string, unregister bool, state ConfigState, v *ConfigView, write WriteManaged) (map[string]any, error) {
if !unregister && it.Scope != ScopeHome && it.Kind != KindHook && it.Kind != KindSettings && knownKind(it.Kind) {
return nil, errors.New(SharedTextAtTheTerminal(it.Kind))
}
if !unregister {
it.By, it.At = p.Node, time.Now().UTC().Format(time.RFC3339)
if problem := it.Problem(); problem != "" {
return map[string]any{"registered": false, "reason": problem}, nil
}
} else if !knownKind(it.Kind) {
return map[string]any{"unregistered": false, "reason": fmt.Sprintf("%q is not a kind", it.Kind)}, nil
}
if it.Scope == ScopeMesh {
nodes = []string{""}
} else if len(nodes) == 0 {
nodes = []string{p.Node}
} else if problem := nodesProblem(nodes); problem != "" {
return map[string]any{verbOf(unregister): false, "reason": problem}, nil
}
if !unregister && it.Scope == ScopeHome {
// Every target judged before anything is put: a list is refused whole (novox/hq issue 340).
for _, n := range nodes {
if why := HomeScopeRefused(p, n); why != "" {
return nil, errors.New(why)
}
if conflict := HomeConflict(p, it); conflict != "" {
return map[string]any{"registered": false, "reason": conflict}, nil
}
}
}
// Compared before and after rather than read from Take: this node's own watch may take the same change
// first, and then Take here finds nothing new although this call made it.
before, _ := json.Marshal(v.Items())
var keys []string
for _, n := range nodes {
key := it.Key(n)
keys = append(keys, key)
var err error
if unregister {
err = state.Delete(key)
} else {
err = state.Put(key, it)
}
if err != nil {
return nil, err
}
op := "put"
if unregister {
op = "delete"
}
item := it
v.Take(key, op, &item)
}
after, _ := json.Marshal(v.Items())
here := string(before) != string(after)
for _, k := range keys {
here = here || v.Applies(k)
}
verb := map[bool]string{false: "registered", true: "unregistered"}[unregister]
answer := map[string]any{verb: it.Kind + " " + it.Name, "keys": keys}
if !unregister {
switch {
case it.Scope == ScopeHome && it.Kind == KindCommand:
answer["offered as"] = "/" + it.Name
case it.Scope == ScopeHome:
answer["offered as"] = it.Name + ", from the home"
case it.Kind == KindSkill || it.Kind == KindAgent || it.Kind == KindOutputStyle:
answer["offered as"] = Plugin + ":" + it.Name
case it.Kind == KindCommand:
answer["offered as"] = "/" + Plugin + ":" + it.Name
}
}
if here {
// Rendered whenever it applies here, so the answer says what this node wrote, whichever of the
// watch and this call took the change.
rendered, err := RenderNow(p, write)
answer["rendered here"] = rendered
if err != nil {
answer["not written here"] = err.Error() // kept on the bus all the same; the next render tries again
}
answer["sessions"] = "a new session takes it; a running one at /reload-plugins"
} else {
answer["here"] = "not this node: each node it is for takes it from the bus"
}
return answer, nil
}
func verbOf(unregister bool) string {
return map[bool]string{false: "registered", true: "unregistered"}[unregister]
}
// nodesProblem says why a list of nodes cannot name keys, or "". "all" has been expanded before this.
func nodesProblem(nodes []string) string {
for _, n := range nodes {
if !nodeName.MatchString(n) {
return fmt.Sprintf("%q is not a node's name", n)
}
}
return ""
}
// ExpandNodes turns `all` into every node running the module; anything else is kept.
func ExpandNodes(nodes []string, running func() ([]string, error)) ([]string, error) {
if len(nodes) != 1 || nodes[0] != "all" {
return nodes, nil
}
all, err := running()
if err != nil {
return nil, fmt.Errorf("which nodes run claude-code: %w", err)
}
if len(all) == 0 {
return nil, fmt.Errorf("no node is known to run claude-code")
}
return all, nil
}
// List is every registration of a kind ("" for every kind) on the mesh, by key, read from the state.
func List(state ConfigState, kind string) (map[string]any, error) {
keys, err := state.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
out := map[string]any{}
for _, key := range keys {
_, _, k, _, ok := ParseKey(key)
if !ok || (kind != "" && k != kind) {
continue
}
raw, found, err := state.Get(key)
if err != nil || !found {
continue
}
var it Item
if json.Unmarshal(raw, &it) != nil {
continue
}
files := make([]string, 0, len(it.Files))
for f := range it.Files {
files = append(files, f)
}
sort.Strings(files)
summary := map[string]any{"by": it.By, "at": it.At}
if len(files) > 0 {
summary["files"] = files
}
if it.Kind == KindHook {
summary["event"], summary["matcher"], summary["command"] = it.Event, it.Matcher, it.Command
}
if it.Kind == KindSettings {
summary["settings"] = it.Settings
}
out[key] = summary
}
return out, nil
}
// Show is one registration in full: its files' content included.
func Show(state ConfigState, key string) (any, error) {
raw, found, err := state.Get(key)
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("nothing is registered at %s", key)
}
var it Item
if err := json.Unmarshal(raw, &it); err != nil {
return nil, err
}
return it, nil
}