dsh-plugin-wsl-env 0.1.3 → 0.1.5

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,88 @@ reference below points at that record's numbering.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.1.5] - 2026-10-02
16
+
17
+ Three defects from a design-closure review. Two are cases where a documented remedy
18
+ could not work, and one where the cause of a failure was invisible.
19
+
20
+ ### Fixed
21
+
22
+ - A failed sandbox probe was remembered for the process lifetime, so installing
23
+ `bubblewrap` after the app had started changed nothing until a restart — while the
24
+ error told the user to install exactly that. Only a success is cached now; a failure
25
+ is re-probed on the next command, and concurrent first commands share one probe. The
26
+ policy lives in [`lib/probe-cache.js`](lib/probe-cache.js), which is pure, so the
27
+ regression is pinned on every platform including CI
28
+ ([`test/probe-cache.test.mjs`](test/probe-cache.test.mjs)).
29
+ - `restrictToDistro` refused another distro's share with `FS_SANDBOX_DENIED`. The
30
+ file-tool layer turns that code into a denial marker plus an escalation offer
31
+ (`mapError` in `dsh-tool-fs`), and no wider permission can lift this fence: it is a
32
+ configuration choice, not a sandbox decision. It now reports `FS_OUTSIDE_DISTRO`,
33
+ which the tool layer passes through unchanged, and the message says what to do.
34
+ - A command that never started because the distro was missing, stopped or
35
+ unregistered reached the model as a bare non-zero exit. `wsl.exe` writes
36
+ `Wsl/Service/WSL_E_DISTRO_NOT_FOUND` to **stdout** — UTF-16LE on some builds — with
37
+ an empty stderr, so every signature in the plugin missed it. `WslShellExecutor` now
38
+ reads that code (NUL-stripped, so either encoding is found), pins `WSL_UTF8` on its
39
+ own spawn so the text is readable, and fails with the distro name and the remedy.
40
+ Deliberately not a sandbox code: a wider permission cannot create a distro.
41
+
42
+ ### Documentation
43
+
44
+ - Two statements were wrong and are corrected against measurement. `read-only` does
45
+ not leave `/dev/null` as the only writable path: `bwrap --dev /dev` mounts a fresh
46
+ `/dev`, so `/dev/shm` is writable scratch and only `/tmp` stays read-only. And
47
+ `restrictToDistro` does not refuse `/mnt/c`, which is a directory *inside* the
48
+ distro — it refuses another distro's share.
49
+ - The shell row's inherited output budgets (`maxOutputBytes`, `maxSpillBytes`,
50
+ `graceMs`) are listed in [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md); the
51
+ fail-closed bullet in [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md) matches the
52
+ re-probing behaviour, and what the file tools do when a distro stops mid-session is
53
+ stated there too.
54
+
55
+ ### Changed
56
+
57
+ - `test:unit` gained `test/probe-cache.test.mjs` and `test/sandbox.test.mjs`;
58
+ `test:coverage` gained the pure one. The sandbox test skips itself where the
59
+ `dsh-sandbox` peer is absent, which is the CI case, and
60
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) explains when a test belongs in which list.
61
+
62
+
63
+ ### Fixed
64
+
65
+ - A shell request that names its workdir only **relatively** now lands on the
66
+ distro user's home, joined under it, when no `cwd` is configured — instead of
67
+ riding on the parent's `process.cwd()` fallback, a Windows directory no
68
+ `wsl.exe --cd` accepts. The empty-workdir case already defaulted to the home;
69
+ the relative tail was the one shape the marker missed. Absolute workdirs in
70
+ any coordinate system are unchanged.
71
+ - A Windows UNC path outside the WSL share (`\\server\share\...`) is now
72
+ treated as the world path it already is, instead of being joined onto the
73
+ default workdir as if it were relative — which named a file that existed
74
+ nowhere. It also no longer triggers a `wsl.exe` home query it can never need:
75
+ `isRelativeWorldPath` now reports every UNC form, WSL or not, as absolute.
76
+
77
+ ## [0.1.4] - 2026-10-02
78
+
79
+ Documentation only: a shorter README, and the reference documents it links to.
80
+
81
+ ### Changed
82
+
83
+ - The README is a quick start now, and nothing else: install, use, configure, the
84
+ sandbox in brief, troubleshooting, development, and an index of the documents. It
85
+ went from 232 lines to 124. Everything that was not part of getting the plugin
86
+ running moved to the document it belongs to — the full configuration table to
87
+ [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md), the limitations to
88
+ [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md), the file layout to
89
+ [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) (the two design rationales were
90
+ already there), and the verification list to
91
+ [`CONTRIBUTING.md`](CONTRIBUTING.md). The Chinese README was rewritten to match,
92
+ section for section, and still does.
93
+ - `docs/CONFIGURATION.md`, `docs/LIMITATIONS.md` and `docs/ARCHITECTURE.md` ship in
94
+ the tarball, so a package consumer has the configuration reference and the honest
95
+ list of limitations without the repository.
96
+
15
97
  ## [0.1.3] - 2026-10-02
