v0.2.39 8cd6818

sandbox_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close