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:
parent
a639d0ecdc
commit
be408cb8b9
2 changed files with 68 additions and 49 deletions
|
|
@ -559,7 +559,7 @@ func gatewayCmd() {
|
|||
})
|
||||
|
||||
// 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(
|
||||
cfg.WorkspacePath(),
|
||||
|
|
@ -985,14 +985,14 @@ func getConfigPath() string {
|
|||
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")
|
||||
|
||||
// Create cron service
|
||||
cronService := cron.NewCronService(cronStorePath, nil)
|
||||
|
||||
// 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)
|
||||
|
||||
// Set the onJob handler
|
||||
|
|
|
|||
|
|
@ -22,18 +22,24 @@ type CronTool struct {
|
|||
executor JobExecutor
|
||||
msgBus *bus.MessageBus
|
||||
execTool *ExecTool
|
||||
execEnabled bool
|
||||
channel string
|
||||
chatID string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// 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{
|
||||
cronService: cronService,
|
||||
executor: executor,
|
||||
msgBus: msgBus,
|
||||
execTool: NewExecTool(workspace, restrict),
|
||||
execTool: execTool,
|
||||
execEnabled: execEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,14 +50,15 @@ func (t *CronTool) Name() string {
|
|||
|
||||
// Description returns the tool description
|
||||
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 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
|
||||
func (t *CronTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
props := map[string]interface{}{
|
||||
"action": map[string]interface{}{
|
||||
"type": "string",
|
||||
"enum": []string{"add", "list", "remove", "enable", "disable"},
|
||||
|
|
@ -59,11 +66,7 @@ func (t *CronTool) Parameters() map[string]interface{} {
|
|||
},
|
||||
"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.",
|
||||
},
|
||||
"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.",
|
||||
"description": "The reminder/task message to display when triggered.",
|
||||
},
|
||||
"at_seconds": map[string]interface{}{
|
||||
"type": "integer",
|
||||
|
|
@ -85,7 +88,23 @@ func (t *CronTool) Parameters() map[string]interface{} {
|
|||
"type": "boolean",
|
||||
"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"},
|
||||
}
|
||||
}
|
||||
|
|
@ -172,11 +191,11 @@ func (t *CronTool) addJob(args map[string]interface{}) *ToolResult {
|
|||
}
|
||||
|
||||
command, _ := args["command"].(string)
|
||||
if command != "" && t.execTool == nil {
|
||||
// Exec is disabled; ignore command parameter
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -273,8 +292,8 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
|
|||
chatID = "direct"
|
||||
}
|
||||
|
||||
// Execute command if present
|
||||
if job.Payload.Command != "" {
|
||||
// Execute command if present (only when exec is enabled)
|
||||
if job.Payload.Command != "" && t.execTool != nil {
|
||||
args := map[string]interface{}{
|
||||
"command": job.Payload.Command,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue