Sudo Requests¶
SASE sudo requests let an agent ask for reviewed privileged execution without ever
asking for, receiving, logging, or proxying a password. The agent submits a typed batch
of exact argv commands, SASE creates a durable sudo gate, and a human reviews the
request before authenticating in a real terminal with the host's normal PAM prompt.
Overview¶
A sudo request moves through four steps:
- An agent finishes all non-root preparation, then pipes one JSON request to
sase sudo request. SASE validates it, snapshots the SHA-256 of every executable, seals the reviewed command manifest, and creates asudogate backed by a gate shell. The shell shows theSUDOstatus, and the agent's turn ends. - A reviewer opens the request in sase's TUI, checks the commands, and chooses which ones to run.
sase sudo answerauthenticates through the realsudoprompt in a terminal. The dedicatedsase_sudo_runnerexecutes the selected commands and returns a ledger.- The gate settles as
SUDOED(approved) orDENIED. After an approval, the request'snext.promptlaunches a successor in the same agent family that receives the ledger. A denial never launches a follow-up.
Agents learn this contract from the generated /sase_sudo skill.
Enable The Workflow¶
Sudo requests are a beta feature behind the agent_sudo_requests feature flag, which is
off by default. While it is off, every sase sudo subcommand fails with
feature_disabled. Enable it persistently on each machine that creates, answers, or
executes sudo requests:
sase flag enable agent_sudo_requests
Use the persisted setting rather than a per-invocation -f agent_sudo_requests. When
you approve from sase's TUI, its terminal handoff does not pass along the TUI's own
resolved flag snapshot, so sase sudo answer resolves the flag afresh from the
machine's saved choice and configuration.
Approval also needs the sase_sudo_runner entry point that ships with SASE's Rust core
package. SASE looks for it next to the running Python interpreter first, then on PATH.
Agent Contract¶
Agents must do all non-root preparation first, then batch the root work into one
sase sudo request JSON object on stdin. Each command needs a stable id, an exact
argv array whose executable is an absolute path, and a short why that explains why
root is needed.
| Field | Default | Meaning |
|---|---|---|
reason |
required | Why the batch needs root. Reviewers see it as untrusted agent text. |
commands |
required | Non-empty array of commands, run in the listed order. |
run_as |
root |
Target user. |
cwd |
current directory | Existing absolute working directory for the commands. |
env |
{} |
Reviewed string environment variables. |
machine |
this host | Enrolled machine alias or SSH target; see Remote Flow. |
stop_on_failure |
true |
Skip the remaining commands after one fails. |
output_to_agent |
tail |
Command output passed to the successor: none, tail, or full. |
gate_timeout_seconds |
300 |
Review window in whole seconds (at most one day). |
timeout_seconds |
none | Default per-command timeout; also the review window when gate_timeout_seconds is unset. |
next.prompt |
none | Prompt for the successor that receives the ledger after approval. |
The working directory must be an existing absolute directory when the request is
created. At execution time, the runner changes the sudo child process into that
directory before invoking /usr/bin/sudo; it does not pass sudo's -D or --chdir
option. The directory must be accessible on the execution host to the user who is
authenticating. Normally the approved command inherits that directory, but sudoers
CWD/runcwd policy remains authoritative. SASE does not fall back to the runner's
directory or use root privileges to enter root-only directories.
The review window is short by default. A request nobody answers within
gate_timeout_seconds settles as timed out and launches no follow-up, so set a longer
window when the reviewer may not be at a terminal.
Each command accepts id, argv, why, and an optional timeout_seconds (at most one
day). Command IDs are 1–64 letters, digits, ., _, or -, start with a letter or
digit, and must be unique within the request. Omitting machine, or setting it to ""
or "local", targets the current host.
Agents must not run raw sudo, doas, pkexec, su, askpass helpers, shells, or
interactive root programs. Request validation enforces that boundary and rejects:
- nested privilege tools:
sudo,sudoedit,su,doas,pkexec, andrun0; - shells and interactive programs (
sh,bash,dash,zsh,fish,ssh,vi,vim,nano,less,more,top, andhtop), plus any command with a bare-cargument; - executables that are relative, missing, not a regular file, or writable by the requesting user;
- credential-shaped data: argv or environment values containing
password=,passwd=,passphrase=,secret=, ortoken=, and environment keys containing those words; - loader and askpass variables:
LD_*,DYLD_*,PYTHONPATH,PYTHONINSPECT,SUDO_ASKPASS, andSSH_ASKPASS.
If output may be secret-adjacent, set "output_to_agent": "none" so the follow-up
prompt receives only the execution outcome.
Example:
{
"reason": "Install the reviewed sysctl policy file and apply it now.",
"cwd": "/",
"commands": [
{
"id": "install-policy",
"argv": [
"/usr/bin/install",
"-o",
"root",
"-g",
"root",
"-m",
"0644",
"/tmp/sase-ptrace-scope.conf",
"/etc/sysctl.d/99-sase-ptrace-scope.conf"
],
"why": "Persist the reviewed ptrace policy as a root-owned sysctl drop-in."
},
{
"id": "apply-policy",
"argv": ["/usr/sbin/sysctl", "-p", "/etc/sysctl.d/99-sase-ptrace-scope.conf"],
"why": "Apply the reviewed ptrace policy immediately."
}
],
"next": {
"prompt": "Verify the sudo ledger, then check sysctl kernel.yama.ptrace_scope."
}
}
sase sudo request prints a JSON gate descriptor. Inside an agent, it then hands the
turn to the gate shell, so run it in the foreground and treat the descriptor as the only
proof that the gate exists. Use -o/--origin-agent to attribute a request created
outside an agent.
Review UX¶
In sase's TUI, a pending request appears on the Agents tab as a gate-shell row with the
SUDO status and in the notification inbox as a sudo request. Its notification carries
the sudo and gate tags and no declared panel, so it lands in a sudo tag tab rather
than Gates. Selecting the notification opens the Sudo Request review modal:
- Context lists the agent's reason, the target host, run-as user, working directory, environment policy, requester, project, expiry, and risk badges. The reason is treated as untrusted text.
- Commands is a checklist of every reviewed command, all selected by default. Approval runs only the selected commands, in their reviewed order.
- Details (
v) shows each command's argv and executable SHA-256, the reviewed environment, timeout, stop and output policies, and the manifest SHA-256. - Denial Feedback holds an optional note for the requesting agent.
| Key | Action |
|---|---|
Space |
Toggle the focused command |
v |
Show or hide details |
Enter |
Authenticate & run the selected commands |
d |
Deny, sending the feedback note |
Ctrl+D / Ctrl+U |
Scroll half a page |
g / G |
Jump to the top / bottom |
q / Esc |
Close without answering |
Inside this modal d denies the request. To open Gate Debug, press d on the
notification row instead.
The approve action is Authenticate & run. It suspends sase's TUI, prints a banner
naming the request and the selected command IDs, and runs
sase sudo answer <id> --run --json (with one --command per selected ID) attached to
the controlling terminal. The terminal is held only for authentication by default; the
reviewed commands then run in a supervised background proc and the gate settles when
that proc validates the ledger. The only credential prompt is the real prompt printed by
/usr/bin/sudo or by the target host over SSH. If the installed runner or remote target
cannot detach yet, approval falls back to the foreground path with a warning. When the
handoff returns, sase's TUI shows whether execution started, completed synchronously, a
selected command failed, authentication failed, cancelled, timed out, another sudo
handoff is active, or no trusted terminal was available. Deny runs through the normal
gate path and needs no terminal because it runs no privileged command.
Only sase sudo answer can approve a sudo gate. Every other answer path, including
sase gate answer, mobile, Telegram, fleet bridges, and detached procs, is refused and
leaves the gate pending. Those surfaces can still display or deny the request, and
sase gate show points at sase sudo answer <id>.
Terminal Commands¶
| Command | Flags | Behavior |
|---|---|---|
sase sudo / sase sudo list |
-a/--all, -j/--json, -l/--limit, -p/--project |
List pending sudo gate shells; --all includes settled ones. |
sase sudo show ID |
-j/--json |
Show one gate; --json adds the sealed manifest, target, and risk badges. |
sase sudo request |
-j/--json, -o/--origin-agent |
Create a sudo gate from one JSON object on stdin. |
sase sudo answer ID |
-u/--run or -a/--approve, -d/--deny, -c/--command ID, -D/--detach, -N/--no-detach, -f/--feedback, -j/--json, -r/--resume, -R/--restart |
Approve (authenticate and run) or deny one gate. |
ID is a sudo gate ID such as sudo-<uuid> or a gate-shell reference. --run and
--approve both authenticate and run. Approval detaches by default after the
authentication phase; use --no-detach to keep command execution in the foreground.
--detach is accepted as an explicit spelling of the default. --command is repeatable
and selects a subset of the reviewed commands; without it, every command runs. Without
--run, --approve, or --deny, an interactive terminal asks
Approve sudo request? [y/N]. A non-interactive call must choose explicitly, and
approval always requires a controlling terminal.
With --json, a failed answer prints status: "pending" plus an outcome of
authentication_failed, cancellation, timeout, lock_contention, missing_tty, or
runner_error. It exits 2 for the first five outcomes and 1 for runner_error.
Only one sudo authentication handoff runs at a time on a host. A second attempt fails immediately and names the request, user, and process that hold the lease. The runner receives the sealed manifest in a private temporary file together with its expected hash. In detached mode, the same timeout bounds the finalize proc that supervises completion. Its overall timeout is the sum of the selected commands' timeouts (300 seconds for each command without one) plus 30 seconds.
SASE accepts the runner's ledger only when it matches the reviewed manifest hash,
contains exactly one entry per selected command, and carries no credential-shaped data.
A command that fails after authentication does not keep the gate pending. The gate still
settles as approved, the ledger records each command's status, and sase's TUI warns that
a selected command failed. With stop_on_failure, the remaining commands do not run.
The successor reads the ledger and decides what to do next.
Remote Flow¶
For a machine-targeted request, set machine to an enrolled alias or an SSH target. An
enrolled alias resolves to its recorded SSH handoff target, which defaults to the alias
itself (sase machine show ALIAS prints it). Any other value must be a valid SSH
destination and is shown to the reviewer as an unenrolled host.
Review still happens on the controller. On approval, the controller:
- checks that
ssh <target>runningsase sudo exec --contractthrough the account's noninteractive login shell reports a compatible contract; - stages the sealed manifest in a private temporary file under the target's
/tmp; - runs
ssh -t <target>attached to the reviewer's/dev/ttywithsase sudo exec --detach ...through that same login shell, so the target's own runner prompts for authentication on the controlling terminal even when parent--jsonstdout is a pipe, then starts a remote detached executor; - starts a local finalize proc that polls the target's JSON ledger, validates it, and removes the staged files after settlement.
Target-side SASE invocations — the contract probe and both synchronous and detached
sase sudo exec — run through the account's noninteractive login shell so tools
installed under login-profile paths such as ~/.local/bin can be found, matching the
remote screenshot contract. Manifest staging, ledger and log reads, liveness probes,
stop signaling, and cleanup stay direct, minimal remote commands. The target needs
sase available in that login environment, its own sase_sudo_runner, and
agent_sudo_requests enabled, because sase sudo exec is behind the same flag. The PAM
conversation stays on the reviewer's /dev/tty between the reviewer and the target
machine, so ACE or --json stdout pipes cannot swallow it. The fleet gateway never
carries that conversation.
The local gate settles from the returned ledger. If the target is unreachable, has no
compatible sase sudo exec, lacks a TTY, or produces no ledger, the gate remains
pending and can be retried.
If the target does not advertise detached sudo execution yet, approval falls back to the older foreground SSH path and keeps the terminal attached until the remote ledger is available.
Policy Rationale¶
The sudo request flow replaces standing passwordless sudo with an explicit review and authentication boundary. SASE records the request, selected command IDs, manifest hash, bounded command output, and runner ledger. It never records the password, password length, password digest, PAM transcript, attempt count, or any derived credential material.
Commands are hash-bound from review to execution. Bundle-owned option commands are rehashed immediately before execution, and the sudo runner verifies the manifest hash before running anything. After a batch settles, the runner invalidates the sudo timestamp so approval does not create reusable ambient privilege.
Recovery¶
A pending sudo gate can be inspected with:
sase sudo list
sase sudo show <id>
If approval fails because authentication was cancelled, a password attempt failed, a timeout occurred, no TTY was available, another handoff held the lease, or the runner could not be found, the gate remains pending. Re-run:
sase sudo answer <id> --run
Use --no-detach as well if you need the approving terminal to stay attached until the
reviewed commands finish.
If a ledger shows sudo rejected -D or --chdir, the host ran an old sudo runner. The
reviewed command never reached the package manager or other target executable. Changing
or omitting cwd in a new request does not repair that old runner, because request
normalization always supplies a cwd and the old runner always translated it to sudo
-D. After installing a fixed runner, create a new reviewed sudo request for any
already-settled failed approval instead of trying to reuse the old gate.
Every approval runs the full selected command set again. If a run was interrupted partway, for example by the runner timeout, no ledger is recorded and some commands may already have taken effect. Check the host, then select only the commands that still need to run:
sase sudo answer <id> --run --command apply-policy
--resume and --restart are accepted for parity with sase gate answer, but they do
not make the runner skip commands that already ran.
During host policy changes, keep an already-authenticated root escape hatch open until the reviewed change has been applied and verified. If a lockout risk appears, use that terminal to restore the last known-good sudoers or sysctl state, then leave the sudo gate pending with an evidence note rather than bypassing the reviewed flow silently.