fix: gate task_shell_start behind allow_shell like exec_shell#2384
Conversation
task_shell_start delegates to ExecShellTool, providing the same shell execution capability as exec_shell. Previously, task_shell_start was registered unconditionally in with_runtime_task_tools while exec_shell was gated behind allow_shell, creating an inconsistent security gate. This caused the model to try exec_shell first, fail, then fall back to task_shell_start — wasting tokens and bypassing the intended security boundary. Split TaskShellStartTool and TaskShellWaitTool out of with_runtime_task_tools into a new with_runtime_task_shell_tools method, and gate both behind the allow_shell check in with_agent_tools. Closes Hmbown#2303
There was a problem hiding this comment.
Code Review
This pull request refactors the registration of shell-related task tools (task_shell_start and task_shell_wait) to ensure they are properly gated behind the allow_shell configuration. It introduces with_runtime_task_shell_tools to separate these from general runtime task tools, adds corresponding unit tests, and updates the documentation. A security review comment points out a potential bypass of the Feature::ShellTool feature flag when allow_shell is enabled, suggesting combining the gates to prevent redundant or unauthorized registration of shell tools.
| && self.config.features.enabled(Feature::ShellTool) | ||
| && self.session.allow_shell | ||
| { | ||
| builder = builder.with_shell_tools(); | ||
| builder = builder.with_shell_tools().with_runtime_task_shell_tools(); | ||
| } |
There was a problem hiding this comment.
Security Gate Bypass / Redundant Registration
There is a security and correctness issue here: the feature flag check self.config.features.enabled(Feature::ShellTool) is completely bypassed when self.session.allow_shell is true.
Why this happens:
At line 58, with_agent_tools(self.session.allow_shell) is called. If self.session.allow_shell is true, with_agent_tools unconditionally registers both with_shell_tools() and with_runtime_task_shell_tools(), regardless of whether Feature::ShellTool is enabled. This makes the check in lines 85-89 redundant and ineffective at disabling shell tools when the feature flag is off.
How to fix:
- Update line 58 to pass the combined gate:
.with_agent_tools(self.session.allow_shell && self.config.features.enabled(Feature::ShellTool))
- Remove this redundant block entirely (lines 84-89).
|
Thanks @HUQIANTAO — this is the right security/UX direction for #2303. I’m not merging this exact revision yet because it has two tiny cleanup items before it is release-safe: avoid double-registering |
The second feature-flag gate in tool_setup.rs was calling with_shell_tools() again when allow_shell was already true, causing duplicate tool registration. Remove the redundant gate since with_agent_tools() already handles the allow_shell check. Also add task_shell_wait to MODES.md alongside task_shell_start.
|
Fixed both items:
|
|
Hi @Hmbown, both cleanup items are patched:
The two gate tests pass. Could you take another look when you get a chance? Thanks! |
|
Thanks for the quick patch, @HUQIANTAO. I pushed one formatting-only follow-up so CI/lint has the exact rustfmt shape it wants. Local verification on a current-main merge simulation is green:\n\n- |
Problem
In Agent mode with default config,
exec_shellis blocked (allow_shell defaults to false) buttask_shell_startbypasses the same gate entirely. Sincetask_shell_startdelegates directly toExecShellTool, this creates an inconsistent security boundary.The model tries
exec_shellfirst (prompted by base.md), fails, then falls back totask_shell_start— wasting tokens and providing an unrestricted path to shell execution.Fix
Split
TaskShellStartToolandTaskShellWaitToolout ofwith_runtime_task_tools()into a newwith_runtime_task_shell_tools()method. Gate both behind theallow_shellcheck inwith_agent_tools()and the feature-flag gate intool_setup.rs.Also updates MODES.md to clarify that Agent mode shell access requires
allow_shell = true.Changes
registry.rs: Split shell task tools, gate behindallow_shelltool_setup.rs: Add shell task tools to feature-flag gateMODES.md: Clarify Agent mode shell requirementsCloses #2303
Greptile Summary
This PR fixes a security inconsistency where
task_shell_startandtask_shell_waitbypassed theallow_shellgate that already blockedexec_shell, creating an unintended shell execution path in Agent mode.tools/registry.rs:TaskShellStartToolandTaskShellWaitToolare split out ofwith_runtime_task_tools()into a newwith_runtime_task_shell_tools()method, then conditionally wired intowith_agent_tools()only whenallow_shellistrue. Two unit tests cover both branches.tool_setup.rs: The now-redundant feature-flag block that re-registered shell tools from outsidewith_agent_toolsis removed, eliminating the pre-existing double-registration ofexec_shell.docs/MODES.md: Agent-mode description updated to explicitly list all three gated tools and theallow_shell = truerequirement.Confidence Score: 5/5
Safe to merge. The shell-tool gating logic is correctly consolidated into with_agent_tools(), Plan mode is unaffected by its separate builder path, and YOLO mode correctly forces allow_shell=true at the session layer.
The fix is narrowly scoped — it moves two tool registrations into a guarded branch and removes a now-redundant block — and the two new unit tests directly exercise both allow_shell branches. All shell execution paths now converge on the same gate.
No files require special attention.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[build_turn_tool_registry_builder] -->|mode == Plan| B[read-only builder path\nwith_runtime_read_only_task_tools] A -->|mode == Agent / YOLO| C[with_agent_tools allow_shell] C --> D[with_file_tools\nwith_search_tools\nwith_runtime_task_tools\n... shared tools] C -->|allow_shell == false| E[builder returned\nno shell tools] C -->|allow_shell == true| F[with_shell_tools\nregisters exec_shell] F --> G[with_runtime_task_shell_tools\nregisters task_shell_start\ntask_shell_wait] B --> H[No shell tools ever registered] E --> HComments Outside Diff (1)
crates/tui/src/core/engine/tool_setup.rs, line 84-89 (link)build_turn_tool_registry_buildercallswith_agent_tools(self.session.allow_shell)at line 58, which — after this PR — already invokeswith_runtime_task_shell_tools()whenallow_shellistrue. The feature-flag block below then callswith_runtime_task_shell_tools()a second time (same forwith_shell_tools()). Both registrations land in the builder'sVec, soregister_alllater hitsHashMap::inserttwice fortask_shell_startandtask_shell_wait, emitting atracing::warn!("Overwriting existing tool: {}")for each. The same double-registration already existed forexec_shellpre-PR, so this is consistent, but the fix could instead drop thewith_runtime_task_shell_tools()call from this block (lettingwith_agent_toolsown it) to avoid the warning.Reviews (3): Last reviewed commit: "style: format shell gating tests" | Re-trigger Greptile