dsh-plugin-wsl-env 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/CONTRIBUTING.md +29 -7
  3. package/README.md +124 -138
  4. package/SECURITY.md +11 -1
  5. package/SUPPORT.md +29 -2
  6. package/cordis.patch.yml +17 -19
  7. package/docs/ARCHITECTURE.md +30 -29
  8. package/docs/CONFIGURATION.md +81 -21
  9. package/docs/LIMITATIONS.md +71 -6
  10. package/docs/MIGRATION.md +98 -0
  11. package/docs/PARITY.md +1 -1
  12. package/docs/PERFORMANCE.md +60 -0
  13. package/docs/QUICKSTART.md +125 -0
  14. package/docs/README.zh.md +117 -137
  15. package/docs/ROADMAP-1.0.md +780 -0
  16. package/docs/ROADMAP.md +4 -0
  17. package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +80 -0
  18. package/docs/UPSTREAM-LSP-SEAM.md +90 -0
  19. package/docs/UPSTREAM-SESSION-NOTICE.md +95 -0
  20. package/lib/agent-errors.d.ts +19 -0
  21. package/lib/agent-errors.js +22 -0
  22. package/lib/agent-exec.js +2 -2
  23. package/lib/agent.d.ts +68 -5
  24. package/lib/agent.js +129 -22
  25. package/lib/distro-route.d.ts +75 -0
  26. package/lib/distro-route.js +86 -0
  27. package/lib/fs-routing.d.ts +4 -1
  28. package/lib/index.d.ts +166 -37
  29. package/lib/index.js +353 -75
  30. package/lib/paths.d.ts +15 -0
  31. package/lib/paths.js +28 -0
  32. package/lib/preflight.d.ts +46 -3
  33. package/lib/preflight.js +83 -14
  34. package/lib/shell-env.d.ts +9 -3
  35. package/lib/shell-env.js +121 -43
  36. package/lib/subprocess.d.ts +12 -3
  37. package/lib/subprocess.js +21 -3
  38. package/package.json +23 -15
  39. package/scripts/diagnose.sh +9 -88
  40. package/scripts/doctor.mjs +548 -0
  41. package/scripts/profile-preset.mjs +254 -0
  42. package/lib/workspace-files-route.d.ts +0 -136
  43. package/lib/workspace-files-route.js +0 -225
  44. package/lib/workspace-files-wsl.d.ts +0 -257
  45. package/lib/workspace-files-wsl.js +0 -418
