dsh-plugin-wsl-env 0.1.3 → 0.1.4

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.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,26 @@ reference below points at that record's numbering.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.1.4] - 2026-10-02
16
+
17
+ Documentation only: a shorter README, and the reference documents it links to.
18
+
19
+ ### Changed
20
+
21
+ - The README is a quick start now, and nothing else: install, use, configure, the
22
+ sandbox in brief, troubleshooting, development, and an index of the documents. It
23
+ went from 232 lines to 124. Everything that was not part of getting the plugin
24
+ running moved to the document it belongs to — the full configuration table to
25
+ [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md), the limitations to
26
+ [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md), the file layout to
27
+ [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) (the two design rationales were
28
+ already there), and the verification list to
29
+ [`CONTRIBUTING.md`](CONTRIBUTING.md). The Chinese README was rewritten to match,
30
+ section for section, and still does.
31
+ - `docs/CONFIGURATION.md`, `docs/LIMITATIONS.md` and `docs/ARCHITECTURE.md` ship in
32
+ the tarball, so a package consumer has the configuration reference and the honest
33
+ list of limitations without the repository.
34
+
15
35
  ## [0.1.3] - 2026-10-02
16
36
 
17
37
  Repository and release engineering. No runtime behaviour changed.
package/CONTRIBUTING.md CHANGED
@@ -104,6 +104,46 @@ of `test/probe/run.sh`. `terminal.sh` and `sandbox-shell.sh` reuse it.
104
104
  Keep probe output idempotent. Several probes delete their scratch files at the
105
105
  start, because a leftover file from an earlier run changes the result.
106
106
 
107
+ ## What has been verified
108
+
109
+ `npm test` covers the pure modules and runs a `--check` pass over every shipped
110
+ module. It cannot import the service modules, because those need DSH peer packages
111
+ that a bare checkout does not have. Only booting the harness closes that gap, which
112
+ is why the probes exist; the archived record's §15.4 documents the five rounds of
113
+ misdiagnosis the gap once caused.
114
+
115
+ `test/probe/sandbox.sh` needs no harness: it uses the same arguments
116
+ `lib/sandbox.js` builds and asserts what bubblewrap does and does not confine,
117
+ recording the interop escape as `INFO` because a Linux sandbox cannot govern a
118
+ Windows process. The other probes boot a throwaway profile bound to the distro:
119
+
120
+ - the filesystem probe asserts the write path and the write checks — a write outside
121
+ the policy root and a write in `read-only` mode both return `FS_SANDBOX_DENIED`,
122
+ while `danger-full-access` is not checked;
123
+ - the shell probe drives all three modes through `ctx.shell` and checks the refusal
124
+ classification the tool layer returns;
125
+ - the terminal probe asserts the distro, the starting directory, and the `DSH_*`
126
+ variables forwarded through `WSLENV`.
127
+
128
+ The setup steps for the throwaway profile are in the header of `test/probe/run.sh`;
129
+ `terminal.sh` and `sandbox-shell.sh` reuse it.
130
+
131
+ On Windows 11 + WSL2 (Ubuntu 26.04) the following has been verified:
132
+
133
+ - the wiring of each service, the UNC path handling and the picker behaviour;
134
+ - the plugin mounted end to end in a real profile;
135
+ - one real model turn in a WSL-only headless profile: `write → chmod → read → edit →
136
+ execute`, with the executable bit surviving the edit;
137
+ - the terminal provider in a throwaway Web boot and in the daily GUI profile;
138
+ - the sandbox in four ways: the arguments measured inside the distro, the filesystem
139
+ write checks, the shell path (`enforcement: partial`, with refusals classified
140
+ correctly), and the daily GUI profile, where a write from the agent's own session
141
+ outside the session workspace is refused inside the distro and the following
142
+ wider-permission request succeeds.
143
+
144
+ Counts: 42 unit assertions, 18 filesystem-probe assertions, 10 shell-probe checks, 10
145
+ sandbox expectations plus the recorded escape, and 3 terminal assertions.
146
+
107
147
  ## Commit and pull request conventions
108
148
 
109
149
  - Write an imperative subject line: `Fix the npm readme`, not `Fixed` or `Fixes`.
package/README.md CHANGED
@@ -6,28 +6,12 @@
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) use a WSL distro as its working environment. Commands run inside the distro. The model's file tools read and write real distro files. The folder picker can open a distro folder directly, and the GUI terminal also opens inside the distro instead of `cmd.exe` on a UNC path.
9
+ This plugin lets [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) use a WSL distro as its working environment: commands run inside the distro, the model's file tools read and write real distro files, the folder picker can open a distro folder, and the GUI terminal opens inside the distro.
10
10
 
11
- **Windows + WSL2 only.** Installation is one command, and the plugin has no dependencies of its own. The WSL environment applies per session, so a session opened on a Windows folder keeps its normal Windows environment.
11
+ **Windows + WSL2 only.** One install command, no dependencies of its own. The environment applies per session, so a session on a Windows folder keeps its normal Windows environment.
12
12
 