16
98
 
17
99
  Repository and release engineering. No runtime behaviour changed.
package/CONTRIBUTING.md CHANGED
@@ -92,9 +92,12 @@ Choose the layer by what the test needs.
92
92
  | Unit test | `test/*.test.mjs` | pure logic: path translation, listing, shell argument building, terminal helpers |
93
93
  | Probe | `test/probe/` | behaviour that needs a real distro, a real profile, or bubblewrap |
94
94
 
95
- Unit tests run under `node --test` and import only Node builtins. Add a file named
96
- `<area>.test.mjs`, then add it to both the `test:unit` and the `test:coverage`
97
- scripts in `package.json`. Both scripts list their files explicitly.
95
+ Unit tests run under `node --test`. A test that imports only this repository goes in
96
+ both the `test:unit` and the `test:coverage` lists in `package.json`, which name
97
+ their files explicitly. A test that needs a DSH peer — `test/sandbox.test.mjs`
98
+ imports `lib/sandbox.js`, which imports `@deepseek-ai/dsh-sandbox` — goes in
99
+ `test:unit` only and must skip itself when the peer is missing, because CI installs
100
+ nothing: try the import, print a `SKIP` line, exit 0.
98
101
 
99
102
  Probes are shell scripts that mount a throwaway profile, plus the small plugin
100
103
  files they load. Those plugin files are the `*-probe.mjs` and `*-probe.yml` files
@@ -104,6 +107,48 @@ of `test/probe/run.sh`. `terminal.sh` and `sandbox-shell.sh` reuse it.
104
107
  Keep probe output idempotent. Several probes delete their scratch files at the
105
108
  start, because a leftover file from an earlier run changes the result.
106
109
 
110
+ ## What has been verified
111
+
112
+ `npm test` covers the pure modules and runs a `--check` pass over every shipped
113
+ module. It cannot import the service modules, because those need DSH peer packages
114
+ that a bare checkout does not have. Only booting the harness closes that gap, which
115
+ is why the probes exist; the archived record's §15.4 documents the five rounds of
116
+ misdiagnosis the gap once caused.
117
+
118
+ `test/probe/sandbox.sh` needs no harness: it uses the same arguments
119
+ `lib/sandbox.js` builds and asserts what bubblewrap does and does not confine,
120
+ recording the interop escape as `INFO` because a Linux sandbox cannot govern a
121
+ Windows process. The other probes boot a throwaway profile bound to the distro:
122
+
123
+ - the filesystem probe asserts the write path and the write checks — a write outside
124
+ the policy root and a write in `read-only` mode both return `FS_SANDBOX_DENIED`,
125
+ while `danger-full-access` is not checked;
126
+ - the shell probe drives all three modes through `ctx.shell` and checks the refusal
127
+ classification the tool layer returns;
128
+ - the terminal probe asserts the distro, the starting directory, and the `DSH_*`
129
+ variables forwarded through `WSLENV`.
130
+
131
+ The setup steps for the throwaway profile are in the header of `test/probe/run.sh`;
132
+ `terminal.sh` and `sandbox-shell.sh` reuse it.
133
+
134
+ On Windows 11 + WSL2 (Ubuntu 26.04) the following has been verified:
135
+
136
+ - the wiring of each service, the UNC path handling and the picker behaviour;
137
+ - the plugin mounted end to end in a real profile;
138
+ - one real model turn in a WSL-only headless profile: `write → chmod → read → edit →
139
+ execute`, with the executable bit surviving the edit;
140
+ - the terminal provider in a throwaway Web boot and in the daily GUI profile;
141
+ - the sandbox in four ways: the arguments measured inside the distro, the filesystem
142
+ write checks, the shell path (`enforcement: partial`, with refusals classified
143
+ correctly), and the daily GUI profile, where a write from the agent's own session
144
+ outside the session workspace is refused inside the distro and the following
145
+ wider-permission request succeeds.
146
+
147
+ Counts: 50 unit assertions on a bare checkout, plus 5 in `test/sandbox.test.mjs`
148
+ which need the `dsh-sandbox` peer and skip themselves without it; 19
149
+ filesystem-probe assertions; 12 shell-probe checks; 10 sandbox expectations plus
150
+ the recorded escape; and 3 terminal assertions.
151
+
107
152
  ## Commit and pull request conventions
108
153
 
109
154
  - 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 a path in **another distro**'s share; `/mnt/c` is inside this distro and is not affected. Refused with `FS_OUTSIDE_DISTRO`, which is not a sandbox denial and cannot be lifted by wider permissions |
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; a fresh `/dev` is mounted writable, so `/dev/null` and `/dev/shm` work, and nothing else does |
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