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:
@@ -0,0 +1,320 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user