package/README.md CHANGED
@@ -6,200 +6,186 @@
6
6
  [![npm](https://img.shields.io/npm/v/dsh-plugin-wsl-env)](https://www.npmjs.com/package/dsh-plugin-wsl-env)
7
7
  [![license](https://img.shields.io/npm/l/dsh-plugin-wsl-env)](LICENSE)
8
8
 
9
- This plugin lets [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) work inside a WSL distro. The model's commands run there. Its file tools read and write the real distro files. The folder picker can open a distro folder, and the GUI terminal opens a shell inside it.
9
+ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that runs a session inside a WSL 2 distribution on Windows.
10
10
 
11
- **Windows + WSL2 only.** One install command, no dependencies of its own. The environment is chosen per session: a session on a Windows folder keeps the normal Windows tools.
11
+ The harness process stays on Windows. Commands, file operations, the folder picker and the GUI terminal use the distro. No helper service or daemon is installed, no port is opened, and confined commands run under `bubblewrap`.
12
12
 
13
- [Install](#install) · [Using it](#using-it) · [How it compares](#how-it-compares) · [Configure](#configure) · [Recipes](#recipes) · [Architecture](#architecture) · [Sandbox](#sandbox) ·
14
- [Troubleshooting](#troubleshooting) · [Development](#development) · [Documentation](#documentation)
13
+ Windows + WSL 2 only.
15
14
 
16
- ## Install
15
+ **New here?** Start with [docs/QUICKSTART.md](docs/QUICKSTART.md) — five minutes, prerequisites, recipes and the honest compatibility table.
17
16
 
18
- Run these four commands in a Windows terminal:
17
+ ## Why this plugin
19
18
 
20
- ```powershell
21
- dsh wsl --from-default-profile web --dump-config # 1. create a profile (a named set of dsh settings) from the Web template
22
- dsh plugin --profile wsl add dsh-plugin-wsl-env # 2. install the plugin and its settings
23
- dsh --profile wsl --dump-config # 3. a quick check: see the merged settings without starting the app
24
- dsh --profile wsl # 4. run
25
- ```
19
+ A Windows coding tool that works on WSL projects has three options: run the whole tool inside the distro, connect to a helper server inside the distro, or let the plugin provide the distro environment. This plugin is the third option. One workflow covers both kinds of session, with the distro's real tools and the command sandbox intact.
26
20
 
27
- Step 3 should print a line `# == dsh-plugin-wsl-env`. That line is the plugin's settings arriving in your profile. The plugin ships its own settings, so step 2 wires everything in automatically — there is nothing to merge by hand.
21
+ - **Real Linux environment.** Commands run in the distro's own login shell. Its `PATH`, `nvm`, `cargo`, `pyenv` and rc files apply. File tools read and write the distro's real files on ext4.
22
+ - **No service in the distro.** There is no helper server, no daemon and no listening port. The only persisted artifact is a content-addressed copy of the native agent in `~/.cache/dsh-wsl/agent/`.
23
+ - **Sandboxed by default.** Confined commands run under `bubblewrap` inside the distro. If `bubblewrap` is missing, the command fails; it does not run unconfined.
24
+ - **Per-session.** A session opened on a distro folder gets the distro environment. A session opened on a Windows folder keeps the normal Windows tools. Both can run in one harness process.
25
+ - **Integrated with the GUI.** The folder picker lists installed distros, the terminal opens a shell in the distro, and the model can see listening ports.
28
26
 
29
- **Before the first command, give the distro bubblewrap.** Every command runs inside a bubblewrap sandbox, and most distros do not have bubblewrap preinstalled. Without it, every command fails instead of running unsandboxed:
27
+ ### Why Linux, not PowerShell
30
28
 
31
- ```powershell
32
- wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap # Debian/Ubuntu
33
- ```
29
+ Most coding agents learn from Linux-first material, and most open-source toolchains are built and tested on Linux first. Asking an agent to work through a Windows shell adds a translation layer on every call:
34
30
 
35
- From a plugin checkout, `pnpm run bootstrap -- <distro>` checks the four tools the plugin uses (bubblewrap, ripgrep, git, inotify-tools) and prints the install command for the distro's package manager; `--install` runs it. If this step is skipped, the session reports the missing package with the exact install command when it opens.
31
+ - **Training prior.** Examples, CI scripts, Dockerfiles and most public answers use POSIX shells and GNU tools. A model's first instinct is `bash`, not PowerShell.
32
+ - **Toolchain support.** Node, Python, Rust, Go, C/C++ and most build systems are validated on Linux first. Windows support is often a port with different quoting, paths, line endings and file locking.
33
+ - **Shell semantics.** PowerShell and `cmd` differ from POSIX in quoting, escaping, pipelines, environment variables, exit codes and path separators. Mixing the two produces errors that look like model mistakes.
34
+ - **Environment drift.** Windows setups vary by PowerShell version, execution policy, PATH and installed tools. Linux environments are easier to reproduce and containerize.
36
35
 
37
- **The shipped preset also needs systemd.** Commands run in one systemd user scope per session (background dev servers survive, `/tmp` is shared, `ps` sees the session). Set `[boot] systemd=true` in `/etc/wsl.conf`, restart WSL, and enable lingering once: `loginctl enable-linger "$USER"`. The session refuses with those remedies if any piece is missing; `sessionSandbox: false` on the preset's `wsl-shell` restores the per-command sandbox.
36
+ The result is more failed commands, more retries and more time spent on syntax instead of the task. Generated code is affected too: shell snippets, build commands and CI-style one-liners are usually written for POSIX, and the distro is where they run unchanged. A real Linux login shell removes that translation layer. Modern PowerShell and Windows-native toolchains narrow the gap, but the default prior and most project scripts still assume POSIX.
38
37
 
39
- Then open a folder like `\\wsl.localhost\<distro>\...` in the GUI. The folder picker lists every installed distro, and **New terminal** opens a shell inside the distro.
38
+ This plugin keeps the GUI on Windows and moves only the execution environment into the distro, so the agent gets the shell that its training and the toolchain both assume.
40
39
 
41
- Uninstall: `dsh plugin --profile wsl remove dsh-plugin-wsl-env`. Upgrade: run the same `add` command again.
40
+ ## How it compares
42
41
 
43
- > **Changed anything under `lib/`? Sync the Windows-side copy, then restart the app.** The app loads the Windows-side copy of the code, so run `pnpm run sync:windows` first. A restart without the sync just reloads the old code, and a running process also caches modules. See [Development](#development).
42
+ | | Desktop remote (VS Code, ZCode) | This plugin |
43
+ |---|---|---|
44
+ | Inside the distro | a helper server | one cached agent binary |
45
+ | Network | a listening port | none |
46
+ | Command sandbox | none | `bubblewrap` |
47
+ | Removal | delete the server | delete the cache |
44
48
 
45
- ## Using it
49
+ A desktop remote reconnects faster and is more mature. A CLI agent installed inside WSL is simpler if it covers your work. This plugin is for the case where the harness stays on Windows and commands must be sandboxed.
46
50
 
47
- Open a distro folder as the workspace — for example `\\wsl.localhost\ubuntu\home\you\project` — and ask the model "what kernel am I on, and what is in `/etc/os-release`?". It runs `uname -r` and reads that file inside the distro. Nothing is copied, and nothing crosses `/mnt/c`.
51
+ ## Requirements
48
52
 
49
- - **A distro folder gets the distro environment.** When a session opens a distro folder, it is automatically given the WSL environment. The very first command is already right.
50
- - **Commands run where you expect.** They run inside the distro, in your own login shell — so your `PATH`, `nvm`, `cargo`, `pyenv` and rc files all apply. The shell comes from the distro's configuration; it is not hardcoded to bash.
51
- - **Files are the real files.** `/home/you/x` and `\\wsl.localhost\ubuntu\home\you\x` are the same file. `/mnt/c/...` reaches the Windows disk as usual.
52
- - **The terminal too.** Right sidebar, *New terminal*: a shell inside the distro, in the session's folder.
53
- - **Port visibility.** The model sees which ports are listening inside the distro (refreshed about every 10 seconds) and can return the exact URL of a dev server it starts. WSL2 forwards localhost to Windows, so the URL opens in the browser directly.
54
- - **Permissions work like on a Linux host.** The Permissions selector offers `read-only`, `workspace-write` (the default) and `danger-full-access`. When something is refused, the model is offered one retry with the smallest permission that would work. Only `danger-full-access` runs without the sandbox.
53
+ - Windows 10 or 11 with WSL 2 and at least one distro.
54
+ - `bubblewrap` in the distro. It is required.
55
+ - systemd in the distro for the default session sandbox: set `[boot] systemd=true` in `/etc/wsl.conf`, restart WSL, then run `loginctl enable-linger "$USER"` once. Without systemd, set `sessionSandbox: false` on both `wsl-shell` and `wsl-fs`.
56
+ - Optional: `ripgrep` for search, `git` for change snapshots, `inotify-tools` for file watching.
55
57
 
56
- ## How it compares
58
+ No Node.js is required inside the distro.
57
59
 
58
- Three ways for a Windows coding tool to work on a WSL project:
60
+ ## Install
59
61
 
60
- 1. **Install the tool inside WSL.** Codex CLI, Claude Code and ZCode CLI all recommend this, and they are right — it is the simplest path when a CLI tool covers your work. It needs the whole tool to live inside WSL. DeepSeek Harness cannot, so it needs one of the next two.
61
- 2. **Connect from a desktop app.** VS Code (Remote-WSL, and its open-source mirror [open-remote-wsl](https://github.com/jeanp413/open-remote-wsl)) and agent desktops like [ZCode](https://github.com/zai-org/ZCode) keep the app on Windows and set up a helper server inside the distro. Battle-tested. The cost: a server folder (`~/.vscode-server`, `~/.zcode/server`), a port, and no sandbox for commands.
62
- 3. **This plugin.** The harness stays on Windows. Nothing is installed in the distro, no port is opened, and commands run inside a bubblewrap sandbox. If something is missing, the session says what to install the moment it opens.
62
+ Run these commands in a Windows terminal:
63
63
 
64
- | | Option 2 (desktop remote) | Option 3 (this plugin) |
65
- |---|---|---|
66
- | installed in the distro | a server folder | nothing |
67
- | opens a port | yes | no |
68
- | command sandbox | none | bubblewrap + systemd session scope |
69
- | after uninstall | remove the server manually | nothing left |
70
- | after a WSL upgrade | server may need repair | the next command recovers automatically |
64
+ ```powershell
65
+ dsh wsl --from-default-profile web --dump-config # create the profile
66
+ dsh plugin --profile wsl add dsh-plugin-wsl-env # install the plugin and its settings
67
+ dsh --profile wsl --dump-config # verify: prints "# == dsh-plugin-wsl-env"
68
+ dsh --profile wsl # start
69
+ ```
71
70
 
72
- **Where the others win.** A desktop remote re-opens a workspace instantly — its server stays warm — and VS Code's remote has years of hardening behind it. A CLI inside WSL needs none of this. This plugin is for one case: DeepSeek Harness on Windows, working on WSL projects, with commands sandboxed.
71
+ Install `bubblewrap` before the first command:
73
72
 
74
- ## Configure
73
+ ```powershell
74
+ wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap
75
+ ```
75
76
 
76
- Each setting lives on a named row. To change one, add a row with the same id to `$DSH_HOME/profiles/<name>/cordis.patch.yml` (`$DSH_HOME` is DSH's settings folder). The keys worth knowing:
77
+ From a checkout, `pnpm run doctor` is the one-command environment check: Windows/WSL versions, the distro and its shell, bubblewrap usability, systemd / the user manager / Linger, the optional tools, peer versions, the agent cache, a fresh-agent handshake, stale session scopes and the known profile-patch trap. Each failure prints one copy-paste remedy; `--json` is for scripts, and `--fix` sweeps stale scopes (package installation needs `--fix --install` and an explicit confirmation). `pnpm run diagnose` is its read-only alias. `pnpm run bootstrap -- <distro>` remains the narrower tool-install helper.
77
78
 
78
- | Row | Key | Default | Meaning |
79
- |---|---|---|---|
80
- | `wsl-shell` | `distro` | `''` | which distro to use; empty means WSL's default distro |
81
- | | `sandbox` | `true` | run commands inside the `bubblewrap` sandbox; `false` turns it off |
82
- | | `sessionSandbox` | `true` | run the session's commands in one persistent systemd user scope: background dev servers survive, `/tmp` is shared, `ps` sees the session. Needs systemd as PID 1, a reachable user manager and `Linger`; the preflight refuses with the remedy. `false` restores the per-command sandbox |
83
- | | `agentBackend` | `auto` | the session runtime's agent: `auto` uses the native in-distro agent when a verified source exists (packaged static-musl binary at `agent/native/linux-<arch>/dsh-wsl-agent` (x86_64/aarch64) or the `DSH_WSL_AGENT_NATIVE` override), else the shell agent; `native` refuses instead of falling back; `shell` forces the shell agent. Covers commands and confined writes. The native backend is the default; the shell agent is the fallback |
84
- | `wsl-fs` | `distro` | `''` | same as above |
85
- | | `restrictToDistro` | `true` | refuse paths that belong to a **different** distro. (`/mnt/c` belongs to this distro, so it is not affected.) The refusal is `FS_OUTSIDE_DISTRO`; wider permissions cannot lift it |
86
- | | `sandbox` | `true` | check file writes against the same policy |
87
- | | `sessionSandbox` | `true` | run confined file writes in the same session runtime as commands (one real `/tmp`, one workspace mount view); needs the same systemd/linger setup; `false` uses the per-policy confined resident |
88
- | | `substrate` | `agent` | how file operations reach the distro. Only one value remains: the in-distro agent, so reads and writes run on the distro's own filesystem, with native symlinks and permissions. The former `"share"` option used the 9p share and is rejected at startup |
89
- | `directory-picker-wsl` | `includeHostHome` | `true` | also list the Windows home directory in the picker |
90
- | `subprocess-wsl` | `distro` | `''` | which distro the GUI terminal opens in |
79
+ Upgrade by running the `add` command again. Uninstall with `dsh plugin --profile wsl remove dsh-plugin-wsl-env`; the agent cache under `~/.cache/dsh-wsl` can then be deleted. If you added a local `workspace-files-wsl` workaround to your profile to restore previews, remove it after upgrading: the plugin no longer mounts that row.
91
80
 
92
- `wsl-shell` and `wsl-fs` cannot be reached by adding `- id: wsl-shell` to your override file: they live inside the `wsl` preset's own settings (a preset is a named environment DSH can give a session), so the loader only warns `patch: entry "wsl-shell" not found` and the shipped value stays in force. To change them, copy and override the whole `preset-wsl` row — [docs/CONFIGURATION.md](docs/CONFIGURATION.md#overriding-the-rows-inside-preset-wsl) has the recipe.
81
+ ## Using it
93
82
 
94
- [`cordis.patch.yml`](cordis.patch.yml) is the commented reference for every value the plugin ships. [docs/CONFIGURATION.md](docs/CONFIGURATION.md) lists the rest — `shell`, `loginShell`, `cwd`, `timeoutMs`, `preferredDistro`, `maxEntries` — and [examples/profile.cordis.patch.yml](examples/profile.cordis.patch.yml) is a settings file you can copy and edit.
83
+ Open a distro folder as the workspace, for example `\\wsl.localhost\ubuntu\home\you\project`.
95
84
 
96
- ## Recipes
85
+ - Opening a folder in any distro runs there, even when it is not WSL's default.
86
+ - Commands run in the distro's login shell. `/home/you/x` and `\\wsl.localhost\ubuntu\home\you\x` are the same file.
87
+ - **New terminal** opens a shell in the same distro and directory.
88
+ - The model sees listening ports and can return the URL of a dev server. WSL 2 forwards localhost to Windows.
89
+ - The Permissions selector offers `read-only`, `workspace-write` (default) and `danger-full-access`. Only `danger-full-access` runs without the sandbox.
97
90
 
98
- - **Use your Windows git credentials inside the distro**: `git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"` (adjust the path to where your Windows Git is installed). After this, `git push` inside the distro uses the same saved credentials as Windows.
99
- - **Keep projects on the distro's own disk.** The model works on Linux paths (`/home/...`) on the distro's own disk, which is fast. It can still reach Windows files through `/mnt/c`, but that bridge is slow when many small files are involved. `pnpm run bootstrap -- <distro>` tells you whether ripgrep, git and inotifywait — used for search and file watching — are installed.
100
- - **Environment variables**: WSL only forwards the variables named in `WSLENV`. This plugin forwards its own `DSH_*` values, and translates the two that hold Windows paths (`DSH_HOME`, `DSH_PROFILE_DIR`) into Linux paths. Your `PATH` is never forwarded — forwarding it would hide the distro's own PATH.
91
+ ## Configure
92
+
93
+ Settings live on named rows in `$DSH_HOME/profiles/<name>/cordis.patch.yml`. The main values:
94
+
95
+ | Row | Key | Default | Purpose |
96
+ |---|---|---|---|
97
+ | `wsl-shell` | `distro` | `''` | distro mode; empty/`auto` follows the workspace, `default` forces WSL's default |
98
+ | | `sandbox` | `true` | confine commands with `bubblewrap` |
99
+ | | `sessionSandbox` | `true` | run the session's commands in one systemd user scope |
100
+ | | `agentBackend` | `auto` | `auto`, `native` or `shell` for the in-distro agent |
101
+ | | `maskWindowsDrive` | `false` | hide `/mnt` from confined commands |
102
+ | `wsl-fs` | `distro` | `''` | distro mode; empty/`auto` follows each target path |
103
+ | | `restrictToDistro` | `true` | under a fixed distro, refuse paths that belong to another distro |
104
+ | | `sandbox` | `true` | check confined writes against the policy |
105
+ | | `sessionSandbox` | `true` | run confined writes in the session runtime |
106
+ | | `substrate` | `agent` | in-distro agent; `share` is rejected at startup |
107
+ | `directory-picker-wsl` | `includeHostHome` | `true` | also list the Windows home directory |
108
+ | `subprocess-wsl` | `distro` | `''` | distro mode; empty/`auto` follows the launch directory |
109
+
110
+ `wsl-shell` and `wsl-fs` are inside the shipped `wsl` preset, so overriding them by id does not work. Override the whole `preset-wsl` row instead; [docs/CONFIGURATION.md](docs/CONFIGURATION.md) has the recipe and the remaining keys. [`cordis.patch.yml`](cordis.patch.yml) is the commented reference for every shipped value.
101
111
 
102
112
  ## Architecture
103
113
 
104
- One DSH process can serve both kinds of session at the same time: a session on a Windows folder, and a session inside the distro. The plugin's parts attach at two levels, because some things belong to one session and some belong to the whole app:
114
+ One harness process can serve Windows-folder and distro-folder sessions at the same time. Services attach at two levels:
105
115
 
106
116
  ```text
107
- composition (app level, one per process)
108
- ├─ subprocess-wsl the GUI terminal's execution world
109
- │ WSL-folder session → the distro shell, in the session's Linux directory
110
- │ Windows-folder session → powershell.exe, in the session's Windows directory
111
- ├─ directory-picker-wsl installed distros listed beside the Windows home
112
- ├─ workspace-files-wsl the GUI file tree and previews of a distro workspace, served from inside it
113
- ├─ fs-routing the root filesystem, routed by coordinate
114
- │ distro UNC → the resident agent; a drive path → the host backend
115
- ├─ wsl-shell-env the DSH_WSL_DISTRO / _SHELL / _HOME / _PORTS facts the model sees
116
- └─ auto-preset binds the wsl preset when a session opens a distro folder
117
-
118
- preset-wsl (the wsl agent preset; its settings are separate from the app's)
119
- ├─ wsl-shell ctx.shell — wsl.exe --exec <login shell>, confined by bubblewrap inside the distro
120
- └─ wsl-fs ctx.fs — real distro files on the resident agent substrate (ext4)
117
+ app level (one per process)
118
+ ├─ directory-picker-wsl, workspace-files, subprocess-wsl
119
+ ├─ fs-routing: root filesystem routed by path
120
+ └─ auto-preset: binds the wsl preset when a session opens a distro folder
121
+
122
+ wsl preset (per session)
123
+ ├─ wsl-shell: commands
124
+ └─ wsl-fs: file operations
121
125
  ```
122
126
 
123
- **Why two levels.** The `wsl` preset holds `wsl-shell` and `wsl-fs`, the two things that differ per session. When a session opens a distro folder, `auto-preset` gives that session the preset; a Windows-folder session keeps the normal Windows tools. One environment per session, in one process. The GUI terminal is the exception: it reads its shell from the app level, which never sees a preset, so `subprocess-wsl` is mounted at the app level. The GUI file tree and the root filesystem have the same shape of problem — every session uses them, so no single preset can own them — and `workspace-files-wsl` and `fs-routing` sit at the app level for that reason. They replace two shipped rows; [docs/root-fs-routing.md](docs/root-fs-routing.md) has the routing design.
124
-
125
- **How a command runs.** A session gets one resident agent inside a systemd user scope. By default that agent is the native `dsh-wsl-agent` — the packaged static-musl binary for the distro's architecture, selected by `agentBackend: auto`; the runtime's shell agent is the fallback when the binary is missing or fails its copy/digest/exec check, and `agentBackend: shell` forces it while `native` refuses instead of falling back. The agent executes each command in your login shell under a bubblewrap mount profile. Because the scope outlives the command, a background dev server survives, `/tmp` is the distro's real `/tmp`, and `ps` sees the whole session. With `sessionSandbox: false`, the agent stays per distro, each command gets its own bwrap profile (`--unshare-pid`, a fresh `/tmp`), background processes do not survive, and the native backend is not used. If the agent is unavailable (or you set `agent: false`), the one-shot `wsl.exe` path runs the command instead — same result, slightly slower, and per-command isolation.
126
-
127
- **How file operations run.** Reads, writes and searches also happen inside the distro, on its own filesystem, with the distro's own tools. A search over a distro workspace runs the distro's `rg`, never the Windows binary over the 9p share. File writes are checked against the same policy the command sandbox enforces; with `wsl-fs` `sessionSandbox` on (the shipped preset), a confined write runs in the same session runtime as the commands — the same native/shell agent — so it sees the same `/tmp` and workspace mounts. Reads use the shared native read agent (A7; the packaged binary, shell fallback): 16 concurrent reads measure ~2.4 ms, down from 342-614 ms on the shell.
127
+ - `wsl-shell` runs commands through one resident agent inside a systemd user scope. The agent is the packaged static-musl `dsh-wsl-agent` for the distro's architecture (`x86_64` or `aarch64`), with the shell agent as fallback. Each command runs in the login shell under a `bubblewrap` mount profile. The scope outlives the command, so background servers survive and `ps` sees the session. With `sessionSandbox: false`, the agent stays per distro and each command gets its own profile.
128
+ - `wsl-fs` serves reads and writes through the same in-distro agent. Confined writes run in a long-lived agent whose mount profile matches the policy, so the kernel enforces the write boundary. There is no host-side file fallback: if the agent is unavailable, file operations fail.
129
+ - The GUI file tree and previews are the shipped `workspace-files` row, not a plugin replacement: its client half is the browser `file` provider. It reads WSL paths through the routed root filesystem, so it stays distro-side, at the cost of a few extra agent round trips.
130
+ - App-level services route by path. A distro UNC path goes to the distro; a Windows drive path stays on the host. See [docs/root-fs-routing.md](docs/root-fs-routing.md).
128
131
 
129
132
  ## Sandbox
130
133
 
131
- Commands run inside a `bubblewrap` sandbox in the distro, and file writes are checked against the same policy. The Windows sandbox cannot be used here: its restricted account cannot reach WSL at all.
132
-
133
- | Mode | What a command inside the distro can do |
134
+ | Mode | What a command can do |
134
135
  |---|---|
135
- | `read-only` | read the whole distro. A fresh `/dev` is mounted so `/dev/null` and `/dev/shm` work; nothing else is writable |
136
- | `workspace-write` | all of the above, plus the session workspace is writable and `/tmp` is the distro's real `/tmp` (with `sessionSandbox: false`, the per-command profile mounts a fresh tmpfs there instead) |
137
- | `danger-full-access` | no sandbox; used when you approve a wider-permission request |
136
+ | `read-only` | read the whole distro; a fresh `/dev` is mounted, nothing else is writable |
137
+ | `workspace-write` | the above, plus the session workspace; `/tmp` is the distro's real `/tmp` with the session runtime |
138
+ | `danger-full-access` | no sandbox |
138
139
 
139
- **bubblewrap is required, and the plugin fails closed.** Without it, every confined command reports `SANDBOX_UNAVAILABLE` — the command does not run unsandboxed. To turn the sandbox off, set `sandbox: false` on the row that owns the operation: `wsl-shell` for commands, `wsl-fs` for file writes, and the top-level `fs-routing` row for writes to the app's own filesystem. (The two preset rows are reached through `preset-wsl` — see [Configure](#configure).) The model is then told that these operations have no sandbox.
140
+ With `sessionSandbox: false`, each command gets a private `/tmp` instead of the distro's real one.
140
141
 
141
- **Known limits.** A command inside the distro can still start a Windows program (anything under `/mnt/c/.../*.exe`), and bubblewrap does not watch Windows programs. `pnpm run probe:sandbox` demonstrates this boundary on your machine. Setting `maskWindowsDrive: true` on the three rows narrows it: `/mnt` disappears from the command's view, so the drive's files cannot be read and its programs cannot be started. It does not close the hole completely — a command could still copy a Windows program into the workspace and run it — so the report stays `partial`. The only complete fix is in the distro itself: `[interop] enabled=false` in `wsl.conf` (see [docs/CONFIGURATION.md](docs/CONFIGURATION.md)).
142
+ `bubblewrap` is required. If it is missing or broken, confined commands report `SANDBOX_UNAVAILABLE` and do not run. Setting `sandbox: false` on the owning row disables confinement; the model is told that the operation is unsandboxed.
142
143
 
143
- See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) for the design, and [docs/LIMITATIONS.md](docs/LIMITATIONS.md) for everything the plugin does not do.
144
+ Enforcement is reported as `partial`. A command inside the distro can still start a Windows executable through `/mnt/c`, and `bubblewrap` cannot control Windows processes. `maskWindowsDrive: true` hides `/mnt` from the confined view, which narrows the hole but does not close it. The complete fix is distro-level: set `[interop] enabled=false` in `/etc/wsl.conf`. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) and [SECURITY.md](SECURITY.md).
144
145
 
145
146
  ## Troubleshooting
146
147
 
147
- | Symptom | Cause | Fix |
148
- |---|---|---|
149
- | every command reports `SANDBOX_UNAVAILABLE` | `bubblewrap` is missing from the distro, or present but broken — the error text tells you which one it is and what to do | follow the fix in the error, or set `sandbox: false` on `wsl-shell` (commands) and `wsl-fs` (writes) — see [Configure](#configure) for how to reach those rows |
150
- | a command or write is refused outside the session folder | expected: `workspace-write` only allows writes in the session folder | accept the wider-permission offer, or open the session on the folder you need |
151
- | writes are refused even inside the workspace | the session is in `read-only` mode | switch the Permissions selector |
152
- | `dsh plugin add` prints no confirmation that anything was added | the plugin is already installed, so there is nothing new to report — this is success, not failure | nothing to do: the settings are already in place, and `dsh --profile wsl --dump-config` still shows `# == dsh-plugin-wsl-env` |
153
- | the terminal still opens `cmd.exe` | the `terminal-controller` row from the plugin's settings did not apply | check that `dsh --profile wsl --dump-config` shows `shell: { path: wsl.exe, name: WSL }` |
154
- | changes to `lib/` have no effect | the app runs a Windows-side copy of the code, and a running process also caches modules | run `pnpm run sync:windows` to update the copy, then restart the app |
155
- | `link:\\wsl.localhost\...` leaves a broken symlink | pnpm cannot link a UNC path | link a Windows path instead; developing inside the distro needs the Windows-side copy, see [Development](#development) |
156
- | `glob` and `grep` are slow | searches inside the distro need `rg` installed in the distro; Windows-folder searches are unaffected | run `pnpm run bootstrap -- <distro> --install` (installs ripgrep); distro searches always run the distro's rg — they never cross the slow bridge |
157
- | the terminal reports `unknown` activity | only while the in-distro agent is temporarily unavailable — distro terminals are watched from inside the distro (a `DSH_TERMINAL_ID` marker scanned in `/proc`: a shell alone is `idle`, a shell running anything is `busy`) | check the distro is running; idle terminals are closed automatically after the controller's idle timeout (2 h by default), and `terminalIdleReclaim: false` turns the auto-close off |
158
- | a result names an `FS_*` code | the code says what refused it, and what clears it | see the error-code table in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#error-codes) |
148
+ | Symptom | Fix |
149
+ |---|---|
150
+ | every command reports `SANDBOX_UNAVAILABLE` | install `bubblewrap` in the distro, or set `sandbox: false` |
151
+ | the session refuses before the first command | run `pnpm run doctor`; enable systemd and linger, or set `sessionSandbox: false` on `wsl-shell` and `wsl-fs` |
152
+ | `glob` and `grep` are slow in the distro | install `ripgrep` with `pnpm run bootstrap -- <distro> --install` |
153
+ | `dsh plugin add` prints nothing | the plugin is already installed; this is success |
154
+ | a profile patch for `wsl-shell`/`wsl-fs` changes nothing | those rows are nested inside `preset-wsl`. Run `pnpm run preset -- --check <patch>`; it fails with the complete block to paste |
155
+ | changes under `lib/` have no effect | run `pnpm run sync:windows`, then restart the app |
156
+
157
+ More error codes and refusals are in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#error-codes). Everything the plugin does not do is in [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
159
158
 
160
159
  ## Development
161
160
 
162
161
  ```bash
163
- pnpm run build # compile src/ into lib/ (the built files are committed; rebuild after editing src/)
164
- pnpm test # style checks, syntax pass, build consistency, type check, and unit tests
165
- pnpm run sync:windows # copy the checkout onto the Windows-side copy the app loads (after any lib/ change, then restart the app)
166
- pnpm run test:coverage # the unit tests with coverage thresholds (Node 22.8+)
167
- pnpm run diagnose # read-only diagnostic report for a bug issue: versions, tools, bwrap probe
168
- pnpm run bootstrap -- <distro> # check the four distro tools; add --install to install what is missing
169
- pnpm run probe:sandbox # measure inside the distro what bubblewrap does and does not confine
170
- pnpm run probe # filesystem probe against a real distro (Windows + WSL only)
171
- pnpm run probe:sandbox-shell # boot a real harness and drive the confined executor
172
- pnpm run probe:terminal # open a PTY through the terminal provider
173
- pnpm run probe:substrate # drive the agent filesystem over a real wsl.exe transport (from inside the distro)
174
- pnpm run probe:watch # arm the in-distro watcher over a real directory (from inside the distro)
175
- pnpm run probe:agent # compare the resident path against the fallback path (from inside the distro)
176
- pnpm run probe:exec # the agent-backed execution: timeout, kill, cwd failure (from inside the distro)
177
- pnpm run probe:missing-wsl # boot a profile whose wslPath cannot start (Windows + WSL only)
178
- pnpm run probe:picker # list the picker's root level, refusals and its cap (Windows + WSL only)
179
- pnpm run probe:mode # which POSIX-mode facts survive the share (needs a Windows node and an installed harness; opens no profile)
180
- pnpm run probe:sandbox-off # prove sandbox: false unconfines both providers (Windows + WSL only)
162
+ pnpm install # development dependencies
163
+ pnpm run build # compile src/ into lib/ (lib/ is committed)
164
+ pnpm test # style, syntax, build consistency, types, unit tests
165
+ pnpm run test:coverage # unit tests with coverage thresholds
166
+ pnpm run sync:windows # update the Windows-side copy the app loads
181
167
  ```
182
168
 
183
- The app runs a Windows-side copy of the code, not your checkout. After changing anything under `lib/`, run `pnpm run sync:windows` before restarting the app — otherwise the restart just loads the old code. `pnpm run build` regenerates `lib/` from `src/`; the built files are committed, and `pnpm test` checks that they match.
184
-
185
- Run `pnpm install` once to get the development dependencies. The tests use the pinned `@deepseek-ai/*` packages; the plugin itself ships none. CI runs the tests on Node 22 and 24, on Linux and Windows, and coverage on Node 24.
169
+ The toolchain follows the harness host: Node `^22.19.0 || >=24.0.0`. `src/` is TypeScript; `lib/` is generated and must stay in sync. The app loads a Windows-side copy of `lib/`, so run `sync:windows` and restart the app after a build.
186
170
 
187
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) has the file layout, how the parts attach, and the sandbox design. [CONTRIBUTING.md](CONTRIBUTING.md) has the development loop, the Windows-side copy, the full gate list, and what has been verified.
171
+ Real-machine probes run on Windows + WSL only. See [CONTRIBUTING.md](CONTRIBUTING.md) and `test/probe/`.
188
172
 
189
173
  ## Documentation
190
174
 
191
- | Document | What it covers |
175
+ | Document | Content |
192
176
  |---|---|
193
- | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | every configuration key, and how to override it |
194
- | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | the plugin's parts and how they attach, path coordinates, the sandbox, testing, file layout |
195
- | [docs/ROADMAP.md](docs/ROADMAP.md) | the plan: done, next (A7 native shared reads, A8 aarch64), deferred (v6) |
196
- | [docs/LIMITATIONS.md](docs/LIMITATIONS.md) | what the plugin does not do, and why |
197
- | [docs/VALUE.zh.md](docs/VALUE.zh.md) | the plugin's value in Chinese: what it is worth, what it costs, and when not to use it |
198
- | [CONTRIBUTING.md](CONTRIBUTING.md) | the development loop, gates, conventions, verification |
199
- | [SECURITY.md](SECURITY.md) | reporting a vulnerability privately |
200
- | [SUPPORT.md](SUPPORT.md) | supported versions, and where to ask |
201
- | [docs/RELEASING.md](docs/RELEASING.md) | the release checklist |
202
- | [docs/archive/README.en.md](docs/archive/README.en.md) | English index of the archived engineering record |
177
+ | [docs/QUICKSTART.md](docs/QUICKSTART.md) | five-minute first run, recipes, compatibility |
178
+ | [docs/PERFORMANCE.md](docs/PERFORMANCE.md) | ext4 agent vs 9p, measured latency, how to reproduce |
179
+ | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | every configuration key and how to override it |
180
+ | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | services, path routing, sandbox, error codes |
181
+ | [docs/LIMITATIONS.md](docs/LIMITATIONS.md) | what the plugin does not do |
182
+ | [docs/ROADMAP.md](docs/ROADMAP.md) | current plan and deferred work |
183
+ | [docs/ROADMAP-1.0.md](docs/ROADMAP-1.0.md) | the 1.0 and ecosystem plan |
184
+ | [docs/VALUE.zh.md](docs/VALUE.zh.md) | value and trade-offs in Chinese |
185
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | development loop, gates, probes |
186
+ | [SECURITY.md](SECURITY.md) | private vulnerability reporting |
187
+ | [SUPPORT.md](SUPPORT.md) | supported versions and support channels |
188
+ | [docs/RELEASING.md](docs/RELEASING.md) | release checklist |
203
189
 
204
190
  ## License
205
191
 
package/SECURITY.md CHANGED
@@ -88,8 +88,18 @@ This is a documented design boundary, not a vulnerability by itself. The command
88
88
  distro, where `bubblewrap` and `node` must be installed; the README states it in
89
89
  its Sandbox section.
90
90
 
91
+ The strict profile (`maskWindowsDrive: true` with `sandbox: true`) narrows this:
92
+ confined profiles shadow `/mnt` with an empty tmpfs, so the Windows executables
93
+ cannot be launched from there. It does not close the hole — a Windows executable
94
+ written into the workspace still runs, because WSL's `binfmt_misc` dispatches on
95
+ content, not location — so the reported enforcement stays `partial`. The complete
96
+ closure is distro-level (`[interop] enabled=false` in `/etc/wsl.conf`). The three
97
+ states and their evidence are tabulated in
98
+ [docs/CONFIGURATION.md](docs/CONFIGURATION.md#the-strict-profile).
99
+
91
100
  A report is still welcome if you find an escape that does not depend on interop,
92
- or if the plugin ever reports `full` while interop is reachable.
101
+ if the masked profile is bypassable from `/mnt`, or if the plugin ever reports
102
+ `full` while interop is reachable.
93
103
 
94
104
  ## Credentials
95
105
 
package/SUPPORT.md CHANGED
@@ -7,6 +7,12 @@
7
7
  | Operating system | Windows 10 and Windows 11 |
8
8
  | Linux side | WSL2 with at least one distro, and systemd as PID 1 with a lingering user manager (`loginctl enable-linger`) for the shipped session runtime. Set `sessionSandbox: false` on the preset's `wsl-shell` to keep the per-command sandbox instead |
9
9
  | DSH packages | the `0.2.1-rc.2`-era packages listed in `peerDependencies` |
10
+
11
+ The DSH row is checked weekly: `.github/workflows/peer-canary.yml` resolves the
12
+ `next` dist-tag of every pinned peer, installs that line into a scratch copy and
13
+ runs `pnpm test` plus the composition boot test. A successful run prints the
14
+ exact version delta in its summary; update the row above from that summary. A
15
+ red run means the plugin has not been validated against the new line yet.
10
16
  | Plugin version | the latest published version only |
11
17
  | Node | 22.19+ (`^22.19.0`), or 24 and later (`>=24.0.0`) |
12
18
 
@@ -22,9 +28,14 @@ supported configuration.
22
28
 
23
29
  Open an issue in the repository:
24
30
 
25
- https://github.com/Big-Dao/dsh-plugin-wsl-env/issues
31
+ https://github.com/Big-Dao/dsh-plugin-wsl-env/issues/new/choose
32
+
33
+ The chooser offers a **feature request** and a **setup report**; the setup
34
+ report requires the `doctor --json` output, which is what makes a first-run
35
+ problem diagnosable without a round trip. For a security issue, use the private
36
+ advisory link instead (see [SECURITY.md](SECURITY.md)).
26
37
 
27
- Use it for bug reports and for questions about configuration. Search the open and
38
+ Use issues for bug reports and for questions about configuration. Search the open and
28
39
  the closed issues first, and check whether the answer is already written down:
29
40
  [docs/LIMITATIONS.md](docs/LIMITATIONS.md) lists the known design boundaries, and
30
41
  the Troubleshooting table in [README.md](README.md#troubleshooting) covers the
@@ -56,6 +67,22 @@ loudly instead of quietly.
56
67
 
57
68
  There is no long-term support branch. Fixes go to the latest version.
58
69
 
70
+ ## Triage and release cadence
71
+
72
+ - **Roadmap.** The public plan is [docs/ROADMAP-1.0.md](docs/ROADMAP-1.0.md);
73
+ the open issues #5–#22 map to its WSL-1xx/2xx/3xx blocks.
74
+ - **Triage target.** A `priority:P1` report gets its first response within three
75
+ business days, best effort. Security reports follow the response targets in
76
+ [SECURITY.md](SECURITY.md).
77
+ - **Cadence.** Releases ship when there is a change worth shipping, roughly every
78
+ two to four weeks; the tag workflow and its gates are in
79
+ [docs/RELEASING.md](docs/RELEASING.md), and the configuration-stability and
80
+ migration rules are in [docs/MIGRATION.md](docs/MIGRATION.md). There is no
81
+ long-term support branch.
82
+ - **Good first issues.** The `good first issue` label marks changes that need no
83
+ prior context; [CONTRIBUTING.md](CONTRIBUTING.md#good-first-issues-and-the-contributor-path)
84
+ lists the usual shapes and how to claim one.
85
+
59
86
  ## What a good bug report contains
60
87
 
61
88
  The one-command path: `pnpm run diagnose` (from a checkout of the plugin, or
package/cordis.patch.yml CHANGED
@@ -66,6 +66,8 @@
66
66
  - id: subprocess-wsl
67
67
  name: 'dsh-plugin-wsl-env/subprocess'
68
68
  config:
69
+ # auto: follow the launch directory's UNC distro; "default" forces WSL's
70
+ # default, and a name pins. Empty is auto.
69
71
  distro: ''
70
72
 
71
73
  # ═════════════════════════════════════════════════════════════════════════════
@@ -104,25 +106,24 @@
104
106
  name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
105
107
 
106
108
  # ═════════════════════════════════════════════════════════════════════════════
107
- # The distro-side takeovers of the two root-plane services.
109
+ # The distro-side takeover of the root-plane filesystem.
108
110
  #
109
111
  # A patch layer is a flat list: an `id:` entry patches the shipped row, and a
110
112
  # bare `insert:` list appends new rows as data. A disable written INSIDE an
111
113
  # insert list is not a patch — it becomes a duplicate-id data row that shadows
112
114
  # the real service row (the loader keys rows by id, last occurrence wins), and
113
- # a nested `insert:` list is never interpreted at all. Both takeovers are
115
+ # a nested `insert:` list is never interpreted at all. This takeover is
114
116
  # therefore two top-level entries: disable the shipped row by id, then append
115
117
  # the replacement with a bare insert. test/composition.test.mjs runs this file
116
118
  # through the real patch algorithm to keep the shape honest.
117
119
  # ═════════════════════════════════════════════════════════════════════════════
118
120
 
119
- # The GUI file tree and previews for a distro workspace are served from inside
120
- # the distro (resident agent) instead of Windows-side 9P walks. The variant
121
- # extends the shipped service — a distro UNC routes distro-side, a drive path
122
- # calls super (host native), the watch stream delegates unchanged — so
123
- # replacing the row is safe for Windows-folder sessions too.
124
- - id: workspace-files
125
- disabled: true
121
+ # The GUI file service keeps the shipped `workspace-files` row: its client half
122
+ # is the browser `file` resource provider the Sidebar previews need, and a
123
+ # server-only replacement drops it (docs/LIMITATIONS.md). The shipped service
124
+ # reads WSL paths through the routed root filesystem below, so the GUI file tree
125
+ # and previews still run inside the distro, at the cost of a few extra agent
126
+ # round trips.
126
127
 
127
128
  # The root-plane filesystem routes by coordinate: distro UNC identities are
128
129
  # served by the resident agent (real watch events, ext4 reads), drive paths
@@ -133,17 +134,11 @@
133
134
  disabled: true
134
135
 
135
136
  - insert:
136
- - id: workspace-files-wsl
137
- name: 'dsh-plugin-wsl-env/workspace-files-wsl'
138
- config:
139
- maxBytes: 2097152
140
- maxFileBytes: 33554432
141
- maxLines: 5000
142
- maxEntries: 2000
143
-
144
137
  - id: fs-routing
145
138
  name: 'dsh-plugin-wsl-env/fs-routing'
146
139
  config:
140
+ # auto: route each UNC target to its own distro; "default" forces WSL's
141
+ # default, and a name pins. Empty is auto.
147
142
  distro: ''
148
143
  wslPath: wsl.exe
149
144
  sandbox: true
@@ -232,6 +227,8 @@
232
227
  - id: wsl-shell
233
228
  name: 'dsh-plugin-wsl-env'
234
229
  config:
230
+ # auto: follow the request workdir's UNC distro; "default"
231
+ # forces WSL's default, and a name pins. Empty is auto.
235
232
  distro: ''
236
233
  loginShell: true
237
234
  timeoutMs: 120000
@@ -246,14 +243,15 @@
246
243
  - id: wsl-fs
247
244
  name: 'dsh-plugin-wsl-env/fs'
248
245
  config:
246
+ # auto: follow each target path's UNC distro; "default"
247
+ # forces WSL's default, and a name pins. Empty is auto.
249
248
  distro: ''
250
249
  restrictToDistro: true
251
250
  # agent (the default): reads/writes/identities run inside the
252
251
  # distro over the resident agent (lib/fs-substrate.js) —
253
252
  # native symlinks and mode bits, the write guard survives to
254
253
  # publication, and confined mutations are kernel-enforced.
255
- # share: the opt-out — the Windows-side host fs stack over
256
- # the 9p share.
254
+ # "share" is retired; a profile carrying it is refused at boot.
257
255
  substrate: agent
258
256
  # Confined writes ride the session runtime (ADR 0001), so they
259
257
  # share the command's real /tmp and workspace mounts.