13
- [What you get](#what-you-get) · [Install](#install) · [Using it](#using-it) ·
14
- [Configure](#configure) · [Sandbox](#sandbox) · [Troubleshooting](#troubleshooting) ·
15
- [Limitations](#limitations) · [Development](#development) · [Design notes](#design-notes) ·
16
- [Contributing and support](#contributing-and-support)
17
-
18
- ## What you get
19
-
20
- | Before | After |
21
- |---|---|
22
- | Commands run on Windows | Commands run **inside your distro**, using your login shell, starting in the session's Linux directory |
23
- | `read`/`write`/`edit`/`glob`/`grep` work on Windows paths | These tools work on **the distro's real files**, through the `\\wsl.localhost\<distro>` share |
24
- | The folder picker cannot see WSL | One picker lists the Windows home directory and every installed distro, so you can open `/home/you/project` |
25
- | The terminal opens `cmd.exe` in a UNC directory | The terminal opens **inside the distro**, in the session's directory, as your distro user |
26
- | Commands have no sandbox | Commands are confined by **bubblewrap inside the distro**; see [Sandbox](#sandbox) |
27
-
28
- If you open a session on a folder inside a distro, the WSL environment applies automatically. The `wsl` preset is bound while the session is created, so the first tool call already runs in the distro. You do not switch anything by hand.
29
-
30
- **How to check that it works.** Ask the model to run `uname -r`. The output should be a WSL2 kernel such as `6.18.40.1-microsoft-standard-WSL2`. You can also run `echo $WSL_DISTRO_NAME`.
13
+ [Install](#install) · [Using it](#using-it) · [Configure](#configure) · [Sandbox](#sandbox) ·
14
+ [Troubleshooting](#troubleshooting) · [Development](#development) · [Documentation](#documentation)
31
15
 
32
16
  ## Install
33
17
 
@@ -40,84 +24,58 @@ dsh --profile wsl --dump-config # 3. compose only, no boot (t
40
24
  dsh --profile wsl # 4. run
41
25
  ```
42
26
 
43
- Step 3 should print a layer named `# == dsh-plugin-wsl-env`, plus the rows that layer changes. The most important row is `- id: terminal-controller`, which should now carry `shell: { path: wsl.exe, name: WSL }`.
44
-
45
- Then open a folder under `\\wsl.localhost\<distro>\...` in the GUI. The picker lists every installed distro at its root level. You can also just press **New terminal**.
27
+ Step 3 should print a layer named `# == dsh-plugin-wsl-env`, and `- id: terminal-controller` should now carry `shell: { path: wsl.exe, name: WSL }`. The package is a DSH bundle, so step 2 applies [`cordis.patch.yml`](cordis.patch.yml) as a configuration layer; nothing is merged by hand.
46
28
 
47
- The package is a DSH **bundle**. It declares `dsh.bundle.patch`, so step 2 applies [`cordis.patch.yml`](cordis.patch.yml) as a configuration layer. You do not merge any patch by hand. Step 1 uses the Web template because this layer overrides rows that only the Web surface has: the app-wide `subprocess` provider, the terminal controller and the directory picker.
29
+ Then open a folder under `\\wsl.localhost\<distro>\...` in the GUI. The picker lists every installed distro at its root level, and **New terminal** opens a shell in the distro.
48
30
 
49
- You also need **bubblewrap inside the distro** (`sudo apt install bubblewrap`). Without it every command fails immediately. See [Sandbox](#sandbox).
31
+ You also need **bubblewrap inside the distro**: `sudo apt install bubblewrap`. Without it every command fails closed, see [Sandbox](#sandbox).
50
32
 
51
- Uninstall: `dsh plugin --profile wsl remove dsh-plugin-wsl-env`.
33
+ Uninstall: `dsh plugin --profile wsl remove dsh-plugin-wsl-env`. Upgrade: run the same `add` command again.
52
34
 
53
- Upgrade: run `dsh plugin --profile wsl add dsh-plugin-wsl-env` again. The new version replaces the old configuration layer with its own.
54
-
55
- > **If you change `lib/` in a checkout, restart the app.** A running process caches ES modules and keeps using the old code until you do.
35
+ > **Changed anything under `lib/`? Restart the app.** A running process caches ES modules and keeps the old code otherwise.
56
36
 
57
37
  ## Using it
58
38
 
59
- A typical session: open `\\wsl.localhost\ubuntu\home\you\project` as the workspace, then ask "what kernel am I on, and what is in `/etc/os-release`?". The model runs `uname -r` and reads that file inside the distro. Nothing goes through `/mnt/c`, and no files are copied.
39
+ Open `\\wsl.localhost\ubuntu\home\you\project` as the workspace, then 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 goes through `/mnt/c`, and no files are copied.
60
40
 
61
- - **Open a distro folder.** The picker lists the Windows home directory and one entry per distro. If you pick `\\wsl.localhost\ubuntu\home\you\project`, the session workspace, the shell's working directory and the terminal all follow it.
62
- - **Commands** run as `wsl.exe -d <distro> --cd <linux dir> --exec <your login shell> -lc <command>`. Your `PATH`, `nvm`, `cargo`, `pyenv` and rc files therefore apply. The shell is not hardcoded to bash.
41
+ - **A distro folder gets the distro environment automatically.** The `wsl` preset is bound while the session is created, so the first tool call is already correct.
42
+ - **Commands** run as `wsl.exe -d <distro> --cd <linux dir> --exec <your login shell> -lc <command>`, so your `PATH`, `nvm`, `cargo`, `pyenv` and rc files apply. The shell is not hardcoded to bash.
63
43
  - **Files are the distro's real files.** `/home/you/x` and `\\wsl.localhost\ubuntu\home\you\x` are the same file, and `/mnt/c/...` reaches the Windows disk.
64
44
  - **The terminal** (right sidebar, then *New terminal*) opens a shell inside the distro, in the session's folder.
65
- - **The model can see its shell environment.** The plugin registers `DSH_WSL_DISTRO`, `DSH_WSL_SHELL` and `DSH_WSL_HOME` in the managed `DSH_*` namespace, and the shell tool points the model at them.
66
- - **Permissions work as they do on a Linux host.** The Permissions selector switches between `read-only`, `workspace-write` (the default) and `danger-full-access`. When a command or a write is refused, the tool offers to run it with wider permissions. If you approve, that single call runs without the sandbox.
45
+ - **The model sees its shell environment.** The plugin registers `DSH_WSL_DISTRO`, `DSH_WSL_SHELL` and `DSH_WSL_HOME` in the managed `DSH_*` namespace.
46
+ - **Permissions work as on a Linux host.** The Permissions selector switches between `read-only`, `workspace-write` (the default) and `danger-full-access`. A refused command or write comes back with an offer to run it with wider permissions; if you approve, that one call runs without the sandbox.
67
47
 
68
48
  ## Configure
69
49
 
70
- To change a value, override that row by id in your own profile layer. The file is `$DSH_HOME/profiles/<name>/cordis.patch.yml`; see [`examples/profile.cordis.patch.yml`](examples/profile.cordis.patch.yml). In the table below, *(shipped)* marks the values set by [`cordis.patch.yml`](cordis.patch.yml). The others are schema defaults, listed because they are worth knowing.
50
+ Override a row by id in `$DSH_HOME/profiles/<name>/cordis.patch.yml`. The keys worth knowing:
71
51
 
72
52
  | Row | Key | Default | Meaning |
73
53
  |---|---|---|---|
74
- | `wsl-shell` | `distro` | `''` *(shipped)* | distro name; empty means WSL's default distro |
75
- | | `shell` | `''` | pin a shell inside the distro; empty means the user's login shell |
76
- | | `loginShell` | `true` *(shipped)* | use `<shell> -lc`, which sources your profile, instead of a bare `-c` |
54
+ | `wsl-shell` | `distro` | `''` | distro name; empty means WSL's default distro |
77
55
  | | `sandbox` | `true` | confine commands with `bubblewrap`; `false` disables the sandbox |
78
- | | `cwd` | `''` | default working directory; empty means the distro user's home |
79
- | | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000` *(shipped)* | per-call time limit, and the ceiling a call may ask for |
80
- | `wsl-fs` | `distro` | `''` *(shipped)* | as above |
81
- | | `restrictToDistro` | `true` *(shipped)* | refuse paths outside the pinned distro, including `/mnt/c` |
56
+ | `wsl-fs` | `distro` | `''` | as above |
57
+ | | `restrictToDistro` | `true` | refuse paths outside the pinned distro, including `/mnt/c` |
82
58
  | | `sandbox` | `true` | check `writeText` and `editText` against the policy |
83
- | | `resolveSymlinks` | `true` | follow Linux symlinks that the share cannot traverse, such as `/etc/os-release` and `/bin` |
84
- | | `cwd` | `''` | base directory for relative paths; empty means the distro user's home |
85
- | `directory-picker-wsl` | `preferredDistro` | `''` *(shipped)* | the distro listed first in the picker |
86
- | | `includeHostHome` | `true` *(shipped)* | also list the Windows home directory |
87
- | | `maxEntries` | `1000` *(shipped)* | maximum entries listed per directory |
88
- | `subprocess-wsl` | `distro` | `''` *(shipped)* | which distro the GUI terminal opens in |
89
- | | `shell` | `''` | pin a shell; empty lets `wsl.exe` decide |
90
- | | `loginShell` | `true` | whether a pinned shell uses login semantics |
59
+ | `directory-picker-wsl` | `includeHostHome` | `true` | also list the Windows home directory |
60
+ | `subprocess-wsl` | `distro` | `''` | which distro the GUI terminal opens in |
91
61
 
92
- You can override the other keys in the same way; they keep their defaults. Those are `wslPath`, `hostCwd` and `forwardEnv` on the shell and terminal rows, `diffBasisMaxBytes` on `wsl-fs`, and `distroCacheMs` on the picker.
62
+ [`cordis.patch.yml`](cordis.patch.yml) is the commented reference for every shipped value. [docs/CONFIGURATION.md](docs/CONFIGURATION.md) lists the rest, including `shell`, `loginShell`, `cwd`, `timeoutMs`, `resolveSymlinks`, `preferredDistro` and `maxEntries`; [examples/profile.cordis.patch.yml](examples/profile.cordis.patch.yml) is a machine-local layer to copy from.
93
63
 
94
64
  ## Sandbox
95
65
 
96
- **In short:** commands are confined by `bubblewrap` inside the distro, and writes are checked against the same policy. The reported enforcement level is `partial`, not `full`, because a process inside the distro can still reach Windows through WSL interop. `bubblewrap` must be installed; without it the plugin refuses to run rather than run unconfined.
97
-
98
- On Windows, DSH confines commands with `dsh-sandbox-windows-acl`. It uses a restricted, low-integrity Windows token and an allowlist of writable paths. **That token cannot reach WSL.** `wsl.exe` fails with `Wsl/E_ACCESSDENIED`, and `\\wsl.localhost\<distro>` reports access denied, while both work normally outside the sandbox. The WSL environment therefore cannot reuse the Windows sandbox and uses a Linux sandbox instead. The plugin builds the `bubblewrap` arguments on the Windows side and passes them to `wsl.exe --exec`, which runs them inside the distro:
99
-
100
- ```text
101
- wsl.exe -d <distro> --cd <linux dir> --exec bwrap \
102
- --ro-bind / / --dev /dev --unshare-pid --proc /proc --die-with-parent \
103
- [--tmpfs /tmp --bind <workspace> <workspace>] -- <shell> -lc <cmd>
104
- ```
105
-
106
- These are the same arguments DSH uses on a Linux host (`dsh-sandbox-local`), so the behaviour and the error messages match.
66
+ Commands are confined by `bubblewrap` inside the distro, and file writes are checked against the same policy. The Windows ACL sandbox cannot be used here: its restricted token cannot reach WSL at all.
107
67
 
108
68
  | Mode | What a command inside the distro can do |
109
69
  |---|---|
110
- | `read-only` | read the whole distro; only `/dev/null` is writable, which a shell needs |
70
+ | `read-only` | read the whole distro; only `/dev/null` is writable |
111
71
  | `workspace-write` | the above, plus the session workspace is writable and `/tmp` is a temporary mount |
112
- | `danger-full-access` | no sandbox at all, used for an approved wider-permission request |
113
-
114
- `WslFileSystem` checks writes against the same policy and the same writable list. That avoids a mismatch where `bash` could write `/tmp` but the write tool could not. Both providers report their current mode through `sandboxMode`, which is what brings back the Permissions selector and the "refused, then request wider permissions" flow.
72
+ | `danger-full-access` | no sandbox; used for an approved wider-permission request |
115
73
 
116
- **Why the level is `partial`, not `full`.** A process inside the distro can run a Windows program through interop, for example `/mnt/c/.../*.exe`. That is not a Linux process, so bubblewrap does not govern it. It runs with your normal Windows token and can write anywhere you can. `npm run probe:sandbox` demonstrates this, and every run of that probe checks it again. Closing the hole would mean denying execution of programs under `/mnt`. That would also break sessions whose workspace is `/mnt/c/...`, so the hole stays open and is documented here. Network and process visibility are outside the scope of this mode on every platform.
74
+ **`bubblewrap` is required, and the failure is closed.** Without it every confined command reports `SANDBOX_UNAVAILABLE` instead of running unconfined. Set `sandbox: false` on either provider to opt out; the tool layer then tells the model these operations have no sandbox.
117
75
 
118
- **`bubblewrap` is required, and the failure is closed.** Without it, every confined command reports `SANDBOX_UNAVAILABLE`. The plugin does not fall back to running without a sandbox, and the `sandboxMode` value disappears too, so it never claims to confine when it cannot. If you do not want a sandbox, set `sandbox: false` on either provider. Commands then run unconfined, `sandboxMode` returns `undefined`, and the tool layer tells the model that these operations have no sandbox.
76
+ **The reported enforcement is `partial`, not `full`.** A process inside the distro can still run a Windows program through WSL interop, for example `/mnt/c/.../*.exe`, and bubblewrap does not govern it. `npm run probe:sandbox` demonstrates the boundary on your machine.
119
77
 
120
- **The GUI terminal is not sandboxed.** It is an interactive shell for a person, which matches the shipped terminal provider.
78
+ 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.
121
79
 
122
80
  ## Troubleshooting
123
81
 
@@ -133,18 +91,6 @@ These are the same arguments DSH uses on a Linux host (`dsh-sandbox-local`), so
133
91
  | `glob` and `grep` are slow | the Windows-side ripgrep walks the 9p share | expected; narrow the path, or use `bash` to call tools inside the distro |
134
92
  | the terminal reports `unknown` activity | shell integration only covers `bash`/`zsh` started directly on a POSIX host | close the tab to release the process; idle reclamation does not run for these terminals |
135
93
 
136
- ## Limitations
137
-
138
- - **The sandbox does not govern WSL interop.** A confined command can still run a Windows program, which escapes the Linux-side boundary. The plugin reports this as `enforcement: partial`. See [Sandbox](#sandbox).
139
- - **`bubblewrap` must be installed**, or both providers fail immediately.
140
- - `workspace-write` binds the workspace root as writable, and bubblewrap refuses a bind whose source directory does not exist. If a session's workspace directory is deleted, commands fail with a runner error; the plugin does not recreate the directory.
141
- - New files get the distro's umask default (0644). A `chmod` from the Windows side on a share path is silently ignored; overwrites and edits keep the original permission bits. Run `chmod +x` inside the distro when you need an executable.
142
- - The check before a write is "check, then write", which is not atomic. This backend checks the caller's guard itself, because the Windows backend publishes a guarded new file with a hard link, and the share does not support hard links. This is the race that `dsh-fs-sandbox` already documents.
143
- - `watch()` (file change monitoring) is not provided. Watching a 9p share is unreliable, so the plugin refuses instead of arming it.
144
- - `glob` and `grep` use the Windows-side ripgrep over the share: correct results, but not fast, and `.gitignore` is interpreted with Windows rules. `editText` reads the whole file into memory before writing it back.
145
- - The terminal belongs to the whole app, not to one session. A session opened on a Windows folder still gets the distro terminal, starting in `/mnt/<drive>/...`, and its shell menu is deliberately reduced to the one configured profile.
146
- - Terminal activity reporting stops at `wsl.exe`, so the controller never reclaims these terminals when they are idle.
147
-
148
94
  ## Development
149
95
 
150
96
  ```bash
@@ -156,76 +102,22 @@ npm run probe:sandbox-shell # boot a real harness and drive the confined execut
156
102
  npm run probe:terminal # open a PTY through the terminal provider
157
103
  ```
158
104
 
159
- The package has no dependencies, so there is nothing to install first.
160
-
161
- Layout:
162
-
163
- ```text
164
- lib/paths.js pure conversion between the three path formats
165
- lib/wsl.js wsl.exe interop primitives (no DSH imports)
166
- lib/listing.js pure directory listing and breadcrumb helpers (no DSH imports)
167
- lib/index.js WslShellExecutor (ctx.shell) and WslFileSystem (ctx.fs)
168
- lib/sandbox.js the distro-side bwrap confinement shared by both providers
169
- lib/picker.js WslDirectoryPicker (ctx.directoryPicker)
170
- lib/subprocess.js WslSubprocessRuntime (ctx.subprocess), the terminal window
171
- lib/auto-preset.js per-session environment selection
172
- lib/shell-env.js registers the DSH_WSL_* environment variables
173
- lib/{shell,fs}.js one-line subpath entry points
174
- cordis.patch.yml the bundle configuration layer (dsh.bundle), commented
175
- examples/ one machine-local profile layer, for comparison
176
- test/ unit tests and behaviour probes
177
- docs/archive/ the designs this one replaced, and why
178
- ```
179
-
180
- `npm test` covers the pure modules and runs a `--check` syntax pass over every shipped module. It cannot import the service modules, because those need DSH peer packages that a bare checkout does not have. Only booting the harness closes that gap; the archived record's §15.4 documents the five rounds of misdiagnosis it once caused.
181
-
182
- `test/probe/sandbox.sh` needs no harness. It uses the same arguments that `lib/sandbox.js` builds, and asserts what bubblewrap does and does not confine. It records the interop escape as `INFO`, because a Linux sandbox cannot govern a Windows process.
183
-
184
- The other probes boot a throwaway profile bound to the distro:
185
-
186
- - The filesystem probe asserts the write path and the write checks. A write outside the policy root, and a write in `read-only` mode, both return `FS_SANDBOX_DENIED`; `danger-full-access` is not checked.
187
- - The shell probe drives all three modes through `ctx.shell` and checks the refusal classification that the tool layer returns.
188
- - The terminal probe asserts the distro, the starting directory, and the `DSH_*` variables forwarded through `WSLENV`.
189
-
190
- The setup steps for the throwaway profile are in the header of `test/probe/run.sh`; `terminal.sh` and `sandbox-shell.sh` reuse it.
191
-
192
- **What has been verified** on Windows 11 + WSL2 (Ubuntu 26.04):
193
-
194
- - the wiring of each service, the UNC path handling and the picker behaviour;
195
- - the plugin mounted end to end in a real profile;
196
- - one real model turn in a WSL-only headless profile: `write → chmod → read → edit → execute`, with the executable bit surviving the edit;
197
- - the terminal provider in a throwaway Web boot and in the daily GUI profile;
198
- - the sandbox in four ways: the arguments measured inside the distro, the filesystem write checks, the shell path (`enforcement: partial`, with refusals classified correctly), and the daily GUI profile, where a write from the agent's own session outside the session workspace is refused inside the distro and the following wider-permission request succeeds.
199
-
200
- Counts: 42 unit assertions, 18 filesystem-probe assertions, 10 shell-probe checks, 10 sandbox expectations plus the recorded escape, and 3 terminal assertions.
201
-
202
- **The runtime mirror.** The checkout is developed inside the distro, but the harness is a Windows process, and a profile can only link a Windows path: pnpm rewrites `link:\\wsl.localhost\...` into a broken `/wsl.localhost/...` symlink. The Windows copy at `default-workspace/dsh-plugin-wsl` is therefore a runtime mirror. Sync it with `test/probe/sync-to-windows.sh` before launching the app. That destination is outside every session workspace, so an agent running the sync in a confined shell is refused by `workspace-write` and must approve `danger-full-access` for that one command. That is the sandbox behaving as designed, not a broken script; run the sync from a normal distro terminal if you prefer not to see the prompt.
203
-
204
- **CI** runs `npm test` on Node 20, 22 and 24, on Linux and on Windows, and runs
205
- `npm run test:coverage` on Node 24 to enforce the coverage thresholds.
206
-
207
- ## Design notes
208
-
209
- Two decisions are not visible in the profile YAML. The full reasoning, the measurements and every rejected design are in the archived engineering record: [docs/archive/engineering-record.zh.md](docs/archive/engineering-record.zh.md). It is a historical document, not a second README; if it disagrees with this file, this file is correct.
210
-
211
- **Why the terminal provider is app-wide.** `ctx.shell` and `ctx.fs` are provided per session, inside the isolated scope of the `wsl` agent preset. A workspace on Windows therefore keeps the shipped sandboxed PowerShell environment, while a WSL workspace gets the distro. Both can exist in one process at the same time.
212
-
213
- The terminal cannot be provided that way. `dsh-api-terminal-controller` looks up its environment with `agent.ctx.get("subprocess")`. An Agent's context is created by the agent loop under the **root** scope, while a preset's isolated scope is created by `dsh-agent-preset-registry` under the registry's own context. These two scope chains never meet, so a `subprocess` provider mounted inside `preset-wsl` is invisible to the terminal window.
214
-
215
- For that reason, [`cordis.patch.yml`](cordis.patch.yml) replaces the **app-wide** `subprocess` row with a subclass that overrides only `spawnTerminal`. Ordinary `spawn()`, the Windows-side ripgrep search, the pwsh executor and the LSP host still use the shipped implementation. The cost is that the terminal follows the app configuration rather than the session; see [Limitations](#limitations).
105
+ There is nothing to install first: the package has no dependencies, and every module under test imports only Node builtins. CI runs `npm test` on Node 20, 22 and 24, on Linux and Windows, and `npm run test:coverage` on Node 24.
216
106
 
217
- **Why the sandbox is on the Linux side.** The Windows ACL runner's restricted token cannot reach WSL at all, so the confinement has to be built on the Windows side and run inside the distro. `lib/sandbox.js` copies the Linux `bwrap` approach from `dsh-sandbox-local` instead of using `ctx.sandbox`. `dsh-tool-bash` never asks for `ctx.sandbox` anyway: it reads the executor's `sandboxMode` and `ctx.sandboxPolicy`, and this plugin supplies both.
107
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) has the file layout, the mounting, and the sandbox design. [CONTRIBUTING.md](CONTRIBUTING.md) has the development loop, the full gate list, and what has been verified.
218
108
 
219
- ## Contributing and support
109
+ ## Documentation
220
110
 
221
111
  | Document | What it covers |
222
112
  |---|---|
223
- | [CONTRIBUTING.md](CONTRIBUTING.md) | the development loop, the gates a pull request must pass, and the conventions used here |
224
- | [SECURITY.md](SECURITY.md) | reporting a vulnerability privately, response targets, and what counts as in scope |
225
- | [SUPPORT.md](SUPPORT.md) | what is supported, where to ask, and what a useful bug report contains |
226
- | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | how the providers are mounted, the sandbox design, and the test layers |
227
- | [docs/RELEASING.md](docs/RELEASING.md) | the release checklist, including the trusted-publisher setup |
228
- | [docs/archive/README.en.md](docs/archive/README.en.md) | an English index of the archived engineering record |
113
+ | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | every configuration key, and how to override it |
114
+ | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | providers, mounting, path coordinates, sandbox, test layers, file layout |
115
+ | [docs/LIMITATIONS.md](docs/LIMITATIONS.md) | what the plugin does not do, and why |
116
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | the development loop, gates, conventions, verification |
117
+ | [SECURITY.md](SECURITY.md) | reporting a vulnerability privately |
118
+ | [SUPPORT.md](SUPPORT.md) | supported versions, and where to ask |
119
+ | [docs/RELEASING.md](docs/RELEASING.md) | the release checklist |
120
+ | [docs/archive/README.en.md](docs/archive/README.en.md) | English index of the archived engineering record |
229
121
 
230
122
  ## License
231
123
 
@@ -0,0 +1,217 @@
1
+ # Architecture
2
+
3
+ This file describes the plugin as it is today. It covers what the code does and
4
+ where each piece lives. The reasoning behind the design, including the designs that
5
+ were rejected, is recorded in [`CHANGELOG.md`](../CHANGELOG.md) and in
6
+ [`archive/engineering-record.zh.md`](archive/engineering-record.zh.md).
7
+
8
+ ## What "provider" means here
9
+
10
+ DSH exposes its capabilities as services. The model-facing tools (`bash`, `read`,
11
+ `write`, `edit`, `glob`, `grep`) consume those services. They never touch a
12
+ filesystem or a shell directly.
13
+
14
+ This plugin supplies WSL-backed implementations of three of them, plus three
15
+ smaller integrations.
16
+
17
+ ## The three service providers
18
+
19
+ | Service | Class | File | What it does |
20
+ |---|---|---|---|
21
+ | `ctx.shell` | `WslShellExecutor` | [`lib/index.js`](../lib/index.js) | Runs each command as `wsl.exe -d <distro> --cd <linux dir> --exec <login shell> -lc <cmd>`, wrapped in a distro-side `bwrap` sandbox. |
22
+ | `ctx.fs` | `WslFileSystem` | [`lib/index.js`](../lib/index.js) | Maps Linux paths onto the distro's UNC share, so the host filesystem stack and the packaged ripgrep work on real distro files. |
23
+ | `ctx.subprocess` | `WslSubprocessRuntime` | [`lib/subprocess.js`](../lib/subprocess.js) | Opens the GUI terminal inside the distro, in the session workspace. |
24
+
25
+ `WslShellExecutor` extends the shipped `LocalBashExecutor`, and `WslFileSystem`
26
+ extends the shipped `LocalFileSystem`. Only the parts that need a distro are
27
+ overridden.
28
+
29
+ ## The three smaller integrations
30
+
31
+ | Subpath | Class or name | File | What it does |
32
+ |---|---|---|---|
33
+ | `dsh-plugin-wsl-env/picker` | `WslDirectoryPicker` | [`lib/picker.js`](../lib/picker.js) | Adds the installed distros to the folder picker, next to the Windows home directory. |
34
+ | `dsh-plugin-wsl-env/auto-preset` | `auto-preset` | [`lib/auto-preset.js`](../lib/auto-preset.js) | Binds the `wsl` agent preset when a new session's workspace is inside a distro. |
35
+ | `dsh-plugin-wsl-env/shell-env` | `wsl-shell-env` | [`lib/shell-env.js`](../lib/shell-env.js) | Contributes `DSH_WSL_DISTRO`, `DSH_WSL_SHELL`, and `DSH_WSL_HOME` to the managed `DSH_*` namespace. |
36
+
37
+ Three modules support them and import no DSH package.
38
+ [`lib/paths.js`](../lib/paths.js) translates paths.
39
+ [`lib/listing.js`](../lib/listing.js) builds directory listings and breadcrumbs.
40
+ [`lib/wsl.js`](../lib/wsl.js) holds the `wsl.exe` interop primitives.
41
+
42
+ ## How the pieces are mounted
43
+
44
+ [`cordis.patch.yml`](../cordis.patch.yml) is the bundle layer. It is a top-level
45
+ YAML array of patch entries, and `dsh plugin --profile <name> add
46
+ dsh-plugin-wsl-env` applies it as one layer.
47
+
48
+ The environment is per session, not per process:
49
+
50
+ | Row | Mounted | Why there |
51
+ |---|---|---|
52
+ | `wsl-shell`, `wsl-fs` | inside `preset-wsl`, an agent preset with isolate realms | so one process can serve a Windows workspace and a distro workspace at the same time |
53
+ | `subprocess-wsl` | at the app level, called the composition | because no session identity reaches the terminal controller, see below |
54
+ | `directory-picker-wsl`, `wsl-shell-env`, `auto-preset` | at the app level | they answer questions that are not scoped to a session |
55
+
56
+ The preset mount requires the isolate realms. Without them the registry rejects the
57
+ providers with `Preset services require isolate realms: shell, fs`.
58
+
59
+ ### Why the terminal provider is app-level
60
+
61
+ An agent preset runs its services in an isolate realm. The preset registry creates
62
+ that realm under its own context, with `createScope(this.owner, ...)`. The agent
63
+ loop creates the Agent context under the root realm, with
64
+ `createScope(loopCtx, ...)`.
65
+
66
+ The terminal controller resolves its execution world with
67
+ `agent.ctx.get("subprocess")`, which looks in the second tree. A provider mounted in
68
+ the first tree is therefore invisible to it. The registry ships
69
+ `agentPresets.serviceFor(agent, name)` for this gap, and the terminal controller
70
+ does not use it.
71
+
72
+ So the layer disables the shipped `subprocess` row and inserts
73
+ `dsh-plugin-wsl-env/subprocess` in its place at the app level. The subclass
74
+ overrides `spawnTerminal` only. Ordinary `spawn()`, the host ripgrep search, the
75
+ pwsh executor, and the LSP host reach the shipped implementation unchanged.
76
+
77
+ The cost is that the terminal follows the app configuration, not the session. A
78
+ session opened on a Windows folder still gets a distro terminal, started in
79
+ `/mnt/<drive>/...`.
80
+
81
+ ## Path coordinates
82
+
83
+ Three coordinate systems appear in the code:
84
+
85
+ | System | Example | Used by |
86
+ |---|---|---|
87
+ | Linux inside the distro | `/home/you/proj/a.ts` | commands, `ctx.shell` |
88
+ | The distro's UNC share | `\\wsl.localhost\ubuntu\home\you\proj\a.ts` | the host filesystem stack, `ctx.fs` |
89
+ | The Windows filesystem | `C:\Users\you\...`, or `/mnt/c/Users/you/...` from inside | sessions opened on a Windows folder |
90
+
91
+ [`lib/paths.js`](../lib/paths.js) converts between them. It is pure: it imports no
92
+ DSH package and performs no I/O. Its containment helpers decide whether a target
93
+ path sits under a writable root. The filesystem fence uses them.
94
+
95
+ ## Sandbox
96
+
97
+ ### Why the Windows runner cannot be used
98
+
99
+ On Windows, DSH confines commands with `dsh-sandbox-windows-acl`. It uses a
100
+ restricted, low-integrity Windows token and a write allowlist.
101
+
102
+ That token cannot reach WSL. `wsl.exe` fails with `Wsl/E_ACCESSDENIED`, and
103
+ `\\wsl.localhost\<distro>` reports access denied. Both work normally outside the
104
+ sandbox.
105
+
106
+ ### What the plugin does instead
107
+
108
+ The plugin builds a `bubblewrap` profile on the Windows side and passes it to
109
+ `wsl.exe --exec`. The confinement is created inside the distro:
110
+
111
+ ```text
112
+ wsl.exe -d <distro> --cd <linux dir> --exec bwrap \
113
+ --ro-bind / / --dev /dev --unshare-pid --proc /proc --die-with-parent \
114
+ [--tmpfs /tmp --bind <workspace> <workspace>] -- <shell> -lc <cmd>
115
+ ```
116
+
117
+ These are the same arguments DSH's Linux runner uses (`dsh-sandbox-local`), so the
118
+ behaviour and the error messages match a Linux host. The function
119
+ `bwrapProfileArgs` in [`lib/sandbox.js`](../lib/sandbox.js) builds them.
120
+
121
+ | Mode | What a command inside the distro can do |
122
+ |---|---|
123
+ | `read-only` | read the whole distro; only `/dev/null` is writable |
124
+ | `workspace-write` | the above, plus the session workspace is writable and `/tmp` is a temporary mount |
125
+ | `danger-full-access` | no sandbox; used for an approved wider-permission request |
126
+
127
+ `WslFileSystem` checks writes against the same policy and the same writable list.
128
+ That avoids a mismatch where `bash` could write `/tmp` but the write tool could
129
+ not. Both providers report their mode through `sandboxMode`. The tool layer and the
130
+ Permissions selector read that value.
131
+
132
+ ### Reported enforcement
133
+
134
+ `WslSandbox` reports `enforcement: partial`, not `full`. A process inside the
135
+ distro can still run a Windows program through WSL interop, for example
136
+ `/mnt/c/.../*.exe`. Bubblewrap does not govern that process.
137
+
138
+ `npm run probe:sandbox` demonstrates the boundary on your machine and records it as
139
+ `INFO`.
140
+
141
+ ### Failure mode
142
+
143
+ The providers fail closed. Without a usable `bwrap` in the distro, `WslSandbox`
144
+ throws `SandboxUnavailableError`, and every confined command reports
145
+ `SANDBOX_UNAVAILABLE`. Nothing runs unconfined and silently. The string
146
+ `read-only file system` is classified as a denial, so the tool layer can offer the
147
+ wider-permission path.
148
+
149
+ Set `sandbox: false` on either provider to opt out. Commands then run unconfined,
150
+ and `sandboxMode` returns `undefined`.
151
+
152
+ ## The runtime mirror
153
+
154
+ The checkout lives inside the distro. A DSH profile can only link a Windows path,
155
+ and pnpm rewrites `link:\\wsl.localhost\...` into a broken
156
+ `/wsl.localhost/...` symlink. The harness therefore cannot load the plugin from a
157
+ distro checkout.
158
+
159
+ The Windows copy at `default-workspace/dsh-plugin-wsl` is a runtime mirror.
160
+ [`test/probe/sync-to-windows.sh`](../test/probe/sync-to-windows.sh) keeps it in
161
+ step with the checkout, and `npm run sync:windows` runs that script.
162
+
163
+ ## Test layers
164
+
165
+ | Layer | Location | Needs a harness |
166
+ |---|---|---|
167
+ | Style and packaging checks | [`test/style.mjs`](../test/style.mjs) | no |
168
+ | Syntax pass | [`test/syntax.mjs`](../test/syntax.mjs) | no |
169
+ | Unit tests | `test/*.test.mjs` | no |
170
+ | Sandbox probe | [`test/probe/sandbox.sh`](../test/probe/sandbox.sh) | no; it applies the profile arguments directly |
171
+ | Harness probes | [`test/probe/run.sh`](../test/probe/run.sh), `terminal.sh`, `sandbox-shell.sh` | yes: Windows, WSL2, a mounted profile, and a linked checkout |
172
+ | Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
173
+
174
+ The unit tests import only Node builtins, so they run on any platform. CI runs them
175
+ on Linux and Windows, on Node 20, 22 and 24. The harness probes are a manual step,
176
+ because a hosted runner has no WSL distro and no profile to mount.
177
+
178
+ The probe scripts share [`test/probe/env.sh`](../test/probe/env.sh), which derives
179
+ the Windows user, the distro name and the UNC spelling of the checkout from the
180
+ machine, and [`test/probe/env.mjs`](../test/probe/env.mjs) for the Node-side probes.
181
+ No probe carries a user name, and the YAML overlays are templates with `@NAME@`
182
+ placeholders that the scripts substitute into generated copies.
183
+
184
+ ## File layout
185
+
186
+ ```text
187
+ lib/paths.js pure conversion between the three path formats
188
+ lib/wsl.js wsl.exe interop primitives (no DSH imports)
189
+ lib/listing.js pure directory listing and breadcrumb helpers (no DSH imports)
190
+ lib/index.js WslShellExecutor (ctx.shell) and WslFileSystem (ctx.fs)
191
+ lib/sandbox.js the distro-side bwrap confinement shared by both providers
192
+ lib/picker.js WslDirectoryPicker (ctx.directoryPicker)
193
+ lib/subprocess.js WslSubprocessRuntime (ctx.subprocess), the terminal window
194
+ lib/auto-preset.js per-session environment selection
195
+ lib/shell-env.js registers the DSH_WSL_* environment variables
196
+ lib/{shell,fs}.js one-line subpath entry points
197
+ cordis.patch.yml the bundle configuration layer (dsh.bundle), commented
198
+ examples/ one machine-local profile layer, for comparison
199
+ scripts/ release helpers, used by the release workflow
200
+ test/ unit tests, the style gate, and the behaviour probes
201
+ docs/ this file, the configuration and limitation references, the
202
+ release checklist, and the archived designs
203
+ ```
204
+
205
+ `package.json` publishes a subset of this tree: the `files` list is the contract, and
206
+ `npm run lint:style` fails if an entry stops matching tracked content.
207
+
208
+ ## Where the reasoning lives
209
+
210
+ - [`CHANGELOG.md`](../CHANGELOG.md) records what changed in each release. It also
211
+ names the documentation a reader would have acted on that is no longer true.
212
+ - [`archive/engineering-record.zh.md`](archive/engineering-record.zh.md) is the
213
+ full engineering record, in Chinese: measured contracts, debugging rounds, and
214
+ the conclusions as they stood then.
215
+ - [`archive/README.md`](archive/README.md) indexes the archived material in
216
+ Chinese, and [`archive/README.en.md`](archive/README.en.md) is the English index.
217
+ - [`README.md`](../README.md) is the only statement of current state.
@@ -0,0 +1,48 @@
1
+ # Configuration
2
+
3
+ Every value lives in a profile row, and a row is overridden by id in your own
4
+ profile layer: `$DSH_HOME/profiles/<name>/cordis.patch.yml`. Start from
5
+ [`examples/profile.cordis.patch.yml`](../examples/profile.cordis.patch.yml), and keep
6
+ [`cordis.patch.yml`](../cordis.patch.yml) open beside it — that file is the commented
7
+ reference for every value the plugin ships.
8
+
9
+ In the table below, *(shipped)* marks the values set by `cordis.patch.yml`. The rest
10
+ are schema defaults, listed because they are the ones worth knowing.
11
+
12
+ | Row | Key | Default | Meaning |
13
+ |---|---|---|---|
14
+ | `wsl-shell` | `distro` | `''` *(shipped)* | distro name; empty means WSL's default distro |
15
+ | | `shell` | `''` | pin a shell inside the distro; empty means the user's login shell |
16
+ | | `loginShell` | `true` *(shipped)* | use `<shell> -lc`, which sources your profile, instead of a bare `-c` |
17
+ | | `sandbox` | `true` | confine commands with `bubblewrap`; `false` disables the sandbox |
18
+ | | `cwd` | `''` | default working directory; empty means the distro user's home |
19
+ | | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000` *(shipped)* | per-call time limit, and the ceiling a call may ask for |
20
+ | `wsl-fs` | `distro` | `''` *(shipped)* | as above |
21
+ | | `restrictToDistro` | `true` *(shipped)* | refuse paths outside the pinned distro, including `/mnt/c` |
22
+ | | `sandbox` | `true` | check `writeText` and `editText` against the policy |
23
+ | | `resolveSymlinks` | `true` | follow Linux symlinks that the share cannot traverse, such as `/etc/os-release` and `/bin` |
24
+ | | `cwd` | `''` | base directory for relative paths; empty means the distro user's home |
25
+ | `directory-picker-wsl` | `preferredDistro` | `''` *(shipped)* | the distro listed first in the picker |
26
+ | | `includeHostHome` | `true` *(shipped)* | also list the Windows home directory |
27
+ | | `maxEntries` | `1000` *(shipped)* | maximum entries listed per directory |
28
+ | `subprocess-wsl` | `distro` | `''` *(shipped)* | which distro the GUI terminal opens in |
29
+ | | `shell` | `''` | pin a shell; empty lets `wsl.exe` decide |
30
+ | | `loginShell` | `true` | whether a pinned shell uses login semantics |
31
+
32
+ You can override the other keys in the same way; they keep their defaults. Those are
33
+ `wslPath`, `hostCwd` and `forwardEnv` on the shell and terminal rows,
34
+ `diffBasisMaxBytes` on `wsl-fs`, and `distroCacheMs` on the picker.
35
+
36
+ ## Which distro is used
37
+
38
+ `distro: ''` means the distro `wsl.exe` treats as default, so an installation with a
39
+ single distro needs no configuration. Name one when you have several, or when the
40
+ default is not the one you want sessions in.
41
+
42
+ ## Turning the sandbox off
43
+
44
+ `sandbox: false` on `wsl-shell` and `wsl-fs` runs commands and writes without
45
+ confinement. `sandboxMode` then reports `undefined`, and the tool layer tells the
46
+ model that these operations have no sandbox. That is the documented opt-out; see
47
+ [ARCHITECTURE.md](ARCHITECTURE.md#sandbox) for what the sandbox does and does not
48
+ cover.
@@ -0,0 +1,30 @@
1
+ # Limitations
2
+
3
+ What the plugin does not do, and why. A limitation that is documented is a design
4
+ boundary; one that is not is a bug report waiting to happen.
5
+
6
+ - **The sandbox does not govern WSL interop.** A confined command can still run a
7
+ Windows program, which escapes the Linux-side boundary. The plugin reports this as
8
+ `enforcement: partial`. See [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement).
9
+ - **`bubblewrap` must be installed**, or both providers fail immediately.
10
+ - `workspace-write` binds the workspace root as writable, and bubblewrap refuses a
11
+ bind whose source directory does not exist. If a session's workspace directory is
12
+ deleted, commands fail with a runner error; the plugin does not recreate the
13
+ directory.
14
+ - New files get the distro's umask default (0644). A `chmod` from the Windows side on
15
+ a share path is silently ignored; overwrites and edits keep the original permission
16
+ bits. Run `chmod +x` inside the distro when you need an executable.
17
+ - The check before a write is "check, then write", which is not atomic. This backend
18
+ checks the caller's guard itself, because the Windows backend publishes a guarded
19
+ new file with a hard link, and the share does not support hard links. This is the
20
+ race that `dsh-fs-sandbox` already documents.
21
+ - `watch()` (file change monitoring) is not provided. Watching a 9p share is
22
+ unreliable, so the plugin refuses instead of arming it.
23
+ - `glob` and `grep` use the Windows-side ripgrep over the share: correct results, but
24
+ not fast, and `.gitignore` is interpreted with Windows rules. `editText` reads the
25
+ whole file into memory before writing it back.
26
+ - The terminal belongs to the whole app, not to one session. A session opened on a
27
+ Windows folder still gets the distro terminal, starting in `/mnt/<drive>/...`, and
28
+ its shell menu is deliberately reduced to the one configured profile.
29
+ - Terminal activity reporting stops at `wsl.exe`, so the controller never reclaims
30
+ these terminals when they are idle.
package/docs/README.zh.md CHANGED
@@ -6,25 +6,12 @@
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
- 这个插件让 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 直接把 WSL 发行版当作工作环境使用。命令在发行版里执行,模型的文件工具读写发行版里的真实文件,打开文件夹时可以直接选择发行版目录,GUI 终端也开在发行版里,而不是开在 UNC 路径下的 `cmd.exe`。
9
+ 这个插件让 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 把 WSL 发行版当作工作环境:命令在发行版里执行,模型的文件工具读写发行版里的真实文件,文件夹选择器可以直接打开发行版目录,GUI 终端也开在发行版里。
10
10
 
11
- **只支持 Windows + WSL2。** 安装只有一条命令,插件自身没有依赖。WSL 环境按会话生效:会话如果打开的是 Windows 文件夹,它继续使用原来的 Windows 环境,不受影响。
11
+ **只支持 Windows + WSL2。** 安装只需一条命令,插件自身没有依赖。环境按会话生效:打开 Windows 文件夹的会话继续使用原来的 Windows 环境。
12
12
 
13
- 目录:[能做什么](#能做什么) · [安装](#安装) · [使用](#使用) · [配置](#配置) · [沙箱](#沙箱) · [常见问题](#常见问题) · [已知限制](#已知限制) · [开发](#开发) · [设计说明](#设计说明) · [参与与支持](#参与与支持)
14
-
15
- ## 能做什么
16
-
17
- | 原来的行为 | 现在的行为 |
18
- |---|---|
19
- | 命令在 Windows 上执行 | 命令在**你的发行版里**执行,使用你的登录 shell,起始目录是会话的 Linux 目录 |
20
- | `read`/`write`/`edit`/`glob`/`grep` 操作 Windows 路径 | 这些工具直接操作**发行版里的真实文件**,路径经由 `\\wsl.localhost\<发行版名>` 共享访问 |
21
- | 文件夹选择器看不到 WSL | 同一个选择器既列出 Windows 家目录,也列出每个已安装的发行版,可以直接打开 `/home/you/project` |
22
- | 终端开在 UNC 目录下的 `cmd.exe` | 终端开在**发行版里**的会话目录,身份是你的发行版用户 |
23
- | 命令没有沙箱约束 | 命令由**发行版内的 bubblewrap** 约束,见[沙箱](#沙箱)一节 |
24
-
25
- 如果会话打开的是发行版里的文件夹,WSL 环境会自动生效:创建会话时就会绑定 `wsl` preset,所以第一次调用工具时环境已经正确,不需要手动切换。
26
-
27
- **怎么确认它在工作?** 让模型执行 `uname -r`,输出的应该是 WSL2 内核,例如 `6.18.40.1-microsoft-standard-WSL2`。也可以执行 `echo $WSL_DISTRO_NAME`。
13
+ [安装](#安装) · [使用](#使用) · [配置](#配置) · [沙箱](#沙箱) · [常见问题](#常见问题) ·
14
+ [开发](#开发) · [文档](#文档)
28
15
 
29
16
  ## 安装
30
17
 
@@ -37,84 +24,58 @@ dsh --profile wsl --dump-config # 3. 只做组合检查,不
37
24
  dsh --profile wsl # 4. 启动
38
25
  ```
39
26
 
40
- 第 3 步应该能看到 `# == dsh-plugin-wsl-env` 这一层,以及被它覆盖的行,其中最关键的是 `- id: terminal-controller` 上的 `shell: { path: wsl.exe, name: WSL }`。
41
-
42
- 然后在 GUI 里打开 `\\wsl.localhost\<发行版名>\...` 下的文件夹(选择器会在根一级列出每个发行版),或者直接点 **New terminal**。
27
+ 第 3 步应该打印出名为 `# == dsh-plugin-wsl-env` 的层,并且 `- id: terminal-controller` 上应出现 `shell: { path: wsl.exe, name: WSL }`。这个包是 DSH bundle,所以第 2 步会把 [`cordis.patch.yml`](../cordis.patch.yml) 作为配置层施加进去,没有任何需要手工合并的补丁。
43
28
 
44
- 这个包是一个 DSH bundle:`package.json` 里声明了 `dsh.bundle.patch`,所以第 2 步会把 [`cordis.patch.yml`](../cordis.patch.yml) 作为配置层加入,不需要手工合并补丁。第 1 步用 Web 模板创建 profile,原因是这一层要覆盖几个只有 Web 界面才有的行:组合层的 `subprocess` provider、终端控制器和文件夹选择器。
29
+ 然后在 GUI 里打开 `\\wsl.localhost\<发行版>\...` 下的文件夹。选择器会在根一级列出每个已安装的发行版,**New terminal** 会在发行版里打开 shell。
45
30
 
46
- 还需要在发行版里安装 **bubblewrap**(`sudo apt install bubblewrap`)。没有它时每条命令都会直接失败,原因见[沙箱](#沙箱)。
31
+ 还需要在发行版里安装 **bubblewrap**:`sudo apt install bubblewrap`。没有它每条命令都会失败关闭,见[沙箱](#沙箱)。
47
32
 
48
- 卸载:`dsh plugin --profile wsl remove dsh-plugin-wsl-env`。
33
+ 卸载:`dsh plugin --profile wsl remove dsh-plugin-wsl-env`。升级:再执行一次同样的 `add` 命令。
49
34
 
50
- 升级:再执行一次 `dsh plugin --profile wsl add dsh-plugin-wsl-env`,新版本会用自带的配置层替换旧层。
51
-
52
- > **如果改的是 checkout 里的 `lib/`,必须重启应用。** 运行中的进程会缓存 ES module,不重启就还在用旧代码。
35
+ > **改了 `lib/` 下的任何文件?必须重启应用。** 运行中的进程会缓存 ES module,否则会继续用旧代码。
53
36
 
54
37
  ## 使用
55
38
 
56
- 一个典型用法:把 `\\wsl.localhost\ubuntu\home\you\project` 作为工作区打开,然后问"当前内核版本是什么,`/etc/os-release` 里有什么"。模型会直接在发行版里执行 `uname -r` 并读取该文件,不需要经过 `/mnt/c`,也不需要复制文件。
39
+ 把 `\\wsl.localhost\ubuntu\home\you\project` 作为工作区打开,然后问模型"我在什么内核上,`/etc/os-release` 里有什么"。它会在发行版里执行 `uname -r` 并读取该文件,不经过 `/mnt/c`,也不复制文件。
57
40
 
58
- - **打开发行版文件夹**:选择器列出 Windows 家目录和每个发行版。选择 `\\wsl.localhost\ubuntu\home\you\project` 之后,会话工作区、shell 的起始目录和终端都会跟着它。
59
- - **命令**以 `wsl.exe -d <发行版> --cd <Linux 目录> --exec <你的登录 shell> -lc <命令>` 的形式执行,所以你的 `PATH`、`nvm`、`cargo`、`pyenv` 和 rc 配置都会生效,而不是固定使用 bash。
60
- - **文件**就是发行版里的真实文件:`/home/you/x` 和 `\\wsl.localhost\ubuntu\home\you\x` 指向同一个文件,`/mnt/c/...` 指向 Windows 磁盘。
61
- - **终端**(右侧栏 → New terminal)在发行版里打开会话目录下的 shell。
62
- - **模型知道自己的 shell 环境**:插件向受管的 `DSH_*` 命名空间注册了 `DSH_WSL_DISTRO`、`DSH_WSL_SHELL` 和 `DSH_WSL_HOME`,shell 工具会引导模型读取这些变量。
63
- - **权限**和在 Linux 主机上一致:权限选择器可在 `read-only`、`workspace-write`(默认)和 `danger-full-access` 之间切换。命令或写入被拒绝时,工具会给出放宽权限的提示;批准后,这一次调用会在不加沙箱的情况下执行。
41
+ - **发行版文件夹自动获得发行版环境。** 创建会话时就会绑定 `wsl` preset,所以第一次工具调用就已经正确。
42
+ - **命令**以 `wsl.exe -d <发行版> --cd <Linux 目录> --exec <你的登录 shell> -lc <命令>` 执行,所以你的 `PATH`、`nvm`、`cargo`、`pyenv` 和 rc 配置都生效。shell 不是硬编码的 bash。
43
+ - **文件就是发行版的真实文件。** `/home/you/x` 与 `\\wsl.localhost\ubuntu\home\you\x` 是同一个文件,`/mnt/c/...` 通向 Windows 磁盘。
44
+ - **终端**(右侧栏 → *New terminal*)在发行版里打开会话目录下的 shell。
45
+ - **模型能看到自己的 shell 环境。** 插件向受管的 `DSH_*` 命名空间注册 `DSH_WSL_DISTRO`、`DSH_WSL_SHELL` 和 `DSH_WSL_HOME`。
46
+ - **权限与 Linux 主机一致。** 权限选择器在 `read-only`、`workspace-write`(默认)和 `danger-full-access` 之间切换。被拒绝的命令或写入会带回"放宽权限"的提议;批准后,这一次调用不加沙箱执行。
64
47
 
65
48
  ## 配置
66
49
 
67
- 修改配置的方式是在自己的 profile 层里按 id 覆盖某一行。文件是 `$DSH_HOME/profiles/<名字>/cordis.patch.yml`,示例见 [`examples/profile.cordis.patch.yml`](../examples/profile.cordis.patch.yml)。下表中标了 *(shipped)* 的值是 [`cordis.patch.yml`](../cordis.patch.yml) 里实际设置的值,其余是 schema 的默认值,列出来是因为它们比较常用。
50
+ 在自己的 profile 层里按 id 覆盖某一行:`$DSH_HOME/profiles/<名字>/cordis.patch.yml`。值得知道的键:
68
51
 
69
52
  | 行 | 键 | 默认值 | 说明 |
70
53
  |---|---|---|---|
71
- | `wsl-shell` | `distro` | `''` *(shipped)* | 发行版名;空表示用 WSL 的默认发行版 |
72
- | | `shell` | `''` | 指定发行版内的 shell;空表示用该用户的登录 shell |
73
- | | `loginShell` | `true` *(shipped)* | 用 `<shell> -lc`(会加载你的 profile),而不是裸 `-c` |
54
+ | `wsl-shell` | `distro` | `''` | 发行版名;空表示用 WSL 的默认发行版 |
74
55
  | | `sandbox` | `true` | 用 `bubblewrap` 约束命令;`false` 表示不用沙箱 |
75
- | | `cwd` | `''` | 默认工作目录;空表示发行版用户的家目录 |
76
- | | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000` *(shipped)* | 单次调用的时限,以及调用方可以申请的上限 |
77
- | `wsl-fs` | `distro` | `''` *(shipped)* | 同上 |
78
- | | `restrictToDistro` | `true` *(shipped)* | 拒绝发行版之外的路径(包括 `/mnt/c`) |
56
+ | `wsl-fs` | `distro` | `''` | 同上 |
57
+ | | `restrictToDistro` | `true` | 拒绝发行版之外的路径(包括 `/mnt/c`) |
79
58
  | | `sandbox` | `true` | 写入时按策略检查 `writeText` 和 `editText` |
80
- | | `resolveSymlinks` | `true` | 跟随共享层无法穿越的 Linux 符号链接,例如 `/etc/os-release`、`/bin` |
81
- | | `cwd` | `''` | 相对路径的基准目录;空表示发行版用户的家目录 |
82
- | `directory-picker-wsl` | `preferredDistro` | `''` *(shipped)* | 在选择器里排在第一位的发行版 |
83
- | | `includeHostHome` | `true` *(shipped)* | 同时列出 Windows 家目录 |
84
- | | `maxEntries` | `1000` *(shipped)* | 单个目录最多列出多少条 |
85
- | `subprocess-wsl` | `distro` | `''` *(shipped)* | GUI 终端开在哪个发行版 |
86
- | | `shell` | `''` | 指定 shell;空表示由 `wsl.exe` 决定 |
87
- | | `loginShell` | `true` | 指定 shell 时是否使用登录语义 |
59
+ | `directory-picker-wsl` | `includeHostHome` | `true` | 同时列出 Windows 家目录 |
60
+ | `subprocess-wsl` | `distro` | `''` | GUI 终端开在哪个发行版 |
88
61
 
89
- 其余键也可以用同样的方式覆盖,默认值不变:shell 和终端行的 `wslPath`、`hostCwd`、`forwardEnv`;`wsl-fs` 的 `diffBasisMaxBytes`;选择器的 `distroCacheMs`。
62
+ [`cordis.patch.yml`](../cordis.patch.yml) 是每个随包值的注释参考。[docs/CONFIGURATION.md](CONFIGURATION.md) 列出其余键,包括 `shell`、`loginShell`、`cwd`、`timeoutMs`、`resolveSymlinks`、`preferredDistro` 和 `maxEntries`;[examples/profile.cordis.patch.yml](../examples/profile.cordis.patch.yml) 是一份可照抄的本机层。
90
63
 
91
64
  ## 沙箱
92
65
 
93
- **简单说:** 命令在发行版内由 `bubblewrap` 约束,文件写入按同一份策略检查。但约束范围报为 `partial`(部分生效),因为发行版里的进程仍然可以通过 WSL interop 访问 Windows。bubblewrap 必须安装;缺少它时插件会直接拒绝执行,而不是静默地不加约束。
94
-
95
- 在 Windows 上,DSH 用 `dsh-sandbox-windows-acl` 约束命令:它使用受限的低完整性令牌和写入白名单。这个令牌**完全无法访问 WSL**:`wsl.exe` 会报 `Wsl/E_ACCESSDENIED`,`\\wsl.localhost\<发行版名>` 会报拒绝访问,而在沙箱之外两者都正常。所以 WSL 执行环境无法沿用 Windows 沙箱,改用 Linux 沙箱:插件在 Windows 侧拼出 bubblewrap 的参数,交给 `wsl.exe --exec` 在发行版内执行。
66
+ 命令由**发行版内的 `bubblewrap`** 约束,文件写入按同一份策略检查。Windows ACL 沙箱在这里用不了:它的受限令牌完全到不了 WSL。
96
67
 
97
- ```text
98
- wsl.exe -d <发行版> --cd <Linux 目录> --exec bwrap \
99
- --ro-bind / / --dev /dev --unshare-pid --proc /proc --die-with-parent \
100
- [--tmpfs /tmp --bind <工作区> <工作区>] -- <shell> -lc <命令>
101
- ```
102
-
103
- 这套参数与 DSH 自带的 Linux 方案(`dsh-sandbox-local`)逐参数一致,所以行为和错误信息都与在 Linux 主机上运行相同。
104
-
105
- | 模式 | 发行版里的命令得到什么 |
68
+ | 模式 | 发行版里的命令可以做什么 |
106
69
  |---|---|
107
- | `read-only` | 整个发行版只读,只有 `/dev/null` 可写,这是 shell 必需的 |
70
+ | `read-only` | 整个发行版只读,只有 `/dev/null` 可写 |
108
71
  | `workspace-write` | 在上一行基础上,把会话工作区绑定为可写,并把 `/tmp` 挂成临时目录 |
109
- | `danger-full-access` | 完全不加包装,用于批准后的放宽权限请求 |
110
-
111
- `WslFileSystem` 在写入路径上使用同一份策略和同一批可写目录,所以不会出现"bash 能写 `/tmp`,但写工具不能"这种不一致。两个 provider 都通过 `sandboxMode` 上报当前模式,权限选择器和"被拒绝 → 申请放宽权限"的流程因此恢复。
72
+ | `danger-full-access` | 不加沙箱;用于批准后的放宽权限请求 |
112
73
 
113
- **约束范围是 `partial`,不是 `full`,这一点必须说明。** 发行版里的进程仍然可以通过 interop 执行 Windows 程序(`/mnt/c/.../*.exe`)。那不是 Linux 进程,bubblewrap 管不到它;它以你平常的 Windows 令牌运行,能写你有权限写的任何位置。`npm run probe:sandbox` 会实际演示这一点,并且以后每次运行都会再确认一次。要堵住这个缺口,就必须禁止执行 `/mnt` 下的程序,而那会同时切断 `/mnt/c/...` 会话,所以这里如实说明,不做掩盖。网络和进程可见性在所有平台上都不属于这个模式的管辖范围。
74
+ **bubblewrap 是必需项,而且失败是关闭的。** 没有它时,每条受限命令都报 `SANDBOX_UNAVAILABLE`,而不是不受约束地运行。要退出约束就在任一 provider 上设 `sandbox: false`,工具层会如实告诉模型这些操作没有沙箱。
114
75
 
115
- **bubblewrap 是必需项,缺少时会直接失败。** 没有它时,每条受约束的命令都会报 `SANDBOX_UNAVAILABLE`,不会退化成不加约束地执行;同时 `sandboxMode` 也不再上报,避免出现"声称有沙箱、实际没有"的情况。确实不想用沙箱时,可以在任一 provider 上设 `sandbox: false`:命令会不加约束地执行,`sandboxMode` 返回 `undefined`,工具层会如实告诉模型这些操作没有沙箱。
76
+ **上报的强制程度是 `partial` 而不是 `full`。** 发行版里的进程仍可经 WSL interop 执行 Windows 程序(例如 `/mnt/c/.../*.exe`),bubblewrap 管不到它。`npm run probe:sandbox` 会在你的机器上演示这条边界。
116
77
 
117
- **GUI 终端不受沙箱约束。** 它是给人用的交互式 shell,与官方终端 provider 的行为一致。
78
+ 设计见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#sandbox),插件不做的事情见 [docs/LIMITATIONS.md](LIMITATIONS.md)。
118
79
 
119
80
  ## 常见问题
120
81
 
@@ -130,18 +91,6 @@ wsl.exe -d <发行版> --cd <Linux 目录> --exec bwrap \
130
91
  | `glob`/`grep` 很慢 | Windows 侧的 ripgrep 在 9p 共享上遍历 | 属于预期,收窄搜索路径,或改用 `bash` 调用发行版内的工具 |
131
92
  | 终端活动显示 `unknown` | 官方 shell 集成只对 POSIX 主机上直接启动的 `bash`/`zsh` 生效 | 关闭标签页以释放进程;空闲回收不会对它触发 |
132
93
 
133
- ## 已知限制
134
-
135
- - **沙箱管不到 WSL interop**,见[沙箱](#沙箱):受约束的命令仍然可以运行 Windows 程序,从而超出 Linux 侧的约束范围。插件把这种情况报为 `enforcement: partial`。
136
- - **必须安装 `bubblewrap`**,否则两个 provider 都会直接失败。
137
- - `workspace-write` 会把工作区根目录绑定为可写,而 bubblewrap 不允许绑定的源目录不存在。所以如果会话的工作区目录被删掉,命令会因 runner 报错而失败,插件不会替你重建目录。
138
- - 新建文件的权限是发行版的 umask 默认值(0644)。在 Windows 侧对共享路径执行 `chmod` 会被静默忽略;覆盖和编辑会保留原有权限位。需要可执行权限时,请在发行版内执行 `chmod +x`。
139
- - 写入前的检查是"先检查再写入",不是原子操作。本后端自己检查调用方的守卫,因为 Windows 后端发布受守卫保护的新建文件时使用硬链接,而共享层不支持硬链接。这是 `dsh-fs-sandbox` 已经记录过的竞态。
140
- - 不提供 `watch()`(文件变更监视)。9p 共享上的监视不可靠,所以直接拒绝,而不是勉强启用。
141
- - `glob`/`grep` 使用 Windows 侧的 ripgrep 在共享路径上搜索:结果正确,但速度不快,`.gitignore` 规则也按 Windows 侧的规则处理。`editText` 会把整个文件读入内存后再写回。
142
- - 终端属于整个应用(组合层),不属于单个会话。所以工作在 Windows 文件夹的会话也会打开发行版终端,起始目录是 `/mnt/<盘符>/...`,并且它的 shell 菜单被有意缩减为配置里的那一个。
143
- - 终端活动状态的上报止步于 `wsl.exe`,所以控制器不会对这些终端触发空闲回收。
144
-
145
94
  ## 开发
146
95
 
147
96
  ```bash
@@ -153,75 +102,21 @@ npm run probe:sandbox-shell # 启动真实 harness,驱动受限执行器
153
102
  npm run probe:terminal # 通过终端 provider 打开一个 PTY
154
103
  ```
155
104
 
156
- 这个包没有依赖,所以不需要先安装任何东西。
157
-
158
- 目录结构:
159
-
160
- ```text
161
- lib/paths.js 三套路径写法之间的纯转换函数
162
- lib/wsl.js wsl.exe 互操作原语(不 import DSH)
163
- lib/listing.js 目录列举与面包屑的纯函数(不 import DSH)
164
- lib/index.js WslShellExecutor(ctx.shell)和 WslFileSystem(ctx.fs)
165
- lib/sandbox.js 两个 provider 共用的发行版内 bwrap 约束
166
- lib/picker.js WslDirectoryPicker(ctx.directoryPicker)
167
- lib/subprocess.js WslSubprocessRuntime(ctx.subprocess),即终端窗口
168
- lib/auto-preset.js 按会话选择环境
169
- lib/shell-env.js 注册 DSH_WSL_* 环境变量
170
- lib/{shell,fs}.js 各一行的子路径入口
171
- cordis.patch.yml bundle 配置层(dsh.bundle),带逐行注释
172
- examples/ 一份本机 profile 层,作为对照
173
- test/ 单元测试和行为探针
174
- docs/archive/ 被本设计替换掉的方案,以及原因
175
- ```
176
-
177
- `npm test` 覆盖纯模块,并对每个随包模块执行一次 `--check` 语法检查。它无法 import 服务类模块,因为这些模块需要 DSH 的 peer 包,而裸 checkout 里没有。这个缺口只有在真正启动 harness 时才会暴露;归档记录 §15.4 记录了它曾经导致的五轮误诊。
178
-
179
- `test/probe/sandbox.sh` 不需要 harness:它使用 `lib/sandbox.js` 生成的同一套参数,断言 bubblewrap 能约束什么、不能约束什么,并把 interop 逃逸记为 `INFO`,因为 Linux 沙箱管不了 Windows 进程。
180
-
181
- 其余探针会启动绑定到发行版的一次性 profile:
182
-
183
- - 文件系统探针断言写入路径和写入检查。策略根目录之外、以及 `read-only` 模式下的写入都会返回 `FS_SANDBOX_DENIED`,`danger-full-access` 不做检查。
184
- - shell 探针通过 `ctx.shell` 驱动三种模式,并检查工具层返回的拒绝分类。
185
- - 终端探针断言发行版、起始目录,以及经 `WSLENV` 转发的 `DSH_*` 变量。
186
-
187
- 一次性 profile 的搭建步骤写在 `test/probe/run.sh` 的头部;`terminal.sh` 和 `sandbox-shell.sh` 复用它。
188
-
189
- **已验证**(Windows 11 + WSL2,Ubuntu 26.04):
190
-
191
- - 与各服务接入点的对接、UNC 路径处理和选择器行为;
192
- - 插件在真实 profile 里端到端挂载;
193
- - 在仅 WSL 的 headless profile 里完成一次真实模型回合:`write → chmod → read → edit → execute`,编辑后可执行权限仍然保留;
194
- - 终端 provider 在一次性 Web 启动和日常 GUI profile 中都成立;
195
- - 沙箱用四种方式验证:在发行版里实测参数、文件系统写入检查、shell 路径(`enforcement: partial`,拒绝被正确分类),以及日常 GUI profile 里 agent 自己的会话在会话工作区之外写入时被发行版内拒绝、随后的放宽权限请求成功。
196
-
197
- 数量:42 条单元测试断言、18 条文件系统探针断言、10 条 shell 探针检查、10 条沙箱预期另加被记录的逃逸、3 条终端断言。
198
-
199
- **运行时镜像。** checkout 在发行版内开发,但 harness 是 Windows 进程,而 profile 只能链接 Windows 路径:pnpm 会把 `link:\\wsl.localhost\...` 改写成坏掉的 `/wsl.localhost/...` 符号链接。所以 `default-workspace/dsh-plugin-wsl` 这份 Windows 副本是运行时镜像,启动应用前用 `test/probe/sync-to-windows.sh` 同步。该目标目录不在任何会话工作区内,所以 agent 在受限 shell 里执行同步时会被 `workspace-write` 拒绝,需要为这一条命令批准 `danger-full-access`。这是沙箱的预期行为,不是脚本坏了;不想看到提示就在普通 distro 终端里执行。
200
-
201
- **CI** 在 Linux 和 Windows 上用 Node 20、22、24 运行 `npm test`,并在 Node 24 上运行
202
- `npm run test:coverage` 以执行覆盖率阈值。
203
-
204
- ## 设计说明
205
-
206
- 有两处设计从 profile YAML 里看不出来。完整推理、实测数据和所有被放弃的方案都在归档工程记录里:[docs/archive/engineering-record.zh.md](archive/engineering-record.zh.md)。它是历史文档,不是第二份 README;如果它与本文冲突,以本文为准。
207
-
208
- **为什么终端 provider 放在组合层。** `ctx.shell` 和 `ctx.fs` 是按会话提供的,位于 `wsl` agent preset 的 isolate realm 中。因此宿主工作区继续使用官方受限的 PowerShell 环境,WSL 工作区使用发行版,两者可以在同一个进程里同时存在。
209
-
210
- 终端无法这样提供。`dsh-api-terminal-controller` 通过 `agent.ctx.get("subprocess")` 查找执行环境。Agent 的 context 由 agent loop 创建在**根** realm 之下;preset 的 isolate realm 则由 `dsh-agent-preset-registry` 创建在**注册表自身的** context 之下。这两条作用域链不会相交,所以挂在 `preset-wsl` 里的 `subprocess` provider 对终端窗口不可见。
211
-
212
- 因此 [`cordis.patch.yml`](../cordis.patch.yml) 替换的是**组合层**的 `subprocess` 行,换成一个只覆写 `spawnTerminal` 的子类。普通的 `spawn()`、Windows 侧的 ripgrep 搜索、pwsh 执行器和 LSP 宿主都仍然走官方实现。代价是终端跟着组合走,而不是跟着会话走,见[已知限制](#已知限制)。
105
+ 这个包没有依赖,被测模块只 import Node 内置模块,所以不需要先安装任何东西。CI 在 Linux 和 Windows 上用 Node 20、22、24 运行 `npm test`,并在 Node 24 上运行 `npm run test:coverage`。
213
106
 
214
- **为什么沙箱放在 Linux 侧。** Windows ACL runner 的受限令牌完全到不了 WSL,所以约束只能在 Windows 侧构造、在发行版内执行。`lib/sandbox.js` 因此照搬 `dsh-sandbox-local` 的 Linux `bwrap` 方案,而不是去用 `ctx.sandbox`;后者 `dsh-tool-bash` 从来不用,它读的是执行器的 `sandboxMode` 和 `ctx.sandboxPolicy`,这两样都由本插件提供。
107
+ [docs/ARCHITECTURE.md](ARCHITECTURE.md) 有文件布局、挂载方式和沙箱设计;[CONTRIBUTING.md](../CONTRIBUTING.md) 有开发循环、完整质量门清单和已验证内容。
215
108
 
216
- ## 参与与支持
109
+ ## 文档
217
110
 
218
111
  | 文档 | 内容 |
219
112
  |---|---|
220
- | [CONTRIBUTING.md](../CONTRIBUTING.md) | 开发循环、提交前必须通过的质量门、本仓库的约定 |
221
- | [SECURITY.md](../SECURITY.md) | 如何私下上报漏洞、响应时限、以及哪些问题在范围内 |
222
- | [SUPPORT.md](../SUPPORT.md) | 支持范围、去哪里提问、以及一份有用的缺陷报告应包含什么 |
223
- | [docs/ARCHITECTURE.md](ARCHITECTURE.md) | provider 如何挂载、沙箱设计、测试分层 |
224
- | [docs/RELEASING.md](RELEASING.md) | 发布检查单,含信任发布(trusted publishing)的配置 |
113
+ | [docs/CONFIGURATION.md](CONFIGURATION.md) | 每个配置键,以及如何覆盖 |
114
+ | [docs/ARCHITECTURE.md](ARCHITECTURE.md) | provider、挂载方式、路径坐标、沙箱、测试分层、文件布局 |
115
+ | [docs/LIMITATIONS.md](LIMITATIONS.md) | 插件不做的事情,以及原因 |
116
+ | [CONTRIBUTING.md](../CONTRIBUTING.md) | 开发循环、质量门、约定、已验证内容 |
117
+ | [SECURITY.md](../SECURITY.md) | 如何私下上报漏洞 |
118
+ | [SUPPORT.md](../SUPPORT.md) | 支持范围,以及去哪里提问 |
119
+ | [docs/RELEASING.md](RELEASING.md) | 发布检查单 |
225
120
  | [docs/archive/README.en.md](archive/README.en.md) | 归档工程记录的英文索引 |
226
121
 
227
122
  ## 许可
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-wsl-env",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Run a DeepSeek Harness session against a WSL distro: a ctx.shell executor that runs commands inside the distro and a ctx.fs backend over its UNC share",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -35,6 +35,9 @@
35
35
  "examples/**/*.yml",
36
36
  "README.md",
37
37
  "docs/README.zh.md",
38
+ "docs/CONFIGURATION.md",
39
+ "docs/LIMITATIONS.md",
40
+ "docs/ARCHITECTURE.md",
38
41
  "CHANGELOG.md",
39
42
  "CONTRIBUTING.md",
40
43
  "SECURITY.md",