Skip to content

Add profile-applying descent commands to the daemon (#133) - #136

Merged
AcoPiper merged 5 commits into
mainfrom
AcoPiper/issue-133
Sep 28, 2026
Merged

AcoPiper merged 5 commits into
mainfrom
AcoPiper/issue-133

Conversation

@AcoPiper

@AcoPiper AcoPiper commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds InDaemonExecutor::descent_command and InDaemonExecutor::settle_descent. With them, bootler's root session helper can get a std::process::Command that descends through sudo to a service account or to an authenticated operator, and applies a pinned execution profile after sudo has chosen the identity. The helper sets stdio and its own process group and spawns the command itself. It does not re-implement descent, the sentinel script or elevation classification.

  • New public items in src/executor.rs: Descent (Service(ServiceAccount) / Operator(OperatorIds)), OperatorIds, DescentProfile, DescentError and DescentSettle, all with rustdoc and # Errors. None of them has FromStr, Deserialize or a From impl. OperatorIds::new refuses a uid or gid of 0 or u32::MAX.
  • One site builds every descent. A new private sudo_descent helper builds the sudo words for both paths. run's Identity::Service path and descent_command both call it, so a service account gets the same sudo -u <account> words as run. An operator gets sudo -u #<uid> -g #<gid>, so sudo sets the uid, the primary gid and the group-database groups as a login would, with no pre_exec and no new unsafe. Neither path passes -n or -S or sends a password line.
  • The descent script. The command runs sh -c <script> bootler-descent <cwd> <K=V>… <command> <args…>. The script enters cwd, or prints a fixed no-directory marker and exits 1. It discards cd's own diagnostic, which would echo the path, so nothing the script writes before its fixed words comes from the caller. Otherwise it prints SUDO_OK_SENTINEL and execs env -i "$@". Every value is its own argv word and none is spliced into the script text. The script's text contains neither the sentinel nor the marker: it prints each from two halves (printf '%s%s' '__BOOTLER' '_SUDO_OK__'). When sudoers denies a command, sudo repeats the whole command line on stderr, script included, and a script holding the literal sentinel made that denial settle as Started. The returned Command sets only its program, its arguments and the working directory /. It sets no stdio, process group, session, uid/gid, pre_exec or environment, and descent_command never spawns anything.
  • Validation before building. command must be an absolute path with no =. Env names must match [A-Za-z_][A-Za-z0-9_]* and appear once, and no value may contain NUL. cwd must be absolute with no NUL. No word the caller supplies (command, argument, env name or value, cwd) may contain the sentinel or the no-directory marker, because a sudoers denial would echo it. An argument holding one is refused as DescentError::InvalidArgument { index }, and the others through their existing variants. No error variant or message carries an env value.
  • Shared settling. The sentinel search and the 64 KiB transport-limit rule move out of judge in src/executor/channel.rs into one private helper, channel::announcement, which both open_channel's judge and settle_descent call. open_channel's behaviour and its tests are unchanged. settle_descent adds the no-directory marker on top of the helper. A marker ahead of any sentinel gives NoWorkingDirectory however much stderr came before it, with the reason capped at 64 KiB. For a refusal it returns the same ExecutorError::SudoRefused that classify_elevation produces for that stderr. It also refuses runaway output while the stream is still open.
  • Rustdoc warns that the K=V words appear in the process table until exec, so no secret belongs in the profile. It says that umask and resource limits come from the caller, sudo and PAM, not from this crate. It also says sudo and the command stay in the caller's process group only when there is no controlling terminal.
  • Unchanged: Identity::Operator inside the daemon still returns NoOperatorIdentity. No existing public signature or ExecutorError variant changes. There is no new dependency and no new unsafe. There is no CHANGELOG.md entry because deploy-core has no release.

Tests use sudo stubs under tempfile::tempdir() and cover:

  • the argv prefixes;
  • the exact profile environment and working directory;
  • awkward arguments and values arriving intact;
  • process-group membership through /proc/<pid>/stat on Linux, with one stub that stays alive and one that execs;
  • every settle outcome, including the limit and partial-sentinel boundaries;
  • one case per validation refusal.

An #[ignore] test runs the real sudo as root to a real account and checks uid, gid, groups, environment, directory and process group. CI does not run it.

Closes #133

Deviations from the issue

  • NoWorkingDirectory's reason is capped and trimmed, and never carries cd's own diagnostic. The issue gives the reason as "the text before" the marker. The implementation takes at most the first 64 KiB of that text and trims surrounding whitespace, as the Refused reasons are capped and trimmed. That keeps every reason settle_descent returns bounded and formatted the same way. The descent script also sends cd's diagnostic to /dev/null (cd -- "$1" 2>/dev/null || …), so the text before the marker is only what sudo and its PAM session wrote, usually nothing. The shell's diagnostic echoes the requested path, so a missing directory whose name contains SUDO_OK_SENTINEL would otherwise have been reported as Started. Such a path is now also refused before building (see the next entry). The diagnostic stays discarded so that nothing the script writes before its fixed words comes from the caller. The caller already knows which directory it asked for. What it loses is the shell's errno text, such as "No such file or directory" as opposed to "Permission denied".
  • DescentError gains an InvalidArgument { index: usize } variant, and every caller-supplied word is refused if it holds a word the descent reports its outcome with. The issue fixes DescentError at four variants and does not validate arguments, and it requires every argument to arrive intact. When sudoers denies a command, sudo prints the whole command line on stderr, including the command, its arguments, the K=V words and cwd. If any of them contained SUDO_OK_SENTINEL or the no-directory marker, that denial would settle as Started or NoWorkingDirectory rather than the Refused the issue requires. So the command, each argument, each env name and value, and cwd are refused if they contain either word. Arguments had no variant to report this, so InvalidArgument carries only the argument's position. The other inputs use their existing variants, with the docs and messages extended. No other argument is refused, and every argument that passes still arrives intact.
  • DescentProfile also derives Clone and Copy. The issue's sketch shows no derives on it. The struct holds only two borrowed references, so copying it costs nothing. It gains no Debug, FromStr, Deserialize or From impl.
  • One existing test was edited. The issue says existing tests pass unchanged. One line in an unrelated supervisor test in src/executor.rs now reads Duration::from_mins(5) instead of Duration::from_secs(300), which is the same five minutes. Clippy on the current stable toolchain flags the old form under -D warnings, so CI's clippy run would fail without the change. The test behaves exactly as before.

Test plan

  • cargo fmt -- --check --config group_imports=StdExternalCrate passes

  • cargo clippy --all-targets -- -D warnings passes

  • cargo clippy --all-targets --features test-support -- -D warnings passes

  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --document-private-items --features test-support passes

  • cargo test passes

  • cargo test --features test-support passes

  • CI's Platform (ubuntu-24.04-arm) job (cargo test --features test-support) and Platform (macos-latest) job (cargo build --all-targets --features test-support) pass

  • Argv: a_service_descends_as_run_does_and_an_operator_by_id. With a recording stub, Service(ServiceAccount::Insight) shows the same sudo -u <account> prefix that run shows. Operator(OperatorIds::new(1000, 1000)) shows -u #1000 -g #1000. Neither shows -n, -S or -p, and every script, directory, K=V, command and argument word follows as a word of its own

  • Built, not spawned: the_command_is_built_and_not_spawned. The returned Command has the sudo program, working directory / and no environment change, and building it runs nothing

  • Profile: the_command_sees_exactly_the_profile_in_its_directory. The test goes through a descending stub that also skips -g <group>, and the parent environment carries an extra variable set via Command::env on the returned Command. /usr/bin/env still prints exactly the six profile pairs, and /bin/pwd prints cwd

  • Arguments: every_argument_and_value_arrives_intact. /usr/bin/printf '%s\n' prints back, unchanged, arguments with spaces, quotes, $(…), backticks, a newline, a leading -, = after the command, an empty word and *. A literal $AWKWARD argument is not expanded, and a profile value with spaces, quotes, $(x) and a newline arrives unchanged

  • Process group (Linux): the_descent_stays_in_the_group_it_is_spawned_into. The Command is spawned with process_group(0) and stdin piped, over a stub that stays alive beside its child and over one that execs. While /bin/cat blocks on stdin, /proc/<pid>/stat of the stub and of cat both show the stub's own group. Closing stdin makes both exit

  • Settle, no directory: a_missing_directory_starts_nothing_and_settles_as_such. A missing cwd gives NoWorkingDirectory, and the command never runs. Stderr is exactly the marker, nothing echoes the path, and the reason is empty. (A path containing SUDO_OK_SENTINEL is now refused before building; see the outcome-word validation test.)

  • Settle, denial echoing the command line: a_denial_repeating_the_command_line_settles_as_refused. A stub denies as sudoers does, repeating the command line it was asked to run, so its stderr contains the whole script and the arguments. For both descents this gives Refused(SudoRefused) with that text as reason, not Started. The test also checks that the script text contains neither word. With the old script it fails. A real sudo 1.9.16p2 with root ALL=(ALL:ALL) /usr/bin/true in its sudoers was checked by hand: denying the old script echoed both words, and denying the new one echoed neither

  • Settle, refused: a_refusal_settles_as_run_classifies_it. A stub prints sudo: unknown user and exits. While the stream is still open this gives Pending, and once it has ended it gives Refused(SudoRefused), whose host and reason equal what classify_elevation returns for the same stderr

  • Settle, sentinel and limit: settling_follows_the_sentinel_and_the_transport_limit covers these cases:

    • a sentinel split across two calls gives Pending, then Started with the offset just past it
    • a marker after the sentinel belongs to the command
    • a sentinel at exactly 64 KiB still gives Started
    • 64 KiB plus one byte with no sentinel gives Refused while not ended
    • 64 KiB followed by a partial sentinel gives Pending
    • a sentinel arriving after more than 64 KiB gives Refused with the first 64 KiB as reason, and so does a marker after that sentinel
    • a marker after more than 64 KiB with no sentinel gives NoWorkingDirectory, ended or not, with the first 64 KiB as reason
  • Validation: an_invalid_descent_is_refused_before_building. These are all refused before building:

    • a relative, =-bearing or empty command
    • env names that are malformed, empty, contain = or are non-ASCII
    • a duplicate name and a NUL-bearing value
    • a relative, empty or NUL-bearing cwd
    • a uid or gid of 0 or u32::MAX

    Neither the Display nor the Debug text of any env error contains the value

  • Validation, outcome words: a_word_holding_an_outcome_word_is_refused_before_building. For both the sentinel and the no-directory marker, each of these is refused before building: a command containing the word (InvalidCommand), an argument containing it (InvalidArgument { index: 1 }), an env name equal to it or a value containing it (InvalidEnvironment, and the value does not leak), and a cwd containing it (InvalidWorkingDirectory)

  • Shared settling: Add a long-lived elevated channel to Executor #132's channel tests in src/executor/channel.rs pass unchanged now that judge goes through the shared announcement helper

  • Regression: operator_inside_the_daemon_refuses_and_runs_nothing and the other existing in-daemon tests pass unchanged

  • Real sudo (manual, not run by CI): descent_through_the_real_sudo descends to the nobody account twice, once as a service account and once by ids as an operator. Each time it checks uid, gid, groups, environment, directory, argument passing and process group. It needs root, sudo, and no controlling terminal, hence setsid -w.

    Run record. The test ran as root inside a container with no controlling terminal: setsid -w cargo test --features test-support -- --ignored --nocapture descent_through_the_real_sudo. Environment:

    • Docker rust:latest image on an arm64 host: Debian GNU/Linux 13 (trixie), kernel 7.0.14 aarch64, rustc 1.98.0
    • sudo installed from Debian with its default sudoers; sudo -V reports Sudo version 1.9.16p2
    • nobody is uid 65534, gid 65534 (nogroup), groups 65534

    Output:

    running 1 test
    Service(Fixture("nobody")): uid 65534, gid 65534, groups 65534
    Operator(OperatorIds { uid: 65534, gid: 65534 }): uid 65534, gid 65534, groups 65534
    test executor::tests::conformance::descents::descent_through_the_real_sudo ... ok
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1204 filtered out; finished in 0.13s
    

    In that run nobody had only its primary group. So the groups check confirmed the ids but could not tell sudo's group-database groups apart from a descent that drops every supplementary group.

    Second run, with a supplementary group. A later run used the same image, kernel, rustc and sudo 1.9.16p2. This time nobody was first added to a supplementary group with usermod -aG users nobody, so id nobody gave uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup),100(users). The same command then showed both descents receiving the group-database group:

    running 1 test
    Service(Fixture("nobody")): uid 65534, gid 65534, groups 65534 100
    Operator(OperatorIds { uid: 65534, gid: 65534 }): uid 65534, gid 65534, groups 65534 100
    test executor::tests::conformance::descents::descent_through_the_real_sudo ... ok
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1204 filtered out; finished in 0.11s
    

    Third run, after the script began printing its words from two halves. The same image, kernel (7.0.14-orbstack aarch64), rustc 1.98.0, sudo 1.9.16p2 and supplementary users group gave:

    Service(Fixture("nobody")): uid 65534, gid 65534, groups 65534 100
    Operator(OperatorIds { uid: 65534, gid: 65534 }): uid 65534, gid 65534, groups 65534 100
    test executor::tests::conformance::descents::descent_through_the_real_sudo ... ok
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1206 filtered out; finished in 0.15s
    

    The full cargo test --features test-support also ran on Linux in the same container, and every descent test passed, including the_descent_stays_in_the_group_it_is_spawned_into. One test failed: retain::tests::writable_ancestors_are_refused, which this PR does not touch. It fails only because the container runs as root, where the ancestor-permission check does not apply. The same test binary passes when run as nobody, and the test passes in CI's non-root Linux job.

bootler's session helper is a root process that spawns, bounds and
kills its own children, and must run them as a service account or as
the operator its session authenticated, under a pinned environment and
working directory. InDaemonExecutor could do neither: its resolution
was private, it applied no profile, and it refuses Identity::Operator.

descent_command builds, without spawning, a Command that descends
through sudo from the same site run uses, then applies the profile as
the target identity: cd into the directory, announce the start, and
exec env -i with the K=V words. Every value stays a discrete argv
word. The operator goes through sudo -u #uid -g #gid rather than
CommandExt::uid, which would drop supplementary groups and need new
unsafe to restore them.

settle_descent classifies the child's stderr. Its sentinel search and
64 KiB transport limit are factored out of the channel's judge, so
both starts are settled by one rule.

Also switch one test Duration to from_mins, which clippy on the
current stable toolchain requires.

Closes #133
Under a controlling terminal, sudo with use_pty -- the Debian and
Ubuntu default -- runs the command in a session of its own, so the
process-group check fails for a reason that is the test's environment
and not the descent. That is exactly the condition descent_command
documents, but the test's instructions said to run it from a shell.

Say so, suggest setsid -w, and refuse up front with that advice rather
than failing on the group assertion.

Part of #133
Stock `nobody` has only its primary group, so the ignored real-sudo
test's groups assertion cannot tell group-database groups apart from a
descent that dropped them all -- the very property `sudo` was chosen
over `CommandExt::uid` for. Say how to make a run that shows them.

