mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
#38: emit OSC 7 (ESC ]7;file://host/path) on TUI setup and /cd so terminals like kitty open new tabs/splits in the launch cwd instead of inheriting a stale extension-subprocess directory. Verified end-to-end against kitty 0.46.2. #39: stop blanket-rejecting cd into subdirectories of the sandbox root. CheckCommand now resolves the cd target and rejects only real escapes. Add Sandbox.DisplayPath to present jailed tool-result/error paths relative to root, reducing the absolute-path bias that pushed the model toward unjailing.
146 lines
3.9 KiB
Go
146 lines
3.9 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestSandboxLockedBlocksOutside(t *testing.T) {
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
outsideFile := filepath.Join(outside, "a.txt")
|
|
os.WriteFile(outsideFile, []byte("secret"), 0o644)
|
|
|
|
sb := NewSandbox(root)
|
|
sb.Lock()
|
|
|
|
if err := sb.CheckPath(outsideFile); err == nil {
|
|
t.Fatal("expected outside path to be blocked")
|
|
}
|
|
inside := filepath.Join(root, "ok.txt")
|
|
if err := sb.CheckPath(inside); err != nil {
|
|
t.Fatalf("inside path blocked unexpectedly: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSandboxUnlockedAllows(t *testing.T) {
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
sb := NewSandbox(root)
|
|
if err := sb.CheckPath(filepath.Join(outside, "a.txt")); err != nil {
|
|
t.Fatalf("unlocked should allow: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSandboxCommandBanned(t *testing.T) {
|
|
sb := NewSandbox(t.TempDir())
|
|
sb.Lock()
|
|
cases := []string{
|
|
"sudo apt-get install foo",
|
|
"rm -rf /",
|
|
"cd /etc && ls",
|
|
"cd .. && rm foo",
|
|
}
|
|
for _, c := range cases {
|
|
if err := sb.CheckCommand(c); err == nil {
|
|
t.Fatalf("expected %q to be banned", c)
|
|
}
|
|
}
|
|
// Allowed:
|
|
for _, c := range []string{"ls", "go test ./...", "cd subdir && ls"} {
|
|
if err := sb.CheckCommand(c); err != nil {
|
|
t.Fatalf("expected %q to be allowed: %v", c, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSandboxAllowsCDIntoSubdir is the regression for issue #39: a `cd`
|
|
// into a subdirectory of the sandbox root, spelled as an absolute path,
|
|
// must be allowed. The old guard rejected any `cd /...` outright, which
|
|
// wasted turns and nudged the model toward trying to break out of jail.
|
|
func TestSandboxAllowsCDIntoSubdir(t *testing.T) {
|
|
root := t.TempDir()
|
|
sub := filepath.Join(root, "packages", "provider")
|
|
if err := os.MkdirAll(sub, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sb := NewSandbox(root)
|
|
sb.Lock()
|
|
|
|
allowed := []string{
|
|
"cd " + sub + " && go build ./...",
|
|
"cd " + root + " && go build ./...",
|
|
"cd " + root, // bare cd to root
|
|
"cd packages/provider && go build",
|
|
"cd \"" + sub + "\" && ls", // quoted absolute path
|
|
}
|
|
for _, c := range allowed {
|
|
if err := sb.CheckCommand(c); err != nil {
|
|
t.Fatalf("expected %q to be allowed: %v", c, err)
|
|
}
|
|
}
|
|
|
|
blocked := []string{
|
|
"cd /etc",
|
|
"cd / && ls",
|
|
"cd ..", // parent of root escapes
|
|
"cd " + filepath.Dir(root), // explicit parent
|
|
}
|
|
for _, c := range blocked {
|
|
if err := sb.CheckCommand(c); err == nil {
|
|
t.Fatalf("expected %q to be blocked", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSandboxDisplayPath covers issue #39's secondary cause: tool
|
|
// results / errors should present paths relative to the sandbox root
|
|
// when jailed, so the model isn't biased toward absolute paths.
|
|
func TestSandboxDisplayPath(t *testing.T) {
|
|
root := t.TempDir()
|
|
sub := filepath.Join(root, "pkg", "foo.go")
|
|
outside := filepath.Join(t.TempDir(), "x.go")
|
|
|
|
sb := NewSandbox(root)
|
|
|
|
// Unlocked: returns the given form verbatim.
|
|
if got := sb.DisplayPath(sub, "pkg/foo.go"); got != "pkg/foo.go" {
|
|
t.Fatalf("unlocked DisplayPath = %q; want verbatim", got)
|
|
}
|
|
|
|
sb.Lock()
|
|
if got := sb.DisplayPath(sub, sub); got != "./pkg/foo.go" {
|
|
t.Fatalf("DisplayPath(abs inside) = %q; want ./pkg/foo.go", got)
|
|
}
|
|
if got := sb.DisplayPath(root, root); got != "." {
|
|
t.Fatalf("DisplayPath(root) = %q; want .", got)
|
|
}
|
|
// Outside root: fall back to the given form (don't fabricate a path).
|
|
if got := sb.DisplayPath(outside, "x.go"); got != "x.go" {
|
|
t.Fatalf("DisplayPath(outside) = %q; want given fallback", got)
|
|
}
|
|
}
|
|
|
|
func TestReadToolRejectsOutsideWhenLocked(t *testing.T) {
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
outsideFile := filepath.Join(outside, "a.txt")
|
|
os.WriteFile(outsideFile, []byte("x"), 0o644)
|
|
|
|
sb := NewSandbox(root)
|
|
sb.Lock()
|
|
tool := &ReadTool{CWD: root, Sandbox: sb}
|
|
|
|
_, err := tool.Execute(context.Background(),
|
|
mustJSONRaw(t, map[string]any{"path": outsideFile}), nil)
|
|
if err == nil {
|
|
t.Fatal("expected sandbox error")
|
|
}
|
|
}
|
|
|
|
func mustJSONRaw(t *testing.T, v any) []byte {
|
|
t.Helper()
|
|
return mustJSON(t, v)
|
|
}
|