WSL workspace support for DeepSeek Harness——无缝的 WSL 工作区使用体验,无需在 WSL 之中再安装一个dsh,安装该插件后在 GUI 里直接添加 WSL 工作区即可。
Обзор
English · 中文 · 日本語 · 한국어 · Français · Deutsch · Español · Português · Русский Add a WSL workspace from the DeepSeek Harness web GUI and run the whole agent session — bash commands and file reads/writes — inside a local WSL distribution with Linux paths. Nothing needs to be installed inside WSL. The session can reach both WSL and Windows at the same time: bash commands run inside the WSL distribution, while Windows files stay accessible via /mnt/ (for example /mnt/c/Users/...). Pick one of the three ways below, then restart dsh web: After restarting dsh web, a W button appears beside Settings at the sidebar foot. Click the W button beside Settings at the sidebar foot to open the "Add WSL workspace" dialog. Pick a distribution from the list, then browse the directory tree or type an absolute Linux path (for example /home/me/proj) — use the Check button to verify the path exists before creating the workspace. The dialog follows the DeepSeek Harness UI language.
README
dsh-wsl-workspace
English · 中文 · 日本語 · 한국어 · Français · Deutsch · Español · Português · Русский
Add a WSL workspace from the DeepSeek Harness web GUI and run the whole agent session — bash commands and file reads/writes — inside a local WSL distribution with Linux paths. Nothing needs to be installed inside WSL. The session can reach both WSL and Windows at the same time: bash commands run inside the WSL distribution, while Windows files stay accessible via /mnt/ (for example /mnt/c/Users/...).
Install
Pick one of the three ways below, then restart dsh web:
# 1) npm package
dsh plugin --profile web add dsh-wsl-workspace
# 2) GitHub repository (ships the prebuilt lib/, no local build required)
dsh plugin --profile web add https://github.com/6Mikao9/dsh-wsl-workspace
# 3) Local directory (development / self-hosted)
dsh plugin --profile web add D:\path\to\dsh-wsl-workspace
After restarting dsh web, a W button appears beside Settings at the sidebar foot.
Usage
Click the W button beside Settings at the sidebar foot to open the “Add WSL workspace” dialog. Pick a distribution from the list, then browse the directory tree or type an absolute Linux path (for example /home/me/proj) — use the Check button to verify the path exists before creating the workspace. The dialog follows the DeepSeek Harness UI language. The username field is optional: leave it empty to run commands as the distribution’s default user, or name a Linux user of that distribution to run the session as that user instead (equivalent to wsl.exe -u ). The username only changes the bash tool’s run identity — the file tools go through the Windows-side WSL share and are unaffected. Each workspace’s username is kept in /wsl-workspaces.json; delete the entry (or recreate the workspace from the dialog) to return to the default user.
Click “Create & open” to start a new session in the workspace. In the new session the bash tool executes commands inside the chosen distribution and read/write/edit operate on WSL files, so every path the model sees is a Linux path. The mode picker keeps working as usual: Standard, PTC, Minimal and Creative each land on their WSL variant automatically (the WSL variant entries in the picker are bilingual, e.g. WSL · Standard mode(标准模式)), and Windows files stay reachable from inside the session under /mnt/ (for example /mnt/c/Users/...). The dialog’s “?” button opens a panel with the DSH releases this build declares, how the plugin is used, and the limitations it cannot fix.
Behavior notes
- bash tool: runs inside the WSL distribution as the configured username (empty = the distro default user, often
root), so it can read and write anywhere in the distro. The Windows ACL sandbox cannot wrapwsl.exe— its children run on the Linux kernel side — so WSL itself is the isolation boundary and the DSH file policy does not apply to bash. - File tools (
read/write/edit): go through the Windows-side WSL 9P share; the username field does not affect them. Two seams the host’s own provider would supply are re-established inside the WSL world, because a variant mounts this plugin’sfsprovider in the preset’s isolate realm where the host’sfs-sandboxwrapper is not in the call path. Symlinks: the share lists a Linux link but cannot resolve it, so a link path used to look like a missing file;resolve/lstatnow ask the distribution (wsl.exe … readlink -f) and continue at the real path, and a link is never replaced by a regular file. The access mode:write/editare fenced byctx.sandboxPolicyexactly as the host backend fences them — the samewritableRootsallow-list (plus the distribution’s/tmp, the temp area of the world the session runs in), the sameFS_SANDBOX_DENIEDthe tool layer renders as a denial, and the samesandboxModefact it reads for escalation. Because the fence runs after resolution it judges the real path: a link out of the workspace is an outside write and is denied underworkspace-write. A deployment that mounts no policy service fences nothing, as on the host. - File search (
grep/glob): the host’s discovery suite spawns a packaged Windows ripgrep, so a WSL variant used to drop the row and leave the model to search through the shell. The world now mounts its own twin (src/host/wsl-search.ts→lib/wsl-search.js), which runs the search inside the distribution: the tool names, parameter schemas, caps, output schema (Line N:grouping, found-count header, capped-result footer), search cards and formatted-result spill all come from@deepseek-ai/dsh-tool-fs-search’s own exported pieces, so what the model sees matches the host.grepuses the distribution’s GNU grep (-rnIEH -Z; POSIX ERE —\d,\w,\band(?i)work, lookaround and backreferences do not), skips hidden files and directories plusnode_moduleslike ripgrep’s defaults do, does not read.gitignore, expands{a,b}into one--includeper alternative, matches aincludecontaining/in this process (ripgrep semantics), and fails loudly on a distribution without GNU grep rather than framing unreadable records.globlists with GNUfind(-printfgives each file’s mtime without a stat per file) and matches gitignore-style patterns here (*never crosses a separator,**does,?,[...],{a,b}and a leading!negation), ordering oldest-first exactly asrg --sort=modifieddoes. Both search the Linux tree directly — symlinks, permissions and.gitignore-free traversal included — never the 9P share, both prune VCS metadata directories, and neither follows a symlink found during recursion, which is ripgrep’s default too. Modes whose source preset mounts no search suite (Minimal) gain none. - Skill catalog: the session’s skill catalog is discovered starting at the session cwd’s nearest
.gitancestor (falling back to the cwd itself), then scanning downward for.dsh/skills/.agents/skills— including nested projects — bounded to 4 directory levels, 64 skill directories and 4096 visited directories. Register the workspace at the project root you work in; if the registered workspace itself sits inside a larger git repository, the scan starts at that repository’s root (matching the host’s own rule) and sibling projects may surface. A Linux symlink the Windows-side share cannot resolve is resolved through the distribution instead (wsl.exe … readlink -f, at most 32 per lookup, four in flight) and the walk continues at the real path, so a project linked in withln -s— and nested projects below it — is discovered and deduplicated by that real path. Skill bodies always load live. The generated preset pinswatch: falseon theskill-filesystemrow because watching a\\wsl.localhost\...path fails, so the plugin polls instead, in two passes. The cheap pass runs every 3 seconds over the skills directories it published and re-stats each skill file: an added, removed or edited skill therefore reaches the model’s next turn, and an edit is why the stamp matters — the model’s catalog is rebuilt only when the registry’s revision moves, and a directory listing cannot tell a rewrittenSKILL.mdfrom an untouched one. The full re-discovery walk runs every 30 seconds, because only a walk can find a skills directory that did not exist before (a new nested project’s first.dsh/skills, say). Neither pass re-reads a skill body. - Shell lifetime:
bashis a stateful shell —cd, exported variables, activated virtualenvs and background jobs survive between calls. The world mounts the host’s PTY registry and its config-driven backend (@deepseek-ai/dsh-terminal-bash) pointed at this plugin’s relay (src/host/wsl-relay.ts→lib/wsl-relay.js, run by the host’s own node), which hands the PTY towsl.exe … bash: the distribution comes from the session’s UNC cwd (elseDSH_WSL_DISTRO), the optional username fromDSH_WSL_USER, and the relay runsbash -lc 'cd … && exec bash -i'so the login environment is loaded while the session directory survives (a plainbash -lcan be sent to$HOMEby a profile). That tool registers thebashname, so it replaces the one-shotdsh-tool-bashrow a non-WSL preset would use — the world also provides its own no-opsandboxcapability (src/host/wsl-sandbox.ts), because the host’s Windows ACL runner cannot read a\\wsl.localhost\…path’s security descriptor and the PTY backend confines through it before spawning. Both shells run inside the distribution and are outside the DSH file policy — WSL is their isolation boundary. Two consequences of the host’s own wrapping are stated in the tool’s description (which this plugin overrides, because the host default mentions neither): the shell is one process for the whole Agent, so acdin one call decides where the next call starts — use absolute paths or an explicitcd; and the host wraps each command aseval -- $'…', so a command ending in&backgrounds the whole wrapped command — the call then returns immediately with exit code 0 and no output while the real output arrives later, possibly inside the next call’s. Write background work as( long-job > log 2>&1 ) &on its own line, or use the background-job tool. - Tracked background jobs:
bash_backgroundstarts one command in the background and returns a registry job id immediately; the host’sjob_list,job_output(incremental reads, status transitions, completion notices) andjob_killthen work on it exactly as they do for the host’s one-shot tool. The row exists because the persistent shell’s schema declares onlycommand: without a producer,job_listalways answered “no background jobs” and arun_in_background: trueargument passed tobashwas silently ignored — the parameter schema does not forbid extra properties, so nothing reported the mistake. A real session found exactly that. The tool is mounted only alongside the persistent shell; a world that keeps the one-shot bash row already hasrun_in_backgroundon that tool. - Older hosts fall back to a one-shot shell: the persistent stack is the host’s code, and on Windows it needs a platform process inspector that only exists from
0.1.0-rc.8on — in0.1.0-rc.7spawnTerminalthrowssubprocess-local: terminal inspection is unsupported on platform win32before any process starts, so everybashcall in that release fails outright (grep/glob, which never touch the PTY, keep working). The plugin therefore probes the substrate at startup instead of assuming: it handsspawnTerminala program that cannot exist, which reaches the inspector check and nothing else — no process is created either way, and the rejection says which half failed. When the answer is “no inspector”, the generated world keeps the one-shotdsh-tool-bashrow (this plugin’s ownctx.shellprovider, no PTY) and the model gets a working, stateless shell instead of an error on every call.0.1.0-rc.7is the only declared release in that state; every later one gets the persistent shell. - The garbled
localhostport-forwarding bannerwsl.exeprints to stderr when the distro was not running yet is harmless.
Changelog
0.7.3 — 2026-09-23
- The plugin loads on DSH
0.1.7-rc.1again, and its modes come back. The0.1.7line renamed the host preset face —read()becamereadDocument(), which returns a document ({agentPreset, content, name, description}) instead of the composition text, andAgentPresetlost itspath— so the variant generator threwagentPresets.read is not a functionon every boot and nowsl-*mode ever reached the picker. The roster face is now probed by capability and both generations are served:readDocument()where it exists,read()behind it. - A variant is a declaration row on that line.
0.1.7stopped scanning$DSH_HOME/.agent-presets/— a preset there is a declarative@deepseek-ai/dsh-agent-presetrow, and the directory a variant used to be written to is read by nothing at all. The generator now expands the composed variant back into an entry list and publishes it throughctx.agentPresets.register(); the plugin’s effect owns the returned disposers, so an unload or hot reload retires the variants instead of leaving orphans the next apply could not replace (Duplicate agent preset: wsl-). Earlier releases keep the directory channel, unchanged. - The world’s own providers are named as
file:URLs on that channel. A preset mounted from a declaration is imported by the registry’s entry tree, which — unlike the boot-time Include — does not turn an absolute path into afile:URL. Without the rewrite the provider rows never started, the audit reported each asnever started, and the whole variant was refused as unusable. - The PTC variant is named again on
0.1.7. The label table that gives the shipped modes their bilingualWSL · …names carried the mode’s older id (code) but notptc, the id it has used since0.1.1. That stayed invisible as long as the release published its own display name, because the lookup fell through to it — but0.1.7publishes none, so the mode reached the picker asWSL · ptcwith the genericWSL execution world for ptc: …description. Both ids are covered now, and the variant readsWSL · PTC mode(PTC 模式)on every channel, like the other three shipped modes. dsh.compatibility.dshReleasesdeclares0.1.7-rc.1. The two modules the declaration channel needs (@deepseek-ai/cordis-plugin-include,js-yaml) are resolved at call time and declared as optional peers, so a release that lacks them fails one variant rather than refusing to load the plugin.
0.7.2 — 2026-09-21
- The WSL skill catalog is no longer re-walked on the request path (issue #25).
The host rebuilds the catalog during a request and awaits each provider’s
list(), and this provider kept its own answer for only 10 s — so every time the catalog was re-collected (a new session or scope, or simply a lookup more than 10 s after the last one) that request paid a full walk of the workspace, one directory at a time: twostats and onereaddireach. Areaddirover the\\wsl.localhost\…9P share measures 3-16 ms here and the walk’s budget is 4096 directories, which is why a large workspace cost 20.4 s, on the request path. A published catalog is now served as-is, and only the provider’s own change detector can drop it: a repeat lookup costs 1-3 ms and no filesystem traffic. The freshness contract is unchanged — a new nested skills directory still appears within 30 s, and an added, removed or edited skill within 3 s. - The walk itself is cheaper: one BFS layer is probed concurrently (bounded) and
published in frontier order, so the catalog stays deterministic, and a
directory’s
.dsh/skills/.agents/skillsare probed only when its own listing showed that marker. The budget-sized walk went from 20.4 s to 4.8 s on the same machine; node’s filesystem thread pool caps the real parallelism.
0.7.1 — 2026-09-20
npm install dsh-wsl-workspaceno longer fails. Verifying the published artifact turned up a regression this release introduced: npm auto-installs missing peer dependencies, and the@deepseek-ai/dsh-tool-fs-searchpeer added in 0.6.0 itself peers on@deepseek-ai/dsh-retention, which is not published — so a plainnpm installdied withE404 … @deepseek-ai/dsh-retention(0.4.3 installs fine, so it was ours).dsh plugin adduses pnpm, which only warns about unmet peers, which is why every harness run and real install passed. All ten host peers are now marked optional inpeerDependenciesMeta: the package still declares what the host must provide, but npm no longer tries to fetch it.
0.7.0 — 2026-09-20
The WSL world now matches the host everywhere a session can tell the difference, and the last two known issues are closed. Everything below ships together: a WSL variant gets Linux symlinks, the session’s access mode, in-distribution search, a live skill catalog, a stateful shell, and tracked background jobs.
- The host’s
bashcontract is stated in the tool description. The persistent tool wraps each command aseval -- $'…', so a command ending in&backgrounds the whole wrapped command — the call returns immediately with exit code 0 and no output while the real output arrives later, possibly inside the next call’s. And the shell is one process for the whole Agent, so acdcarries into the next call. The host default says neither, and DSH’s own Minimal preset recommends the hazardous form (sleep 10 &). The world now overridesdescription(a supported key on every declared release) with both facts and the safe forms. 0.1.0-rc.7falls back to a working one-shot shell. That release’sdsh-subprocess-localhas no Windows process inspector, so the host’s PTY-backed persistent shell cannot start on Windows at all — everybashcall failed withsubprocess-local: terminal inspection is unsupported on platform win32(the host ships the same gap: its Minimal preset mountspersistent-bashthere with no Windows guard). The plugin now probes the substrate instead of assuming — it handsspawnTerminala program that cannot exist, which reaches the inspector check and nothing else — and when the answer is no, the world keeps the one-shotdsh-tool-bashrow: a working, stateless shell rather than an error per call.- Tracked background jobs, restored. Replacing the one-shot bash tool with the
persistent one also removed the only thing that started a registry job, so
job_listalways answered “no background jobs” and arun_in_background: trueargument handed tobashwas silently ignored (the parameter schema allows extra properties, so nothing complained). The world now mountsbash_background(src/host/wsl-jobs.ts), a thin producer over the host’s ownctx.jobs.startplus this plugin’sctx.shell.start: it returns a job id, andjob_list/job_output/job_killwork on it as usual. It is mounted only where the source mode also mounts thejob_*tools, and only alongside the persistent shell. - Six defects found by hunting the new code with worst-case input: a hidden-file
guard that also applied to an explicitly named file (
grep path=.envreturned nothing), a discardedfindexit status (glob path=/nope-missinglooked like an empty directory), a line-terminated glob header (a root whose name contains a newline came back truncated), untranslated Windows paths (grep path='D:\proj'failed wherereadworked), a closed spill schema (which would have failed the tool’s own output validation on every capped search), and a catalog detector that could stack polls on a slow share. Plus two in the new producer: it was mounted in a mode with nojob_*tools to read its ids, and it defaulted a job’s working directory to the host process’s rather than the session workspace. - Verification: thirteen checks on each of the eight declared releases
(
0.1.0-rc.7…0.1.5-rc.2) —search-realdrives the real tools against a real distribution fixture — leaving only the two pre-existing baseline failures (typecheck, andhost-apiwhich needs a live server). 152 unit tests, including a parity check of every renderer against the host suite’s own formatters. Real browser sessions on five releases for the tool behaviour, and a frontend pass on all eight (entry button, dialog, path check, create & open, mode picker, help panel with v0.7.0 and 8 release chips), with the session log as evidence for the tool set, the search results, the catalog replacement, the shell fallback and the background-job lifecycle.
0.6.0 — 2026-09-19
- WSL sessions get
grepandglob: the host suite spawns the packaged Windows ripgrep and every path the model hands it is a Linux one, so the generated world droppedtool-fs-searchand left the model to grep through the shell — the last bullet of the panel’s known issues. The world now mounts an in-distribution twin that keeps the host suite’s contract: the same tool names, parameter schemas, inline caps (250 matches / 100 paths), output schema,Line N:grouping, found-count header, capped-result footer, search card and formatted-result spill — the rendering comes from@deepseek-ai/dsh-tool-fs-search’s own exported formatters, and the two projections that package keeps private (the card metadata and the glob page) are reproduced and compared against it in unit tests.grepruns GNU grep inside the distribution (-rnIEH -Z, POSIX ERE, hidden entries andnode_modulesskipped like ripgrep’s defaults, no.gitignoresupport),globuses GNUfindwith in-process gitignore-style matching and ripgrep’s oldest-first modification order. Model-controlled values travel as separate argv elements after a fixed script, so nothing the model types is ever parsed by a shell. - The skill catalog notices an edited skill, not just an added one: the catalog message is rebuilt only when the registry’s revision moves, and the old detector compared directory listings — so rewriting an existing
SKILL.md(a description, say) changed nothing it could see and the model kept the old text until a new session. The cheap pass now also stamps every skill file with its modification time and size, and runs every 3 seconds instead of 10. The full re-discovery walk — the only pass that can find a skills directory that did not exist before — moved to its own 30-second cadence, so the change detection is both faster and cheaper than the single 10-second poll it replaces. lib/is rebuilt deterministically:tsdownwrites into a committedlib/, and stale code-split chunks from an earlier build survived every rebuild (clean: falseplus two configurations sharing one output directory). Local build tooling now clears the directory first, and the three new runtime peers (@deepseek-ai/dsh-tool-fs-search,@deepseek-ai/dsh-tools,@deepseek-ai/schemastery) are declared, which is also what keeps them external instead of bundling a second copy of DSH’s tool stack into this plugin.- Four defects found by hunting the new code with worst-case input: a hidden-file guard that also applied to an explicitly named file (
grep path=.envreturned nothing), a discardedfindexit status (glob path=/nope-missinglooked like an empty directory), a line-terminated glob header (a root whose name contains a newline came back truncated) and untranslated Windows paths (grep path='D:\proj'failed wherereadworked). The spill schema was also closed, which would have failed the tool’s own output validation on every capped search, and the catalog detector gained an in-flight guard so a slow poll cannot stackwsl.execalls. - The host’s
bashwrapper is documented in the tool description: the persistent tool wraps each command aseval -- $'…', so a trailing&backgrounds the whole wrapped command and the call reports exit code 0 with no output; and the shell is one process, so acdcarries into the next call. The host default says neither, and DSH’s own Minimal preset recommendssleep 10 &— the world now overrides the description with both facts and the safe forms. 0.1.0-rc.7no longer gets a broken shell: that release’sdsh-subprocess-localhas no Windows process inspector, so the host’s PTY-backed persistent shell cannot start on Windows at all (the host ships the same gap: its Minimal preset mountspersistent-bashthere with no Windows guard). The world probes the substrate at startup and, when the answer is no, keeps the one-shotbashrow — a working stateless shell — instead of failing every call. Verified by a real session on that release.- Tracked background jobs are back in WSL sessions: replacing the one-shot bash tool with the persistent one also removed the only thing that started a registry job, so
job_listalways said “no background jobs” andrun_in_background: truehanded tobashwas silently ignored (the parameter schema allows extra properties, so nothing complained) — the exact defect an operator’s session surfaced. The world now mountsbash_background(src/host/wsl-jobs.ts), a thin producer over the host’s ownctx.jobs.start+ this plugin’sctx.shell.start: the tool returns a job id, andjob_list/job_output/job_killwork on it as usual. Verified in a real session:started background job bash-1→job_listshowsrunning→ incrementaljob_outputreads (tick 1,tick 2, thentick 3) →[status: completed, exit code: 0]plus the runtime’s completion notice. - Verification: thirteen checks on each of the eight declared releases (
0.1.0-rc.7…0.1.5-rc.2) —search-realjoins the suite, driving the real tools against a real distribution fixture (framing, includes and braces, caps and footers, spill, cards, error codes, argv-safety, explicit dot-files, unreadable roots, odd root names,/mntpaths, abort and overflow, glob ordering and pruning) — leaving only the two pre-existing baseline failures (typecheck, andhost-apiwhich needs a live server). Unit tests addtests/wsl-search.test.ts(33 cases) and the refresh, cadence, stacking and description cases;skills-realproves an edited skill file invalidates the catalog through the share’s own modification times. Real browser sessions on five releases confirm the behaviour end to end, including the0.1.0-rc.7fallback.
0.5.0 — 2026-09-19
- The file tools now follow Linux symlinks: the
\\wsl.localhostshare lists a link entry but cannot describe it —lstat,statandreadFileon the link all fail andresolve()hands back a lexical identity for it — so a link path behaved like a missing file and a linked-in project’s files could not be read or written at all.resolve/lstatnow ask the distribution (wsl.exe … readlink -f, the same resolver the skill scan uses) whenever this share cannot already describe the path, and continue at the real path. A link is never replaced by a regular file, and writing through a dangling link creates its target while keeping the link. - The access mode constrains a WSL session again: a variant mounts its own
fsprovider in the preset’s isolate realm, so the host’sfs-sandboxwrapper was not in the call path andworkspace-writedid not stop a write outside the workspace (measured before the fix: a Linux path and aD:\...path both went through).writeText/editTextnow fence the mutation exactly as@deepseek-ai/dsh-fs-sandboxdoes:ctx.sandboxPolicy(the tool layer’s per-call value, else the service), the samewritableRootsallow-list plus the distribution’s/tmp, the sameFS_SANDBOX_DENIED, and thesandboxModegetter the tool reads to advertise escalation. Because the fence runs after link resolution it judges the real path, so a link out of the workspace is an outside write. - Live skill catalog: with
watch: falsepinned on the UNC-hostile watcher, a skill added while a session ran only appeared in the next session. The provider now keeps a change detector per scan root it has served, re-checking the published directory shape every 10 s (roots plus entry names and kinds, never re-reading skill files) and callingcontrol.invalidate()when it changed, which makes the catalog middleware re-collect on the session’s next turn. bashis now a stateful WSL shell — the capability the per-mode matrix kept showing was missing (everybashcall used to be a fresh process). DSH’s PTY registry takes replaceable backends and@deepseek-ai/dsh-terminal-bashis a config-driven one, so the world mounts it (inside its ownpersistent-shellgroup, because the registry is an agent-owned service) withbackendType: wsland points it at this plugin’s relay (src/host/wsl-relay.ts→lib/wsl-relay.js) run by the host’s own node. The relay resolves the distribution (session UNC cwd →DSH_WSL_DISTRO→ host default) and the optionalDSH_WSL_USER, then hands its stdio — the PTY — towsl.exe -d … --cd … -e bash -lc 'cd … && exec bash -i': login environment, interactive, and the session directory preserved.@deepseek-ai/dsh-tool-bash-persistentregisters thebashname, so it takes the place of the one-shotdsh-tool-bashrow (mounting both fails the whole preset — the same collision DSH’s Minimal mode sidesteps by being a persistent-shell-only agent). The world also isolates and provides its own no-opsandboxcapability: the PTY backend confines throughctx.sandboxbefore spawning, and the host’s Windows runner cannot read the security descriptor of a\\wsl.localhost\…workspace root (GetNamedSecurityInfoW failed (Win32 1)), so a WSL session declaresenforcement: 'partial'and keeps the policy where it is meaningful — in the file tools.- Verification: the eight declared releases (
0.1.0-rc.7…0.1.5-rc.2) run twelve harness checks —fs-real(link resolution through the real backend, reads through links and chains, dangling-link creation, link preservation, the fence on an outside link target, the distro/tmpallowance) andrelay-real(stateful shell against real WSL, distribution and user resolution, clean exit) are new — with the same two documented baseline failures (typecheck, andhost-apineeding a live server). Unit:tests/fs-policy.test.ts(7 fence cases) and the skill-provider refresh cases, on top of the existing suite.
0.4.5 — 2026-09-19
- A project linked into a WSL workspace is discoverable now: the
\\wsl.localhost9P share lists a Linux symlink but cannot resolve its target, so the skill scan — which already followed directory links on substrates that resolve them — skipped every linked-in project, and with it every nested project below it (the layout issue #10 describes). When the share reports a link it cannot follow, the provider now asks the distribution itself (wsl.exe -d -- readlink -f) and continues the walk at the real path. The fallback is bounded on purpose: at most 32 links per lookup, four calls in flight, a 10 s timeout each, and the existing depth / visited-directory / skill-directory budgets are untouched. Because the walk continues at the resolved path, a project reachable both directly and through a link is visited once, and a link that points back at the workspace root is absorbed by the visited set instead of looping. - What the fallback does not cover:
read/write/editstill resolve their paths throughWslFileSystem, which does not follow Linux links, so reading or writing a link path reports it missing — use the real path. The help panel’s known-issues list now states that instead of promising a fallback “not implemented yet”. - Why one
wsl.exeper link (measured, and worth recording):wsl.exesilently drops the arguments that follow a command (sh -c 'echo $#' sh a b canswers 0), and its command-line parser truncates an argument containing a double quote, so a batchedshloop cannot be made reliable through it. A barereadlink -f a b cis no better: GNUreadlinkstops at the first path it cannot resolve and still exits non-zero, which would silently starve the rest of the batch. Passing each path as a process argument to one short call avoids quoting entirely — paths with spaces, quotes and backslashes all resolve — at the cost of one process per link (about 35 ms warm; six links cost 179 ms end to end on this machine, and a workspace with no links never starts a distribution process at all). - Verification: the eight declared releases (
0.1.0-rc.7…0.1.5-rc.2) pass the same 8/10 harness checks as 0.4.4 — only the documentedtypecheckbaseline and the check that needs a live server fail. The real-9P check now builds a fixture whose only path in is a symlink and asserts the linked project, its nested project and its service throughget(); the same walk with the fallback face removed finds neither, which is the pre-fix behaviour reproduced in the same run. On a live WSL fixture (/home/mille/symprobe/ws: a link out of the workspace, a link chain, a file link, a dangling link and a loop back to the root) the catalog went from 2 skills to 5, andget()read every body through the resolved locator.
0.4.4 — 2026-09-19
- A preset built on top of a WSL variant could not be used at all: this generator recognises its own output by id prefix (
wsl-), so a user preset that started life as a copy ofwsl-standardorwsl-cordis— a “data mode” that carries its own world, say — was treated as a plain source preset and had a second world group appended to it. DSH refuses a composition carrying twowsl-worldrows, so choosing that mode failed outright with无法切换到「WSL · 」:duplicate loader entry id: wsl-world; on a release that mounts the group before validating row ids the same duplication surfaces one step later astool "str replace editor" is already registered in this scope(the report in #24). The generator now replaces the world group it finds — identified by the mountedshell-wsl/fs-wslprovider ids, so a copy whose group was renamed is caught too — and every variant ends up with exactly one world pointing at this installation’s providers. A top-level row id that appears twice in a source is reduced to its first occurrence as well, because DSH rejects the whole preset on a duplicate id rather than the offending row. tool-str-replace-editorrows are replaced like the olderstr-replace-editorrow (#24): newer rosters name the editor row that way, and it registers the samestr_replace_editortool as the world group’s own editor row, so the source row is dropped just like its predecessor and the WSL-aware editor the variant injects stays.- Variant display names are no longer double-quoted: the variant’s
preset.ymlcopied the source’sname:scalar verbatim, so a quotedname: 'Data mode'reached the mode picker asWSL · ''Data mode''. The scalar is unquoted before it is re-emitted. - Not adopted from #24: disabling the
tool-cordisrow to avoid a duplicate inspect-provider registration. Adisabledrow never applies, so the WSL variant of Creator mode silently lostcordis_inspect_list/cordis_inspect_query(checked against 0.4.3, where both are present and answer with the host and the client providers); the PR’s own description that the model “can still see the tools in the catalog” is not what happens. The registration their report shows needs that row applied twice, which the row-id reduction above now prevents where a copied preset caused it. - Help panel tidied up: the panel now opens with a greeting line and the repository link, carries a “What’s new” section for this build, and lists only the limitations that still apply — the historical “fixed in 0.4.3” note and the per-generation API walkthrough are gone. The compatibility chips are untouched: they are the manifest this build declares, not history.
- Verification: the eight declared releases (
0.1.0-rc.7…0.1.5-rc.2) pass the same 8/10 harness checks as 0.4.3 — only the documentedtypecheckbaseline and the check that needs a live server fail; 136 transforms over every shipped preset of the 17 installed runtimes are unchanged apart from the repair, and all 68 “copied variant” cases resolve to a single fresh world group. - Per-mode matrix with a real model (every WSL variant, not just the default one): on
0.1.0-rc.7,0.1.1-rc.2,0.1.3-alpha.2and0.1.5-rc.2each of the four variants — Standard, PTC, Minimal, Creator — was driven through the browser and asked to write a file with its file tool, rununame -r; pwd; whoamiin bash and land that output in the workspace, then read the file back. Every mode producedMODE--OKand a WSL2 kernel line in/home/mille//notes/with no loader error; the follow-up bash call lands in the workspace again, which is the documented per-call shell (the PTY group stays dropped).0.1.2-rc.1and0.1.5-rc.1were driven through all four modes without the file/bash assertions. - Browser + real-model spot checks of the copied-variant mode (mounts and answers), Creator mode (inspect tools intact) and the
0.1.0-rc.7standard flow complete the pass.
0.4.3 — 2026-09-11
- The persona text moved in
0.1.3-alpha.2(#22): DSH renamed the persona’s model-facing scalar fromtextto an inlinesuffixplus a foldedprefix, and the variant generator only recognisedtext: >-. On that line the WSL environment sentence was never appended - the session still ran inside the distribution, but the model was never told that its working directory is a Linux path reachable from Windows as/mnt/. The generator now amendssuffix,textorprefix(folding an inline scalar into a block scalar when needed, so the sentence joins the working-directory line exactly where the legacytextblock put it), and a persona carryingcomplete: trueis still left alone. Verified on seven releases: the five older ones keep their persona block byte-identical, and the two newer ones now carry the sentence into the model’s system message. - Help panel: the dialog gained a “?” button that opens an in-place panel - the DSH releases this build declares (read from
package.jsonthrough the host route, so the list can never drift from the manifest), how the plugin is used, its features, and the limitations it cannot fix. - The skill catalog now reaches UNC workspaces: the host skill provider watches a workspace through
chokidar, and watching a\\wsl.localhost\...path fails; the failed watcher makes the skill snapshot reportcomplete: false, anddsh-tool-skillwithholds the entire catalog message while a snapshot is incomplete — so a WSL session’s model saw no skills at all, not even the ones the plugin had discovered. The variant generator now pinswatch: falseon theskill-filesystemrow (merged into an existingconfig:block when there is one, and left alone when the source declareswatchitself), which makes the host collect the catalog once at session start instead. Verified end to end on0.1.5-rc.2: the model’s context carries the `` list. Trade-off: a skill added mid-session appears in the next session rather than the running one; skill bodies are still read live. verify-libhardening: its comment/string stripper could pair a lone apostrophe inside a comment with a later one and swallow the rest of the bundle, which made everynode:*import look tree-shaken. The quote rules now stop at a newline, exactly as a JavaScript string does.
0.4.2 — 2026-09-10
- Create & open in a
0.1.2-rc.1workspace: the session starter is now resolved when the dialog writes, not when the plugin applies. This plugin applies before the UI domain that publishesuiWorkspaceregisters its service, so the lookup cached at apply time stayedundefinedfor the whole page life:Create & opencreated the workspace and then silently opened no session, leavingsessionIdsempty while the dialog reported success. A release exposing neitheruiWorkspace.startSessionnorworkspaces.startSessionnow fails before anything is written, instead of leaving an orphaned workspace behind. - Skill body integrity: skill bodies no longer lose their first character.
findFrontmatterEndalready returns the index of the body’s first character (the closing delimiter’s newline plus one), so the slice must start there; the previous offset dropped that character and made the one after the delimiter look like the body. The existing fixtures always put a blank line after the delimiter, which is exactly what hid it. - UTF-8 BOM skills are no longer dropped: a
SKILL.mdsaved with a leading BOM (Notepad, VS Code’s “UTF-8 with BOM”, PowerShell redirection) did not match the opening---and disappeared from the catalog entirely. The parser strips the BOM before the fence check. - Binding converges on late inputs: the agent-preset roster and the registered
/mnt/workspace set are both inputs to binding, and both land asynchronously after the plugin’s first pass. Each now re-runs the pass when it arrives instead of waiting for a session-store event that may never come. - Compatibility manifest corrected:
0.1.3-alpha.1is not published (npm view @deepseek-ai/[email protected]is a 404), so the declaration could never be verified; it is replaced by the published0.1.3-alpha.2. - Reproducible publishes: a new
.gitattributes(* text=auto eol=lf,lib/** -text) pins line endings.core.autocrlf=trueused to rewrite text files to CRLF on checkout, and sincelib/is committed and published verbatim the same commit produced different npm tarballs depending on the machine; the repository already stored LF, so no renormalisation was needed. - Closed-loop tests:
tests/client-lifecycle.test.mjsdrives the browser half through the shippedlib/client.jsfor both service shapes — legacy (connection.api.agentPresets+workspaces.startSession) and current (remote.agentPresets+uiWorkspace) — and assertsCreate & openfor the normal, late-registration and no-starter cases. The skill tests now cover a body that starts on the delimiter’s next line, for LF and CRLF files.
0.4.1 — 2026-09-03
- DSH v0.1.2-rc.1 compatibility: Added backward compatibility support for DSH v0.1.2-rc.1 and later versions through feature detection and compatibility wrappers. The plugin now automatically detects the DSH version at runtime and uses the appropriate API:
uiWorkspace.startSession()for v0.1.2-rc.1+workspaces.startSession()for v0.1.1-rc.2 and earliersummary.projectionValues?.agentPresetfor v0.1.2-rc.1+summary.agentPresetfor v0.1.1-rc.2 and earlier- Projection-based auto-sync for v0.1.2-rc.1+
sessions.noteAgentPreset()for v0.1.1-rc.2 and earlier
- Updated compatibility manifest: Added v0.1.2-rc.1 to the
dsh.compatibility.dshReleasesdeclaration. - Fixed
without injectcrash on v0.1.2-rc.1+: the agent-preset roster is read through theremote.agentPresetsnamespace service viactx.get('remote.agentPresets')(topology-free store lookup) instead of theremoteaggregate’sagentPresetsproperty, which Cordis’ associate proxy rejects when the dotted property is not declared ininject.injectstays limited to the services both DSH generations share (slots,locale,sessions,workspaces). - Compatibility manifest: declared v0.1.3-alpha.1 compatible (its plugin-facing API surface matches v0.1.2-rc.1). Final adaptation notes consolidated in
docs/COMPATIBILITY_SUMMARY.md(supersedes the root-level draft plans).
0.4.0 — 2026-08-29
Follow-ups from the #12 limitation list and the #13 compatibility work:
- Lookup cache: completed skill-catalog lookups are cached per scan root for 10 seconds, so repeated catalog builds no longer rescan the workspace over the slow 9P share;
get()keeps reading skill bodies live, and freshly added skills appear within the TTL window. - Symlinked projects — investigated in 0.4.0, resolved in 0.4.5: the discovery walk now recognizes directory symlinks explicitly and prunes them safely (no crashes, no loops), and the probe showed that following them is impossible over the
\\wsl.localhostshare itself (the Windows side cannot resolve Linux symlink targets:readlink→EISDIR,stat/readdir→ENOENT); 0.4.5 resolves them through the distribution instead, so linked-in projects are discoverable (see that changelog entry). A name+body fingerprint dedupe also guarantees aliased skill files can never publish twice on substrates that do resolve links. - Block-scalar frontmatter:
description:/whenToUse:written as YAML block scalars (|literal,>folded) now parse — such skills were silently dropped before. - Compatibility manifest:
dsh.compatibility.dshReleasesdeclares per-release compatibility with the official DSH versions, backed by reproducible disposable-Profile install/start/uninstall evidence (scripts/verify-dsh-compat.sh), andenginesdeclares the Node.js floor. - Guard scripts:
scripts/check-rank-parity.mjsfails the release when the copied project-rank constants drift from the host’sdsh-skill-filesystem.
0.3.2 — 2026-08-29
- WSL workspace sessions now inject nested-project skill catalogs (#10):
.dsh/skillsand.agents/skillsdirectories of projects nested below the registered workspace root are discovered and published with the host’s project ranks and sources, so the model sees the same skill catalog it would see when the session cwd is the project folder itself. Discovery is depth- and budget-bounded, prunesnode_modules/dot-directories, and leaves non-WSL sessions untouched. - Host-parity scan root: lookups from inside a project subtree resolve the nearest
.gitancestor first, so the enclosing project’s skills stay visible from deeper cwds; skills above that ancestor do not leak. - Hardening: the skill-root budget is enforced per push, and the
skills.registerProvidercall is guarded so a host whoseskillsservice has a different shape can no longer break plugin load. - Housekeeping: removed stale prebuilt
lib/chunks that shipped dead vendor code (including an inlined schemastery copy that triggered dsh.so’snew Functionstatic rule); addedscripts/repro-setup.shplus a nested skill-catalog regression suite, and a matching TESTING.md section.
License & attribution
MIT — see LICENSE and NOTICE. The NOTICE precisely lists:
- Adapted/inherited source code: DeepSeek Harness (MIT) —
dsh-bash-local(executor mechanics),dsh-fs-local(WslFileSystemsubclasses it), and the shipped agent presets (read and transformed by the variant generator); - Design references (no source copied): dsh-bash-terminal (MIT, wsl argv / WSLENV approach), dsh-side-panel (BSD-3-Clause, host-route pattern), vpshub (MIT, roadmap reference).
Keep LICENSE and NOTICE when redistributing.
Acknowledgments
Special thanks to dsh-deep-whale (DSH Web 鲸鱼娘 skin series · 深海女仆工坊 maid-atelier, CC BY-NC-SA 4.0): the whale girl skin plugin brings a full set of adorable skins to the DeepSeek Harness Web UI and makes daily use of DSH a warmer experience.
Рекомендуемые инструменты
Попробуйте другой запрос или уберите фильтр.
Установка
npx skillfish add 6mikao9/dsh-wsl-workspace