Part of #133
@AcoPiper

Copy link
Copy Markdown
Contributor Author

[Reviewer Round 1]

Verdict: Request changes. The builder uses the intended sudo argv and shares service-account selection with run. The profile and process-group tests exercise the main path. I found two settling errors:

  1. A failed cd can be reported as Started. The script lets cd print the requested path to stderr before it prints the no-directory marker (executor.rs). If a missing path contains __BOOTLER_SUDO_OK__, that text appears in the shell’s diagnostic. settle_descent searches for the sentinel before the marker (executor.rs), so it reports Started although the command never ran. I reproduced the shell output with such a path. Prevent path text from imitating the start announcement and add a regression test.

  2. The declared over-limit marker deviation changes a known directory failure into a sudo refusal. Once stderr exceeds 64 KiB, announcement returns Overran; settle_descent returns Refused without checking for the no-directory marker (executor.rs). The issue assigns NoWorkingDirectory when that marker is present. The stated reason for the deviation does not require losing that distinction: the diagnostic can remain capped while retaining the correct classification.

The other declared deviations—capping and trimming the reason, deriving Clone and Copy, and the equivalent duration edit—look reasonable. The supplied PR body has the required Closes #133, test plan, and deviations section.

@AcoPiper

Copy link
Copy Markdown
Contributor Author

