test: sandbox coverage + backend contract tests

- sandbox: remove dead code (DefaultConfig, Load/Save, LayeredConfig, Merge,
  Version, SystemDefaults, validateName, ConfigPath, OpenSandbox); unexport
  internal symbols; LoadSandbox now takes io.Reader; loadSandboxConfig owns
  file opening; 83% package coverage via new sandbox_test.go
- backend: add BaseURL field to AnthropicBackend for testability; new
  backend_test.go runs checkContract against all five implementations
  (Ollama, OpenAI, Copilot, Anthropic, CodeWhisperer) via httptest servers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-21 13:00:06 +02:00
parent c6f6e7feeb
commit 46272f22a6
8 changed files with 692 additions and 347 deletions

View File

@ -2,6 +2,7 @@ package sandbox
import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
@ -12,7 +13,7 @@ import (
"gopkg.in/yaml.v3"
)
// Config represents the global sandbox configuration
// Config represents the sandbox configuration
type Config struct {
General GeneralConfig `yaml:"general"`
Filesystem FilesystemConfig `yaml:"filesystem"`
@ -21,13 +22,6 @@ type Config struct {
Advanced AdvancedConfig `yaml:"advanced"`
}
// LayeredConfig represents a single layer (system/backend/role/task)
type LayeredConfig struct {
Filesystem FilesystemConfig `yaml:"filesystem"`
Network NetworkConfig `yaml:"network"`
Env []string `yaml:"env"`
}
// GeneralConfig contains general sandbox settings
type GeneralConfig struct {
BestEffort bool `yaml:"best_effort"`
@ -56,148 +50,11 @@ type AdvancedConfig struct {
AddExec bool `yaml:"add_exec"`
}
// DefaultConfig returns the default locked-down configuration
func DefaultConfig() *Config {
return &Config{
General: GeneralConfig{
BestEffort: false, // Fail-closed: refuse to run if sandboxing unavailable
LogLevel: "error",
},
Filesystem: FilesystemConfig{
RO: []string{
"/etc/passwd", // UID→homedir lookup (getpwuid)
"/dev/null", // Null device
"/proc/meminfo", // Memory info
"/proc/self/cgroup", // Cgroup info
"/proc/self/maps", // Process memory maps
"/proc/version", // Kernel version
},
ROX: []string{
"/usr",
"/lib",
"/lib64",
"/bin",
"/sbin",
},
RW: []string{
"{CWD}",
"{TMPDIR}",
"{OLLIE_TMP_PATH}",
"{HOME}/.claude",
"{HOME}/.kiro",
"{HOME}/.claude.json",
},
RWX: []string{
"{OLLIE_CFG_PATH}",
"{OLLIE}",
},
},
Network: NetworkConfig{
Enabled: false,
Unrestricted: true, // Unrestricted network (fine-grained restrictions require Landlock v5+)
BindTCP: []string{},
ConnectTCP: []string{"443"},
},
Env: []string{
"HOME",
"USER",
"PATH",
"LANG",
"TERM",
"OLLIE_SESSION_ID",
"OLLIE",
"OLLIE_CFG_PATH",
"OLLIE_DATA_PATH",
"OLLIE_TMP_PATH",
"OLLIE_MEMORY_PATH",
},
Advanced: AdvancedConfig{
LDD: false,
AddExec: true,
},
}
}
// defaultSandbox is the sandbox config used when none is specified
const defaultSandbox = "default"
// ConfigPath returns the path to the sandbox config file
func ConfigPath() string {
return filepath.Join(paths.CfgDir(), "sandbox.yaml")
}
// Load loads the config from disk, creating defaults if it doesn't exist
func Load() (*Config, error) {
path := ConfigPath()
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
cfg := DefaultConfig()
if saveErr := Save(cfg); saveErr != nil {
return cfg, saveErr
}
return cfg, nil
}
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// Save saves the config to disk
func Save(cfg *Config) error {
path := ConfigPath()
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
header := `# Ollie Sandbox Configuration
# Controls landrun sandboxing for execute_code
# Documentation: https://github.com/landlock-lsm/landrun
#
# Path templates:
# {CWD} - Session working directory
# {HOME} - User home directory
# {TMPDIR} - Temporary directory (/tmp or $TMPDIR)
#
# Changes apply to NEW sessions only.
`
data = append([]byte(header), data...)
return os.WriteFile(path, data, 0644)
}
// DefaultSandbox is the sandbox config used when none is specified
const DefaultSandbox = "default"
// validateName checks if a name is safe for use in file paths
func validateName(name string) error {
if name == "" {
return fmt.Errorf("name cannot be empty")
}
if len(name) > 64 {
return fmt.Errorf("name too long (max 64 characters)")
}
for _, r := range name {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_') {
return fmt.Errorf("name contains invalid character: %c (only alphanumeric, hyphen, underscore allowed)", r)
}
}
return nil
}
// ExpandPath replaces template variables with actual values
func ExpandPath(pattern, cwd string) string {
// expandPath replaces template variables with actual values
func expandPath(pattern, cwd string) string {
s := pattern
s = strings.ReplaceAll(s, "{CWD}", cwd)
@ -256,32 +113,10 @@ func ExpandPath(pattern, cwd string) string {
return s
}
// LoadMerged loads the base config merged with a named sandbox layer.
// If the named layer doesn't exist, returns the base config alone.
func LoadMerged(name string) (*Config, error) {
baseCfg, err := Load()
if err != nil {
return nil, err
}
if name == "" {
name = "default"
}
baseLayer := LayeredConfig{
Filesystem: baseCfg.Filesystem,
Network: baseCfg.Network,
Env: baseCfg.Env,
}
layers := []LayeredConfig{baseLayer}
if sbxLayer, err := LoadSandbox(name); err == nil {
layers = append(layers, sbxLayer)
}
return Merge(baseCfg.General, baseCfg.Advanced, layers...), nil
}
// CheckPath checks if the given absolute path is allowed by the sandbox config.
// checkPath checks if the given absolute path is allowed by the sandbox config.
// If write is true, the path must fall under an RW or RWX entry.
// If write is false, any entry (RO, ROX, RW, RWX) grants access.
func CheckPath(cfg *Config, path string, write bool, cwd string) error {
func checkPath(cfg *Config, path string, write bool, cwd string) error {
cleaned := filepath.Clean(path)
if resolved, err := filepath.EvalSymlinks(cleaned); err == nil {
cleaned = resolved
@ -294,17 +129,17 @@ func CheckPath(cfg *Config, path string, write bool, cwd string) error {
var allowed []string
for _, p := range cfg.Filesystem.RW {
allowed = append(allowed, ExpandPath(p, cwd))
allowed = append(allowed, expandPath(p, cwd))
}
for _, p := range cfg.Filesystem.RWX {
allowed = append(allowed, ExpandPath(p, cwd))
allowed = append(allowed, expandPath(p, cwd))
}
if !write {
for _, p := range cfg.Filesystem.RO {
allowed = append(allowed, ExpandPath(p, cwd))
allowed = append(allowed, expandPath(p, cwd))
}
for _, p := range cfg.Filesystem.ROX {
allowed = append(allowed, ExpandPath(p, cwd))
allowed = append(allowed, expandPath(p, cwd))
}
}
@ -324,144 +159,15 @@ func pathUnder(path, root string) bool {
return path == root || strings.HasPrefix(path, root+string(filepath.Separator))
}
// SystemDefaults returns the system-level defaults (restrictive baseline)
func SystemDefaults() LayeredConfig {
return LayeredConfig{
Filesystem: FilesystemConfig{
RO: []string{
"/etc/passwd",
"/dev/null",
"/proc/meminfo",
"/proc/self/cgroup",
"/proc/self/maps",
"/proc/version",
},
ROX: []string{
"/usr",
"/lib",
"/lib64",
"/bin",
"/sbin",
},
RW: []string{
"{TMPDIR}",
},
},
Network: NetworkConfig{
Enabled: false,
Unrestricted: false,
},
Env: []string{
"HOME",
"USER",
"PATH",
"LANG",
"TERM",
"OLLIE_SESSION_ID",
"OLLIE",
"OLLIE_CFG_PATH",
"OLLIE_DATA_PATH",
},
}
}
// LoadSandbox loads a named sandbox layer config from $OLLIE_CFG_PATH/sandbox/<name>.yaml
func LoadSandbox(name string) (LayeredConfig, error) {
if err := validateName(name); err != nil {
return LayeredConfig{}, fmt.Errorf("invalid sandbox name: %w", err)
}
path := filepath.Join(paths.CfgDir(), "sandbox", name+".yaml")
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return LayeredConfig{}, fmt.Errorf("sandbox %q not found (looked in %s)", name, path)
}
// LoadSandbox parses a sandbox config from r.
func LoadSandbox(r io.Reader) (*Config, error) {
data, err := io.ReadAll(r)
if err != nil {
return LayeredConfig{}, err
return nil, err
}
var cfg LayeredConfig
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return LayeredConfig{}, fmt.Errorf("parse %s: %w", path, err)
return nil, err
}
return cfg, nil
}
// Merge combines multiple layers into a final Config (most permissive wins)
func Merge(general GeneralConfig, advanced AdvancedConfig, layers ...LayeredConfig) *Config {
cfg := &Config{
General: general,
Advanced: advanced,
Filesystem: FilesystemConfig{
RO: []string{},
ROX: []string{},
RW: []string{},
RWX: []string{},
},
Network: NetworkConfig{
Enabled: false,
Unrestricted: false,
},
Env: []string{},
}
pathPerms := make(map[string]int)
envSet := make(map[string]bool)
for _, layer := range layers {
for _, p := range layer.Filesystem.RO {
if pathPerms[p] < 1 {
pathPerms[p] = 1
}
}
for _, p := range layer.Filesystem.ROX {
if pathPerms[p] < 2 {
pathPerms[p] = 2
}
}
for _, p := range layer.Filesystem.RW {
if pathPerms[p] < 3 {
pathPerms[p] = 3
}
}
for _, p := range layer.Filesystem.RWX {
if pathPerms[p] < 4 {
pathPerms[p] = 4
}
}
if layer.Network.Enabled {
cfg.Network.Enabled = true
}
if layer.Network.Unrestricted {
cfg.Network.Unrestricted = true
}
cfg.Network.BindTCP = append(cfg.Network.BindTCP, layer.Network.BindTCP...)
cfg.Network.ConnectTCP = append(cfg.Network.ConnectTCP, layer.Network.ConnectTCP...)
for _, e := range layer.Env {
envSet[e] = true
}
}
for path, perm := range pathPerms {
switch perm {
case 1:
cfg.Filesystem.RO = append(cfg.Filesystem.RO, path)
case 2:
cfg.Filesystem.ROX = append(cfg.Filesystem.ROX, path)
case 3:
cfg.Filesystem.RW = append(cfg.Filesystem.RW, path)
case 4:
cfg.Filesystem.RWX = append(cfg.Filesystem.RWX, path)
}
}
for e := range envSet {
cfg.Env = append(cfg.Env, e)
}
return cfg
return &cfg, nil
}

View File

@ -2,7 +2,6 @@ package sandbox
import (
"os/exec"
"strings"
"sync"
)
@ -11,27 +10,12 @@ var (
availableOnce sync.Once
)
// IsAvailable checks if landrun is available on the system.
// isAvailable checks if landrun is available on the system.
// The result is cached after the first call for performance.
func IsAvailable() bool {
func isAvailable() bool {
availableOnce.Do(func() {
_, err := exec.LookPath("landrun")
available = (err == nil)
})
return available
}
// Version returns the landrun version string, or empty if unavailable.
func Version() string {
if !IsAvailable() {
return ""
}
cmd := exec.Command("landrun", "--version")
out, err := cmd.Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}

View File

@ -0,0 +1,505 @@
package sandbox
import (
"bytes"
"os"
"path/filepath"
"testing"
)
// TestMain creates a fake landrun binary so IsAvailable() returns true for all tests.
func TestMain(m *testing.M) {
tmpDir, err := os.MkdirTemp("", "sandbox-test-*")
if err != nil {
panic(err)
}
fakeLandrun := filepath.Join(tmpDir, "landrun")
if err := os.WriteFile(fakeLandrun, []byte("#!/bin/sh\necho 'landrun 0.0.0'\n"), 0755); err != nil {
panic(err)
}
os.Setenv("PATH", tmpDir+":"+os.Getenv("PATH"))
os.Exit(m.Run())
}
// ---- expandPath ----
func TestExpandPath(t *testing.T) {
tests := []struct {
name string
pattern string
cwd string
env map[string]string
want string
}{
{
name: "literal",
pattern: "/etc/passwd",
cwd: "/cwd",
want: "/etc/passwd",
},
{
name: "CWD",
pattern: "{CWD}/foo",
cwd: "/my/cwd",
want: "/my/cwd/foo",
},
{
name: "TMPDIR from env",
pattern: "{TMPDIR}",
cwd: "/cwd",
env: map[string]string{"TMPDIR": "/custom/tmp"},
want: "/custom/tmp",
},
{
name: "TMPDIR default",
pattern: "{TMPDIR}",
cwd: "/cwd",
env: map[string]string{"TMPDIR": ""},
want: "/tmp",
},
{
name: "custom env var",
pattern: "{MY_SANDBOX_TEST_VAR}/bar",
cwd: "/cwd",
env: map[string]string{"MY_SANDBOX_TEST_VAR": "/custom"},
want: "/custom/bar",
},
{
name: "unknown var unchanged",
pattern: "{NONEXISTENT_SANDBOX_VAR_XYZ}",
cwd: "/cwd",
want: "{NONEXISTENT_SANDBOX_VAR_XYZ}",
},
{
name: "XDG_CONFIG_HOME from env",
pattern: "{XDG_CONFIG_HOME}/app",
cwd: "/cwd",
env: map[string]string{"XDG_CONFIG_HOME": "/xdg/config"},
want: "/xdg/config/app",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
for k, v := range tc.env {
t.Setenv(k, v)
}
got := expandPath(tc.pattern, tc.cwd)
if got != tc.want {
t.Errorf("expandPath(%q, %q) = %q, want %q", tc.pattern, tc.cwd, got, tc.want)
}
})
}
}
// ---- WrapCommand ----
func TestWrapCommand(t *testing.T) {
tmpDir := t.TempDir()
rwDir := filepath.Join(tmpDir, "rw")
roDir := filepath.Join(tmpDir, "ro")
if err := os.MkdirAll(rwDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(roDir, 0755); err != nil {
t.Fatal(err)
}
t.Run("starts with landrun", func(t *testing.T) {
cfg := &Config{General: GeneralConfig{}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
got := WrapCommand(cfg, []string{"echo", "hi"}, tmpDir)
if len(got) == 0 || got[0] != "landrun" {
t.Errorf("expected landrun as first arg, got %v", got)
}
})
t.Run("separator before original command", func(t *testing.T) {
cfg := &Config{General: GeneralConfig{}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
got := WrapCommand(cfg, []string{"echo", "test"}, tmpDir)
sepIdx := indexOf(got, "--")
if sepIdx == -1 {
t.Fatal("missing -- separator")
}
if got[sepIdx+1] != "echo" || got[sepIdx+2] != "test" {
t.Errorf("expected [echo test] after --, got %v", got[sepIdx+1:])
}
})
t.Run("log level flag", func(t *testing.T) {
cfg := &Config{General: GeneralConfig{LogLevel: "debug"}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertFlagValue(t, got, "--log-level", "debug")
})
t.Run("no log level when empty", func(t *testing.T) {
cfg := &Config{General: GeneralConfig{LogLevel: ""}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertDoesNotContain(t, got, "--log-level")
})
t.Run("best effort flag", func(t *testing.T) {
cfg := &Config{General: GeneralConfig{BestEffort: true}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertContains(t, got, "--best-effort")
})
t.Run("no best effort when false", func(t *testing.T) {
cfg := &Config{General: GeneralConfig{BestEffort: false}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertDoesNotContain(t, got, "--best-effort")
})
t.Run("ldd flag", func(t *testing.T) {
cfg := &Config{General: GeneralConfig{}, Advanced: AdvancedConfig{LDD: true}, Network: NetworkConfig{}}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertContains(t, got, "--ldd")
})
t.Run("add-exec flag", func(t *testing.T) {
cfg := &Config{General: GeneralConfig{}, Advanced: AdvancedConfig{AddExec: true}, Network: NetworkConfig{}}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertContains(t, got, "--add-exec")
})
t.Run("rw path included when exists", func(t *testing.T) {
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Filesystem: FilesystemConfig{RW: []string{rwDir}},
Network: NetworkConfig{},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertFlagValue(t, got, "--rw", rwDir)
})
t.Run("ro path included when exists", func(t *testing.T) {
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Filesystem: FilesystemConfig{RO: []string{roDir}},
Network: NetworkConfig{},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertFlagValue(t, got, "--ro", roDir)
})
t.Run("nonexistent path excluded", func(t *testing.T) {
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Filesystem: FilesystemConfig{RW: []string{"/nonexistent/path/that/does/not/exist/xyz"}},
Network: NetworkConfig{},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertDoesNotContain(t, got, "--rw")
})
t.Run("CWD template expanded to real path", func(t *testing.T) {
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Filesystem: FilesystemConfig{RW: []string{"{CWD}"}},
Network: NetworkConfig{},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertFlagValue(t, got, "--rw", tmpDir)
})
t.Run("unrestricted network", func(t *testing.T) {
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Network: NetworkConfig{Enabled: true, Unrestricted: true},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertContains(t, got, "--unrestricted-network")
assertDoesNotContain(t, got, "--connect-tcp")
assertDoesNotContain(t, got, "--bind-tcp")
})
t.Run("restricted network ports", func(t *testing.T) {
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Network: NetworkConfig{
Enabled: true,
ConnectTCP: []string{"443", "80"},
BindTCP: []string{"8080"},
},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertFlagValue(t, got, "--connect-tcp", "443")
assertFlagValue(t, got, "--connect-tcp", "80")
assertFlagValue(t, got, "--bind-tcp", "8080")
assertDoesNotContain(t, got, "--unrestricted-network")
})
t.Run("network disabled ignores ports", func(t *testing.T) {
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Network: NetworkConfig{Enabled: false, ConnectTCP: []string{"443"}},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertDoesNotContain(t, got, "--connect-tcp")
assertDoesNotContain(t, got, "--unrestricted-network")
})
t.Run("env vars", func(t *testing.T) {
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Network: NetworkConfig{},
Env: []string{"HOME", "PATH"},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
assertFlagValue(t, got, "--env", "HOME")
assertFlagValue(t, got, "--env", "PATH")
})
t.Run("parent path sorted before child", func(t *testing.T) {
parent := filepath.Join(tmpDir, "parent")
child := filepath.Join(tmpDir, "parent", "child")
if err := os.MkdirAll(child, 0755); err != nil {
t.Fatal(err)
}
cfg := &Config{
General: GeneralConfig{},
Advanced: AdvancedConfig{},
Filesystem: FilesystemConfig{RW: []string{child, parent}}, // child listed first
Network: NetworkConfig{},
}
got := WrapCommand(cfg, []string{"sh"}, tmpDir)
parentIdx := indexOf(got, parent)
childIdx := indexOf(got, child)
if parentIdx == -1 || childIdx == -1 {
t.Fatal("parent or child path missing from args")
}
if parentIdx > childIdx {
t.Errorf("parent (idx %d) should appear before child (idx %d) in args", parentIdx, childIdx)
}
})
}
// ---- checkPath ----
func TestCheckPath(t *testing.T) {
tmpDir := t.TempDir()
rwDir := filepath.Join(tmpDir, "rw")
roDir := filepath.Join(tmpDir, "ro")
if err := os.MkdirAll(rwDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(roDir, 0755); err != nil {
t.Fatal(err)
}
cfg := &Config{
Filesystem: FilesystemConfig{
RO: []string{roDir},
RW: []string{rwDir},
},
}
tests := []struct {
name string
path string
write bool
wantErr bool
}{
{"rw path read allowed", filepath.Join(rwDir, "file.txt"), false, false},
{"rw path write allowed", filepath.Join(rwDir, "file.txt"), true, false},
{"ro path read allowed", filepath.Join(roDir, "file.txt"), false, false},
{"ro path write denied", filepath.Join(roDir, "file.txt"), true, true},
{"outside path read denied", "/outside/path/xyz", false, true},
{"outside path write denied", "/outside/path/xyz", true, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := checkPath(cfg, tc.path, tc.write, tmpDir)
if (err != nil) != tc.wantErr {
t.Errorf("checkPath(%q, write=%v) error = %v, wantErr %v", tc.path, tc.write, err, tc.wantErr)
}
})
}
t.Run("CWD template resolved", func(t *testing.T) {
cfg2 := &Config{Filesystem: FilesystemConfig{RW: []string{"{CWD}"}}}
err := checkPath(cfg2, filepath.Join(tmpDir, "newfile.txt"), true, tmpDir)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
t.Run("RWX grants write access", func(t *testing.T) {
rwxDir := filepath.Join(tmpDir, "rwx")
if err := os.MkdirAll(rwxDir, 0755); err != nil {
t.Fatal(err)
}
cfg3 := &Config{Filesystem: FilesystemConfig{RWX: []string{rwxDir}}}
if err := checkPath(cfg3, filepath.Join(rwxDir, "bin"), true, tmpDir); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
// ---- LoadSandbox ----
func TestLoadSandbox(t *testing.T) {
t.Run("full config", func(t *testing.T) {
yaml := []byte(`
general:
best_effort: true
log_level: debug
filesystem:
ro:
- /etc/passwd
rox:
- /usr
rw:
- "{CWD}"
rwx:
- "{OLLIE_CFG_PATH}"
network:
enabled: true
unrestricted: false
bind_tcp:
- "8080"
connect_tcp:
- "443"
env:
- HOME
- PATH
advanced:
ldd: true
add_exec: false
`)
cfg, err := LoadSandbox(bytes.NewReader(yaml))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !cfg.General.BestEffort {
t.Error("BestEffort should be true")
}
if cfg.General.LogLevel != "debug" {
t.Errorf("LogLevel = %q, want %q", cfg.General.LogLevel, "debug")
}
if !containsStr(cfg.Filesystem.RO, "/etc/passwd") {
t.Error("RO missing /etc/passwd")
}
if !containsStr(cfg.Filesystem.ROX, "/usr") {
t.Error("ROX missing /usr")
}
if !containsStr(cfg.Filesystem.RW, "{CWD}") {
t.Error("RW missing {CWD}")
}
if !containsStr(cfg.Filesystem.RWX, "{OLLIE_CFG_PATH}") {
t.Error("RWX missing {OLLIE_CFG_PATH}")
}
if !cfg.Network.Enabled {
t.Error("Network.Enabled should be true")
}
if cfg.Network.Unrestricted {
t.Error("Network.Unrestricted should be false")
}
if !containsStr(cfg.Network.BindTCP, "8080") {
t.Error("BindTCP missing 8080")
}
if !containsStr(cfg.Network.ConnectTCP, "443") {
t.Error("ConnectTCP missing 443")
}
if !containsStr(cfg.Env, "HOME") || !containsStr(cfg.Env, "PATH") {
t.Error("Env missing HOME or PATH")
}
if !cfg.Advanced.LDD {
t.Error("Advanced.LDD should be true")
}
if cfg.Advanced.AddExec {
t.Error("Advanced.AddExec should be false")
}
})
t.Run("empty yaml produces zero config", func(t *testing.T) {
cfg, err := LoadSandbox(bytes.NewReader(nil))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.General.BestEffort || cfg.General.LogLevel != "" {
t.Error("expected zero GeneralConfig")
}
if len(cfg.Filesystem.RW) != 0 || len(cfg.Env) != 0 {
t.Error("expected empty filesystem and env")
}
})
t.Run("invalid yaml returns error", func(t *testing.T) {
_, err := LoadSandbox(bytes.NewReader([]byte("{ not: valid: yaml: [")))
if err == nil {
t.Error("expected error for invalid yaml")
}
})
t.Run("partial config leaves other fields zero", func(t *testing.T) {
yaml := []byte(`
env:
- TERM
`)
cfg, err := LoadSandbox(bytes.NewReader(yaml))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !containsStr(cfg.Env, "TERM") {
t.Error("Env missing TERM")
}
if len(cfg.Filesystem.RW) != 0 {
t.Error("expected empty RW")
}
})
}
// ---- helpers ----
func assertContains(t *testing.T, args []string, s string) {
t.Helper()
if indexOf(args, s) == -1 {
t.Errorf("args %v does not contain %q", args, s)
}
}
func assertDoesNotContain(t *testing.T, args []string, s string) {
t.Helper()
if indexOf(args, s) != -1 {
t.Errorf("args %v should not contain %q", args, s)
}
}
func assertFlagValue(t *testing.T, args []string, flag, value string) {
t.Helper()
for i, a := range args {
if a == flag && i+1 < len(args) && args[i+1] == value {
return
}
}
t.Errorf("args %v missing %s %s", args, flag, value)
}
func indexOf(args []string, s string) int {
for i, a := range args {
if a == s {
return i
}
}
return -1
}
func containsStr(ss []string, s string) bool {
for _, x := range ss {
if x == s {
return true
}
}
return false
}

View File

@ -17,7 +17,7 @@ type pathEntry struct {
// WrapCommand wraps a command with landrun based on the configuration.
// Returns the wrapped command args, or the original command if landrun is unavailable.
func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string {
if !IsAvailable() {
if !isAvailable() {
return originalCmd
}
@ -40,25 +40,25 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string {
var entries []pathEntry
for _, path := range cfg.Filesystem.RO {
expanded := ExpandPath(path, cwd)
expanded := expandPath(path, cwd)
if pathExists(expanded) {
entries = append(entries, pathEntry{expanded, "--ro"})
}
}
for _, path := range cfg.Filesystem.ROX {
expanded := ExpandPath(path, cwd)
expanded := expandPath(path, cwd)
if pathExists(expanded) {
entries = append(entries, pathEntry{expanded, "--rox"})
}
}
for _, path := range cfg.Filesystem.RW {
expanded := ExpandPath(path, cwd)
expanded := expandPath(path, cwd)
if pathExists(expanded) {
entries = append(entries, pathEntry{expanded, "--rw"})
}
}
for _, path := range cfg.Filesystem.RWX {
expanded := ExpandPath(path, cwd)
expanded := expandPath(path, cwd)
if pathExists(expanded) {
entries = append(entries, pathEntry{expanded, "--rwx"})
}
@ -95,7 +95,7 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string {
args = append(args, originalCmd...)
// Wrap with superpowers run-session if superpowerd is running
if IsSuperpowerdRunning() {
if isSuperpowerdRunning() {
if superpowersPath, err := exec.LookPath("superpowers"); err == nil {
args = append([]string{args[0], "--env", "SUPERPOWERD_SESSION_TOKEN"}, args[1:]...)
args = append([]string{superpowersPath, "run-session", "--"}, args...)
@ -105,8 +105,8 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string {
return args
}
// IsSuperpowerdRunning checks if superpowerd is running by testing socket connectivity
func IsSuperpowerdRunning() bool {
// isSuperpowerdRunning checks if superpowerd is running by testing socket connectivity
func isSuperpowerdRunning() bool {
socketDir := os.Getenv("SUPERPOWERD_SOCKET_DIR")
if socketDir == "" {
socketDir = os.Getenv("XDG_RUNTIME_DIR")

View File

@ -15,13 +15,14 @@ const anthropicDefaultMaxTokens = 8192
// AnthropicBackend speaks the Anthropic Messages API.
type AnthropicBackend struct {
apiKey string
model string
client *http.Client
apiKey string
model string
client *http.Client
BaseURL string // default: https://api.anthropic.com; override for testing
}
func NewAnthropic(apiKey string) *AnthropicBackend {
b := &AnthropicBackend{apiKey: apiKey, client: &http.Client{}}
b := &AnthropicBackend{apiKey: apiKey, client: &http.Client{}, BaseURL: "https://api.anthropic.com"}
b.model = b.DefaultModel()
return b
}
@ -110,7 +111,7 @@ func (b *AnthropicBackend) ChatStream(ctx context.Context, messages []Message, t
return nil, err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.anthropic.com/v1/messages", bytes.NewReader(data))
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, b.BaseURL+"/v1/messages", bytes.NewReader(data))
if err != nil {
return nil, err
}

135
pkg/backend/backend_test.go Normal file
View File

@ -0,0 +1,135 @@
package backend_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"ollie/pkg/backend"
)
// stubBackend is a minimal Backend used to verify the contract.
type stubBackend struct {
model string
}
func (s *stubBackend) Name() string { return "stub" }
func (s *stubBackend) DefaultModel() string { return "stub-default" }
func (s *stubBackend) Model() string { return s.model }
func (s *stubBackend) SetModel(m string) { s.model = m }
func (s *stubBackend) ContextLength(_ context.Context) int { return 4096 }
func (s *stubBackend) Models(_ context.Context) []string { return []string{"stub-default"} }
func (s *stubBackend) ChatStream(_ context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) {
ch := make(chan backend.StreamEvent, 1)
ch <- backend.StreamEvent{Done: true, StopReason: "end_turn"}
close(ch)
return ch, nil
}
// checkContract verifies Backend invariants. Call it with any Backend implementation.
func checkContract(t *testing.T, b backend.Backend) {
t.Helper()
if b.Name() == "" {
t.Error("Name() must be non-empty")
}
if b.DefaultModel() == "" {
t.Error("DefaultModel() must be non-empty")
}
b.SetModel("test-model")
if got := b.Model(); got != "test-model" {
t.Errorf("Model() = %q after SetModel; want %q", got, "test-model")
}
msgs := []backend.Message{{Role: "user", Content: "ping"}}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ch, err := b.ChatStream(ctx, msgs, nil, backend.GenerationParams{})
if err != nil {
t.Fatalf("ChatStream returned error: %v", err)
}
var gotDone bool
for ev := range ch {
if ev.Done {
gotDone = true
}
}
if !gotDone {
t.Error("ChatStream channel closed without a Done==true event")
}
}
func TestStubBackendContract(t *testing.T) {
checkContract(t, &stubBackend{model: "stub-default"})
}
func TestOllamaContract(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-ndjson")
w.Write([]byte(`{"done":true,"done_reason":"stop","message":{"role":"assistant","content":""}}` + "\n"))
}))
defer srv.Close()
checkContract(t, backend.NewOllama(srv.URL))
}
func TestOpenAIContract(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Write([]byte("data: [DONE]\n\n"))
}))
defer srv.Close()
checkContract(t, backend.NewOpenAI("openai", srv.URL, "fake-key"))
}
func TestCopilotContract(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Write([]byte("data: [DONE]\n\n"))
}))
defer srv.Close()
// Copilot is an OpenAI backend; exercise it via NewOpenAI with the copilot name.
checkContract(t, backend.NewOpenAI("copilot", srv.URL, "fake-token"))
}
func TestAnthropicContract(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Write([]byte(
"event: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":0}}\n\n" +
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
))
}))
defer srv.Close()
b := backend.NewAnthropic("fake-key")
b.BaseURL = srv.URL
checkContract(t, b)
}
func TestCodeWhispererContract(t *testing.T) {
// Static auth has no DefaultEndpoint, so ChatStream terminates immediately
// with Done==true via the endpoint-resolution error path. This verifies that
// the contract invariant — stream always terminates with Done — holds even
// on error paths.
b, err := backend.NewCodeWhisperer("fake-token")
if err != nil {
t.Fatalf("NewCodeWhisperer: %v", err)
}
checkContract(t, b)
}
func TestRateLimitError(t *testing.T) {
e := &backend.RateLimitError{RetryAfter: 30 * time.Second, Message: "quota exceeded"}
want := "rate limited (retry after 30s): quota exceeded"
if got := e.Error(); got != want {
t.Errorf("Error() = %q; want %q", got, want)
}
e2 := &backend.RateLimitError{Message: "over limit"}
want2 := "rate limited: over limit"
if got := e2.Error(); got != want2 {
t.Errorf("Error() = %q; want %q", got, want2)
}
}

View File

@ -5,7 +5,12 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"ollie/internal/sandbox"
"ollie/pkg/paths"
"regexp"
"strings"
)
@ -132,8 +137,17 @@ func (e *Server) ValidateCode(code, language string) error {
return nil
}
func loadLayeredConfig(name string) (*sandbox.Config, error) {
return sandbox.LoadMerged(name)
func loadSandboxConfig(name string) (*sandbox.Config, error) {
if name == "" {
name = "default"
}
path := filepath.Join(paths.CfgDir(), "sandbox", name+".yaml")
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("sandbox %q not found: %w", name, err)
}
defer f.Close()
return sandbox.LoadSandbox(f)
}
type limitedWriter struct {

View File

@ -278,7 +278,7 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
}
}
cfg, err := loadLayeredConfig(sandboxName)
cfg, err := loadSandboxConfig(sandboxName)
if err != nil {
return "", err
}