# /etc/sudoers.d/ai-tools
# Grants the ai-ops operators group (the login users that drive the sandbox) two NOPASSWD rules,
# which are the session lifecycle: start one, end them all. The first DROPS privilege to
# ai-tools; the second runs a fixed-path helper as root. ai-tools itself has NO rule
# here, and the agent runs AS ai-tools, so no rule grants the agent anything.
#   1. execute ai-tools-run as ai-tools (an agent's wrapper resolves the versioned
#      executable, exports it as AI_TOOLS_AGENT_EXEC, then calls ai-tools-run which wraps the
#      session in a systemd transient service (--user --pty) with RestrictNamespaces=yes +
#      UMask=0007; NoNewPrivileges is intentionally ABSENT -- the session itself runs under NNP
#      because RestrictNamespaces forces PR_SET_NO_NEW_PRIVS, but ai-tools-run must not set NNP
#      at the scope it occupies before exec, or sudo's own PAM authentication breaks)
#   2. run ai-tools-stop as ROOT (`ai-tools --stop`, which terminates every running agent
#      session; see the security note below)
#
# One rule serves every agent: the rule names ai-tools-run, the shared confinement shim, which is
# agent-agnostic. A second agent package ships its own wrapper and inherits this grant rather than
# adding one.
#
# The sandbox toolchain update runs as ai-tools in its own systemd --user instance
# (nvm-update.service), writing the shared .nvm tree directly, so it does not need a sudo rule
# here.
# The post-upgrade entrypoint relabel fires as root through the ai-tools-relabel.path watcher, and
# an administrator reconciles on demand with `sudo ai-tools-admin system entrypoints relabel`, so
# that helper does not appear in this file either.
#
# A group rule (`%ai-ops`) covers every operator from one static drop-in: the package ships it,
# and membership in ai-ops (managed by ai-tools-admin) is what grants access, so there is no
# per-operator line to generate. Ownership handback (ai-tools-chown, -setgid, -launcher-symlink)
# is NOT here: it goes through the ai-tools-handback socket daemon, which the agent reaches via
# /usr/local/bin/ai-tools-handback-client -- sudo's SUID bit is dropped under NNP anyway.
#
# The umask, env_keep, and umask_override Defaults are scoped per-command (Defaults!<command>),
# not to all of an operator's sudo, so they apply ONLY to these commands:
#   - umask=0007 + umask_override (ai-tools-run): sets ai-tools-run's own process umask. A service
#     unit does NOT inherit the caller's umask, so the agent's umask is set authoritatively by
#     the UMask=0007 property ai-tools-run puts on the unit -- files 660/770 rather than 600/700.
#     The operator and ai-tools are co-writers on project files; world bits are still
#     stripped (o=0). A stricter umask (e.g. 0027) would give group read-only after handback,
#     blocking an agent's in-place edit tool on handed-back files.
#   - env_keep AI_TOOLS_AGENT_EXEC AI_TOOLS_PROJECT_DIR (ai-tools-run): the versioned executable
#     resolved and validated by the wrapper is passed through sudo so ai-tools-run can re-validate
#     and exec it directly; AI_TOOLS_PROJECT_DIR carries the validated project dir and becomes the
#     transient unit's WorkingDirectory (units default to /). These two are the whole
#     wrapper-to-shim contract -- the agent's identity is derived from the executable path against
#     the installed manifests, so it does not need a variable of its own here.
#
# SECURITY NOTES:
#   - NOPASSWD is intentional: both commands here are called non-interactively (ai-tools-run from a
#     launch wrapper, the stop from a detector with no human present).
#   - ai-tools-run is at a fixed, non-glob path (550 root:ai-tools, not writable by
#     ai-tools). The versioned agent binary is not a direct sudo target; ai-tools-run
#     exec's it after re-validating AI_TOOLS_AGENT_EXEC against the enabled agents' launchers.
#   - ai-tools-stop runs AS ROOT: a session lives in the sandbox account's cgroups, and only root
#     may signal across accounts and write cgroup.kill. NOPASSWD is the point of the rule, not a
#     convenience -- an unattended detector cannot answer a password prompt. The same trailing `""`
#     pins it to the ZERO-ARGUMENT form, which grants the bare command (all automation needs, since
#     the confirmation defaults YES with no terminal) and withholds --force and --dry-run. The
#     helper does not take a target or an authorization input, so the rule does not expose any
#     argument surface. Why it is granted this way, and what it widens: docs/session-stop.md.
#   - The first rule DROPS privilege; the agent runs AS ai-tools and cannot invoke an
#     ai-ops rule, so no rule here grants the agent anything -- including the root rule, which
#     ai-tools cannot reach.
#   - ai-tools has no login shell and no other sudo rights, and is not a member of ai-ops.
#
# Syntax check before deploy:  visudo -c -f /etc/sudoers.d/ai-tools

Defaults!/opt/ai-tools/bin/ai-tools-run umask=0007,umask_override
Defaults!/opt/ai-tools/bin/ai-tools-run env_keep += "AI_TOOLS_AGENT_EXEC AI_TOOLS_PROJECT_DIR"

%ai-ops ALL=(ai-tools:ai-tools) NOPASSWD: /opt/ai-tools/bin/ai-tools-run
%ai-ops ALL=(root) NOPASSWD: /usr/local/libexec/ai-tools/ai-tools-stop ""
