# 工作流审批子系统对抗审查报告 > 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 1. **头号 bug [P0 阻断]**: `human_node.rs:41` 发 `HumanApprovalRequest` 缺 `.await`。`EventBus::send` 是 async fn,`let _ = async_fn()` 丢弃 Future 未 poll → body 不执行 → **Request 从未进入 channel**。审批链最上游断裂。 2. **①②(前端契约失配) 被头号遮蔽**: Request 没发 → 前端 `onEvent` 收不到 → type 匹配分支不可达。修 41 行后 ①② 才显形为 critical。 3. **当前 UI 无 human 节点 DAG 入口**: `demoDag` 仅 script 节点;AI 工具 `run_workflow` 返回提示不执行。审批相关 9 项发现现实触发率=0,全部潜伏。 4. **根因**: 审批功能前端从未端到端跑通(41 行 await 漏掉即铁证),单测绿但不覆盖 human→前端弹窗→审批→返回链路。 --- ## 0. 头号发现 [P0] ### 0.1 缺陷定位 `crates/df-nodes/src/human_node.rs:41-47` ```rust 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 修复 ```diff - let _ = ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { ... }); + ctx.event_bus.send(WorkflowEvent::HumanApprovalRequest { ... }).await; ``` --- ## 1. 反转结论 ### 1.1 ①② 被头号遮蔽(降级: 当前不可达) - ① `project.ts:214` `payload.event?.type === 'HumanApprovalRequest'`(大驼峰)vs 后端 `events.rs:18` `#[serde(tag="type", rename_all="snake_case")]` → 序列化值 `'human_approval_request'`,永不匹配。 - ② `project.ts:215` `payload.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. 根因 审批功能前端从未端到端验证。证据链: 1. `B-260614-03a` 重写 HumanNode `execute`(subscribe→send→select!) 时引入 `:41` await 缺失,7 单测全绿未抓(单测不覆盖 Request 发出)。 2. 前端无 human 节点 DAG 入口,无任何集成测试覆盖 human→弹窗→审批→返回链路。 3. ①② 是前后端契约层断裂,`types.ts:127` `event.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,主代理二轮复核补