dsh-permission-rules 验证记录

August 15, 2026 · View on GitHub

日期:2026-08-14 · 运行时:dsh 0.1.0-rc.6(全局安装,dsh on PATH)· 平台:Windows(工具面为 pwsh

v0.4.1 审计安全修复(2026-08-15)验证记录

响应社区反馈 Issue #2(22xuan):rc.6 宿主的 Session.append 静默丢弃 { ignorable: true } 选项,审计事件无标记落盘,导致会话在更严格构建上无法恢复(SessionFormatUnsupportedError)。已对照本地 rc.6 源码核实(lib/index.js append() 只透传 sourceEventSeqs/surfaceOp,返回事件无 ignorable 字段)。

修复(全部有测试锁定,test/audit-support.spec.ts 6 用例 + config/prose 用例):

  • 预检先行:首次审计写入前读取 peer 版本(@deepseek-ai/dsh-session/package.jsoncreateRequire),0.1.0-rc.1rc.6 判定为已知无标记宿主 → 一次也不写入,一次性警告后停用会话日志审计(rc.6 上会话日志零污染)。
  • 写入后探针:非已知版本线在首次 append 后检查返回事件信封是否带 ignorable: trueisMarkedAuditEvent),不带则降级——post-rc.6 宿主行为同样被实证锁定。
  • opt-in 逃生门:新配置 allowUnmarkedAudit(默认 false);设 true 恢复 rc.6 上的会话内轨迹(接受未来升级时需 repair-session-logs.mjs 的风险)。/rules decisions 在降级宿主上显示说明行(五语 auditDisabledNotice)。
  • 测试 harness 默认 allowUnmarkedAudit: true 挂载(既有审计断言不变);isUnmarkedHostVersion/isMarkedAuditEvent 导出为能力判定助手。
  • 文档:五语 README(config 表新增一行、Known limitations 如实重写、新增 Acknowledgments 致谢 22xuan)、AGENTS.md、CHANGELOG、events.ts 注释全部修正。

v0.4.0 完善轮次(2026-08-15)验证记录

在 v0.3.0 基础上实施四类完善(候选文件监听、--platform 测试标志、规则来源归属、缓存键规范化),全部通过仓库本地门禁(typecheck / lint / vitest 133 tests / 覆盖率 90-80-90-90 / build / pack / 五语 README 同步)。关键行为均有测试锁定(watch.spec.ts 12 用例、command.spec.ts 21 用例):

  • 候选文件监听(真实缺口修复):chokidar 5 在 Windows 上对「父目录也缺失」的路径监听不可靠(实测探针:父目录存在时 add 可达,父子同建时无事件),故对未生效候选文件改监听其最近存在祖先目录,事件回调里 existsSync 判定后再重载——空链时创建 .dsh/rules.yaml、fallback 挂载后删除再重建、项目文件重建从 fallback 切回,三条路径均自动采纳,无需 /rules reload
  • /rules test --platform <name>when.platform 维度在任何宿主上可干跑测试;未知平台名响亮报错(五语 testBadPlatform)。
  • 来源归属:多文件链(searchUp)下每条规则行标注自己的来源文件(工作区内显示相对路径,区外显示绝对路径);/rules list 为裸列出显式别名。
  • 缓存键规范化resolve(cwd) + Windows 大小写折叠,同目录不同拼写共享一条缓存与一套 watcher(watch.spec.ts 断言 activeWatcherCount() === 1)。
  • CI 稳定性修复:v0.3.0 起 windows-latest/Node 24 矩阵格持续变红,根因是真实 chokidar watcher 在临时工作区被测试删除时触发 Node 24 libuv 断言(src\win\fs-event.c:72,测试进程崩溃 → vitest worker ERR_IPC_CHANNEL_CLOSED)。修复:test/harness.ts 默认 watch: false 挂载(仅 chokidar 被 mock 的 watch.spec.ts 显式开启),测试套件不再创建真实 watcher。

待办(延续)

  • rc.7 上线复核(见第 0 节待办三项)不变。
  • searchUp 下更深祖先层的新建文件仍需 /rules reload(已写入 docs 与 AGENTS.md 的既定限制)。

0. 2026-08-14 加固轮次(Unreleased)验证记录

在上一轮验证基础上实施完善方案(P0-P3),全部通过仓库本地门禁:

