Skip to content

feat(tools): add read-only support for bash tool - #253

Merged
F16shen merged 4 commits into
AI-Shell-Team:mainfrom
F16shen:feat/bash-read-only-enforcement
Jun 26, 2026
Merged

feat(tools): add read-only support for bash tool#253
F16shen merged 4 commits into
AI-Shell-Team:mainfrom
F16shen:feat/bash-read-only-enforcement

Conversation

@F16shen

@F16shen F16shen commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add read-only command classification for the bash tool in aish-tools::bash (ReadOnlyVerdict, classify, is_read_only, preflight_enforce), with quote-aware segment parsing and 20+ unit tests.
  • Introduce ToolExecutionPolicy.enforce_read_only_bash and Tool::preflight_with_context so bash/secure_bash can block non-read-only commands when policy is enabled.
  • Wire /diagnose: verify commands are gated by read-only classification; failure diagnose sub-sessions set enforce_read_only_bash: true and remove duplicate shell-side readonly logic.
  • Fix ReAct diagnose spinner flicker by ignoring react_agent generation end/start for spinner stop/restart.

Main session behavior is unchanged (enforce_read_only_bash defaults to false).

Test plan

  • make format-check && make lint
  • cargo test -p aish-tools -p aish-llm -p aish-shell
  • Manual: /diagnose with a non-read-only verify command → blocked with i18n message
  • Manual: failure diagnose sub-agent attempts rm/echo > → preflight block
  • Manual: /diagnose run → thinking spinner no longer flickers mid-run

Summary by CodeRabbit

  • New Features
    • Added enhanced Bash read-only classification and matching block/unparseable outcomes.
    • Introduced context-aware tool execution policies so Bash read-only enforcement can be applied reliably during tool preflight.
  • Bug Fixes
    • Ensured read-only enforcement behavior is consistent across diagnostic and secure Bash flows when enabled.
    • Improved failure-diagnose command verification messaging and UI state handling during ReAct-style sub-sessions.
  • Documentation
    • Updated user-facing English and Chinese messages for read-only enforcement outcomes.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the pull request. A maintainer will review it when available.

Please keep the PR focused, explain the why in the description, and make sure local checks pass before requesting review.

Contribution guide: https://github.com/AI-Shell-Team/aish/blob/main/CONTRIBUTING.md

@github-actions

Copy link
Copy Markdown
Contributor

This pull request description looks incomplete. Please update the missing sections below before review.

Missing items:

  • User-visible Changes
  • Compatibility
  • Testing
  • Change Type
  • Scope

@F16shen F16shen changed the title feat(tools): bash read-only analysis and /diagnose enforcement feat(tools): add read-only support for bash tool Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2dce1c95-eed9-4e3f-98a3-348c650d04c8

📥 Commits

Reviewing files that changed from the base of the PR and between fda29ee and 1d2beb2.

📒 Files selected for processing (1)
  • crates/aish-tools/src/bash/read_only.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/aish-tools/src/bash/read_only.rs

📝 Walkthrough

Walkthrough

This PR adds session-scoped bash read-only policy plumbing, shared command classification and enforcement, shell caller updates, and react-agent event filtering in LLM callbacks.

Changes

Read-only bash policy

