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
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(
cfg.WorkspacePath(),
@ -988,14 +988,14 @@ func getConfigPath() string {
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")
// Create cron service
cronService := cron.NewCronService(cronStorePath, nil)
// 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)
// Set the onJob handler

View file

@ -3,6 +3,8 @@
"defaults": {
"workspace": "~/.picoclaw/workspace",
"restrict_to_workspace": true,
"provider": "",
"allow_patterns": [],
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
@ -122,6 +124,10 @@
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
},
"duckduckgo": {
"enabled": true,
"max_results": 5
},
"perplexity": {
"enabled": false,
"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))
// Shell execution
registry.Register(tools.NewExecTool(workspace, restrict))
registry.Register(tools.NewExecTool(workspace, restrict, cfg.Agents.Defaults.AllowPatterns...))
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,

View file

@ -61,6 +61,7 @@ type AgentsConfig struct {
type AgentDefaults struct {
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_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"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
@ -233,6 +234,7 @@ func DefaultConfig() *Config {
Defaults: AgentDefaults{
Workspace: "~/.picoclaw/workspace",
RestrictToWorkspace: true,
AllowPatterns: []string{},
Provider: "",
Model: "glm-4.7",
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 {
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
}
if len(existing.Agents.Defaults.AllowPatterns) == 0 && len(incoming.Agents.Defaults.AllowPatterns) > 0 {
existing.Agents.Defaults.AllowPatterns = incoming.Agents.Defaults.AllowPatterns
}
return existing
}

View file

@ -261,6 +261,7 @@ func TestConvertConfig(t *testing.T) {
"temperature": 0.5,
"max_tool_iterations": float64(10),
"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" {
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) {
@ -361,6 +371,36 @@ func TestMergeConfig(t *testing.T) {
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) {

View file

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

View file

@ -21,7 +21,7 @@ type ExecTool struct {
restrictToWorkspace bool
}
func NewExecTool(workingDir string, restrict bool) *ExecTool {
func NewExecTool(workingDir string, restrict bool, allowPatterns ...string) *ExecTool {
denyPatterns := []*regexp.Regexp{
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
regexp.MustCompile(`\bdel\s+/[fq]\b`),
@ -33,11 +33,19 @@ func NewExecTool(workingDir string, restrict bool) *ExecTool {
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{
workingDir: workingDir,
timeout: 60 * time.Second,
denyPatterns: denyPatterns,
allowPatterns: nil,
allowPatterns: allowRegex,
restrictToWorkspace: restrict,
}
}