/ internal / tools / safe_path.go
safe_path.go
 1  package tools
 2  
 3  import (
 4  	"context"
 5  	"os"
 6  	"path/filepath"
 7  	"strings"
 8  
 9  	"github.com/Kocoro-lab/ShanClaw/internal/cwdctx"
10  )
11  
12  // ExpandHome expands a leading ~ in a path to the user's home directory.
13  // Returns the path unchanged if it doesn't start with ~.
14  func ExpandHome(path string) string {
15  	if !strings.HasPrefix(path, "~") {
16  		return path
17  	}
18  	home, err := os.UserHomeDir()
19  	if err != nil {
20  		return path
21  	}
22  	if path == "~" {
23  		return home
24  	}
25  	if strings.HasPrefix(path, "~/") {
26  		return filepath.Join(home, path[2:])
27  	}
28  	return path
29  }
30  
31  // isPathUnderSessionCWD returns true if the given path resolves to a location
32  // under the session CWD from context. Falls back to isPathUnderCWD if no
33  // session CWD is set.
34  func isPathUnderSessionCWD(ctx context.Context, path string) bool {
35  	if cwdctx.FromContext(ctx) != "" {
36  		return cwdctx.IsUnderSessionCWD(ctx, path)
37  	}
38  	return isPathUnderCWD(path)
39  }
40  
41  // isPathUnderCWD returns true if the given path resolves to a location
42  // under the current working directory. Used by read-only tools to
43  // auto-approve safe paths.
44  func isPathUnderCWD(path string) bool {
45  	if path == "" || path == "." {
46  		return true
47  	}
48  
49  	cwd, err := os.Getwd()
50  	if err != nil {
51  		return false
52  	}
53  
54  	// Expand ~ before resolving
55  	path = ExpandHome(path)
56  
57  	// Resolve the path to absolute
58  	absPath := path
59  	if !filepath.IsAbs(path) {
60  		absPath = filepath.Join(cwd, path)
61  	}
62  	absPath = filepath.Clean(absPath)
63  
64  	cwdClean := filepath.Clean(cwd)
65  	if absPath == cwdClean {
66  		return true
67  	}
68  	return strings.HasPrefix(absPath, cwdClean+string(filepath.Separator))
69  }