claude-code: refuse registering what a session runs, so no agent can plant it in the operator's sessions
The tool runner is not told who called, so a hook, a managed setting, a permission rule or a tool server registered through these tools could come from an agent confined on another node and run as the operator there (hq issue 340). They are set at the controller's terminal now; a skill, subagent or command that runs something by itself is refused too, kept hooks and settings are never rendered, and claude_code_registrations lists what is registered and by which node.
This commit is contained in:
@@ -67,44 +67,26 @@ func TestEachKindLandsInItsOnePlace(t *testing.T) {
|
||||
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"} {
|
||||
root + "commands/ship.md", root + "output-styles/terse.md"} {
|
||||
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 {
|
||||
if !plugin[root+"skills/review/scripts/run.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)
|
||||
// The mesh's own guard on the shell first (novox/hq ADR 0245), then the hook registered.
|
||||
if pre := hooks.Hooks["PreToolUse"]; len(pre) != 2 || pre[0].Matcher != GuardMatcher || pre[0].Hooks[0].Command != GuardCommand() ||
|
||||
pre[1].Matcher != "Bash" || pre[1].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 {
|
||||
if managed["enabledPlugins"].(map[string]any)[Plugin+"@"+Plugin] != true {
|
||||
t.Errorf("managed settings: %v", managed)
|
||||
}
|
||||
if _, inPlugin := plugin[root+"settings.json"]; inPlugin {
|
||||
@@ -174,25 +156,6 @@ func TestTheMeshsOwnKeysCannotBeSetOrReplaced(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -248,7 +211,7 @@ func TestAnItemAboveTheLimitOrMisshapenIsRefused(t *testing.T) {
|
||||
"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"},
|
||||
"a hook anywhere": {Kind: KindHook, Name: "h", Scope: ScopeMesh, Event: "Stop", Command: "true"},
|
||||
}
|
||||
for label, it := range cases {
|
||||
if it.Problem() == "" {
|
||||
|
||||
Reference in New Issue
Block a user