[Review Verdict Round 1: NOT_APPROVED]

The descent script let cd print its diagnostic, which echoes the
requested directory. A missing directory whose name contains the start
sentinel therefore put the sentinel on stderr ahead of the no-directory
marker, and settle_descent reported a command as started that never
ran. The script now discards cd's diagnostic, so nothing it writes
before its fixed words comes from the caller.

settle_descent also stopped looking for the marker once more than
64 KiB preceded it, reporting a known missing directory as a sudo
refusal. The marker is now honoured wherever it lands ahead of any
sentinel, with its reason still capped at 64 KiB.

Part of #133
@AcoPiper

Copy link
Copy Markdown
Contributor Author

[Author Round 1]

I fixed both review items on PR #136 and pushed them as 938a07e, which starts a new CI run. Locally, fmt, both clippy runs, doc and both test runs pass.

  1. A failed cd could be reported as Started — Fixed. The descent script now runs cd -- "$1" 2>/dev/null || { <marker>; exit 1; }. The shell's own error message is thrown away because it repeats the requested path, and a path containing __BOOTLER_SUDO_OK__ made settle_descent report a start for a command that never ran. Nothing the script writes before its fixed marker or sentinel now comes from the caller.

    • Test: a_missing_directory_starts_nothing_and_settles_as_such now also uses a missing path whose name contains the sentinel. For both descents and both paths it checks that stderr is exactly the no-directory marker and that the result is NoWorkingDirectory. With the old script, the new case fails.
    • Cost: the NoWorkingDirectory reason is now only what sudo and PAM wrote before the marker, which is usually nothing. It no longer carries the shell's error text, such as "No such file or directory" versus "Permission denied". The caller already knows which directory it asked for. The rustdoc on NoWorkingDirectory::reason and descent_script, and the DESCENT_ARG0 comment, say this now.
  2. More than 64 KiB of stderr turned a missing directory into a sudo refusal — Fixed. settle_descent now looks for the marker in everything before any sentinel, including when the 64 KiB limit has been passed. A marker there gives NoWorkingDirectory with its reason capped at 64 KiB. A marker that arrives after a sentinel still counts as the command's own output, so a sentinel past the limit still gives Refused. The rustdoc on settle_descent and DescentSettle::Refused is updated.

    • Tests: a marker after 64 KiB + 1 byte with no sentinel gives NoWorkingDirectory with a capped reason, whether or not the stream has ended. A marker after a sentinel that came past the limit still gives Refused.

