mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 06:21:10 +00:00
Squash merge PR #112 after resolving the project changelog conflict with current dev. Local checks passed: config gateway tests, provider/http/tools deny-pattern tests, go build ./..., and go build -tags sqliteonly ./.... PR CI run 26704832350 passed release-versioning, go, and web.
280 lines
8.6 KiB
Go
280 lines
8.6 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
)
|
|
|
|
// TestShellDenyGroupsConfigReload_UpdatesGlobal asserts the pub/sub subscriber
|
|
// dispatches a TopicConfigChanged event into ExecTool.SetGlobalShellDenyGroups —
|
|
// the regression coverage that the original PR #1005 was missing.
|
|
func TestShellDenyGroupsConfigReload_UpdatesGlobal(t *testing.T) {
|
|
msgBus := bus.New()
|
|
defer msgBus.Unsubscribe("shell-deny-groups-config-reload")
|
|
|
|
toolsReg := tools.NewRegistry()
|
|
execTool := tools.NewExecTool("/tmp", false)
|
|
toolsReg.Register(execTool)
|
|
|
|
subscribeShellDenyGroupsReload(msgBus, toolsReg)
|
|
|
|
msgBus.Broadcast(bus.Event{
|
|
Name: bus.TopicConfigChanged,
|
|
Payload: &config.Config{
|
|
Tools: config.ToolsConfig{
|
|
ShellDenyGroups: map[string]bool{"package_install": true},
|
|
},
|
|
},
|
|
})
|
|
|
|
got := execTool.EffectiveDenyGroupsForTest(context.Background())
|
|
if v, ok := got["package_install"]; !ok || v != true {
|
|
t.Fatalf("expected pub/sub to set global package_install=true, got %v", got)
|
|
}
|
|
rules := execTool.CommandKeywordAllowlistForTest()
|
|
if len(rules) != 0 {
|
|
t.Fatalf("expected empty command keyword allowlist, got %v", rules)
|
|
}
|
|
}
|
|
|
|
func TestShellDenyGroupsConfigReload_UpdatesCommandKeywordAllowlist(t *testing.T) {
|
|
msgBus := bus.New()
|
|
defer msgBus.Unsubscribe("shell-deny-groups-config-reload")
|
|
|
|
toolsReg := tools.NewRegistry()
|
|
execTool := tools.NewExecTool("/tmp", false)
|
|
toolsReg.Register(execTool)
|
|
|
|
subscribeShellDenyGroupsReload(msgBus, toolsReg)
|
|
|
|
msgBus.Broadcast(bus.Event{
|
|
Name: bus.TopicConfigChanged,
|
|
Payload: &config.Config{
|
|
Tools: config.ToolsConfig{
|
|
CommandKeywordAllowlist: []config.CommandKeywordAllowlistRule{
|
|
{ID: "github-content", Command: "gh", Args: []string{"--body"}, Keywords: []string{"secret"}},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
rules := execTool.CommandKeywordAllowlistForTest()
|
|
if len(rules) != 1 || rules[0].ID != "github-content" {
|
|
t.Fatalf("expected command keyword allowlist to reload, got %v", rules)
|
|
}
|
|
}
|
|
|
|
// TestShellDenyGroupsConfigReload_IgnoresOtherEvents: subscriber must guard
|
|
// on event.Name and ignore non-TopicConfigChanged broadcasts.
|
|
func TestShellDenyGroupsConfigReload_IgnoresOtherEvents(t *testing.T) {
|
|
msgBus := bus.New()
|
|
defer msgBus.Unsubscribe("shell-deny-groups-config-reload")
|
|
|
|
toolsReg := tools.NewRegistry()
|
|
execTool := tools.NewExecTool("/tmp", false)
|
|
execTool.SetGlobalShellDenyGroups(map[string]bool{"package_install": false}) // baseline
|
|
toolsReg.Register(execTool)
|
|
|
|
subscribeShellDenyGroupsReload(msgBus, toolsReg)
|
|
|
|
msgBus.Broadcast(bus.Event{
|
|
Name: bus.TopicAgentDeleted,
|
|
Payload: &config.Config{
|
|
Tools: config.ToolsConfig{
|
|
ShellDenyGroups: map[string]bool{"package_install": true},
|
|
},
|
|
},
|
|
})
|
|
|
|
got := execTool.EffectiveDenyGroupsForTest(context.Background())
|
|
if v := got["package_install"]; v != false {
|
|
t.Fatalf("expected non-config event to be ignored; package_install changed to %v", v)
|
|
}
|
|
}
|
|
|
|
// TestShellDenyGroupsConfigReload_IgnoresWrongPayload: subscriber must
|
|
// type-assert payload to *config.Config and skip mismatched payloads.
|
|
func TestShellDenyGroupsConfigReload_IgnoresWrongPayload(t *testing.T) {
|
|
msgBus := bus.New()
|
|
defer msgBus.Unsubscribe("shell-deny-groups-config-reload")
|
|
|
|
toolsReg := tools.NewRegistry()
|
|
execTool := tools.NewExecTool("/tmp", false)
|
|
execTool.SetGlobalShellDenyGroups(map[string]bool{"package_install": false})
|
|
toolsReg.Register(execTool)
|
|
|
|
subscribeShellDenyGroupsReload(msgBus, toolsReg)
|
|
|
|
msgBus.Broadcast(bus.Event{
|
|
Name: bus.TopicConfigChanged,
|
|
Payload: "not-a-config-pointer",
|
|
})
|
|
|
|
got := execTool.EffectiveDenyGroupsForTest(context.Background())
|
|
if v := got["package_install"]; v != false {
|
|
t.Fatalf("expected wrong-payload event to be ignored; package_install changed to %v", v)
|
|
}
|
|
}
|
|
|
|
func TestShellDenyGroupsConfigReload_ReplacesConfigClaudeCLIProvider(t *testing.T) {
|
|
msgBus := bus.New()
|
|
defer msgBus.Unsubscribe("shell-deny-provider-policy-reload")
|
|
|
|
providerReg := providers.NewRegistry(store.TenantIDFromContext)
|
|
defer providerReg.Close()
|
|
|
|
initial := config.Default()
|
|
initial.Providers.ClaudeCLI.CLIPath = "claude"
|
|
initial.Tools.ShellDenyGroups = map[string]bool{"package_install": true}
|
|
reloadShellDenyProviderPolicies(providerReg, nil, nil, initial)
|
|
|
|
before, err := providerReg.Get(context.Background(), "claude-cli")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
beforeCLI, ok := before.(*providers.ClaudeCLIProvider)
|
|
if !ok {
|
|
t.Fatalf("expected ClaudeCLI provider, got %T", before)
|
|
}
|
|
|
|
updated := config.Default()
|
|
updated.Providers.ClaudeCLI.CLIPath = "claude"
|
|
updated.Tools.ShellDenyGroups = map[string]bool{"package_install": false}
|
|
subscribeProviderShellDenyGroupsReload(msgBus, providerReg, nil, nil)
|
|
msgBus.Broadcast(bus.Event{Name: bus.TopicConfigChanged, Payload: updated})
|
|
|
|
after, err := providerReg.Get(context.Background(), "claude-cli")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
afterCLI, ok := after.(*providers.ClaudeCLIProvider)
|
|
if !ok {
|
|
t.Fatalf("expected ClaudeCLI provider after reload, got %T", after)
|
|
}
|
|
if beforeCLI == afterCLI {
|
|
t.Fatal("expected config change to replace Claude CLI provider runtime")
|
|
}
|
|
}
|
|
|
|
func TestShellDenyGroupsConfigReload_ReplacesDBClaudeCLIProvider(t *testing.T) {
|
|
providerReg := providers.NewRegistry(store.TenantIDFromContext)
|
|
defer providerReg.Close()
|
|
|
|
tenantID := uuid.New()
|
|
binary := writeTestExecutable(t)
|
|
provStore := &shellDenyGroupsProviderStore{providers: []store.LLMProviderData{
|
|
{
|
|
TenantID: tenantID,
|
|
Name: "tenant-claude",
|
|
ProviderType: store.ProviderClaudeCLI,
|
|
APIBase: binary,
|
|
Enabled: true,
|
|
},
|
|
}}
|
|
|
|
initial := config.Default()
|
|
initial.Tools.ShellDenyGroups = map[string]bool{"package_install": true}
|
|
reloadShellDenyProviderPolicies(providerReg, provStore, nil, initial)
|
|
|
|
before, err := providerReg.GetForTenant(tenantID, "tenant-claude")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
beforeCLI, ok := before.(*providers.ClaudeCLIProvider)
|
|
if !ok {
|
|
t.Fatalf("expected ClaudeCLI provider, got %T", before)
|
|
}
|
|
|
|
updated := config.Default()
|
|
updated.Tools.ShellDenyGroups = map[string]bool{"package_install": false}
|
|
reloadShellDenyProviderPolicies(providerReg, provStore, nil, updated)
|
|
|
|
after, err := providerReg.GetForTenant(tenantID, "tenant-claude")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
afterCLI, ok := after.(*providers.ClaudeCLIProvider)
|
|
if !ok {
|
|
t.Fatalf("expected ClaudeCLI provider after reload, got %T", after)
|
|
}
|
|
if beforeCLI == afterCLI {
|
|
t.Fatal("expected config change to replace DB Claude CLI provider runtime")
|
|
}
|
|
}
|
|
|
|
func TestConfiguredShellDenyPatternsDropsDisabledPackageInstall(t *testing.T) {
|
|
cfg := config.Default()
|
|
cfg.Tools.ShellDenyGroups = map[string]bool{"package_install": false}
|
|
|
|
patterns := configuredShellDenyPatterns(cfg)
|
|
|
|
if matchesAny(patterns, "pip install requests") {
|
|
t.Fatal("package_install=false should remove package-install deny patterns")
|
|
}
|
|
if !matchesAny(patterns, "env") {
|
|
t.Fatal("unrelated default deny patterns should remain active")
|
|
}
|
|
}
|
|
|
|
type shellDenyGroupsProviderStore struct {
|
|
providers []store.LLMProviderData
|
|
}
|
|
|
|
func (s *shellDenyGroupsProviderStore) CreateProvider(context.Context, *store.LLMProviderData) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (s *shellDenyGroupsProviderStore) GetProvider(context.Context, uuid.UUID) (*store.LLMProviderData, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func (s *shellDenyGroupsProviderStore) GetProviderByName(context.Context, string) (*store.LLMProviderData, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func (s *shellDenyGroupsProviderStore) ListProviders(context.Context) ([]store.LLMProviderData, error) {
|
|
return s.providers, nil
|
|
}
|
|
|
|
func (s *shellDenyGroupsProviderStore) ListAllProviders(context.Context) ([]store.LLMProviderData, error) {
|
|
return s.providers, nil
|
|
}
|
|
|
|
func (s *shellDenyGroupsProviderStore) UpdateProvider(context.Context, uuid.UUID, map[string]any) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func (s *shellDenyGroupsProviderStore) DeleteProvider(context.Context, uuid.UUID) error {
|
|
return errors.New("not implemented")
|
|
}
|
|
|
|
func writeTestExecutable(t *testing.T) string {
|
|
t.Helper()
|
|
binary := filepath.Join(t.TempDir(), "claude-test")
|
|
if err := os.WriteFile(binary, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return binary
|
|
}
|
|
|
|
func matchesAny(patterns []*regexp.Regexp, command string) bool {
|
|
for _, pattern := range patterns {
|
|
if pattern.MatchString(command) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|