Translate PWD for WSL-launched Windows agents (#7111)

Bridge PWD through WSLENV /p when launching a Windows npm shim from WSL so project-root discovery uses the live cwd. The no-launch recipe adds PWD/p without freezing PWD; the concrete cwd override applies only on direct launch.
This commit is contained in:
Lee Jackson 2026-07-20 05:05:22 +01:00 committed by GitHub
commit 39497e6516
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 28 additions and 4 deletions

View file

@ -1274,6 +1274,16 @@ def _install_agent(name: str, install_hint: str) -> Optional[str]:
return executable
def _wsl_shim_env(command: list, env: dict, unset_env: tuple) -> tuple[dict, tuple]:
wsl_env_bridge = _wsl_bridge_names(env, unset_env) if _wsl_windows_executable(command) else ()
if not wsl_env_bridge:
return env, wsl_env_bridge
# Bridge PWD via WSLENV (PWD/p) so the Windows shim finds its project root from the
# live cwd, not a stale inherited Linux PWD. Don't freeze env["PWD"]: a --no-launch
# recipe must translate the live PWD when run, not when generated; _launch overrides it.
return env, (*wsl_env_bridge, "PWD/p")
def _launch(
command: list,
env: dict,
@ -1283,9 +1293,11 @@ def _launch(
executable = shutil.which(command[0]) or _install_agent(command[0], install_hint)
if executable is None:
_fail(f"`{command[0]}` not found on PATH. Install it with: {install_hint}")
wsl_env_bridge = _wsl_bridge_names(env, unset_env) if _wsl_windows_executable(command) else ()
env, wsl_env_bridge = _wsl_shim_env(command, env, unset_env)
child_env = dict(os.environ)
if wsl_env_bridge:
# Override stale inherited PWD with the real cwd so the shim resolves the project root.
env = {**env, "PWD": os.getcwd()}
child_env["WSLENV"] = _merge_wslenv(child_env.get("WSLENV", ""), wsl_env_bridge)
for name in unset_env:
child_env[name] = ""
@ -1353,8 +1365,8 @@ def _run(
if launch and clear_screen:
click.clear()
typer.echo(f"Unsloth {base} · model {entry['id']}")
wsl_env_bridge = _wsl_bridge_names(env, unset_env) if _wsl_windows_executable(command) else ()
if not launch:
env, wsl_env_bridge = _wsl_shim_env(command, env, unset_env)
_print_env(env, command, unset_env = unset_env, wsl_env_bridge = wsl_env_bridge)
return
try:

View file

@ -476,8 +476,10 @@ def test_connect_claude_launch_scrubs_conflicting_auth_env(fake_studio, monkeypa
reason = "WSL-from-Linux scenario (calling a Windows agent .exe from inside WSL); "
"os.name is 'posix' under WSL, so this path can't run on a native Windows runner.",
)
def test_connect_claude_windows_shim_from_wsl_bridges_env(fake_studio, monkeypatch):
def test_connect_claude_windows_shim_from_wsl_bridges_env(fake_studio, monkeypatch, tmp_path):
captured = {}
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("PWD", "/stale/outer/repo")
monkeypatch.setenv("WSL_DISTRO_NAME", "Ubuntu")
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-anthropic-stale")
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "oauth-stale")
@ -505,6 +507,8 @@ def test_connect_claude_windows_shim_from_wsl_bridges_env(fake_studio, monkeypat
assert captured["env"]["ANTHROPIC_AUTH_TOKEN"] == "sk-unsloth-feedfacefeedface"
assert captured["env"]["ANTHROPIC_BASE_URL"] == BASE
assert captured["env"]["ANTHROPIC_MODEL"] == MODEL["id"]
assert captured["env"]["PWD"] == str(tmp_path)
assert "PWD/p" in captured["env"]["WSLENV"].split(":")
for name in (
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
@ -520,7 +524,11 @@ def test_connect_claude_windows_shim_from_wsl_bridges_env(fake_studio, monkeypat
reason = "WSL-from-Linux scenario (calling a Windows agent .exe from inside WSL); "
"os.name is 'posix' under WSL, so this path can't run on a native Windows runner.",
)
def test_connect_claude_no_launch_windows_shim_from_wsl_prints_wslenv(fake_studio, monkeypatch):
def test_connect_claude_no_launch_windows_shim_from_wsl_prints_wslenv(
fake_studio, monkeypatch, tmp_path
):
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("PWD", "/stale/outer/repo")
monkeypatch.setenv("WSL_DISTRO_NAME", "Ubuntu")
monkeypatch.setattr(
start.shutil, "which", lambda _: "/mnt/c/Users/samle/AppData/Roaming/npm/claude"
@ -532,6 +540,10 @@ def test_connect_claude_no_launch_windows_shim_from_wsl_prints_wslenv(fake_studi
assert "export ANTHROPIC_API_KEY=" in result.output
assert "export CLAUDE_CODE_OAUTH_TOKEN=" in result.output
assert "export WSLENV=" in result.output
# PWD must NOT be frozen into the recipe (no `export PWD=`): WSLENV PWD/p translates the
# shell's live PWD at run time, so a recipe reused from another dir resolves the project root.
assert "export PWD=" not in result.output
assert "PWD/p" in result.output
assert "ANTHROPIC_AUTH_TOKEN" in result.output
assert "CLAUDE_CODE_OAUTH_TOKEN" in result.output