XINY11451/dsh-wsl

dsh-wsl

把 DSH 扩展到 WSL。多步任务共用一个长期 shell,目录与环境保留、子任务列在输入框上方;自洽命令每次全新开始,可并行,带退出码、stdin 与超时。你打开的终端可协作:全程可见,只读至你点「接管输入」。WSL 终端里 `dsh` 可向指定对话发言并流式回显。另有路径互转、环境体检、工具链补装、后台作业;危险命令先征求同意。以你的身份运行,位于 DSH 沙箱之下。

Uncategorized

dsh-wsl

English | 简体中文

A model-facing WSL tool plugin for DeepSeek Harness (DSH). It lets an agent run Linux commands through wsl.exe directly — no hand-written .sh scripts or pwsh wrappers — and for command execution it essentially matches a native Linux DSH: a real Linux kernel and bash, exit codes and signals, timeouts, truncation with spill files, background jobs the built-in job tools read back, stdin, and automatic Windows/WSL path translation.

It can also work the way you would, in a shell that keeps its state: one shell per piece of work, every command recorded with its exit code, a compact count of that work above the composer, and — when you name one of your own terminals — the keyboard back in your hands with one click.

Two limits are worth knowing before you rely on it. A wsl call runs below DSH's sandbox, so a file policy does not confine it (see Sandboxing); and a project kept on a Windows drive keeps Windows filesystem semantics — no POSIX permissions, case-insensitive names, no file-change notifications — at a fraction of the speed (see Notes).

Tools

The plugin registers ten tools. Each has a switch of its own except wsl-term, and wsl-doctor and wsl-bootstrap are OFF by default:

Tool · Purpose wsl · Run a Linux command and return stdout/stderr with exit-code, timeout and truncation markers. wsl-path · Convert between Windows and WSL paths via wslpath. wsl-env · Summarize the WSL environment (distros, kernel, cpu, mem, disk). wsl-doctor · Compare what a project needs with what the distribution has — including commands that are really Windows binaries. Read-only. Off by default (turn it on when diagnosing a project). wsl-bootstrap · Install the missing toolchain inside the distribution, from a fixed recipe list, plan first. wsl-term · Work in the session's terminal: one shell per piece of work, with a record of each above the composer. wsl-read · Read a text file inside the distribution, whole or by line range. Binary and non-UTF-8 files are refused. wsl-write · Write a text file inside the distribution, atomically, creating parents. wsl-edit · Replace one unique string in a text file inside the distribution. wsl-grep · Search file contents inside the distribution with a regular expression, grouped by file with line numbers.

wsl

Runs:

wsl.exe [-d <distro>] -e bash -lc "cd <workdir> && <command>"

and returns stdout/stderr with [exit code: N] / [killed by signal: ...] / [timed out after Nms; the command was killed] / [output truncated: ...] markers.

is the caller's distro argument, else DSH_WSL_DISTRO, else omitted entirely so wsl.exe uses the **system default distribution** — that is what makes the package portable to a machine that has no Ubuntu-22.04`.

A script longer than the Windows command-line limit (32767 characters) is fed to wsl.exe [-d ] -e bash -ls on stdin instead, so long commands work without any size ceiling.

Pass stdin to write text into the command (the default is /dev/null, so an interactive command gets EOF), and runInBackground: true for work that outlives the call: the command becomes a job in the host's registry, which the model then reads with the `job_output` tool and stops with `job_kill` — no extra tool is involved. Background jobs need @deepseek-ai/dsh-tool-jobs in the preset's composition; without it the call fails with that instruction instead of silently running in the foreground.

wsl-path

Converts a path in either direction: C:\Users\me\a.txt -> /mnt/c/Users/me/a.txt or /home/me/a.txt -> \\wsl.localhost\Ubuntu-22.04\home\me\a.txt. Direction is auto-detected from the path, or forced with direction: 'win' | 'linux'.

wsl-env

Returns the distribution list, the probed distro, kernel and architecture, CPU count, memory and disk usage, and what the machine can actually do:

