From b21c56b96043e00e4f0c13207f54e435d822f461 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 6 Mar 2026 10:18:31 +0100 Subject: [PATCH] anvilmcp: use layered sandbox config from sandbox package Reuse the same layered config approach as anvilsrv: - global.yaml provides base permissions (including /usr, /lib, /bin) - sandbox/.yaml adds sandbox-specific permissions - Merge uses most-permissive-wins strategy This fixes the fallback to default sandbox, which now has proper system paths from global.yaml. --- cmd/anvilmcp/execute.go | 145 +++++++++++------------------------- internal/sandbox/config.go | 4 +- internal/sandbox/wrapper.go | 8 +- 3 files changed, 49 insertions(+), 108 deletions(-) diff --git a/cmd/anvilmcp/execute.go b/cmd/anvilmcp/execute.go index 4838a0d..e090f51 100644 --- a/cmd/anvilmcp/execute.go +++ b/cmd/anvilmcp/execute.go @@ -13,7 +13,7 @@ import ( "sync" "time" - "gopkg.in/yaml.v3" + "anvillm/internal/sandbox" ) var dangerousPatterns = []*regexp.Regexp{ @@ -117,95 +117,47 @@ func validateCode(code string) error { return nil } -// SandboxConfig represents the sandbox configuration loaded from YAML -type SandboxConfig struct { - CWD string `yaml:"cwd"` // "temp" for temp workspace, or path template like "{CWD}" - Filesystem struct { - RO []string `yaml:"ro"` - ROX []string `yaml:"rox"` - RW []string `yaml:"rw"` - RWX []string `yaml:"rwx"` - } `yaml:"filesystem"` - Env []string `yaml:"env"` - Network struct { - Unrestricted bool `yaml:"unrestricted"` - } `yaml:"network"` -} +// loadLayeredConfig loads sandbox config using the layered approach: +// global.yaml -> backend (anvilmcp) -> sandbox/.yaml +func loadLayeredConfig(name string) (*sandbox.Config, error) { + // Load global.yaml as base + baseCfg, err := sandbox.Load() + if err != nil { + return nil, fmt.Errorf("failed to load global config: %w", err) + } -// loadSandboxConfig loads sandbox config from ~/.config/anvillm/sandbox/.yaml -// Falls back to ./cfg/sandbox/.yaml for development -func loadSandboxConfig(name string) (*SandboxConfig, error) { - // Validate name to prevent path traversal + // Convert base config to layered format + baseLayer := sandbox.LayeredConfig{ + Filesystem: baseCfg.Filesystem, + Network: baseCfg.Network, + Env: baseCfg.Env, + } + layers := []sandbox.LayeredConfig{baseLayer} + + // Load sandbox layer if name == "" { name = "anvilmcp" } - for _, r := range name { - if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_') { - return nil, fmt.Errorf("invalid sandbox name: %s", name) - } - } - - homeDir, _ := os.UserHomeDir() - cfgPath := filepath.Join(homeDir, ".config", "anvillm", "sandbox", name+".yaml") - - // Fall back to ./cfg/sandbox/ for development - if _, err := os.Stat(cfgPath); os.IsNotExist(err) { - cfgPath = filepath.Join("cfg", "sandbox", name+".yaml") - } - - data, err := os.ReadFile(cfgPath) + sbxLayer, err := sandbox.LoadSandbox(name) if err != nil { - return nil, fmt.Errorf("failed to load sandbox config %s: %v", name, err) + return nil, fmt.Errorf("failed to load sandbox %q: %w", name, err) + } + layers = append(layers, sbxLayer) + + // Merge layers + general := sandbox.GeneralConfig{ + BestEffort: baseCfg.General.BestEffort, + LogLevel: baseCfg.General.LogLevel, + } + advanced := sandbox.AdvancedConfig{ + LDD: baseCfg.Advanced.LDD, + AddExec: baseCfg.Advanced.AddExec, } - var cfg SandboxConfig - if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("failed to parse sandbox config: %v", err) - } - - return &cfg, nil + return sandbox.Merge(general, advanced, layers...), nil } -// expandPath replaces template variables with actual values -func expandPath(pattern string) string { - s := pattern - homeDir, _ := os.UserHomeDir() - - // Generic environment variable expansion: {VAR_NAME} - re := regexp.MustCompile(`\{([A-Z_][A-Z0-9_]*)\}`) - s = re.ReplaceAllStringFunc(s, func(match string) string { - varName := match[1 : len(match)-1] - - switch varName { - case "HOME": - return homeDir - case "PLAN9": - if plan9 := os.Getenv("PLAN9"); plan9 != "" { - return plan9 - } - return "" // Will cause path to be skipped - case "NAMESPACE": - if ns := os.Getenv("NAMESPACE"); ns != "" { - return ns - } - if nsCmd, err := exec.Command("namespace").Output(); err == nil { - return strings.TrimSpace(string(nsCmd)) - } - return fmt.Sprintf("/tmp/ns.%s.:0", os.Getenv("USER")) - case "CWD": - return "" // Handled specially - replaced with workDir - } - - if val := os.Getenv(varName); val != "" { - return val - } - return "" // Will cause path to be skipped - }) - - return s -} - -func executeCode(code, language string, timeout int, sandbox string) (string, error) { +func executeCode(code, language string, timeout int, sandboxName string) (string, error) { start := time.Now() if timeout <= 0 { @@ -225,8 +177,8 @@ func executeCode(code, language string, timeout int, sandbox string) (string, er return "", err } - // Load sandbox config - cfg, err := loadSandboxConfig(sandbox) + // Load layered sandbox config + cfg, err := loadLayeredConfig(sandboxName) if err != nil { return "", err } @@ -236,12 +188,12 @@ func executeCode(code, language string, timeout int, sandbox string) (string, er return "", fmt.Errorf("failed to get home dir: %v", err) } - // Determine working directory based on config - var workDir string + // Use current working directory + workDir, _ := os.Getwd() + + // For anvilmcp sandbox, use temp workspace var cleanupWorkDir bool - - if cfg.CWD == "" || cfg.CWD == "temp" { - // Use temp workspace (original behavior) + if sandboxName == "" || sandboxName == "anvilmcp" { workspaceBase := filepath.Join(homeDir, ".cache", "anvillm", "exec") if err := os.MkdirAll(workspaceBase, 0700); err != nil { return "", fmt.Errorf("failed to create workspace base: %v", err) @@ -251,13 +203,6 @@ func executeCode(code, language string, timeout int, sandbox string) (string, er return "", fmt.Errorf("failed to create workspace: %v", err) } cleanupWorkDir = true - } else { - // Use configured path (e.g., "{CWD}" expands to current directory) - workDir = expandPath(cfg.CWD) - if workDir == "" { - workDir, _ = os.Getwd() - } - cleanupWorkDir = false } if cleanupWorkDir { @@ -280,13 +225,9 @@ func executeCode(code, language string, timeout int, sandbox string) (string, er // Add filesystem permissions from config addPaths := func(flag string, paths []string) { for _, p := range paths { - expanded := expandPath(p) - if expanded == "" { - continue - } - // Handle {CWD} specially - if strings.Contains(p, "{CWD}") { - expanded = strings.ReplaceAll(p, "{CWD}", workDir) + expanded := sandbox.ExpandPath(p, workDir) + if expanded == "" || strings.Contains(expanded, "{") { + continue // Skip unexpanded templates } if _, err := os.Stat(expanded); err == nil { args = append(args, flag, expanded) diff --git a/internal/sandbox/config.go b/internal/sandbox/config.go index 5761e98..1bd70c7 100644 --- a/internal/sandbox/config.go +++ b/internal/sandbox/config.go @@ -196,8 +196,8 @@ func validateName(name string) error { 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 // CWD is special - passed as parameter, not from env diff --git a/internal/sandbox/wrapper.go b/internal/sandbox/wrapper.go index ee525c0..61805c9 100644 --- a/internal/sandbox/wrapper.go +++ b/internal/sandbox/wrapper.go @@ -48,25 +48,25 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string { // Collect all filesystem paths 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"}) }