diff --git a/emrg/tools/bash_tool.py b/emrg/tools/bash_tool.py index 16d0c8ad..3d4e0964 100644 --- a/emrg/tools/bash_tool.py +++ b/emrg/tools/bash_tool.py @@ -145,6 +145,11 @@ def _translate_windows_heredocs(cmd: str) -> tuple[str, str | None]: # WIP) — the mutator regex above would false-positive on them because it matches # the bare `git stash` token. Exempt a pure stash read (no chain to a mutator). _GIT_STASH_READ_RE = re.compile(r"\bgit\s+stash\s+(?:list|show)(?:\s|$|\|)") +# `git worktree list` / `git submodule status` are READ-ONLY inspection (same +# false-positive class as `git stash list/show`): the mutator regex above +# matches the bare `git worktree` / `git submodule` tokens. Exempt a pure +# read; any worktree/submodule MUTATOR keyword below still blocks. +_GIT_WT_READ_RE = re.compile(r"\bgit\s+(?:worktree\s+list|submodule\s+status)\b") def _extract_write_targets(cmd: str) -> list[str]: @@ -267,19 +272,30 @@ def _check_sandbox(cmd: str, mode: str, workdir: str | None = None) -> tuple[boo # (stash / checkout . / reset --hard / clean) write no file targets # and escaped the target scan (community issue #979). Also blocks # working-tree writers: apply / am / archive / submodule / worktree. - # Pure `git stash list` / `git stash show` (read-only WIP inspection) - # are exempt — but a chain to a mutator (e.g. `&& git stash drop`) - # stays blocked. + # Pure read-only inspections are exempt (`git stash list/show`, + # `git worktree list`, `git submodule status`) — but a chain to a + # mutator (e.g. `&& git stash drop`, `; git worktree remove`) stays + # blocked. m = _GIT_MUTATOR_RE.search(cmd) or _GIT_DELETE_RE.search(cmd) - if m and not ( - _GIT_STASH_READ_RE.search(cmd) - and not re.search(r"\bgit\s+stash\s+(?:drop|pop|clear|apply|push|branch|create)\b", cmd) - and not re.search(r"&&|;", cmd) - ): - return False, ( - f"read-only sandbox: blocked git mutating command {m.group(0)!r} " - "(dirty-tree guard, community issue #979)" - ), "partial" + if m: + read_only_inspection = ( + _GIT_STASH_READ_RE.search(cmd) + or _GIT_WT_READ_RE.search(cmd) + ) + mutator_keyword = ( + re.search(r"\bgit\s+stash\s+(?:drop|pop|clear|apply|push|branch|create)\b", cmd) + or re.search( + r"\bgit\s+(?:worktree\s+(?:add|remove|move|prune|lock|unlock)|" + r"submodule\s+(?:update|add|deinit|set-url|sync|absorbgitdirs|" + r"foreach))\b", + cmd, + ) + ) + if not (read_only_inspection and not mutator_keyword and not re.search(r"&&|;", cmd)): + return False, ( + f"read-only sandbox: blocked git mutating command {m.group(0)!r} " + "(dirty-tree guard, community issue #979)" + ), "partial" return True, None, "partial" # workspace-write diff --git a/tests/test_bash_tool_sandbox.py b/tests/test_bash_tool_sandbox.py index 3ae65405..a32352b3 100644 --- a/tests/test_bash_tool_sandbox.py +++ b/tests/test_bash_tool_sandbox.py @@ -108,7 +108,9 @@ def test_check_read_only_allows_dev_null_redirect(): def test_check_read_only_allows_read_commands(): for cmd in ("ls -la", "git status", "cat file.txt", "pwd", "echo hi", "git stash list", "git stash show -p", - "git stash show stash@{0}", "git stash list | grep foo"): + "git stash show stash@{0}", "git stash list | grep foo", + "git worktree list", "git worktree list --porcelain", + "git submodule status", "git submodule status | head"): allowed, _, _ = _check_sandbox(cmd, "read-only") assert allowed is True, cmd @@ -149,6 +151,17 @@ def test_check_read_only_blocks_git_mutators(): "git archive --output=tree.tar HEAD", "git submodule update --init", "git worktree add ../wt master", + # worktree/submodule MUTATORS stay blocked (cycle 20260825-200038) + "git worktree remove ../wt", + "git worktree move ../wt ../wt2", + "git worktree prune", + "git worktree lock ../wt", + "git worktree unlock ../wt", + "git submodule add https://example.com/repo.git sub", + "git submodule deinit -f .", + "git submodule sync", + "git worktree list && git worktree remove ../wt", + "git submodule status; git submodule update", ): allowed, reason, enforcement = _check_sandbox(cmd, "read-only") assert allowed is False, f"{cmd!r} should be blocked"