PR description:

  • I removed the deviation for over-limit markers.
  • The reason deviation now also says that cd's error text is discarded, why, and what is lost.
  • The summary and test-plan entries match the new behaviour.
  • Closes #133 is still there.

The #[ignore] real-sudo test doesn't exercise either path, so I didn't re-run it.

@AcoPiper

Copy link
Copy Markdown
Contributor Author

[Reviewer Round 2]

[Reviewer Round 2] — Request changes.

Both Round 1 findings are resolved in the updated diff. Failed cd output is suppressed, with a regression test for a path containing the sentinel. A directory marker beyond 64 KiB now returns NoWorkingDirectory with a capped reason. The declared deviations and PR linkage are acceptable.

New finding: a sudoers denial can be reported as Started. The script passed to sudo contains the literal __BOOTLER_SUDO_OK__ (executor.rs). For a command-specific denial, sudo’s denial message includes the rejected command and its arguments. That echoes the script to stderr even though sudo never ran it. settle_descent finds the echoed sentinel and immediately returns Started (executor.rs), contradicting the required Refused outcome for a sudoers denial. The refusal test uses an “unknown user” message that does not echo argv, so it misses this path. The start announcement needs to remain distinguishable from a denial that repeats the command; add a regression test using such a denial.

@AcoPiper

Copy link
Copy Markdown
Contributor Author

[Review Verdict Round 2: NOT_APPROVED]

When sudoers denies a command, sudo repeats the whole command line on
stderr. The descent script held the start sentinel and the
no-directory marker literally, so a denial echoed them and
settle_descent reported a start for a command that never ran.

The script now prints each word from two halves, so its text holds
neither. The caller's command, arguments, profile entries and working
directory reach the same echoed line, so any of them holding either
word is refused before building. Arguments had no variant for this,
so DescentError gains InvalidArgument, which carries only the
argument's position.

Part of #133
@AcoPiper

Copy link
Copy Markdown
Contributor Author

[Author Round 2]

I fixed the Round 2 finding and pushed it as 5520909 on PR #136, which starts a new CI run. Locally, fmt, both clippy runs, doc and both test runs pass.

