Skip to content

feat(deps): cargo-deny gate, unsafe guard, command.run env fix, edition 2024 - #159

Merged
github-actions[bot] merged 29 commits into
mainfrom
feat/rust-standards-edition-2024
Oct 31, 2026
Merged

github-actions[bot] merged 29 commits into
mainfrom
feat/rust-standards-edition-2024

Conversation

@brujack

@brujack brujack commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • cargo-deny is the single advisory gate. The cargo-audit job (id kept as the required-check name) now runs cargo deny check advisories licenses bans sources; licenses, bans and sources were enforced nowhere before. deny.toml sets unmaintained = "all" and unsound = "all": the unsound default of "workspace" hid RUSTSEC-2026-0306 (faster-hex) and RUSTSEC-2026-0205 (scc), which only cargo-audit reported. Both are cleared by bumps (serial_test 4, faster-hex 0.10.1), no new ignore. The weekly cargo-audit-scheduled.yml (with its drifted second ignore list) is replaced by cargo-deny-scheduled.yml.
  • unsafe_code = "deny" in lib, app, jsonschemagen; every remaining unsafe block carries #[expect(unsafe_code, reason)] stating its real invariant. Two backlog rows record the Lua plugin trust model and a duplicated macOS stat helper.
  • command.run env fixed. env: now goes to the child through Exec.environment instead of set_var on the etch process. Before: inherited values were deleted rather than restored, a failed step leaked env: to later actions, sudo never received it, and Debug printed the values. An unprivileged program is resolved on the step's own env: PATH. privileged: true with env: is refused at plan time: the privilege provider would put the values on its command line (ps, sudo logs). No etch-config manifest combines them.
  • Test env mutation only through test_helpers::EnvGuard (one unsafe block, restores on drop and on panic); tests/test_env_guard_serial.py requires every guard user to be #[serial] and catches let _ = misuse.
  • Edition 2024 in all four crates, no rust-version; rustfmt.toml pins style_edition = "2021" so this PR carries no reformat (the flip is PR2).
  • ADR-0018 records the decisions. Spec: docs/cursor/specs/2026-10-31-rust-standards-edition-2024-spec.md; plan: docs/cursor/plans/2026-10-31-rust-standards-edition-2024-plan.md.

Test Plan

  • mutation-pr fit: 58 mutants, --jobs 2, 490 s locally — 43 caught, 0 missed, 15 unviable (macOS-only chflags.rs/flags.rs excluded, Backlog row)
  • make test — 1185 Rust tests + 197 Python tests pass (Ubuntu 26.04, matches ubuntu-latest)
  • cargo test -p etch-lib (threaded runner) — 1041 pass
  • cargo deny check advisories licenses bans sources — all ok
  • macOS cross-clippy (cargo clippy --target aarch64-apple-darwin --workspace --all-targets, -D warnings) — clean, with a positive control
  • make fuzz-manifest FUZZ_TIMEOUT=10 — 205668 runs, no crash
  • V4 on the Mac Studio (post-merge, main at f9cc33c, rustc 1.99.1, fresh clone): cargo clippy --all-targets -D warnings exit 0; cargo test --workspace 1219 passed, 0 failed (cargo-nextest not installed there)
  • V9 on the Mac Studio (Darwin arm64): /usr/bin/env PATH=/tmp/p etchprobe prints ok
  • Coverage: Rust floor 81 / 84.25% local tarpaulin (5429/6444 lines); Python floor 90 / 94.34%

Evidence (spec V items)

  • V2 mutation controls: GPL fixture relabelled MIT fails the rejection test; empty --config fails the MIT (config-loaded) test; a scratch --ignore RUSTSEC fails the one-list test; a removed #[expect] fails make lint; which() in place of which_in fails the same-step resolution test. Phase 3 added: env sort removal fails 20/20; a fallback to etch's PATH fails resolve_program_misses_when_step_path_lacks_program.
  • V6 (base tree, before the fix): (a) restore case read unset (expected orig); (b) failed-step case read x; (c) PATH override broke the next action: "Command sh not found in path"; (d) same-step resolution passed.
  • V8 (2021-tree pre-check): 0 set_var/remove_var; 4 gen; 1 if let; 13 drop-order warnings at 12 sites, all in the spec's Problem 4 files.
  • V3 drop-order sites: apply.rs trace! join (String), rollback.rs if let Err (anyhow::Result), status.rs where/plan match (rhai/plan Result), copy.rs render_str (Result), actions/mod.rs eval_with_scope (Result<bool, Box>), exec.rs info! join (String), chmod.rs/chown.rs/load.rs error! (path Display), plugin/globals.rs json→lua (Resultmlua::Value), contexts/mod.rs trace! ×2 (String). No lock guard, RefCell borrow or side-effecting Drop; no behaviour change; no action.
  • V10: on main's lockfile, cargo-audit lists 0205/0306 as unsound and cargo-deny with unsound = "all" errors on exactly those two; on this branch's lockfile neither appears.
  • Perf: 4 Criterion benchmarks vs a fresh main baseline, worst 0.99x.

