dsh-pwsh-guard
把「读文件用 read、搜内容用 grep、找路径用 glob」从约定变成运行时硬约束。
插件用 ctx.tools.guard 注册一条全局守卫,每次工具调用前同步检查一遍:
pwsh / bash / ops_bash 参数里的命令一旦是「用 shell 读文件 / 搜文本」,直接拒绝执行,
并在拒绝原因里写清楚该换哪个工具。模型收到的是明确的「这么做不行 + 这么做可以」,
不是一次失败的命令。
拦截清单
| 规则 id | 组 | 拦什么 | 改成 |
|---|
search.Select-String | textSearch | Select-String | grep 工具 |
search.sls | textSearch | sls(Select-String 别名) | grep 工具 |
search.findstr | textSearch | findstr | grep 工具 |
search.grep | textSearch | grep(git grep 不管) | grep 工具 |
search.rg | textSearch | rg | grep 工具 |
read.Get-Content | textRead | Get-Content | read 工具 |
read.gc / read.cat / read.type | textRead | gc / cat / type | read 工具 |
read.more / read.head / read.tail | textRead | more / head / tail | read 工具(offset + limit) |
bypass.node-eval | runtimeBypass | node -e 脚本里出现 fs / readFileSync 等 | read / grep 工具 |
bypass.python-c | runtimeBypass | python -c 脚本里出现 open( / Path( / glob. 等 | read / grep 工具 |
bypass.php-r | runtimeBypass | php -r 脚本里出现 file_get_contents 等 | read / grep 工具 |
bypass.dotnet-io | runtimeBypass | [IO.File]::ReadAllText(...) 之类 .NET 读文件 | read 工具 |
wait.Start-Sleep | sleep | Start-Sleep | job_output 的 wait、schedule_create |
wait.sleep | sleep | sleep(Start-Sleep 别名) | 同上 |
pipeline.stderr-merge | stderrMerge | 2>&1 之后又接管道 | 看 $LASTEXITCODE,或 2>err.txt |
判定分两种视角,直接影响误报率:
- 抹掉引号的正文(默认):
git commit -m "禁止 Select-String" 只是「提到」这个名字,放行。
- 原文:
node -e "…readFileSync…" 这种脚本内容就在引号里的,必须看原文才能拦到。
短别名(gc / cat / type / sls / head / rg / grep …)只在命令位置判定
(行首、; | & ( { 之后),所以 Get-ChildItem -Type f、git gc、git log --grep=x
都不会误伤。
配置
写在 profile 的 cordis.patch.yml,也可以安装后由插件管理器写入。全部字段可省略。
- id: pwsh-guard
name: '@local/dsh-pwsh-guard'
config:
enabled: true # 总开关,默认 true
tools: [pwsh, bash, ops_bash] # 参与检查的工具名,默认 [pwsh, bash, ops_bash]
disabledGroups: [] # textSearch / textRead / runtimeBypass / sleep / stderrMerge
allowPatterns: [] # 命中即整体放行(正则,忽略大小写)
rules: [] # 追加自定义规则,排在内置规则之前
自定义规则示例:
rules:
- id: no-force-push
pattern: 'git\s+push\s+--force'
title: '禁止强推'
hint: '改用 --force-with-lease'
group: custom
想让常驻终端(terminal_send)也受管,把 tools 写成 [pwsh, bash, ops_bash, terminal_send]。
默认不含它是刻意的:该工具的 text 有可能整段脚本/文件内容,容易误伤。
ops_bash 是 dsh-ops 提供的另一个 shell 入口(装了 dsh-ops 就有,而且通常是首选执行器),
默认在名单里 —— 漏掉它,规则等于没装。它用 script_path 跑脚本文件时,守卫只看得到路径、
看不到脚本体,这条按已知缺口处理。
安装 / 卸载 / 改完怎么生效
安装(对插件所在目录执行一次,会把包 link: 进当前 profile 并选中 bundle):
plugin_manager action: install_bundle target: <clone 下来的 dsh-pwsh-guard 目录的绝对路径>
例如(Windows):target: D:\project\dsh-plugins\dsh-pwsh-guard。
已知安装问题:install_bundle 可能回报 failedAt: registry,而失败来自 profile 里另一个依赖
(例如 dsh-ops 依赖的 @dsh-ops/bash-win32-x64@0.2.1 在 npmmirror 上不存在),pnpm 整轮解析失败。
这跟本插件无关 —— 此时包链接、package.json 依赖、dsh.profile.bundles 往往已经写好了。
判定用事实:<profile>/node_modules/@local/dsh-pwsh-guard/index.js 存在,
且 profile 的 cordis.yml 里有 id: pwsh-guard 这一行。别为了绕开它去换国外 registry,容易卡住。
改完代码/配置怎么生效(不需要重启 DSH):
- 先拿到入口 id:
cordis_inspect_query → host/Config/listConfigs → name: '@local/dsh-pwsh-guard'
→ 返回的 id(形如 include:pwsh-guard)。
plugin_manager action: set_plugin target: <那个 id> enabled: false,再 true。
一次禁用+启用就会重新 apply()、重新 import 模块,application: applied 即已生效。
卸载:plugin_manager action: remove_bundle target: @local/dsh-pwsh-guard。
验证
三条命令就能确认装对了(装好后 AI 自己也会被这套规则管住):
| 命令 | 期望 |
|---|
pwsh: Start-Sleep -Seconds 1 | 被拦,规则 wait.Start-Sleep |
ops_bash: cat /etc/hosts | 被拦,规则 read.cat(证明 ops_bash 也在名单里) |
ops_bash: echo ok | 正常输出 ok(证明没有一刀切) |
自测
引擎是纯函数,不需要 DSH 就能跑:
node test/check.mjs
覆盖 36 条命令用例(21 条必拦 + 13 条必放行 + 2 条已知缺口)和 8 条配置断言。
改规则后先跑它,再装。
已知缺口
- 引号包住命令:
& "Select-String" -Path a.txt、powershell -Command "Get-Content a.txt"
拦不到 —— 抹引号换来的低误报,代价就是这两类写法。这类不是模型的自然写法。
-EncodedCommand:Base64 编码的命令行不解析。
ops_bash --script_path:只看得到路径,看不到脚本体。
git grep 放行:它是 git 子命令,不是 shell 的 grep。
- 不拦目录列举:
Get-ChildItem / ls / dir 照常可用;递归范围是使用方自己的规定,
不在本插件职责内。
- 不是安全边界:目的是纠正习惯,不是防越权;也不是沙箱,不限制进程、权限和网络。
要防控破坏性命令、密钥访问这类风险,另配一个专门的 guard(例如
cc-safety-net)。
为什么不直接配 cc-safety-net
cc-safety-net 用的是同一套机制(ctx.tools.guard + pwsh → arguments.command),
但它的规则模型表达不了「一律禁用某个命令」(以 2.6.x 的规则 schema 为准):
- v1 规则是
command + 可选 subcommand + 必填 block_args(至少一个参数),命中靠「某个参数出现过」。
- v2 规则是
command + 必填且非空 match.command_path(必须精确等于命令行里紧跟的若干个词)。
- 官方文档明确:
Literal and case-sensitive: No regex, glob, or substring matching.
后果:Select-String -Path a.txt 能被某条带 -Path 的规则命中,Select-String a.txt 不会;
node -e "…readFileSync…" 这种「解释器 + 脚本内容」的组合更是没法写。
实测 cc-safety-net explain "Select-String -Path .\a.txt -Pattern foo" 输出 Status: ALLOWED。
所以分工是:cc-safety-net 负责破坏性命令与密钥访问,dsh-pwsh-guard 负责工具使用纪律 ——
两者互补,各管一摊。
设计要点
- 守卫是同步的(
ToolGuard 返回字符串即拒绝),所以插件里不做任何 IO,纯正则判定。
- fail-open:守卫内部异常、配置写错、非法正则,一律放行。宁可漏拦一次,也不能让整机 shell 卡死。
- 无 Config schema:不引入任何依赖(链接目录解析不到 profile 的 node_modules),配置在代码里归一化,
非法值退化成默认行为而不是报错。
- 自定义规则排在内置规则之前,便于用同 id 覆盖内置判定顺序。