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 一律写「远程服务器」。
This commit is contained in:
1 parent
c70d5d860e
commit
5ad755116e
173 files changed
+27632
No files matched your search
@@ -0,0 +1,93 @@
|
||||
# 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 的平台托管段接管;两段并存时「整体替换」语义会互相覆盖。
|
||||
Reference in new issue
Block a user