Separate local parsing from model indexing, bind review decisions to immutable manifests, persist vectors behind active profiles, and expose retrieval, chat, evaluation, and document workflows through the React workbench. Constraint: Live Bailian authentication currently fails for all three configured capabilities Rejected: Direct upload-to-embedding flow | bypasses local review and manifest binding Confidence: high Scope-risk: broad Directive: Keep private-data deployment blocked until authentication, RBAC, and separate database roles land Tested: make verify; fresh and replay Docker document smoke; worker recovery smoke; frozen synthetic evaluation; migration 0003-0004 roundtrip Not-tested: Successful live Bailian calls, OCR, real multi-user authorization
4.7 KiB
ADR-0007:拆分本地解析 Worker 与模型 Worker 的信任边界
- 状态: accepted
- 日期: 2026-07-13
背景
文档入库同时涉及两类高风险资源:原始上传文件,以及访问云模型的能力。若一个通用 Worker 同时挂载上传卷、内部模型 Token 和模型网络,那么解析器漏洞、恶意 DOCX/PDF 或任务 payload 缺陷可能把未经审批的原文直接发送到云端。仅靠业务代码中的状态判断,无法形成可独立验证的 最小权限边界。
本项目仍采用模块化单体和同一个后端镜像;拆分的是运行身份、capability、网络、卷和 Secret, 不是拆成两套业务服务或数据库。两个 Worker 当前仍共用同一个 PostgreSQL 应用角色,因此该拆分 只隔离上传卷、模型 Token 和网络能力,不构成数据库授权边界。
决策
Compose 运行两个互斥能力的长期 Worker:
| 边界 | worker-local |
worker-model |
|---|---|---|
| capability | document_parse |
embedding |
| 数据库网络 | 有 | 有 |
| 内部模型网络 | 无 | 有 |
| 公网 egress | 无 | 无;只能访问 internal model-gateway |
| 上传卷 | 有 | 无 |
| 数据库 app Secret | 有 | 有 |
| model-gateway Token | 无 | Worker 身份 Token |
| 百炼 API Key | 无 | 无 |
model-gateway 是唯一持有百炼 API Key 和普通 egress 网络的进程;它不连接数据库、不挂载上传
卷,也不发布宿主机端口。worker-model 只能发送数据库中已通过 manifest 审批的 cloud_text,
不能读取原始文件。worker-local 能读取隔离上传卷,但没有模型网络与 Token,即使解析器被恶意
文件影响,也缺少直接调用模型的凭据和路由。
两个 Worker 均以非 root 后端用户运行,根文件系统只读,使用临时 /tmp、no-new-privileges
和 cap_drop: ALL,不暴露端口。它们共用 PostgreSQL 任务队列,但领取 SQL 只匹配各自
required_capability。心跳、失败回写、阶段提交和最终激活必须匹配 job_id + lease_owner + lease_token 且租约仍有效,避免旧进程覆盖已重领任务。
上传卷初始化由一次性 upload-init 完成。该容器临时以 root 运行,但 network_mode: none、根
文件系统只读,只保留初始化目录所需的最小文件能力,成功后正常 Exited (0);长期 API 和
worker-local 不获得这些额外 capability。
被否决方案
- 单一通用 Worker 同时挂卷和模型 Token: 部署简单,但把未审核原文与云出口放在同一 攻击面,违背 manifest 审批门禁。
- 只靠 Python
if review_state == CLOUD_APPROVED: 状态判断仍然需要,但不能替代网络、 Secret 和文件系统的纵深隔离。 - 每种任务拆成独立代码仓库/数据库: 当前单机规模没有对应收益,会引入分布式事务、双写、 部署和备份复杂度。
- 让
worker-model直接访问百炼公网: 会把供应商协议、Key 和公网出口扩散到业务进程, 无法集中轮换、限流和脱敏错误。
影响
- 优点:原始文件与云调用能力不能在单个长期 Worker 中汇合;Compose 契约可直接验证卷、网络 和 Secret;不同任务可独立扩缩容。
- 代价:需要维护两个 Worker 服务和 capability 路由;跨阶段任务只能通过数据库中的已审批 工件交接,不能依赖本地临时文件。
- 限制:Docker internal network 是重要纵深防线,但不是形式化沙箱;API 与两个 Worker 当前共享 数据库应用角色,文档 Actor 也仍是服务器固定的 synthetic 身份。真实多用户/私有数据部署必须 先落地认证、对象级 RBAC、分离数据库角色或受控存储过程,并结合主机防火墙、出口 allowlist、 集中 Secret Manager、容器运行时策略和审计。
后续约束
- 新增 OCR Worker 时默认归入本地高风险解析边界,除非独立 ADR 证明它需要模型出口;OCR 结果仍须重新生成并审批 outbound manifest。
- 任何服务若要同时获得上传卷与模型网络/Token,必须先做威胁建模、更新 ADR 并增加 Compose 契约测试,不能通过临时排障静默扩大权限。
- 新任务类型必须声明唯一
required_capability,并验证错误能力的 Worker 无法领取。 DOCUMENT_NAMESPACE_MODE只能由服务端部署配置选择fake或bailiansynthetic 命名空间; 请求不得携带任意 scope 绕过授权。真实多租户上线前必须用认证/RBAC 替代固定 Actor。- 变更 Worker 网络、卷、Secret 或部署拓扑时,必须重跑 document pipeline Docker E2E、租约 fencing smoke、Secret 扫描和 Compose 安全契约。