# 工作流审批子系统对抗审查报告

> 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，主代理二轮复核补