Code review

reviewed-sha: 1b5db1f
diff-hash: 120dda07ae632e7b472592fbdaffaa21223ac002944b4bf665b4139c9e76aeee
reviewers: 5/5

Three rounds, 5/5 reviewers each.

  • Round 1 (dd5fc6a): privileged command.run env: became sudo env K=V … cmd, exposing values in ps, /proc/<pid>/cmdline and sudo logs (scored 75). Operator chose fail closed; fixed in f3296b9 (plan() refuses privileged: true with env:; spec R19 amended, R21 added).
  • Round 2 (a288726): no required or informational findings; a stale backlog row (below threshold) deleted in a288726.
  • Round 3 (8f905e9, delta since round 2: test fixes c5b52c2, record_state extraction e9f9e59): one comment-accuracy finding on the record_state doc (it cited a too_many_lines ratchet the repo does not configure); comment-only fix in a849809. After round 3 the only non-docs change is 1589263, one .gitignore line (tests/fixtures/**/Cargo.lock) removing tool residue; no code changed.
  • Round 4 (1 Opus reviewer, delta 1589263..3c060a9 only, after CI mutation-pr timed out): the mutation-pr fit (timeout-minutes: 30, --jobs 2, .cargo/mutants.toml excluding the two macOS-only files) and two tests killing the CI-missed mutants. Two low findings fixed in 1b5db1f: the schema test now asserts an independent literal, and the backlog row names both mutation workflows.

Required

Informational

Kept

RESOLVED: 4 fixed, 0 kept

Spec alignment

  • spec: docs/cursor/specs/2026-10-31-rust-standards-edition-2024-spec.md
  • anchor: ebd29c1
  • in scope: R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12, R13, R14, R15, R16, R17, R18, R19, R20, R21
  • out of scope: none

Findings

ID Reviewer Verdict Reason Amendment
R18 both NOT-BUILT I looked for rustfmt.toml changing style_edition to "2024" plus a formatting-only diff; this diff creates rustfmt.toml with style_edition = "2021" and a comment deferring the flip to a separate PR. - R18 reviewed: R18 is labelled [PR2]; this diff is PR1, which pins style_edition = "2021" per R17 and Delivery, and the flip ships as its own reformat-only PR after this merges. — deferred by design, not a deviation.

Reviewed

  • R18 reviewed: R18 is labelled [PR2]; this diff is PR1, which pins style_edition = "2021" per R17 and Delivery, and the flip ships as its own reformat-only PR after this merges. — deferred by design, not a deviation.

