Repository navigation
fix(wsl): pin each pane's distro, keep guest cwd in splits/tabs/forks, pass guest paths safely - #351
Conversation
b2ff3f8 to
d3f49d3
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Raw distribution names with spaces break pinned launches, and guest-command options can incorrectly suppress cwd inheritance.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Improves WSL pane identity, cwd inheritance, path safety, and zsh shell integration.
Changes:
- Snapshots WSL distributions and preserves guest cwd across pane operations.
- Centralizes WSL parsing and safe path handling.
- Adds shared zsh bootstrap support and regression tests.
| File | Description |
|---|---|
scripts/tests/test_shell_integration.py |
Expands zsh integration coverage. |
nebula_app/src/shell_detect.rs |
Adds WSL parsing, snapshots, quoting, and environment handling. |
nebula_app/src/runtime_exec.rs |
Applies spawn-time WSL identity to execution. |
nebula_app/src/platform/wsl_hooks/windows.rs |
Reuses centralized WSL detection. |
nebula_app/src/platform/shell_integration.rs |
Creates shared local/WSL zsh bootstrap files. |
nebula_app/src/gpui_shell/workspace/tests.rs |
Tests safe file-tree WSL launches. |
nebula_app/src/gpui_shell/workspace/tab_duplication.rs |
Implements WSL-aware split, tab, duplicate, and cwd inheritance. |
nebula_app/src/gpui_shell/workspace/file_tree.rs |
Safely constructs WSL terminal launches. |
nebula_app/src/gpui_shell/workspace/agents.rs |
Preserves WSL context in AI forks. |
nebula_app/src/gpui_shell/workspace.rs |
Routes tab and split creation through WSL-aware logic. |
nebula_app/src/gpui_shell/terminal/view/startup.rs |
Captures distribution identity at spawn. |
nebula_app/src/gpui_shell/terminal/view/pointer.rs |
Resolves links using guest cwd. |
nebula_app/src/gpui_shell/terminal/view.rs |
Stores pane-level WSL identity. |
nebula_app/src/gpui_shell/terminal/session.rs |
Injects WSL zsh startup environment. |
nebula_app/src/gpui_shell/terminal/osc_links.rs |
Maps WSL prompt links through the snapshot. |
nebula_app/src/completion_context.rs |
Uses centralized WSL distribution resolution. |
nebula_app/src/agent_env.rs |
Shares WSLENV merging logic. |
nebula_app/res/shell/zshrc |
Restores compinit and new-user behavior. |
nebula_app/res/shell/zshenv |
Limits bootstrap state to interactive zsh. |
nebula_app/AGENTS.md |
Links WSL architecture notes. |
docs/architecture-decisions.md |
Documents WSL zsh lifecycle signals. |
architecture/notes/nebula_app/shell_detect/2026-09-28-wsl-spawn-distro-snapshot.md |
Records WSL snapshot design. |
architecture/notes/nebula_app/platform/2026-09-28-wsl-zsh-startup-integration.md |
Records zsh integration design. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Kuddev
left a comment
There was a problem hiding this comment.
Reviewed head 4eb7069c1207702409333e16795fa2470ba53311.
[P1] Preserve guest startup files when the WSL bootstrap cannot be read. In nebula_app/src/shell_detect.rs:836-846, ZDOTDIR is replaced with a host-drive bootstrap before the guest can verify that it is reachable. With disabled automount, failed path translation, or restrictive drvfs permissions, zsh cannot load that bootstrap and consequently never restores the user's startup directory. The PR's own architecture note documents this at lines 104-108. The zsh startup contract reads user files from $ZDOTDIR and falls back to $HOME only when ZDOTDIR is unset: https://zsh.sourceforge.io/Doc/Release/Files.html . A cwd integration must preserve startup configuration in these supported WSL configurations; requiring a manual WSLENV opt-out after shell initialization breaks is not a sufficient fallback.
[P2] An unspecified guest command does not prove that the login shell is zsh. command.is_none_or(is_zsh_program) also forwards the host ZDOTDIR to default fish/nushell sessions. Those shells never run the zsh restoration or bash PROMPT_COMMAND cleanup. Installers using ${ZDOTDIR:-$HOME} then write user configuration into the shared bootstrap directory; the next application process overwrites it. This is explicitly acknowledged at lines 112-116 of the same note. Preserve the guest environment for unknown/non-zsh shells, including processes started before the first prompt.
Please keep the WSL distribution/cwd and zsh integration functionality, but resolve these startup/environment regressions and cover non-zsh default shells and an unreachable bootstrap before merging. These findings are grounded in the submitted control flow, its documented consequences and the official startup contract; I have not claimed an end-to-end WSL reproduction or completed review of every remaining change.
|
Addressed both findings in P1 / P2 share one root cause: the host decided the takeover from the launch arguments, which cannot show the guest's login shell or the guest's view of a drvfs path. The guest now answers before the first takeover of each (distribution, user), through
Cost and threading: one probe per guest per process on its own thread (10 s budget, failures retried after 5 min, cache bounded to 32); the spawn waits at most 2 s for a guest's first verdict, so a guest still booting starts that pane without zsh reports and later panes use the cached verdict. Rationale and rejected alternatives (host-side Evidence: |
…f them Cross-review of Kuddev#351 found the spawn snapshot widened which panes feed their OSC 7 cwd to host-side guest helpers, while those helpers still went through `wsl.exe -- …`, i.e. the guest login shell: - Side panel git/find and the merge tab's cat/git now start through `shell_detect::wsl_exec_command` (`--exec`). Measured: a directory named `x$(touch /tmp/pwned)` ran the touch through `--`, not through `--exec`. find's -printf format drops the doubled backslash `--` needed; the merge tab writes with `tee` instead of a quoted `sh -c`. Paths `wsl.exe` cannot carry (`"`) are not handed to the helpers. - pane.exec / Runtime git strip ZDOTDIR, NEBULA_ZSH_INTEGRATION, NEBULA_ZDOTDIR_WAS_SET and their WSLENV entries: a direct exec never runs the bootstrap .zshenv that removes them. - zshenv/zprofile only take over when NEBULA_ZSH_INTEGRATION is set, so a zsh started while the parent still exports the bootstrap ZDOTDIR (tmux from a global rc) loads the user's rc instead of `/.zshrc`. - run_bounded kills and reaps the child when stat of its output fails. - Document `exec_argument_unsupported`; correct the note on `"` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f them Cross-review of Kuddev#351 found the spawn snapshot widened which panes feed their OSC 7 cwd to host-side guest helpers, while those helpers still went through `wsl.exe -- …`, i.e. the guest login shell: - Side panel git/find and the merge tab's cat/git now start through `shell_detect::wsl_exec_command` (`--exec`). Measured: a directory named `x$(touch /tmp/pwned)` ran the touch through `--`, not through `--exec`. find's -printf format drops the doubled backslash `--` needed; the merge tab writes with `tee` instead of a quoted `sh -c`. Paths `wsl.exe` cannot carry (`"`) are not handed to the helpers. - pane.exec / Runtime git strip ZDOTDIR, NEBULA_ZSH_INTEGRATION, NEBULA_ZDOTDIR_WAS_SET and their WSLENV entries: a direct exec never runs the bootstrap .zshenv that removes them. - zshenv/zprofile only take over when NEBULA_ZSH_INTEGRATION is set, so a zsh started while the parent still exports the bootstrap ZDOTDIR (tmux from a global rc) loads the user's rc instead of `/.zshrc`. - run_bounded kills and reaps the child when stat of its output fails. - Document `exec_argument_unsupported`; correct the note on `"` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d2abe1b to
e9c9562
Compare
…f them Cross-review of Kuddev#351 found the spawn snapshot widened which panes feed their OSC 7 cwd to host-side guest helpers, while those helpers still went through `wsl.exe -- …`, i.e. the guest login shell: - Side panel git/find and the merge tab's cat/git now start through `shell_detect::wsl_exec_command` (`--exec`). Measured: a directory named `x$(touch /tmp/pwned)` ran the touch through `--`, not through `--exec`. find's -printf format drops the doubled backslash `--` needed; the merge tab writes with `tee` instead of a quoted `sh -c`. Paths `wsl.exe` cannot carry (`"`) are not handed to the helpers. - pane.exec / Runtime git strip ZDOTDIR, NEBULA_ZSH_INTEGRATION, NEBULA_ZDOTDIR_WAS_SET and their WSLENV entries: a direct exec never runs the bootstrap .zshenv that removes them. - zshenv/zprofile only take over when NEBULA_ZSH_INTEGRATION is set, so a zsh started while the parent still exports the bootstrap ZDOTDIR (a multiplexer from a global rc) loads the user's rc instead of `/.zshrc`. - run_bounded kills and reaps the child when stat of its output fails. - Document `exec_argument_unsupported`; correct the note on `"` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e9c9562 to
af3e659
Compare
…f them Cross-review of Kuddev#351 found the spawn snapshot widened which panes feed their OSC 7 cwd to host-side guest helpers, while those helpers still went through `wsl.exe -- …`, i.e. the guest login shell: - Side panel git/find and the merge tab's cat/git now start through `shell_detect::wsl_exec_command` (`--exec`). Measured: a directory named `x$(touch /tmp/pwned)` ran the touch through `--`, not through `--exec`. find's -printf format drops the doubled backslash `--` needed; the merge tab writes with `tee` instead of a quoted `sh -c`. Paths `wsl.exe` cannot carry (`"`) are not handed to the helpers. - pane.exec / Runtime git strip ZDOTDIR, NEBULA_ZSH_INTEGRATION, NEBULA_ZDOTDIR_WAS_SET and their WSLENV entries: a direct exec never runs the bootstrap .zshenv that removes them. - zshenv/zprofile only take over when NEBULA_ZSH_INTEGRATION is set, so a zsh started while the parent still exports the bootstrap ZDOTDIR (a multiplexer from a global rc) loads the user's rc instead of `/.zshrc`. - run_bounded kills and reaps the child when stat of its output fails. - Document `exec_argument_unsupported`; correct the note on `"` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80a081c to
d58f2f1
Compare
WSL guests whose login shell is zsh never reported OSC 7, and bare `wsl`, default-shell and profile WSL panes had no guest identity, so the file tree, Git view, split/new-tab directory inheritance and prompt-path links ignored them. - Forward the shared zsh bootstrap as ZDOTDIR through WSLENV (/pu), written once per process and only from a fixed local drive. The bootstrap takes over interactive zsh only; `zsh -c` restores the user's ZDOTDIR and drops the NEBULA_* variables, so terminal multiplexers and nested shells keep XDG-style configs. bash guests drop the variables at their first prompt. - Snapshot each pane's distribution at spawn (explicit -d or the registry default, resolved once and shared with completion scoping). Workspace WSL location, prompt links, split, duplicate and new tab read that snapshot. - Splits of WSL panes stay in the guest even before a cwd report; bare launches pin the snapshotted distro; a leading `~` yields to --cd. New tabs inherit the guest cwd only for the same distro and user, keep a profile's own directory, and avoid UNC probes on the UI thread. - Use a single WSL program detector; move the platform cfg into platform/. Rationale: architecture/notes/nebula_app/platform/2026-09-28-wsl-zsh-startup-integration.md and architecture/notes/nebula_app/shell_detect/2026-09-28-wsl-spawn-distro-snapshot.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of b5f8473 found injected guest paths broken or unsafe, helpers that disagreed on where wsl.exe options end, and bootstrap side effects in guests. - Encode `--cd` for wsl.exe's own splitting (`shell_detect::wsl_raw_arg`): quote paths with whitespace, keep backslashes literal, and refuse paths containing `"`. wsl.exe does not parse CRT `\"`, so such a directory name could run a guest command on split, duplicate, fork or file-tree open. - One option-region parser (`wsl_options`) for distro, user, distribution selection and guest command; it stops at `--`, `-e`/`--exec` or the first unknown argument. `--system` no longer snapshots the default distro, and `--distribution-id` keeps its place when rewriting `--cd`. - Exec context, WSL hooks, completion and a PTY-default `shell=wsl` pane use the spawn snapshot and the single `is_wsl_launcher` detector. - Duplicate and AI fork carry the guest cwd only into the same guest and user (or the pane's own identity without a snapshot); guest paths never reach a host `is_dir`. Splits before the first report replay the spawn, keeping the launch's own `--cd`. Relative prompt links resolve in the guest cwd. - zsh bootstrap: no `ZDOTDIR` for non-zsh guest commands; `NEBULA_*` stay unexported; Ubuntu's global compinit is deferred until the user's ZDOTDIR is back (respecting GLOBAL_RCS and a `.zprofile` skip); the newuser wizard runs where zsh itself would check. Bootstrap preparation caches failures and rewrites deleted files. - Share the WSLENV merge helpers with agent_env. Rationale and measured wsl.exe behavior are in the two 2026-09-28 notes under architecture/notes/nebula_app/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…compinit case Hosted Ubuntu runners ship group/world-writable fpath directories, so the login shell's compinit stopped at compaudit's confirmation prompt and the test timed out. The production bootstrap still runs the same plain compinit as Ubuntu's /etc/zsh/zshrc; only the fixture now filters fpath. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Ubuntu global-compinit case stopped at compaudit's "insecure directories" prompt because hosted runners ship group/world-writable fpath entries. The fixture now drops what compaudit rejects before the login shell's compinit; the bootstrap still runs the same plain compinit as Ubuntu's /etc/zsh/zshrc. - split_of_wsl_pane_without_guest_cwd_stays_in_the_guest used temp_dir() as the host cwd; on macOS/Linux that is a POSIX path and reads as a guest cwd. Use a Windows host path, as WSL panes report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
new_tab_launch scanned the whole argv for `--cd`, so a guest command's option (`wsl -d Ubuntu -e tool --cd /tool-dir`) suppressed guest cwd inheritance. wsl_options now records a leading `~`, `--cd` or `--cd=` before the guest command, and new_tab_launch asks it through wsl_launch_chooses_directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aking over ZDOTDIR Review of the zsh integration found two startup regressions that no guest-side script can repair: ZDOTDIR was replaced before the guest could verify that the host bootstrap is reachable (automount off, failed /p translation, drvfs permissions for another -u user), so zsh skipped the user's own startup files; and an unspecified guest command was taken as proof that the login shell is zsh, so fish/nushell logins kept a ZDOTDIR nothing restores. `platform::wsl_guest_shell` now asks the guest once per (distribution, user) per process: `wsl.exe --exec sh -s` runs `res/shell/wsl-guest-probe.sh`, which reports the passwd login shell and whether that user can read all three bootstrap files at the WSLENV-translated path. A login shell is taken over only when the guest said zsh and readable, an explicit `-e zsh` only when readable; other guest commands are never probed, and unknown (failed, timed out, --distribution-id/--system) leaves the guest untouched. The spawn waits at most 2 s for a guest's first verdict; a guest still booting starts that pane without zsh reports and later panes use the cached verdict. `wsl_cwd_report_env` no longer guesses from the guest command. Tests: probe command/env, answer parsing and the takeover decision with a guest double; `wsl_launch_command` option-region reads; the probe script under real `sh` (readable, one file missing, missing directory, untranslated Windows path, empty variable; shell compared with the passwd entry). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… fallback The probe script fell back to $SHELL wherever getent is missing, which the macOS CI runners exposed (Directory Services, no getent). A musl or busybox guest would have hit the same path. The script now reads /etc/passwd before falling back to $SHELL; the Python case covers a guest without getent and expects the $SHELL fallback only where the account is not in /etc/passwd. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…bounded runner Self-review of the probe cut: - The spawn waited up to 2 s per pane for the guest's first verdict, on the UI thread and serially across restored panes. `verified` now answers only from the cache and starts the probe otherwise; `main` warms the default shell's guest at process start on a worker thread, so a running distribution has usually answered before the first pane. - The spawn options of every WSL pane are pinned to the distribution snapshot (`wsl_args_pinned`) while the persisted launch stays as configured, so the pane, its probe and its hook installer name the same guest by construction. - `platform::process::run_bounded` is the one bounded child runner; the probe and the WSL hook installer both use it, and the hook installer appends its WSLENV entry through `append_wslenv`. - A failed bootstrap write is retried after five minutes instead of disabling the integration for the rest of the process. - Non-zsh guest commands no longer materialise the bootstrap on spawn; running probes are never evicted from the verdict cache. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uests and warm-up Cross-review (three reviewers, three verifying judges, majority verdicts): - pane.exec in a WSL pane refuses a guest cwd or argument containing `"`: `Command`'s CRT `\"` ends wsl.exe's quote and the rest runs as a guest command (reproduced). `shell_detect::wsl_accepts_arg` is the one rule for launches and exec. git exec appends its WSLENV entries with `append_wslenv`. - The bootstrap `.zshenv` and the bash first prompt remove the zsh entries from the guest's WSLENV: WSL forwards them to a nested `wsl.exe` whatever the `/u` flag, so another distribution, user or installer received an unverified ZDOTDIR (reproduced; now `unset` in the nested guest). - Each ZDOTDIR restore keeps the user's export attribute, so an XDG `~/.zshenv` that sets ZDOTDIR without export still lets child zsh read `~/.zshenv`. Shared with local zsh. - Warm-up resolves the first pane's shell with the pane's own authority (`shell_launch::startup_shell`): `resolve_id` missed bare `wsl` whenever distributions are registered. It is skipped for an explicit command, and the user-forwarded ZDOTDIR opt-out is checked before any probe. - The bootstrap is written only on the probe worker; the spawn reads a cached verdict and `wsl_zsh_directory_ready` (try_lock, no write). - `shell_detect::spawn_shell` composes the pinned spawn and is unit-tested; the dead `PaneExecContext::with_spawn_distro` is gone. - A leading `~` gives way to an inherited host directory like `--cd` does. - The command palette no longer stats a `\wsl.localhost` path while rendering. - `run_bounded` has host tests; notes corrected to match the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f them Cross-review of Kuddev#351 found the spawn snapshot widened which panes feed their OSC 7 cwd to host-side guest helpers, while those helpers still went through `wsl.exe -- …`, i.e. the guest login shell: - Side panel git/find and the merge tab's cat/git now start through `shell_detect::wsl_exec_command` (`--exec`). Measured: a directory named `x$(touch /tmp/pwned)` ran the touch through `--`, not through `--exec`. find's -printf format drops the doubled backslash `--` needed; the merge tab writes with `tee` instead of a quoted `sh -c`. Paths `wsl.exe` cannot carry (`"`) are not handed to the helpers. - pane.exec / Runtime git strip ZDOTDIR, NEBULA_ZSH_INTEGRATION, NEBULA_ZDOTDIR_WAS_SET and their WSLENV entries: a direct exec never runs the bootstrap .zshenv that removes them. - zshenv/zprofile only take over when NEBULA_ZSH_INTEGRATION is set, so a zsh started while the parent still exports the bootstrap ZDOTDIR (a multiplexer from a global rc) loads the user's rc instead of `/.zshrc`. - run_bounded kills and reaps the child when stat of its output fails. - Document `exec_argument_unsupported`; correct the note on `"` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…takeover Cross-review found several locally reasonable pieces that repeated a rule already owned elsewhere. Net counted change against main drops from about 2560 to about 1970 lines without changing covered behavior. - The guest probe owns the bootstrap: its verdict carries the path, so the host-side state machine, the fixed-drive check (the probe already proves the guest can read it) and the UI-thread ready lookup are gone. The verdict cache is a OnceLock slot per guest. - One WSL option-region parse: callers read `wsl_launch` fields and `WslOptions::spawn_distro`; the command/directory/injected-default wrappers are removed. - Split, duplicate, fork and new tab share `follow_guest`; a split reuses the duplicate rule. A pane without a snapshot no longer passes its guest cwd on. - Completion scoping reads the pinned spawn shell instead of a second branch. - The zsh takeover is two variables in one constant; NEBULA_ZDOTDIR_WAS_SET is no longer forwarded (the host always sent 0, the scripts default to it). - Behaviour-neutral refactors of pre-existing code (WSLENV append helpers in agent_env, runtime_exec and the hook prepare step; the exec detector swap) are reverted to keep this change focused. - Tests are table-driven; the zsh note is condensed to failure facts and current decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r the zsh probe
- Delete process::run_bounded and its tests; platform::process_output gains
read_with_input (stdin from a temp file), with read/read_cancellable as thin
wrappers. The zsh probe and the WSL hook exchange use it, so both now run in
the process group/job object like the other short-lived probes. Its bounded
test now also runs on Windows and covers stdin.
- takes_zsh_bootstrap parses the launch once and builds the Target from it;
Target::from_launch is gone and Target::command reuses
shell_detect::wsl_exec_command, which gains an optional user (-d/-u).
- GuestShell is { login_zsh, readable }; takes_zsh_bootstrap returns bool and
the spawn path takes the path from the pure shell_integration::wsl_zsh_path.
- wsl_cwd_report_env no longer re-checks a forwarded ZDOTDIR; its argument
must be takes_zsh_bootstrap's answer, which already refuses that case.
- CR normalization of the zsh bootstrap and probe script goes through
nebula_terminal::tty::shell_line_endings (now pub) instead of copies.
- Tests: spawn_distro is asserted beside the pin rule for every selector
spelling (including --distribution-id=), the identity table drops that
column, and rows that repeated another test's branch are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y copy through one rule
- Drop `TerminalView.wsl_distro`: the pinned spawn options already record the
snapshot in `exec_context`, so `TerminalView::wsl_distro()` reads it from
there (pointer links, `active_wsl_cwd`) and startup no longer threads a
second copy through its tuple.
- Replace `split_launch`, `pane_split`, `new_tab_launch`, `duplicate_launch`,
`follow_guest`, `focused_guest` and `host_visible_cwd` with
`PaneOrigin::of(view)` + `CopyKind { Split, Duplicate, NewTab }` +
`copy_launch`. A split of a non-WSL pane still opens the default shell in
the host cwd; a WSL pane still pins then follows even without a guest cwd
(fish, or a split before the first prompt). The guest user now comes from
`exec_context` too, which the pinned spawn keeps equal to the launch's `-u`.
- Behaviour note: the host-visible cwd is now computed eagerly on split,
duplicate, new tab and fork, including when same-guest `--cd` inheritance
wins and it is discarded. That costs at most one `is_dir` on a
`/mnt/<drive>` path (or the pane's host cwd), only on an explicit user
action; UNC paths are still never probed.
- Inline the single-use `at` closure in the file-tree WSL test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h test scaffolding - wsl-guest-probe.sh: one loop for the three readability checks and one awk filter over getent-or-/etc/passwd; the output lines are unchanged. - zshenv: set the compinit marker with a quoted := default; one-line reasons. - zshrc: one-line reason for the Ubuntu compinit condition. - tests: one run_bootstrapped_zsh helper for the three WSL-shaped zsh runs; the non-interactive WSLENV strip check moves into the leak test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…separable Point the notes at process_output::read_with_input, copy_launch and the exec_context accessor, move zsh-takeover text out of the snapshot note, and record that the takeover builds on the spawn-time snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR now carries one decision: the spawn-time WSL distribution snapshot, with guest cwd plumbing, `wsl.exe --exec` hardening and the copy rules. The WSL zsh startup takeover (guest shell probe, ZDOTDIR bootstrap through WSLENV, startup warm-up and its tests and note) is a separate decision that builds on this snapshot, and together they exceeded the PR size limit. It returns in a follow-up PR on top of this one. Removed: platform/wsl_guest_shell.rs, res/shell/wsl-guest-probe.sh and the zsh startup note. Reverted to base where the change only served the takeover: the zsh bootstrap scripts, shell_integration.rs, the pane session env, the startup warm-up, process_output's stdin runner (the WSL hook setup keeps its own loop), tty::shell_line_endings visibility, the shell integration Python tests and the WSL bash/zsh hooks line. shell_detect drops the zsh parameter of wsl_cwd_report_env, the takeover cleanup in the bash report, the WSLENV helpers, the guest command field of WslOptions and the user argument of wsl_exec_command; runtime_exec no longer strips the takeover from pane.exec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1a98d71 to
c7089aa
Compare
|
@Kuddev 这个 PR 已按行数审阅拆分,麻烦再看一下 / Split after the size review, ready for another look:
This PR now carries only the spawn-time distro snapshot, guest cwd plumbing and the |
Kuddev
left a comment
There was a problem hiding this comment.
[P2] 区分 WSL 启动选项与 --exec 后的真实 argv,保留已能正确传递的双引号参数。
nebula_app/src/runtime_exec.rs:231-244 新增检查将 guest cwd 和 argv 合并,任何参数包含双引号都会返回 exec_argument_unsupported。例如 python -c 的普通字符串、git commit -m 中的引号都被这条新规则阻断,文档还将其声明为永久限制。
本机用 Rust 标准库 std::process::Command(与产品 build_command 的实际启动 API 相同)做了只读参数回显:wsl.exe --exec /usr/bin/printf <格式参数> <单个值>。WSL 2.7.13.0、Windows 10.0.22631.5472 下,普通路径、带空格路径、包含双引号的 /tmp/a" OTHER,以及带空格且尾随反斜杠的值,都返回完全相同的单个 argv;四个断言均通过。此前 Python subprocess 的独立回显也一致。这里只打印参数,没有写入 guest 文件或运行参数中的内容。
这证明 --cd/raw 启动命令行的测量结论不适合直接扩大为所有 --exec argv 的永久限制。请保留确有依据的 cwd/启动参数防护,分别处理并测试元选项与执行 argv,恢复合法参数的精确传递;如需覆盖特定旧 WSL 行为,请给出版本化复现证据,而不是让全部版本退化为受限 argv。
旧审阅针对的 ZDOTDIR takeover 已移至 #409,本轮不再用那两条已移出当前 diff 的路径作为阻塞理由。当前合并仍等待上述新回归修正以及未完成 review threads 的处理。
# Conflicts: # nebula_app/src/gpui_shell/workspace/tab_duplication.rs
|
维护者已直接修正并整合当前主线:保留 #455 的完整分屏复制,对每个 pane 固定其实际 WSL 发行版;只限制启动 --cd 的双引号,恢复 --exec argv 及 Git/文件 helper 参数的原样传递。新增 bare-WSL 复制回归,保留原嵌套混合布局测试。架构、格式以及包含 GPUI 测试代码的本地 cargo check 已通过。 另一个独立提交修复了此次 CI 触发的检查器问题:Git 的 combined-diff hunk 标题截断 UTF-8,而实际文档有效。仅新增源码做严格解码,元数据按字节解析;21 项名称检查测试和原失败 range 通过,未更改模式表、豁免或历史扫描范围。 两条旧线程的依据已核对:目录选择现在只读 WSL 自身选项区;发行版名称包含空白的担忧不符合当前 WSL 注册约束,官方 s_ValidateDistroName 使用 ^[a-zA-Z0-9._-]{1,N}$,见 https://github.com/microsoft/WSL/blob/449a7eb5c98638bb35cd4d9b4717d090b15ba271/src/windows/service/exe/LxssUserSession.cpp#L4310-L4320 。因此关闭这两条已解决/不适用的讨论。新 head 仍等待完整 CI,尚未合并。 |
Kuddev
left a comment
There was a problem hiding this comment.
源码复审完成。已有两条 review thread 均已解决;逐窗格 WSL 发行版与完整分屏恢复接线、启动 --cd 与 --exec 参数边界保持一致。维护者另行修正了 Git diff 标题截断 UTF-8 的检查器误报,新增源码仍严格解码,扫描范围及名称规则未放宽。最终测试样本引用现有规则数据,不新增产品名称。
本地产品 tests 编译检查及架构/格式检查通过;名称检查 21 项通过。此批准是对精确 head 的代码审阅结论,不代表原生 CI 已结束;必须等新 head 的全部必需检查和所选原生作业成功后再合并。

Result / 用户结果
Improves how every WSL pane knows its guest, and how guest paths reach
wsl.exe:wsl, a WSL default shell (including the PTY-defaultshell=wsl) and imported WSL profiles now get a guest identity: the distribution is snapshotted at spawn (explicit-dor the registry default). The file tree, Git view, prompt-path links, command execution, image paste and path drop all use it, and a later default-distribution change no longer redirects a running pane. The pane's ownwsl.exespawn is pinned to that snapshot (the persisted launch stays as configured)./against the current drive). Splits made before the first prompt keep the launch's own--cd/~. Relative prompt links resolve in the guest cwd.wsl.exe --execinstead of--, so a directory or file name containing$(…)is no longer expanded by the guest login shell. A directory name containing"is no longer injected into thewsl.execommand line, andpane.execin a WSL pane refuses a guest cwd or argument containing"for the same reason.-d,--distribution,--distribution-id,--system,-u,--cd,~) and stops at the guest command, so a guest command's-dis no longer mistaken for the distribution. One detector decides what is a WSL launch.Scope change: the WSL zsh startup takeover (guest probe, zsh bootstrap changes, start-up warm-up) was moved out of this PR to keep it one decision under the size limit. It depends on this PR's option parser and spawn pinning, so it follows as the draft PR #409, stacked on this one (review its last commit only; it will be rebased onto
mainafter this PR merges). Until then a WSL guest whose login shell is zsh behaves as onmain(no OSC 7); nothing regresses for it.Design / 设计边界
shell_detectowns WSL argument parsing (one option-region parser,wsl_options; one program detector,is_wsl_launcher;wsl_spawn_distro;wsl_args_pinned;spawn_shell), the--cdencoding (wsl_raw_arg,wsl_accepts_arg) and the direct-exec builderwsl_exec_command. The pane'sPaneExecContextrecords the pinned options andTerminalView::wsl_distroreads them;workspace/tab_duplicationowns the copy rule in one place (PaneOrigin::of,CopyKind,copy_launch).wsl.exemust keep launching the guest login shell for the pane itself (the 1.1.0--exec bashregression); only helpers use--exec.wsl_launch_distrokeeps its explicit-only contract;nebula_terminalis unchanged.wslpane persists-d <snapshot>in its launch; the restored original keeps following the default. Rationale and rejected alternatives:architecture/notes/nebula_app/shell_detect/2026-09-28-wsl-spawn-distro-snapshot.md.--execno longer pass through the guest login shell, so agitfound only via aPATHset in the user's shell rc (e.g. some nix/linuxbrew setups) may not be found by the Git view. Host cwd for a copy is computed when the user splits/duplicates (at most oneis_diron/mnt/<drive>; UNC paths are never probed). Known limits:--distribution-id/--systempanes have no snapshot.Evidence / 验证依据
mainat1e569537):cargo test -p nebula --bin pebrel: 1883 passed, 0 failed, 19 ignored.cargo test -p nebula_terminal --lib tty(before the merge;nebula_terminalis not touched by this PR): 54 passed.python3 scripts/check_architecture.py --base 1e569537(in WSL): exit 0.python3 -m unittest scripts.tests.test_shell_integration(Ubuntu 26.04 WSL guest): 22 ran, OK, 1 skipped.rustfmt --edition 2024 --checkon the touched files: clean.wsl.exe --cdencoding checked against a realwsl.exe(spaces, trailing and inner backslashes,%); CRT\"escaping was shown to inject a guest command.--vs--exec, measured on 2026-09-30: a directory namedx$(touch /tmp/pwned)ran thetouchthrough--and not through--exec.pane.execquoting: a CRT-quoted--cd '/tmp/i" touch /tmp/x #'runstouchin the guest; the exec path now refuses it (wsl_exec_refuses_what_wsl_cannot_receive).--cdno longer suppresses cwd inheritance (wsl_identity_reads_only_the_option_region). Distribution names cannot contain whitespace (wsl --import "Nebula Probe"is rejected withE_INVALIDARG), so the pinned-dvalue stays unquoted.pane.exec"refusal; leading~yields to an inherited host directory; the command palette no longer stats\\wsl.localhostwhile rendering;spawn_shellextracted and tested; notes corrected.copy_launch, the duplicateTerminalView.wsl_distrofield was replaced by a read of the exec context, overlapping test rows were merged, and the zsh takeover was moved to a follow-up PR (see Scope change).-e tool -d x,--distribution=,--system,--distribution-idin both spellings),--cdquoting and refusal of", snapshot pinning, split/duplicate/new-tab/fork inheritance, the exec-context snapshot and relative prompt-link bases.main).Required Review / 必须确认
CONTRIBUTING.md,docs/architecture.md, anddocs/project-constraints.md.python3 scripts/check_architecture.py --base <PR-base-commit>passes; budgets were not inflated to fit the change.Checkboxes explain the review; they do not replace CI or maintainer approval.
中文版本
用户结果
让每个 WSL pane 都知道自己在哪个来宾里,并让来宾路径安全地传给
wsl.exe:wsl、WSL 默认 shell(包括 PTY 默认的shell=wsl)和导入的 WSL profile 现在都有来宾身份:发行版在 spawn 时记录下来(显式的-d,否则取注册表里的默认发行版)。文件树、Git 视图、提示符路径链接、命令执行、图片粘贴和路径拖放都用这个身份;之后改默认发行版,也不会让已经在运行的 pane 跑到别的发行版去。pane 自己的wsl.exe启动参数固定为这个快照,持久化的 launch 仍保持用户原来的配置。/解析到当前盘符)。第一个提示符出现之前的分屏,沿用 launch 自己的--cd/~。提示符里的相对链接按来宾 cwd 解析。wsl.exe --exec直接执行,不再用--,所以目录名或文件名里的$(…)不会再被来宾登录 shell 展开。含"的目录名不会再注入wsl.exe命令行;出于同样的原因,WSL pane 里的pane.exec会拒绝含"的来宾 cwd 或参数。-d、--distribution、--distribution-id、--system、-u、--cd、~),遇到来宾命令就停下,所以来宾命令里的-d不会再被误当成发行版。是不是 WSL 启动,也只由一个检测函数判断。范围调整: WSL zsh 启动接管(来宾探测、zsh bootstrap 改动、启动预热)已移出本 PR,让本 PR 只包含一个决策,并控制在行数上限以内。它依赖本 PR 的选项解析器和 spawn 固定,所以作为堆叠在本 PR 之上的 Draft PR #409 跟进(只需看它的最后一个 commit;本 PR 合入后它会 rebase 到
main上)。在那之前,登录 shell 是 zsh 的 WSL 来宾与main上的行为相同(不上报 OSC 7),不会出现回退。设计边界
shell_detect负责 WSL 参数解析(一个选项区解析器wsl_options、一个程序检测函数is_wsl_launcher,以及wsl_spawn_distro、wsl_args_pinned、spawn_shell)、--cd编码(wsl_raw_arg、wsl_accepts_arg)和直接执行的构造函数wsl_exec_command。pane 的PaneExecContext记录固定后的选项,TerminalView::wsl_distro从中读取;workspace/tab_duplication在一处统一负责复制规则(PaneOrigin::of、CopyKind、copy_launch)。wsl.exe必须继续启动来宾登录 shell(1.1.0 的--exec bash回退),只有 helper 使用--exec。wsl_launch_distro保持「只认显式指定」的约定;nebula_terminal没有改动。wslpane 时,副本的 launch 会持久化-d <快照>;恢复出来的原 pane 仍跟随默认发行版。决策依据和被否决的方案见architecture/notes/nebula_app/shell_detect/2026-09-28-wsl-spawn-distro-snapshot.md。--exec运行的 helper 不再经过来宾登录 shell,所以如果git只能通过用户 shell rc 里设置的PATH找到(例如某些 nix/linuxbrew 配置),Git 视图可能找不到它。复制时的宿主 cwd 改为在用户分屏或复制的当下计算(最多对/mnt/<盘符>做一次is_dir,UNC 路径从不探测)。已知限制:--distribution-id/--system启动的 pane 没有快照。验证依据
main的1e569537):cargo test -p nebula --bin pebrel:1883 个通过,0 个失败,19 个忽略。cargo test -p nebula_terminal --lib tty(合并前运行;本 PR 不改nebula_terminal):54 个通过。python3 scripts/check_architecture.py --base 1e569537(在 WSL 中运行):退出码 0。python3 -m unittest scripts.tests.test_shell_integration(Ubuntu 26.04 WSL 来宾):运行 22 个,全部通过,跳过 1 个。rustfmt --edition 2024 --check:无差异。wsl.exe --cd编码用真实的wsl.exe验证过(空格、末尾和中间的反斜杠、%);实测 CRT 的\"转义可以注入来宾命令。--与--exec对比(2026-09-30 实测):名为x$(touch /tmp/pwned)的目录,经--时执行了touch,经--exec时没有。pane.exec引号:CRT 引号包裹的--cd '/tmp/i" touch /tmp/x #'会在来宾里执行touch;现在执行路径会拒绝它(wsl_exec_refuses_what_wsl_cannot_receive)。--cd不再阻止 cwd 继承(wsl_identity_reads_only_the_option_region)。发行版名称不能含空白(wsl --import "Nebula Probe"会以E_INVALIDARG被拒绝),所以固定的-d值不加引号。pane.exec拒绝";开头的~让位于继承来的宿主目录;命令面板渲染时不再 stat\\wsl.localhost;抽出并测试了spawn_shell;修正了 notes。copy_launch;重复的TerminalView.wsl_distro字段改为从 exec context 读取;合并了重叠的测试行;zsh 接管移到后续 PR(见「范围调整」)。-e tool -d x、--distribution=、--system、两种写法的--distribution-id)、--cd引号处理与对"的拒绝、快照固定、分屏/复制/新标签/fork 的继承、exec context 快照,以及相对提示符链接的基准目录。main),在 1500 行上限以内。必须确认
以上英文版的勾选项同样适用于本中文说明。
🤖 Generated with Claude Code