# /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 -- the root
# rule included, which ai-tools cannot reach.
#   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; the unit properties that confine it are set there,
#      not here). NNP is deliberately not set at the sudo level: ai-tools-run must not run under
#      PR_SET_NO_NEW_PRIVS at the scope it occupies before exec, or sudo's own PAM authentication
#      breaks. The session gets it from the unit instead.
#   2. run ai-tools-stop as ROOT (`atr stop`, which terminates every running agent
#      session; see the security note in this file)
#
# One rule serves every agent, because it names the shared agent-agnostic shim: a second agent
# package ships its own wrapper and inherits this grant rather than adding one. A group rule
# (`%ai-ops`) likewise covers every operator from one static drop-in, so there is no per-operator
# line to generate.
#
# Three privileged operations deliberately do NOT appear here: the toolchain update (runs as
# ai-tools in its own `systemd --user instance`), the post-upgrade entrypoint relabel (root,
# through the ai-tools-relabel.path watcher), and the ownership handback
# (`ai-tools-{chown,setgid,launcher-symlink}`, reached through the ai-tools-handback socket daemon
# rather than sudo, whose SUID bit NNP drops inside a session anyway). Why each is placed where it
# is: launch.rule.md.
#
# 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 comes authoritatively from
#     the UMask=0007 property ai-tools-run puts on the unit -- files 660/770 rather than 600/700,
#     the operator and ai-tools being co-writers on project files (world bits are still
#     stripped). A stricter umask such as 0027 would leave the group read-only after handback,
#     blocking an agent's in-place edit tool on the files it just handed back.
#   - 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/sessions/stop.md.
#   - ai-tools has no login shell and no other sudo rights, and is not a member of ai-ops.
#     ai-tools-run refuses to launch if it ever becomes one.
#
# 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 ""