pnpm run typecheck     # tsc src + tests,通过
pnpm run lint          # eslint src/test/scripts,通过
pnpm test              # vitest:106 tests / 8 套件,全部通过
pnpm run test:coverage # 覆盖率门禁 90/80/90/90:语句 95.13% / 分支 89.43% / 函数 99.27% / 行 95.13%,通过
pnpm run build         # lib/types (tsc 声明) + lib/index.js (tsdown)
pnpm run pack:check    # 构建 + pack 产物
node scripts/check-readme-sync.mjs   # 五语 README 同步门禁,通过

本轮关键行为均有单元/集成测试锁定(真实 rc.6 Context/Session/Commands/ApprovalService):

  • 安全修复:Windows 大小写路径绕过(caseInsensitivePaths,win32 默认开,回归用例 rules.spec.ts);嵌套参数候选提取(MCP 形态,深度上限 8);回溯守卫(glob 星号数上限 maxGlobStars=2 + regex 嵌套无界量词/重叠交替拒绝,compilePatternRegex 用例组);审计 outcome 补记(下游 deny 不被误记 allow,dispatch.spec.ts)。
  • 新能力/rules decisions [n]/rules test <tool> <json>language 五语(en/zh/es/pt/hi,prose.spec.ts 全语言冒烟)、searchUp 分层合并(子覆盖父 + 审计归属具体文件,file-load.spec.ts)、规则元数据 enabled/description/tags、参数否定 !patternabsent 维度、when 条件(env/platform)、audit: 'hits' 粒度、遮蔽告警、LRU 逐出与 watcher 定时器清理(watch.spec.ts)。
  • 工程:CI 增加 lint/覆盖率/五语 README 同步门禁;release workflow(tag 触发 pack + 校验 CHANGELOG + 发布 tarball);inject 补齐 toolspackage.json 发布 hygiene。

真实回放实测(新构建,rc.6 宿主)

demo profile(permission-rules-demo)重装本地 dsh-permission-rules-0.1.1.tgz 后,用 .verification 的 llm-replay fixture 在 .verification/workspace 重跑两条链路(dsh --profile permission-rules-demo,keyless):

  • deny(fixture-deny):会话日志 seq 21 permissionRules/decision {action:"deny", outcome:"deny", ruleIndex:0} → seq 22 tool/result "Error: 禁止 push 到受保护路径"(模型可见 = reason;pwsh 未执行)。新 outcome 字段在真实宿主上落盘 ✅
  • ask(fixture-ask):seq 21 permissionRules/decision {action:"ask", outcome:"ask", ruleIndex:1} → seq 22 approval/asked(reason 原样进入官方审批 seam)→ seq 23 approval/decided {outcome:"rejected"}(headless never 策略 fail-closed)✅

解码用 .verification/dump-session.mjs(独立 zstd 帧扫描,不依赖 harness 源码;原 dump-session.ts 依赖 harness 源文件路径映射,保留作参考)。

待办:rc.7 上线复核

AuditAppend 假定 post-rc.6 宿主会在信封上真正落下 ignorable: true。rc.7 发布后一周内,用 .verification 流程实测一次:

  1. 用 rc.7 的 dsh 重放 fixture-deny.session.jsonl,确认 deny 拦截与 permissionRules/decision 审计行(带 ignorable 标记)不变;
  2. 用不含本插件的 rc.7 构建加载一段由 rc.6 宿主写入的会话日志,确认 repair-session-logs.mjs repair 后的日志可被加载;
  3. CI 增加对 next tag 宿主的冒烟矩阵(peer 临时覆盖 + --dump-config + headless deny 回放)。

1. 静态检查与单元测试

pnpm run typecheck   # tsc src + tests,通过
pnpm test            # vitest:58 tests / 7 suites,全部通过
pnpm run build       # lib/types (tsc 声明) + lib/index.js (tsdown),lib/types/index.d.ts 存在
pnpm pack            # dsh-permission-rules-0.1.0.tgz

