sandbox: copy from anvillm, break dependency
Vendor anvillm/pkg/sandbox into internal/sandbox, updating config paths from ~/.config/anvillm to ~/.config/ollie. Remove anvillm from go.mod. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
edee1a5889
commit
c7d2684cfc
17
go.mod
17
go.mod
|
|
@ -4,20 +4,5 @@ go 1.25.6
|
|||
|
||||
require (
|
||||
9fans.net/go v0.0.7
|
||||
anvillm v0.0.0-00010101000000-000000000000
|
||||
github.com/hymkor/go-multiline-ny v0.22.4
|
||||
github.com/mattn/go-runewidth v0.0.19
|
||||
github.com/mattn/go-tty v0.0.7
|
||||
github.com/nyaosorg/go-readline-ny v1.14.1
|
||||
github.com/nyaosorg/go-ttyadapter v0.3.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace anvillm => ../anvillm/main
|
||||
|
|
|
|||
19
go.sum
19
go.sum
|
|
@ -2,23 +2,7 @@
|
|||
9fans.net/go v0.0.7/go.mod h1:Rxvbbc1e+1TyGMjAvLthGTyO97t+6JMQ6ly+Lcs9Uf0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/hymkor/go-multiline-ny v0.22.4 h1:Ag2rkBpDnr3jp+AHe1CuXSOQ4AQ8D0+gBNVf+BAX+/0=
|
||||
github.com/hymkor/go-multiline-ny v0.22.4/go.mod h1:v2lqQooHVAO53WICAIbgTn74bQtzsg4tFn29d+7JPwY=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
|
||||
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
|
||||
github.com/nyaosorg/go-readline-ny v1.14.1 h1:bWyXpR6jRaCXysx4bnioxk36+YjQ6dypHKMjHnzIXdk=
|
||||
github.com/nyaosorg/go-readline-ny v1.14.1/go.mod h1:/BDf3/H/AScnvey4LoDws1bjTZDB76EE7uKnW2apoKU=
|
||||
github.com/nyaosorg/go-ttyadapter v0.3.0 h1:/Y7+rGJ0LEcs+AExevwNmND2VJvvpBmgbMuCbntKq3c=
|
||||
github.com/nyaosorg/go-ttyadapter v0.3.0/go.mod h1:w6ySb/Y8rpr0uIju4vN/TMRHC/6ayabORHmEVs6d/qE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
|
@ -41,9 +25,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,382 @@
|
|||
package sandbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config represents the global sandbox configuration
|
||||
type Config struct {
|
||||
General GeneralConfig `yaml:"general"`
|
||||
Filesystem FilesystemConfig `yaml:"filesystem"`
|
||||
Network NetworkConfig `yaml:"network"`
|
||||
Env []string `yaml:"env"`
|
||||
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"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
}
|
||||
|
||||
// FilesystemConfig contains filesystem permission settings
|
||||
type FilesystemConfig struct {
|
||||
RO []string `yaml:"ro"` // Read-only
|
||||
ROX []string `yaml:"rox"` // Read-only with execute
|
||||
RW []string `yaml:"rw"` // Read-write
|
||||
RWX []string `yaml:"rwx"` // Read-write with execute
|
||||
}
|
||||
|
||||
// NetworkConfig contains network permission settings
|
||||
type NetworkConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Unrestricted bool `yaml:"unrestricted"`
|
||||
BindTCP []string `yaml:"bind_tcp"`
|
||||
ConnectTCP []string `yaml:"connect_tcp"`
|
||||
}
|
||||
|
||||
// AdvancedConfig contains advanced landrun settings
|
||||
type AdvancedConfig struct {
|
||||
LDD bool `yaml:"ldd"`
|
||||
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}",
|
||||
"{HOME}/.claude",
|
||||
"{HOME}/.kiro",
|
||||
"{HOME}/.config/ollie",
|
||||
"{HOME}/.claude.json",
|
||||
},
|
||||
RWX: []string{},
|
||||
},
|
||||
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",
|
||||
},
|
||||
Advanced: AdvancedConfig{
|
||||
LDD: false,
|
||||
AddExec: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the sandbox config file
|
||||
func ConfigPath() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".config", "ollie", "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 {
|
||||
s := pattern
|
||||
s = strings.ReplaceAll(s, "{CWD}", cwd)
|
||||
|
||||
re := regexp.MustCompile(`\{([A-Z_][A-Z0-9_]*)\}`)
|
||||
s = re.ReplaceAllStringFunc(s, func(match string) string {
|
||||
varName := match[1 : len(match)-1]
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
switch varName {
|
||||
case "HOME":
|
||||
if home != "" {
|
||||
return home
|
||||
}
|
||||
case "TMPDIR":
|
||||
if tmpdir := os.Getenv("TMPDIR"); tmpdir != "" {
|
||||
return tmpdir
|
||||
}
|
||||
return "/tmp"
|
||||
case "XDG_CONFIG_HOME":
|
||||
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(home, ".config")
|
||||
case "XDG_DATA_HOME":
|
||||
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(home, ".local/share")
|
||||
case "XDG_CACHE_HOME":
|
||||
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(home, ".cache")
|
||||
case "XDG_STATE_HOME":
|
||||
if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(home, ".local/state")
|
||||
case "XDG_RUNTIME_DIR":
|
||||
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return fmt.Sprintf("/run/user/%d", os.Getuid())
|
||||
}
|
||||
|
||||
if val := os.Getenv(varName); val != "" {
|
||||
return val
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// 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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSandbox loads a named sandbox layer config from ~/.config/ollie/sandbox/<name>.yaml
|
||||
func LoadSandbox(name string) (LayeredConfig, error) {
|
||||
if err := validateName(name); err != nil {
|
||||
return LayeredConfig{}, fmt.Errorf("invalid sandbox name: %w", err)
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
path := filepath.Join(home, ".config", "ollie", "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)
|
||||
}
|
||||
if err != nil {
|
||||
return LayeredConfig{}, err
|
||||
}
|
||||
|
||||
var cfg LayeredConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return LayeredConfig{}, fmt.Errorf("parse %s: %w", path, 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
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package sandbox
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
available bool
|
||||
availableOnce sync.Once
|
||||
)
|
||||
|
||||
// IsAvailable checks if landrun is available on the system.
|
||||
// The result is cached after the first call for performance.
|
||||
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))
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package sandbox
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// pathEntry holds a path with its permission type
|
||||
type pathEntry struct {
|
||||
path string
|
||||
flag string // --ro, --rox, --rw, --rwx
|
||||
}
|
||||
|
||||
// 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() {
|
||||
return originalCmd
|
||||
}
|
||||
|
||||
args := []string{"landrun"}
|
||||
|
||||
if cfg.General.LogLevel != "" {
|
||||
args = append(args, "--log-level", cfg.General.LogLevel)
|
||||
}
|
||||
|
||||
if cfg.General.BestEffort {
|
||||
args = append(args, "--best-effort")
|
||||
}
|
||||
|
||||
if cfg.Advanced.LDD {
|
||||
args = append(args, "--ldd")
|
||||
}
|
||||
if cfg.Advanced.AddExec {
|
||||
args = append(args, "--add-exec")
|
||||
}
|
||||
|
||||
var entries []pathEntry
|
||||
for _, path := range cfg.Filesystem.RO {
|
||||
expanded := ExpandPath(path, cwd)
|
||||
if pathExists(expanded) {
|
||||
entries = append(entries, pathEntry{expanded, "--ro"})
|
||||
}
|
||||
}
|
||||
for _, path := range cfg.Filesystem.ROX {
|
||||
expanded := ExpandPath(path, cwd)
|
||||
if pathExists(expanded) {
|
||||
entries = append(entries, pathEntry{expanded, "--rox"})
|
||||
}
|
||||
}
|
||||
for _, path := range cfg.Filesystem.RW {
|
||||
expanded := ExpandPath(path, cwd)
|
||||
if pathExists(expanded) {
|
||||
entries = append(entries, pathEntry{expanded, "--rw"})
|
||||
}
|
||||
}
|
||||
for _, path := range cfg.Filesystem.RWX {
|
||||
expanded := ExpandPath(path, cwd)
|
||||
if pathExists(expanded) {
|
||||
entries = append(entries, pathEntry{expanded, "--rwx"})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort so parents come before children
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
if len(entries[i].path) != len(entries[j].path) {
|
||||
return len(entries[i].path) < len(entries[j].path)
|
||||
}
|
||||
return entries[i].path < entries[j].path
|
||||
})
|
||||
|
||||
for _, e := range entries {
|
||||
args = append(args, e.flag, e.path)
|
||||
}
|
||||
|
||||
if cfg.Network.Unrestricted {
|
||||
args = append(args, "--unrestricted-network")
|
||||
} else if cfg.Network.Enabled {
|
||||
for _, port := range cfg.Network.BindTCP {
|
||||
args = append(args, "--bind-tcp", port)
|
||||
}
|
||||
for _, port := range cfg.Network.ConnectTCP {
|
||||
args = append(args, "--connect-tcp", port)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range cfg.Env {
|
||||
args = append(args, "--env", name)
|
||||
}
|
||||
|
||||
args = append(args, "--")
|
||||
args = append(args, originalCmd...)
|
||||
|
||||
// Wrap with superpowers run-session if superpowerd is running
|
||||
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...)
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// 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")
|
||||
if socketDir == "" {
|
||||
return false
|
||||
}
|
||||
socketDir = filepath.Join(socketDir, "superpowerd")
|
||||
}
|
||||
conn, err := net.Dial("unixpacket", filepath.Join(socketDir, "superpowerd.sock"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// pathExists checks if a file or directory exists
|
||||
func pathExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"anvillm/pkg/sandbox"
|
||||
"ollie/internal/sandbox"
|
||||
)
|
||||
|
||||
const defaultMaxOutputChars = 8000
|
||||
|
|
|
|||
Reference in New Issue