@@ -6,6 +6,7 @@ import (
8 "path/filepath"
8 "path/filepath"
10 "charm.land/fantasy"
11 "charm.land/fantasy"
11 "github.com/charmbracelet/crush/internal/filepathext"
12 "github.com/charmbracelet/crush/internal/filepathext"
@@ -57,17 +58,50 @@ func NewTouchTool(
58 filePath := filepathext.SmartJoin(workingDir, params.FilePath)
59 filePath := filepathext.SmartJoin(workingDir, params.FilePath)
61+ absWorkingDir, err := filepath.Abs(workingDir)
63+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
65+ absFilePath, err := filepath.Abs(filePath)
67+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
69+ relPath, relErr := filepath.Rel(absWorkingDir, absFilePath)
70+ isOutsideWorkDir := relErr != nil || strings.HasPrefix(relPath, "..")
72+ if isOutsideWorkDir {
73+ granted, permReqErr := permissions.Request(ctx,
74+ permission.CreatePermissionRequest{
75+ SessionID: sessionID,
78+ ToolName: TouchToolName,
80+ Description: fmt.Sprintf("Create empty file outside working directory: %s", absFilePath),
81+ Params: TouchPermissionsParams{
82+ FilePath: absFilePath,
86+ if permReqErr != nil {
87+ return fantasy.ToolResponse{}, permReqErr
90+ return NewPermissionDeniedResponse(), nil
60− fileInfo, err := os.Stat(filePath)
94+ fileInfo, err := os.Stat(absFilePath)
61 if err == nil {
95 if err == nil {
62 if fileInfo.IsDir() {
96 if fileInfo.IsDir() {
63− return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
97+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", absFilePath)), nil
65− return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", filePath)), nil
99+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", absFilePath)), nil
66 } else if !os.IsNotExist(err) {
100 } else if !os.IsNotExist(err) {
67 return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
101 return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
70− dir := filepath.Dir(filePath)
104+ dir := filepath.Dir(absFilePath)
71 if err = os.MkdirAll(dir, 0o755); err != nil {
105 if err = os.MkdirAll(dir, 0o755); err != nil {
72 return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
106 return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
@@ -75,13 +109,13 @@ func NewTouchTool(
75 p, err := permissions.Request(ctx,
109 p, err := permissions.Request(ctx,
76 permission.CreatePermissionRequest{
110 permission.CreatePermissionRequest{
77 SessionID: sessionID,
111 SessionID: sessionID,
78− Path: fsext.PathOrPrefix(filePath, workingDir),
112+ Path: fsext.PathOrPrefix(absFilePath, absWorkingDir),
79 ToolCallID: call.ID,
113 ToolCallID: call.ID,
80 ToolName: TouchToolName,
114 ToolName: TouchToolName,
81 Action: "write",
115 Action: "write",
82− Description: fmt.Sprintf("Create empty file %s", filePath),
116+ Description: fmt.Sprintf("Create empty file %s", absFilePath),
83 Params: TouchPermissionsParams{
117 Params: TouchPermissionsParams{
84− FilePath: filePath,
118+ FilePath: absFilePath,
85 OldContent: "",
119 OldContent: "",
86 NewContent: "",
120 NewContent: "",
@@ -94,10 +128,10 @@ func NewTouchTool(
94 return NewPermissionDeniedResponse(), nil
128 return NewPermissionDeniedResponse(), nil
97− file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
131+ file, err := os.OpenFile(absFilePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
98 if err != nil {
132 if err != nil {
99 if os.IsExist(err) {
133 if os.IsExist(err) {
100− return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", filePath)), nil
134+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", absFilePath)), nil
102 return fantasy.ToolResponse{}, fmt.Errorf("error creating file: %w", err)
136 return fantasy.ToolResponse{}, fmt.Errorf("error creating file: %w", err)
@@ -105,21 +139,21 @@ func NewTouchTool(
105 return fantasy.ToolResponse{}, fmt.Errorf("error closing file: %w", err)
139 return fantasy.ToolResponse{}, fmt.Errorf("error closing file: %w", err)
108− _, err = files.Create(ctx, sessionID, filePath, "")
142+ _, err = files.Create(ctx, sessionID, absFilePath, "")
109 if err != nil {
143 if err != nil {
110 return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
144 return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
113− filetracker.RecordRead(ctx, sessionID, filePath)
147+ filetracker.RecordRead(ctx, sessionID, absFilePath)
115− notifyLSPs(ctx, lspManager, filePath)
149+ notifyLSPs(ctx, lspManager, absFilePath)
117− result := fmt.Sprintf("Empty file successfully created: %s", filePath)
151+ result := fmt.Sprintf("Empty file successfully created: %s", absFilePath)
118 result = fmt.Sprintf("<result>\n%s\n</result>", result)
152 result = fmt.Sprintf("<result>\n%s\n</result>", result)
119− result += getDiagnostics(filePath, lspManager)
153+ result += getDiagnostics(absFilePath, lspManager)
120 return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
154 return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
121 TouchResponseMetadata{
155 TouchResponseMetadata{
122− FilePath: filePath,
156+ FilePath: absFilePath,