Layer / File(s) Summary
Tool policy plumbing
crates/aish-llm/src/tool_context.rs, crates/aish-llm/src/types.rs, crates/aish-llm/src/lib.rs, crates/aish-llm/src/session.rs
ToolExecutionPolicy and ToolContext are added, Tool::preflight_with_context is introduced, and LlmSession stores and forwards a per-session tool execution policy.
Sub-session policy propagation
crates/aish-llm/src/subsession.rs, crates/aish-llm/src/session.rs, crates/aish-llm/src/diagnose_agent.rs, crates/aish-llm/src/system_diagnose/system_diagnose.rs, crates/aish-llm/tests/llm_integration_test.rs
SubSessionConfig gains enforce_read_only_bash, create_subsession propagates the policy, and diagnose/system-diagnose sub-sessions plus tests set explicit values for the flag.
Read-only classifier and messages
crates/aish-tools/src/bash/read_only.rs, crates/aish-tools/src/lib.rs, crates/aish-i18n/locales/en-US.yaml, crates/aish-i18n/locales/zh-CN.yaml
ReadOnlyVerdict and the read-only classification helpers are added, ask_user re-exports them, and the English and Chinese locales add blocked and unparseable messages.
Bash preflight enforcement
crates/aish-tools/src/bash/bash.rs, crates/aish-tools/src/secure_bash/secure_bash.rs
BashTool and SecureBashTool route preflight through shared helpers that honor ToolContext policy, and BashTool exposes read-only classification helpers with enforcement tests.
Shell callers use shared classification
crates/aish-shell/src/ai_handler.rs, crates/aish-shell/src/app.rs
handle_failure_diagnose enables read-only bash enforcement, the local read-only guard is removed, and /diagnose verification switches to BashTool::classify_read_only and ReadOnlyVerdict.

ReAct sub-session event filtering

Layer / File(s) Summary
React-agent event filtering
crates/aish-shell/src/app.rs
react_agent_llm_event identifies react-agent sub-session events, and the main and SSH callbacks skip generation start/end handling and tool-execution overlay resets for them.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AI-Shell-Team/aish#251: Introduces the /diagnose failure-diagnosis path and local read-only guard logic that this PR replaces with shared BashTool classification and policy propagation.
  • AI-Shell-Team/aish#233: Extends the same Tool trait in crates/aish-llm/src/types.rs, adding a different hook and updating tool metadata plumbing.
  • AI-Shell-Team/aish#157: Touches the SecureBashTool preflight path that this PR refactors to accept context-aware read-only enforcement.

Suggested labels

security, tests

Poem

A rabbit hopped through policy trails,
With read-only carrots tucked in its pails.
The bash stayed quiet, the spinners stayed bright,
And react-agent whispers slipped out of sight.
Soft paws, safer commands, moonlit tales.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly summarizes the main change: adding read-only support for the bash tool.
Docstring Coverage ✅ Passed Docstring coverage is 82.24% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Add read-only command classification in aish-tools::bash, wire
ToolExecutionPolicy through preflight_with_context, and enforce it on
/diagnose verify and failure diagnose sub-sessions while leaving the
main session unchanged.
@F16shen
F16shen force-pushed the feat/bash-read-only-enforcement branch from d8bbe50 to 08f34ab Compare June 25, 2026 12:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
crates/aish-llm/src/subsession.rs (1)

29-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Lock the new default in the unit test.

false preserves existing main-session behavior, so add an assertion in test_subsession_config_default to prevent accidental drift.

Suggested test addition
     assert_eq!(config.max_context_messages, 50);
     assert_eq!(config.max_iterations, 10);
     assert!(config.system_prompt.is_none());
+    assert!(!config.enforce_read_only_bash);

Also applies to: 129-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-llm/src/subsession.rs` at line 29, Add an explicit assertion in
test_subsession_config_default to lock in the new default for
enforce_read_only_bash, since the SubsessionConfig default currently sets it to
false and should not drift. Update the unit test in subsession.rs so it verifies
the default config value alongside the other defaults, using the existing test
name and SubsessionConfig initializer as the anchor for the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aish-llm/src/session.rs`:
- Around line 1054-1056: The execute_tool_by_name path in LlmSession still
bypasses tool preflight by calling execute_async directly, so update that method
to run the same preflight step used in the tool-call loop before execution.
Reuse ToolContext::for_session(self) and call preflight or
preflight_with_context on the selected tool, then only proceed to execute_async
if the check passes so read-only bash enforcement and future ToolContext
policies apply consistently.
- Line 1265: When creating a subsession in create_subsession(), do not reset
tool_execution_policy to ToolExecutionPolicy::default(); instead inherit the
parent session’s current policy so stricter settings carry forward to child
sessions. Update the subsession construction in Session to clone or copy the
existing tool_execution_policy from the parent session, and keep the
diagnose-path override only if it is still explicitly needed for that flow.