覆盖矩阵(test/):三态分派(deny/ask 短路、allow/透传严格 next())、参数 glob 匹配(跨 /、标量字符串化、数组任一、缺失键不命中)、paths 匹配(工作区相对、Windows 盘符、** 零层、工作区外绝对候选丢弃)、规则顺序(首条命中)、无命中透传、坏文件 fail(每次调用响亮失败)与 ignore-with-warning(告警降级)两路径、绝对 rulesFile/fallbackPath 挂载期校验、maxRules 超限失败、Chokidar HMR(变更生效、坏重载保留旧规则不崩溃、unlink 重解析、重建恢复、watcher 错误只告警)、/rules 走真实 dsh-commands registry(列出/重载/坏文件/未知参数)、与 dsh-auto-review 集成(mock answerer 替代真实模型:ask 规则 → 裁决放行/拒绝 → 审计链完整;无 answerer → 官方 unavailable fail-closed)。

2. 组合验证:dsh --dump-config 行生效

Profile ~/.dsh/profiles/permission-rules-demo:bundles @deepseek-ai/dsh-base + @deepseek-ai/dsh-headless + dsh-permission-rules,patch 层挂 @deepseek-ai/dsh-llm-replay(keyless 回放)并禁用 session-title-llm

# == dsh-permission-rules
- id: permission-rules
  name: dsh-permission-rules
  config:
    rulesFile: .dsh/rules.yaml
    badFilePolicy: fail
    maxRules: 256
    patternMode: glob
    watch: true
# == C:\Users\zzhdz\.dsh\profiles\permission-rules-demo\cordis.patch.yml
- id: llm-replay
  name: '@deepseek-ai/dsh-llm-replay'

无 FAILED 行(完整输出见 .verification/dump-config.txt)。

3. headless 实测(无 API key,llm-replay 回放模型脚本)

工作区 Project\Plugins\dsh-permission-rules\.verification\workspace,规则文件 .dsh/rules.yaml

rules:
  - match: { tools: [pwsh], params: { command: "git push*" } }
    action: deny
    reason: "禁止 push 到受保护路径"
  - match: { tools: [pwsh], params: { command: "Write-Output secret*" } }
    action: ask
    reason: "输出 secret 需要审批"

回放 fixture(.verification/fixture-deny.session.jsonl / fixture-ask.session.jsonl)记录两轮模型响应:第 1 轮发出 pwsh 工具调用,第 2 轮输出 DONE 结束回合。运行:

$env:DSH_SNAPSHOT_FILE = '<repo>\.verification\fixture-deny.session.jsonl'
dsh --profile permission-rules-demo "verify the deny rule blocks git push"   # cwd = .verification\workspace

3.1 deny 阻止 pwsh(fixture-deny)

回放出的 pwsh(git push origin main)tools/pre-execute 被规则 1 阻止。会话日志(~/.dsh/sessions/.../session.jsonl.zstd 解帧):

{"type":"permissionRules/decision","seq":21,"data":{"toolName":"pwsh","callId":"call-1",
 "source":"D:\\deepseek-harness\\Project\\Plugins\\dsh-permission-rules\\.verification\\workspace\\.dsh\\rules.yaml",
 "action":"deny","ruleIndex":0,"reason":"禁止 push 到受保护路径"}}
{"type":"tool/result","seq":22,"data":{...,"content":[{"type":"tool-result","toolCallId":"call-1",
 "content":[{"type":"text","text":"Error: 禁止 push 到受保护路径"}],"isError":true}]}}

模型可见结果 = 规则 reason;pwsh 进程从未执行。✅

3.2 ask 触发官方审批(fixture-ask)

pwsh(Write-Output secret hello) 命中规则 2 → 返回 ask → 官方 tools 流水线调用 ctx.approval.request。headless 组合的会话预设为 danger-full-access,审批策略因此为 never(官方服务在任何 answerer 之前确定拒绝),审计对完整:

{"type":"permissionRules/decision","seq":21,"data":{...,"action":"ask","ruleIndex":1,"reason":"输出 secret 需要审批"}}
{"type":"approval/asked","seq":22,"data":{"id":"d205813e-...","toolName":"pwsh","callId":"call-1","reason":"输出 secret 需要审批"}}
{"type":"approval/decided","seq":23,"data":{"id":"d205813e-...","outcome":"rejected"}}
{"type":"tool/result","seq":24,"data":{...,"text":"Error: the user rejected tool \"pwsh\"","isError":true}}

ask 规则完整走官方审批 seam(approval/askedapproval/decided),reason 原样进入审批请求。

