// Package sandbox provides a pluggable per-session execution sandbox that sits // between acp.session_service.Create and acp.Supervisor.Spawn. It confines an // agent child process to a low-privilege UID, bind-mounts only that session's // worktree + the per-user agent home, applies OS resource limits, and routes // egress through an allowlist proxy — all WITHOUT touching proc.Group's // Setpgid-based tree-kill (Apply only ADDS to cmd.SysProcAttr, never replaces). // // Three modes, config-gated via acp.sandbox.mode: // - none : no-op (default; Windows dev + CI + any non-Linux box) // - uid : drop to a low-priv UID + setrlimit (+ optional namespace/bind // mounts) — Linux only // - container : run the agent inside a rootless runc/bwrap or docker container // on an egress-restricted network — Linux only // // The Linux-specific mechanics live in build-tagged files; this file holds the // cross-platform contract and the factory so app.go and supervisor.go compile // on every platform. package sandbox import "os/exec" // Mode selects the sandbox strategy. type Mode string const ( // ModeNone disables sandboxing (no-op Apply). Default everywhere except // production Linux. ModeNone Mode = "none" // ModeUID drops to a low-priv UID + setrlimit (+ optional bind mounts). ModeUID Mode = "uid" // ModeContainer runs the child inside a rootless container. ModeContainer Mode = "container" ) // Limits captures the OS resource limits applied to the child (and its tree). // A zero field means "do not set this limit" (inherit the parent's). type Limits struct { AddressSpaceBytes uint64 // RLIMIT_AS — caps total virtual memory (anti big-alloc) NProc uint64 // RLIMIT_NPROC — caps process/thread count (anti fork-bomb) CPUSeconds uint64 // RLIMIT_CPU — caps CPU seconds PIDs uint64 // cgroup pids.max (container mode); mirrors NProc for uid DiskBytes uint64 // RLIMIT_FSIZE — caps max file size the child can write } // Spec is the per-spawn sandbox request, derived from the session + agent kind. type Spec struct { Mode Mode UID int // target low-priv uid (uid mode); base+offset computed by caller GID int // target low-priv gid HomeDir string // per-user agent home (writable, bind-mounted in) WorktreeDir string // this session's worktree (writable, bind-mounted in) DataRoot string // /data root to mask everything else under Rlimits Limits ProxyURL string // HTTP(S)_PROXY value injected into the child env (egress allowlist) NoProxy string // NO_PROXY value (e.g. localhost,127.0.0.1) } // Sandbox applies a Spec to an exec.Cmd before it is started. type Sandbox interface { // Apply mutates cmd.SysProcAttr (Credential, namespace/chroot flags) and // cmd.Env (HTTP(S)_PROXY/NO_PROXY) BEFORE proc.Group.Prepare and cmd.Start. // It returns a cleanup closure (unmount binds, remove ephemeral dirs, stop // container) that the supervisor calls from monitorProcess AFTER // group.Close(). cleanup is always non-nil (a no-op when there is nothing to // clean) and must be safe to call exactly once. Apply MUST NOT set Setpgid — // that is proc.Group's job; on Linux it ADDS to the existing SysProcAttr. Apply(cmd *exec.Cmd, spec Spec) (cleanup func(), err error) } // New returns the Sandbox implementation for the given mode. On non-Linux // platforms (or mode=none) it always returns the no-op sandbox. The actual // uid/container constructors are provided by build-tagged files; newPlatform // returns nil when the mode is unsupported on this OS, in which case we fall // back to no-op so dev/CI never break. func New(mode Mode) Sandbox { switch mode { case ModeUID, ModeContainer: if sb := newPlatform(mode); sb != nil { return sb } // Unsupported on this platform → safe no-op fallback. return noopSandbox{} default: return noopSandbox{} } } // noopSandbox makes no changes and returns a no-op cleanup. It is the default // and the fallback for unsupported modes/platforms. type noopSandbox struct{} func (noopSandbox) Apply(_ *exec.Cmd, _ Spec) (func(), error) { return func() {}, nil } // injectProxyEnv appends egress-proxy env vars to cmd.Env when a proxy URL is // configured. Shared by uid + container modes (both honor HTTP(S)_PROXY). Kept // here so the cross-platform contract is testable without build tags. func injectProxyEnv(cmd *exec.Cmd, spec Spec) { if spec.ProxyURL == "" { return } noProxy := spec.NoProxy if noProxy == "" { noProxy = "localhost,127.0.0.1,::1" } cmd.Env = append(cmd.Env, "HTTP_PROXY="+spec.ProxyURL, "HTTPS_PROXY="+spec.ProxyURL, "http_proxy="+spec.ProxyURL, "https_proxy="+spec.ProxyURL, "NO_PROXY="+noProxy, "no_proxy="+noProxy, ) }