A sudoers denial could be reported as Started — Fixed.

  • Script: the reviewer was right. I checked with a real sudo 1.9.16p2 in a Debian container whose sudoers only lets root run /usr/bin/true. The denial message repeats the whole command line, including the descent script, which holds both __BOOTLER_SUDO_OK__ and the no-directory marker. descent_script now prints each of those two words in two halves (printf '%s%s' '__BOOTLER' '_SUDO_OK__'), so the script's text contains neither. Run again against the same real sudo, the denial message no longer contains either word.
  • Caller input: that same check showed the denial message also repeats the command, its arguments, the K=V words and cwd. So any of these that contains either word is now refused before the command is built:
    • the command gives InvalidCommand;
    • a profile entry, by name or value, gives InvalidEnvironment, and the error still never includes the value;
    • cwd gives InvalidWorkingDirectory;
    • an argument gives a new variant, InvalidArgument { index }, which carries only the argument's position. None of the issue's four variants fits an argument.
  • Deviation: the issue fixes DescentError at four variants and doesn't validate arguments, so the extra variant and the new refusals are recorded under "Deviations from the issue". Every other argument still arrives intact.
  • Tests:
    • a_denial_repeating_the_command_line_settles_as_refused uses a stub that repeats the command line as sudoers does. It expects Refused(SudoRefused) for both descent kinds and checks that the script text contains neither word. It fails with the old script.
    • a_word_holding_an_outcome_word_is_refused_before_building covers each refusal above, for both words.
    • a_missing_directory_starts_nothing_and_settles_as_such no longer uses a path containing the sentinel, because that path is now refused before building. It still checks that stderr is exactly the marker.
  • Round 1 fix kept: cd's error message is still thrown away. With such paths now refused this is belt-and-braces, but it keeps the rule that nothing the script prints before its fixed words comes from the caller.
  • Real sudo test: the script that real sudo runs changed, so I re-ran the #[ignore] test in the same container as before (sudo 1.9.16p2, nobody in the extra users group). It passed for both descent kinds with groups 65534 100, and the PR description now has this third run.
  • PR description: the summary, the validation text, the test plan and the deviations are updated, and it still has Closes #133.

Per the instructions, I posted no PR comment.

@AcoPiper

Copy link
Copy Markdown
Contributor Author

[Reviewer Round 3]

[Reviewer Round 3] — Approve. No unresolved findings.

The Round 2 denial case is fixed. The script contains neither outcome marker as a literal, and validation prevents caller supplied words from putting either marker into a denial that echoes the command line (executor.rs, executor.rs). The new denial test exercises both descent kinds (executor.rs).

The Round 1 directory failures remain resolved. The shared announcement helper preserves the channel’s limit rule, and descent settling still recognizes a directory marker beyond the limit (channel.rs, executor.rs). The declared deviations have sound reasons and match the code. The supplied PR body has the required Closes #133, test plan, and deviation record.

@AcoPiper

Copy link
Copy Markdown
Contributor Author

[Review Verdict Round 3: APPROVED]

@AcoPiper

Copy link
Copy Markdown
Contributor Author

Suggested squash commit

Title

Add profile-applying descent commands to the daemon

Body

bootler's session helper is a root process that spawns, bounds and
kills its own children, and must run them as a service account or as
the operator its session authenticated, under a pinned environment and
working directory. InDaemonExecutor could do neither: its resolution
was private, it applied no profile, and it refuses Identity::Operator.

descent_command builds, without spawning, a Command that descends
through sudo from the same site run uses, then applies the profile as
the target identity: cd into the directory, announce the start, and
exec env -i with the K=V words. Every value stays a discrete argv
word. The operator goes through sudo -u #uid -g #gid rather than
CommandExt::uid, which would drop supplementary groups and need new
unsafe to restore them.

settle_descent classifies the child's stderr. Its sentinel search and
64 KiB transport limit are factored out of the channel's judge, so
both starts are settled by one rule.

When sudoers denies a command, sudo repeats the whole command line on
stderr, and a shell's failed cd echoes the requested path. Either
would let caller-controlled text forge a start. So the script discards
cd's diagnostic and prints the sentinel and the no-directory marker
from two halves, so its own text holds neither. Any command, argument,
profile entry or working directory holding one is refused before
building; arguments get a new InvalidArgument variant, which carries
only the argument's position.

The ignored real-sudo test requires no controlling terminal, since
sudo with use_pty moves the command into a session of its own.

Also switch one test Duration to from_mins, which clippy on the
current stable toolchain requires.

Closes #133

@AcoPiper
AcoPiper merged commit 19fef3f into main Sep 28, 2026
5 checks passed
@AcoPiper
AcoPiper deleted the AcoPiper/issue-133 branch September 28, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build profile-applying descent commands in the root daemon

1 participant