Files
dsh_shenxian/dsh-server-docs/04-调整方案/66-业务插件P0误报与admin显式信任.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

5.6 KiB
Raw Blame History

66 · 业务插件上传的「P0 误报」与 admin 显式信任

  • 日期:2026-09-12
  • 触发:把 AnySearch 插件投放进候选池时被安全检测 P0 阻断(命中 package/README.md 等 4 个文档);用户提出「让 admin 在插件管理里手动标注信任」
  • 结论一句话:安全检测从「命中即 400、命中详情只进日志」改成「默认拒绝但逐条回显 + admin 显式声明信任后放行并留痕」 —— 防线不动,把判断责任交给本就可信、且能看到证据的 admin。
  • 状态:✅ 已实现、已验证、已部署(2026-09-12 11:15)

TL;DR|结论:P0 规则 /\.credentials\.yaml/ 是纯字面量匹配、扫所有文本文件,第三方插件的 README 只要教用户把 key 写进凭据文件就会被判「读取实例会话密钥」—— 这是第二次同类误报(档案 29 记过 dsh-memory 的 docs/…example.yml)。 关键:不降低规则强度(不改规则),改为 fail-closed + 可显式覆盖 + 全程留痕。 状态:✅ 已上线


一、问题

1.1 现象

投放 @anysearch/[email protected] 时:

安全检测未通过(P0 阻断):读取实例会话密钥(package/README.md)
statusCode: 400

按 P0 规则全集扫描该包,命中 4 处,全部是文档、代码文件 0 命中:

文件 命中内容
README.md "Store the key in $DSH_HOME/.credentials.yaml"
README.zh-CN.md 中文版同句
docs/user-guide.zh-CN.md 凭据解析顺序说明
docs/agent-installation.md 指导 agent 让用户配置 key

即:教用户正确配置凭据被当成了读取凭据。

1.2 根因

src/web/security-scan.ts 的 P0 规则 { re: /\.credentials\.yaml/, why: '读取实例会话密钥' }:

  • 纯字面量匹配,无上下文判断
  • 扫描所有文本文件(SCAN_TEXT_EXT 含 .md)
  • 命中即 throw httpError(400, ...),调用方无法裁决
  • 命中详情只能通过异常消息带出一条,且旧文案是脱敏的一句话 —— admin 看不到"到底哪里命中了什么"

1.3 为什么不能靠收窄规则解决

两个候选方案都被否:

方案 否决理由
把 P0 规则限定在代码类文件(.md/.txt 降级为 P1) 这是降低所有人的防线(虽然实际防护力损失很小),且以后每遇到一类新误报都要再动一次规则
手工把 tgz 放进候选池(绕过扫描) 绕过安全机制,且不可复现 —— admin 将来通过门户重新上传同一个包仍会被挡

用户提出的方案更优:把判断责任交给 admin(上传本就仅 admin 可做,责任边界清楚),前提是把证据给足。

二、方案

三层改动,规则本身一行未动:

层 改动
扫描 新增 scanDirDetailed():收集 P0 命中而不抛(walk() 抽出为共享遍历);scanDir() 行为完全不变(技能上传等路径仍"命中即 400")
上传 API P0 命中且未声明信任 → 409 + blocked[](文件 + 原因)+ warnings[];带 trust.confirmed=true → 放行 + 写两条 audit
门户 被挡时逐条列出命中(文件 + 规则)+ 信任理由输入框 +「我已逐条确认,信任并投放」按钮

三条设计底线(写进代码注释与 UI 文案):

  1. 必须显式 —— 缺省 fail-closed,没有"默认放过"的路径;
  2. 必须留痕 —— upload_business_plugin(含 trustedOverride)+ trust_business_plugin(含逐条命中与理由)两条审计;
  3. 必须展示证据 —— 让 admin 真的看到自己信任了什么,而不是点一个盲确认。

三、实现

文件 改动
src/web/security-scan.ts 新增 ScanBlock / ScanResult;walk() 抽出共享;scanDir() 保持抛错语义;新增 scanDirDetailed()
src/web/routes/business-plugins.ts StagedPlugin 增 blocked/warnings;stageTgzArchive() 改用收集模式;POST 接受 trust 并裁决 + 留痕
web/portal.html 抽出 doUpload(file, trust);新增 renderScanBlocked() 渲染命中详情 + 信任入口

自检:npm run typecheck / npm run build 均 exit 0。

四、验证(2026-09-12 部署后实测)

场景 期望 实测
不带信任投放 AnySearch fail-closed 拒绝 + 列 4 处命中 ✅ 列出 README.md / README.zh-CN.md / docs/agent-installation.md / docs/user-guide.zh-CN.md,exit 2
带 --trust 投放 放行 + 落库 + 留痕 ✅ 投放成功;business_plugins 已有 @anysearch/anysearch-dsh @ 0.1.4
审计留痕 两条 ✅ upload_business_plugin{trustedOverride:true, via:…} + trust_business_plugin{blocked:[4条], reason:…}
既有行为未受影响 技能上传仍 fail-closed ✅ scanDir() 未改语义(仅内部重构)

五、待办

  1. 官方目录批量导入路径(/api/plugins/whitelist/import)未接信任入口 —— 它仍会因 P0 直接失败(结果是 {ok:false, error})。走那条路的插件目前无法信任投放;需要同样的「命中详情 + 信任」UI(多选批量的交互要单独设计)。
  2. 信任状态未持久化 —— 本轮未加 DB 列,列表页暂不显示「已信任」徽标。建议与「默认开启」的字段合并成一次 migration v6(避免两次迁移 + 两次重启)。
  3. scripts/ensure-anysearch-admin.cjs 的手工覆写段待退役 —— 其 web provider 覆写职责已由档案 65 的平台托管段接管;两段并存时「整体替换」语义会互相覆盖。