feat: 🎸 add allow patterns to config file

This commit is contained in:
candywater 2026-02-18 01:31:51 +08:00
parent 0d16525fab
commit 8e4961e252
8 changed files with 71 additions and 8 deletions

View file

@ -563,7 +563,7 @@ func gatewayCmd() {
// Setup cron tool and service // Setup cron tool and service
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout) cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg.Agents.Defaults.AllowPatterns)
heartbeatService := heartbeat.NewHeartbeatService( heartbeatService := heartbeat.NewHeartbeatService(
cfg.WorkspacePath(), cfg.WorkspacePath(),
@ -988,14 +988,14 @@ func getConfigPath() string {
return filepath.Join(home, ".picoclaw", "config.json") return filepath.Join(home, ".picoclaw", "config.json")
} }
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration) *cron.CronService { func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, allowPatterns []string) *cron.CronService {
cronStorePath := filepath.Join(workspace, "cron", "jobs.json") cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
// Create cron service // Create cron service
cronService := cron.NewCronService(cronStorePath, nil) cronService := cron.NewCronService(cronStorePath, nil)
// Create and register CronTool // Create and register CronTool
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout) cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, allowPatterns...)
agentLoop.RegisterTool(cronTool) agentLoop.RegisterTool(cronTool)
// Set the onJob handler // Set the onJob handler

View file