In `@crates/aish-tools/src/bash/read_only.rs`:
- Around line 196-203: The curl checks in read_only handling are too narrow and
miss common mutating and output-writing forms. Update the logic in the curl
branch of the read-only parser to properly parse curl options in the segment
instead of relying on substring matches, and ensure the existing curl detection
path covers symbols like the mutating-method check and the output-file check so
it catches forms such as -XPOST, --request POST, -d, -o/tmp/x, and
--output=/tmp/x.
- Around line 168-211: Fix the read-only detector in read_only.rs so it does not
assume commands are safe just because the first token is not blocked. The
current logic in the command classification path only checks the first token and
then falls through to ReadOnly, which lets wrapped or prefixed mutating commands
slip through. Update the decision logic around the command parsing helper to use
a positive allowlist for known safe commands, and explicitly treat
wrappers/interpreters like command, env, sh, bash, and similar shell launchers
as non-read-only unless the full command is safely parsed. Ensure cases like
variable-prefixed commands and shell-invoked rm-style operations are classified
as unsafe.
- Around line 86-103: The bash parser in read_only.rs does not handle backslash
escapes outside quotes, so an escaped quote can be misread as starting a quoted
region. Update the token-scan logic in the quote-handling loop so backslashes
are recognized and consumed when outside single quotes as well, without letting
them toggle quoting state in the main parser flow. Make the fix in the read-only
parsing path that includes this `chars` loop and mirror the same behavior in the
related matching logic around the referenced block. Add regression tests for
escaped single quotes before separators and redirects, such as escaped-quote
cases that must still expose `;` and `>` as active operators.
- Around line 27-42: The read-only check in `read_only.rs` only removes a
leading `sudo` once before `split_compound_segments`, so later compound segments
like `sudo rm` are not analyzed correctly. Update `is_read_only_command` to
handle `sudo` stripping per segment, likely by applying `strip_sudo_prefix`
inside the loop before calling `non_readonly_segment_reason`, so each `seg` is
evaluated after removing any sudo wrapper.
- Around line 171-175: The blanket BLOCKED list in read_only.rs is overreaching
by including sed, which makes the later sed -i-specific handling in the command
validation logic unreachable. Update the bash read-only checks around the
BLOCKED constant and the sed validation path so only destructive sed usage is
blocked while read-only commands like sed -n remain allowed, keeping the
existing command parsing logic in the read_only module consistent.

---

Nitpick comments:
In `@crates/aish-llm/src/subsession.rs`:
- Line 29: Add an explicit assertion in test_subsession_config_default to lock
in the new default for enforce_read_only_bash, since the SubsessionConfig
default currently sets it to false and should not drift. Update the unit test in
subsession.rs so it verifies the default config value alongside the other
defaults, using the existing test name and SubsessionConfig initializer as the
anchor for the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1075b2b1-d3c1-4970-8e89-0c53a01dc033

📥 Commits

Reviewing files that changed from the base of the PR and between 31939c4 and 08f34ab.

📒 Files selected for processing (16)
  • crates/aish-i18n/locales/en-US.yaml
  • crates/aish-i18n/locales/zh-CN.yaml
  • crates/aish-llm/src/diagnose_agent.rs
  • crates/aish-llm/src/lib.rs
  • crates/aish-llm/src/session.rs
  • crates/aish-llm/src/subsession.rs
  • crates/aish-llm/src/tool_context.rs
  • crates/aish-llm/src/types.rs
  • crates/aish-llm/tests/llm_integration_test.rs
  • crates/aish-shell/src/ai_handler.rs
  • crates/aish-shell/src/app.rs
  • crates/aish-tools/src/bash/bash.rs
  • crates/aish-tools/src/bash/read_only.rs
  • crates/aish-tools/src/lib.rs
  • crates/aish-tools/src/secure_bash/secure_bash.rs
  • crates/aish-tools/src/system_diagnose/system_diagnose.rs

