Merge PR #7696: fix(coding-agent): stop Python eval shell children inheriting the runner control channel; resolve Git Bash on Windows (@metaphorics)

This commit is contained in:
can1357
2026-08-05 21:50:24 +02:00
4 changed files with 76 additions and 5 deletions
+38 -1
View File
@@ -40,6 +40,7 @@ import os
import re
import runpy
import shlex
import shutil
import signal
import subprocess
import sys
@@ -576,8 +577,10 @@ class _BoundedLineScanner:
def _magic_pip(args: str) -> None:
argv = shlex.split(args) if args else ["--help"]
cmd = [sys.executable, "-m", "pip", *argv]
# stdin=DEVNULL: see _run_shell_body.
proc = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
@@ -737,9 +740,37 @@ def _magic_run(args: str) -> None:
_STATE.user_ns[name] = value
def _resolve_bash() -> str:
if os.name != "nt":
return "/bin/bash"
# Prefer Git Bash over WSL's System32 bash.exe, which runs inside a
# separate Linux environment and does not share the Windows filesystem
# layout or PATH.
for env_var, suffix in (
("ProgramFiles", r"Git\bin\bash.exe"),
("ProgramFiles(x86)", r"Git\bin\bash.exe"),
("LOCALAPPDATA", r"Programs\Git\bin\bash.exe"),
):
root = os.environ.get(env_var)
if root:
candidate = os.path.join(root, suffix)
if os.path.isfile(candidate):
return candidate
found = shutil.which("bash")
if found and "system32" not in found.lower():
return found
# WSL's System32 bash.exe runs in a separate Linux environment, so
# silently falling back to it would execute the cell somewhere the user
# did not intend; fail loudly instead.
raise RuntimeError(
"%%bash requires a POSIX bash, but none was found. "
"Install Git for Windows or add a non-WSL bash to PATH."
)
@cell_magic("bash")
def _magic_cell_bash(args: str, body: str) -> int:
return _run_shell_body(body, shell_arg="/bin/bash")
return _run_shell_body(body, shell_arg=_resolve_bash())
@cell_magic("capture")
@@ -780,8 +811,12 @@ def _magic_cell_writefile(args: str, body: str) -> str:
def _run_shell_body(body: str, *, shell_arg: str) -> int:
# stdin=DEVNULL: children must not inherit the runner's stdin, which is
# the host's NDJSON control channel (a reading child would steal frames,
# and inheriting the pipe deadlocks nested interpreters on Windows).
proc = subprocess.Popen(
[shell_arg, "-c", body],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
@@ -821,9 +856,11 @@ class _ShellResult(list):
def __omp_shell(cmd: str) -> _ShellResult:
# stdin=DEVNULL: see _run_shell_body.
proc = subprocess.Popen(
cmd,
shell=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)