fix(security): hide cron command parameter when exec is disabled

CronTool bypassed the tools.exec.enabled config by instantiating its
own internal ExecTool. When exec is disabled, the command parameter
is now removed from the tool schema and description so the agent
cannot recognize or use it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
KoheiYamashita 2026-02-18 08:56:27 +09:00
parent a639d0ecdc
commit be408cb8b9
2 changed files with 68 additions and 49 deletions

View file

@ -559,7 +559,7 @@ func gatewayCmd() {
}) })
// Setup cron tool and service // Setup cron tool and service
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.DataPath(), cfg.Agents.Defaults.RestrictToWorkspace) cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.DataPath(), cfg.Agents.Defaults.RestrictToWorkspace, cfg.Tools.Exec.Enabled)
heartbeatService := heartbeat.NewHeartbeatService( heartbeatService := heartbeat.NewHeartbeatService(
cfg.WorkspacePath(), cfg.WorkspacePath(),
@ -985,14 +985,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, dataDir string, restrict bool) *cron.CronService { func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, dataDir string, restrict bool, execEnabled bool) *cron.CronService {
cronStorePath := filepath.Join(dataDir, "cron", "jobs.json") cronStorePath := filepath.Join(dataDir, "cron", "jobs.json")
// Create cron service // Create cron service
cronService := cron.NewCronService(cronStorePath, nil) cronService := cron.NewCronService(cronStorePath, nil)
// Create and register CronTool (workspace is for ExecTool sandboxing) // Create and register CronTool (workspace is for ExecTool sandboxing)
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict) cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execEnabled)
agentLoop.RegisterTool(cronTool) agentLoop.RegisterTool(cronTool)
// Set the onJob handler // Set the onJob handler

View file

@ -22,18 +22,24 @@ type CronTool struct {
executor JobExecutor executor JobExecutor
msgBus *bus.MessageBus msgBus *bus.MessageBus
execTool *ExecTool execTool *ExecTool
execEnabled bool
channel string channel string
chatID string chatID string
mu sync.RWMutex mu sync.RWMutex
} }
// NewCronTool creates a new CronTool // NewCronTool creates a new CronTool
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool) *CronTool { func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execEnabled bool) *CronTool {
var execTool *ExecTool
if execEnabled {
execTool = NewExecTool(workspace, restrict)
}
return &CronTool{ return &CronTool{
cronService: cronService, cronService: cronService,
executor: executor, executor: executor,
msgBus: msgBus, msgBus: msgBus,
execTool: NewExecTool(workspace, restrict), execTool: execTool,
execEnabled: execEnabled,
} }
} }
@ -44,14 +50,15 @@ func (t *CronTool) Name() string {
// Description returns the tool description // Description returns the tool description
func (t *CronTool) Description() string { func (t *CronTool) Description() string {
if t.execEnabled {
return "Schedule reminders, tasks, or system commands. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules. Use 'command' to execute shell commands directly." return "Schedule reminders, tasks, or system commands. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules. Use 'command' to execute shell commands directly."
} }
return "Schedule reminders and tasks. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules."
}
// Parameters returns the tool parameters schema // Parameters returns the tool parameters schema
func (t *CronTool) Parameters() map[string]interface{} { func (t *CronTool) Parameters() map[string]interface{} {
return map[string]interface{}{ props := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{ "action": map[string]interface{}{
"type": "string", "type": "string",
"enum": []string{"add", "list", "remove", "enable", "disable"}, "enum": []string{"add", "list", "remove", "enable", "disable"},
@ -59,11 +66,7 @@ func (t *CronTool) Parameters() map[string]interface{} {
}, },
"message": map[string]interface{}{ "message": map[string]interface{}{
"type": "string", "type": "string",
"description": "The reminder/task message to display when triggered. If 'command' is used, this describes what the command does.", "description": "The reminder/task message to display when triggered.",
},
"command": map[string]interface{}{
"type": "string",
"description": "Optional: Shell command to execute directly (e.g., 'df -h'). If set, the agent will run this command and report output instead of just showing the message. 'deliver' will be forced to false for commands.",
}, },
"at_seconds": map[string]interface{}{ "at_seconds": map[string]interface{}{
"type": "integer", "type": "integer",
@ -85,7 +88,23 @@ func (t *CronTool) Parameters() map[string]interface{} {
"type": "boolean", "type": "boolean",
"description": "If true, send message directly to channel. If false, let agent process message (for complex tasks). Default: true", "description": "If true, send message directly to channel. If false, let agent process message (for complex tasks). Default: true",
}, },
}, }
if t.execEnabled {
props["command"] = map[string]interface{}{
"type": "string",
"description": "Optional: Shell command to execute directly (e.g., 'df -h'). If set, the agent will run this command and report output instead of just showing the message. 'deliver' will be forced to false for commands.",
}
// Update message description to mention command
props["message"] = map[string]interface{}{
"type": "string",
"description": "The reminder/task message to display when triggered. If 'command' is used, this describes what the command does.",
}
}
return map[string]interface{}{
"type": "object",
"properties": props,
"required": []string{"action"}, "required": []string{"action"},
} }
} }
@ -172,11 +191,11 @@ func (t *CronTool) addJob(args map[string]interface{}) *ToolResult {
} }
command, _ := args["command"].(string) command, _ := args["command"].(string)
if command != "" && t.execTool == nil {
// Exec is disabled; ignore command parameter
command = ""
}
if command != "" { if command != "" {
// Commands must be processed by agent/exec tool, so deliver must be false (or handled specifically)
// Actually, let's keep deliver=false to let the system know it's not a simple chat message
// But for our new logic in ExecuteJob, we can handle it regardless of deliver flag if Payload.Command is set.
// However, logically, it's not "delivered" to chat directly as is.
deliver = false deliver = false
} }
@ -273,8 +292,8 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
chatID = "direct" chatID = "direct"
} }
// Execute command if present // Execute command if present (only when exec is enabled)
if job.Payload.Command != "" { if job.Payload.Command != "" && t.execTool != nil {
args := map[string]interface{}{ args := map[string]interface{}{
"command": job.Payload.Command, "command": job.Payload.Command,
} }