@jayyuen66/dsh-danger-guard 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +229 -2
- package/client.js +613 -0
- package/cordis.patch.yml +27 -0
- package/host.js +2718 -0
- package/icon.svg +1 -0
- package/package.json +101 -3
- package/settings.js +135 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JayYuen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,230 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @jayyuen66/dsh-danger-guard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文](#中文) · [English](#english)
|
|
4
|
+
|
|
5
|
+
## 中文
|
|
6
|
+
|
|
7
|
+
### 它做什么
|
|
8
|
+
|
|
9
|
+
- 只挂一个挂点的工具调用前置守卫:`ctx.tools.guard`(host.ts)里的同步谓词,返回字符串即物理拒绝该次调用。
|
|
10
|
+
- 宿主把理由作为该次工具调用的错误结果回给模型:模型拿到的是"为什么拦 + 改成什么",不是静默失败。
|
|
11
|
+
- 它做三条防线:
|
|
12
|
+
- bash/pwsh 危险命令:lib/danger-rules.ts 的 `bashDanger`——git 钩子绕过、灾难性 rm、下载即执行、长驻 dev server。
|
|
13
|
+
- 编辑/写类工具的密钥路径:`secretPathDeny`。
|
|
14
|
+
- 首次编辑事实强制门:lib/fact-gate.ts 状态机 + host.ts 的取证判定。
|
|
15
|
+
- 另有一条"需确认"而不是"直接拒"的分支(规则键 `nestedShellUnconfirmed`):嵌套 shell 的 `-c` 命令体取不出可靠文本(引号不闭合)或套娃超过 16 层下钻上限时命中。
|
|
16
|
+
- 回的文案是"贴给用户、由用户确认后再执行",因为这既不是"已确认危险"也不是"已确认安全"。
|
|
17
|
+
|
|
18
|
+
### 安装
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
dsh plugin --profile web add @jayyuen66/dsh-danger-guard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- 包在公共 npm(`registry.npmjs.org`)上,安装不需要凭据。
|
|
25
|
+
- 不再需要配作用域源:公共 npm 就是默认源。卸载:`dsh plugin --profile web remove @jayyuen66/dsh-danger-guard`。
|
|
26
|
+
- 要求宿主 `dsh >= 0.2.0-rc.2`:真源是 `package.json` 里 `peerDependencies` 下的 `@deepseek-ai/dsh`(`engines.dsh` 同值,但宿主不读它)。发布产物只有 `host.js` / `settings.js` / `client.js` / `cordis.patch.yml`。
|
|
27
|
+
- 运行时依赖四枚,全在 `dependencies`:`@deepseek-ai/schemastery`(宿主 fork,`3.18.4`)、`@jayyuen66/dsh-plugin-shared`(`workspace:^`)、`@deepseek-ai/dsh-brand`(`0.2.0-rc.2`)与 `zod`(`^4.6.5`)。MIT,源码 git+https://github.com/JayYuen666/dsh-danger-guard.git。
|
|
28
|
+
- `zod` 的真值导入点是 `lib/tool-ledger.ts` 的 `stateSchema`(投影态回填的形状校验),`@deepseek-ai/dsh-brand` 的是 `host.ts` 的 `brandNumber`。
|
|
29
|
+
- 后者是「服务面 `@deepseek-ai/*` 一律 type-only」的例外那一族:幻影品牌没有 type-only 的造法,这类值导入必须落 `dependencies`——`build-host.mjs` 的外部化名单只读 `dependencies`∪`peerDependencies`,放 `devDependencies` 就会被 rolldown 把官方函数体内联进产物、每包各持一份官方构造器。
|
|
30
|
+
- `host.js` 里 `from "zod"` 与 `from "@deepseek-ai/dsh-brand"` 两枚裸说明符都由 `test/build-host.test.ts` 钉着「说明符在、官方函数体不在」;设置半不导入 brand,所以 `settings.js` 里没有那枚说明符(同一条测试反向钉)。
|
|
31
|
+
|
|
32
|
+
### 在 dsh 里启用
|
|
33
|
+
|
|
34
|
+
- 组合包(bundle)形态:`package.json` 的 `dsh.bundle.patch` 指向 `cordis.patch.yml`,由 `dsh plugin add/remove` 维护装配层。
|
|
35
|
+
- 包内是**两行**(bulkhead / 隔舱):`- id: danger-guard`(拦截半,**没有 Config**,name 指向包本体)与 `- id: danger-guard-settings`(设置半,持有严格 schema,name 指向 `./settings` 子路径导出)。
|
|
36
|
+
- 卡片只由第一行带出:宿主只对**裸包名**条目扫 `dsh.client`(`dsh-client-modules` 的 `exactPackageSpecifier` 对子路径返回 undefined),所以设置行的 name 写成子路径是有意的,不是漏改。
|
|
37
|
+
- 卡片只在 web 平台装载(`dsh.client = { platform: "web", immediately: true }`),位置是设置页插件列表里的「danger-guard 危险拦截」。
|
|
38
|
+
- 部署默认写在**设置那一行**(`- id: danger-guard-settings`)的 `config:` 上:优先级为 设置卡运行时值 > 行 config > `BUILTIN_BASE`。
|
|
39
|
+
- 写在拦截那一行没有效果——它的 `apply(ctx)` 不收 config 参数(该条目刻意没有 `Config`),值面只经设置条目挂出的读数口取。
|
|
40
|
+
- 行里的显式 `undefined` 不覆盖底座;非法数值(如 `maxDenies: 0`)回落内置默认——0 等于把事实门整道关掉。
|
|
41
|
+
- 装载失败的方向是保守的:设置面在独立条目 `danger-guard-settings` 上,那份严格 schema 只挂它。用户配置越界(`maxDenies: 0`、`strictMode: "yes"`…)只让**设置条目** FAILED;拦截条目没有 Config,读不到值面就回落内置默认底座并告警一次——一行手改坏的配置杀不掉闸门。
|
|
42
|
+
- `apply` 的抛错条件只有**设置面缺件**:宿主没有 `effect`、或 `settings` 不带 `describe` 时直接抛 `required services (settings/effect) missing`,该条目 FAILED(`host.ts` 的服务面守卫只探这两件)。
|
|
43
|
+
- ⚠ 缺 `tools` **不抛错**:`svc.tools?.guard(...)` 拿不到注册口就跳过挂载(`host.ts` 的可选链 + `dispose === undefined` 分支,行为由 `test/host.test.ts` 钉住),`apply` 里没有针对它的守卫。
|
|
44
|
+
- 但部署上这不叫静默放行:`tools` 在 `inject` 清单里,服务缺席时 cordis **不激活**该条目,宿主启动诊断会打 `warning: N entry did not activate` 并逐条列 `pending (waiting for service: tools)`(宿主 `dsh-app-boot` 的 activation 报告)。看到的现象是"闸门没装 + 启动有话",不是"闸门没装 + 一切正常"。
|
|
45
|
+
|
|
46
|
+
### 设置项(命名空间 `danger-guard-settings` = 设置条目 id,默认值即 `lib/settings-schema.ts` 的 `BUILTIN_BASE`)
|
|
47
|
+
|
|
48
|
+
| 字段 | 类型 | 默认值 | 作用 |
|
|
49
|
+
| ---------------------- | ---------------------------- | ----------------------------- | ----------------------------------------------------------------------------- |
|
|
50
|
+
| `enabled` | boolean | `true` | 总开关;`false` 时三类判定全部静默(连教训上报也不发) |
|
|
51
|
+
| `factGateEnabled` | boolean | `true` | 只关首次编辑事实门,危险命令与密钥面照拦 |
|
|
52
|
+
| `maxDenies` | natural,1–5 | `2` | 同一目标连续"未收敛"拒绝的次数上限,超过即配额放行并告警 |
|
|
53
|
+
| `strictMode` | boolean | `false` | `true` 时忽略写面分档,每个代码文件首次编辑都走全探查 |
|
|
54
|
+
| `smallEditChars` | natural,≥1(卡片 1–2000) | `200` | 写面 ≤ 此值且不含导出/签名关键字 → 判小改,只要求 read |
|
|
55
|
+
| `bigEditChars` | natural,≥2(卡片 2–100000) | `2000` | 写面 > 此值 → 判大段重写,升最高档全探查 |
|
|
56
|
+
| `extraTestDirs` | string[] | `[]` | 追加 test 层目录名(内置 `test/tests/__tests__/spec/specs`),只追加 |
|
|
57
|
+
| `extraDevServerWords` | string[] | `[]` | 追加长驻命令头(内置 `vite/webpack/next/nuxt/cargo-watch/watchexec`),只追加 |
|
|
58
|
+
| `extraDevRunArgs` | string[] | `[]` | 追加 run 脚本名(内置 `dev/watch/serve/start/start:dev`),只追加 |
|
|
59
|
+
| `extraSecretPatterns` | string[] | `[]` | 追加密钥路径正则片段(卡片每行一条;非法片段跳过并告警),只追加、无白名单 |
|
|
60
|
+
| `refSearchTools` | string[] | `["grep","glob","zg_search"]` | 哪些检索工具算"引用探查"凭证;清空 = 任何检索都不计入 |
|
|
61
|
+
| `refSearchStrictTools` | string[] | `["zg_search"]` | 上表里改走"root + query/fts/vector 相对路径"严格口径的子集;只写这里不给凭证 |
|
|
62
|
+
|
|
63
|
+
### 拦截与豁免的判定
|
|
64
|
+
|
|
65
|
+
| 四类命令规则的共同口径 | 内容 |
|
|
66
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| 判定范围与分段 | 只判工具名 `bash` 与 `pwsh`(`command` 非字符串则放行,不猜);按 `;`、`&&`、`\|\|`、`\|`、换行、`&`、`(`、`)`、反引号逐段判、引号内不切段 |
|
|
68
|
+
| 包装词、bin 前缀、引号、续行 | 都不是逃逸口:包装词(`sudo env time nice stdbuf timeout taskset watch setsid doas pkexec noglob nohup exec command` 与前导 `VAR=x`)、bin 路径前缀(`/bin/rm`)、引号包裹与反斜杠续行 |
|
|
69
|
+
| 原则与唯一例外 | 认不准就放行;唯一例外是嵌套 `-c` 体——取不出可靠文本或超过 16 层时判 `nestedShellUnconfirmed`,出路是交用户确认或改写成单层闭合引号形态 |
|
|
70
|
+
|
|
71
|
+
| 四类命令与密钥路径各自的判据 | 拦 | 放行与出路 |
|
|
72
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
73
|
+
| git 钩子绕过 | `commit/push/merge/cherry-pick/rebase/am` 上的 `--no-verify` 及其不歧义缩写(`--no-v`…)、`commit`/`rebase` 的短形式 `-n`、`git -c core.hooksPath=` 与 `git config core.hooksPath <dir>`(含 `--global` 与 `=` 合写) | `git push -n`(dry-run)、`git am -n`/`cherry-pick -n`、`git config --get/--unset/--list core.hooksPath`、`git status --no-verify`、以及提交信息里出现的该字样 |
|
|
74
|
+
| 灾难性 rm | 门槛**按命令词轨分别**,不是无条件性质:**POSIX/cmd 轨**要"递归 + 强制",唯一例外是短旗标簇里的大写 `-R` 单独即灾难级(`rm -r ~` 放行、`rm -R ~` 拦);**`Remove-Item`/`ri` 轨**任何 Recurse 形态**单独**命中灾难目标即灾难级、不要求 `-Force`(`-Recurse` ≡ `-recurse` ≡ `-rec` ≡ `-r` ≡ `-R`:参数名不区分大小写,它们是**一枚参数**而不是四种拼法。两轨不对称是语义不对称的镜像:`Remove-Item -Recurse ~` 不给 `-Force` 也**静默删整树**,POSIX `rm -r ~` 不给 `-f` 会在受保护项上逐个提示)。短旗标簇**整簇宽松扫字母、不设封闭字母表**(`-rf`/`-drf`/`-RWx`/`-srf` 同判;`--recursive --force` 长旗标;cmd 内建另认 `/s` `/q` `/f`);`Remove-Item`/`ri` 换一面——只有这两个命令词上的 `-字母…` 一律按**参数名**认(`-Recurse`/`-Force` 与其无歧义缩写 `-rec`/`-fo`/`-r`),不进簇扫;参数名那一面在 `rm` 与 cmd 内建上是**叠加**在簇扫之上的,因为簇扫大小写敏感、`-Force` 在簇里只取得到 `r`)**且**目标是 `/`、`~` 与 `~/…`、家目录字面路径(`<家目录>`,取 `os.homedir()` 即 HOME 环境变量)、`$HOME` 与 `${HOME…}` 各形态、`.`/`..`/`../…`、系统与用户目录前缀的整树(`/etc`、`/usr`、`/bin` 等,完整表见 `SYSTEM_PATH_PREFIXES`)、绝对 glob(`/*`、`/usr/**`);路径先做词法归一(`/tmp/../` 判成 `/`)。命令词面含 pwsh/cmd:`Remove-Item`/`ri`/`del`/`erase`/`rd`/`rmdir`(小写比对,带反斜杠的路径前缀同剥);Windows 形态目标(盘符根 `C:\`、系统与用户树 `C:\Windows`·`D:\Users\x`、UNC 共享根 `\\server\share`、家目录的 Windows 字形(比的是宿主 `os.homedir()` 折出的 `winHome`:家目录本身是盘符形态时整树与其子路径同判;家目录是 POSIX 字形时这一形不成立,`<POSIX 家目录>\x` 只有恰好落在系统前缀表里才拦,如 macOS 的 `/Users/…`)、`~\notes.txt` 这一形(波浪号 + 反斜杠写的家目录子路径,**只**在 Windows 读法里算:pwsh 中 `~` 处处等价家目录、`\` 是合法分隔符;`~\`、`~\\notes.txt` 同判;波浪号后接词字符的 `~xyz`、`~mailbox\notes.txt` 不算家目录,与 `$HOME` 那条同一个尾闸)——**这一轨**只在**宿主是 win32** 或**调用工具是 `pwsh`**(方言由工具身份给,不从字形猜)时参与;两条**环境变量形态**不在这轨上:系统根 `%SystemRoot%\System32`、`$env:windir` 与用户根 `%USERPROFILE%\Documents`、`$env:USERPROFILE` 是**未展开**的字面判据,与 `$HOME` 同构、不读环境变量,也**不随轨道/平台退出**(非 Windows 宿主 + `bash` 名下照拦),变量名之后须接分隔符或词尾 | 项目内相对清理(`rm -rf node_modules`、`./dist`、`src/old`、`rd /s /q build`)、POSIX/cmd 轨上只有 `-f`(`rm -f ~`)或只有小写 `-r`(`rm -r ~`、`rm --recursive ~`)——**这一档按写法而非语义**:同是"只有递归",`rm -R ~`、`rm -Recurse <家目录>` 因大写 `R` 走那条单独即灾难级的既有判据;同是"只有强制",`rm -Force <家目录>` 因簇扫给出 `r`、参数名面补上 `f` 而落进拦(那是 pwsh 长词叠在 POSIX 轨上的既有取舍,宁可误拦);cmdlet 轨上门槛同样**以灾难目标为前提**:写法里**不含任何 Recurse 形态**才放行,而**有 Recurse 但目标不是灾难路径**也照放(`Remove-Item -Recurse ./build` 放行)(`Remove-Item -Force <家目录下某文件>`、`Remove-Item -Filter *.log <树>` 照放,`-Recurse`/`-recurse`/`-rec`/`-r`/`-R` 任一形态配灾难目标即拦,不要求 `-Force`)、`Remove-Item -rf C:\Users\bob`(`-rf` 不是参数名,pwsh 自己报参数找不到;对照 `rm -Force -LiteralPath <家目录下某文件>` 在 pwsh 里**仍被拦**——命令词是 `rm` ⇒ 走 POSIX 宽松簇那一轨,宁可误拦)、`/var` 与 `/tmp`(有意不列:误拦代价高于漏拦)、`/usrx` 这类不同段,以及非 Windows 宿主上 `bash` 名下的 `C:\Windows` 与 `\\home\bob`(那是字面相对路径,回到 POSIX 读法)、同一种读法下的 `~\notes.txt`(bash 不展开这枚波浪号:`\n` 被当转义,实测本机 `printf '%s\n' ~\notes.txt` 给出 `~notes.txt` 那样的相对文件名 ⇒ 放行是正确语义,不是漏拦) |
|
|
75
|
+
| 下载即执行 | `curl\|wget\|fetch` 之后接 shell 解释器即拦,含 `\| sudo sh`、`\| /bin/bash`、`\| xargs -I{} sh {}`、`\| parallel sh`,以及反序的 `sh <(curl …)`、`source <(curl …)`、`. <(curl …)` | `curl -O`、`curl … \| jq .name`、`cat x \| grep foo`、无下载来源的 `ls \| sh`、`source ./setup.sh` |
|
|
76
|
+
| 长驻 dev server | `vite`/`next`/`nuxt` 裸调用、`webpack serve\|--watch`、`cargo watch`、`npm\|pnpm\|yarn\|bun run dev\|watch\|serve\|start\|start:dev` 与一切 `dev` 词根脚本名(`dev:web`、`dev-server`、`dev:build`)、`npx\|bunx\|exec\|dlx <长驻包>`;`vite build --watch` 仍拦 | 一次性形态 `npm test`、`npm run build`、`vite build`、`next build`、`webpack --config prod.js`、`cargo run/check/test`、`vitest run`、`npm exec vite -- build` |
|
|
77
|
+
| 密钥路径编辑 | `.ssh`/`.gnupg`/`.aws`/`.kube`/`.docker` 目录内任何文件、`id_rsa\|id_ed25519\|id_ecdsa` 主名(含 `.pub` 与 `._-` 后缀拷贝)、`.pem\|.key\|.p12\|.pfx\|.keystore\|.jks` 结尾、`.env` 全族、`.envrc\|.npmrc\|.pypirc\|.netrc\|.pgpass\|.git-credentials`、`credentials.json`/`service-account*.json`/`token.json`、`secret(s).yaml\|yml\|json`、`.git/config`;反斜杠 Windows 路径(`<家目录>\.ssh\id_rsa`)同判 | 放行的反例:`.env.example`/`.sample`/`.template`/`.defaults`、`api.key.ts`、`main.pem.tsx`、`.gitignore`、`mysecrets.yaml`、`secrets.yaml.bak`、`myid_rsa` |
|
|
78
|
+
| 密钥路径的工具面 | `edit`/`write`(`file_path` 或 `path`)与 `str_replace_editor` 的 `create`/`str_replace`/`insert` | `read` 与 `view` 只读不拦 |
|
|
79
|
+
|
|
80
|
+
| 事实强制门、配额与误拦出路 | 内容 |
|
|
81
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| 事实强制门的适用与分档 | 只判带目标路径的 `edit`/`write`/`str_replace_editor`,按写面风险分三档 |
|
|
83
|
+
| low | 要求成功 `read` 过目标 |
|
|
84
|
+
| mid | 再加一次项目级引用探查(起点是目标的祖先目录,签名含目标名或 ≥4 字词干;严格口径另认可凭相对路径片段命中) |
|
|
85
|
+
| high | 再加对**实际存在**的 test/e2e/docs 层各一次可归因检索(不存在的层由 fs 探测直接豁免) |
|
|
86
|
+
| 升 high 的信号 | 新建(`write`/`create`)、触及导出/签名关键字、manifest·lockfile·构建配置·装配文件·`schema.sql` 等高风险路径、写面 > `bigEditChars`、取不到写面大小;`strictMode` 一律 high |
|
|
87
|
+
| 不过门与证据口径 | `md/markdown/txt/rst/adoc` 完全不过门,`test/` 目录不再豁免;证据只认会话里**成功**的调用(报错或被门禁拒掉的不算);拿不到会话历史时走"请用户拍板"的交互确认且不消耗配额;确凿新建(ENOENT 且该调用能创建)免 read 要求 |
|
|
88
|
+
| 配额放行与有效期 | 同一目标连续未收敛拒绝 `maxDenies` 次后(缺项集合严格变小算收敛、计数重置),下一次直接放行并 `console.warn`;该免检对同一目标持续有效,直到目标被会话外改动、该会话分片被 LRU 淘汰(64 会话 × 每会话 200 文件)或插件重载 |
|
|
89
|
+
| 误拦的三条路 | 本包没有任何"白名单/放行名单"设置项,所以误拦只有三条路:临时关 `enabled`(或只关 `factGateEnabled`)——设置卡或注册行 `config:` 皆可;靠 `maxDenies` 配额放行;按拒绝文案改写命令形态(一次性构建、项目内相对路径、单层闭合引号),长驻服务由用户自己在终端起 |
|
|
90
|
+
|
|
91
|
+
### 对外接口
|
|
92
|
+
|
|
93
|
+
| 接口面 | 内容 |
|
|
94
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
95
|
+
| 宿主扩展点 | 消费 `ctx.tools.guard`(唯一拦截面,`inject: ["tools", "settings"]`);**拦截条目刻意不带 `Config`**——设置面在同 profile 的另一个条目 `danger-guard-settings` 上(它 `inject: ["settings"]` 并导出 `Config`,0.1.7 由宿主隐式注册、命名空间即该条目 id,本包不调用任何注册 API),这样一行被改坏的用户层 config 只会让设置条目 FAILED,闸门照旧拦;跨命名空间只经 `settings.describe()` 读官方 `locale` 那一条的 `preference`(决定回给模型的文案语言,条目未投影即中文);订 `session/disposed` 释放事实门分片;另有一条**可选**子注入 `ctx.inject(["sessionProjections"], …)`,把 `danger-guard.toolLedger` 单元挂上并把 `stateOf()` 读口装进守卫,注册表缺席时那条回调根本不激活、判定沿用回退扫描——它刻意不写进插件级 `inject`,否则没装投影包的 profile 会整条闸门下线 |
|
|
96
|
+
| 不提供与可选消费 | 不注册工具、不提供 HTTP 端点、不加 CLI 子命令。可选消费 `ctx.get("lessonLoop")` 的 `report` / `pass`:`source: "danger-guard"`,category 三类 `dangerous-bash` / `secret-path` / `factgate-deny`,签名分别是命中命令原文、`bash-nested-shell-unconfirmed`、`edit-before-factgate` |
|
|
97
|
+
| 包导出面与卡片 | 总线缺失、抛错或返回 rejected Promise 都不影响拦截,只丢一条告警。`.` → 拦截半入口(仓内 `host.ts`,发布产物 `host.js`)、`./settings` → 设置半(仓内 `settings-host.ts`,发布产物 `settings.js`)、`./client` → `client.js`、`./cordis.patch.yml`、`./package.json`;卡片注入 slot `plugins.bundle.config`、key = bundle 包名 `@jayyuen66/dsh-danger-guard`(该槽按 bundle 包名 keyed:宿主派发 `entryKey: pkg.name`,见 `~/.dsh/profiles/web/package.json` 的 `dsh.profile.bundles`;写成裸条目 id 就是卡片不渲染),而 `configForms.get()` 与 settings 命名空间仍是 bulkhead 的设置条目 id `danger-guard-settings`——两个标识不是一回事。React 外部化后包成模块加载器条目,改动先进草稿、点「保存」才写入 settings,作用域只读(非本机 loopback 页面)时控件禁用并显示只读状态 |
|
|
98
|
+
|
|
99
|
+
### 数据与隐私
|
|
100
|
+
|
|
101
|
+
| 隐私口径 | 内容 |
|
|
102
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
103
|
+
| 不写盘、不联网 | 判定链没有任何 fs 写入、网络请求或子进程调用,输出面只有 `console.*` 告警与回给模型的文案 |
|
|
104
|
+
| 读什么 | 会话事件历史里的 tool 台账(主路径:`ctx.sessionProjections` 上注册的 `danger-guard.toolLedger` 单元,守卫里走 `stateOf()` 同步水位读;注册表缺席或台账不可采信时回退 `session.snapshotEvents(0)` 全量扫描,判定逐字相同);触发回退的三种情形:台账行数越过 `LEDGER_WINDOW`(2000)、`event.seq` 相对已折长度跳号(折叠起点不在日志开头)、回填形状不符;目标文件与层目录的 `statSync`(存在性、mtime、项目根标记);"目标疑似被会话外改动"时对目标全文的一次 `readFileSync`(内容自证);家目录取 `os.homedir()`,即只读 HOME 环境变量 |
|
|
105
|
+
| 记什么 | 全部在进程内存里(事实门按会话分片 64 × 200 文件,判定台账按 LRU 各 200 条淘汰),宿主进程退出即消失,`<dsh 数据目录>` 下不留本包的文件 |
|
|
106
|
+
| 出域的只有两处且都可关 | 装配 lesson-loop 时 `report` 载荷含 cwd、session id、命中命令原文或目标路径与拒绝文案(`enabled: false` 时连上报也不发);设置值由宿主持久化在 settings 的 `danger-guard-settings` 命名空间里 |
|
|
107
|
+
|
|
108
|
+
### 常见问题
|
|
109
|
+
|
|
110
|
+
| 问题 | 怎么办 |
|
|
111
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
112
|
+
| 拦错了合法命令怎么办? | 三份 `extra*` 名单与 `refSearchTools` 都只能收紧、没有白名单,所以只有三条路:临时关 `enabled`、靠 `maxDenies` 配额放行、或按拒绝文案改写命令形态 |
|
|
113
|
+
| 事实门一直不放行? | 它只认成功的动作:照消息把检索起点指向项目根这类祖先目录(搜文件自身或兄弟目录算"作用域过窄");用了别的检索插件就把工具名加进 `refSearchTools`(名单为空时消息会直说原因);实在取不到证就走配额放行或临时关 `factGateEnabled` |
|
|
114
|
+
| 怎么确认它在工作? | 设置页出现该卡片行、`dsh --profile web --dump-config` 出现本包层与 `- id: danger-guard` 即已装载;宿主版本需 `>= 0.2.0-rc.2`。已知边界按**类**登记,每类各钉了现状用例:① **包装词那一类**——段头是"另一个解释器/包装器"时其后的整树删除进不到命令词表,`cmd.exe /c rd /s /q C:\Windows`、`cmd /c …`(去 `.exe`)、`pwsh -c "…"`、`powershell -Command "…"` 四种活拼法**都**判不到(补这一层要跳包装词,并连带定策「`.exe` 后缀算不算命令词」,这点本仓已明确保守);② **转义折叠那一类**——嵌套 `-c` 体里 `rm -rf C:\Windows` 的反斜杠被当转义吃掉,交给内层的已是 `rm -rf C:Windows`,不再是任何 Windows 形状 ⇒ 放行(同串的正斜杠写法 `bash -c "rm -rf C:/Windows"` 照拦,所以方言继承本身没问题,缺的是"字面反斜杠 vs 转义"的判读);③ **MSYS/Cygwin 挂载形态**——真实 Windows 宿主上 `rm -rf /c/Windows/System32` 与 `/C:/Windows` 判不到:要把 `/c` 折回盘符就得带一张挂载表,属新依赖加新设计决定,路线图「明确不做」已记。另:④ **dry-run 修饰不进判据**(over-block 那一侧的现状)——`Remove-Item -WhatIf -Recurse ~` 照拦:`-WhatIf`/`-Confirm` 只是让 pwsh 打印将做什么,本闸不读它们,dry-run 与真删同判;收掉它要先定策"dry-run 算不算一类可豁免形态"(`-WhatIf` 自己也可能被 `-w` 这类歧义缩写撞开),测试里按现状钉住。⑤ **无盘符的根相对 Windows 形态(现已收)**——`\Users\bob\notes.txt` 在轨道参与时(`pwsh` 方言 / win32 宿主)照拦,POSIX 读法照放(bash 把 `\U` 当转义吃掉,同一串是相对文件 `Users…`,拦它才是误拦)。收法**不猜盘符字母**:只把"前导单枚 `\` = 当前盘根"这一形接进一条与系统与用户树**严格同族**的树名表(`windows`/`programdata`/`program files`/`users`),所以既没拆掉 `^[a-z]:` 那条前提,也没把 `\etc`、`\usr` 这类相对删除接进 24 条 POSIX 系统前缀表(那正是原注释说的"硬猜必然误拦",它仍然成立,故 `\etc` 的对照留在「Windows 字形归一不得改写 POSIX 判据的入参」那组)。同一批收掉的还有混分隔符的家目录后代:家目录写成 POSIX 字形再用 `\` 下钻(`/data/bob\.ssh`)在轨道参与时算家目录之内。⑥ **裸 `~user`(词尾无 `/`)不收**——`rm -rf ~root` 在真实 bash 里展开成 `/var/root`(本机实测),而 `~bob`/`~backup` 因这台机器上没有该账号而原样不展开:结论取决于本机口令表,判据既不碰 fs 也不查用户(查它 = 新依赖 + 新设计决定,与 ③ 同族),收裸形会误拦以 `~` 开头的相对文件名。**带分隔符的 `~user/…` 已收**(`rm -rf ~bob/x`、`~root/.ssh` 与 `$HOME` 同判,方言侧不放松),它落在"展开后必在某个家目录树内"这一族。方言入口只认 `bash`/`pwsh` 两个工具名(日后注册 `cmd`/`powershell` 要在映射里补一行,不从字形反推);嵌套 `-c` 体沿用调用方的方言,换一层 shell 不换读法 |
|
|
115
|
+
| 同一条纪律的**已知误拦(⑥b)**:账号不存在时 bash 原样不展开,`rm -rf ~bob/x` 就真是个以 `~` 开头的相对路径名(本机 `dscl` 查无 `bob`/`backup`/`dev`)⇒ 本包按「宁可误拦」处理,与引号里的 `"$HOME"` 同族(那一形永不展开,也照拦)。 |
|
|
116
|
+
|
|
117
|
+
## English
|
|
118
|
+
|
|
119
|
+
### What it does
|
|
120
|
+
|
|
121
|
+
- A pre-execution tool guard with exactly one hook: a synchronous predicate on `ctx.tools.guard` (host.ts); returning a string physically rejects that call.
|
|
122
|
+
- The host hands the reason back to the model as that tool call's error result, so the model gets "why it was blocked + what to change instead" rather than a silent failure.
|
|
123
|
+
- Three defences:
|
|
124
|
+
- Dangerous bash/pwsh commands (`bashDanger` in lib/danger-rules.ts - git hook bypass, catastrophic rm, download-and-run, long-running dev server).
|
|
125
|
+
- Secret/credential paths on edit/write tools (`secretPathDeny`).
|
|
126
|
+
- The first-edit fact gate (state machine in lib/fact-gate.ts plus the evidence verdict in host.ts).
|
|
127
|
+
- One branch asks for confirmation instead of declaring danger (rule key `nestedShellUnconfirmed`): it hits when the `-c` body of a nested shell cannot be extracted reliably (unbalanced quotes) or the nesting goes past the 16-level drill-down limit.
|
|
128
|
+
- The text sent back says "paste this to the user and run it only after they explicitly confirm", because it is neither "confirmed dangerous" nor "confirmed safe".
|
|
129
|
+
|
|
130
|
+
### Install
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
dsh plugin --profile web add @jayyuen66/dsh-danger-guard
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- The packages are on the public npm registry, so installation needs no credentials.
|
|
137
|
+
- No registry line is needed any more: the public npm registry is the default. To remove:
|
|
138
|
+
- Requires host `dsh >= 0.2.0-rc.2`: the source of truth is the `@deepseek-ai/dsh` entry under `peerDependencies` (the host checks it on plugin install from 0.1.7-rc; alpha.1 has no such gate yet, so this documents rather than enforces).
|
|
139
|
+
- `engines.dsh` carries the same value but nothing reads it. The published artifact holds only `host.js` / `settings.js` / `client.js` / `cordis.patch.yml`.
|
|
140
|
+
- Four runtime dependencies, all under `dependencies`: `@deepseek-ai/schemastery` (the host's fork, `3.18.4`), `@jayyuen66/dsh-plugin-shared` (`workspace:^`), `@deepseek-ai/dsh-brand` (`0.2.0-rc.2`) and `zod` (`^4.6.5`).
|
|
141
|
+
- `zod` is imported for value by `lib/tool-ledger.ts` (its `stateSchema`), `@deepseek-ai/dsh-brand` by `host.ts` (`brandNumber`).
|
|
142
|
+
- The brand package is the exception to "every `@deepseek-ai/*` **service surface** is type-only": phantom brands have no type-only way to be built, so this family of value imports must live in `dependencies` - the externalization list in `build-host.mjs` reads only `dependencies` ∪ `peerDependencies`, and a `devDependencies` entry lets rolldown inline the official function body into the artifact, giving each package its own copy of the official constructor.
|
|
143
|
+
- `test/build-host.test.ts` pins both bare specifiers in `host.js` (`from "zod"`, `from "@deepseek-ai/dsh-brand"`) together with the absence of the official function bodies; the settings half never imports brand, and the same test asserts the reverse for `settings.js`.
|
|
144
|
+
- MIT, source at git+https://github.com/JayYuen666/dsh-danger-guard.git.
|
|
145
|
+
|
|
146
|
+
### Enabling it in dsh
|
|
147
|
+
|
|
148
|
+
- Bundle form: `dsh.bundle.patch` in package.json points at `cordis.patch.yml`, and `dsh plugin add/remove` maintains the assembly layer.
|
|
149
|
+
- The package carries **two** lines (bulkhead): `- id: danger-guard` (guard half, **no Config**, name points at the package root) and `- id: danger-guard-settings` (settings half, strict schema, name points at the `./settings` subpath export).
|
|
150
|
+
- Only the first line brings up the card: the host scans `dsh.client` on **bare package-name** entries only (`exactPackageSpecifier` in `dsh-client-modules` returns undefined for a subpath), so the settings line's subpath name is deliberate.
|
|
151
|
+
- The card loads on the web platform only (`dsh.client = { platform: "web", immediately: true }`), as "danger-guard danger block" in the settings plugin list.
|
|
152
|
+
- Deployment defaults sit on the **settings row's** `config:` (`- id: danger-guard-settings`): precedence is settings card runtime value > row config > `BUILTIN_BASE`.
|
|
153
|
+
- An explicit `undefined` in the row does not override the base, and illegal numbers (e.g. `maxDenies: 0`) fall back to the built-in default - 0 would switch the fact gate off entirely.
|
|
154
|
+
- Defaults belong on the `- id: danger-guard-settings` row: the guard half takes no `config` argument at all (`apply(ctx)` in `host.ts`), so a `config:` on the `- id: danger-guard` row is inert by design.
|
|
155
|
+
- Failures go the conservative way: the settings surface lives on its own entry, `danger-guard-settings`, and only that entry carries the strict schema.
|
|
156
|
+
- An out-of-range value (`maxDenies: 0`, `strictMode: "yes"`) fails **that** entry alone; the guard entry has no `Config`, so when it cannot read the value surface it falls back to `BUILTIN_BASE` and warns once - one mistyped line of configuration cannot switch the gate off.
|
|
157
|
+
- `apply` only throws when the **settings surface** is missing: no `effect`, or a `settings` service without `describe`, raises `required services (settings/effect) missing` and that entry goes FAILED (the host-side service probe checks exactly those two).
|
|
158
|
+
- ⚠ A missing `tools` service does **not** throw: `svc.tools?.guard(...)` yields no registration handle and the mount is skipped (optional chain plus the `dispose === undefined` branch in `host.ts`, pinned by `test/host.test.ts`); `apply` has no guard for it.
|
|
159
|
+
- That is still not a silent pass in a real deployment: `tools` is in `inject`, so cordis never activates the entry without it, and the startup diagnostic prints `warning: N entry did not activate` with `pending (waiting for service: tools)`. What you see is "no gate mounted, and the host says so".
|
|
160
|
+
|
|
161
|
+
### Settings (namespace `danger-guard-settings` = the settings entry id; defaults are `BUILTIN_BASE` in lib/settings-schema.ts)
|
|
162
|
+
|
|
163
|
+
| Field | Type | Default | Purpose |
|
|
164
|
+
| ---------------------- | ---------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
165
|
+
| `enabled` | boolean | `true` | Master switch; `false` silences all three verdicts (lesson reports included) |
|
|
166
|
+
| `factGateEnabled` | boolean | `true` | Turns off only the first-edit fact gate; danger commands and secret paths still block |
|
|
167
|
+
| `maxDenies` | natural, 1–5 | `2` | Consecutive non-converging denials per target before the quota releases it with a warning |
|
|
168
|
+
| `strictMode` | boolean | `false` | `true` ignores the risk tiers and runs the full probe on the first edit of every code file |
|
|
169
|
+
| `smallEditChars` | natural, >=1 (card 1–2000) | `200` | A write of at most this many chars without signature keywords is small: only read is required |
|
|
170
|
+
| `bigEditChars` | natural, >=2 (card 2–100000) | `2000` | A write above this size is a rewrite and escalates to the full probe |
|
|
171
|
+
| `extraTestDirs` | string[] | `[]` | Appends test-layer directory names (built-in `test/tests/__tests__/spec/specs`); append-only |
|
|
172
|
+
| `extraDevServerWords` | string[] | `[]` | Appends long-running command heads (built-in `vite/webpack/next/nuxt/cargo-watch/watchexec`); append-only |
|
|
173
|
+
| `extraDevRunArgs` | string[] | `[]` | Appends run script names (built-in `dev/watch/serve/start/start:dev`); append-only |
|
|
174
|
+
| `extraSecretPatterns` | string[] | `[]` | Appends secret path regex fragments (one per line on the card; invalid ones skipped with a warning); append-only, no allowlist |
|
|
175
|
+
| `refSearchTools` | string[] | `["grep","glob","zg_search"]` | Which search tools count as reference evidence; clearing it means none count |
|
|
176
|
+
| `refSearchStrictTools` | string[] | `["zg_search"]` | The subset of the above judged by the strict "root + query/fts/vector relative path" criterion; a name listed only here gets no credential |
|
|
177
|
+
|
|
178
|
+
### How blocking and exemptions are decided
|
|
179
|
+
|
|
180
|
+
| Shared mechanics of the four command rules | Content |
|
|
181
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
182
|
+
| Scope and splitting | Only tool names `bash` and `pwsh` are judged (a non-string `command` passes, no guessing); the command is split at `;`, `&&`, `\|\|`, `\|`, newline, `&`, `(`, `)`, backtick and each segment judged, with no splitting inside quotes |
|
|
183
|
+
| Wrapper words, bin prefixes, quotes, continuations | None of these are escape hatches: wrapper words (`sudo env time nice stdbuf timeout taskset watch setsid doas pkexec noglob nohup exec command` and leading `VAR=x`), bin path prefixes (`/bin/rm`), quoting and backslash line continuations |
|
|
184
|
+
| Principle and its one exception | "When unsure, let it through" - with one exception: a nested `-c` body that cannot be extracted reliably, or nesting past 16 levels, becomes `nestedShellUnconfirmed`, and the way out is explicit user confirmation or rewriting it as one quote-balanced layer |
|
|
185
|
+
|
|
186
|
+
| The four command rules and secret path edits | Blocks | Passes / way out |
|
|
187
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
188
|
+
| Git hook bypass | `--no-verify` and its unambiguous abbreviations (`--no-v`…) on `commit/push/merge/cherry-pick/rebase/am`, the short `-n` on `commit`/`rebase`, and `git -c core.hooksPath=` / `git config core.hooksPath <dir>` (with `--global` and the `=` form too) | `git push -n` (dry-run), `git am -n`/`cherry-pick -n`, `git config --get/--unset/--list core.hooksPath`, `git status --no-verify`, and that text appearing inside a commit message |
|
|
189
|
+
| Catastrophic rm | The threshold is **per command-word track**, never unconditional: on the POSIX/cmd track recursive+force is required, the one exception being an upper-case `-R` inside a short cluster, which is already disaster-level on its own (`rm -r ~` passes, `rm -R ~` denies); on the `Remove-Item`/`ri` track **any Recurse form alone** against a disaster target is disaster-level, **no `-Force` required** (`-Recurse` ≡ `-recurse` ≡ `-rec` ≡ `-r` ≡ `-R` - parameter names are case-insensitive, so those are one parameter, not four spellings. The two tracks differ because `Remove-Item -Recurse ~` silently deletes the whole tree without `-Force`, while `rm -r ~` prompts on each protected entry without `-f`). Short-flag clusters are scanned **letter-by-letter with no closed alphabet** (`-rf`, `-drf`, `-RWx`, `-srf` all judge the same); long flags `--recursive --force`; cmd builtins additionally `/s` `/q` `/f`; `Remove-Item`/`ri` switch faces - only on those two command words a `-letter…` word is read as a **parameter name** (`-Recurse`/`-Force` and their unambiguous abbreviations `-rec`/`-fo`/`-r`) and never as a cluster, while the parameter-name face still runs **on top of** the cluster scan on `rm` and the cmd builtins because the cluster scan is case-sensitive and sees only an `r` inside `-Force`) **and** a target of `/`, `~` or `~/…`, the literal home path (`<home>`, from `os.homedir()`, i.e. the HOME env var), `$HOME` and every `${HOME…}` form, `.`/`..`/`../…`, whole trees under system and user directory prefixes (`/etc`, `/usr`, `/bin` and friends; full list in `SYSTEM_PATH_PREFIXES`), absolute globs (`/*`, `/usr/**`), with the path lexically normalized first (`/tmp/../` judges as `/`). The command-word side covers pwsh/cmd (`Remove-Item`/`ri`/`del`/`erase`/`rd`/`rmdir`, matched lower-cased, backslash path prefixes stripped too); Windows targets (drive root `C:\`, system and user trees `C:\Windows`·`D:\Users\x`, UNC share root `\\server\share`, the Windows glyph of the home directory (it compares against `winHome`, i.e. the host's own `os.homedir()` after glyph folding: when home itself is a drive-letter path the tree and its children judge the same; a POSIX-glyph home makes this form inapplicable, so `<posix home>\x` is only caught when it happens to sit under a listed system prefix such as macOS `/Users/…`), `~\notes.txt` (a home child written with tilde + backslash - the **Windows** reading only: pwsh takes `~` for the home dir everywhere and `\` as a separator; `~\` and `~\\notes.txt` judge the same, while a word character right after the tilde (`~xyz`, `~mailbox\notes.txt`) is not home - the same tail gate the `$HOME` rule uses)) - **that track** only joins when the **host is win32** or the **tool is `pwsh`** (the dialect comes from tool identity, never guessed from the string shape); the two unexpanded env forms sit **outside** the track: system root `%SystemRoot%\System32`, `$env:windir` and user root `%USERPROFILE%\Documents`, `$env:USERPROFILE` are literal unexpanded checks, same shape as the `$HOME` rule - the environment is never read and they never drop out with the track/platform (a non-Windows host under the `bash` tool still denies them), variable name must be followed by a separator or end of word | Relative cleanup inside the project (`rm -rf node_modules`, `./dist`, `src/old`, `rd /s /q build`), on the POSIX/cmd track force-only (`rm -f ~`) or lower-case recursion-only (`rm -r ~`, `rm --recursive ~`) - that slot is keyed by the **spelling**, not by the semantics: the same "recursion only" written as `rm -R ~` or `rm -Recurse <home>` is denied by the pre-existing case-sensitive upper-case `-R` rule, and "force only" written as `rm -Force <home>` is denied because the cluster scan supplies the `r` while the parameter-name face supplies the `f` (the deliberate false-positive side of pwsh long words on the POSIX track); on the cmdlet track the threshold is likewise conditional on a **disaster target**: a spelling with **no Recurse form at all** passes, and so does a Recurse form aimed at a non-disaster path (`Remove-Item -Recurse ./build` passes) (`Remove-Item -Force <file under home>` and `Remove-Item -Filter *.log <tree>` still pass, while `-Recurse`/`-recurse`/`-rec`/`-r`/`-R` against a disaster target deny with no `-Force` at all), `Remove-Item -rf C:\Users\bob` (`-rf` is not a parameter name, pwsh itself errors on it; the twin `rm -Force -LiteralPath <file under home>` on pwsh **is** still denied - the command word is `rm`, so the permissive POSIX cluster decides, and a false positive is the cheaper side), `/var` and `/tmp` (deliberately excluded - blocking them costs more than missing them), a different segment such as `/usrx`, and on a non-Windows host `C:\Windows` / `\\home\bob` under the `bash` tool name (those are literal relative paths there, so the POSIX reading decides), together with `~\notes.txt` under that same reading (bash never expands this tilde - `\n` is an escape, so the word is the relative file `~notes.txt`, measured with `printf '%s\n' ~\notes.txt` on this machine - passing is the correct verdict, not a hole) |
|
|
190
|
+
| Download-and-run | A shell interpreter after `curl\|wget\|fetch` blocks, including `\| sudo sh`, `\| /bin/bash`, `\| xargs -I{} sh {}`, `\| parallel sh`, and the reversed `sh <(curl …)`, `source <(curl …)`, `. <(curl …)` | `curl -O`, `curl … \| jq .name`, `cat x \| grep foo`, `ls \| sh` with nothing downloaded, `source ./setup.sh` |
|
|
191
|
+
| Long-running dev server | Bare `vite`/`next`/`nuxt`, `webpack serve\|--watch`, `cargo watch`, `npm\|pnpm\|yarn\|bun run dev\|watch\|serve\|start\|start:dev` and any script name rooted in `dev` (`dev:web`, `dev-server`, `dev:build`), and `npx\|bunx\|exec\|dlx <long-running package>`; `vite build --watch` still blocks | The one-shot forms `npm test`, `npm run build`, `vite build`, `next build`, `webpack --config prod.js`, `cargo run/check/test`, `vitest run`, `npm exec vite -- build` |
|
|
192
|
+
| Secret path edits | Any file inside `.ssh`/`.gnupg`/`.aws`/`.kube`/`.docker`, the `id_rsa\|id_ed25519\|id_ecdsa` stems (with `.pub` and `._-` copy suffixes), names ending `.pem\|.key\|.p12\|.pfx\|.keystore\|.jks`, the whole `.env` family, `.envrc\|.npmrc\|.pypirc\|.netrc\|.pgpass\|.git-credentials`, `credentials.json`/`service-account*.json`/`token.json`, `secret(s).yaml\|yml\|json` and `.git/config`; backslashed Windows paths (`<home>\.ssh\id_rsa`) judge the same | Deliberate passes: `.env.example`/`.sample`/`.template`/`.defaults`, `api.key.ts`, `main.pem.tsx`, `.gitignore`, `mysecrets.yaml`, `secrets.yaml.bak`, `myid_rsa` |
|
|
193
|
+
| Secret path tool surface | `edit`/`write` (`file_path` or `path`) and `str_replace_editor`'s `create`/`str_replace`/`insert` | `read` and `view` are read-only and never blocked |
|
|
194
|
+
|
|
195
|
+
| The fact gate, the quota and false-positive routes | Content |
|
|
196
|
+
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
197
|
+
| The fact gate's scope and tiers | Judges only `edit`/`write`/`str_replace_editor` calls that carry a target path, tiered by write risk |
|
|
198
|
+
| low | Requires a successful `read` of the target |
|
|
199
|
+
| mid | Adds one project-level reference probe (origin must be an ancestor of the target, signature containing the target name or a stem of at least 4 chars; the strict criterion may also match on the relative path) |
|
|
200
|
+
| high | Adds one attributable search per **existing** test/e2e/docs layer (absent layers are exempted by fs probing) |
|
|
201
|
+
| Escalation to high | Creating a file (`write`/`create`), touching export/signature keywords, a risky path (manifest, lockfile, build config, assembly file, `schema.sql`), a write above `bigEditChars`, or a write size that cannot be measured; `strictMode` is always high |
|
|
202
|
+
| Skip rules and evidence standard | `md/markdown/txt/rst/adoc` skip the gate entirely and `test/` no longer does; only **successful** calls in the session count as evidence (errors and calls the gate itself rejected do not); when the session history is unavailable the gate asks the user instead and does not consume the quota; a genuinely new file (ENOENT **and** a creatable call) is exempt from the read requirement |
|
|
203
|
+
| Quota release and how long it stands | After `maxDenies` consecutive denials whose gap set did not strictly shrink (a strictly smaller set resets the counter), the next call is released outright with a `console.warn`; that release stands for the target until it is changed outside the session, the session shard is LRU-evicted (64 sessions × 200 files) or the plugin reloads |
|
|
204
|
+
| Three routes for a false positive | There is no allowlist setting anywhere in this package, so a false positive has exactly three routes: temporarily turn `enabled` off (or only `factGateEnabled`) via the card or the row's `config:`; take the `maxDenies` quota release; or rewrite the command as the denial text suggests (one-shot build, project-relative cleanup, one quote-balanced layer) and have long-running servers started by the user in their own terminal |
|
|
205
|
+
|
|
206
|
+
### Public surface
|
|
207
|
+
|
|
208
|
+
| Surface | Content |
|
|
209
|
+
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
210
|
+
| Host extension points | Consumes `ctx.tools.guard` (the only interception surface, `inject: ["tools", "settings"]`), the guard entry deliberately carries no `Config` — the settings surface lives in a separate profile entry, `danger-guard-settings` (`inject: ["settings"]`, exporting `Config`; under 0.1.7 the host registers it implicitly and its namespace is that entry id, and this package calls no registration API), so one broken user-layer config line only FAILs the settings entry while the gate keeps blocking. Across namespaces it reads only the official `locale` row's `preference` via `settings.describe()` (it decides the language of the text sent back to the model; Chinese when that entry is not projected), and subscribes to `session/disposed` to release fact-gate shards. There is also an **optional** child injection, `ctx.inject(["sessionProjections"], …)`, which registers the `danger-guard.toolLedger` unit and mounts the `stateOf()` reader inside the guard; without that registry the callback never activates and the verdicts keep using the fallback scan - which is exactly why it is deliberately kept out of the plugin-level `inject`, since that would stop the gate from activating on profiles that do not ship the projection package |
|
|
211
|
+
| What it does not add, and the optional bus | It registers no tools, exposes no HTTP endpoint and adds no CLI subcommand. Optionally consumes `report` / `pass` from `ctx.get("lessonLoop")`: `source: "danger-guard"`, three categories `dangerous-bash` / `secret-path` / `factgate-deny`, signatures being the matched command text, `bash-nested-shell-unconfirmed` and `edit-before-factgate` |
|
|
212
|
+
| Exports and card | A missing bus, a throwing one or a rejected Promise never affects blocking - only a warning is logged. `.` → the guard entry (`host.ts` in-repo, `host.js` when published), `./settings` → the settings entry (`settings-host.ts` / `settings.js`), `./client` → `client.js`, `./cordis.patch.yml`, `./package.json`. The card injects into slot `plugins.bundle.config` keyed by the bundle package name `@jayyuen66/dsh-danger-guard` (the slot is keyed by package name: the host dispatches `entryKey: pkg.name`, source of truth is `dsh.profile.bundles` in `~/.dsh/profiles/web/package.json`; a bare entry id renders no card at all), while `configForms.get()` and the settings namespace stay the bulkhead's settings entry id `danger-guard-settings` — the two identifiers are not the same thing. Bundled with React externalized into a module-loader entry; edits stay in a draft until Save writes them to settings, and on a read-only scope (a non-loopback page) the controls are disabled with a read-only status shown |
|
|
213
|
+
|
|
214
|
+
### Data and privacy
|
|
215
|
+
|
|
216
|
+
| Privacy aspect | Content |
|
|
217
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
218
|
+
| Nothing is written, nothing is fetched | The verdict chain does no fs writes, no network calls and no subprocess spawning; the only output surfaces are `console.*` warnings and the text sent back to the model |
|
|
219
|
+
| What it reads | The tool ledger from the session event history - primary path: the `danger-guard.toolLedger` unit registered on `ctx.sessionProjections`, read synchronously via `stateOf()` inside the guard; when that registry is absent, or the ledger cannot be vouched for, it falls back to a full `session.snapshotEvents(0)` scan with byte-identical verdicts (the three fallback triggers: the ledger passing `LEDGER_WINDOW`, 2000 rows, an `event.seq` jump meaning the fold did not start at the log head, and a stored state whose shape does not parse). Plus `statSync` of the target and of layer directories (existence, mtime, project-root markers); one whole-file `readFileSync` of the target when a change from outside the session is suspected (content self-proof); the home directory comes from `os.homedir()`, i.e. the HOME env var only |
|
|
220
|
+
| What it keeps | Entirely in process memory (fact-gate shards of 64 sessions × 200 files, verdict ledgers LRU-capped at 200 entries), gone when the host exits, with no file left under `<dsh data dir>` |
|
|
221
|
+
| The only two things that leave, both switchable | With lesson-loop installed the `report` payload carries cwd, session id, the matched command text or the target path plus the denial text (`enabled: false` stops the reports too); and settings values are persisted by the host under the `danger-guard-settings` namespace (the settings entry id, not the guard entry's) |
|
|
222
|
+
|
|
223
|
+
### FAQ
|
|
224
|
+
|
|
225
|
+
| Question | Answer |
|
|
226
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
227
|
+
| It blocked a legitimate command? | The three `extra*` lists and `refSearchTools` can only tighten and there is no allowlist, so the only routes are: turn `enabled` off temporarily, take the `maxDenies` quota release, or rewrite the command as the denial text says |
|
|
228
|
+
| The fact gate keeps refusing? | It credits only actions that succeeded: point the search origin at an ancestor such as the project root as the message says (searching the file itself or a sibling counts as "wrong scope"); if another search plugin is in play, add its tool name to `refSearchTools` (an empty list is called out explicitly in the message); when evidence is unreachable, take the quota release or turn `factGateEnabled` off temporarily |
|
|
229
|
+
| How do I check it is live? | The card row appearing in settings, and `dsh --profile web --dump-config` showing this package's layer with `- id: danger-guard`, mean it is mounted; the host must be `>= 0.2.0-rc.2`. Known boundaries are recorded **by class**, and each class pins its current verdict in a test: (1) **the wrapper-head class** - when the segment head is another interpreter/wrapper, the delete behind it never reaches the command-word table, so all four live spellings escape: `cmd.exe /c rd /s /q C:\Windows`, `cmd /c …` (without `.exe`), `pwsh -c "…"` and `powershell -Command "…"` (closing it means skipping a wrapper _and_ deciding whether a `.exe` suffix still names the command, where this repo is deliberately conservative); (2) **the escape-folding class** - inside a nested `-c` body the backslashes of `rm -rf C:\Windows` are consumed as escapes, so the inner command is already `rm -rf C:Windows`, which is no longer a Windows shape ⇒ allowed (the forward-slash twin `bash -c "rm -rf C:/Windows"` _is_ denied, so dialect inheritance itself works - what is missing is telling a literal backslash from an escape); (3) **the MSYS/Cygwin mount form** - on a real Windows host `rm -rf /c/Windows/System32` and `/C:/Windows` are not detected, because mapping `/c` back to a drive letter needs a mount table, i.e. a new dependency plus a new design call, recorded under "explicitly not doing" in the roadmap. Also: (4) **dry-run modifiers never reach the predicate** (the over-block side of the status quo) - `Remove-Item -WhatIf -Recurse ~` is denied, because `-WhatIf`/`-Confirm` only make pwsh print what it would do and the guard does not read them, so a dry run judges exactly like a real delete; closing it needs a policy call on whether a dry run is its own exempt class (`-WhatIf` can also collide with an ambiguous `-w`), so the current verdict is pinned by a test instead; (5) **the drive-letter-less root-relative Windows form - now closed** - `\Users\bob\notes.txt` is now denied whenever the Windows track joins (a `pwsh` dialect call site, or a win32 host) and still passes under the POSIX reading, because bash consumes the `\U` as an escape so that string really names a relative file (`Users…`); denying it there would be a false block. The fix never guesses a drive letter: it folds only the "single leading `\` = root of the current drive" shape into a tree-name list that is **strictly the same family as** the system and user trees (`windows`/`programdata`/`program files`/`users`), so the `^[a-z]:` premise stays intact and relative deletes like `\etc` or `\usr` are still never pushed into the 24 POSIX system prefixes (the original reasoning - "guessing necessarily over-blocks" - remains true, which is why the `\etc` contrast still lives in the "Windows glyph normalization must not rewrite POSIX predicate inputs" group). Closed in the same stroke: a POSIX-shaped home followed by a backslash descendant (`/data/bob\.ssh`) counts as inside the home while the track joins; (6) **a bare `~user` (no trailing `/`) is deliberately not denied** - on a real bash `rm -rf ~root` expands to `/var/root` (measured on this machine) while `~bob`/`~backup` stay literal because no such account exists, so the verdict depends on this host's password table, and the predicate neither touches fs nor looks users up (doing that would be a new dependency plus a new design call, same family as (3)); denying the bare form would mis-block relative paths that start with `~`. The **separated form `~user/…` is denied** (`rm -rf ~bob/x`, `~root/.ssh` judge like `$HOME`, and the dialect never relaxes it) since once expanded it always lands inside somebody's home tree; and the dialect has exactly two entry points, the tool names `bash` and `pwsh` (a future `cmd`/`powershell` tool needs a line in that mapping, dialect is never inferred from the glyphs); and a nested `-c` body keeps the caller's dialect |
|
|
230
|
+
| The same discipline carries a **known over-block (boundary 6b)**: when no such account exists bash leaves the word literal, so `rm -rf ~bob/x` really is a relative path starting with `~` (this machine has no `bob`/`backup`/`dev`) - the guard errs toward denying, exactly like the quoted `"$HOME"` family, which also never expands yet is still denied. |
|