dsh-plugin-wsl-env 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,72 @@ were part of the record.
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.12.0] - 2026-10-19
20
+
21
+ The GUI file service keeps the shipped `workspace-files` row: its server-only
22
+ replacement dropped the browser `file` resource provider and broke Sidebar
23
+ previews. The shipped row now serves WSL paths through `fs-routing`. The
24
+ `wsl-shell.maskWindowsDrive` wiring fix ships with it.
25
+
26
+ ### Fixed
27
+
28
+ - **`wsl-shell.maskWindowsDrive` reaches the per-command sandbox.** The shell
29
+ executor built its `WslSandbox` without the configured key. With
30
+ `sessionSandbox: false` the per-command profile therefore left `/mnt`
31
+ visible after the operator asked for the mask; with `sessionSandbox: true`
32
+ the runtime still masked the drive, but the decorated result reported
33
+ `windowsDrive: "visible"`. The executor now passes the value, matching
34
+ `wsl-fs` and `fs-routing`.
35
+
36
+ - **Sidebar previews work: the GUI file service keeps the shipped
37
+ `workspace-files` row.** The former `workspace-files-wsl` takeover replaced
38
+ that row with a server-only subclass, which dropped the browser `file`
39
+ resource provider and made every Sidebar preview report "The file resource
40
+ service is unavailable." The shipped row now serves WSL paths through
41
+ `fs-routing`, so the server and client halves stay paired. The trade-off is
42
+ GUI-only: a listing, `stat` or page read takes roughly four to six agent round
43
+ trips instead of one, and a deep page read streams the preceding lines through
44
+ the pipe. The model's file tools, search and `@` references are unaffected.
45
+
46
+ ## [0.11.0] - 2026-10-18
47
+
48
+ The shared read agent is the native binary (A7): unconfined reads now use the
49
+ packaged static-musl agent with the shell as the fallback, cutting 16
50
+ concurrent reads from 342-614 ms to 2.4-8.2 ms. The A6 FS measurement and
51
+ metrics ship with it.
52
+
53
+ ### Added
54
+
55
+ - **The shared read agent is native (A7).** `WslAgent` gained an async
56
+ `prepare()` hook; `sharedAgent` uses it to select the packaged native binary
57
+ through `agentBackend` (`auto`/`native`), with the shell as the fallback
58
+ (`shell`, a failed preparation, or a native HELLO/runtime failure; the reads
59
+ never fail closed). The A6 probe measures 16 concurrent shared reads at
60
+ 2.4-8.2 ms, down from 342-614 ms on the shell. The native shared agent also
61
+ serves the picker, the subprocess activity probes and the non-session
62
+ command path, so those inherit EXEC concurrency.
63
+
64
+ - **`WslAgent.metrics()`.** Per-kind queue wait (sent -> ACK), execution time
65
+ (ACK -> settle), in-flight peak, rejected and cancelled counters; the
66
+ 47-check lifecycle suite covers the counters and the reset.
67
+
68
+ - **`test/probe/fs-queue.mjs`.** A live measurement of both FS agents. The A6
69
+ run showed the shared shell reads were the batch bottleneck (16 concurrent
70
+ `stat`/`read`/`list` at 342-614 ms, queue up to 583 ms) while native FS ops
71
+ execute in 0.2-0.3 ms; the A7 run measures the native shared reads at
72
+ 2.4-8.2 ms. v6's lock removal only takes 16 native writes from 7.7 ms to
73
+ ~5 ms, so it stays deferred.
74
+
75
+ ### Changed
76
+
77
+ - **The plan after the A6 measurement: A7 (native shared reads) landed, v6
78
+ deferred.** [`docs/ROADMAP.md`](docs/ROADMAP.md) records the done, next and
79
+ deferred steps; ADR 0002, LIMITATIONS and the READMEs describe the native
80
+ shared reads and the v6 deferral.
81
+
82
+ - `test/probe/fs-queue.mjs` and the README/LIMITATIONS/ROADMAP describe the
83
+ native shared reads; `test/agent-shared.test.mjs` covers the shell selection.
84
+
19
85
  ## [0.10.0] - 2026-10-18
20
86
 
21
87
  The native in-distro agent is the default session-runtime backend: `agentBackend: auto`
package/CONTRIBUTING.md CHANGED
@@ -304,7 +304,7 @@ On Windows 11 + WSL2 (Ubuntu 26.04) the following has been verified:
304
304
  outside the session workspace is refused inside the distro and the following
305
305
  wider-permission request succeeds.
306
306
 
307
- Counts: 28 unit test files under `test/` — 27 of them registered in the
307
+ Counts: 33 unit test files under `test/` — 32 of them registered in the
308
308
  `test:unit` list, see the OI-7 note above — which need the pinned packages
309
309
  installed, and over 1000 `assert` call sites across them; 19 filesystem-probe
310
310
  assertions; 15 shell-probe checks; 9 picker checks; 6 missing-executable checks;
