dsh-plugin-wsl-env
Verifieddsh-plugin-wsl-env · v0.7.3 · MIT
让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离
Install
dsh plugin add dsh-plugin-wsl-env Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-plugin-wsl-env
English · 中文
This plugin lets 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.
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.
Install · Using it · Configure · Recipes · Architecture · Sandbox · Troubleshooting · Development · Documentation
Install
Run these four commands in a Windows terminal:
dsh wsl --from-default-profile web --dump-config # 1. create the profile from the Web template
dsh plugin --profile wsl add dsh-plugin-wsl-env # 2. install the code and its configuration layer
dsh --profile wsl --dump-config # 3. compose only, no boot (the fast check)
dsh --profile wsl # 4. run
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 as a configuration layer; nothing is merged by hand.
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.
You also need bubblewrap inside the distro: run npm run bootstrap -- <distro> --install (drop --install for a read-only check), or paste wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap. Without it every command fails closed, see Sandbox.
Uninstall: dsh plugin --profile wsl remove dsh-plugin-wsl-env. Upgrade: run the same add command again.
Changed anything under
lib/? Restart the app. A running process caches ES modules and keeps the old code otherwise.
Using it
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.
- A distro folder gets the distro environment automatically. The
wslpreset is bound while the session is created, so the first tool call is already correct. - Commands run as
wsl.exe -d <distro> --cd <linux dir> --exec <your login shell> -lc <command>, so yourPATH,nvm,cargo,pyenvand rc files apply. The shell is not hardcoded to bash. - Files are the distro's real files.
/home/you/xand\\wsl.localhost\ubuntu\home\you\xare the same file, and/mnt/c/...reaches the Windows disk. - The terminal (right sidebar, then New terminal) opens a shell inside the distro, in the session's folder.
- Port visibility. The model sees which ports have a listener inside the distro (
DSH_WSL_PORTS, refreshed about every 10 s), so it can hand you the exact URL of a dev server it just started; WSL2's localhost forwarding makes it reachable from Windows directly. - The model sees its shell environment. The plugin registers
DSH_WSL_DISTRO,DSH_WSL_SHELLandDSH_WSL_HOMEin the managedDSH_*namespace. - Permissions work as on a Linux host. The Permissions selector switches between
read-only,workspace-write(the default) anddanger-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.
Configure
Override a row by id in $DSH_HOME/profiles/<name>/cordis.patch.yml. The keys worth knowing:
| Row | Key | Default | Meaning |
|---|---|---|---|
wsl-shell |
distro |
'' |
distro name; empty means WSL's default distro |
sandbox |
true |
confine commands with bubblewrap; false disables the sandbox |
|
wsl-fs |
distro |
'' |
as above |
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 |
|
sandbox |
true |
check writeText and editText against the policy |
|
substrate |
agent |
which I/O substrate serves the file tools. Only the resident in-distro agent remains: reads, writes and identities run on ext4 (native symlinks and mode bits; the write guard survives to publication, kernel-enforced under a confined policy). The former "share" opt-out — the Windows-side host stack over the 9p share — is refused at boot; no file tool crosses the share |
|
directory-picker-wsl |
includeHostHome |
true |
also list the Windows home directory |
subprocess-wsl |
distro |
'' |
which distro the GUI terminal opens in |
cordis.patch.yml is the commented reference for every shipped value. docs/CONFIGURATION.md lists the rest, including shell, loginShell, cwd, timeoutMs, preferredDistro and maxEntries; examples/profile.cordis.patch.yml is a machine-local layer to copy from.
Recipes
- Git credential sharing: let git inside the distro use the Windows-side Git
Credential Manager —
git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"(adjust the path to your Windows Git install; WSL2's localhost forwarding is platform behaviour, so ports a dev server listens on inside the distro are reachable from Windows directly). - Paths and performance: the model sees and operates on Linux paths
(
/home/...) on the distro's own ext4./mnt/creaches the Windows disk over 9p — noticeably slow for many small files; keep heavy-IO projects on the distro filesystem.npm run bootstrap -- <distro>also reports whether ripgrep, git and inotifywait (the search, snapshot and watch backends) are in place. - WSLENV passthrough: WSL imports only the variables listed in
WSLENV. This plugin admits the managedDSH_*namespace by prefix, translating the two Windows-path ones (DSH_HOME,DSH_PROFILE_DIR) with/p.PATHis deliberately never forwarded — it would shadow the distro's own PATH.
Architecture
One DSH process serves both kinds of session at once — a workspace on a Windows folder, and a workspace inside the distro — because its providers mount at two levels:
composition (app level, one per process)
├─ subprocess-wsl the GUI terminal's execution world
│ WSL-folder session → the distro shell, in the session's Linux directory
│ Windows-folder session → powershell.exe, in the session's Windows directory
├─ directory-picker-wsl installed distros listed beside the Windows home
├─ wsl-shell-env the DSH_WSL_DISTRO / _SHELL / _HOME / _PORTS facts the model sees
└─ auto-preset binds the wsl preset when a session opens a distro folder
preset-wsl (the wsl agent preset; its services run in isolate realms)
├─ wsl-shell ctx.shell — wsl.exe --exec <login shell>, confined by bubblewrap inside the distro
└─ wsl-fs ctx.fs — real distro files; agent substrate on ext4, or the 9p share
Why two levels. wsl-shell and wsl-fs live inside the wsl agent preset, which auto-preset binds whenever a session's workspace is inside the distro: a Windows-folder session keeps the stock providers, a distro session gets the WSL ones — the environment is a property of the session, not of the process. The terminal controller is the exception: it resolves its execution world through the root context, which never sees a preset's isolate realms, so subprocess-wsl sits at the composition level instead.
Commands and files. A command runs as wsl.exe -d <distro> --cd <linux dir> --exec <login shell> -lc <cmd> inside a distro-side bubblewrap profile assembled with the same arguments as DSH's own Linux runner, so confinement semantics and error messages match a Linux host. The file tools read and write real distro files on the resident in-distro agent — reads, writes and identities run on ext4 with native symlinks and mode bits — and the file-search spawn is rewritten the same way: a search over a distro workspace runs the distro's own rg (wsl.exe --exec), never the Windows binary over the 9p share. Writes are checked against the same policy the command sandbox enforces.
Sandbox
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.
| Mode | What a command inside the distro can do |
|---|---|
read-only |
read the whole distro; a fresh /dev is mounted writable, so /dev/null and /dev/shm work, and nothing else does |
workspace-write |
the above, plus the session workspace is writable and /tmp is a temporary mount |
danger-full-access |
no sandbox; used for an approved wider-permission request |
bubblewrap is required, and it fails 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.
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.
See docs/ARCHITECTURE.md for the design, and docs/LIMITATIONS.md for everything the plugin does not do.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
every command reports SANDBOX_UNAVAILABLE |
bubblewrap is not installed in the distro |
run npm run bootstrap -- <distro> --install, or set sandbox: false on both providers |
| a command or write is refused outside the session folder | expected behaviour of workspace-write |
accept the wider-permission offer, or open a session on the folder you need |
| writes are refused even inside the workspace | the session is in read-only mode |
switch the Permissions selector |
dsh plugin add warns that no layer was activated |
the dependency was already installed, so add had nothing to record |
run dsh plugin --profile wsl remove dsh-plugin-wsl-env, then add it again |
the terminal still opens cmd.exe |
the terminal-controller row from the layer did not apply |
check that dsh --profile wsl --dump-config shows shell: { path: wsl.exe, name: WSL } |
changes to lib/ have no effect |
ES module cache | restart the app |
link:\\wsl.localhost\... leaves a broken symlink |
pnpm cannot link a UNC path | link a Windows path instead; developing inside the distro needs the runtime mirror, see Development |
glob and grep are slow |
a distro search without rg inside the distro falls back to rg's own "command not found"; a Windows-folder search is native and unaffected | run npm run bootstrap -- <distro> --install (installs ripgrep); distro searches always run the distro's rg — they never walk the 9p share |
the terminal reports unknown activity |
only while the resident agent is out — distro terminals are observed 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 reclaimed automatically after the controller's unattended timeout (2 h by default), and terminalIdleReclaim: false restores the close-by-hand posture |
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 |
Development
npm test # style and packaging checks, syntax check, unit tests
npm run test:coverage # the unit tests with coverage thresholds (Node 22.8+)
npm run probe:sandbox # measure inside the distro what bubblewrap does and does not confine
npm run probe # filesystem probe against a real distro (Windows + WSL only)
npm run probe:sandbox-shell # boot a real harness and drive the confined executor
npm run probe:terminal # open a PTY through the terminal provider
npm run probe:substrate # drive the agent filesystem substrate over a real wsl.exe transport (from inside the distro)
npm run probe:watch # arm the in-distro watcher over a real directory (from inside the distro)
npm run probe:agent # the resident-vs-one-shot fallback parity legs (from inside the distro)
npm run probe:exec # the agent-backed execution handle: timeout, kill, cwd failure (from inside the distro)
npm run probe:missing-wsl # boot a profile whose wslPath cannot start (Windows + WSL only)
npm run probe:picker # list the picker's root level, refusals and its cap (Windows + WSL only)
npm run probe:mode # which POSIX-mode facts survive the share (Windows node only)
npm run probe:sandbox-off # prove sandbox: false unconfines both providers (Windows + WSL only)
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.
docs/ARCHITECTURE.md has the file layout, the mounting, and the sandbox design. CONTRIBUTING.md has the development loop, the full gate list, and what has been verified.
Documentation
| Document | What it covers |
|---|---|
| docs/CONFIGURATION.md | every configuration key, and how to override it |
| docs/ARCHITECTURE.md | providers, mounting, path coordinates, sandbox, test layers, file layout |
| docs/LIMITATIONS.md | what the plugin does not do, and why |
| CONTRIBUTING.md | the development loop, gates, conventions, verification |
| SECURITY.md | reporting a vulnerability privately |
| SUPPORT.md | supported versions, and where to ask |
| docs/RELEASING.md | the release checklist |
| docs/archive/README.en.md | English index of the archived engineering record |
License
MIT. See LICENSE.