Comment thread crates/aish-llm/src/session.rs Outdated
Comment thread crates/aish-llm/src/session.rs Outdated
Comment thread crates/aish-tools/src/bash/read_only.rs Outdated
Comment thread crates/aish-tools/src/bash/read_only.rs
Comment thread crates/aish-tools/src/bash/read_only.rs Outdated
Comment thread crates/aish-tools/src/bash/read_only.rs
Comment thread crates/aish-tools/src/bash/read_only.rs Outdated
F16shen added 2 commits June 26, 2026 09:48
Strip sudo per compound segment so later segments like `sudo rm` are
classified correctly, and treat backslash escapes outside single quotes
so separators and redirects cannot be hidden behind escaped quotes.
Strip sudo per segment, handle shell wrappers and subshells recursively,
split background jobs on single &, tighten curl detection, and allow
read-only sed while blocking -i. Route execute_tool_by_name through
shared preflight and inherit tool_execution_policy in subsessions.
@github-actions github-actions Bot added size: XL and removed size: L labels Jun 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/aish-tools/src/bash/read_only.rs (3)

199-200: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Block find -exec command execution.

find . -exec rm -rf {} \; is allowed today because only -delete is checked, but -exec/-execdir/-ok can run mutating commands.

🛡️ Proposed fix
-    if base == "find" && segment.to_lowercase().contains("-delete") {
-        return Some("find delete".into());
+    if base == "find" {
+        let tokens = tokenize_shell_words(segment);
+        if tokens.iter().any(|t| {
+            matches!(
+                t.to_ascii_lowercase().as_str(),
+                "-delete" | "-exec" | "-execdir" | "-ok" | "-okdir"
+            )
+        }) {
+            return Some("find mutating or command execution".into());
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/bash/read_only.rs` around lines 199 - 200, The
find-command guard in read_only.rs only blocks "-delete", so potentially
mutating forms like "-exec", "-execdir", and "-ok" can still slip through.
Update the filtering logic in the find validation branch to detect these
execution flags in the same place where base == "find" is checked, and return a
rejection for any segment containing them so read-only mode blocks command
execution consistently.

175-176: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Normalize shell escapes before matching blocked commands.

Bash resolves quoted/escaped command words before execution, so forms like r\m -rf /tmp/x or 'r'm -rf /tmp/x execute rm but won’t match BLOCKED with the current raw split_whitespace() base extraction.

🛡️ Suggested direction
-    let first = segment.split_whitespace().next().unwrap_or("");
-    let base = first.rsplit('/').next().unwrap_or(first).to_lowercase();
+    let tokens = tokenize_shell_words(segment);
+    let first = tokens.first().map(|t| normalize_shell_word(t)).unwrap_or_default();
+    let base = first.rsplit('/').next().unwrap_or(&first).to_lowercase();

normalize_shell_word should remove shell quote delimiters and unescaped backslashes outside single quotes before command comparison.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/bash/read_only.rs` around lines 175 - 176, The
blocked-command check in `read_only.rs` is comparing the raw first token, so
escaped or quoted forms like split shell words can bypass `BLOCKED`. Update the
command extraction in the `segment.split_whitespace().next()` / `rsplit('/')`
path to normalize the first shell word before matching by removing shell quote
delimiters and unescaped backslashes outside single quotes. Apply this
normalization in the same flow used for command comparison so the
blocked-command lookup sees the executed command name, not the raw text.

148-152: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Block process substitution as command execution.

cat <(rm -rf /tmp/x) executes the inner command, but only $( and backticks are detected here, so this can bypass the read-only policy.

🛡️ Proposed fix
-    if scan_outside_quotes(segment, |window| {
-        window.starts_with("$(") || window.starts_with('`')
-    }) {
-        return Some("command substitution".into());
+    if scan_outside_quotes(segment, |window| {
+        window.starts_with("$(")
+            || window.starts_with('`')
+            || window.starts_with("<(")
+            || window.starts_with(">(")
+    }) {
+        return Some("command or process substitution".into());
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/bash/read_only.rs` around lines 148 - 152, The
command-substitution check in the read-only bash parser only recognizes "$(" and
backticks, so process substitution like "<(…)" can still execute commands.
Update the detection logic in the `scan_outside_quotes` block to also flag
process substitution patterns (for example, `<(` and `>(` if applicable) and
return the same forbidden-command result so `read_only.rs` blocks these cases
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aish-llm/src/session.rs`:
- Around line 1242-1248: The PreflightSecurityContext::fallback call is passing
one argument too many in the session security setup. Update the fallback
invocation in the security unwrap_or_else block inside session.rs to match the
defined fallback(tool_name, message, mode) signature by removing the extra None,
and make the same correction at the other fallback call site referenced by this
review.

In `@crates/aish-tools/src/bash/read_only.rs`:
- Around line 108-118: The segment parsing in split_compound_segments is
dropping a standalone background operator, which lets trailing background jobs
slip through. Update the logic around the '&' handling so a single '&' is
treated as unsafe instead of being discarded, while preserving '&&' as a normal
compound separator; add or use a has_background_operator check with the same
quote/escape handling as split_compound_segments to detect this in read_only.rs.

---

Outside diff comments:
In `@crates/aish-tools/src/bash/read_only.rs`:
- Around line 199-200: The find-command guard in read_only.rs only blocks
"-delete", so potentially mutating forms like "-exec", "-execdir", and "-ok" can
still slip through. Update the filtering logic in the find validation branch to
detect these execution flags in the same place where base == "find" is checked,
and return a rejection for any segment containing them so read-only mode blocks
command execution consistently.
- Around line 175-176: The blocked-command check in `read_only.rs` is comparing
the raw first token, so escaped or quoted forms like split shell words can
bypass `BLOCKED`. Update the command extraction in the
`segment.split_whitespace().next()` / `rsplit('/')` path to normalize the first
shell word before matching by removing shell quote delimiters and unescaped
backslashes outside single quotes. Apply this normalization in the same flow
used for command comparison so the blocked-command lookup sees the executed
command name, not the raw text.
- Around line 148-152: The command-substitution check in the read-only bash
parser only recognizes "$(" and backticks, so process substitution like "<(…)"
can still execute commands. Update the detection logic in the
`scan_outside_quotes` block to also flag process substitution patterns (for
example, `<(` and `>(` if applicable) and return the same forbidden-command
result so `read_only.rs` blocks these cases consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9eb1d7e5-b369-4013-90de-bd9dde335ef6

📥 Commits

Reviewing files that changed from the base of the PR and between 08f34ab and fda29ee.

📒 Files selected for processing (4)
  • crates/aish-llm/src/session.rs
  • crates/aish-llm/src/subsession.rs
  • crates/aish-tools/src/bash/bash.rs
  • crates/aish-tools/src/bash/read_only.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/aish-llm/src/subsession.rs
  • crates/aish-tools/src/bash/bash.rs

Comment thread crates/aish-llm/src/session.rs
Comment thread crates/aish-tools/src/bash/read_only.rs
Block trailing background operators, find -exec/-ok, process substitution,
and escaped command names; normalize shell words before BLOCKED matching.
@F16shen
F16shen merged commit 3a9e093 into AI-Shell-Team:main Jun 26, 2026
8 checks passed
@F16shen
F16shen deleted the feat/bash-read-only-enforcement branch June 26, 2026 03:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant