reject cd prefixes in bash(): teach repo.cd()/os.chdir instead
53 unmodified lines545556575859606162636465666768697071727374757677787980818283848586871215 unmodified linesExpand all53 unmodified lines handle = bash("echo again") awaited = await handle self.assertEqual(handle.poll(), awaited)
async def test_rejects_cd_prefixes_with_chdir_guidance(self): for command in ( "cd /tmp && pwd", "cd /tmp; pwd", "cd /tmp", " cd ~ && echo hi", "echo hi && cd /tmp && pwd", "echo hi || cd /tmp", "echo hi; cd /tmp", "pwd\ncd /tmp\nls", ): with self.subTest(command=command): with self.assertRaisesRegex(ValueError, "cd"): bash(command)
async def test_allows_cd_mentions_outside_a_prefix(self): # A word containing cd, or cd inside a quoted argument, is not a prefix. handle = bash("echo \"cd foo\"") result = await handle self.assertEqual(result.exit_code, 0) self.assertIn("cd foo", result.output)
handle = bash("true") # sanity: plain commands still spawn result = await handle self.assertEqual(result.exit_code, 0)
async def test_status_pipe_survives_high_fds_and_strict_posix_shell(self): # Regression: dash rejects multi-digit fds in redirections at parse # time, so the script must never reference the raw status-pipe fd. dummies = [os.open(os.devnull, os.O_RDONLY) for _ in range(30)]1215 unmodified linesExpand all4 unmodified lines567891011121329 unmodified lines43444546474849505152535455615 unmodified linesExpand all671672673674675676677678679680681682683684685218 unmodified linesExpand all4 unmodified linesimport asyncioimport atexitimport jsonimport osimport reimport secretsimport selectorsimport shutilimport signal29 unmodified lines# Cancelled one-shot awaits: TERM grace before the group KILL, then the bounded# wait for a confirmed group exit before CancelledError propagates._CANCEL_TERM_GRACE = 0.5_CANCEL_KILL_WAIT = 2.0# A `cd` prefix in a bash() command is always a mistake: each call is its own# process, so cd there cannot move the persistent kernel cwd. Catch the leading# form and the chained form (`x && cd y`) and point at os.chdir / repo.cd._CD_PREFIX_RE = re.compile(r"^\s*cd(?:\s|$)")_CD_CHAIN_RE = re.compile(r"(?:&&|\|\||;|\n)\s*cd(?:\s|$)")
_live_handles: set["BashHandle"] = set()_live_lock = threading.Lock()_hook_installed = False615 unmodified linesExpand all handle.output()/tail(). """ if not isinstance(command, str) or not command: raise TypeError("command must be a non-empty str") if _CD_PREFIX_RE.match(command) or _CD_CHAIN_RE.search(command): raise ValueError( "bash() rejects `cd` prefixes: each bash() call runs in its own process, " "so `cd` here cannot move the persistent kernel working directory. Move it " "once with os.chdir(...) or repo.cd(...), then call bash() without the prefix. " "For git, use the git skill (git.<cmd>), which resolves the repo root itself." ) _install_shutdown_hook() return BashHandle(command)
218 unmodified linesExpand all