claude-code: register the agent's configuration at three scopes, served as the nox-mesh plugin

Skills, subagents, commands and hooks had no machine-wide place, so they were
copied into homes by hand and drifted. Each is now registered once through the
module's tools, kept in its config state, and written per node: the plugin in
the managed directory, settings and instructions in the managed files, or the
account's own directory, touching only what the module placed. hq ADR 0216.
This commit is contained in:
jochen
2026-10-05 11:57:35 +02:00
parent 170b3296e9
commit dd1035a27d
9 changed files with 1653 additions and 25 deletions
@@ -0,0 +1,312 @@
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)
}
// 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: a comma-separated list of nodes; 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")}
return Register(p, it, nodesOf(a["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)}
return Register(p, it, nodesOf(a["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)
return SetSettings(p, scopeOf(a), nodesOf(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)}
return Register(p, it, nodesOf(a["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")
}
return SetSettings(p, scopeOf(a), nodesOf(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")
return SetSettings(p, scopeOf(a), nodesOf(a["nodes"]), func(held map[string]any) map[string]any {
perms, _ := held["permissions"].(map[string]any)
rules, _ := perms[list].([]any)
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")
}
return Register(p, it, nodesOf(a["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}
}
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
}