Skip to main content
B

dsh-plugin-wsl-env

big-dao/dsh-plugin-wsl-env

让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离

Install

dsh plugin --profile web add github:big-dao/dsh-plugin-wsl-env

README

dsh-plugin-wsl-env

English · 中文

CI npm license

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 pnpm 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 wsl preset 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 your PATH, nvm, cargo, pyenv and rc files apply. The shell is not hardcoded to bash.
  • 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.
  • 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_SHELL and DSH_WSL_HOME in the managed DSH_* namespace.
  • 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.

Configure

Override a row by id in $DSH_HOME/profiles/<name>/cordis.patch.yml. The keys worth knowing:

RowKeyDefaultMeaning
wsl-shelldistro''distro name; empty means WSL's default distro
sandboxtrueconfine commands with bubblewrap; false disables the sandbox
wsl-fsdistro''as above
restrictToDistrotruerefuse 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
sandboxtruecheck writeText and editText against the policy
substrateagentwhich 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-wslincludeHostHometruealso list the Windows home directory
subprocess-wsldistro''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/c reaches the Windows disk over 9p — noticeably slow for many small files; keep heavy-IO projects on the distro filesystem. pnpm 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 managed DSH_* namespace by prefix, translating the two Windows-path ones (DSH_HOME, DSH_PROFILE_DIR) with /p. PATH is 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.

ModeWhat a command inside the distro can do
read-onlyread the whole distro; a fresh /dev is mounted writable, so /dev/null and /dev/shm work, and nothing else does
workspace-writethe above, plus the session workspace is writable and /tmp is a temporary mount
danger-full-accessno 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. pnpm 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

SymptomCauseFix
every command reports SANDBOX_UNAVAILABLEbubblewrap is not installed in the distrorun pnpm run bootstrap -- <distro> --install, or set sandbox: false on both providers
a command or write is refused outside the session folderexpected behaviour of workspace-writeaccept the wider-permission offer, or open a session on the folder you need
writes are refused even inside the workspacethe session is in read-only modeswitch the Permissions selector
dsh plugin add warns that no layer was activatedthe dependency was already installed, so add had nothing to recordrun dsh plugin --profile wsl remove dsh-plugin-wsl-env, then add it again
the terminal still opens cmd.exethe terminal-controller row from the layer did not applycheck that dsh --profile wsl --dump-config shows shell: { path: wsl.exe, name: WSL }
changes to lib/ have no effectES module cacherestart the app
link:\\wsl.localhost\... leaves a broken symlinkpnpm cannot link a UNC pathlink a Windows path instead; developing inside the distro needs the runtime mirror, see Development
glob and grep are slowa distro search without rg inside the distro falls back to rg's own "command not found"; a Windows-folder search is native and unaffectedrun pnpm run bootstrap -- <distro> --install (installs ripgrep); distro searches always run the distro's rg — they never walk the 9p share
the terminal reports unknown activityonly 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_* codethe code says what refused it, and what clears itsee the error-code table in docs/ARCHITECTURE.md

Development

pnpm test                     # style and packaging checks, syntax check, unit tests
pnpm run test:coverage        # the unit tests with coverage thresholds (Node 22.8+)
pnpm run probe:sandbox        # measure inside the distro what bubblewrap does and does not confine
pnpm run probe                # filesystem probe against a real distro (Windows + WSL only)
pnpm run probe:sandbox-shell  # boot a real harness and drive the confined executor
pnpm run probe:terminal       # open a PTY through the terminal provider
pnpm run probe:substrate      # drive the agent filesystem substrate over a real wsl.exe transport (from inside the distro)
pnpm run probe:watch          # arm the in-distro watcher over a real directory (from inside the distro)
pnpm run probe:agent          # the resident-vs-one-shot fallback parity legs (from inside the distro)
pnpm run probe:exec           # the agent-backed execution handle: timeout, kill, cwd failure (from inside the distro)
pnpm run probe:missing-wsl    # boot a profile whose wslPath cannot start (Windows + WSL only)
pnpm run probe:picker         # list the picker's root level, refusals and its cap (Windows + WSL only)
pnpm run probe:mode           # which POSIX-mode facts survive the share (Windows node only)
pnpm run probe:sandbox-off    # prove sandbox: false unconfines both providers (Windows + WSL only)

One pnpm install reproduces the pinned development dependencies before the first run: the 0.7.x tests import pinned @deepseek-ai/* packages (the composition and boot tests exercise the loader's real patch algorithm), while the runtime package itself still ships zero dependencies. pnpm is resolved through the packageManager field, so any corepack-enabled Node works. CI runs pnpm test on Node 22 and 24, on Linux and Windows, and pnpm run test:coverage on Node 24; engines matches the harness host's own floor (^22.19.0 || >=24.0.0).

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

DocumentWhat it covers
docs/CONFIGURATION.mdevery configuration key, and how to override it
docs/ARCHITECTURE.mdproviders, mounting, path coordinates, sandbox, test layers, file layout
docs/LIMITATIONS.mdwhat the plugin does not do, and why
CONTRIBUTING.mdthe development loop, gates, conventions, verification
SECURITY.mdreporting a vulnerability privately
SUPPORT.mdsupported versions, and where to ask
docs/RELEASING.mdthe release checklist
docs/archive/README.en.mdEnglish index of the archived engineering record

License

MIT. See LICENSE.

Related plugins