squash合并: - 意图识别层论证(8维度+10业界佐证) - 多主题上下文管理愿景+并存论证+补充论证(多轮agentic) - 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化) - 前端架构技术债清单归档
8.2 KiB
8.2 KiB
工作流审批子系统对抗审查报告
2026-06-14 · 多代理审查(31 agents / 5 维度 × 对抗验证) + 主代理独立复核 + 二轮对抗论证 范围: df-workflow/{executor,state,eventbus}.rs · df-nodes/human_node.rs · df-core/events.rs · src-tauri/{commands/workflow,state}.rs · src/stores/project.ts · src/api/{types,workflow}.ts · src/views/ProjectDetail.vue 方法: workflow fan-out 审查 → 每发现独立对抗验证(默认反驳) → 主代理读源码复核 criticals → 二轮对抗论证(存在性/可达性/危害三维)
TL;DR
- 头号 bug [P0 阻断]:
human_node.rs:41发HumanApprovalRequest缺.await。EventBus::send是 async fn,let _ = async_fn()丢弃 Future 未 poll → body 不执行 → Request 从未进入 channel。审批链最上游断裂。 - ①②(前端契约失配) 被头号遮蔽: Request 没发 → 前端
onEvent收不到 → type 匹配分支不可达。修 41 行后 ①② 才显形为 critical。 - 当前 UI 无 human 节点 DAG 入口:
demoDag仅 script 节点;AI 工具run_workflow返回提示不执行。审批相关 9 项发现现实触发率=0,全部潜伏。 - 根因: 审批功能前端从未端到端跑通(41 行 await 漏掉即铁证),单测绿但不覆盖 human→前端弹窗→审批→返回链路。
0. 头号发现 [P0]
0.1 缺陷定位
crates/df-nodes/src/human_node.rs:41-47
let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest {
execution_id: ctx.execution_id.clone(),
node_id: ctx.node_id.clone(),
title: title.to_string(),
description: description.to_string(),
options: options.clone(),
}); // ← 无 .await
0.2 机制
EventBus::send签名 (eventbus.rs:33):pub async fn send(&self, event: WorkflowEvent)— async fn。let _ = async_fn()求值得 Future,绑定_后语句结束立即 drop,Future 零 poll。- async fn body (
self.sender.send(event)) 仅在 Future 被 poll 时执行 → 此处永不执行 → Request 未进 broadcast channel。
0.3 对抗自检(排除假阳)
| 质疑 | 核实 |
|---|---|
| 测试为何 pass? | normal_approval_returns_decision 的 helper send_response 用 send(...).await(human_node.rs:172 有 await) 发的是 Response;HumanNode 的 Request 那行没 await。测试只断言 Response 被 rx 收到并返回 decision,不验证 Request 是否发出。绿测不能证伪。 |
| 编译器为何不报? | let _ = expr 合法通配符绑定;async fn 生成的 Future 默认无 #[must_use],零 warning。 |
| 是否误用同步 fn? | eventbus.rs:44 emit_human_approval_request 是同步 fn(返 Result),但 HumanNode 未用它,用的是 async send。 |
| 多代理为何漏? | 审查聚焦前端契约(①②)与串扰(③),未逐行核 HumanNode 内 send 调用点是否 await。主代理二轮读 human_node.rs 全文才发现。 |
0.4 触发路径
human_node.rs:38 subscribe → :41 send(无await) → Request 未发 → workflow.rs 转发任务 rx 收不到 → 前端 onEvent 不触发 HumanApprovalRequest 分支 → pendingApproval 恒 null → 弹窗不开 → HumanNode select! 阻塞至 :86 sleep_until(deadline) 默认 3600s 超时 → Err → 工作流 failed。
0.5 修复
- let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { ... });
+ ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { ... }).await;
1. 反转结论
1.1 ①② 被头号遮蔽(降级: 当前不可达)
- ①
project.ts:214payload.event?.type === 'HumanApprovalRequest'(大驼峰)vs 后端events.rs:18#[serde(tag="type", rename_all="snake_case")]→ 序列化值'human_approval_request',永不匹配。 - ②
project.ts:215payload.event.data—— event 是WorkflowEvent本体扁平结构({type, execution_id, node_id, title, description, options}),无data包装层 → undefined。 - 两者代码层面确凿,但 Request 未发(§0 遮蔽) → 前端收不到事件 → 分支不可达。修 41 行后 ①② 立即变 critical。
1.2 当前 UI 无 human 节点入口(多数发现现实触发率=0)
| 证据 | 位置 |
|---|---|
| 前端唯一 DAG = demoDag,仅 3 个 script 节点 | ProjectDetail.vue:271-273 |
| AI 工具 run_workflow 返回提示不执行 DAG | tool_registry.rs:346-350 |
| runDemoWorkflow 按钮 disabled=workflowRunning 防重入 | ProjectDetail.vue:281-289 |
→ 审批相关发现(①②④⑤⑦⑧⑨⑩)全部潜伏在未接入路径。
2. 逐条对抗裁定
| # | 定位 | 存在性(代码层) | 现实可达性 | 危害校正 | 裁定 |
|---|---|---|---|---|---|
| 0 | human_node.rs:41 | 确凿 缺await | human DAG 运行即必现,UI 无入口 | Request 不发,链最上游断 | 真·潜伏头号 P0 |
| ① | project.ts:214 | 确凿 snake_case | 被0遮蔽+无human入口 | 潜伏,修0后才显形 | 降级: 当前不可达 |
| ② | project.ts:215 | 确凿 flat无data | 同① 双重遮蔽 | 潜伏 | 降级: 同① |
| ③ | workflow.rs:67-97 | 确凿 全局bus+Node*无exec_id+matches!只看变体 | 需并发工作流,UI防重入+AI不执行 | 审批路由不坏(Request自带exec_id),仅日志串扰+提前break | 真实架构缺陷,现实触发0;危害被夸大 |
| ④ | project.ts:15-21 | 确凿 单槽 | 三重遮蔽(0+无入口+单流) | 潜伏 | 真实,潜伏 |
| ⑤ | project.ts:208-261 | 确凿 无终态监听 | 同④;"稍后"按钮可视觉关 | UX退化非卡死 | 真实,危害偏低 |
| ⑥ | state.rs:106 | 确凿 直接insert | 需⑦竞态命中 | snapshot()零调用方(死代码),DB不受影响 | 真实但无害 |
| ⑦ | project.ts:228 | 确凿 无互斥 | 需0+①②+入口全通+双击+500ms窗口 | 窄窗口 | 真实,潜伏 |
| ⑧ | workflow.rs:171 | 确凿 无校验(对比cancel有registry守卫) | 需human+超时窗口 | 诊断损失非功能损坏 | 真实,潜伏 |
| ⑨ | human_node.rs:73-81 | 确凿 warn+continue | 需256积压;当前无节点发NodeProgress,每节点≤2事件,需128+并发节点 | 概率近0 | 真实,当前不可达 |
| ⑩ | workflow.rs:91-97 | 确凿 Lagged+Closed死代码(bus持于AppState全程) | 同⑨ 需256积压 | DB终态仍正确,仅task泄漏 | 真实,当前不可达 |
| ⑪ | workflow.rs:140 | 确凿 String::new() | demoDag失败即触发 | 低危: error字段含first_err.context"节点X失败",failed_node冗余空 | 真实且当前可达,危害低 |
3. 根因
审批功能前端从未端到端验证。证据链:
B-260614-03a重写 HumanNodeexecute(subscribe→send→select!) 时引入:41await 缺失,7 单测全绿未抓(单测不覆盖 Request 发出)。- 前端无 human 节点 DAG 入口,无任何集成测试覆盖 human→弹窗→审批→返回链路。
- ①② 是前后端契约层断裂,
types.ts:127event.type: string弱类型无编译期拦截。
→ 单测绿 + 无端到端测试 = 这批潜伏 bug 的存活土壤。
4. 修复优先级
| 序 | 动作 | 优先级 | 依赖 | 会暴露 |
|---|---|---|---|---|
| 1 | 补 human 节点端到端集成测试(含human的DAG→运行→断言前端收到Request+弹窗开) | P0 | 无 | 0+①+② |
| 2 | human_node.rs:41 加 .await | P0 | 测试暴露后修 | — |
| 3 | project.ts:214 type→snake_case; :215 取 event 本体字段; types.ts event.type 收窄字面量联合 | P0(修2后) | 2 | — |
| 4 | ③ Node*事件补 execution_id + 转发过滤 | P2 | 并发工作流时 | — |
| 5 | ④⑤ pendingApproval 改 Map + 终态清空 | P2 | 2③后 | — |
| 6 | ⑥ set_cancelled 查终态 no-op | P3 | 无(无消费者零危害) | — |
| 7 | ⑦⑧⑨⑩⑪ | P2-P3 | 见§2 | — |
附: 多代理 workflow 元数据
- 31 agents / 5 维度(concurrency / state-machine / error-handling / lifecycle / frontend-contract) × 对抗验证
- 26 原始发现 → 12 确认 / 11 反驳 / 3 验证代理因 API 限流未跑(主代理手动补判)
- 验证阶段正确识别
executor.rs:124三条为 not-a-bug(B-03b-R1 已修,:127有 is_cancelled guard + test_cancelled_node_skips_set_failed) - 漏抓头号(§0): 因未核 send await,主代理二轮复核补