package/README.md CHANGED
@@ -6,199 +6,179 @@
6
6
  [![npm](https://img.shields.io/npm/v/dsh-plugin-wsl-env)](https://www.npmjs.com/package/dsh-plugin-wsl-env)
7
7
  [![license](https://img.shields.io/npm/l/dsh-plugin-wsl-env)](LICENSE)
8
8
 
9
- This plugin lets [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) work inside a WSL distro. The model's commands run there. Its file tools read and write the real distro files. The folder picker can open a distro folder, and the GUI terminal opens a shell inside it.
9
+ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that runs a session inside a WSL 2 distribution on Windows.
10
10
 
11
- **Windows + WSL2 only.** One install command, no dependencies of its own. The environment is chosen per session: a session on a Windows folder keeps the normal Windows tools.
11
+ The harness process stays on Windows. Commands, file operations, the folder picker and the GUI terminal use the distro. No helper service or daemon is installed, no port is opened, and confined commands run under `bubblewrap`.
12
12
 
13
- [Install](#install) · [Using it](#using-it) · [How it compares](#how-it-compares) · [Configure](#configure) · [Recipes](#recipes) · [Architecture](#architecture) · [Sandbox](#sandbox) ·
14
- [Troubleshooting](#troubleshooting) · [Development](#development) · [Documentation](#documentation)
13
+ Windows + WSL 2 only.
15
14
 
16
- ## Install
15
+ ## Why this plugin
17
16
 
18
- Run these four commands in a Windows terminal:
17
+ A Windows coding tool that works on WSL projects has three options: run the whole tool inside the distro, connect to a helper server inside the distro, or let the plugin provide the distro environment. This plugin is the third option. One workflow covers both kinds of session, with the distro's real tools and the command sandbox intact.
19
18
 
20
- ```powershell
21
- dsh wsl --from-default-profile web --dump-config # 1. create a profile (a named set of dsh settings) from the Web template
22
- dsh plugin --profile wsl add dsh-plugin-wsl-env # 2. install the plugin and its settings
23
- dsh --profile wsl --dump-config # 3. a quick check: see the merged settings without starting the app
24
- dsh --profile wsl # 4. run
25
- ```
19
+ - **Real Linux environment.** Commands run in the distro's own login shell. Its `PATH`, `nvm`, `cargo`, `pyenv` and rc files apply. File tools read and write the distro's real files on ext4.
20
+ - **No service in the distro.** There is no helper server, no daemon and no listening port. The only persisted artifact is a content-addressed copy of the native agent in `~/.cache/dsh-wsl/agent/`.
21
+ - **Sandboxed by default.** Confined commands run under `bubblewrap` inside the distro. If `bubblewrap` is missing, the command fails; it does not run unconfined.
22
+ - **Per-session.** A session opened on a distro folder gets the distro environment. A session opened on a Windows folder keeps the normal Windows tools. Both can run in one harness process.
23
+ - **Integrated with the GUI.** The folder picker lists installed distros, the terminal opens a shell in the distro, and the model can see listening ports.
26
24
 
27
- Step 3 should print a line `# == dsh-plugin-wsl-env`. That line is the plugin's settings arriving in your profile. The plugin ships its own settings, so step 2 wires everything in automatically — there is nothing to merge by hand.
25
+ ### Why Linux, not PowerShell
28
26
 
29
- **Before the first command, give the distro bubblewrap.** Every command runs inside a bubblewrap sandbox, and most distros do not have bubblewrap preinstalled. Without it, every command fails instead of running unsandboxed:
27
+ Most coding agents learn from Linux-first material, and most open-source toolchains are built and tested on Linux first. Asking an agent to work through a Windows shell adds a translation layer on every call:
30
28
 
31
- ```powershell
32
- wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap # Debian/Ubuntu
33
- ```
29
+ - **Training prior.** Examples, CI scripts, Dockerfiles and most public answers use POSIX shells and GNU tools. A model's first instinct is `bash`, not PowerShell.
30
+ - **Toolchain support.** Node, Python, Rust, Go, C/C++ and most build systems are validated on Linux first. Windows support is often a port with different quoting, paths, line endings and file locking.
31
+ - **Shell semantics.** PowerShell and `cmd` differ from POSIX in quoting, escaping, pipelines, environment variables, exit codes and path separators. Mixing the two produces errors that look like model mistakes.
32
+ - **Environment drift.** Windows setups vary by PowerShell version, execution policy, PATH and installed tools. Linux environments are easier to reproduce and containerize.
33
+
34
+ The result is more failed commands, more retries and more time spent on syntax instead of the task. Generated code is affected too: shell snippets, build commands and CI-style one-liners are usually written for POSIX, and the distro is where they run unchanged. A real Linux login shell removes that translation layer. Modern PowerShell and Windows-native toolchains narrow the gap, but the default prior and most project scripts still assume POSIX.
34
35
 
35
- From a plugin checkout, `pnpm run bootstrap -- <distro>` checks the four tools the plugin uses (bubblewrap, ripgrep, git, inotify-tools) and prints the install command for the distro's package manager; `--install` runs it. If this step is skipped, the session reports the missing package with the exact install command when it opens.
36
+ This plugin keeps the GUI on Windows and moves only the execution environment into the distro, so the agent gets the shell that its training and the toolchain both assume.
36
37
 
37
- **The shipped preset also needs systemd.** Commands run in one systemd user scope per session (background dev servers survive, `/tmp` is shared, `ps` sees the session). Set `[boot] systemd=true` in `/etc/wsl.conf`, restart WSL, and enable lingering once: `loginctl enable-linger "$USER"`. The session refuses with those remedies if any piece is missing; `sessionSandbox: false` on the preset's `wsl-shell` restores the per-command sandbox.
38
+ ## How it compares
38
39
 
39
- Then open a folder like `\\wsl.localhost\<distro>\...` in the GUI. The folder picker lists every installed distro, and **New terminal** opens a shell inside the distro.
40
+ | | Desktop remote (VS Code, ZCode) | This plugin |
41
+ |---|---|---|
42
+ | Inside the distro | a helper server | one cached agent binary |
43
+ | Network | a listening port | none |
44
+ | Command sandbox | none | `bubblewrap` |
45
+ | Removal | delete the server | delete the cache |
40
46
 
41
- Uninstall: `dsh plugin --profile wsl remove dsh-plugin-wsl-env`. Upgrade: run the same `add` command again.
47
+ A desktop remote reconnects faster and is more mature. A CLI agent installed inside WSL is simpler if it covers your work. This plugin is for the case where the harness stays on Windows and commands must be sandboxed.
42
48
 
43
- > **Changed anything under `lib/`? Sync the Windows-side copy, then restart the app.** The app loads the Windows-side copy of the code, so run `pnpm run sync:windows` first. A restart without the sync just reloads the old code, and a running process also caches modules. See [Development](#development).
49
+ ## Requirements
44
50
 
45
- ## Using it
51
+ - Windows 10 or 11 with WSL 2 and at least one distro.
52
+ - `bubblewrap` in the distro. It is required.
53
+ - systemd in the distro for the default session sandbox: set `[boot] systemd=true` in `/etc/wsl.conf`, restart WSL, then run `loginctl enable-linger "$USER"` once. Without systemd, set `sessionSandbox: false` on both `wsl-shell` and `wsl-fs`.
54
+ - Optional: `ripgrep` for search, `git` for change snapshots, `inotify-tools` for file watching.
46
55
 
47
- Open a distro folder as the workspace — for example `\\wsl.localhost\ubuntu\home\you\project` — and ask the model "what kernel am I on, and what is in `/etc/os-release`?". It runs `uname -r` and reads that file inside the distro. Nothing is copied, and nothing crosses `/mnt/c`.
56
+ No Node.js is required inside the distro.
48
57
 
49
- - **A distro folder gets the distro environment.** When a session opens a distro folder, it is automatically given the WSL environment. The very first command is already right.
50
- - **Commands run where you expect.** They run inside the distro, in your own login shell — so your `PATH`, `nvm`, `cargo`, `pyenv` and rc files all apply. The shell comes from the distro's configuration; it is not hardcoded to bash.
51
- - **Files are the real files.** `/home/you/x` and `\\wsl.localhost\ubuntu\home\you\x` are the same file. `/mnt/c/...` reaches the Windows disk as usual.
52
- - **The terminal too.** Right sidebar, *New terminal*: a shell inside the distro, in the session's folder.
53
- - **Port visibility.** The model sees which ports are listening inside the distro (refreshed about every 10 seconds) and can return the exact URL of a dev server it starts. WSL2 forwards localhost to Windows, so the URL opens in the browser directly.
54
- - **Permissions work like on a Linux host.** The Permissions selector offers `read-only`, `workspace-write` (the default) and `danger-full-access`. When something is refused, the model is offered one retry with the smallest permission that would work. Only `danger-full-access` runs without the sandbox.
58
+ ## Install
55
59
 
56
- ## How it compares
60
+ Run these commands in a Windows terminal:
57
61
 
58
- Three ways for a Windows coding tool to work on a WSL project:
62
+ ```powershell
63
+ dsh wsl --from-default-profile web --dump-config # create the profile
64
+ dsh plugin --profile wsl add dsh-plugin-wsl-env # install the plugin and its settings
65
+ dsh --profile wsl --dump-config # verify: prints "# == dsh-plugin-wsl-env"
66
+ dsh --profile wsl # start
67
+ ```
59
68
 
60
- 1. **Install the tool inside WSL.** Codex CLI, Claude Code and ZCode CLI all recommend this, and they are right — it is the simplest path when a CLI tool covers your work. It needs the whole tool to live inside WSL. DeepSeek Harness cannot, so it needs one of the next two.
61
- 2. **Connect from a desktop app.** VS Code (Remote-WSL, and its open-source mirror [open-remote-wsl](https://github.com/jeanp413/open-remote-wsl)) and agent desktops like [ZCode](https://github.com/zai-org/ZCode) keep the app on Windows and set up a helper server inside the distro. Battle-tested. The cost: a server folder (`~/.vscode-server`, `~/.zcode/server`), a port, and no sandbox for commands.
62
- 3. **This plugin.** The harness stays on Windows. Nothing is installed in the distro, no port is opened, and commands run inside a bubblewrap sandbox. If something is missing, the session says what to install the moment it opens.
69
+ Install `bubblewrap` before the first command:
63
70
 
64
- | | Option 2 (desktop remote) | Option 3 (this plugin) |
65
- |---|---|---|
66
- | installed in the distro | a server folder | nothing |
67
- | opens a port | yes | no |
68
- | command sandbox | none | bubblewrap + systemd session scope |
69
- | after uninstall | remove the server manually | nothing left |
70
- | after a WSL upgrade | server may need repair | the next command recovers automatically |
71
+ ```powershell
72
+ wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap
73
+ ```
71
74
 
72
- **Where the others win.** A desktop remote re-opens a workspace instantly — its server stays warm — and VS Code's remote has years of hardening behind it. A CLI inside WSL needs none of this. This plugin is for one case: DeepSeek Harness on Windows, working on WSL projects, with commands sandboxed.
75
+ From a checkout, `pnpm run bootstrap -- <distro>` checks the four distro tools and prints the install command for the distro's package manager. Add `--install` to run it.
73
76
 
74
- ## Configure
77
+ Upgrade by running the `add` command again. Uninstall with `dsh plugin --profile wsl remove dsh-plugin-wsl-env`; the agent cache under `~/.cache/dsh-wsl` can then be deleted. If you added a local `workspace-files-wsl` workaround to your profile to restore previews, remove it after upgrading: the plugin no longer mounts that row.
75
78
 
76
- Each setting lives on a named row. To change one, add a row with the same id to `$DSH_HOME/profiles/<name>/cordis.patch.yml` (`$DSH_HOME` is DSH's settings folder). The keys worth knowing:
79
+ ## Using it
77
80
 
78
- | Row | Key | Default | Meaning |
79
- |---|---|---|---|
80
- | `wsl-shell` | `distro` | `''` | which distro to use; empty means WSL's default distro |
81
- | | `sandbox` | `true` | run commands inside the `bubblewrap` sandbox; `false` turns it off |
82
- | | `sessionSandbox` | `true` | run the session's commands in one persistent systemd user scope: background dev servers survive, `/tmp` is shared, `ps` sees the session. Needs systemd as PID 1, a reachable user manager and `Linger`; the preflight refuses with the remedy. `false` restores the per-command sandbox |
83
- | | `agentBackend` | `auto` | the session runtime's agent: `auto` uses the native in-distro agent when a verified source exists (packaged static-musl binary at `agent/native/linux-<arch>/dsh-wsl-agent` (x86_64/aarch64) or the `DSH_WSL_AGENT_NATIVE` override), else the shell agent; `native` refuses instead of falling back; `shell` forces the shell agent. Covers commands and confined writes. The native backend is the default; the shell agent is the fallback |
84
- | `wsl-fs` | `distro` | `''` | same as above |
85
- | | `restrictToDistro` | `true` | refuse paths that belong to a **different** distro. (`/mnt/c` belongs to this distro, so it is not affected.) The refusal is `FS_OUTSIDE_DISTRO`; wider permissions cannot lift it |
86
- | | `sandbox` | `true` | check file writes against the same policy |
87
- | | `sessionSandbox` | `true` | run confined file writes in the same session runtime as commands (one real `/tmp`, one workspace mount view); needs the same systemd/linger setup; `false` uses the per-policy confined resident |
88
- | | `substrate` | `agent` | how file operations reach the distro. Only one value remains: the in-distro agent, so reads and writes run on the distro's own filesystem, with native symlinks and permissions. The former `"share"` option used the 9p share and is rejected at startup |
89
- | `directory-picker-wsl` | `includeHostHome` | `true` | also list the Windows home directory in the picker |
90
- | `subprocess-wsl` | `distro` | `''` | which distro the GUI terminal opens in |
81
+ Open a distro folder as the workspace, for example `\\wsl.localhost\ubuntu\home\you\project`.
91
82
 
92
- `wsl-shell` and `wsl-fs` cannot be reached by adding `- id: wsl-shell` to your override file: they live inside the `wsl` preset's own settings (a preset is a named environment DSH can give a session), so the loader only warns `patch: entry "wsl-shell" not found` and the shipped value stays in force. To change them, copy and override the whole `preset-wsl` row — [docs/CONFIGURATION.md](docs/CONFIGURATION.md#overriding-the-rows-inside-preset-wsl) has the recipe.
83
+ - Commands run in the distro's login shell. `/home/you/x` and `\\wsl.localhost\ubuntu\home\you\x` are the same file.
84
+ - **New terminal** opens a shell in the same distro and directory.
85
+ - The model sees listening ports and can return the URL of a dev server. WSL 2 forwards localhost to Windows.
86
+ - The Permissions selector offers `read-only`, `workspace-write` (default) and `danger-full-access`. Only `danger-full-access` runs without the sandbox.
93
87
 
94
- [`cordis.patch.yml`](cordis.patch.yml) is the commented reference for every value the plugin ships. [docs/CONFIGURATION.md](docs/CONFIGURATION.md) lists the rest — `shell`, `loginShell`, `cwd`, `timeoutMs`, `preferredDistro`, `maxEntries` — and [examples/profile.cordis.patch.yml](examples/profile.cordis.patch.yml) is a settings file you can copy and edit.
88
+ ## Configure
95
89
 
96
- ## Recipes
90
+ Settings live on named rows in `$DSH_HOME/profiles/<name>/cordis.patch.yml`. The main values:
97
91
 
98
- - **Use your Windows git credentials inside the distro**: `git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"` (adjust the path to where your Windows Git is installed). After this, `git push` inside the distro uses the same saved credentials as Windows.
99
- - **Keep projects on the distro's own disk.** The model works on Linux paths (`/home/...`) on the distro's own disk, which is fast. It can still reach Windows files through `/mnt/c`, but that bridge is slow when many small files are involved. `pnpm run bootstrap -- <distro>` tells you whether ripgrep, git and inotifywait — used for search and file watching — are installed.
100
- - **Environment variables**: WSL only forwards the variables named in `WSLENV`. This plugin forwards its own `DSH_*` values, and translates the two that hold Windows paths (`DSH_HOME`, `DSH_PROFILE_DIR`) into Linux paths. Your `PATH` is never forwarded — forwarding it would hide the distro's own PATH.
92
+ | Row | Key | Default | Purpose |
93
+ |---|---|---|---|
94
+ | `wsl-shell` | `distro` | `''` | distro to use; empty means the WSL default |
95
+ | | `sandbox` | `true` | confine commands with `bubblewrap` |
96
+ | | `sessionSandbox` | `true` | run the session's commands in one systemd user scope |
97
+ | | `agentBackend` | `auto` | `auto`, `native` or `shell` for the in-distro agent |
98
+ | | `maskWindowsDrive` | `false` | hide `/mnt` from confined commands |
99
+ | `wsl-fs` | `distro` | `''` | distro for file operations |
100
+ | | `restrictToDistro` | `true` | refuse paths that belong to another distro |
101
+ | | `sandbox` | `true` | check confined writes against the policy |
102
+ | | `sessionSandbox` | `true` | run confined writes in the session runtime |
103
+ | | `substrate` | `agent` | in-distro agent; `share` is rejected at startup |
104
+ | `directory-picker-wsl` | `includeHostHome` | `true` | also list the Windows home directory |
105
+ | `subprocess-wsl` | `distro` | `''` | distro used by the GUI terminal |
106
+
107
+ `wsl-shell` and `wsl-fs` are inside the shipped `wsl` preset, so overriding them by id does not work. Override the whole `preset-wsl` row instead; [docs/CONFIGURATION.md](docs/CONFIGURATION.md) has the recipe and the remaining keys. [`cordis.patch.yml`](cordis.patch.yml) is the commented reference for every shipped value.
101
108
 
102
109
  ## Architecture
103
110
 
104
- One DSH process can serve both kinds of session at the same time: a session on a Windows folder, and a session inside the distro. The plugin's parts attach at two levels, because some things belong to one session and some belong to the whole app:
111
+ One harness process can serve Windows-folder and distro-folder sessions at the same time. Services attach at two levels:
105
112
 
106
113
  ```text
107
- composition (app level, one per process)
108
- ├─ subprocess-wsl the GUI terminal's execution world
109
- │ WSL-folder session → the distro shell, in the session's Linux directory
110
- │ Windows-folder session → powershell.exe, in the session's Windows directory
111
- ├─ directory-picker-wsl installed distros listed beside the Windows home
112
- ├─ workspace-files-wsl the GUI file tree and previews of a distro workspace, served from inside it
113
- ├─ fs-routing the root filesystem, routed by coordinate
114
- │ distro UNC → the resident agent; a drive path → the host backend
115
- ├─ wsl-shell-env the DSH_WSL_DISTRO / _SHELL / _HOME / _PORTS facts the model sees
116
- └─ auto-preset binds the wsl preset when a session opens a distro folder
117
-
118
- preset-wsl (the wsl agent preset; its settings are separate from the app's)
119
- ├─ wsl-shell ctx.shell — wsl.exe --exec <login shell>, confined by bubblewrap inside the distro
120
- └─ wsl-fs ctx.fs — real distro files on the resident agent substrate (ext4)
114
+ app level (one per process)
115
+ ├─ directory-picker-wsl, workspace-files, subprocess-wsl
116
+ ├─ fs-routing: root filesystem routed by path
117
+ └─ auto-preset: binds the wsl preset when a session opens a distro folder
118
+
119
+ wsl preset (per session)
120
+ ├─ wsl-shell: commands
121
+ └─ wsl-fs: file operations
121
122
  ```
122
123
 
123
- **Why two levels.** The `wsl` preset holds `wsl-shell` and `wsl-fs`, the two things that differ per session. When a session opens a distro folder, `auto-preset` gives that session the preset; a Windows-folder session keeps the normal Windows tools. One environment per session, in one process. The GUI terminal is the exception: it reads its shell from the app level, which never sees a preset, so `subprocess-wsl` is mounted at the app level. The GUI file tree and the root filesystem have the same shape of problem — every session uses them, so no single preset can own them — and `workspace-files-wsl` and `fs-routing` sit at the app level for that reason. They replace two shipped rows; [docs/root-fs-routing.md](docs/root-fs-routing.md) has the routing design.
124
-
125
- **How a command runs.** A session gets one resident agent inside a systemd user scope. By default that agent is the native `dsh-wsl-agent` — the packaged static-musl binary for the distro's architecture, selected by `agentBackend: auto`; the runtime's shell agent is the fallback when the binary is missing or fails its copy/digest/exec check, and `agentBackend: shell` forces it while `native` refuses instead of falling back. The agent executes each command in your login shell under a bubblewrap mount profile. Because the scope outlives the command, a background dev server survives, `/tmp` is the distro's real `/tmp`, and `ps` sees the whole session. With `sessionSandbox: false`, the agent stays per distro, each command gets its own bwrap profile (`--unshare-pid`, a fresh `/tmp`), background processes do not survive, and the native backend is not used. If the agent is unavailable (or you set `agent: false`), the one-shot `wsl.exe` path runs the command instead — same result, slightly slower, and per-command isolation.
126
-
127
- **How file operations run.** Reads, writes and searches also happen inside the distro, on its own filesystem, with the distro's own tools. A search over a distro workspace runs the distro's `rg`, never the Windows binary over the 9p share. File writes are checked against the same policy the command sandbox enforces; with `wsl-fs` `sessionSandbox` on (the shipped preset), a confined write runs in the same session runtime as the commands — the same native/shell agent — so it sees the same `/tmp` and workspace mounts. Reads use the shared shell agent.
124
+ - `wsl-shell` runs commands through one resident agent inside a systemd user scope. The agent is the packaged static-musl `dsh-wsl-agent` for the distro's architecture (`x86_64` or `aarch64`), with the shell agent as fallback. Each command runs in the login shell under a `bubblewrap` mount profile. The scope outlives the command, so background servers survive and `ps` sees the session. With `sessionSandbox: false`, the agent stays per distro and each command gets its own profile.
125
+ - `wsl-fs` serves reads and writes through the same in-distro agent. Confined writes run in a long-lived agent whose mount profile matches the policy, so the kernel enforces the write boundary. There is no host-side file fallback: if the agent is unavailable, file operations fail.
126
+ - The GUI file tree and previews are the shipped `workspace-files` row, not a plugin replacement: its client half is the browser `file` provider. It reads WSL paths through the routed root filesystem, so it stays distro-side, at the cost of a few extra agent round trips.
127
+ - App-level services route by path. A distro UNC path goes to the distro; a Windows drive path stays on the host. See [docs/root-fs-routing.md](docs/root-fs-routing.md).
128
128
 
129
129
  ## Sandbox
130
130
 
131
- Commands run inside a `bubblewrap` sandbox in the distro, and file writes are checked against the same policy. The Windows sandbox cannot be used here: its restricted account cannot reach WSL at all.
132
-
133
- | Mode | What a command inside the distro can do |
131
+ | Mode | What a command can do |
134
132
  |---|---|
135
- | `read-only` | read the whole distro. A fresh `/dev` is mounted so `/dev/null` and `/dev/shm` work; nothing else is writable |
136
- | `workspace-write` | all of the above, plus the session workspace is writable and `/tmp` is the distro's real `/tmp` (with `sessionSandbox: false`, the per-command profile mounts a fresh tmpfs there instead) |
137
- | `danger-full-access` | no sandbox; used when you approve a wider-permission request |
133
+ | `read-only` | read the whole distro; a fresh `/dev` is mounted, nothing else is writable |
134
+ | `workspace-write` | the above, plus the session workspace; `/tmp` is the distro's real `/tmp` with the session runtime |
135
+ | `danger-full-access` | no sandbox |
138
136
 
139
- **bubblewrap is required, and the plugin fails closed.** Without it, every confined command reports `SANDBOX_UNAVAILABLE` — the command does not run unsandboxed. To turn the sandbox off, set `sandbox: false` on the row that owns the operation: `wsl-shell` for commands, `wsl-fs` for file writes, and the top-level `fs-routing` row for writes to the app's own filesystem. (The two preset rows are reached through `preset-wsl` — see [Configure](#configure).) The model is then told that these operations have no sandbox.
137
+ With `sessionSandbox: false`, each command gets a private `/tmp` instead of the distro's real one.
140
138
 
141
- **Known limits.** A command inside the distro can still start a Windows program (anything under `/mnt/c/.../*.exe`), and bubblewrap does not watch Windows programs. `pnpm run probe:sandbox` demonstrates this boundary on your machine. Setting `maskWindowsDrive: true` on the three rows narrows it: `/mnt` disappears from the command's view, so the drive's files cannot be read and its programs cannot be started. It does not close the hole completely — a command could still copy a Windows program into the workspace and run it — so the report stays `partial`. The only complete fix is in the distro itself: `[interop] enabled=false` in `wsl.conf` (see [docs/CONFIGURATION.md](docs/CONFIGURATION.md)).
139
+ `bubblewrap` is required. If it is missing or broken, confined commands report `SANDBOX_UNAVAILABLE` and do not run. Setting `sandbox: false` on the owning row disables confinement; the model is told that the operation is unsandboxed.
142
140
 
143
- See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) for the design, and [docs/LIMITATIONS.md](docs/LIMITATIONS.md) for everything the plugin does not do.
141
+ Enforcement is reported as `partial`. A command inside the distro can still start a Windows executable through `/mnt/c`, and `bubblewrap` cannot control Windows processes. `maskWindowsDrive: true` hides `/mnt` from the confined view, which narrows the hole but does not close it. The complete fix is distro-level: set `[interop] enabled=false` in `/etc/wsl.conf`. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) and [SECURITY.md](SECURITY.md).
144
142
 
145
143
  ## Troubleshooting
146
144
 
147
- | Symptom | Cause | Fix |
148
- |---|---|---|
149
- | every command reports `SANDBOX_UNAVAILABLE` | `bubblewrap` is missing from the distro, or present but broken — the error text tells you which one it is and what to do | follow the fix in the error, or set `sandbox: false` on `wsl-shell` (commands) and `wsl-fs` (writes) — see [Configure](#configure) for how to reach those rows |
150
- | a command or write is refused outside the session folder | expected: `workspace-write` only allows writes in the session folder | accept the wider-permission offer, or open the session on the folder you need |
151
- | writes are refused even inside the workspace | the session is in `read-only` mode | switch the Permissions selector |
152
- | `dsh plugin add` prints no confirmation that anything was added | the plugin is already installed, so there is nothing new to report — this is success, not failure | nothing to do: the settings are already in place, and `dsh --profile wsl --dump-config` still shows `# == dsh-plugin-wsl-env` |
153
- | the terminal still opens `cmd.exe` | the `terminal-controller` row from the plugin's settings did not apply | check that `dsh --profile wsl --dump-config` shows `shell: { path: wsl.exe, name: WSL }` |
154
- | changes to `lib/` have no effect | the app runs a Windows-side copy of the code, and a running process also caches modules | run `pnpm run sync:windows` to update the copy, then restart the app |
155
- | `link:\\wsl.localhost\...` leaves a broken symlink | pnpm cannot link a UNC path | link a Windows path instead; developing inside the distro needs the Windows-side copy, see [Development](#development) |
156
- | `glob` and `grep` are slow | searches inside the distro need `rg` installed in the distro; Windows-folder searches are unaffected | run `pnpm run bootstrap -- <distro> --install` (installs ripgrep); distro searches always run the distro's rg — they never cross the slow bridge |
157
- | the terminal reports `unknown` activity | only while the in-distro agent is temporarily unavailable — distro terminals are watched from inside the distro (a `DSH_TERMINAL_ID` marker scanned in `/proc`: a shell alone is `idle`, a shell running anything is `busy`) | check the distro is running; idle terminals are closed automatically after the controller's idle timeout (2 h by default), and `terminalIdleReclaim: false` turns the auto-close off |
158
- | a result names an `FS_*` code | the code says what refused it, and what clears it | see the error-code table in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#error-codes) |
145
+ | Symptom | Fix |
146
+ |---|---|
147
+ | every command reports `SANDBOX_UNAVAILABLE` | install `bubblewrap` in the distro, or set `sandbox: false` |
148
+ | the session refuses before the first command | enable systemd and linger, or set `sessionSandbox: false` on `wsl-shell` and `wsl-fs` |
149
+ | `glob` and `grep` are slow in the distro | install `ripgrep` with `pnpm run bootstrap -- <distro> --install` |
150
+ | `dsh plugin add` prints nothing | the plugin is already installed; this is success |
151
+ | changes under `lib/` have no effect | run `pnpm run sync:windows`, then restart the app |
152
+
153
+ More error codes and refusals are in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#error-codes). Everything the plugin does not do is in [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
159
154
 
160
155
  ## Development
161
156
 
162
157
  ```bash
163
- pnpm run build # compile src/ into lib/ (the built files are committed; rebuild after editing src/)
164
- pnpm test # style checks, syntax pass, build consistency, type check, and unit tests
165
- pnpm run sync:windows # copy the checkout onto the Windows-side copy the app loads (after any lib/ change, then restart the app)
166
- pnpm run test:coverage # the unit tests with coverage thresholds (Node 22.8+)
167
- pnpm run diagnose # read-only diagnostic report for a bug issue: versions, tools, bwrap probe
168
- pnpm run bootstrap -- <distro> # check the four distro tools; add --install to install what is missing
169
- pnpm run probe:sandbox # measure inside the distro what bubblewrap does and does not confine
170
- pnpm run probe # filesystem probe against a real distro (Windows + WSL only)
171
- pnpm run probe:sandbox-shell # boot a real harness and drive the confined executor
172
- pnpm run probe:terminal # open a PTY through the terminal provider
173
- pnpm run probe:substrate # drive the agent filesystem over a real wsl.exe transport (from inside the distro)
174
- pnpm run probe:watch # arm the in-distro watcher over a real directory (from inside the distro)
175
- pnpm run probe:agent # compare the resident path against the fallback path (from inside the distro)
176
- pnpm run probe:exec # the agent-backed execution: timeout, kill, cwd failure (from inside the distro)
177
- pnpm run probe:missing-wsl # boot a profile whose wslPath cannot start (Windows + WSL only)
178
- pnpm run probe:picker # list the picker's root level, refusals and its cap (Windows + WSL only)
179
- pnpm run probe:mode # which POSIX-mode facts survive the share (needs a Windows node and an installed harness; opens no profile)
180
- pnpm run probe:sandbox-off # prove sandbox: false unconfines both providers (Windows + WSL only)
158
+ pnpm install # development dependencies
159
+ pnpm run build # compile src/ into lib/ (lib/ is committed)
160
+ pnpm test # style, syntax, build consistency, types, unit tests
161
+ pnpm run test:coverage # unit tests with coverage thresholds
162
+ pnpm run sync:windows # update the Windows-side copy the app loads
181
163
  ```
182
164
 
183
- The app runs a Windows-side copy of the code, not your checkout. After changing anything under `lib/`, run `pnpm run sync:windows` before restarting the app — otherwise the restart just loads the old code. `pnpm run build` regenerates `lib/` from `src/`; the built files are committed, and `pnpm test` checks that they match.
184
-
185
- Run `pnpm install` once to get the development dependencies. The tests use the pinned `@deepseek-ai/*` packages; the plugin itself ships none. CI runs the tests on Node 22 and 24, on Linux and Windows, and coverage on Node 24.
165
+ The toolchain follows the harness host: Node `^22.19.0 || >=24.0.0`. `src/` is TypeScript; `lib/` is generated and must stay in sync. The app loads a Windows-side copy of `lib/`, so run `sync:windows` and restart the app after a build.
186
166
 
187
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) has the file layout, how the parts attach, and the sandbox design. [CONTRIBUTING.md](CONTRIBUTING.md) has the development loop, the Windows-side copy, the full gate list, and what has been verified.
167
+ Real-machine probes run on Windows + WSL only. See [CONTRIBUTING.md](CONTRIBUTING.md) and `test/probe/`.
188
168
 
189
169
  ## Documentation
190
170
 
191
- | Document | What it covers |
171
+ | Document | Content |
192
172
  |---|---|
193
- | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | every configuration key, and how to override it |
194
- | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | the plugin's parts and how they attach, path coordinates, the sandbox, testing, file layout |
195
- | [docs/LIMITATIONS.md](docs/LIMITATIONS.md) | what the plugin does not do, and why |
196
- | [docs/VALUE.zh.md](docs/VALUE.zh.md) | the plugin's value in Chinese: what it is worth, what it costs, and when not to use it |
197
- | [CONTRIBUTING.md](CONTRIBUTING.md) | the development loop, gates, conventions, verification |
198
- | [SECURITY.md](SECURITY.md) | reporting a vulnerability privately |
199
- | [SUPPORT.md](SUPPORT.md) | supported versions, and where to ask |
200
- | [docs/RELEASING.md](docs/RELEASING.md) | the release checklist |
201
- | [docs/archive/README.en.md](docs/archive/README.en.md) | English index of the archived engineering record |
173
+ | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | every configuration key and how to override it |
174
+ | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | services, path routing, sandbox, error codes |
175
+ | [docs/LIMITATIONS.md](docs/LIMITATIONS.md) | what the plugin does not do |
176
+ | [docs/ROADMAP.md](docs/ROADMAP.md) | current plan and deferred work |
177
+ | [docs/VALUE.zh.md](docs/VALUE.zh.md) | value and trade-offs in Chinese |
178
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | development loop, gates, probes |
179
+ | [SECURITY.md](SECURITY.md) | private vulnerability reporting |
180
+ | [SUPPORT.md](SUPPORT.md) | supported versions and support channels |
181
+ | [docs/RELEASING.md](docs/RELEASING.md) | release checklist |
202
182
 
203
183
  ## License
204
184
 
package/cordis.patch.yml CHANGED
@@ -104,25 +104,24 @@
104
104
  name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
105
105
 
106
106
  # ═════════════════════════════════════════════════════════════════════════════
107
- # The distro-side takeovers of the two root-plane services.
107
+ # The distro-side takeover of the root-plane filesystem.
108
108
  #
109
109
  # A patch layer is a flat list: an `id:` entry patches the shipped row, and a
110
110
  # bare `insert:` list appends new rows as data. A disable written INSIDE an
111
111
  # insert list is not a patch — it becomes a duplicate-id data row that shadows
112
112
  # the real service row (the loader keys rows by id, last occurrence wins), and
113
- # a nested `insert:` list is never interpreted at all. Both takeovers are
113
+ # a nested `insert:` list is never interpreted at all. This takeover is
114
114
  # therefore two top-level entries: disable the shipped row by id, then append
115
115
  # the replacement with a bare insert. test/composition.test.mjs runs this file
116
116
  # through the real patch algorithm to keep the shape honest.
117
117
  # ═════════════════════════════════════════════════════════════════════════════
118
118
 
119
- # The GUI file tree and previews for a distro workspace are served from inside
120
- # the distro (resident agent) instead of Windows-side 9P walks. The variant
121
- # extends the shipped service — a distro UNC routes distro-side, a drive path
122
- # calls super (host native), the watch stream delegates unchanged — so
123
- # replacing the row is safe for Windows-folder sessions too.
124
- - id: workspace-files
125
- disabled: true
119
+ # The GUI file service keeps the shipped `workspace-files` row: its client half
120
+ # is the browser `file` resource provider the Sidebar previews need, and a
121
+ # server-only replacement drops it (docs/LIMITATIONS.md). The shipped service
122
+ # reads WSL paths through the routed root filesystem below, so the GUI file tree
123
+ # and previews still run inside the distro, at the cost of a few extra agent
124
+ # round trips.
126
125
 
127
126
  # The root-plane filesystem routes by coordinate: distro UNC identities are
128
127
  # served by the resident agent (real watch events, ext4 reads), drive paths
@@ -133,14 +132,6 @@
133
132
  disabled: true
134
133
 
135
134
  - insert:
136
- - id: workspace-files-wsl
137
- name: 'dsh-plugin-wsl-env/workspace-files-wsl'
138
- config:
139
- maxBytes: 2097152
140
- maxFileBytes: 33554432
141
- maxLines: 5000
142
- maxEntries: 2000
143
-
144
135
  - id: fs-routing
145
136
  name: 'dsh-plugin-wsl-env/fs-routing'
146
137
  config:
@@ -26,19 +26,25 @@ root-plane services, and adds three smaller integrations.
26
26
  extends the shipped `LocalFileSystem`. Only the parts that need a distro are
27
27
  overridden.
28
28
 
29
- ## The two root-plane takeovers
29
+ ## The root-plane filesystem takeover
30
30
 
31
- Two of the shipped services are session-less — the GUI file tree and the root
32
- `ctx.fs` — so they cannot be served from an agent preset. The layer replaces
33
- their shipped rows outright, with subclasses that route by coordinate:
31
+ One of the shipped services is session-less — the root `ctx.fs` — so it cannot
32
+ be served from an agent preset. The layer replaces its shipped row outright with
33
+ a subclass that routes by coordinate:
34
34
 
35
35
  | Service | Class | File | What it does |
36
36
  |---|---|---|---|
37
- | `ctx.workspaceFiles` | `WorkspaceFilesWsl` | [`lib/workspace-files-wsl.js`](../lib/workspace-files-wsl.js) | The GUI file tree and previews for a `\\wsl.localhost\<distro>` workspace, served from inside the distro over the resident agent instead of Windows-side 9P walks. Extends the shipped service: a distro UNC routes distro-side, a drive path calls `super`, and the watch stream delegates unchanged — so replacing the row is safe for Windows-folder sessions too. Replaces the shipped `workspace-files` row. |
38
37
  | `ctx.fs` (root plane) | `WslRoutingFileSystem` | [`lib/fs-routing.js`](../lib/fs-routing.js) | The root filesystem, routed by coordinate: a distro UNC identity is served by the resident agent (real watch events, ext4 reads), a drive path stays host-native. Replaces the shipped `fs-sandbox` row, whose write fence has no root-plane consumers — the model's writes ride the preset filesystems. [`root-fs-routing.md`](root-fs-routing.md) is the design. |
39
38
 
40
- Both routes are composition-level for the mirror-image reason the terminal is
41
- (see below): no session identity reaches them. Each takeover is a top-level
39
+ The GUI file tree and previews keep the shipped `workspace-files` row. Its
40
+ client half is the browser `file` resource provider the Sidebar previews need,
41
+ so replacing the row with a server-only subclass would drop it; the shipped
42
+ service reads WSL paths through the routed root filesystem instead. The cost is
43
+ extra `ctx.fs` round trips per listing, `stat` or page read (see
44
+ [LIMITATIONS](LIMITATIONS.md)).
45
+
46
+ The takeover is composition-level for the mirror-image reason the terminal is
47
+ (see below): no session identity reaches the root filesystem. It is a top-level
42
48
  `disabled:` entry targeting the shipped row plus a bare `insert:` list holding
43
49
  the replacement — because a `disabled` written *inside* an insert list is not a
44
50
  patch but a duplicate-id data row, and a nested `insert:` is never interpreted at
@@ -79,7 +85,7 @@ The environment is per session, not per process:
79
85
  |---|---|---|
80
86
  | `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 |
81
87
  | `subprocess-wsl` | at the app level, called the composition; the shipped `subprocess` row is disabled | because no session identity reaches the terminal controller, see below |
82
- | `workspace-files-wsl` | at the app level; the shipped `workspace-files` row is disabled | the GUI file tree is session-less, so it cannot be served from a preset |
88
+ | `workspace-files` | shipped, not replaced | its client half is the browser `file` resource provider the Sidebar previews need; it reads WSL paths through the routed root filesystem |
83
89
  | `fs-routing` | at the app level; the shipped `fs-sandbox` row is disabled | the root `ctx.fs` is session-less, and its write fence has no root-plane consumers |
84
90
  | `directory-picker-wsl`, `wsl-shell-env`, `auto-preset` | at the app level | they answer questions that are not scoped to a session |
85
91
 
@@ -282,7 +288,8 @@ failures are named as well. Every `FsError` code the plugin itself raises appear
282
288
  below — a model sees the code, and a reader asking what refused them finds it here.
283
289
  Codes the shipped host stack raises behind a drive path pass through
284
290
  `lib/fs-routing.js` unchanged and are the peer's, not this table's; the GUI file
285
- tree's own wire refusals are a separate dialect, listed after the table.
291
+ service is the shipped `workspace-files` row, so its wire refusals are the peer's
292
+ own dialect, described after the table.
286
293
 
287
294
  | Code | Raised by | Meaning | What clears it |
288
295
  |---|---|---|---|
@@ -301,14 +308,11 @@ tree's own wire refusals are a separate dialect, listed after the table.
301
308
  | `FS_EDIT_NOT_FOUND` | `lib/fsio-text.js` (`applyLiteralEdit`) | `old_string` was empty, or matched nothing in the file | give an `old_string` the file actually contains |
302
309
  | `FS_AMBIGUOUS_EDIT` | `lib/fsio-text.js` (`applyLiteralEdit`) | `old_string` matched more than once and `replace_all` was not set | make `old_string` unique, or set `replace_all` |
303
310
 
304
- The GUI file tree speaks a second dialect. `WorkspaceFilesWsl` refuses through the
305
- typert wire's `RemoteError`s, not `FsError`s, so its codes are not in the table
306
- above: `workspace-file/not-found`, `workspace-file/outside-workspace`,
307
- `workspace-file/not-directory`, `workspace-file/not-regular-file`,
308
- `workspace-file/not-text`, and `workspace-file/too-large` (one code for both the
309
- full-file cap and the byte-window cap), plus `gateway/bad-request` for a malformed
310
- page request (a non-integer offset, or a limit above `maxLines`). A drive-path
311
- workspace calls the shipped service and gets the peer's own answers instead.
311
+ The GUI file service keeps the shipped `workspace-files` row, so its wire
312
+ refusals are the peer's `workspace-file/*` dialect (plus `gateway/bad-request`
313
+ for a malformed page request), not this plugin's `FsError` codes. The plugin
314
+ supplies the routed `ctx.fs`; an agent outage reaches the service as
315
+ `FS_IO_ERROR` and the peer maps it.
312
316
 
313
317
  Two failures come from `wsl.exe` itself rather than from a code of ours, and both used
314
318
  to be invisible:
@@ -361,9 +365,8 @@ waits for the app to exit and then runs the whole real-machine suite once —
361
365
  The unit tests come in two kinds: most import only Node builtins, while
362
366
  [`test/composition.test.mjs`](../test/composition.test.mjs),
363
367
  [`test/fs.boot.test.mjs`](../test/fs.boot.test.mjs),
364
- [`test/fsio-agent.test.mjs`](../test/fsio-agent.test.mjs),
365
- [`test/provider.test.mjs`](../test/provider.test.mjs) and
366
- [`test/workspace-files-wsl.test.mjs`](../test/workspace-files-wsl.test.mjs) import
368
+ [`test/fsio-agent.test.mjs`](../test/fsio-agent.test.mjs) and
369
+ [`test/provider.test.mjs`](../test/provider.test.mjs) import
367
370
  pinned `@deepseek-ai/*` development dependencies (the composition and boot tests run
368
371
  the loader's real patch algorithm). One `pnpm install` reproduces that tree before
369
372
  the first run; the runtime package itself still ships zero dependencies. CI runs
@@ -444,10 +447,6 @@ src/terminal-activity.ts the distro-side activity probe for idle terminal
444
447
  reclamation
445
448
  src/terminal-route.ts which execution world a terminal launch belongs to (pure)
446
449
  src/watcher.ts in-distro file watching for WslFileSystem.watch
447
- src/workspace-files-route.ts pure builders and parsers for the routed
448
- workspace-files service
449
- src/workspace-files-wsl.ts WorkspaceFilesWsl (ctx.workspaceFiles), the GUI
450
- file tree and previews
451
450
  src/wsl.ts wsl.exe interop primitives (no DSH imports)
452
451
  ```
453
452