Repository navigation
Conversation
…ecks `extract_file_paths` skipped every argument that starts with a dash, so a target directory written in attached form (`cp -tDIR`, `cp --target-directory=DIR`) vanished from the extracted paths. The dangerous-path check and the ACCEPT_EDITS/DONT_ASK working-directory check both consume that list, so `cp -t../outside ./note.txt` and `cp -t~/.ssh ./note.txt` were auto-allowed. Recognize the attached target directory for cp/mv/ln in both the tree-sitter path and the token-based fallback. The separated spelling (`cp -t DIR`) keeps working unchanged. Fixes agentscope-ai#3663
2 tasks done
…e too The attached value was only recognised when it was unquoted and spelled out. A quoted attached value is a concatenation node in the tree-sitter grammar, and a quoted separated value is a raw_string node, so both were still skipped; an abbreviated long option such as --target-d=... was not recognised either. GNU long options may be abbreviated as long as the abbreviation stays unambiguous. The target directory of cp/mv/ln is now visible in every spelling: attached or separated, quoted or unquoted, spelled out or abbreviated. Fixes agentscope-ai#3663
Contributor
Author
|
Revised after a self-review of my own change: the first version of the fix only covered the unquoted, spelled-out attached value, and three further spellings were still slipping through.
The second commit ( No change to the scope of the fix: still only |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
Bash.check_permissionsdecides whether a filesystem command may run unattended from the paths returned byBashCommandParser.extract_file_paths. Two safety checks consume that list: the bypass-immune dangerous-path check (_bash.py, step 4) and the ACCEPT_EDITS / DONT_ASK working-directory check (_bash.py, step 6).extract_file_pathsskips every argument that starts with a dash:GNU
cp,mvandlnaccept the target directory as an option value, and the value may ride along with the option token:cp -t<dir>andcp --target-directory=<dir>. Such a token starts with a dash, so the directory it names is dropped together with the option and never reaches either check. The separated spelling (cp -t <dir>,cp --target-directory <dir>) works, because there the value is a bare word.As a result, in
ACCEPT_EDITSandDONT_ASKa singlecp/mvmay write into a directory outside the working directory, or into a dangerous directory such as~/.ssh, without any confirmation.This is not the compound-command shape fixed by #3514 / #3519 (merged 2026-11-02): that fix requires every subcommand of a compound command to be a filesystem command and touched only
_bash.py, leavingextract_file_pathsuntouched. The commands below are single commands that pass that guard.Root cause
extract_file_pathstreats "starts with a dash" as "is a flag", but an option may carry its value inline. The parser already handles this shape forsed(check_sed_constraintssplits--expression=<value>withpartition("=")), but the generic path extraction did not.Fix
_bash_parser.pynow recognizes the target directory ofcp/mv/lnin every spelling a shell accepts, and keeps it in the extracted paths:cp -t<dir>,cp --target-directory=<dir>.cp -t <dir>,cp --target-directory <dir>(the value is the next argument).cp -t'<dir>',cp --target-directory='<dir>',cp -t '<dir>'— in the tree-sitter grammar a quoted attached value is aconcatenationnode and a quoted separated value is araw_stringnode, so neither is a plainwordand both used to be skipped.cp --target-d=<dir>— GNU long options may be abbreviated as long as the abbreviation stays unambiguous, and--target-directoryis the onlyt-initial long option ofcp/mv/ln.cp -rt<dir>— the value of-tmay follow other value-less short flags. An uppercaseTis never treated as a value carrier (ln -Tmeans--no-target-directory).Only
cp,mvandlnare considered, and only when the preceding characters of a short option cluster are known value-less flags. The rule is applied in both extraction paths: the tree-sitter walk (_extract_paths_from_node) and the token-based fallback (_extract_paths_fallback), so the directory stays visible even when the shell syntax cannot be parsed.Because the extracted list is the single input of both checks, the directory now reaches the dangerous-path check (bypass-immune ASK) and the working-directory check (no auto-allow) at once. A target directory inside the working directory is still auto-allowed.
Before / After
All rows below were observed on
main@9c006f3withPermissionContext(mode=PermissionMode.ACCEPT_EDITS);DONT_ASKshares the same branch.cp -t../outside ./note.txtallowpassthroughcp --target-directory=../outside ./note.txtallowpassthroughmv -t../outside ./note.txtallowpassthroughcp -t'<dir>' ./note.txt(quoted)allowpassthroughcp --target-directory='<dir>' ./note.txt(quoted)allowpassthroughcp -t '<dir>' ./note.txt(quoted, separated)allowpassthroughcp --target-d=../outside ./note.txt(abbreviated)allowpassthroughcp -t<home>/.ssh ./note.txtallowask(bypass-immune)cp -t ./sub ./note.txt(inside working dir)allowallowcp -t ../outside ./note.txt(separated)passthroughpassthroughThe
allowreason was "all target paths are within a working directory" in every auto-allowed case above: the target directory in the option value never reached the check.Note that this deliberately tightens
cp -t /tmp/out fileand friends — a target directory outside the working directory is no longer auto-allowed. That is the same outcome the separated spelling already had, so the two spellings now behave identically.Tests
The regression tests are added to the existing test files, not to new ones.
tests/permission_bash_parser_test.py(BashParserFilePathsTest):test_attached_target_directory_option—cp -t../outside,cp --target-directory=../outside,mv -t../outside,ln -t../outside,cp -r -t../outside, the separatedcp -t ../outside, the quotedcp -t'../outside',cp --target-directory='../outside',cp --target-directory '../outside'and the abbreviatedcp --target-d=../outsideall extract the same paths.test_attached_target_directory_option_fallback— the token-based fallback keeps the attached target directory visible.tests/builtin_bash_test.py(BashToolTest):test_attached_target_directory_not_auto_allowed— the attached, quoted and abbreviated spellings are not auto-allowed inACCEPT_EDITSandDONT_ASK.test_attached_target_directory_inside_working_dir— a target directory inside the working directory stays auto-allowed.test_attached_target_directory_dangerous_path_asks—cp -t<home>/.sshtriggers the bypass-immune dangerous-path ASK.Red on the unpatched base (
main@9c006f3), green with this change:The failing assertions on the unpatched base were exactly the expected ones, e.g.
AssertionError: <PermissionBehavior.ALLOW: 'allow'> != <PermissionBehavior.ALLOW: 'allow'>forcp -t../outside ./note.txt, andAssertionError: <PermissionBehavior.ALLOW: 'allow'> != <PermissionBehavior.ASK: 'ask'>forcp -t<home>/.ssh ./note.txt.black --line-length=79,flake8 --extend-ignore=E203andgit diff --checkare clean on the changed files.Fixes #3663