fix(tools): enforce exec allowlist when approval_mode is off#662
Conversation
ApprovalManager::check_command short-circuited to Proceed in ApprovalMode::Off without consulting the configured allowlist, silently disabling the primary exec security control in every headless deployment (approval_mode = "never" is the only viable mode for autonomous agents, since on-miss hangs forever waiting for a human approver). In Off mode with a non-empty allowlist, now enforce: safe bins + allowlist matches Proceed, everything else is denied with a clear error. An empty allowlist preserves historical unrestricted semantics so existing deployments are unaffected. SecurityLevel::Full still bypasses the list. Fixes #654. Entire-Checkpoint: d4cff416df40
Greptile SummaryThis PR fixes a security regression (#654) where Confidence Score: 5/5Safe to merge — the fix is correct, all new branches are covered by regression tests, and backward compatibility is preserved via the empty-allowlist fast path. No P0 or P1 issues found. The logic for Off mode (dangerous-deny, allowlist enforcement, safe-bin bypass with warn, empty-allowlist no-op) is consistent and all edge cases have dedicated tests. The interaction between SecurityLevel::Full and the dangerous-pattern safety floor is correct and explicitly covered. Template docs are in sync with the new behavior. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[check_command] --> B{dangerous pattern\nmatches?}
B -- yes, not allowlisted --> C{ApprovalMode::Off?}
C -- yes --> D[Err: dangerous command\ndenied in off mode]
C -- no --> E[NeedsApproval]
B -- yes, allowlisted --> F[debug log, continue]
B -- no --> F
F --> G{SecurityLevel?}
G -- Deny --> H[Err: security level deny]
G -- Full --> I[Proceed]
G -- Allowlist --> J{ApprovalMode?}
J -- Always --> K[NeedsApproval]
J -- OnMiss --> L{safe bin or\nallowlist match or\nprev approved?}
L -- yes --> I
L -- no --> K
J -- Off --> M{allowlist empty?}
M -- yes --> I
M -- no --> N{matches\nallowlist?}
N -- yes --> I
N -- no --> O{is safe bin?}
O -- yes --> P[warn log\nProceed]
O -- no --> Q[Err: not in allowlist]
Reviews (3): Last reviewed commit: "fix(tools): warn when safe-bin bypasses ..." | Re-trigger Greptile |
Merging this PR will degrade performance by 23.1%
Performance Changes
Comparing Footnotes
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Addresses Greptile P1 on #662: the dangerous-pattern safety floor returned NeedsApproval even in ApprovalMode::Off, so a headless agent hitting `rm -rf /`, `git reset --hard`, etc. would block forever waiting for a human approver that never arrives — the exact scenario this PR set out to make safe. Deny with a clear error in Off mode; OnMiss/Always still escalate to NeedsApproval. Explicit allowlist override still wins (preserved by test_dangerous_overridden_by_allowlist). Entire-Checkpoint: fa683203b181
|
Addressed Greptile P1 in f30a0d0: the dangerous-pattern safety floor now returns Renamed Re P2 ( |
Addresses Greptile P2 on #662: when an operator sets a non-empty allowlist under approval_mode=off, safe bins (cat, grep, sed, awk, ...) still bypass the list so ops don't have to enumerate common read-only utilities. Emit a warn! on that path so strict-posture deployments can detect the gap at runtime by grepping logs. Also tightened the stale doc comment on the dangerous-pattern guard — it no longer "forces approval regardless of mode" now that Off mode denies. Entire-Checkpoint: cf7a6246e6ca
|
Addressed both findings from the latest Greptile pass in 07374e9: P2 — Stale doc comment: rewrote the safety-floor comment to distinguish 34 tests still pass, fmt + clippy clean. Existing |
|
Re CodSpeed: the regressed benchmark is Greptile's latest pass (commit 07374e9) is 5/5 "Safe to merge" with no P0/P1/P2 findings, so I'll acknowledge the CodSpeed report rather than chase it. If we see |
Summary
Fixes #654.
ApprovalManager::check_commandshort-circuited toProceedinApprovalMode::Offwithout consulting the configuredallowlist, silently disabling the primary exec security control in every headless deployment. Sinceon-misshangs forever waiting for a human approver that will never arrive,approval_mode = "never"is the only viable mode for autonomous agents — which is exactly where the user-configured allowlist was being ignored.Offmode with a non-empty allowlist, enforce it: safe bins + allowlist matches proceed, everything else is denied with a clear error.SecurityLevel::Fullstill bypasses the list (early return before the mode match).SecurityLevel::Denystill denies everything.rm -rf /,git reset --hard,DROP TABLE) now denies inOffmode instead of returningNeedsApproval, so headless agents fail fast instead of hanging forever (addresses Greptile P1 on this PR).Config template comment updated so users know the allowlist is now enforced under
approval_mode = "never"when non-empty.Validation
Completed
cargo test -p moltis-tools approval— 34 tests pass (7 new)cargo +nightly-2025-11-30 fmt --all -- --checkcargo +nightly-2025-11-30 clippy -p moltis-tools --all-targets -- -D warningscargo +nightly-2025-11-30 clippy -p moltis-config --all-targets -- -D warningsRemaining
just lint(blocked locally by pre-existing CUDA/CMake issue inllama-cpp-sys-2— unrelated to this change; CI runs the OS-aware path)just test(CI)just release-preflight(CI)Manual QA
moltis.toml:git status→ proceeds (allowlist match).echo hi→ proceeds (safe bin).curl https://example.com→ denied withexec denied: command not in allowlist (approval_mode=off): curl ...(previously: silently ran).rm -rf /(dangerous, not in allowlist) → denied withexec denied: dangerous command pattern 'rm -r on filesystem root' (approval_mode=off): rm -rf /(previously: hung forever).allowlist = []withapproval_mode = "never"→ unrestricted (existing behavior preserved).New test cases
test_approval_off_with_allowlist_matchtest_approval_off_with_allowlist_miss_deniestest_approval_off_with_allowlist_safe_bintest_approval_off_empty_allowlist_unrestrictedtest_approval_off_full_security_bypasses_allowlisttest_dangerous_denied_when_mode_off(renamed fromtest_dangerous_forces_approval_when_mode_off)test_dangerous_denied_when_mode_off_full_securityFollow-ups (out of scope)
approval_mode = "never"+ empty allowlist +security_level = "allowlist"(belt-and-suspenders UX from issue [Bug]: tools.exec.allowlist is silently ignored when approval_mode = "off" #654's Option 3).SAFE_BINSdoes not bypass user-configured allowlists (Greptile P2 — would break existingon-missdeployments, needs to be opt-in).