Merge pull request 'claude-code: register the agent's configuration at three scopes, served as the nox-mesh plugin (hq ADR 0216)' (#52) from feat/nox-mesh-plugin into main

This commit was merged in pull request #52.
This commit is contained in:
2026-10-05 12:30:10 +00:00
9 changed files with 1996 additions and 27 deletions
+29 -4
View File
@@ -22,11 +22,17 @@ whenever the node's tool runtime collects the module's tools:
| file | holds |
|---|---|
| `managed-mcp.json` | the tool servers every session loads: the mesh's console as `mesh`, and the servers set in this module's `mcp_servers` setting. **Exclusive**: a server not listed here does not load — not one added with `claude mcp add`, not a project's `.mcp.json`, not a plugin's |
| `managed-settings.json` | the keys set in this module's `managed_settings` setting, under the mesh's own: the repositories' attribution convention, the claude.ai connectors kept beside the managed servers, and the key-helper while the node holds an API-key licence |
| `CLAUDE.md` | how a session on this mesh works, this node's name and role, the conventions |
| `managed-settings.json` | the keys set in this module's `managed_settings` setting, then the settings registered through this module (the mesh's, then this node's), under the mesh's own keys: the repositories' attribution convention, the claude.ai connectors kept beside the managed servers, the key-helper while the node holds an API-key licence, and the two that name the `nox-mesh` marketplace and enable its plugin |
| `CLAUDE.md` | how a session on this mesh works, this node's name and role, the conventions — then the instruction sections registered for every node and for this one |
| `marketplace/` | the `nox-mesh` plugin (hq ADR 0216): the skills, subagents, commands, hooks and output styles registered for every node and for this one, offered in a session as `nox-mesh:<name>`. Replaced whole, staged beside and swapped in |
Under the operator's home, only `~/.claude/.credentials.json`, and only when the licence manager hands
this node a subscription token. Nothing else under the home is read or written.
Under the operator's home: `~/.claude/.credentials.json`, only when the licence manager hands this node a
subscription token; and what is registered at the **home** scope for this node — a skill, subagent,
command, output style, or instructions as a rule file — each path recorded in the module's state
(`home-placed.json`). It writes, changes and removes only those — never a path the person made, even one with the
same content, and never through a directory that is a symbolic link. A placed file changed by hand is left
alone, and one the person deleted stays deleted until the item is unregistered (hq ADR 0182). The status tool reads the rest of the home's
items to report them; nothing else is read or written.
## Over NATS
@@ -41,6 +47,7 @@ must see, a node that joins later included — kept, so it carries no secret eit
| a person ran `/login` here | the credentials file gains a refresh token this module never writes; its next report shows it, and the licence manager asks `claude_code_grant` for it, giving its key — the one time a refresh token leaves the node, for the manager to adopt by refreshing it |
| what this node should hold | the licence manager's `bindings` state, this node's key; on a newer generation this module asks `anthropic-licence-manager.current` for its token, sealed to the key it sends, and writes it access-token-only — so the agent here never refreshes. A node that was off reads its key when it is back |
| an MCP server registered through this module | a key in the module's `servers` state — `all.<server>` for every node, `<node>.<server>` for one; every node watches it and renders what applies to it, a node's own entry over the one for every node. A node that joins later, or was off, reads the whole current set at start; unregistering is a delete. An entry with a secret in its `env` or `headers` is refused by the runtime |
| the agent's configuration (hq ADR 0216) | a key in the module's `config` state per registration — `mesh.<kind>.<name>` for every node, `node.<node>.<kind>.<name>` for one, `home.<node>.<kind>.<name>` for one account's own directory — the item and its files in one value, at most 256 KiB. Every node watches it and renders what applies to it, a node item over a mesh item of the same kind and name |
## Tools
@@ -49,6 +56,24 @@ manager), `claude_code_mcp_list`,
`claude_code_mcp_register` (this node by default; `nodes: "all"` or a list for more — called for this
node alone, its answer names the other nodes running claude-code), `claude_code_mcp_unregister`.
The agent's configuration (hq ADR 0216), each registered at a **scope** — `mesh` (the default), `node`
(`nodes`: a list, or `"all"` for every node running claude-code; absent is this node) or `home` (the
operator account's own `~/.claude` on those nodes):
- for each kind — `skill`, `agent`, `command`, `hook`, `output_style`, `instructions` —
`claude_code_<kind>_list`, `_register`, `_unregister`. A skill is its files (`files`, or `content` for a
lone SKILL.md); a hook is an `event`, a `matcher`, a `command` and its scripts as `files`, with
`${HOOK_DIR}` in the command naming their directory. Hooks and settings take no home scope;
- `claude_code_settings_get`, `_set` (merged into the scope, or `replace`), `_clear`;
`claude_code_permission_add` and `_remove` for one allow, ask or deny rule. The agent refuses to loosen
its own settings: these are the operator's to call;
- `claude_code_config_list`, `_show` (one registration in full), `_status` (what applies here, the plugin
as written, and the home's own items — which the mesh placed, which share a name with a mesh item, which
call a tool server not loaded here) and `_import` (an item of this node's home, registered at a scope;
the original stays).
A new session takes a change; a running one at `/reload-plugins`.
## Settings
Per node or for the whole mesh, through `mesh-controller.settings module=claude-code`:
@@ -69,19 +69,25 @@ func TestTheRendererWritesWhatTheTypeScriptOneWrote(t *testing.T) {
for _, file := range []string{"managed-mcp.json", "managed-settings.json"} {
var a, b any
_ = json.Unmarshal([]byte(got[file]), &a)
// The plugin's two keys are new since the TypeScript (ADR 0216); everything else means the same.
if m, ok := a.(map[string]any); ok && file == "managed-settings.json" {
for k := range MarketplaceKeys() {
delete(m, k)
}
}
_ = json.Unmarshal([]byte(want[file]), &b)
if !reflect.DeepEqual(a, b) {
t.Errorf("%s: %s means something else:\n--- go\n%s\n--- typescript\n%s", label, file, got[file], want[file])
}
}
}
same("with an API key", Render(f.Facts, f.Settings, &Binding{Licence: "api", Kind: "api-key"}, "/state/api-key-helper", f.Registered), f.WithKey)
same("plain", Render(f.Facts, Settings{}, nil, "/h", Servers{}), f.Plain)
same("with an API key", Render(f.Facts, f.Settings, &Binding{Licence: "api", Kind: "api-key"}, "/state/api-key-helper", f.Registered, Config{}), f.WithKey)
same("plain", Render(f.Facts, Settings{}, nil, "/h", Servers{}, Config{}), f.Plain)
}
func TestASettingCannotReplaceTheMeshsOwnEntryAndABadNameIsLeftOut(t *testing.T) {
out := Render(Facts{Node: "w", Console: "http://127.0.0.1:4270/mcp"},
Settings{MCPServers: map[string]map[string]any{"mesh": {"type": "http", "url": "http://evil"}, "bad name": {}}}, nil, "/h", nil)
Settings{MCPServers: map[string]map[string]any{"mesh": {"type": "http", "url": "http://evil"}, "bad name": {}}}, nil, "/h", nil, Config{})
var mcp struct {
MCPServers map[string]map[string]any `json:"mcpServers"`
}
@@ -89,7 +95,7 @@ func TestASettingCannotReplaceTheMeshsOwnEntryAndABadNameIsLeftOut(t *testing.T)
if mcp.MCPServers["mesh"]["url"] != "http://127.0.0.1:4270/mcp" || mcp.MCPServers["bad name"] != nil {
t.Fatalf("%v", mcp.MCPServers)
}
if !reflect.DeepEqual(Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil), Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil)) {
if !reflect.DeepEqual(Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil, Config{}), Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil, Config{})) {
t.Fatal("rendering is not deterministic")
}
}
@@ -104,7 +110,7 @@ func TestTheOperatorsManagedSettingsAreLaidUnderTheMeshsOwnKeys(t *testing.T) {
}}
read := func(binding *Binding) map[string]any {
var m map[string]any
out := Render(Facts{Console: "x"}, settings, binding, "/h", nil)
out := Render(Facts{Console: "x"}, settings, binding, "/h", nil, Config{})
if err := json.Unmarshal([]byte(out["managed-settings.json"]), &m); err != nil {
t.Fatal(err)
}
@@ -0,0 +1,937 @@
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, hook, 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)
// settings the managed settings file, mesh then node, under the mesh's keys
//
// 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"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"time"
)
// 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}$`)
// meshOwnedKeys are the settings a registration may not set: the mesh's own, and those that would deny
// the mesh's console or its marketplace by another way.
var meshOwnedKeys = []string{"attribution", "allowAllClaudeAiMcps", "apiKeyHelper", "extraKnownMarketplaces", "enabledPlugins",
"allowedMcpServers", "deniedMcpServers", "allowManagedMcpServersOnly", "strictKnownMarketplaces", "blockedMarketplaces"}
// hookEvents are the events a hook may be registered for.
var hookEvents = map[string]bool{"PreToolUse": true, "PostToolUse": true, "UserPromptSubmit": true, "Notification": true,
"Stop": true, "SubagentStop": true, "SessionStart": true, "SessionEnd": true, "PreCompact": true}
// 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 !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 == KindSettings {
if it.Scope == ScopeHome {
return "settings take the mesh and node scopes only: the home's settings file is the person's"
}
if it.Name != SettingsName {
return "a scope holds one set of settings, named " + SettingsName
}
if len(it.Settings) == 0 {
return "no settings given"
}
for _, key := range meshOwnedKeys {
if _, ok := it.Settings[key]; ok {
return fmt.Sprintf("%q is one of the mesh's own keys, or would turn off the mesh's console or plugin; a registration cannot set it", key)
}
}
} else if !itemName.MatchString(it.Name) {
return fmt.Sprintf("%q is not a name: lower-case letters, digits and -, at most 64", it.Name)
}
if it.Kind == KindHook {
if it.Scope == ScopeHome {
return "a hook takes the mesh and node scopes only: at home it would live in the person's settings file"
}
if !hookEvents[it.Event] {
return fmt.Sprintf("%q is not a hook event the agent knows", it.Event)
}
if strings.TrimSpace(it.Command) == "" {
return "a hook needs a command"
}
}
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 {
v := &ConfigView{p: p, items: map[string]Item{}}
_ = readJSON(p.config(), &v.items)
return v
}
// 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, _ := os.ReadFile(v.p.config())
if string(before) == string(now) {
return false
}
_ = os.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
}
// 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.
type PluginFile struct {
Content string
Executable bool
}
// 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 {
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} {
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{}
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"]}
case KindHook:
for rel, content := range it.Files {
out[root+"hooks/"+it.Name+"/"+rel] = PluginFile{Content: content, Executable: true}
}
command := strings.ReplaceAll(it.Command, "${HOOK_DIR}", `"${CLAUDE_PLUGIN_ROOT}/hooks/`+it.Name+`"`)
entry := map[string]any{"hooks": []any{map[string]any{"type": "command", "command": command}}}
if it.Matcher != "" {
entry["matcher"] = it.Matcher
}
hooks[it.Event] = append(hooks[it.Event], entry)
}
}
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},
}
}
// RegisteredSettings is the settings registered for the mesh, with those registered for this node laid over.
func RegisteredSettings(c Config) map[string]any {
out := map[string]any{}
for _, layer := range [][]Item{c.Mesh, c.Node} {
for _, it := range layer {
if it.Kind == KindSettings {
out = mergeSettings(out, it.Settings)
}
}
}
return out
}
// 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
for _, part := range []struct {
title string
items []Item
}{{"Instructions for every node", c.Mesh}, {"Instructions for this node", c.Node}} {
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\nRegistered through the `claude-code` module's instruction tools; change them there.\n", part.title)
for _, it := range sections {
fmt.Fprintf(&b, "\n### %s\n\n%s\n", it.Name, strings.TrimSpace(it.Files[it.Name+".md"]))
}
}
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) {
dir := filepath.Join(p.Home, ".claude")
var placed Placed
if !readJSON(p.placed(), &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 := os.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 := os.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 := os.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 := os.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 := os.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 := os.WriteFile(p.placed(), 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 := os.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 := os.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 := os.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 and rule files, 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} {
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 := os.ReadDir(filepath.Join(dir, "skills")); err == nil {
for _, e := range entries {
if !e.IsDir() {
continue
}
body, err := os.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 := os.ReadDir(filepath.Join(dir, sub))
if err != nil {
continue
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
body, _ := os.ReadFile(filepath.Join(dir, sub, e.Name()))
add(kind, strings.TrimSuffix(e.Name(), ".md"), sub+"/"+e.Name(), 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 := filepath.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 := os.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 := os.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)
}
// 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.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 {
for _, n := range nodes {
if n == p.Node {
if conflict := HomeConflict(p, it); conflict != "" {
return map[string]any{"registered": false, "reason": conflict}, nil
}
}
}
}
var keys []string
changed := false
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
changed = v.Take(key, op, &item) || changed
}
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 changed {
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 if v.Applies(keys[0]) || len(keys) > 1 {
answer["here"] = "already so"
} 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
}
@@ -0,0 +1,457 @@
package main
// The agent's configuration registered at three scopes (novox/hq ADR 0216): each test is one row of the
// record's "How it is checked".
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// memConfig is the `config` state as a map, shared by the nodes of a test the way the bus shares it.
type memConfig map[string]json.RawMessage
func (m memConfig) Put(key string, value any) error {
raw, err := json.Marshal(value)
m[key] = raw
return err
}
func (m memConfig) Delete(key string) error { delete(m, key); return nil }
func (m memConfig) Keys() ([]string, error) {
out := []string{}
for k := range m {
out = append(out, k)
}
return out, nil
}
func (m memConfig) Get(key string) (json.RawMessage, bool, error) {
raw, ok := m[key]
return raw, ok, nil
}
// deliver hands every key of the state to a node's view, as its watch would.
func deliver(m memConfig, v *ConfigView) {
for key, raw := range m {
var it Item
_ = json.Unmarshal(raw, &it)
v.Take(key, "put", &it)
}
}
func one(name, content string) map[string]string { return map[string]string{name + ".md": content} }
func pluginOf(t *testing.T, w map[string]string) map[string]PluginFile {
t.Helper()
var files map[string]PluginFile
if err := json.Unmarshal([]byte(w[MarketplaceDir+"/"]), &files); err != nil {
t.Fatalf("no marketplace written: %v", err)
}
return files
}
func TestEachKindLandsInItsOnePlace(t *testing.T) {
p, w := node(t, "laptop")
state, view := memConfig{}, NewConfigView(p)
register := func(it Item) {
t.Helper()
answer, err := Register(p, it, nil, false, state, view, writer(w))
if err != nil || answer["registered"] == false {
t.Fatalf("%s %s: %v %v", it.Kind, it.Name, answer, err)
}
}
register(Item{Kind: KindSkill, Name: "review", Scope: ScopeMesh,
Files: map[string]string{"SKILL.md": "---\nname: review\ndescription: d\n---\nbody", "scripts/run.sh": "#!/bin/sh\necho hi\n"}})
register(Item{Kind: KindAgent, Name: "reviewer", Scope: ScopeMesh, Files: one("reviewer", "---\nname: reviewer\n---\nx")})
register(Item{Kind: KindCommand, Name: "ship", Scope: ScopeMesh, Files: one("ship", "ship it")})
register(Item{Kind: KindOutputStyle, Name: "terse", Scope: ScopeMesh, Files: one("terse", "---\nname: terse\n---\nshort")})
register(Item{Kind: KindHook, Name: "guard", Scope: ScopeMesh, Event: "PreToolUse", Matcher: "Bash",
Command: "${HOOK_DIR}/guard.sh", Files: map[string]string{"guard.sh": "#!/bin/sh\nexit 0\n"}})
register(Item{Kind: KindInstructions, Name: "conventions", Scope: ScopeMesh, Files: one("conventions", "Commit in the imperative.")})
register(Item{Kind: KindSettings, Name: SettingsName, Scope: ScopeMesh,
Settings: map[string]any{"permissions": map[string]any{"deny": []any{"Bash(rm -rf:*)"}}}})
plugin := pluginOf(t, w)
root := "plugins/" + Plugin + "/"
for _, want := range []string{".claude-plugin/marketplace.json", root + ".claude-plugin/plugin.json",
root + "skills/review/SKILL.md", root + "skills/review/scripts/run.sh", root + "agents/reviewer.md",
root + "commands/ship.md", root + "output-styles/terse.md", root + "hooks/hooks.json", root + "hooks/guard/guard.sh"} {
if _, ok := plugin[want]; !ok {
t.Errorf("the plugin lacks %s", want)
}
}
if !plugin[root+"skills/review/scripts/run.sh"].Executable || !plugin[root+"hooks/guard/guard.sh"].Executable {
t.Error("a script is not executable")
}
var hooks struct {
Hooks map[string][]struct {
Matcher string `json:"matcher"`
Hooks []struct {
Command string `json:"command"`
} `json:"hooks"`
} `json:"hooks"`
}
_ = json.Unmarshal([]byte(plugin[root+"hooks/hooks.json"].Content), &hooks)
if pre := hooks.Hooks["PreToolUse"]; len(pre) != 1 || pre[0].Matcher != "Bash" ||
pre[0].Hooks[0].Command != `"${CLAUDE_PLUGIN_ROOT}/hooks/guard"/guard.sh` {
t.Errorf("the hook's command does not name its directory, quoted: %s", plugin[root+"hooks/hooks.json"].Content)
}
if !strings.Contains(w["CLAUDE.md"], "### conventions\n\nCommit in the imperative.") {
t.Errorf("the instruction section is not in the managed instruction file:\n%s", w["CLAUDE.md"])
}
var managed map[string]any
_ = json.Unmarshal([]byte(w["managed-settings.json"]), &managed)
if managed["permissions"] == nil || managed["enabledPlugins"].(map[string]any)[Plugin+"@"+Plugin] != true {
t.Errorf("managed settings: %v", managed)
}
if _, inPlugin := plugin[root+"settings.json"]; inPlugin {
t.Error("settings were put in the plugin, which drops them")
}
}
func TestAMeshItemReachesEveryNodeANodeItemOneAndAHomeItemOneHome(t *testing.T) {
a, wa := node(t, "laptop")
b, wb := node(t, "server")
state := memConfig{}
va, vb := NewConfigView(a), NewConfigView(b)
for _, r := range []struct {
it Item
nodes []string
}{
{Item{Kind: KindCommand, Name: "everywhere", Scope: ScopeMesh, Files: one("everywhere", "x")}, nil},
{Item{Kind: KindCommand, Name: "server-only", Scope: ScopeNode, Files: one("server-only", "x")}, []string{"server"}},
{Item{Kind: KindCommand, Name: "at-home", Scope: ScopeHome, Files: one("at-home", "x")}, []string{"laptop"}},
} {
if _, err := Register(a, r.it, r.nodes, false, state, va, writer(wa)); err != nil {
t.Fatal(err)
}
}
deliver(state, vb)
if _, err := RenderNow(b, writer(wb)); err != nil {
t.Fatal(err)
}
root := "plugins/" + Plugin + "/commands/"
pa, pb := pluginOf(t, wa), pluginOf(t, wb)
if _, ok := pa[root+"everywhere.md"]; !ok {
t.Error("the mesh item is missing on the laptop")
}
if _, ok := pb[root+"everywhere.md"]; !ok {
t.Error("the mesh item is missing on the server")
}
if _, ok := pa[root+"server-only.md"]; ok {
t.Error("the server's item reached the laptop")
}
if _, ok := pb[root+"server-only.md"]; !ok {
t.Error("the server's item is missing on the server")
}
if _, err := os.Stat(filepath.Join(a.Home, ".claude", "commands", "at-home.md")); err != nil {
t.Error("the home item was not placed in the laptop's home")
}
if _, err := os.Stat(filepath.Join(b.Home, ".claude", "commands", "at-home.md")); err == nil {
t.Error("the laptop's home item reached the server's home")
}
if _, ok := pa[root+"at-home.md"]; ok {
t.Error("a home item went into the plugin")
}
}
func TestTheMeshsOwnKeysCannotBeSetOrReplaced(t *testing.T) {
for _, key := range []string{"extraKnownMarketplaces", "enabledPlugins", "attribution", "apiKeyHelper"} {
it := Item{Kind: KindSettings, Name: SettingsName, Scope: ScopeMesh, Settings: map[string]any{key: true}}
if it.Problem() == "" {
t.Errorf("a registration could set %s", key)
}
}
out := Render(Facts{Console: "x"}, Settings{ManagedSettings: map[string]any{"enabledPlugins": map[string]any{Plugin + "@" + Plugin: false}}},
nil, "/h", nil, Config{})
var managed map[string]any
_ = json.Unmarshal([]byte(out["managed-settings.json"]), &managed)
if managed["enabledPlugins"].(map[string]any)[Plugin+"@"+Plugin] != true {
t.Error("the operator's setting turned the mesh's plugin off")
}
}
func TestSettingsLayMeshThenNodeAndJoinTheirLists(t *testing.T) {
c := Config{
Mesh: []Item{{Kind: KindSettings, Scope: ScopeMesh, Settings: map[string]any{
"permissions": map[string]any{"deny": []any{"A"}}, "model": "mesh-model"}}},
Node: []Item{{Kind: KindSettings, Scope: ScopeNode, Settings: map[string]any{
"permissions": map[string]any{"deny": []any{"A", "B"}}, "model": "node-model"}}},
}
out := Render(Facts{Console: "x"}, Settings{ManagedSettings: map[string]any{"permissions": map[string]any{"allow": []any{"C"}}}},
nil, "/h", nil, c)
var managed struct {
Permissions map[string][]string `json:"permissions"`
Model string `json:"model"`
}
_ = json.Unmarshal([]byte(out["managed-settings.json"]), &managed)
if strings.Join(managed.Permissions["deny"], ",") != "A,B" || strings.Join(managed.Permissions["allow"], ",") != "C" || managed.Model != "node-model" {
t.Fatalf("%+v", managed)
}
}
func TestTheHomeScopeOwnsOnlyWhatItPlaced(t *testing.T) {
p, w := node(t, "laptop")
state, view := memConfig{}, NewConfigView(p)
mine := filepath.Join(p.Home, ".claude", "agents", "mine.md")
_ = os.MkdirAll(filepath.Dir(mine), 0o755)
writeFile(t, mine, "the person's own")
answer, _ := Register(p, Item{Kind: KindAgent, Name: "mine", Scope: ScopeHome, Files: one("mine", "the mesh's")}, nil, false, state, view, writer(w))
if answer["registered"] != false {
t.Fatalf("a name the person uses was taken: %v", answer)
}
if raw, _ := os.ReadFile(mine); string(raw) != "the person's own" {
t.Fatal("the person's file was overwritten")
}
placed := filepath.Join(p.Home, ".claude", "agents", "placed.md")
if _, err := Register(p, Item{Kind: KindAgent, Name: "placed", Scope: ScopeHome, Files: one("placed", "v1")}, nil, false, state, view, writer(w)); err != nil {
t.Fatal(err)
}
if raw, _ := os.ReadFile(placed); string(raw) != "v1" {
t.Fatal("the home item was not placed")
}
if _, err := Register(p, Item{Kind: KindAgent, Name: "placed", Scope: ScopeHome}, nil, true, state, view, writer(w)); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(placed); err == nil {
t.Fatal("unregistering did not remove what the mesh placed")
}
if _, err := os.Stat(mine); err != nil {
t.Fatal("unregistering removed the person's file")
}
// Changed by hand after it was placed: left alone when unregistered.
if _, err := Register(p, Item{Kind: KindAgent, Name: "edited", Scope: ScopeHome, Files: one("edited", "v1")}, nil, false, state, view, writer(w)); err != nil {
t.Fatal(err)
}
edited := filepath.Join(p.Home, ".claude", "agents", "edited.md")
writeFile(t, edited, "changed by hand")
if _, err := Register(p, Item{Kind: KindAgent, Name: "edited", Scope: ScopeHome}, nil, true, state, view, writer(w)); err != nil {
t.Fatal(err)
}
if raw, _ := os.ReadFile(edited); string(raw) != "changed by hand" {
t.Fatal("a placed file changed by hand was removed")
}
}
func TestAnItemAboveTheLimitOrMisshapenIsRefused(t *testing.T) {
big := Item{Kind: KindSkill, Name: "big", Scope: ScopeMesh, Files: map[string]string{"SKILL.md": strings.Repeat("x", MaxItemBytes+1)}}
cases := map[string]Item{
"too big": big,
"a bad name": {Kind: KindAgent, Name: "Bad Name", Scope: ScopeMesh, Files: one("Bad Name", "x")},
"a skill without one": {Kind: KindSkill, Name: "s", Scope: ScopeMesh, Files: map[string]string{"other.md": "x"}},
"a path outside": {Kind: KindSkill, Name: "s", Scope: ScopeMesh, Files: map[string]string{"SKILL.md": "x", "../escape": "x"}},
"a hook at home": {Kind: KindHook, Name: "h", Scope: ScopeHome, Event: "Stop", Command: "true"},
"settings at home": {Kind: KindSettings, Name: SettingsName, Scope: ScopeHome, Settings: map[string]any{"model": "x"}},
"an unknown event": {Kind: KindHook, Name: "h", Scope: ScopeMesh, Event: "Whenever", Command: "true"},
}
for label, it := range cases {
if it.Problem() == "" {
t.Errorf("%s was accepted", label)
}
}
}
func TestAHomeItemIsImportedAndTheStaleOnesAreNamed(t *testing.T) {
p, _ := node(t, "laptop")
dir := filepath.Join(p.Home, ".claude")
_ = os.MkdirAll(filepath.Join(dir, "skills", "old", "scripts"), 0o755)
writeFile(t, filepath.Join(dir, "skills", "old", "SKILL.md"), "---\nname: old\n---\nuse mcp__gone__do_it")
writeFile(t, filepath.Join(dir, "skills", "old", "scripts", "a.sh"), "#!/bin/sh\n")
it, err := ImportFromHome(p, KindSkill, "old")
if err != nil || len(it.Files) != 2 || it.Files["scripts/a.sh"] == "" {
t.Fatalf("%+v %v", it, err)
}
items := HomeItems(p, Config{Mesh: []Item{{Kind: KindSkill, Name: "old"}}}, Servers{})
if len(items) != 1 || len(items[0].Notes) != 2 {
t.Fatalf("%+v", items)
}
}
// The manifest lists exactly the tools the bundle serves: a tool missing from it is never announced.
func TestTheManifestListsEveryToolServed(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Tools []string `json:"tools"`
}
_ = json.Unmarshal(raw, &m)
listed := map[string]bool{}
for _, n := range m.Tools {
listed[n] = true
}
p, _ := node(t, "laptop")
served := tools(p, nil, NewServerView(p), memConfig{}, NewConfigView(p))
for _, tool := range served {
if !listed[tool.Name] {
t.Errorf("%s is served and not in the manifest", tool.Name)
}
delete(listed, tool.Name)
}
for n := range listed {
t.Errorf("%s is in the manifest and not served", n)
}
}
// The tree writer's comparison: a directory holding exactly the files given, executable bits included.
func TestATreeIsTheSameOnlyWhenEveryFileAndModeIs(t *testing.T) {
dir := t.TempDir()
files := map[string]PluginFile{"a.md": {Content: "a"}, "s/run.sh": {Content: "#!/bin/sh\n", Executable: true}}
_ = os.MkdirAll(filepath.Join(dir, "s"), 0o755)
writeFile(t, filepath.Join(dir, "a.md"), "a")
writeFile(t, filepath.Join(dir, "s", "run.sh"), "#!/bin/sh\n")
if sameTree(dir, files) {
t.Fatal("a script without its executable bit counted as the same")
}
_ = os.Chmod(filepath.Join(dir, "s", "run.sh"), 0o755)
if !sameTree(dir, files) {
t.Fatal("the same tree counted as different")
}
writeFile(t, filepath.Join(dir, "extra.md"), "x")
if sameTree(dir, files) {
t.Fatal("an extra file counted as the same")
}
}
// A registration removed while a node was away is dropped when it is back: the watch hands over only what is
// there, so the view is pruned to the keys the state still holds.
func TestWhatWasRemovedWhileANodeWasAwayIsDropped(t *testing.T) {
p, w := node(t, "laptop")
state, view := memConfig{}, NewConfigView(p)
for _, name := range []string{"kept", "gone"} {
if _, err := Register(p, Item{Kind: KindCommand, Name: name, Scope: ScopeMesh, Files: one(name, "x")}, nil, false, state, view, writer(w)); err != nil {
t.Fatal(err)
}
}
delete(state, "mesh.command.gone") // removed from another node while this one was off
back := NewConfigView(p) // the node starts again from what it last wrote
if len(back.Items()) != 2 {
t.Fatalf("the view did not start from what was last written: %v", back.Items())
}
keys, _ := state.Keys()
if !back.Prune(keys) || len(back.Items()) != 1 {
t.Fatalf("after pruning: %v", back.Items())
}
}
// The review's findings, each held by a test.
func TestAPathOrAKeyThatIsNotWhatItSaysIsRefused(t *testing.T) {
for label, files := range map[string]map[string]string{
"a dot": {"SKILL.md": "x", ".": "x"},
"an empty segment": {"SKILL.md": "x", "a//b": "x"},
"a parent segment": {"SKILL.md": "x", "a/../b": "x"},
"a file and directory": {"SKILL.md": "x", "a": "x", "a/b": "x"},
} {
if (Item{Kind: KindSkill, Name: "s", Scope: ScopeMesh, Files: files}).Problem() == "" {
t.Errorf("%s was accepted", label)
}
}
p, _ := node(t, "laptop")
v := NewConfigView(p)
home := Item{Kind: KindCommand, Name: "x", Scope: ScopeHome, Files: one("x", "y")}
if v.Take("mesh.command.x", "put", &home); len(v.Items()) != 0 {
t.Fatal("a mesh key holding a home item was taken")
}
for _, key := range []string{"mesh.command.x", "mesh.agent.x"} {
mesh := Item{Kind: KindCommand, Name: "x", Scope: ScopeMesh, Files: one("x", "y")}
v.Take(key, "put", &mesh)
}
if len(v.Items()) != 1 {
t.Fatalf("an item under a key of another kind was taken: %v", v.Items())
}
}
func TestAllIsEveryNodeAndANameThatCannotBeAKeyIsRefused(t *testing.T) {
nodes, err := ExpandNodes([]string{"all"}, func() ([]string, error) { return []string{"ace", "g14"}, nil })
if err != nil || strings.Join(nodes, ",") != "ace,g14" {
t.Fatalf("%v %v", nodes, err)
}
p, w := node(t, "laptop")
answer, _ := Register(p, Item{Kind: KindCommand, Name: "c", Scope: ScopeNode, Files: one("c", "x")}, []string{"a.b"}, false, memConfig{}, NewConfigView(p), writer(w))
if answer["registered"] != false {
t.Fatalf("a node name with a dot was used in a key: %v", answer)
}
if (Item{Kind: KindSettings, Name: SettingsName, Scope: ScopeMesh, Settings: map[string]any{"deniedMcpServers": []any{}}}).Problem() == "" {
t.Fatal("a registration could deny the mesh's console")
}
}
func TestTheOperatorsOwnPluginsAndMarketplacesAreKept(t *testing.T) {
out := Render(Facts{Console: "x"}, Settings{ManagedSettings: map[string]any{
"enabledPlugins": map[string]any{"theirs@market": true},
"extraKnownMarketplaces": map[string]any{"market": map[string]any{"source": map[string]any{"source": "github", "repo": "o/r"}}},
}}, nil, "/h", nil, Config{})
var managed struct {
Enabled map[string]any `json:"enabledPlugins"`
Known map[string]any `json:"extraKnownMarketplaces"`
}
_ = json.Unmarshal([]byte(out["managed-settings.json"]), &managed)
if managed.Enabled["theirs@market"] != true || managed.Enabled[Plugin+"@"+Plugin] != true || managed.Known["market"] == nil || managed.Known[Plugin] == nil {
t.Fatalf("%+v", managed)
}
}
func TestTheHomeLeavesThePersonsChoicesAlone(t *testing.T) {
p, _ := node(t, "laptop")
dir := filepath.Join(p.Home, ".claude")
// An identical file the person made is not taken over, so unregistering never removes it.
_ = os.MkdirAll(filepath.Join(dir, "agents"), 0o755)
writeFile(t, filepath.Join(dir, "agents", "same.md"), "x")
if _, left := PlaceHome(p, map[string]string{"agents/same.md": "x"}); len(left) != 1 {
t.Fatalf("an identical file of the person's was taken over: %v", left)
}
PlaceHome(p, map[string]string{})
if _, err := os.Stat(filepath.Join(dir, "agents", "same.md")); err != nil {
t.Fatal("the person's file was removed")
}
// A placed file the person deleted stays deleted while it is registered.
PlaceHome(p, map[string]string{"commands/c.md": "x"})
_ = os.Remove(filepath.Join(dir, "commands", "c.md"))
PlaceHome(p, map[string]string{"commands/c.md": "x"})
if _, err := os.Stat(filepath.Join(dir, "commands", "c.md")); err == nil {
t.Fatal("a file the person deleted was placed again")
}
// The kind's own directory stays when the mesh's last item in it goes.
PlaceHome(p, map[string]string{"skills/s/SKILL.md": "x"})
PlaceHome(p, map[string]string{})
if _, err := os.Stat(filepath.Join(dir, "skills", "s")); err == nil {
t.Fatal("the item's own directory was left")
}
if _, err := os.Stat(filepath.Join(dir, "skills")); err != nil {
t.Fatal("the kind's directory was removed")
}
// Never through a symbolic link.
elsewhere := t.TempDir()
if err := os.Symlink(elsewhere, filepath.Join(dir, "output-styles")); err != nil {
t.Skip("no symbolic links here")
}
PlaceHome(p, map[string]string{"output-styles/o.md": "x"})
if _, err := os.Stat(filepath.Join(elsewhere, "o.md")); err == nil {
t.Fatal("a file was written through a symbolic link")
}
}
func TestOneFileThatCannotBeWrittenDoesNotStopTheOthers(t *testing.T) {
p, _ := node(t, "laptop")
w := map[string]string{}
failing := func(name, content string) (string, error) {
if name == MarketplaceDir+"/" {
return "", os.ErrPermission
}
w[name] = content
return name + ": written", nil
}
if _, err := RenderNow(p, failing); err == nil {
t.Fatal("the failure was not reported")
}
if w["managed-settings.json"] == "" || w["managed-mcp.json"] == "" || w["CLAUDE.md"] == "" {
t.Fatalf("the other files were not written: %v", w)
}
}
@@ -0,0 +1,353 @@
package main
// The tools that register the agent's configuration (novox/hq ADR 0216): for each kind a list, a register and an
// unregister; for settings, a read, a set, a clear and the permission rules; and the status and import tools
// for what the module did not place.
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// kindTool is how one kind is named in its tools and described to whoever calls them.
type kindTool struct {
kind, tool, what, lands string
}
var kindTools = []kindTool{
{KindSkill, "skill", "a skill: a folder with a SKILL.md (front matter `name` and `description`) and any files beside it",
"the plugin's skills/<name>/ (home: ~/.claude/skills/<name>/)"},
{KindAgent, "agent", "a subagent: one markdown file with front matter (`name`, `description`, optionally `tools`, `model`)",
"the plugin's agents/<name>.md (home: ~/.claude/agents/<name>.md)"},
{KindCommand, "command", "a slash command: one markdown file, its front matter optional (`description`, `argument-hint`, `allowed-tools`)",
"the plugin's commands/<name>.md, run as /" + Plugin + ":<name> (home: ~/.claude/commands/<name>.md, run as /<name>)"},
{KindHook, "hook", "a hook: an event, an optional matcher, a command, and optionally the scripts it runs — ${HOOK_DIR} in the command is their directory",
"the plugin's hooks/hooks.json, its scripts in hooks/<name>/ (mesh and node scopes only)"},
{KindOutputStyle, "output_style", "an output style: one markdown file with front matter (`name`, `description`)",
"the plugin's output-styles/<name>.md (home: ~/.claude/output-styles/<name>.md)"},
{KindInstructions, "instructions", "a section of instructions every session reads",
"a section of the managed instruction file, after the mesh's own text (home: a rule file, ~/.claude/rules/<name>.md)"},
}
func boolArg(a map[string]any, k string) bool { b, _ := a[k].(bool); return b }
func strArg(a map[string]any, k string) string {
s, _ := a[k].(string)
return strings.TrimSpace(s)
}
// targetNodes reads the `nodes` argument: absent is this node, "all" every node running claude-code, else a list.
func targetNodes(a map[string]any) ([]string, error) {
return ExpandNodes(nodesOf(a["nodes"]), nodesRunningMe)
}
// scopeOf reads the `scope` argument; absent is the mesh, which is what a registration is most often for.
func scopeOf(a map[string]any) string {
if s := strArg(a, "scope"); s != "" {
return s
}
return ScopeMesh
}
// filesOf reads an item's files: `content` for a one-file kind, or `files`, a map of path to content.
func filesOf(kind, name string, a map[string]any) (map[string]string, error) {
out := map[string]string{}
if raw, ok := a["files"].(map[string]any); ok {
for rel, v := range raw {
s, ok := v.(string)
if !ok {
return nil, fmt.Errorf("files.%s is not text", rel)
}
out[rel] = s
}
}
if content, ok := a["content"].(string); ok && content != "" {
switch kind {
case KindSkill:
out["SKILL.md"] = content
case KindHook:
return nil, errors.New("a hook's scripts are given as files")
default:
out[name+".md"] = content
}
}
return out, nil
}
func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
scopeArg := str(`mesh (every node; the default), node (the nodes given, or this one) or home (the operator account's own ~/.claude on the nodes given, or this one)`)
nodesArg := str(`for the node and home scopes: "all" for every node running claude-code, or a comma-separated list; absent is this node`)
var out []stdio.Tool
for _, k := range kindTools {
k := k
input := map[string]any{
"name": str("the name: lower-case letters, digits and -"),
"scope": scopeArg,
"nodes": nodesArg,
}
switch k.kind {
case KindSkill:
input["content"] = str("the SKILL.md, when the skill is that one file")
input["files"] = map[string]any{"type": "object", "description": "the skill's files by path inside it, SKILL.md among them, e.g. {\"SKILL.md\": \"...\", \"scripts/run.sh\": \"#!/bin/sh ...\"}"}
case KindHook:
input["event"] = str("PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, SessionStart, SessionEnd or PreCompact")
input["matcher"] = str("for tool events, which tools, e.g. Bash or Edit|Write; absent is every one")
input["command"] = str("the shell command; ${HOOK_DIR} is the directory of the files given, e.g. ${HOOK_DIR}/check.sh")
input["files"] = map[string]any{"type": "object", "description": "the scripts the command runs, by path, e.g. {\"check.sh\": \"#!/bin/sh ...\"}"}
default:
input["content"] = str("the file's content, front matter included")
}
out = append(out,
stdio.Tool{Name: "claude_code_" + k.tool + "_list",
Description: "Every " + k.kind + " registered for Claude Code on the mesh, by key (`mesh.…` every node, `node.<node>.…` one node, `home.<node>.…` an account's own directory), with who registered it and its files. Lands in " + k.lands + ".",
Run: func(map[string]any) (any, error) { return List(state, k.kind) }},
stdio.Tool{Name: "claude_code_" + k.tool + "_register",
Description: "Register " + k.what + ", for every node (scope mesh, the default), some nodes (node) or an account's own directory (home). Kept on the bus: a node that joins later takes it too. Lands in " + k.lands + "; a new session takes it, a running one at /reload-plugins. Refused above 256 KiB, or at home where the person already has one of that name.",
Input: input,
Run: func(a map[string]any) (any, error) {
name := strArg(a, "name")
files, err := filesOf(k.kind, name, a)
if err != nil {
return nil, err
}
it := Item{Kind: k.kind, Name: name, Scope: scopeOf(a), Files: files,
Event: strArg(a, "event"), Matcher: strArg(a, "matcher"), Command: strArg(a, "command")}
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return Register(p, it, nodes, false, state, view, writeManaged)
}},
stdio.Tool{Name: "claude_code_" + k.tool + "_unregister",
Description: "Remove a " + k.kind + " registered through this module, at its scope. At home, only what the mesh placed is removed, and not if it was changed by hand since.",
Input: map[string]any{"name": str("the name"), "scope": scopeArg, "nodes": nodesArg},
Run: func(a map[string]any) (any, error) {
it := Item{Kind: k.kind, Name: strArg(a, "name"), Scope: scopeOf(a)}
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return Register(p, it, nodes, true, state, view, writeManaged)
}},
)
}
settingsScope := str(`mesh (every node; the default) or node (the nodes given, or this one)`)
out = append(out,
stdio.Tool{Name: "claude_code_settings_get",
Description: "Claude Code's managed settings on this node as written, and where each part came from: the operator's `managed_settings` setting (ADR 0213), the settings registered for the mesh and for this node, and the mesh's own keys, which always win.",
Run: func(map[string]any) (any, error) {
written := map[string]any{}
_ = readJSON(filepath.Join(ManagedDir, "managed-settings.json"), &written)
var settings Settings
_ = readJSON(p.Settings, &settings)
c := ConfigOf(view.Items())
layers := map[string]any{"managed_settings setting": settings.ManagedSettings}
for _, it := range c.Mesh {
if it.Kind == KindSettings {
layers["registered for the mesh"] = it.Settings
}
}
for _, it := range c.Node {
if it.Kind == KindSettings {
layers["registered for this node"] = it.Settings
}
}
return map[string]any{"written": written, "layers": layers,
"order": "managed_settings setting, then mesh, then node — objects merged, lists joined — then the mesh's own keys"}, nil
}},
stdio.Tool{Name: "claude_code_settings_set",
Description: "Set Claude Code settings (the vendor's settings keys: permissions, autoMode, env, model, hooks, statusLine, …) for every node or some. Merged into what that scope holds — objects key by key, lists joined — unless replace is set. The mesh's own keys (attribution, the connectors key, the key-helper, the plugin's marketplace) cannot be set. The agent refuses to loosen its own settings: this is the operator's act.",
Input: map[string]any{
"settings": map[string]any{"type": "object", "description": "settings keys, e.g. {\"permissions\": {\"deny\": [\"Bash(rm -rf:*)\"]}}"},
"replace": map[string]any{"type": "boolean", "description": "replace what the scope holds instead of merging into it"},
"scope": settingsScope, "nodes": nodesArg,
},
Run: func(a map[string]any) (any, error) {
given, _ := a["settings"].(map[string]any)
if len(given) == 0 {
return nil, errors.New("no settings given; to remove a scope's settings, claude_code_settings_clear")
}
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return SetSettings(p, scopeOf(a), nodes, func(held map[string]any) map[string]any {
if boolArg(a, "replace") {
return given
}
return mergeSettings(held, given)
}, state, view)
}},
stdio.Tool{Name: "claude_code_settings_clear",
Description: "Remove the Claude Code settings registered at a scope.",
Input: map[string]any{"scope": settingsScope, "nodes": nodesArg},
Run: func(a map[string]any) (any, error) {
it := Item{Kind: KindSettings, Name: SettingsName, Scope: scopeOf(a)}
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return Register(p, it, nodes, true, state, view, writeManaged)
}},
stdio.Tool{Name: "claude_code_permission_add",
Description: "Add a permission rule to Claude Code's managed settings, for every node or some: allow (runs without asking), ask (always asks) or deny (never runs). A rule is a tool and an optional specifier, e.g. Bash(git status:*), Read(./secrets/**), mcp__mesh__mesh_call.",
Input: map[string]any{"list": str("allow, ask or deny"), "rule": str("the rule"), "scope": settingsScope, "nodes": nodesArg},
Run: func(a map[string]any) (any, error) {
list, rule := strArg(a, "list"), strArg(a, "rule")
if (list != "allow" && list != "ask" && list != "deny") || rule == "" {
return nil, errors.New("list is allow, ask or deny, and a rule is needed")
}
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return SetSettings(p, scopeOf(a), nodes, func(held map[string]any) map[string]any {
return mergeSettings(held, map[string]any{"permissions": map[string]any{list: []any{rule}}})
}, state, view)
}},
stdio.Tool{Name: "claude_code_permission_remove",
Description: "Remove a permission rule added through this module, at its scope.",
Input: map[string]any{"list": str("allow, ask or deny"), "rule": str("the rule"), "scope": settingsScope, "nodes": nodesArg},
Run: func(a map[string]any) (any, error) {
list, rule := strArg(a, "list"), strArg(a, "rule")
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return SetSettings(p, scopeOf(a), nodes, func(held map[string]any) map[string]any {
perms, _ := held["permissions"].(map[string]any)
rules, _ := perms[list].([]any)
if len(rules) == 0 {
return held // nothing to remove: what the scope holds stays as it is
}
kept := []any{}
for _, r := range rules {
if r != rule {
kept = append(kept, r)
}
}
next := mergeSettings(map[string]any{}, held)
np := mergeSettings(map[string]any{}, perms)
np[list] = kept
next["permissions"] = np
return next
}, state, view)
}},
stdio.Tool{Name: "claude_code_config_list",
Description: "Everything registered for Claude Code on the mesh through this module — skills, subagents, commands, hooks, output styles, instruction sections, settings — by key, with who registered each and its files.",
Run: func(map[string]any) (any, error) { return List(state, "") }},
stdio.Tool{Name: "claude_code_config_show",
Description: "One registration in full, its files' content included, by its key as a list shows it (e.g. mesh.skill.review).",
Input: map[string]any{"key": str("the key")},
Run: func(a map[string]any) (any, error) { return Show(state, strArg(a, "key")) }},
stdio.Tool{Name: "claude_code_config_status",
Description: "Claude Code's configuration on this node: what was registered and applies here (mesh, node, home), the plugin as written, and the home's own skills, subagents, commands, output styles and rule files — which the mesh placed, which share a name with a mesh item, and which call tools of a tool server not loaded here (stale).",
Run: func(map[string]any) (any, error) {
c := ConfigOf(view.Items())
names := func(items []Item) []string {
out := []string{}
for _, it := range items {
out = append(out, it.Kind+" "+it.Name)
}
return out
}
var mcp struct {
MCPServers Servers `json:"mcpServers"`
}
_ = readJSON(filepath.Join(ManagedDir, "managed-mcp.json"), &mcp)
var placed Placed
_ = readJSON(p.placed(), &placed)
plugin := []string{}
root := filepath.Join(ManagedDir, MarketplaceDir, "plugins", Plugin)
_ = filepath.WalkDir(root, func(full string, d os.DirEntry, err error) error {
if err == nil && !d.IsDir() {
rel, _ := filepath.Rel(root, full)
plugin = append(plugin, rel)
}
return nil
})
return map[string]any{
"applies here": map[string]any{"mesh": names(c.Mesh), "node": names(c.Node), "home": names(c.Home)},
"plugin": map[string]any{"name": Plugin, "files": plugin},
"home": HomeItems(p, c, mcp.MCPServers),
"placed": placed,
}, nil
}},
stdio.Tool{Name: "claude_code_config_import",
Description: "Register an item found in this node's own ~/.claude — a skill, subagent, command, output style, or a rule file as instructions — at the scope given, so something written by hand on one machine reaches every node, some, or stays at home under the mesh's care. The original is left where it is: removing it is the person's act.",
Input: map[string]any{
"kind": str("skill, agent, command, output-style or instructions (a rule file)"),
"name": str("its name in the home: the skill's folder, or the file without .md"),
"scope": scopeArg, "nodes": nodesArg,
},
Run: func(a map[string]any) (any, error) {
it, err := ImportFromHome(p, strArg(a, "kind"), strArg(a, "name"))
if err != nil {
return nil, err
}
it.Scope = scopeOf(a)
if it.Scope == ScopeHome {
return nil, errors.New("it is already at home; import it to the mesh or node scope")
}
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return Register(p, it, nodes, false, state, view, writeManaged)
}},
)
return out
}
// SetSettings changes the settings registered at a scope, one key per node, by what change makes of what
// the key holds. An empty result removes the key.
func SetSettings(p Paths, scope string, nodes []string, change func(held map[string]any) map[string]any,
state ConfigState, view *ConfigView) (map[string]any, error) {
if scope != ScopeMesh && scope != ScopeNode {
return map[string]any{"set": false, "reason": "settings take the mesh and node scopes only"}, nil
}
if scope == ScopeMesh {
nodes = []string{""}
} else if len(nodes) == 0 {
nodes = []string{p.Node}
} else if problem := nodesProblem(nodes); problem != "" {
return map[string]any{"set": false, "reason": problem}, nil
}
answers := map[string]any{}
for _, n := range nodes {
it := Item{Kind: KindSettings, Name: SettingsName, Scope: scope}
held := map[string]any{}
if raw, found, err := state.Get(it.Key(n)); err != nil {
return nil, err
} else if found {
var was Item
if json.Unmarshal(raw, &was) == nil && was.Settings != nil {
held = was.Settings
}
}
it.Settings = change(held)
target := []string(nil)
if n != "" {
target = []string{n}
}
var answer map[string]any
var err error
if len(it.Settings) == 0 {
answer, err = Register(p, it, target, true, state, view, writeManaged)
} else {
answer, err = Register(p, it, target, false, state, view, writeManaged)
}
if err != nil {
return nil, err
}
answer["settings"] = it.Settings
answers[it.Key(n)] = answer
}
return answers, nil
}
+122 -5
View File
@@ -30,6 +30,9 @@ func say(format string, args ...any) {
// writeManaged writes one managed file as root, only when its content changed. From a staged file, never
// /dev/stdin: a child's input may be a socket, which /dev/stdin cannot open (found on the first assignment).
func writeManaged(name, content string) (string, error) {
if strings.HasSuffix(name, "/") {
return writeManagedTree(strings.TrimSuffix(name, "/"), content)
}
path := filepath.Join(ManagedDir, name)
if was, err := os.ReadFile(path); err == nil && string(was) == content {
return name + ": unchanged", nil
@@ -54,6 +57,74 @@ func writeManaged(name, content string) (string, error) {
return name + ": written", nil
}
// writeManagedTree replaces one directory of the managed directory whole — the plugin's marketplace (ADR
// 0216) — when what it holds differs from the files given (a JSON map of path to PluginFile). Staged
// beside, then swapped in as root, so a session never reads half of it.
func writeManagedTree(name, content string) (string, error) {
var files map[string]PluginFile
if err := json.Unmarshal([]byte(content), &files); err != nil {
return "", err
}
target := filepath.Join(ManagedDir, name)
if sameTree(target, files) {
return name + "/: unchanged", nil
}
staged, err := os.MkdirTemp("", "claude-code-tree-")
if err != nil {
return "", err
}
defer os.RemoveAll(staged)
for rel, f := range files {
full := filepath.Join(staged, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return "", err
}
mode := os.FileMode(0o644)
if f.Executable {
mode = 0o755
}
if err := os.WriteFile(full, []byte(f.Content), mode); err != nil {
return "", err
}
_ = os.Chmod(full, mode)
}
_ = os.Chmod(staged, 0o755)
script := `set -e; rm -rf "$2.next" "$2.old"; cp -r "$1" "$2.next"; chown -R root:root "$2.next"; chmod -R go-w,a+rX "$2.next";
if [ -d "$2" ]; then mv "$2" "$2.old"; fi; mv "$2.next" "$2"; rm -rf "$2.old"`
args := []string{"sh", "-c", script, "sh", staged, target}
if os.Geteuid() != 0 {
args = append([]string{"sudo", "-n"}, args...)
}
if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
return "", fmt.Errorf("%s/: could not be written to %s (%s); the module writes there through the operator account's passwordless sudo",
name, ManagedDir, strings.TrimSpace(string(out)))
}
return fmt.Sprintf("%s/: written, %d file(s)", name, len(files)), nil
}
// sameTree says whether a directory holds exactly these files, with these contents and executable bits.
func sameTree(dir string, files map[string]PluginFile) bool {
found := 0
err := filepath.WalkDir(dir, func(full string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
rel, _ := filepath.Rel(dir, full)
f, ok := files[filepath.ToSlash(rel)]
if !ok {
return errors.New("not wanted")
}
raw, err := os.ReadFile(full)
info, ierr := d.Info()
if err != nil || ierr != nil || string(raw) != f.Content || (info.Mode()&0o111 != 0) != f.Executable {
return errors.New("differs")
}
found++
return nil
})
return err == nil && found == len(files)
}
// ask is a tool on the bus, through the runtime: its answer is the tool's value.
func ask(address string, args any) (json.RawMessage, error) { return stdio.Ask(address, args) }
@@ -63,6 +134,13 @@ type stateOf struct{ s stdio.KeptState }
func (s stateOf) Put(key string, value any) error { _, err := s.s.Put(key, value); return err }
func (s stateOf) Delete(key string) error { return s.s.Delete(key) }
func (s stateOf) Keys() ([]string, error) { return s.s.Keys() }
func (s stateOf) Get(key string) (json.RawMessage, bool, error) {
e, err := s.s.Get(key)
if err != nil || e == nil {
return nil, false, err
}
return e.Value, true, nil
}
// nodesRunningMe is the nodes claude-code runs on, from the controller's list of modules — for the register
// tool's question.
@@ -152,9 +230,9 @@ func nodesOf(v any) []string {
return out
}
func tools(p Paths, servers ServerState, view *ServerView) []stdio.Tool {
func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, configView *ConfigView) []stdio.Tool {
nodesArg := str(`more nodes: "all" for every node running claude-code, or a comma-separated list; absent is this node only`)
return []stdio.Tool{
out := []stdio.Tool{
{Name: "claude_code_status",
Description: "Claude Code on this machine as the mesh configured it: the licence it holds and when its token expires, what it reports holding, the managed files, the MCP servers registered here. Fingerprints only, never a token.",
Run: func(map[string]any) (any, error) { return status(p), nil }},
@@ -239,6 +317,7 @@ func tools(p Paths, servers ServerState, view *ServerView) []stdio.Tool {
return RegisterServer(p, Registration{Name: name, Nodes: nodesOf(a["nodes"])}, servers, view, writeManaged, nodesRunningMe)
}},
}
return append(out, configTools(p, config, configView)...)
}
// persist asks the state again until it answers: its bucket or the bus's grant may arrive after the module.
@@ -283,15 +362,53 @@ func main() {
}
servers := stateOf{stdio.State("servers")}
view := NewServerView(p)
go run(p, view)
if err := stdio.Serve("", tools(p, servers, view)); err != nil {
config := stateOf{stdio.State("config")}
configView := NewConfigView(p)
go run(p, view, configView)
if err := stdio.Serve("", tools(p, servers, view, config, configView)); err != nil {
say("%v", err)
os.Exit(1)
}
}
// run is the module's long-running half, beside the tools (ADR 0198).
func run(p Paths, view *ServerView) {
func run(p Paths, view *ServerView, configView *ConfigView) {
// The agent's configuration, at every scope (ADR 0216): the whole current set first, then each change.
go persist("watching the agent's configuration", func() error {
keys, err := stdio.State("config").Keys()
if err != nil {
return err
}
if configView.Prune(keys) {
if _, err := RenderNow(p, writeManaged); err != nil {
say("rendering after what was removed while away: %v", err)
}
}
return stdio.State("config").Watch("", func(c stdio.StateChange) error {
var item *Item
if c.Op == "put" {
item = &Item{}
if json.Unmarshal(c.Value, item) != nil {
item = nil
}
}
if !configView.Take(c.Key, c.Op, item) {
return nil
}
if out, err := RenderNow(p, writeManaged); err != nil {
say("taking %s %s: %v", c.Op, c.Key, err)
} else {
say("took %s %s", c.Op, c.Key)
for _, line := range out {
if !strings.HasSuffix(line, "unchanged") {
say("%s", line)
}
}
}
return nil
})
}, func(n int) { say("watching the agent's configuration%s", refusals(n)) })
// Every node's MCP servers: the whole current set first, then each change (ADR 0201).
go persist("watching the MCP servers", func() error {
return stdio.State("servers").Watch("", func(c stdio.StateChange) error {
+35 -4
View File
@@ -62,6 +62,8 @@ func (p Paths) binding() string { return filepath.Join(p.State, "licence.jso
func (p Paths) apiKey() string { return filepath.Join(p.State, "api-key") }
func (p Paths) helper() string { return filepath.Join(p.State, "api-key-helper") }
func (p Paths) registry() string { return filepath.Join(p.State, "mcp-servers.json") }
func (p Paths) config() string { return filepath.Join(p.State, "config.json") }
func (p Paths) placed() string { return filepath.Join(p.State, "home-placed.json") }
// Ask is a tool on the bus: its address and arguments in, its JSON answer out.
type Ask func(address string, args any) (json.RawMessage, error)
@@ -102,9 +104,15 @@ func Registered(p Paths) Servers {
return s
}
var renderMu sync.Mutex
// RenderNow writes the managed directory from the facts, the settings, the licence held and the servers
// registered here.
func RenderNow(p Paths, write WriteManaged) ([]string, error) {
// One at a time: the watches and the tools all render, and the home's record of what was placed is
// read and written whole.
renderMu.Lock()
defer renderMu.Unlock()
var facts Facts
if !readJSON(p.Facts, &facts) || facts.Console == "" {
return nil, fmt.Errorf("the mesh has not rendered %s yet; nothing to write", p.Facts)
@@ -116,21 +124,44 @@ func RenderNow(p Paths, write WriteManaged) ([]string, error) {
if readJSON(p.binding(), &b) {
binding = &b
}
files := Render(facts, settings, binding, p.helper(), Registered(p))
var items map[string]Item
_ = readJSON(p.config(), &items)
config := ConfigOf(items)
files := Render(facts, settings, binding, p.helper(), Registered(p), config)
tree, _ := json.Marshal(Marketplace(config))
files[MarketplaceDir+"/"] = string(tree)
names := make([]string, 0, len(files))
for n := range files {
names = append(names, n)
}
sort.Strings(names)
// The marketplace first: the settings that enable its plugin must never name one that is not there yet.
sort.Slice(names, func(i, j int) bool {
if (names[i] == MarketplaceDir+"/") != (names[j] == MarketplaceDir+"/") {
return names[i] == MarketplaceDir+"/"
}
return names[i] < names[j]
})
// Every file is attempted: one that cannot be written — a registration the vendor's layout refuses, a
// failed escalation — must not keep the licence, the tool servers or the instructions from landing.
var out []string
var failed []error
for _, n := range names {
line, err := write(n, files[n])
if err != nil {
return out, err
failed = append(failed, err)
continue
}
out = append(out, line)
}
return out, nil
// The home scope: what this module places in the account's own agent directory (ADR 0182).
done, left := PlaceHome(p, HomeFiles(config))
for _, line := range done {
out = append(out, "home "+line)
}
for _, line := range left {
out = append(out, "home "+line)
}
return out, errors.Join(failed...)
}
// ---- the licence ----------------------------------------------------------------------------------
+22 -7
View File
@@ -96,8 +96,10 @@ func jsonFile(v any) string {
}
// Render composes the three files. registered — what was registered through this module and applies
// here — is laid over the servers the operator set in its settings.
func Render(facts Facts, settings Settings, binding *Binding, helperPath string, registered Servers) map[string]string {
// here — is laid over the servers the operator set in its settings; config is the rest of what was
// registered (ADR 0216): its settings laid over the operator's `managed_settings`, its instruction sections
// after the mesh's own text. The plugin itself is Marketplace's, and the home's PlaceHome's.
func Render(facts Facts, settings Settings, binding *Binding, helperPath string, registered Servers, config Config) map[string]string {
servers := map[string]any{}
for _, layer := range []map[string]map[string]any{settings.MCPServers, registered} {
for name, entry := range layer {
@@ -109,14 +111,27 @@ func Render(facts Facts, settings Settings, binding *Binding, helperPath string,
}
servers[meshEntry] = map[string]any{"type": "http", "url": facts.Console}
managed := map[string]any{}
for key, value := range settings.ManagedSettings {
managed[key] = value
}
// The operator's `managed_settings` (ADR 0213), then what was registered for the mesh, then for this node.
managed := mergeSettings(map[string]any{}, settings.ManagedSettings)
managed = mergeSettings(managed, RegisteredSettings(config))
// The mesh's own keys are laid last: a setting never replaces them.
managed["attribution"] = map[string]any{"commit": "", "pr": ""}
managed["allowAllClaudeAiMcps"] = true
delete(managed, "apiKeyHelper")
// The plugin's marketplace and the plugin itself, as entries in the operator's own maps, the mesh's
// entry winning: the operator may know more marketplaces and enable more plugins.
for key, value := range MarketplaceKeys() {
entries := map[string]any{}
if held, ok := managed[key].(map[string]any); ok {
for k, v := range held {
entries[k] = v
}
}
for k, v := range value.(map[string]any) {
entries[k] = v
}
managed[key] = entries
}
if binding != nil && binding.Kind == "api-key" {
managed["apiKeyHelper"] = helperPath
}
@@ -127,6 +142,6 @@ func Render(facts Facts, settings Settings, binding *Binding, helperPath string,
return map[string]string{
"managed-mcp.json": jsonFile(map[string]any{"mcpServers": servers}),
"managed-settings.json": jsonFile(managed),
"CLAUDE.md": instructionsText(facts.Node, role),
"CLAUDE.md": instructionsText(facts.Node, role) + InstructionSections(config),
}
}
+30 -2
View File
@@ -13,7 +13,8 @@
},
"state": [
"servers",
"holdings"
"holdings",
"config"
],
"reads": [
"claude-licence-manager.bindings"
@@ -26,7 +27,34 @@
"claude_code_add_api_key",
"claude_code_mcp_list",
"claude_code_mcp_register",
"claude_code_mcp_unregister"
"claude_code_mcp_unregister",
"claude_code_skill_list",
"claude_code_skill_register",
"claude_code_skill_unregister",
"claude_code_agent_list",
"claude_code_agent_register",
"claude_code_agent_unregister",
"claude_code_command_list",
"claude_code_command_register",
"claude_code_command_unregister",
"claude_code_hook_list",
"claude_code_hook_register",
"claude_code_hook_unregister",
"claude_code_output_style_list",
"claude_code_output_style_register",
"claude_code_output_style_unregister",
"claude_code_instructions_list",
"claude_code_instructions_register",
"claude_code_instructions_unregister",
"claude_code_settings_get",
"claude_code_settings_set",
"claude_code_settings_clear",
"claude_code_permission_add",
"claude_code_permission_remove",
"claude_code_config_list",
"claude_code_config_show",
"claude_code_config_status",
"claude_code_config_import"
],
"resources": [
{