Skip to content

fix(tool): reject mutating find commands as read-only#2004

Merged
DavdGao merged 4 commits into
agentscope-ai:mainfrom
VectorPeak:fix/bash-find-readonly
Jul 16, 2026
Merged

fix(tool): reject mutating find commands as read-only#2004
DavdGao merged 4 commits into
agentscope-ai:mainfrom
VectorPeak:fix/bash-find-readonly

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

AgentScope Version

Current branch based on main at 922e673b. No released version bump is involved.

Description

Fixes #2003 by preventing mutating GNU find actions from being classified as read-only before Bash.check_permissions() reaches the normal permission path.

What Problem This Solves

Bash.check_permissions() has an early allow path for commands that BashCommandParser.is_read_only_command() marks as safe. This is the right behavior for inspection-only commands such as ls, cat, grep, and ordinary find searches, because those commands should not require extra confirmation just to read or list files.

The bug was that find was treated as read-only at the command-prefix level. In _is_single_command_read_only(), the generic check accepted any command where cmd == readonly_cmd or cmd.startswith(readonly_cmd + " "). Since find was present in READ_ONLY_COMMANDS, the parser could classify a command as read-only immediately after seeing the leading find text, without checking the rest of the find expression.

That distinction matters because find is not only a search command. A find expression can contain actions: some delete matched files, some execute arbitrary commands for each match, and some write matching paths to a named output file. These examples all start with the same read-only-looking prefix, but they cross different permission boundaries:

find . -delete              # deletes matched files
find . -exec rm {} \;       # executes rm for each match
find . -fprint results.txt  # writes results to a named file

Before this fix, those commands could be classified through the same prefix path as a harmless search like:

find . -name '*.py'

The intended boundary is more precise: find should be auto-allowable only when the expression is still read-only. Once the expression contains mutating actions such as -delete, -exec, -execdir, -ok, -okdir, -fls, -fprint, -fprint0, or -fprintf, it should fall through to the normal permission decision instead of being treated like a file listing.

The fix also avoids the opposite mistake: pattern values that merely look like action names should remain read-only. For example, searching for a filename literally called -delete should not be treated as deletion:

find . -name '-delete'
find . -path './-fprint'
find . -regex '.*-exec.*'

Changes

This PR adds a small find-specific guard before the generic read-only prefix match runs. The new logic tokenizes a single command, confirms the executable is find, and checks only expression positions for mutating actions.

The mutating actions covered by this PR are:

-delete
-exec
-execdir
-ok
-okdir
-fls
-fprint
-fprint0
-fprintf

The parser also skips arguments for common one-argument find predicates so that pattern values which merely look like actions are not misclassified. These remain read-only:

find . -name '-delete'
find . -path './-fprint'
find . -regex '.*-exec.*'

The change only narrows internal Bash permission classification. It does not change Bash command execution, dangerous path lists, dedicated file tools, command parsing outside this read-only check, user-facing APIs, or documentation examples. Documentation was not updated for that reason.

Evidence

Direct parser verification covered both sides of the boundary:

find . -name '-delete'      -> read_only=True
find . -path './-fprint'    -> read_only=True
find . -regex '.*-exec.*'   -> read_only=True
find . -daystart -delete    -> read_only=False
find . -nogroup -delete     -> read_only=False
find . -delete              -> read_only=False
find . -exec rm {} \;       -> read_only=False
find . -fprint results.txt  -> read_only=False

Direct Bash.check_permissions() verification confirmed the same runtime permission behavior for the tested cases: read-only find searches still return automatic ALLOW, while mutating find actions no longer return automatic ALLOW.

Additional validation performed:

python -m py_compile src\agentscope\tool\_builtin\_bash_parser.py tests\permission_bash_parser_test.py tests\builtin_bash_test.py
git diff --check
python -m pytest tests\permission_bash_parser_test.py::BashParserReadOnlyTest::test_single_read_only_file_commands tests\permission_bash_parser_test.py::BashParserReadOnlyTest::test_mutating_find_commands_are_not_read_only tests\builtin_bash_test.py::BashToolInjectionCheckTest::test_safe_commands_pass tests\builtin_bash_test.py::BashToolInjectionCheckTest::test_mutating_find_commands_not_auto_allowed -q

The targeted Bash pytest cases were invoked on Windows and reported 4 skipped because these Bash parser test classes already have an existing win32 class-level skip. Parser behavior and Bash.check_permissions() behavior were therefore verified directly in addition to syntax and whitespace checks.

Possible call chain / impact

Before this fix, the affected path was:

Bash tool input
  -> Bash.check_permissions(...)
  -> BashCommandParser.is_read_only_command(...)
  -> generic read-only prefix match for "find "
  -> mutating find action could be treated as read-only
  -> command could skip the normal confirmation path

After this fix, mutating find actions are detected before the generic prefix allow path, so they are classified as non-read-only and continue through the normal permission flow.

Sibling surfaces checked:

  • Parser-level classification in tests/permission_bash_parser_test.py
  • Bash tool permission behavior in tests/builtin_bash_test.py
  • Read-only find searches with action-like pattern values (-name, -path, -regex)
  • Zero-argument find tests followed by mutating actions (-daystart, -nogroup, -nouser)

Checklist

Please check the following items before code is ready to be reviewed.

  • An issue has been created for this PR
  • I have read the CONTRIBUTING.md
  • Docstrings are in Google style
  • Related documentation has been updated (e.g. links, examples, etc.) in documentation repository
  • Code is ready for review

@DavdGao DavdGao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@VectorPeak, thanks for the fix. The mutating predicates list and test coverage look correct.

However, _is_mutating_find_command uses shlex.split + manual token iteration, while the rest of this file uses tree-sitter AST for command analysis (extract_file_paths, check_injection_risk, split_compound_command, etc.). Using shlex here introduces a second parsing paradigm and requires maintaining FIND_PREDICATES_WITH_ARGUMENTS just to skip arguments correctly — which is fragile (e.g., -fprintf takes 2 args but the code skips 1).

Suggest using the existing tree-sitter parser instead. This eliminates the argument-counting list entirely and aligns with the file's established pattern:

def _is_mutating_find_command(self, cmd: str) -> bool:
    """Check if a find command contains mutating predicates via AST."""
    try:
        tree = self.parser.parse(bytes(cmd, "utf8"))
    except Exception:
        return False

    root = tree.root_node
    cmd_node = self._find_first_simple_command(root)
    if cmd_node is None:
        return False

    name_node = cmd_node.child_by_field_name("name")
    if name_node is None or name_node.text.decode("utf8") != "find":
        return False

    for child in cmd_node.children:
        if child.type == "word":
            text = child.text.decode("utf8")
            if text in FIND_MUTATING_PREDICATES:
                return True

    return False

This way only FIND_MUTATING_PREDICATES is needed; FIND_PREDICATES_WITH_ARGUMENTS can be removed.

@VectorPeak

Copy link
Copy Markdown
Contributor Author

@DavdGao Updated as suggested: _is_mutating_find_command now uses the existing tree-sitter AST parser, and FIND_PREDICATES_WITH_ARGUMENTS has been removed.

@DavdGao DavdGao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@DavdGao
DavdGao merged commit 7295c98 into agentscope-ai:main Jul 16, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state: awaiting changes Await changes in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

find mutating actions can be classified as read-only

2 participants