Verifications

  • V1: no evidence recorded
  • V2: Run in Phase 2: GPL fixture relabelled MIT fails test_gpl_fixture_is_rejected; empty --config fails the MIT case; scratch --ignore RUSTSEC-0000-0000 fails the one-list test; removed #[expect] in clone.rs fails make lint (usage of an unsafe block); which() in place of which_in fails execute_resolves_program_on_environment_path.
  • V3: All 12 drop-order sites classified in Task 11: temporaries are String, Result, Display adapters and mlua registry refs; no lock guard, RefCell borrow or side-effecting Drop; no behaviour change; no action. Reviewed inline by orchestrator for plugin/globals.rs and actions/mod.rs.
  • V4: Partial: macOS cross-clippy (cargo clippy --target aarch64-apple-darwin --workspace --all-targets, -D warnings, zigbuild wrappers) exits 0, with a positive control (removed #[expect] in chflags.rs fails). macOS nextest run not done: Mac Studio not reachable by ssh alias from claude; operator to run.
  • V5: make fuzz-manifest FUZZ_TIMEOUT=10 exit 0 under edition 2024: 205668 runs in 11 s, no crash.
  • V6: Base tree: (a) left "unset" right "orig"; (b) left "x" right "orig"; (c) Command sh not found in path, exit 1; (d) passed.
  • V7: no evidence recorded
  • V8: 2021-tree pre-check: 0 set_var/remove_var; 4 gen (gen_completions.rs 2, utilities/mod.rs 2); 1 if-let (providers/git.rs); 13 drop-order warnings at 12 sites, all in Problem 4's files.
  • V9: Not run: needs the Mac Studio (operator).
  • V10: main lockfile: cargo audit lists RUSTSEC-2026-0205 and -0306 as unsound; cargo deny with unsound=all errors on exactly those two. PR1 lockfile: neither appears in cargo audit or cargo deny output.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo

brujack and others added 26 commits October 31, 2026 16:03
Avoids an xfail placeholder; the test goes RED against the old
scheduled workflow and green when Task 4 deletes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
licenses, bans and sources were never checked anywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
unsound defaults to "workspace" in cargo-deny, hiding RUSTSEC-2026-0306
and RUSTSEC-2026-0205 that cargo-audit reports. serial_test 4 and an
autocfg/faster-hex lock bump clear both. Fixture tests prove deny.toml
loads (MIT accepted) and rejects GPL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Each remaining unsafe block carries #[expect(unsafe_code, reason)], so
a removed block fails lint instead of leaving a stale allow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
One advisory ignore list (deny.toml). The weekly run still catches
advisories published against an unchanged lockfile; stale
RUSTSEC-2026-0118/0119 ignores go with the old file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Unprivileged steps whose env: sets PATH now find their program on it,
so command.run can stop mutating the etch process environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
command.run applied env: with set_var before the step and remove_var
after it: inherited values were deleted rather than restored, a failed
step leaked them to later actions, sudo never received them, and Debug
printed them. env: now goes to Exec.environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
One unsafe block holds every test env mutation; the guard restores on
drop, including on panic. tests/test_env_guard_serial.py keeps guard
users serialised and catches let _ = misuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Every test env mutation now goes through EnvGuard. Four closure
helpers that set PATH are inlined so the serial source check sees
each guard's test, and two unserialised env tests gain #[serial].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
style_edition stays 2021 here so this commit carries no reformat;
the 2024 style flip ships as its own PR. Drop-order sites were read
and classified (see PR body).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
The privilege provider would carry env values on its command line,
exposing them in ps and sudo logs. Refusing is the only option that
neither leaks nor silently drops env:.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
The workspace is on edition 2024; MSRV is deliberately not declared
(spec N1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
The edition-2024 let-chain rewrite grew execute from 297 to 298 lines,
tripping the clippy::too_many_lines ratchet. Behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
A cargo tool run against a fixture manifest writes Cargo.lock beside
it; the fixtures are policy controls, never built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
@github-actions
github-actions Bot enabled auto-merge (squash) October 31, 2026 22:29
brujack and others added 3 commits October 31, 2026 18:45
66 mutants exceeded the 10-minute job; run 2 jobs under a 30-minute
cap. Linux CI cannot compile the macOS-only chflags/flags paths, so
their mutants are excluded (backlog row). Adds tests that kill the
two CI-missed mutants: the --label filter and CustomPathBuf schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
The expectation re-derived the schema the way the impl does. Also name
both mutation workflows in the macOS-exclusion backlog row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RMRWKKn1ZnoEgzb3hX8ewo
@github-actions
github-actions Bot merged commit ce5cbf6 into main Oct 31, 2026
14 of 15 checks passed
@brujack
brujack deleted the feat/rust-standards-edition-2024 branch October 31, 2026 23:22
github-actions Bot pushed a commit that referenced this pull request Nov 4, 2026
…_program (#165)

resolve_program() returns a full absolute path (e.g. /usr/bin/sudo), not a
bare command name. The original check

    if command.eq("doas") || command.eq("sudo") || command.eq("run0") { ... }

was introduced in #159 without updating for the new full-path return value.
The comparison always evaluates to false, so self.elevate() (which calls
`sudo --validate` with inherited stdin to warm the credential cache) is never
called for privileged steps.

Trigger: a privileged: true non-streaming command.run step runs with expired
sudo credentials. Because elevate() is not called first, sudo runs with stdin
piped (not inherited, Command::output() default), cannot prompt, and fails
with "sudo: a password is required" rather than prompting the user.

Fix: replace the bare-name check with self.is_elevated(), which is the exact
semantic that was intended (privileged=true && not root) and covers all three
providers (sudo, doas, run0) correctly regardless of their resolved path.

Regression test: resolve_program_returns_full_path_not_bare_name asserts
the return value is an absolute path so this bug cannot silently re-appear.


Claude-Session: https://claude.ai/code/session_01RMRWKLTCW3D6caXjkjvCiL

Co-authored-by: Claude <noreply@anthropic.com>
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.

1 participant