mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-26 04:23:33 +00:00
Adds ToolsConfig.ShellDenyGroups (map[string]bool) so operators can toggle shell deny groups (package_install, env_dump, etc.) from the /config Web UI without a process restart. Wiring: - ExecTool gains globalDenyGroups + SetGlobalShellDenyGroups (defensive copy). - effectiveDenyGroups merges per-agent context overrides with global, with per-key agent precedence — multi-tenant invariant preserved. - setupToolRegistry applies the initial config before any request can arrive. - subscribeShellDenyGroupsReload (extracted helper for testability) reacts to bus.TopicConfigChanged and re-applies the new toggles. Regression coverage the original PR #1005 attempt missed: - 6 unit tests for the merge / defensive-copy semantics in internal/tools. - 3 pub/sub dispatch tests in cmd: happy path, wrong event name, wrong payload type.
101 lines
3.7 KiB
Go
101 lines
3.7 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// TestExecToolEffectiveDenyGroups_GlobalOnly: with an empty agent context,
|
|
// effectiveDenyGroups must return the global toggles set via SetGlobalShellDenyGroups.
|
|
func TestExecToolEffectiveDenyGroups_GlobalOnly(t *testing.T) {
|
|
tool := NewExecTool("/tmp", false)
|
|
tool.SetGlobalShellDenyGroups(map[string]bool{"package_install": false})
|
|
|
|
got := tool.effectiveDenyGroups(context.Background())
|
|
if v, ok := got["package_install"]; !ok || v != false {
|
|
t.Fatalf("expected global package_install=false in effective map, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestExecToolEffectiveDenyGroups_AgentOverridesGlobal: per-agent context
|
|
// override wins per-key over global; non-overridden global keys must remain.
|
|
func TestExecToolEffectiveDenyGroups_AgentOverridesGlobal(t *testing.T) {
|
|
tool := NewExecTool("/tmp", false)
|
|
tool.SetGlobalShellDenyGroups(map[string]bool{
|
|
"package_install": false,
|
|
"env_dump": false,
|
|
})
|
|
|
|
ctx := store.WithShellDenyGroups(context.Background(), map[string]bool{"package_install": true})
|
|
|
|
got := tool.effectiveDenyGroups(ctx)
|
|
if v, ok := got["package_install"]; !ok || v != true {
|
|
t.Errorf("expected agent override package_install=true, got %v (ok=%v)", v, ok)
|
|
}
|
|
if v, ok := got["env_dump"]; !ok || v != false {
|
|
t.Errorf("expected global env_dump=false preserved, got %v (ok=%v)", v, ok)
|
|
}
|
|
}
|
|
|
|
// TestExecToolEffectiveDenyGroups_NilGlobalReturnsAgent: when no global
|
|
// is configured, return the agent map (preserving existing per-agent semantics).
|
|
func TestExecToolEffectiveDenyGroups_NilGlobalReturnsAgent(t *testing.T) {
|
|
tool := NewExecTool("/tmp", false)
|
|
|
|
agent := map[string]bool{"foo": true}
|
|
ctx := store.WithShellDenyGroups(context.Background(), agent)
|
|
|
|
got := tool.effectiveDenyGroups(ctx)
|
|
if v, ok := got["foo"]; !ok || v != true {
|
|
t.Fatalf("expected agent map returned when global empty, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestExecToolEffectiveDenyGroups_EmptyAgentReturnsGlobal: when agent
|
|
// context has no overrides, return the global map.
|
|
func TestExecToolEffectiveDenyGroups_EmptyAgentReturnsGlobal(t *testing.T) {
|
|
tool := NewExecTool("/tmp", false)
|
|
tool.SetGlobalShellDenyGroups(map[string]bool{"foo": true})
|
|
|
|
got := tool.effectiveDenyGroups(context.Background())
|
|
if v, ok := got["foo"]; !ok || v != true {
|
|
t.Fatalf("expected global map returned when agent empty, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestExecToolSetGlobalShellDenyGroups_DefensiveCopy: mutating the caller's
|
|
// map after SetGlobalShellDenyGroups must not affect the tool's internal state.
|
|
func TestExecToolSetGlobalShellDenyGroups_DefensiveCopy(t *testing.T) {
|
|
tool := NewExecTool("/tmp", false)
|
|
|
|
src := map[string]bool{"package_install": true}
|
|
tool.SetGlobalShellDenyGroups(src)
|
|
|
|
// Mutate the caller's map AFTER passing it in.
|
|
src["package_install"] = false
|
|
src["env_dump"] = true
|
|
|
|
got := tool.effectiveDenyGroups(context.Background())
|
|
if v := got["package_install"]; v != true {
|
|
t.Errorf("expected internal copy to be insulated from caller mutation; got package_install=%v", v)
|
|
}
|
|
if _, ok := got["env_dump"]; ok {
|
|
t.Errorf("expected internal copy to be insulated from new caller-side keys; got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestExecToolSetGlobalShellDenyGroups_EmptyClears: passing an empty map
|
|
// must clear the internal state, not retain a stale copy.
|
|
func TestExecToolSetGlobalShellDenyGroups_EmptyClears(t *testing.T) {
|
|
tool := NewExecTool("/tmp", false)
|
|
tool.SetGlobalShellDenyGroups(map[string]bool{"foo": true})
|
|
tool.SetGlobalShellDenyGroups(map[string]bool{})
|
|
|
|
got := tool.effectiveDenyGroups(context.Background())
|
|
if len(got) != 0 {
|
|
t.Fatalf("expected cleared global to yield empty effective map, got %v", got)
|
|
}
|
|
}
|