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.. every node running the agent // node... one node — a list of nodes is one key each // home... 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:`. 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 ], " + "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, `.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 and the data it judges with. type GuardFiles struct { Exe string Data GuardData } // 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") + `"` } // 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}}}) } 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 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 { fmt.Fprintf(&b, "\n### %s\n\n%s\n", it.Name, strings.TrimSpace(it.Files[it.Name+".md"])) } } if len(c.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 c.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/` 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 + "\": {\"\": [\"line\", …]}}' [--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 }