@ -3,6 +3,8 @@
"defaults": { "defaults": {
"workspace": "~/.picoclaw/workspace", "workspace": "~/.picoclaw/workspace",
"restrict_to_workspace": true, "restrict_to_workspace": true,
"provider": "",
"allow_patterns": [],
"model": "glm-4.7", "model": "glm-4.7",
"max_tokens": 8192, "max_tokens": 8192,
"temperature": 0.7, "temperature": 0.7,
@ -122,6 +124,10 @@
"api_key": "YOUR_BRAVE_API_KEY", "api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5 "max_results": 5
}, },
"duckduckgo": {
"enabled": true,
"max_results": 5
},
"perplexity": { "perplexity": {
"enabled": false, "enabled": false,
"api_key": "pplx-xxx", "api_key": "pplx-xxx",

View file

@ -71,7 +71,7 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
registry.Register(tools.NewAppendFileTool(workspace, restrict)) registry.Register(tools.NewAppendFileTool(workspace, restrict))
// Shell execution // Shell execution
registry.Register(tools.NewExecTool(workspace, restrict)) registry.Register(tools.NewExecTool(workspace, restrict, cfg.Agents.Defaults.AllowPatterns...))
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{ if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
BraveAPIKey: cfg.Tools.Web.Brave.APIKey, BraveAPIKey: cfg.Tools.Web.Brave.APIKey,

View file

@ -61,6 +61,7 @@ type AgentsConfig struct {
type AgentDefaults struct { type AgentDefaults struct {
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
AllowPatterns []string `json:"allow_patterns" env:"PICOCLAW_AGENTS_DEFAULTS_ALLOW_PATTERNS" envSeparator:","`
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
@ -233,6 +234,7 @@ func DefaultConfig() *Config {
Defaults: AgentDefaults{ Defaults: AgentDefaults{
Workspace: "~/.picoclaw/workspace", Workspace: "~/.picoclaw/workspace",
RestrictToWorkspace: true, RestrictToWorkspace: true,
AllowPatterns: []string{},
Provider: "", Provider: "",
Model: "glm-4.7", Model: "glm-4.7",
MaxTokens: 8192, MaxTokens: 8192,

View file

@ -84,6 +84,9 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
if v, ok := getString(defaults, "workspace"); ok { if v, ok := getString(defaults, "workspace"); ok {
cfg.Agents.Defaults.Workspace = rewriteWorkspacePath(v) cfg.Agents.Defaults.Workspace = rewriteWorkspacePath(v)
} }
if v := getStringSlice(defaults, "allow_patterns"); len(v) > 0 {
cfg.Agents.Defaults.AllowPatterns = v
}
} }
} }
@ -280,6 +283,10 @@ func MergeConfig(existing, incoming *config.Config) *config.Config {
existing.Tools.Web.Brave = incoming.Tools.Web.Brave existing.Tools.Web.Brave = incoming.Tools.Web.Brave
} }
if len(existing.Agents.Defaults.AllowPatterns) == 0 && len(incoming.Agents.Defaults.AllowPatterns) > 0 {
existing.Agents.Defaults.AllowPatterns = incoming.Agents.Defaults.AllowPatterns
}
return existing return existing
} }

View file

@ -261,6 +261,7 @@ func TestConvertConfig(t *testing.T) {
"temperature": 0.5, "temperature": 0.5,
"max_tool_iterations": float64(10), "max_tool_iterations": float64(10),
"workspace": "~/.openclaw/workspace", "workspace": "~/.openclaw/workspace",
"allow_patterns": []interface{}{"^ls(\\s|$)", "^pwd$"},
}, },
}, },
} }
@ -281,6 +282,15 @@ func TestConvertConfig(t *testing.T) {
if cfg.Agents.Defaults.Workspace != "~/.picoclaw/workspace" { if cfg.Agents.Defaults.Workspace != "~/.picoclaw/workspace" {
t.Errorf("Workspace = %q, want %q", cfg.Agents.Defaults.Workspace, "~/.picoclaw/workspace") t.Errorf("Workspace = %q, want %q", cfg.Agents.Defaults.Workspace, "~/.picoclaw/workspace")
} }
if len(cfg.Agents.Defaults.AllowPatterns) != 2 {
t.Fatalf("AllowPatterns length = %d, want 2", len(cfg.Agents.Defaults.AllowPatterns))
}
if cfg.Agents.Defaults.AllowPatterns[0] != "^ls(\\s|$)" {
t.Errorf("AllowPatterns[0] = %q, want %q", cfg.Agents.Defaults.AllowPatterns[0], "^ls(\\s|$)")
}
if cfg.Agents.Defaults.AllowPatterns[1] != "^pwd$" {
t.Errorf("AllowPatterns[1] = %q, want %q", cfg.Agents.Defaults.AllowPatterns[1], "^pwd$")
}
}) })
t.Run("empty config", func(t *testing.T) { t.Run("empty config", func(t *testing.T) {
@ -361,6 +371,36 @@ func TestMergeConfig(t *testing.T) {
t.Errorf("Telegram.Token should be preserved, got %q", result.Channels.Telegram.Token) t.Errorf("Telegram.Token should be preserved, got %q", result.Channels.Telegram.Token)
} }
}) })
t.Run("fills empty allow patterns", func(t *testing.T) {
existing := config.DefaultConfig()
incoming := config.DefaultConfig()
incoming.Agents.Defaults.AllowPatterns = []string{"^ls(\\s|$)", "^pwd$"}
result := MergeConfig(existing, incoming)
if len(result.Agents.Defaults.AllowPatterns) != 2 {
t.Fatalf("AllowPatterns length = %d, want 2", len(result.Agents.Defaults.AllowPatterns))
}
if result.Agents.Defaults.AllowPatterns[0] != "^ls(\\s|$)" {
t.Errorf("AllowPatterns[0] = %q, want %q", result.Agents.Defaults.AllowPatterns[0], "^ls(\\s|$)")
}
})
t.Run("preserves existing allow patterns", func(t *testing.T) {
existing := config.DefaultConfig()
existing.Agents.Defaults.AllowPatterns = []string{"^cat\\s+README\\.md$"}
incoming := config.DefaultConfig()
incoming.Agents.Defaults.AllowPatterns = []string{"^ls(\\s|$)"}
result := MergeConfig(existing, incoming)
if len(result.Agents.Defaults.AllowPatterns) != 1 {
t.Fatalf("AllowPatterns length = %d, want 1", len(result.Agents.Defaults.AllowPatterns))
}
if result.Agents.Defaults.AllowPatterns[0] != "^cat\\s+README\\.md$" {
t.Errorf("AllowPatterns[0] should be preserved, got %q", result.Agents.Defaults.AllowPatterns[0])
}
})
} }
func TestPlanWorkspaceMigration(t *testing.T) { func TestPlanWorkspaceMigration(t *testing.T) {

View file

@ -29,8 +29,8 @@ type CronTool struct {
// NewCronTool creates a new CronTool // NewCronTool creates a new CronTool
// execTimeout: 0 means no timeout, >0 sets the timeout duration // execTimeout: 0 means no timeout, >0 sets the timeout duration
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration) *CronTool { func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, allowPatterns ...string) *CronTool {
execTool := NewExecTool(workspace, restrict) execTool := NewExecTool(workspace, restrict, allowPatterns...)
execTool.SetTimeout(execTimeout) // 0 means no timeout execTool.SetTimeout(execTimeout) // 0 means no timeout
return &CronTool{ return &CronTool{
cronService: cronService, cronService: cronService,

View file

@ -21,7 +21,7 @@ type ExecTool struct {
restrictToWorkspace bool restrictToWorkspace bool
} }
func NewExecTool(workingDir string, restrict bool) *ExecTool { func NewExecTool(workingDir string, restrict bool, allowPatterns ...string) *ExecTool {
denyPatterns := []*regexp.Regexp{ denyPatterns := []*regexp.Regexp{
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`), regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
regexp.MustCompile(`\bdel\s+/[fq]\b`), regexp.MustCompile(`\bdel\s+/[fq]\b`),
@ -33,11 +33,19 @@ func NewExecTool(workingDir string, restrict bool) *ExecTool {
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`), regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
} }
var allowRegex []*regexp.Regexp
if len(allowPatterns) > 0 {
allowRegex = make([]*regexp.Regexp, 0, len(allowPatterns))
for _, p := range allowPatterns {
allowRegex = append(allowRegex, regexp.MustCompile(p))
}
}
return &ExecTool{ return &ExecTool{
workingDir: workingDir, workingDir: workingDir,
timeout: 60 * time.Second, timeout: 60 * time.Second,
denyPatterns: denyPatterns, denyPatterns: denyPatterns,
allowPatterns: nil, allowPatterns: allowRegex,
restrictToWorkspace: restrict, restrictToWorkspace: restrict,
} }
} }