distro: Ubuntu-22.04 (system default)
Linux 6.6.87.2-microsoft-standard-WSL2 x86_64
nproc: 24
Ubuntu 22.04.5 LTS · WSL2 · cgroup v2
systemd: yes · docker: not installed
GPU: /dev/dxg present (GPU passthrough enabled) · nvidia-smi: GPU 0: NVIDIA GeForce RTX 5070 Laptop GPU
drives: /mnt/c /mnt/d
/etc/wsl.conf: [boot];systemd=true;[user];default=xiny; · .wslconfig: not set
launcher: WSL 版本: 2.6.3.0 · 内核版本: 6.6.87.2-1 · WSLg 版本: 1.0.71 · Windows: 10.0.26200.9457

so an agent can decide what is available before running commands: WSL1 vs WSL2, whether systemd manages services, the cgroup version (matters for containers), GPU passthrough, docker (installed / cli-only / daemon version), which drives are mounted, and how /etc/wsl.conf and the Windows-side .wslconfig are configured. Takes an optional distro.

Every fact is optional and degrades honestly: a probe that cannot run leaves its line out, an empty value is stated (docker: not installed, .wslconfig: not set), a failed probe is reported in the summary instead of being dropped, and an unknown distro is an error rather than a partial answer. Note that wsl --version is localized, so its labels are passed through as the launcher printed them rather than parsed by name; the Direct3D/MSRDC/DXCore versions are omitted.

wsl-doctor

Answers the question a project's own README cannot: will the tooling this checkout expects actually work inside this distribution? It reads the manifests in the directory (package.json and lockfiles, .nvmrc, tsconfig.json, Cargo.toml, go.mod, pyproject.toml, requirements.txt, Dockerfile, docker-compose.yml, Makefile, CMakeLists.txt) and reports, per need, whether it resolves on the Linux side:

distro: Ubuntu-22.04 (system default)
workspace: /mnt/d/proj (Windows drive mount /mnt/d — builds, installs and git are much slower here; …)
project: package.json, pnpm-lock.yaml

needs (from the project): node, npm, pnpm
  MISSING node — not on PATH in this distribution (from package.json)
  WINDOWS npm — resolves to /mnt/c/Program Files/nodejs/npm, a Windows binary reached through interop; …
  MISSING pnpm — not on PATH in this distribution (from pnpm-lock.yaml)

usable on the Linux side: python3 3.10.12, gcc 11.4.0, make 4.3, git 2.34.1, curl, rsync, tar, sudo
Windows binaries on the Linux PATH (interop): npm -> /mnt/c/Program Files/nodejs/npm, npx -> …
privileges: uid=1000 (xiny) · sudo: requires a password (unusable from a tool call) · root: switched off

next: node, npm are not usable — wsl-bootstrap({ recipes: ["node", "pnpm"] }) installs them …

That WINDOWS row is the reason the tool exists. WSL appends the Windows PATH to the Linux one, so npm resolves on a machine whose distribution has no node at all — measured here as /mnt/c/Program Files/nodejs/npm — and the errors a model gets from running it against a Linux directory name neither cause. Overall it is read-only, cheap, and worth running before blaming a failed command on the command. Takes an optional workspace (a Linux directory; default: the configured directory, else the session workspace) and distro.

wsl-bootstrap

Installs the missing toolchain inside the distribution, from a fixed recipe list and nothing else: node (Node.js LTS into /usr/local, checksum-verified, with corepack), pnpm, python (python3 + pip + venv), build (build-essential), tools (jq, rsync, curl, git, ca-certificates). Dependencies are added when they are needed: pnpm brings node, and node brings tools when curl is missing.

It is off by default, and its dryRun defaults to true, so the first call prints the plan and changes nothing:

dry run — nothing has been installed (distro Ubuntu-22.04)

already present:
  python — python3, pip and venv (apt)

to install (python, tools):
  [tools] apt packages: ca-certificates curl git jq rsync — runs as root inside the distribution
      export DEBIAN_FRONTEND=noninteractive; apt-get update && apt-get install -y ca-certificates curl git jq rsync

apt would report:
  Inst libjq1 (1.6-2.1ubuntu3.2 Ubuntu:22.04/jammy-updates [amd64])
  …

run it for real with `dryRun: false`.

Recipes that are already satisfied are skipped, and the plan includes what apt itself would do, so you can see the exact changes before anything is installed. Installation runs as root; a failing step stops the run.

wsl-term

Every other tool here starts a fresh shell per call. That is what makes them safe to run in parallel, and it stays the default. wsl-term is the other way round: one shell for one piece of work. It is for the jobs where the shell's own state is the point — a build worth watching, an interactive program, a directory or an environment that has to survive between steps.

