docs(plan): v0.1 acceptance review (pre W4-D/E)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
9e6ff33ef4
commit
cda9255083
1 file changed
+216
@@ -0,0 +1,216 @@
|
||||
# v0.1 独立验收审查
|
||||
|
||||
审查日期:2026-09-27。基线提交 `9e6ff33`。审查期间另一子代理在同一工作区修 `03-decoupling-audit.md`(W4-D),工作区里出现了它的未提交改动。本报告的结论只针对 HEAD `9e6ff33`,那些改动不在审查范围内。
|
||||
|
||||
对照依据:`00-first-version-plan.md` §3.1 / §3.2,REQ v0.7 §2.1、§15.1-15.2、§16.1(只看与 M1 相关的 AT),以及 `01`、`02`、`03` 三份计划文档、README、runbook 和 ADR。
|
||||
|
||||
## 0. 总体结论
|
||||
|
||||
**现在还不能打 v0.1.0。** 下面 4 个阻塞项处理完后可以打,都不需要改架构。
|
||||
|
||||
代码主体已经达到 M0+M1 纵切的要求。实测结果:
|
||||
|
||||
- `deno task ci` 连跑 2 次都通过(退出码 0),耗时 136 秒和 133 秒。两次测试数完全一致:server 136、adapters 236(另有 2 个 skip)、agent 30、contracts 508、db 12、domain 230、mapping 22、tasks 15、testkit 4、web 3,合计 1196 个通过。
|
||||
- 前端冒烟 `deno task test:web:smoke` 22/22 通过,耗时 49 秒。
|
||||
- 五条原则都有 schema 或代码层面的保障。
|
||||
|
||||
阻塞发布的原因有四个:
|
||||
|
||||
1. `03` 清单里的 P0 还没修完(后台循环绕过任务引擎;HTTP 请求里同步执行重活)。
|
||||
2. 版本号还是 0.0.0。
|
||||
3. REQ §15.2 要求 M1 必须验证的一项"授权内异常调查**与恢复**"没有实现,也没有在任务书里写明延期。
|
||||
4. `POST /organization-records` 的 `sourcePaths` 可以指向服务器上任意路径,Move 时还会删源,违背原则 5。
|
||||
|
||||
补充说明:
|
||||
|
||||
- ci 耗时比任务书记录的约 75 秒涨到约 135 秒。其中 server 测试 55 秒、tasks 测试 34 秒。
|
||||
- 冒烟跑在 `PORT=4392 WEB_ROOT=apps/web/build/client` 上,用单容器同端口托管方式,管理员账号 `w31-admin`。
|
||||
|
||||
## 1. 问题清单(按严重度)
|
||||
|
||||
### 1.1 阻塞发布
|
||||
|
||||
| # | 问题 | 位置 | 修法 |
|
||||
|---|---|---|---|
|
||||
| R1 | `03` 的 P0 未落地。`start()` 的 `tick` 仍在直接调 `planImport`/`executeRecord`(`sweepPendingImports`),没有租约保护;`/download-cache/cleanup` 和 `/library/reconcile` 在 HTTP 请求里同步执行 | `apps/server/src/modules/acquisition-organize/index.ts:118-151`,`routes/downloads.ts`、`routes/library.ts` | 按 03 清单 P0 改为 enqueue,并返回 `{taskId, runId}`。W4-D 合入且 ci 通过后,再重跑本报告第 3 节的 e2e |
|
||||
| R2 | 所有包和 app 的版本都是 `0.0.0`,`/health` 返回的也是 `version:"0.0.0"`;仓库里没有任何 tag | `apps/server/deno.json:3` 等 10 处 | 统一改成 `0.1.0`(server 的 `denoJson.version` 会写进备份清单和 health),再打 `v0.1.0` tag |
|
||||
| R3 | REQ §15.2 要求 M1 验证"授权内异常调查与恢复",AT68 也要求"查证后自动执行获准修正并继续"。现在后台调查固定是 `plan_only`:E5 只把结论写回待处理事项并交还人工,没有自动恢复这条路径 | `apps/server/src/modules/agent/service.ts:323-340`,`test/e2e/e5-auto-investigate.test.ts` | 二选一。(a) 在有 `automation_grant` 时允许调查 run 以 `execute` 模式调用 `task.retry` 或 `identity.resolve`,并加一条 e2e:下载失败 → 调查 → 授权内重试 → 入库。(b) 在 `00` §3.2 写明"授权内恢复延到 M2",并在 README 的范围说明里注明。不能不写就发布 |
|
||||
| R4 | `POST /organization-records` 的 `sourcePaths` 没有任何根目录约束。持 `organize.execute` 的 API Key 可以 walk 任意目录(dryRun 时会泄漏文件清单),也可以把任意文件 Move 进媒体库并删掉源文件。这条路径不检查 automation_grant,也不检查 H&R | `apps/server/src/modules/acquisition-organize/services/organize.ts:229-241`,`routes/organization.ts:84-130` | `sourcePaths` 必须落在已登记的下载器路径映射(本机视图)或显式的导入根下,并且不能在任何 library_root 内;`action=move` 且调用方是 api_key 时要求有持续授权。补 403/422 反例测试 |
|
||||
|
||||
### 1.2 应修(v0.1.x 内,11 项)
|
||||
|
||||
| # | 问题 | 位置 | 修法 |
|
||||
|---|---|---|---|
|
||||
| S1 | 持 `organize.execute` 的 API Key 可以 `POST /approvals` 再 `PATCH /approvals/{id}` 自己批准,再去调 `download.submit` 或 `organize.execute`,单次审批就形同虚设 | `packages/contracts/src/api.ts`(approvals 的 scope),`modules/agent/routes.ts:~280-311` | 把审批的决定动作(PATCH 改为 approved)的 scope 改成 `admin`,或者用 `requireAdminSession`;并禁止 `decided_by` 与计划发起人是同一个 api_key |
|
||||
| S2 | 服务端 logger 的脱敏覆盖面比 agent 和 adapters 的实现窄。实测 `redactString("/rss?rsskey=abc&torrent_pass=dead&authkey=zz&downhash=q1&c_secure_pass=cc")` 只遮住了 passkey。现在一共有 4 份脱敏实现(W1 的 S8 还没解决,反而多了一份) | `apps/server/src/logger.ts:6-8`,`packages/agent/src/redact.ts`,`packages/adapters/src/{rss,browser}/redact.ts` | 在 contracts(或新建的共享包)里保留唯一一份实现,取 agent 版的参数全集;logger 改为复用它;在 `logger.test.ts` 加上述 5 类参数的用例 |
|
||||
| S3 | TMDB 网络错误时,`TmdbError` 的 message 直接拼上 Deno fetch 的错误信息。Deno 的这条信息里带完整 URL,也就带着 `api_key`;`cause` 里同样有(W1 的 S7 没有解决) | `packages/adapters/src/tmdb/client.ts:171-175` | message 先经过 redact,`cause` 不透传;或者强制使用 Bearer(accessToken) |
|
||||
| S4 | 服务端授权器的单次审批分支(payloadHash/revision 变化后失效、过期、用后标为 consumed、approvalId 与计划不符)**没有任何测试**。AT15/AT16 在产品层也没有被验证 | `apps/server/src/modules/agent/authorize.ts:115-137` | 新增 `test/agent/authorize.test.ts`,对真实 PG 覆盖 6 条正反例;再加一条 e2e:审批后改计划 → 被拒 → 重新审批 → 成功且只消费一次 |
|
||||
| S5 | M1 要求的"一部常规剧集"没有端到端链路。E1 和 E2 只覆盖电影;剧集只在识别单测(resolve)和整理单测(organize-move)里分别出现过 | `apps/server/test/e2e/` | 新增 E7:RSS 剧集 S01E01 → 识别出 series 和单元 → Slot → qB → Copy 入库;第二集改译名后仍进旧目录 |
|
||||
| S6 | AT24(管理标签被移除、qB 任务消失)在服务端没有测试。代码在 `downloads.ts:359-383` 里,只有 domain 层的 `download-state` 单测覆盖 | `__tests__/vertical.test.ts` | 补两个断言:去掉标签后 `managed=false`、历史保留、之后不再暂停也不再清理;任务消失后 `removed`,并且库文件还在 |
|
||||
| S7 | 前端 Agent 页在生产构建里也能用 `?mock=1` 打开 mock 模式(sessionStorage 会记住),可能把演示数据当成真实运行展示,与 AT78 冲突 | `apps/web/app/api/agent.ts:37-48` | 只在 `import.meta.env.DEV` 或设置了 `VITE_AGENT_MOCK` 时启用 |
|
||||
| S8 | 引擎面板只有 cancel,没有 retry/rerun;§3.1-10 要求 "cancel/retry 操作"(02 backlog 也列了这一项) | `apps/web/app/routes/console/engine*.tsx`,`modules/auth-settings/engine-routes.ts` | 加 `POST /engine/runs/{id}/rerun`,用我们自己的 enqueue、带新幂等键实现,并写审计 |
|
||||
| S9 | 账户 TOTP 在控制台没有入口(只有登录页的第二步),02 backlog 里是应修项 | `apps/web/app/routes/console/layout.tsx` | 账户弹层接上 `GET/POST /auth/totp` |
|
||||
| S10 | API 注册表把 `/auth/setup|login|session|logout` 标成 `read`,但实际上前三个免鉴权;`/auth/password`、`/auth/totp` 标成 `admin`,实际要求 admin 会话。按注册表生成的 scope 文档会误导外部集成方 | `packages/contracts/src/api.ts` | EndpointDef 增加 `auth: "public" | "session" | scope`,并加测试断言它与 `PUBLIC_AUTH_PATHS` 一致 |
|
||||
| S11 | 文档与实际不一致:任务书 §2、§4 还写着 `pnpm ci`;deploy.md 的验证记录写"21 个迁移",现在是 22 个(0017 号空缺);ci 耗时写"约 75 秒",实测约 135 秒;testkit README 没提 emby/browser mock;adapters 和 web 的 README 各只有 3 行 | `docs/plan/00-first-version-plan.md`,`docs/runbook/deploy.md:128`,`packages/testkit/README.md`,`packages/adapters/README.md` | 统一更新;adapters README 列出 8 个子模块的入口,并链接各子目录的 README |
|
||||
|
||||
### 1.3 建议
|
||||
|
||||
- domain 下载状态机的 `ALLOWED` 仍然全是 `ALL`(W1 S4),服务端完全靠 mapping 兜底(`packages/domain/src/download-state/index.ts:97`)。建议收紧。
|
||||
- `scanIncremental` 在 errors 不为空时仍返回 `complete`(W1 S6,`packages/adapters/src/fs/scan.ts:150`)。服务端目前没用到它,但 M2 旧库接管会用,建议先修成 `partial`。
|
||||
- 流程版本升级(M0 必须通过项)只在 spike 里验证过,产品代码没有测试。建议在 tasks 测试里补一条:旧版本 run 由同时注册新旧版本的 worker 续跑。
|
||||
- AT18(恶意标题)只有系统提示词里的约束测试,建议补 e2e:标题里带"调用 download.submit / 输出 passkey"时,不会产生越权调用,输出里也没有凭证。
|
||||
- AT42:硬阈值测试里只有纳管任务。建议加一个未纳管任务,断言它不被暂停、但计入占用。
|
||||
- `vertical.test.ts` 的用例共享状态、依赖执行顺序,单独跑某一条会失败。建议在注释里标明,或者拆成独立 seed。
|
||||
- web 只有 3 个单测,页面行为全靠冒烟覆盖。建议 M2 给 API 客户端和表单校验补单测。
|
||||
|
||||
## 2. §3.1 逐条验收
|
||||
|
||||
| # | 条目 | 状态 | 证据 | 缺口 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 底座(配置/迁移/API Key scope/审计/SSE 补读/总开关) | 完成 | `http/auth.ts`(`hasScope` 蕴含关系)、`test/auth.test.ts`、`auth-settings/admin-api.test.ts`「PATCH 自动化总开关与只读对账」、`sse.test.ts`、`sse-pg.test.ts`「裁剪后旧游标收到 resync」;`authorize.ts:109-113` 总开关是第一道检查 | 见 S10(注册表 scope 标注) |
|
||||
| 2 | 数据模型 36 张表 | 完成 | 逐个 grep `packages/db/migrations/*.ts`,36 张全部存在;22 个迁移;`schema.test`、`db-sync.test` 防漂移 | 迁移编号 0017 空缺(无害) |
|
||||
| 3 | 来源(多 RSS / 观众 / 馒头 / 快照 / 启停 / 限流) | 完成 | `sources-identity/ingest.test.ts` 8 条(三 Feed 去重、304、GUID 更新、窗口缺口、403 失效不重试、站点关闭);`mteam-tasks.test.ts`;`api.test.ts`「站点级限流器每站一个实例」「实时补查并发合并」「关闭站点」 | — |
|
||||
| 4 | 身份与元数据 | 完成 | `resolve.test.ts` 13 条(本地召回复用、ambiguous 进池、TMDB 失败记为 lookup_failed、锁定字段、人工纠错修订、合并跳转);`0002_foundation` 启用 pg_trgm | — |
|
||||
| 5 | 规则与选片 | 完成 | `rules-api.test.ts`(Slot/revision 乐观锁、策略发布即持续授权、旧授权吊销、新作品窗口);`acquisition.ts:539-595` 预算/并发/优先级 | Agent 主观选片按 §3.2 不做 |
|
||||
| 6 | 下载(qB 多实例/标签/阈值/状态机) | 部分 | `vertical.test.ts`「硬阈值……只恢复阈值暂停项」「already_exists 接管只补标签」;`downloads.ts:861` 删数据必经 `assertSafeToDeleteData` | S6(标签移除/任务消失无服务端测试);R1(qB 同步与大文件复制在同一个 tick 里) |
|
||||
| 7 | 入库(Copy/Move/EXDEV/路径固定/整理记录 API) | 部分 | `organize-move.test.ts`「跨卷 Move kill 后 resumed……不重复复制」「路径绑定固定」;`vertical.test.ts`「changes 游标增量」 | R4(sourcePaths 无约束) |
|
||||
| 8 | 持久任务 9 个 + outbox + 恢复 + 幂等 + 取消 | 部分 | 引擎注册了 11 个任务(9 个 + snapshotSite + runAgent);`tasks/test/crash-recovery`、`engine.test`「取消经 abortSignal」、`op-ledger`「租约丢失旧 attempt 提交被拒」、`outbox`「两个 relay 并发 60 条只投一次」;E4 | R1:后台循环绕过引擎 |
|
||||
| 9 | Agent(Pi 封装/14 能力/服务端授权/SSE/后台触发/设置) | 部分 | E2「启动断言 14 个能力全部注册」「没有授权被拒」「已提交≠已入库」;`agent/test/runtime.test.ts` 白名单、脱敏、预算、JSONL 恢复;E5 | R3(调查后不能恢复);S4(审批分支无测试);子代理按 02 后置 |
|
||||
| 10 | 控制台页面 + 引擎面板 | 部分 | `apps/web/app/routes.ts` 共 20 个路由;冒烟 22/22 通过 | S8(引擎面板没有 retry);S9(TOTP 入口);原站视图只是占位(符合 §3.2) |
|
||||
| 11 | 可选 Emby | 完成 | `adapters/src/emby/emby.test.ts`「凭据错误 → unknown(auth),而不是不存在」等 10 条;`vertical.test.ts` 调用了 `/inventory/media-servers` | 服务端只测了无实例的情况(建议补 unknown 透传) |
|
||||
| 12 | 运维(备份/只读对账/单容器/runbook) | 完成 | `scripts/backup.sh`、`restore.sh`(校验和、有活动连接时拒绝、单事务、打开 readOnlyReconcile、映射行数比对);`deploy.md` 验证记录里有完整的 compose 演练;`preflight.test.ts` 7 条 | 恢复没有自动化测试,只有人工演练记录(可以接受) |
|
||||
| 13 | 测试(domain 单测 / adapters / RSS→入库 e2e / Agent e2e / 崩溃恢复) | 完成 | E1、E2、E4(worker 子进程 `kill -9`)、`agent` 的 JSONL SIGKILL 用例;ci 两次数量一致 | S5(剧集链路) |
|
||||
| 14 | 管理员登录门禁 | 完成 | `login.test.ts` 8 条(并发初始化只有一个成功、CSRF、同 IP 5 次 / 跨 IP 15 次限速、吊销、改密、TOTP 防重放);实测:已初始化后 setup 返回 409,缺少或伪造 csrf 返回 403,cookie 为 HttpOnly + SameSite=Lax | Passkey 按 02 后置 |
|
||||
|
||||
§3.2 不做项核对:综艺分段、双版本组合校验、浏览器原站视图、HHClub/CHD、旧库接管、Agent 主观选片、多用户,都没有越界实现。BrowserProvider 只有接口和 camofox 骨架(`effaa32`),符合"接口先落"。
|
||||
|
||||
## 3. REQ 验证项对照
|
||||
|
||||
### 3.1 M0 / M1 必须验证项 → 测试
|
||||
|
||||
| 验证项 | 测试 | 结论 |
|
||||
|---|---|---|
|
||||
| Pi 显式白名单、无隐式扩展 | `agent/test/runtime.test.ts`「白名单:未注册工具(bash)……被拒绝」;E5 `download__submit` 被拒 | 有效 |
|
||||
| 执行器崩溃恢复 | `tasks/test/crash-recovery.test.ts`;E4;`runtime.test.ts`「JSONL 恢复 SIGKILL」 | 有效 |
|
||||
| 流程版本升级 | 只有 `docs/spikes/w0b-openworkflow.md` | **缺**(建议项) |
|
||||
| 提前审批不丢失 | 设计上审批存库、invoke 时再读;`tasks/engine.test`「waitForFact 事实先于等待到达不丢失」 | 引擎层有效,产品层**缺**(S4) |
|
||||
| 多 Feed 并发不重复提交 | `ingest.test`「三个 Feed……release.observed 只发一次」(顺序执行);`outbox.test` 并发;E6「重复评估只留一条候选」 | 部分(没有并发 ingest 的测试) |
|
||||
| 改名与映射修订后路径不变 | `organize-move.test`「路径绑定固定」;`resolve.test`「人工纠错映射」 | 有效(两者分开测) |
|
||||
| 观众 + 馒头站点 | `api.test`(观众 mock)、`mteam-tasks.test`、adapters spiders 与 mteam 测试 | 有效 |
|
||||
| qB 标签接管 | `vertical`「already_exists」「硬阈值」中 `adopted=2` | 有效 |
|
||||
| Copy / Move | E1(Copy);`organize-move`(跨卷 Move) | 有效 |
|
||||
| 一个电影 + 一部常规剧集 | E1/E2 电影 | **剧集缺**(S5) |
|
||||
| 整理记录 API | `vertical`「changes 游标增量」;E1 | 有效 |
|
||||
| 备份 / 审计 / 恢复 / 停用开关 | `deploy.md` 演练;`admin-api.test` 总开关;`preflight.test`「映射清单行数比对」 | 有效(恢复靠人工) |
|
||||
| Agent 自然语言 → 查库存/搜索/选片/提交 → 持续流程入库 → 真实汇报 | E2「有持续授权:……汇报来自整理记录」 | 有效 |
|
||||
| 授权内异常调查与恢复 | E5 只调查 | **缺**(R3) |
|
||||
| 关闭聊天不影响后台 | E3「创建后不再请求,任务系统独立推进到 completed」 | 有效 |
|
||||
|
||||
### 3.2 指定 AT 场景
|
||||
|
||||
| AT | 场景 | 测试 | 结论 |
|
||||
|---|---|---|---|
|
||||
| AT01 | 多来源合并 | `ingest.test` 三 Feed | 有效 |
|
||||
| AT02/AT09 | Slot 隔离 | `rules-api`、E6「按作品选 Slot」;授权按 Slot 独立预留 | 部分:没有"两个 Slot 同时缺失分别执行"的专项测试 |
|
||||
| AT12/AT55 | 崩溃恢复 | E4、`op-ledger`「租约丢失」 | 有效;E4 的 kill 时机是"账本行出现",没有精确钉在"qB 已收单、DB 还没写" |
|
||||
| AT13/AT27 | 复制后崩溃 | `organize-move` 跨卷 resumed | 有效(模拟上一次 attempt) |
|
||||
| AT20/AT56 | 备份恢复 | 人工演练(deploy.md);`auth.test` 撤销后 401 | 部分(没有自动化) |
|
||||
| AT16 | 授权失效重校验 | `rules-api`「新版本发布(旧授权吊销)」;审批分支**无测试** | 部分(S4) |
|
||||
| AT23 | 标签纳管 | `vertical` 硬阈值用例 `adopted` + `origin:"adopted"` | 有效 |
|
||||
| AT24 | 标签移除 / 任务消失 | 无服务端测试 | **缺**(S6) |
|
||||
| AT25/AT26/AT41 | 缓存硬阈值 | `vertical`「硬阈值」「缓存清理」(库内陷阱任务被拒删) | 有效 |
|
||||
| AT67 | Agent 一次委托闭环 | E2 | 有效 |
|
||||
| AT70 | 已提交≠已入库 | E2 `waiting.summary` 含"已提交≠已入库",入库后才 `succeeded` | 有效 |
|
||||
| AT79 | SSE 断线补读 | `sse.test`、`sse-pg.test`(resync)、E6「/events?after= JSON 补读」、E2 `afterSeq` | 有效 |
|
||||
|
||||
## 4. 路由 scope 表
|
||||
|
||||
共 185 个端点:read 89、organize.read 3、organize.execute 21、admin 72。所有业务路由都经过三个 helper 之一(`modules/*/http.ts`、`routes/helpers.ts`、`auth-settings/endpoint.ts`),统一加 `requireScope(def.scope)`。`api-registry.test` 保证 OpenAPI 与注册表双向一致。`POST /capabilities/{id}/invoke` 标的是 read,但处理器内部会再按能力描述符的 scope 校验(`agent/routes.ts:325`),例如 download.submit 需要 organize.execute,policy.apply 需要 admin。
|
||||
|
||||
**免鉴权清单**:`GET /api/v1/health`、`GET /api/openapi.json`、`GET /api/docs`、`POST /auth/setup`(已初始化后返回 409,token 至少 16 位)、`POST /auth/login`、`GET /auth/session`,以及 `WEB_ROOT` 静态文件。实测 `/events`、`/settings`、`/library`、`/auth/whoami` 无凭证时都返回 401。
|
||||
|
||||
| 资源 | read | organize.read | organize.execute | admin |
|
||||
|---|---|---|---|---|
|
||||
| `/acquisition-plans` | GET · / {id} | | POST · | |
|
||||
| `/acquisition-slots` | GET · / {id} | | | POST ·; PATCH {id} |
|
||||
| `/agent/runs` | GET · / {id} | | POST ·; POST {id}/messages; POST {id}/abort | |
|
||||
| `/api-keys`、`/secrets` | | | | GET/POST/PATCH/DELETE |
|
||||
| `/approvals` | GET · / {id} | | POST ·; **PATCH {id}**(S1) | |
|
||||
| `/artifacts`、`/asset-locations`、`/asset-unit-coverage`、`/media-assets`、`/path-bindings`、`/observations`、`/reconcile-runs`、`/server-mappings` | GET · / {id} | | | |
|
||||
| `/audit-events` | | | | GET |
|
||||
| `/auth` | setup/login/session(免鉴权)、logout | | | password、totp(要求 admin 会话) |
|
||||
| `/automation-grants` | GET · / {id} | | | POST ·; PATCH {id} |
|
||||
| `/capabilities` | GET ·; POST {id}/invoke(内部再校验) | | | |
|
||||
| `/content-units` | GET · / {id} | | | PATCH {id} |
|
||||
| `/dashboard`、`/inventory`、`/inventory/media-servers`、`/site-definitions`、`/events`、`/health` | GET | | | |
|
||||
| `/decisions` | GET · / {id} / summary | | POST {id}/resolve | |
|
||||
| `/download-cache` | | | | POST cleanup |
|
||||
| `/download-tasks` | GET · / {id} | | POST {id}/pause、resume | |
|
||||
| `/downloader-instances`、`/media-server-instances`、`/library-roots`、`/path-mappings` | GET · / {id} | | | POST/PATCH/DELETE、test|verify |
|
||||
| `/engine/runs`、`/engine/stats` | | | | GET;POST runs/{id}/cancel |
|
||||
| `/external-identities` | GET · / {id} / revisions | | | POST ·; PATCH {id} |
|
||||
| `/feed-sources` | GET · / {id} | | | POST/PATCH、fetch/enable/disable/reset |
|
||||
| `/identity-pool` | GET | | POST {id}/assign、dismiss、retry | |
|
||||
| `/library` | GET · / {id} | | | POST reconcile |
|
||||
| `/media-entities` | GET · / {id} / detail | | POST {id}/identities、refresh;PUT/DELETE {id}/fields/{field} | POST ·; PATCH {id}; POST {id}/merge |
|
||||
| `/organization-records` | | GET · / {id} / changes | POST ·(R4) | |
|
||||
| `/policy-revisions` | GET · / {id}; POST {id}/preview | | | POST/PATCH、publish、rollback |
|
||||
| `/preference-tags`、`/preference-suppressions` | GET · / {id} / revisions | | | POST/PATCH/DELETE |
|
||||
| `/settings` | GET | | | PATCH |
|
||||
| `/site-accounts`、`/sites` | GET · / {id} / snapshots / releases | | | POST/PATCH、enable/disable/snapshot、PUT credentials |
|
||||
| `/source-releases` | GET · / {id} | | POST {id}/identity、unlock | |
|
||||
| `/subscriptions` | GET · / {id} | | | POST ·; PATCH {id} |
|
||||
| `/tasks` | GET · / {id} / operations | | POST {id}/cancel、retry | |
|
||||
| `/title-aliases` | GET · / {id} | | | POST ·; DELETE {id} |
|
||||
|
||||
## 5. 安全快查
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| 凭证进日志 | 访问日志的 URL 经过 `redactString`,但它只认 passkey/token/access_token/api_key/apikey/auth/sign 这几个参数,**漏掉了 rsskey、torrent_pass、authkey、downhash、c_secure_pass**(S2,已实测)。对象字段按键名脱敏。`secrets.test`「SecretError 进日志只有 ref」和 `logger.test` 6 条通过。本次起的服务日志里 grep 不到未脱敏的 passkey/api_key |
|
||||
| 凭证进模型上下文 | `pi/runtime.ts:157` 的工具输出一律经过 `redactForModel`,错误信息经过 `redactString`;`runtime.test`「工具输出进入模型与事件前已脱敏」。凭证存储为 `secret_ref` + AES(`auth-settings/crypto.ts`),响应里只返回 secretRef(`api.test`「凭证只写」、`admin-api.test`「Pi apiKey 只写」)。风险点:TMDB 错误信息里带 api_key(S3),不过 agent 侧的脱敏能认出 `api_key=` |
|
||||
| 鉴权绕过 | 免鉴权清单见第 4 节;DEV_ADMIN_KEY 只在 `NODE_ENV=development` 下生效(`auth.test`);控制台管理员从 Web 手动调用算作人工授权(`authorize.ts:114`),API Key 必须有 grant 或 approval。漏洞:S1(自己批准)、R4(sourcePaths) |
|
||||
| CSRF | cookie 会话的写方法用 HMAC 双提交校验(`auth.ts:170-176`),实测缺少或伪造都返回 403;Bearer 不校验。cookie 为 HttpOnly、SameSite=Lax,`secure` 看直连 https,或者来自私网代理的 `x-forwarded-proto`。login/setup 免 CSRF(可以接受) |
|
||||
| SSE 鉴权 | `/events` 需要 read(`app.ts:102`),`sse.test`「需要鉴权」;实测用 cookie 连上返回 200 `text/event-stream`。给 API Key 调用方的 `?after=&limit=` JSON 补读已实现(E6)。注意:持 read 的 Key 能看到所有 agent 文本增量 |
|
||||
| qB 删数据防护 | 仅有两个 `deleteFiles:true` 调用点(`downloads.ts:879`、`:902`),都经过 `removeWithData`,在本机视图和下载器视图上各跑一次 `assertSafeToDeleteData`;候选必须满足:已入库 succeeded、没有失败的文件级结果、不再被 download_cache/hardlink 位置引用、保留期和 H&R 已满足。Move 后删种用的是 `deleteFiles:false`(`organize.ts:1101`)。`vertical`「缓存清理」里的库内陷阱任务被拒删,有效 |
|
||||
| API Key scope 覆盖率 | 185/185 个端点都有 scope,见第 4 节 |
|
||||
|
||||
## 6. 工程规范
|
||||
|
||||
- **提交规范**:48 个提交全部符合 `type(scope): 描述`,0 个违规。
|
||||
- **命令一致性**:README、runbook、各包 README 里出现的 `deno task *` 都能在 deno.json 里找到;全文没有残留 pnpm/npm 用法(任务书除外,见 S11)。
|
||||
- **包 README 与导出**:agent、db、domain、mapping、tasks 的 README 与导出一致。contracts 有 18 行,尚可。testkit 缺 emby/browser mock 的说明;adapters 和 web 的 README 只有 3 行(S11)。
|
||||
- **ci 时长与稳定性**:2 次分别 136 秒和 133 秒,测试数与结果一致,没有抖动。server 测试 55 秒、tasks 测试 34 秒是主要耗时。
|
||||
|
||||
### 抽查 10 个关键测试(是否断言了真实行为)
|
||||
|
||||
| 用例 | 结论 |
|
||||
|---|---|
|
||||
| E2「没有授权:download.submit 被拒」 | 有效:断言了 tool_call_end 为 isError、SSE 的 phase=denied、审计 `capability.denied`、download_task 为 0、计划停在 draft、Slot 仍为 empty,汇报标注"未经核查" |
|
||||
| E2「有持续授权……」 | 有效:断言了调用序列、账本 grant_id、run 先 waiting 再 succeeded、汇报引用整理记录 ID、`afterSeq` 补读、qB 只 add 一次 |
|
||||
| E4 kill -9 | 有效:qB 只 add 一次、账本只有 1 行、`resumed` 事件、seq 无重复;kill 时机不够精确(见 3.2) |
|
||||
| `organize-move` 跨卷 resumed | 有效:copy 为 skipped、delete_source 为 done、qB 删种时 deleteFiles=false、download_task 为 imported |
|
||||
| `vertical` 硬阈值 | 有效:只暂停纳管的下载中任务、手动 resume 返回 412、恢复水位区间保持暂停、只恢复阈值暂停项、读不到磁盘时保守阻止 |
|
||||
| `vertical` 缓存清理 | 有效:库内陷阱被拒删并给出原因,库副本仍在,asset_operation 已落库 |
|
||||
| `ingest` 三 Feed 去重 | 有效:1 条 source_release、3 条观察、事件只发 1 次、时间分开保存、游标持久化 |
|
||||
| `login` CSRF / 限速 | 有效:403/429 正反例齐全,被拦的请求不落表 |
|
||||
| `sse-pg` 跨进程 | 有效:子进程发布、API 推送,裁剪后收到 resync |
|
||||
| `agent/registry`「approve 类必须经授权」 | 部分有效:只测了注册中心回调,服务端 `createAuthorizer` 的审批分支没有测试(S4) |
|
||||
|
||||
## 7. 五原则核对(每条两处)
|
||||
|
||||
| 原则 | 保障 | 违背或风险 |
|
||||
|---|---|---|
|
||||
| 身份≠名字 | ① `external_identity_accepted_key`:(provider, entity_type, external_id) 在 accepted 状态下部分唯一(`0005_identity.ts:34`);② 合并时用 `merged_into_id` 跳转,旧 ID 保留(`resolve.test`「合并重复作品」) | domain 的匹配仍要求本地候选带 externalId(W1 建议项,没有修) |
|
||||
| 路径绑定固定 | ① `path_binding_active_key`:(entity, season) 同时只允许一个 active 绑定(`0007_assets.ts:88`);② `organize-move`「译名变化后新集仍进旧目录」 | R4:任意 sourcePaths 的 Move 可以从别的库根把文件搬走 |
|
||||
| 模型不是唯一事实来源 | ① `authorize.ts` 在服务端核对 grant/approval,忽略模型参数里的"已同意";② `agent/report.ts` 的汇报取自整理记录,模型文本标为"未经核查"(E2) | S1:外部 Key 可以自己批准 |
|
||||
| 失败≠不存在 | ① 对账时库根离线只标 unavailable,缺失按 suspected → confirmed 推进(`vertical`「资产对账」);② TMDB 失败记为 `lookup_failed`,不进 no_match(`resolve.test`),Emby 失败返回 unknown | `scanIncremental` 出错仍报 complete(建议项) |
|
||||
| 清理按明确授权 | ① `assertSafeToDeleteData` + `lockReason`(入库已验证、没有部分入库、没有引用、H&R);② 只读对账或总开关关闭时拒绝一切自动 execute(`authorize.ts:109-113`、`downloads.ts:557`) | R4:手动 Move 删源时不检查 H&R 和授权 |
|
||||
|
||||
## 8. 给 M2 的前 5 条建议
|
||||
|
||||
1. **合并凭证处理通道**:只保留一份共享的 redact 和 SecretResolver,qB/TMDB/Emby/RSS 统一走 secret_ref(W1 S8)。浏览器接入之后会有 4 类以上凭证流经日志和模型,在 M2 引入 camofox 之前就要收口,并加一条"全仓凭证泄漏"的故障注入测试(REQ §16.2 要求零容忍)。
|
||||
2. **授权模型产品化**:审批的决定限定为管理员会话;调查型 Agent 在授权内可以执行恢复(R3 的完整版);对 grant/approval/budget 做基于属性的测试(AT15/16/32/52)。
|
||||
3. **按 03 清单把 IO 与 Agent 分成两个 worker 组**,给 verifyAndImport、runAgent 分配独立并发度,并加事件循环延迟探针。在性能基线(REQ §16.2)里记录文件数、挂载类型和 ci 耗时,防止 ci 从约 135 秒继续膨胀(可以考虑按包并行跑 `-j`)。
|
||||
4. **e2e 矩阵扩到剧集和部分失败**:剧集追更(S5)、整季包部分已有(AT54)、部分文件入库后不清理(AT29)、多下载器同一 hash(AT73)。这些是 M2 综艺分段建模的前置样本。
|
||||
5. **把恢复和版本升级纳入 CI**:用 docker compose 跑"备份 → 清库 → 恢复 → 只读对账 → 映射比对"的夜间任务,再加一条 openworkflow 版本升级的回归测试,避免只靠人工演练记录。
|
||||
Reference in new issue
Block a user