1package tools
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8)
9
10func TestSandboxLockedBlocksOutside(t *testing.T) {
11 root := t.TempDir()
12 outside := t.TempDir()
13 outsideFile := filepath.Join(outside, "a.txt")
14 os.WriteFile(outsideFile, []byte("secret"), 0o644)
15
16 sb := NewSandbox(root)
17 sb.Lock()
18
19 if err := sb.CheckPath(outsideFile); err == nil {
20 t.Fatal("expected outside path to be blocked")
21 }
22 inside := filepath.Join(root, "ok.txt")
23 if err := sb.CheckPath(inside); err != nil {
24 t.Fatalf("inside path blocked unexpectedly: %v", err)
25 }
26}
27
28func TestSandboxUnlockedAllows(t *testing.T) {
29 root := t.TempDir()
30 outside := t.TempDir()
31 sb := NewSandbox(root)
32 if err := sb.CheckPath(filepath.Join(outside, "a.txt")); err != nil {
33 t.Fatalf("unlocked should allow: %v", err)
34 }
35}
36
37func TestSandboxCommandBanned(t *testing.T) {
38 sb := NewSandbox(t.TempDir())
39 sb.Lock()
40 cases := []string{
41 "sudo apt-get install foo",
42 "rm -rf /",
43 "cd /etc && ls",
44 "cd .. && rm foo",
45 }
46 for _, c := range cases {
47 if err := sb.CheckCommand(c); err == nil {
48 t.Fatalf("expected %q to be banned", c)
49 }
50 }
51 // Allowed:
52 for _, c := range []string{"ls", "go test ./...", "cd subdir && ls"} {
53 if err := sb.CheckCommand(c); err != nil {
54 t.Fatalf("expected %q to be allowed: %v", c, err)
55 }
56 }
57}
58
59// TestSandboxAllowsCDIntoSubdir is the regression for issue #39: a `cd`
60// into a subdirectory of the sandbox root, spelled as an absolute path,
61// must be allowed. The old guard rejected any `cd /...` outright, which
62// wasted turns and nudged the model toward trying to break out of jail.
63func TestSandboxAllowsCDIntoSubdir(t *testing.T) {
64 root := t.TempDir()
65 sub := filepath.Join(root, "packages", "provider")
66 if err := os.MkdirAll(sub, 0o755); err != nil {
67 t.Fatal(err)
68 }
69 sb := NewSandbox(root)
70 sb.Lock()
71
72 allowed := []string{
73 "cd " + sub + " && go build ./...",
74 "cd " + root + " && go build ./...",
75 "cd " + root, // bare cd to root
76 "cd packages/provider && go build",
77 "cd \"" + sub + "\" && ls", // quoted absolute path
78 }
79 for _, c := range allowed {
80 if err := sb.CheckCommand(c); err != nil {
81 t.Fatalf("expected %q to be allowed: %v", c, err)
82 }
83 }
84
85 blocked := []string{
86 "cd /etc",
87 "cd / && ls",
88 "cd ..", // parent of root escapes
89 "cd " + filepath.Dir(root), // explicit parent
90 }
91 for _, c := range blocked {
92 if err := sb.CheckCommand(c); err == nil {
93 t.Fatalf("expected %q to be blocked", c)
94 }
95 }
96}
97
98// TestSandboxDisplayPath covers issue #39's secondary cause: tool
99// results / errors should present paths relative to the sandbox root
100// when jailed, so the model isn't biased toward absolute paths.
101func TestSandboxDisplayPath(t *testing.T) {
102 root := t.TempDir()
103 sub := filepath.Join(root, "pkg", "foo.go")
104 outside := filepath.Join(t.TempDir(), "x.go")
105
106 sb := NewSandbox(root)
107
108 // Unlocked: returns the given form verbatim.
109 if got := sb.DisplayPath(sub, "pkg/foo.go"); got != "pkg/foo.go" {
110 t.Fatalf("unlocked DisplayPath = %q; want verbatim", got)
111 }
112
113 sb.Lock()
114 if got := sb.DisplayPath(sub, sub); got != "./pkg/foo.go" {
115 t.Fatalf("DisplayPath(abs inside) = %q; want ./pkg/foo.go", got)
116 }
117 if got := sb.DisplayPath(root, root); got != "." {
118 t.Fatalf("DisplayPath(root) = %q; want .", got)
119 }
120 // Outside root: fall back to the given form (don't fabricate a path).
121 if got := sb.DisplayPath(outside, "x.go"); got != "x.go" {
122 t.Fatalf("DisplayPath(outside) = %q; want given fallback", got)
123 }
124}
125
126func TestReadToolRejectsOutsideWhenLocked(t *testing.T) {
127 root := t.TempDir()
128 outside := t.TempDir()
129 outsideFile := filepath.Join(outside, "a.txt")
130 os.WriteFile(outsideFile, []byte("x"), 0o644)
131
132 sb := NewSandbox(root)
133 sb.Lock()
134 tool := &ReadTool{CWD: root, Sandbox: sb}
135
136 _, err := tool.Execute(context.Background(),
137 mustJSONRaw(t, map[string]any{"path": outsideFile}), nil)
138 if err == nil {
139 t.Fatal("expected sandbox error")
140 }
141}
142
143func mustJSONRaw(t *testing.T, v any) []byte {
144 t.Helper()
145 return mustJSON(t, v)
146}