# /etc/sudoers.d/ai-tools
# Grants the ai-ops operators group (the login users that drive the sandbox) three NOPASSWD
# rules. The first DROPS privilege to ai-tools; the other two run 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-relabel-agent as ROOT (the manual `ai-tools --relabel` fallback that
#      restores ai_tools_exec_t on the agent entrypoint; see the security note below)
#   3. run ai-tools-stop as ROOT (`ai-tools --stop`, which terminates every running agent
#      session; with rule 1 these are the session lifecycle -- start one, end them all)
#
# One rule serves every agent: ai-tools-run is the shared confinement shim and names no agent,
# so a second agent package ships its own wrapper and needs no new sudoers grant.
#
# 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 needs no sudo rule here.
# The post-upgrade entrypoint relabel fires automatically through the root-side
# ai-tools-relabel.path watcher; this relabel rule is the operator's on-demand fallback.
#
# 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 needs no variable of its own here.
#
# SECURITY NOTES:
#   - NOPASSWD is intentional: every command here is called non-interactively (ai-tools-run from a
#     launch wrapper, the relabel from a timer-driven or scripted update flow, 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-relabel-agent runs AS ROOT: restoring ai_tools_exec_t on the new agent
#     entrypoint needs unconfined_t, which root holds. The grant is scoped to that one action --
#     a FIXED, non-glob path, and the trailing `""` pins it to the ZERO-ARGUMENT form (a command
#     listed with no arguments at all would instead permit any, per sudoers(5)). The helper's
#     other form, `--remove <agent>`, is therefore reachable by root alone: it is an erase-time
#     step for the agent package's scriptlet, not an operator action. The helper is 750 root:root,
#     writable by root alone. This is an operator grant, keeping the root privilege on the
#     operator side rather than in the agent-reachable handback domain.
#   - 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 takes no target and no authorization input, so the rule has no argument surface at
#     all. 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 two root rules,
#     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-relabel-agent ""
%ai-ops ALL=(root) NOPASSWD: /usr/local/libexec/ai-tools/ai-tools-stop ""
