dsh-bash-native
Rust 实现的 Windows 原生 POSIX bash 执行器:给 DSH 的 shell 提供一个真正的 bash(brush 引擎,不依赖 WSL 或 MSYS兼容层),提供 DSH 的三档权限策略、后台作业和一节模型可见的环境契约。
English | 中文
为什么需要它
避免肥鱼被cmd咬,也规避了子系统和MSYS兼容层的性能开销
| 方案 | 命令在哪里执行 | 每条命令的代价 |
|---|---|---|
| Git for Windows | MSYS2 兼容层上的移植 bash,POSIX 语义由运行时模拟 | 兼容层每次起子 shell 或外部命令都要完整 fork 一个 MSYS2 进程(挂起线程、另起进程、经管道搬运内存),开销随子 shell 启动频率线性放大 |
| WSL | 命令在 Linux VM 的发行版里执行 | 每条命令都要经 Windows/Linux 边界往返一次,延迟对高频短命令尤为不利 |
| 本插件(brush 引擎 + 工具链) | 纯 Rust 实现的原生二进制,无模拟层、无 VM | echo、cat、ls、cp 等由引擎内建、不产生新进程,其余按原生进程启动,没有兼容层或 VM 边界 |
安装
CLI安装
dsh plugin --profile web add dsh-bash-native
dsh网页版安装
dsh插件页面->右上角添加插件,填入
https://github.com/SpookyWaste/dsh-bash-native
重启后,在 Settings → General 里把 Native Bash (Windows) 设为默认 preset(或在会话里切过去)。
| 要求 | 值 |
|---|---|
| 平台 | Windows x64 |
| Node | 24 |
| DSH | >=0.1.7-rc.2 <0.3.0-0(0.1.7-rc.2 与 0.2.0-rc.2 实测通过) |
引擎
引擎是 brush(Rust 写的 bash 兼容 shell,MIT),本仓库对它打补丁、从源码构建,产物 engine/win32-x64/brush.exe(约 15 MB)随包提交;补丁逐支记录在 patches/brush/README.md。
补丁
0001-background-job-pid.patch:让由字面量外部命令组成的后台作业在父进程直接 spawn 并登记,$!与jobs -p有真实 PID。0002-unix-tmp-alias.patch:/tmp成为 shell 自己解析路径时的别名——重定向、cd、test -f,以及通配符展开的根目录。0003-err-trap-scope.patch:ERRtrap 由设置它的那一层触发,而不是按set -E判定。0004-external-argv-tmp-alias.patch:别名改写延伸到 shell 启动的每条命令的参数,并留下DSH_BASH_NATIVE_NO_PATHCONV=1逃生开关。0005-closed-reader-stage-abort.patch:读端已经离开时,断了管的复合命令整体中止,而不是像上游那样静默继续。0006-descriptor-paths.patch:/dev/fd/N、/dev/stdin、/dev/stdout、/dev/stderr按 shell 自己的描述符表解析。0007-wait-selectors.patch:wait汇报它等待的那个作业自己的状态,而不是只支持"等全部"与%spec。0008-windows-kill-builtin.patch:kill成为内建,带作业规格与信号词表,被杀的子进程以 128+n 汇报。0009-drive-mount-aliases.patch:首段是单个字母时按盘符挂载点解析,/c/Windows/System32等于C:\Windows\System32。0010-missing-builtins.patch:补上脚本会用的内建(umask、history、disown、wait -f等),每个都给一个站得住的语义。0011-crlf-script-text.patch:Windows 写出来的脚本按文本读,行尾的 CR 不再进入语法。0012-relative-command-paths.patch:相对路径的命令按 shell 的工作目录解析,./sub/x.exe因此可跑。0013-compound-pipeline-stage-concurrency.patch:非末段的进程内阶段改跑自己的线程,{ cat big.txt; } | head -1这类管道不再因输出超过管道缓冲而死锁。0014-function-call-stage-concurrency.patch:同一条规则覆盖"阶段是函数调用"的情形(f() { cat big.txt; }; f | head -1)。0015-trailing-slash-requires-directory.patch(0.1.1 新增):尾随分隔符只匹配目录,*/与*.md/不再由卷的宽松行为代答。0016-bundled-name-dispatch.patch(0.1.1 新增):引擎按自己的文件名分派到 bundled 工具(rm.exe就是rm),并在这条路上改写/tmp参数。0017-rm-refuses-a-trailing-separator-on-a-file.patch(0.1.1 新增):rm拒绝"尾随分隔符指向非目录",不再静默删掉那个普通文件。
- 每次解析都按 lock 里的 sha256 校验产物,并按盖章跳过重复校验:产物的 size 与 mtime 记进
%LOCALAPPDATA%\dsh-bash-native\verified\,没变就不再读那 15.6 MB(verifyArtifacts: 'always'可恢复每次都哈希)。
实现
- 注册
ctx.shell:ShellExecutor的 Service Provider,构建在@deepseek-ai/dsh-bash-local之上,每条命令交给解析到的 brush 引擎;引擎与工具链都随包分发,并在解析时按 lock 与 manifest 校验。 - 服从 DSH 的三档文件策略(
read-only/workspace-write/danger-full-access):受限档位把引擎 argv 交给ctx.sandbox,不限档位直接 spawn 并如实报告档位。 - 后台作业经
ctx.jobs汇报,超出内存上限的输出落到 spill 文件、路径随结果返回;echo、cat、ls、cp、rm、sort等由引擎自带并解析为 builtin,空PATH也成立。 - Web 面用自带的 agent preset 挂载,agent 面用 overlay 显式启用;它不替换宿主层的
ctx.shell,因此其他 preset 的pwsh工具不受影响。
两个预设
cordis.patch.yml 插入两个 agent preset,它们各自带一个 isolate: { shell: true, terminals: true } 的 realm,好让宿主层与其他 preset(包括用 pwsh 的那些)完全不受影响。
| 预设 | 挂载什么 | 什么时候选 |
|---|---|---|
Native Bash (Windows)(id bash-native,order 5) |
镜像 harness 的 standard 再加 shell 组:一次一条命令的 bash、可换成持久 bash、文件、作业、委派等全套工具 |
日常使用 |
Native Bash (Windows, minimal)(id bash-native-minimal,order 6) |
镜像 harness 的 minimal:一个 persona 与一个持久 bash,没有文件、作业与委派工具 |
只要一个 shell 的轻量会话 |
配置
除下表外,执行预算继承自 @deepseek-ai/dsh-bash-local:cwd、timeoutMs、maxTimeoutMs、maxOutputBytes、maxSpillBytes、graceMs,默认值与上限由那一侧的 schema 单一出处提供。
| 键 | 默认 | 含义 |
|---|---|---|
confine |
true |
是否服从 ctx.sandbox;关闭即放弃文件策略与拒绝标记 |
requireEngineOnLoad |
false |
无引擎时是加载失败,还是每次调用失败(两个预设都设 true) |
promptSection |
true |
是否贡献模型可见的环境契约小节 |
promptDetail |
full |
保留的兼容键:契约只剩两句后,两个取值产出同一段文字,设或不设都一样 |
toolsDir |
'' |
POSIX 工具链目录;空 = 先看每用户目录里自建的那份(%LOCALAPPDATA%\dsh-bash-native\tools\bin),没有程序就用随包工具链。填了则照用,空目录也算数(不会悄悄换成随包那份) |
bashPath |
'' |
引擎绝对路径;空 = 按解析顺序找 |
bundledEngineDir |
'' |
自带引擎目录,先探 brush.exe 再探 bin/brush.exe |
rcFile |
'' |
持久 PTY 会话读的启动文件;空 = %LOCALAPPDATA%\dsh-bash-native\bash-native-rc.sh |
denialSignatureAdditions |
['os error 5'] |
追加的拒绝签名,默认值让非英文 Windows 也标得出拒绝 |
shellEnvOverrides |
{} |
追加环境变量,层叠在模型友好默认值之上 |
verifyArtifacts |
'stamped' |
校验力度:stamped 只在产物或运行副本的 size/mtime 变化时重新哈希;always 每次解析都哈希(引擎 31 MB、工具链 76 MB) |
工具链
工具链随包分发,装完即用
- 目录里装 28 个名字,工具链公布其中 20 个:
grep、sed、awk、jq、find、xargs、diff、cmp、which、timeout、stat、ps,以及tty、nohup、nice、uptime、hostid、pathchk、locate、updatedb;另外 8 个(arch与 shell 内建echo、printf、pwd、test、true、false、kill)只装不公布,留作子进程要用的程序形态。 - 遮蔽
find.exe、timeout.exe这类语义无关的同名 Windows 程序;convert.exe工具链没有对应实现,因此仍会命中 Windows 的卷转换工具,别当 ImageMagick 用。 - 支持与引擎相同的
/tmp与盘符别名(仍然推荐使用$TMP),"程序再启动程序"的链同样成立:printf '/tmp/f\n' | xargs awk '{print}';引擎自带的那些名字在这条路上同样改写(补丁0016),printf '/tmp/x\n' | xargs rm拿到的是真实临时路径而不是字面量。 - 顺带把
bash、sh与引擎自带的 75 个工具名一起放到PATH上(工具链目录在前,名字目录紧随其后,两者都排在宿主原有PATH之前):src/shim.ts在%LOCALAPPDATA%\dsh-bash-native\shim\<引擎 sha256>\里把这 77 个名字指向已校验引擎——同卷时全是**硬链接**,77 个名字共用引擎那一份字节——引擎再按自己的文件名分派到 bundled 实现(补丁0016)——子进程 exec 不到 shell 内建,xargs rm、find . -exec rm {} +只能按名字找程序,工具链也就不再重复发布这 75 个名字(旧布局的 103 个名字降到 28)。于是bash script.sh、bash -c …、sh -c …与调用bash的 Makefile/npm 钩子都能用;**按路径直接执行脚本暂不成立**(./s.sh)。
已知限制
- brush 在 Windows 平台仍处于preview阶段,有存在缺陷的可能。
- shell 不为某个后台作业登记进程时,判活要用
wait而不是kill -0。 - 命令替换里的
jobs/jobs -p看到空表;kill一次只接受一个目标(kill %1 %2报 2);Windows 不送信号,任何信号都以 128+n 结束(TERM 143、KILL 137)。 - 盘符改写分不清路径与程序的脚本操作数:
awk '/x/{print}'到手是X:\{print}、sed '/x/d'报invalid command code;改写awk '$0 ~ /x/',或对那条命令设DSH_BASH_NATIVE_NO_PATHCONV=1。 - 别名改写只看文本不看意图:以
/tmp开头的数据参数也会被改写(grep /tmp/x file会去找临时路径);逃生开关同样是DSH_BASH_NATIVE_NO_PATHCONV=1,开启后所有参数原样传递,因此要自己给真实路径。 select暂不支持——含它的整条命令在解析期即失败。
许可
| 内容 | 许可 | 声明与扫描 |
|---|---|---|
| 本插件 | MIT | LICENSE |
| brush 引擎 | MIT | engine/LICENSE.brush、依赖扫描 engine/THIRD-PARTY.md |
| 随包工具链 | MIT | uutils coreutils/findutils/grep/sed、jaq、goawk、仓库内 posix-extra;toolchain/LICENSES/ 与 toolchain/THIRD-PARTY.md |
No comments yet. Be the first to write one.