You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user