wsl-term works in that terminal:

wsl-term({ action: "list" })                                  → which terminals are open
wsl-term({ action: "read" })                                  → the current screen
wsl-term({ action: "run", command: "make sta", subtask: "跑通 sta 流程" })
                                                               → runs it there, returns output and exit code
wsl-term({ action: "input", data: "ls", submit: true })        → raw keystrokes
wsl-term({ action: "interrupt" })                              → Ctrl-C
wsl-term({ action: "close", id: "…" })                         → ends a terminal you name
wsl-term({ action: "subtask-done", subtask: "跑通 sta 流程", result: "三条连跑全通过" })
                                                               → writes the result and decides the shell

Which shell it drives. Two rules, and the difference is the subtask argument:

  • A `run` with a `subtask` is tracked work. It uses a private WSL shell this tool opens:

one shell per piece of work, every command recorded above the composer, and subtask-done closes it (or leaves it idle). It is not a sidebar tab and only the tool types into it — tracking bookkeeping never touches a terminal you opened.

  • A command without a `subtask` is collaborative. run, read, input and interrupt

use the newest terminal you have open, so you watch the command happen and can read the screen afterwards. While the tool works, that terminal is read-only; press 接管输入 to type again. The tool never closes a terminal you opened.

  • A named `id` always wins, whichever kind of terminal it is.

If you have no terminal open, an ad-hoc command opens a private shell instead — a normal shell for that session, but not a sidebar tab. The bridge makes the collaborative case possible on DSH 0.2.1: the host no longer lets a plugin attach to a browser terminal directly, but this plugin's own browser half runs in the page, where that attachment is allowed, so the host queues attach/write/detach commands for it over two same-origin routes (no extra port and no extra service). Older DSH builds (≤0.2.0) are not supported by the terminal tool any more; the other tools still run there.

Work is recorded as subtasks

Naming a subtask on run opens a record for that piece of work and logs every command it ran with its exit code. The dock above the composer shows how many subtasks this conversation ran (and how many are running); one click on 展开 lists them — running ones first, then finished ones with their result — and clicking a row opens its detail: the terminal's full description, the result, and the commands. subtask-done writes the result and decides the shell: keepTerminal: true (the default) leaves it idle for a follow-up step, false closes a shell the tool opened. A terminal you opened is never closed this way, and close is the other way to end one (it also refuses terminals the tool did not open). The dock also lists the terminals you opened, labelled 用户建立的终端: one shows 使用中 while it is open and flips to 已结束 only once you close it yourself. The records live in memory for the current DSH run — they describe this conversation's work, and a restart clears them along with the terminals.

Terminals belong to the current DSH run — restarting DSH ends them — and the app can re-create a browser terminal under a new id, so an id from an earlier answer may no longer exist. The tool then falls back to one of its own shells and says which one it used.

Five things worth knowing:

  • **A terminal you have open is read-only while the model works in it, and it stays

read-only afterwards: click 「接管输入」** in that terminal to type again. If you are typing when the model calls, the call is refused instead of landing in the middle of your line.

  • `read` returns the screen. Anything that has scrolled away is gone, and

full-screen programs (vim, htop) come back as their raw screen.

  • `run` needs a POSIX-style shell (sh, bash, zsh). On cmd.exe or

PowerShell, use input.

  • A busy terminal is never typed into. If its prompt is not back — another command

owns the foreground, and a dsh reply still streaming is one such command — run waits out its deadline and then declines instead of typing, so nothing is sent and the running command is not disturbed. Wait for it, type the command yourself in that terminal, or use a private shell for that work.

  • Long output is not thrown away. A build or synthesis log's answer is at the

bottom, so what comes back is the end of the output; the complete text is written to a file and the result names it.

Commands run as you, exactly as if you had typed them — and in a collaborative call you see each one as a short, readable line (__dsh_term_run 'make syn'), because the first command in that terminal defines the one-line helper there. What comes back is the output between its two markers, plus the exit code. There is no switch for this tool: it is always available; a tracked subtask runs in a private shell, a collaborative command runs in the terminal you have open, and a named id decides explicitly. A destructive command still stops at the guard unless the caller says it is intended.

Reach for it only when the work needs one shell across several steps — a long build worth watching go by, an interactive program, a directory or environment that has to persist. For everything self-contained the plain wsl tool is the better deal: a fresh shell per call runs in parallel, needs no terminal at all, and never takes the keyboard away from you.