3.3 ask + auto-review answerer(mock 第二模型)— 集成测试

真实模型需要 API key,按交付约定以 mock answerer(脚本化 reviewer 子代理) 代替(test/integration.spec.ts,真实 ApprovalService + 真实 dsh-auto-review bundle):

  • allow 裁决:permissionRules/decision(ask) → approval/askedautoReview/verdict(allow, outcome allowed-once) → approval/decided(allowed-once),事件顺序断言通过。
  • deny 裁决:同链,approval/decided(rejected)。
  • 两者都无(未挂 answerer):approval/decided(unavailable) — 官方 fail-closed。✅

4. /rules 命令

headless 组合没有命令分发面(斜杠命令由客户端 UI 驱动),/rules 的实测走真实 dsh-commands registrytest/command.spec.ts,真实 Session/Agent):

  • /rules 列出 2 条规则、来源路径、1-based 序号与 reason ✅
  • /rules reload 重读文件(1 条新规则生效);坏文件 → error 结果 + 旧规则保留 ✅
  • 未知参数 → 用法提示 ✅

5. 验证环境文件(.verification/,gitignored)

  • workspace/.dsh/rules.yaml — 演示规则
  • fixture-deny.session.jsonl / fixture-ask.session.jsonl — llm-replay 回放脚本
  • dump-config.txt--dump-config 完整输出
  • dump-session.ts — zstd 会话日志解帧工具(帧扫描复用 harness 的 scanZstdFrames

6. GitHub 发布(2026-08-14)

  • 仓库:https://github.com/PerryLink/dsh-permission-rules (public,默认分支 main,初始提交 c0bd092
  • 协议:Apache License 2.0(LICENSE + package.json license 字段;五语 README 同步更新)
  • Topics:dsh dsh-plugin deepseek-harness deepseek cordis permission-rules approval ai-safety
  • 提交树经密钥扫描(ghp_*/常见凭据模式)无命中;.gitignore 排除 node_modules/lib/.verification/*.tgzvendor/dsh-auto-review-0.1.0.tgz 为集成测试 fixture,显式保留在树内)。

7. git 安装通道实测(2026-08-14,提交 8e6d1eb

dsh plugin --profile permission-rules-demo add "github:PerryLink/dsh-permission-rules#8e6d1eb…"(README 承诺的安装路径):

  1. pnpm 首次因 git 包 prepare 构建未获 allowBuilds 许可而拒绝,CLI 打印精确的 allowBuilds 键 → 加入 profile 的 pnpm-workspace.yaml 后重装成功。
  2. 隔离 prepare 环境实测暴露两个仓库缺陷并已修复:
    • vendor/dsh-auto-review-0.1.0.tgz(devDep 的 file: 目标)被 gitignore 导致隔离 pnpm install ENOENT → 改为随仓库提交
    • package.json 的 pnpm.neverBuiltDependencies 在 pnpm 11 被忽略 → 改为在仓库自带 pnpm-workspace.yaml 声明 allowBuilds: { esbuild: true }(隔离 prepare 读取依赖方随包发布的工作区文件)。
  3. 修复后 git 安装:隔离 prepare 构建通过(prepare: Done),dsh --dump-config 显示 permission-rules 行,headless deny 冒烟(fixture-deny 回放)产出 permissionRules/decision(deny, "禁止 push 到受保护路径") ✅。

8. GitHub CI 与 Release(2026-08-14)

  • 新增 .github/workflows/ci.yml(pnpm 11.7.0 + Node 22:install --frozen-lockfiletypechecktestbuild),提交 38c28c0 的 Actions 运行 success
  • tag v0.1.0 + GitHub Release:https://github.com/PerryLink/dsh-permission-rules/releases/tag/v0.1.0 ,附件 dsh-permission-rules-0.1.0.tgz(61,771 bytes)。
  • 演示 profile 已重装到最新提交 38c28c0(git 通道 + allowBuilds 键),--dump-config 行生效、headless deny 冒烟通过。
  • profile 的 pnpm peers check 显示 rc.6 peers 缺失属预期(autoInstallPeers: false 的 profile 惯例):运行时经 $DSH_HOME/profiles/node_modules healed fallback 解析,--dump-config 与 headless 运行均已实证可加载。