package tools import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" ) func newTestShell(t *testing.T) *Shell { t.Helper() s, err := NewShell(t.TempDir(), []string{"echo", "ls", "cat", "wc", "printenv", "sleep", "false", "true", "head", "find"}, 2*time.Second, 200) if err != nil { t.Fatal(err) } return s } func run(t *testing.T, s *Shell, cmd string) (string, error) { t.Helper() b, _ := json.Marshal(map[string]string{"command": cmd}) return s.Call(context.Background(), b) } func TestShellRejectsBypassAttempts(t *testing.T) { s := newTestShell(t) for _, c := range []string{ "rm -rf x", "echo hi && rm x", "echo hi; rm x", "echo hi | sh", "false || bash", "echo $(id)", "echo `id`", "cat <(id)", "echo ${HOME}", "echo $HOME", "echo hi > f", "cat < /etc/passwd", "cat /etc/passwd", "cat ../secret", "cat a/../../secret", "ls ~", "cat --file=/etc/passwd", "sleep 10 &", "(echo hi)", "{ echo hi; }", "/bin/echo hi", "./echo hi", "echo hi\nid", "echo 'unterminated", "echo a\\;id", "find . -exec id ;", "find . -delete", "", "echo hi &&", "| echo", "e''cho hi; r''m x", } { if _, err := s.Parse(c); err == nil || !errors.Is(err, ErrRejected) { t.Errorf("%q should be rejected, got %v", c, err) } } } func TestShellNeverAllowed(t *testing.T) { for _, c := range []string{"sh", "bash", "env", "xargs", "python3", "/bin/ls"} { if _, err := NewShell(t.TempDir(), []string{c}, time.Second, 10); err == nil { t.Errorf("%q must not be allowlistable", c) } } if _, err := NewShell(t.TempDir(), nil, time.Second, 10); err == nil { t.Error("empty allowlist must fail") } } func TestShellRunsChainsAndPipes(t *testing.T) { s := newTestShell(t) os.WriteFile(filepath.Join(s.Dir, "f.txt"), []byte("a\nb\nc\n"), 0o600) out, err := run(t, s, `cat f.txt | wc -l && echo "ok yes" ; false || echo fallback`) if err != nil { t.Fatal(err) } if !strings.Contains(out, "3\nok yes\nfallback") || !strings.HasSuffix(out, "[exit 0]") { t.Fatalf("%q", out) } out, _ = run(t, s, "false && echo never") if strings.Contains(out, "never") || !strings.HasSuffix(out, "[exit 1]") { t.Fatalf("%q", out) } // metacharacters inside quotes are literal arguments, not operators out, _ = run(t, s, `echo "a && b | c ; d"`) if !strings.HasPrefix(out, "a && b | c ; d") { t.Fatalf("%q", out) } } func TestShellEnvironmentIsScrubbed(t *testing.T) { t.Setenv("BYTECLAW_AUTH_TOKEN", "supersecret") t.Setenv("DAHL_API_KEY", "dahl_secret") s := newTestShell(t) out, _ := run(t, s, "printenv") if strings.Contains(out, "secret") || !strings.Contains(out, "HOME="+s.Dir) { t.Fatalf("env leaked or wrong: %q", out) } } func TestShellRunsInWorkspace(t *testing.T) { s := newTestShell(t) os.WriteFile(filepath.Join(s.Dir, "inside.txt"), nil, 0o600) if out, _ := run(t, s, "ls"); !strings.Contains(out, "inside.txt") { t.Fatalf("%q", out) } } func TestShellTimeoutAndOutputLimit(t *testing.T) { s := newTestShell(t) s.Timeout = 300 * time.Millisecond start := time.Now() out, _ := run(t, s, "sleep 5") if time.Since(start) > 3*time.Second || !strings.Contains(out, "timeout") { t.Fatalf("timeout not enforced: %q after %s", out, time.Since(start)) } s.Timeout = 2 * time.Second out, _ = run(t, s, "echo "+strings.Repeat("x", 500)) if !strings.Contains(out, "[output truncated]") || len(out) > 300 { t.Fatalf("limit not enforced: %d", len(out)) } }