Repository navigation
feat(wsl): report zsh guest cwd through a guest-verified ZDOTDIR takeover - #409
Open
MomentDerek wants to merge 1 commit into
Open
MomentDerek wants to merge 1 commit into
MomentDerek wants to merge 1 commit into
Conversation
Merged
5 of 6 tasks
4 of 6 tasks
MomentDerek
force-pushed
the
feat/wsl-zsh-startup
branch
from
October 5, 2026 16:43
81c37f9 to
fcc1f45
Compare
MomentDerek
force-pushed
the
feat/wsl-zsh-startup
branch
from
October 5, 2026 20:14
fcc1f45 to
4744c76
Compare
MomentDerek
marked this pull request as ready for review
October 5, 2026 20:29
MomentDerek
force-pushed
the
feat/wsl-zsh-startup
branch
2 times, most recently
from
October 7, 2026 11:04
3f89c13 to
24dd4de
Compare
…over Restores the WSL zsh startup takeover on top of the spawn-time distro snapshot: the guest probe confirms a zsh login shell and a readable bootstrap before ZDOTDIR is forwarded through WSLENV, the bootstrap keeps the user's global compinit, newuser wizard and XDG ZDOTDIR, nested guests receive none of it, and the first pane's guest is warmed at start-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MomentDerek
force-pushed
the
feat/wsl-zsh-startup
branch
from
October 8, 2026 03:45
24dd4de to
a43d160
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Result / 用户结果
ZDOTDIR).wsl.exestarted inside a taken-over guest receives none of the bootstrap variables.Relation to #351 (merged)
The takeover needs #351's WSL option-region parser (to tell a zsh guest command from
-e htopor an installer) and its spawn-time distribution pinning (a barewslor PTY-defaultshell=wslpane is otherwise not probed, and the probe must read the same guest the pane enters). Both are now onmain, so this PR reuses them instead of keeping a second copy of the option parser, which the snapshot note records as the cause of an earlier incident.Design / 设计边界
platform/shell_integrationwrites the zsh bootstrap shared with local zsh (res/shell/zshenv,zprofile,zshrc). New:platform/wsl_guest_shellasks the guest, once per (distribution, user) per process, which login shell its passwd entry names and whether that user can read the bootstrap at theWSLENV-translated path (res/shell/wsl-guest-probe.shoverwsl.exe -d <d> [-u <u>] --exec sh -s, built byshell_detect::wsl_exec_command);takes_zsh_bootstrapis the single takeover decision. The probe and the WSL hook installer share the existing bounded runnerplatform::process_output(new stdin entry pointread_with_input) instead of a second runner.wsl.exemust keep launching the guest login shell (the 1.1.0--exec bashregression), so zsh integration travels only throughWSLENV(ZDOTDIR/pu). The host cannot see the guest login shell or the guest's view of a drvfs path from the launch arguments, so the guest answers.nebula_terminalonly makestty::shell_line_endingspublic for reuse.mainwarms the first pane's guest at process start, off the main thread (shell_launch::startup_shell), and skips it when an explicit command was given. With a WSL default shell the warm-up starts a stopped distribution. Rationale and rejected alternatives:architecture/notes/nebula_app/platform/2026-09-28-wsl-zsh-startup-integration.md.zsh -crestores the user'sZDOTDIR. It preserves Ubuntu's global compinit (deferred until the user'sZDOTDIRis back, respectingGLOBAL_RCSand a.zprofileskip_global_compinit) and zsh's newuser wizard. Unknown (probe not yet answered or failed,--distribution-id/--system) leaves the guest untouched. ForwardingZDOTDIRin the hostWSLENVopts out before any probe. The bootstrap removes its own entries from the guest'sWSLENVand keeps the export attribute the user gaveZDOTDIR(both also apply to local zsh, fixingNEBULA_*leaking into programs exec'd from rc files and a missing newuser wizard). Probe and hook output is decoded leniently (lossy UTF-8). Known limits: panes spawned before their guest's first verdict have no zsh reports; Windows programs started from the guest still see the hostZDOTDIR; achshor mount change in the guest is seen by the next Pebrel process; a zsh started from a non-zsh login shell is not integrated.Evidence / 验证依据
mainc1c499d3(fix(wsl): pin each pane's distro, keep guest cwd in splits/tabs/forks, pass guest paths safely #351 merged); re-run after the rebase on Linux (WSL 2, Ubuntu 26.04):cargo test -p nebula --bin pebrel: 1855 passed, 0 failed, 14 ignored.python3 -m unittest scripts.tests.test_shell_integration: 31 ran, OK, 1 skipped.python3 scripts/check_architecture.py --base c1c499d3: exit 0.cargo.exe):cargo check -p nebula --all-targetsclean;cargo test -p nebula --bin pebrel: 1935 passed, 0 failed, 22 ignored.mainat1e569537):cargo test -p nebula --bin pebrel: 1891 passed, 0 failed, 19 ignored.python3 -m unittest scripts.tests.test_shell_integrationin an Ubuntu 26.04 WSL guest with real zsh 5.9 andsh: 31 ran, OK, 1 skipped (BSDlsonly). Covers the probe script (including a guest withoutgetent) andtest_zsh_bootstrap_forwards_nothing_to_nested_guests_and_keeps_zdotdir_unexported, which fails against the previous scripts.python3 scripts/check_architecture.py --base 1e569537(in WSL): exit 0.shell=/usr/bin/fish,bootstrap=readablein 0.23 s; a missing directory →bootstrap=unreadable;--user root→shell=/bin/bash,bootstrap=readable.wsl.exe --exec sh -c 'echo ${ZDOTDIR-unset}'printsunset.wsl.exewith a zsh login shell, and a cold-boot probe sincewsl.exenow runs inside the bounded runner's Windows job object.launches_that_cannot_take_the_bootstrap_are_never_probed,the_guest_answer_decides_the_bootstrap, plus the real-shprobe case). It was split out to keep each PR one decision under the size limit.ZDOTDIRleak fromzsh -c, the global compinit dump, the newuser wizard and exportedNEBULA_*fail against the earlier scripts.main, 1149 changed lines, under the 1500 limit.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.
中文版本
用户结果
ZDOTDIR)。wsl.exe,不会收到任何 bootstrap 变量。与 #351(已合并)的关系
接管需要 #351 的 WSL 选项区解析器(用来区分 zsh 来宾命令和
-e htop、安装程序等),也需要它在 spawn 时固定发行版(否则裸wsl或 PTY 默认shell=wsl的 pane 不会被探测,而且探测必须读的是 pane 实际进入的那个来宾)。两者现已在main上,本 PR 直接复用,不再保留第二份选项解析器;快照 note 记录过的一次事故,正是由这种重复造成的。设计边界
platform/shell_integration写入与本地 zsh 共用的 zsh bootstrap(res/shell/zshenv、zprofile、zshrc)。新增platform/wsl_guest_shell:每个进程对每个(发行版,用户)问一次来宾,passwd 记录里的登录 shell 是什么,以及该用户能否在经WSLENV转换后的路径上读到 bootstrap(通过wsl.exe -d <d> [-u <u>] --exec sh -s执行res/shell/wsl-guest-probe.sh,命令由shell_detect::wsl_exec_command构造)。takes_zsh_bootstrap是唯一的接管判断。探测和 WSL hook 安装器共用已有的有界执行器platform::process_output(新增带 stdin 的入口read_with_input),不再另写一个执行器。wsl.exe必须继续启动来宾登录 shell(1.1.0 的--exec bash回退),所以 zsh 集成只能经WSLENV传递(ZDOTDIR/pu)。宿主从启动参数里看不到来宾的登录 shell,也看不到来宾眼里的 drvfs 路径,所以由来宾来回答。nebula_terminal只是把tty::shell_line_endings改为 public 以便复用。main在进程启动时于后台线程预热第一个 pane 的来宾(shell_launch::startup_shell),显式给了命令时跳过。默认 shell 是 WSL 时,预热会启动已停止的发行版。决策依据和被否决的方案见architecture/notes/nebula_app/platform/2026-09-28-wsl-zsh-startup-integration.md。zsh -c会恢复用户的ZDOTDIR。它保留 Ubuntu 的全局 compinit(推迟到用户的ZDOTDIR恢复之后,并遵守GLOBAL_RCS和.zprofile里的skip_global_compinit),也保留 zsh 的 newuser 向导。结论未知时(探测还没回答或失败,或--distribution-id/--system)不动来宾。宿主WSLENV里如果已经转发ZDOTDIR,在任何探测之前就视为退出。bootstrap 会从来宾的WSLENV中删掉自己的条目,并保留用户给ZDOTDIR设的导出属性(这两点对本地 zsh 同样生效,顺带修复了NEBULA_*泄漏到 rc 里 exec 的程序、以及 newuser 向导不出现的问题)。探测和 hook 的输出按宽松 UTF-8 解码。已知限制:来宾第一次结论之前 spawn 的 pane 没有 zsh 上报;从来宾启动的 Windows 程序仍能看到宿主的ZDOTDIR;来宾里的chsh或挂载变化要到下一个 Pebrel 进程才生效;从非 zsh 登录 shell 里再启动的 zsh 不会被集成。验证依据
mainc1c499d3(fix(wsl): pin each pane's distro, keep guest cwd in splits/tabs/forks, pass guest paths safely #351 已合并),rebase 后在 Linux(WSL 2,Ubuntu 26.04)重新运行:cargo test -p nebula --bin pebrel:1855 个通过,0 个失败,14 个忽略。python3 -m unittest scripts.tests.test_shell_integration:运行 31 个,全部通过,跳过 1 个。python3 scripts/check_architecture.py --base c1c499d3:退出码 0。cargo.exe):cargo check -p nebula --all-targets无错误;cargo test -p nebula --bin pebrel:1935 个通过,0 个失败,22 个忽略。main1e569537的 fix(wsl): pin each pane's distro, keep guest cwd in splits/tabs/forks, pass guest paths safely #351):cargo test -p nebula --bin pebrel:1891 个通过,0 个失败,19 个忽略。python3 -m unittest scripts.tests.test_shell_integration(Ubuntu 26.04 WSL 来宾,真实 zsh 5.9 和sh):运行 31 个,全部通过,跳过 1 个(仅适用于 BSDls)。覆盖探测脚本(包括没有getent的来宾)和test_zsh_bootstrap_forwards_nothing_to_nested_guests_and_keeps_zdotdir_unexported,后者在旧脚本上会失败。python3 scripts/check_architecture.py --base 1e569537(在 WSL 中运行):退出码 0。shell=/usr/bin/fish、bootstrap=readable,耗时 0.23 秒;目录不存在 →bootstrap=unreadable;--user root→shell=/bin/bash、bootstrap=readable。wsl.exe --exec sh -c 'echo ${ZDOTDIR-unset}',输出unset。wsl.exe端到端启动一个登录 shell 为 zsh 的 pane;以及冷启动时的探测(wsl.exe现在运行在有界执行器的 Windows job 里)。launches_that_cannot_take_the_bootstrap_are_never_probed、the_guest_answer_decides_the_bootstrap,以及真实sh下的探测用例)。拆出来是为了让每个 PR 只含一个决策,并控制在行数上限以内。zsh -c泄漏 bootstrapZDOTDIR、全局 compinit dump、newuser 向导和导出NEBULA_*的 Python 用例,在旧脚本上都会失败。main一个 commit,1149 行,在 1500 行上限以内。必须确认
以上英文版的勾选项同样适用于本中文说明。
🤖 Generated with Claude Code