You've already forked agentic-coding-workflow
116 lines
4.7 KiB
Go
116 lines
4.7 KiB
Go
// 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,
|
|
)
|
|
}
|