wsl-read, wsl-write, wsl-edit

A distribution's files are reachable from Windows through \\wsl.localhost\..., but that share cannot be written through the platform's file tools: they publish a write by renaming a temporary file into place, and that rename fails on the 9p share (ENOTSUP), while the DACL copy they do first fails with GetFileSecurityW EIO. The path looks like a normal Windows path and is not one, so these three tools run the same operations inside the distribution instead — through the same launcher wsl uses, so there is no second transport and no SMB, and they work on /mnt/c/... too.

wsl-read({ path: '~/project/src/main.js' })                    → the whole file
wsl-read({ path: '/var/log/syslog', offset: 200, limit: 40 })  → lines 200-239, and says so
wsl-write({ path: '~/notes/today.md', content: '…' })          → creates parents, replaces the file
wsl-edit({ path: '~/app/config.json', oldString: '"port": 80', newString: '"port": 8080' })
                                                               → exactly one match, or it refuses

Ten things worth knowing:

  • `read` and `write` answer in the host's own words. wsl-read returns the

// envelope with numbered lines and the same (End of file - total N lines) and (Showing lines a-b of N. Use offset=c to continue.) footers as the platform's own read; wsl-write returns its Created file / Updated file acknowledgement. A model that has learned one format reads both without a second instruction. The parameters keep this plugin's camelCase (path, oldString) rather than the host's file_path/old_string — only the answer follows the host's shape. wsl-edit keeps its own one-line summary, path [2 replacements, 1.2 KB → 1.2 KB], because for a replacement the count and the size change are the two facts worth reporting.

  • A read declares a card for UIs that draw one. The host's presentation vocabulary is

declared with the result (numbered lines, the file's total, and a language hint taken from the extension — python, typescript, …), and stored with the result, so a client that renders it can redraw a replayed read without touching the distribution again. Whether a card is drawn is the client's decision: the bundled Web UI currently shows the plain numbered result, because its tool cards are client plugins keyed by their own tool names. What the model reads is the envelope above either way.

  • Text only, and loud about it. A binary file, or one that is not valid UTF-8, is refused

with a message instead of being returned as mojibake. The workaround is the wsl tool (base64, od, cp), which is also what to use for anything these three do not cover.

  • A line longer than the read window is refused, not paged forever. wsl-read returns up to

1 MiB per call and a window is line-based, so a single line over that budget (minified JSON, a bundled script) cannot be paged: the call says so and points at the wsl tool (head -c, fold, cut) instead of answering Showing lines 1-0 for the rest of the session.

  • A write is atomic, keeps the mode, and follows symlinks. Content goes to a temporary file

in the same directory and is renamed over the target, so an interrupted write leaves the old file intact; the old mode is copied first (falling back to stat -c %a where chmod --reference is unavailable, and failing loudly rather than quietly tightening a 755 script), and a write through a symlink changes the link's target instead of replacing the link. Only a regular file is replaced: a FIFO, socket or device node is refused (is not a regular file) instead of being turned into a text file by the rename.

  • `edit` is not a regexp. oldString must appear exactly once (or replaceAll: true

must be given), and $&, $1 and backticks in newString are literal text. An edit reads the file, checks that the bytes it holds match the size the shell reported, and only then writes — a change made by someone else between that read and the write itself is not detected, and the edit's content wins (last writer wins), so edit a file you are not racing.

  • These are conveniences, not a sandbox. wsl can already write any file you can, so

these add no new power; what they add is that the model does not have to compose a shell command, and that the result is structured instead of screen-scraped.

  • An edit and a write declare diffs. An edit declares the replacement you asked for while it

runs, then the hunks actually applied, three lines of context wide; a write has no before-image, so its declaration is all additions. Both are declared through presentCall/presentResult and stored with the result, so a client that reads that metadata can redraw a replayed call without the file. A UI that only draws cards for its own built-in tools — the bundled Web UI included — shows the plain result instead. The model still reads the one-line summary, not the diff.

  • `asRoot` is how a file you cannot open gets opened. A file the user has no read

permission for is refused with permission denied, not reported as an empty file; the same call with asRoot: true — and 管理员模式 on in the panel — runs as root and reads it. An edit as root copies the old owner onto the new file, so editing someone else's file does not silently hand it to root.