//go:build linux

package sandbox

import (
	"fmt"
	"os/exec"
	"strconv"
	"syscall"
)

// uidSandbox confines the agent child to a low-privilege UID/GID and applies OS
// resource limits, while leaving proc.Group's Setpgid (tree-kill) intact.
//
// Tree-kill invariant: we ONLY add Credential to the existing SysProcAttr. We do
// NOT touch Setpgid (proc.Group sets it after Apply via group.Prepare). The
// negative-pgid SIGTERM/SIGKILL path keeps working because the new process group
// leader is the same child — it simply runs as the low-priv uid now.
//
// Resource limits are applied by wrapping the real binary in `prlimit(1)`:
// `prlimit --as=N --nproc=N --cpu=N --fsize=N -- <binary> <args...>`. prlimit
// sets the limits on the child it execs, so they apply to the agent and (since
// limits are inherited across fork) its whole subtree — exactly the tree the
// pgid covers. Wrapping is additive to Credential/Setpgid and does not change
// the process-group topology (prlimit execs the target in place, becoming the
// group leader). If prlimit is unavailable, UID drop still applies and we log no
// rlimits (best-effort; container mode is the hard boundary).
//
// Bind-mount masking (CLONE_NEWNS + remount /data with only HomeDir+WorktreeDir
// visible) is intentionally NOT auto-enabled here: it requires the server to
// hold CAP_SYS_ADMIN / a user namespace, which the rootless production container
// (USER app) does not have by default. The robust isolation path is
// ModeContainer. uidSandbox therefore provides UID drop + rlimits + egress proxy
// as the minimum viable confinement, and documents that filesystem masking is a
// container-mode guarantee.
type uidSandbox struct{}

func (s *uidSandbox) Apply(cmd *exec.Cmd, spec Spec) (func(), error) {
	if spec.UID <= 0 {
		return func() {}, fmt.Errorf("sandbox uid: invalid target uid %d", spec.UID)
	}

	// UID/GID drop — ADDITIVE to whatever proc.Group will set (Setpgid).
	if cmd.SysProcAttr == nil {
		cmd.SysProcAttr = &syscall.SysProcAttr{}
	}
	gid := spec.GID
	if gid <= 0 {
		gid = spec.UID
	}
	cmd.SysProcAttr.Credential = &syscall.Credential{
		Uid: uint32(spec.UID),
		Gid: uint32(gid),
	}

	// Resource limits via prlimit wrapper (if available).
	if prlimit, err := exec.LookPath("prlimit"); err == nil {
		args := prlimitArgs(spec.Rlimits)
		if len(args) > 0 {
			// Re-point the command through prlimit, preserving the original
			// binary + args after the `--` separator.
			orig := append([]string{cmd.Path}, cmd.Args[1:]...)
			cmd.Path = prlimit
			cmd.Args = append(append([]string{prlimit}, args...), append([]string{"--"}, orig...)...)
		}
	}

	// Egress allowlist: inject HTTP(S)_PROXY/NO_PROXY into the stripped child env.
	injectProxyEnv(cmd, spec)

	// No ephemeral resources to tear down in uid mode (no bind mounts here);
	// return a no-op cleanup.
	return func() {}, nil
}

// prlimitArgs builds the prlimit flags for the non-zero limits in l.
func prlimitArgs(l Limits) []string {
	var args []string
	if l.AddressSpaceBytes > 0 {
		args = append(args, "--as="+strconv.FormatUint(l.AddressSpaceBytes, 10))
	}
	if l.NProc > 0 {
		args = append(args, "--nproc="+strconv.FormatUint(l.NProc, 10))
	}
	if l.CPUSeconds > 0 {
		args = append(args, "--cpu="+strconv.FormatUint(l.CPUSeconds, 10))
	}
	if l.DiskBytes > 0 {
		args = append(args, "--fsize="+strconv.FormatUint(l.DiskBytes, 10))
	}
	return args
}
