diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index e99244d01..af09b0086 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1353,7 +1353,7 @@ func (al *AgentLoop) executeCmdMode(ctx context.Context, agent *AgentInstance, c if workDir == "" { workDir = agent.Workspace } - return al.handleEditCommand(content, workDir), nil + return al.handleEditCommand(content, workDir, agent.Workspace), nil } // Intercept interactive editors @@ -1462,7 +1462,7 @@ func shortenHomePath(path string) string { // :edit + → insert after line N // :edit - → delete line N // :edit -m """""" → write full content (create if needed) -func (al *AgentLoop) handleEditCommand(content, workDir string) string { +func (al *AgentLoop) handleEditCommand(content, workDir, workspace string) string { raw := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(content), ":edit")) if raw == "" { return editUsage() @@ -1479,7 +1479,10 @@ func (al *AgentLoop) handleEditCommand(content, workDir string) string { return editUsage() } - filename := resolveEditPath(parts[0], workDir) + filename, err := resolveEditPath(parts[0], workDir, workspace) + if err != nil { + return fmt.Sprintf("Access denied: %s", err) + } // :edit — show file content if len(parts) == 1 && !strings.Contains(raw, "\n") { @@ -1501,15 +1504,19 @@ func (al *AgentLoop) handleEditCommand(content, workDir string) string { return editUsage() } -func resolveEditPath(name, workDir string) string { - if strings.HasPrefix(name, "~/") { - home, _ := os.UserHomeDir() - return home + name[1:] +func resolveEditPath(name, workDir, workspace string) (string, error) { + // Treat ~ as workspace root (not $HOME) + if name == "~" { + name = "." + } else if strings.HasPrefix(name, "~/") { + name = name[2:] } - if filepath.IsAbs(name) { - return name + // Resolve relative paths against workDir + if !filepath.IsAbs(name) { + name = filepath.Join(workDir, name) } - return filepath.Join(workDir, name) + // Validate against workspace (blocks absolute paths outside workspace, symlink escape, traversal) + return tools.ValidatePath(name, workspace, true) } func editUsage() string { diff --git a/pkg/tools/filesystem.go b/pkg/tools/filesystem.go index 37db8b4ae..76d6f91e9 100644 --- a/pkg/tools/filesystem.go +++ b/pkg/tools/filesystem.go @@ -10,8 +10,8 @@ import ( "time" ) -// validatePath ensures the given path is within the workspace if restrict is true. -func validatePath(path, workspace string, restrict bool) (string, error) { +// ValidatePath ensures the given path is within the workspace if restrict is true. +func ValidatePath(path, workspace string, restrict bool) (string, error) { if workspace == "" { return path, fmt.Errorf("workspace is not defined") } diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 9acf95d69..e4e1bef9b 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -143,7 +143,7 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult cwd := t.workingDir if wd, ok := args["working_dir"].(string); ok && wd != "" { if t.restrictToWorkspace && t.workingDir != "" { - resolvedWD, err := validatePath(wd, t.workingDir, true) + resolvedWD, err := ValidatePath(wd, t.workingDir, true) if err != nil { return ErrorResult("Command blocked by safety guard (" + err.Error() + ")") }