目录

Blog Pipeline HITL 门控失效事故复盘:进程隔离漏洞、架构设计缺陷与完整修复方案

一次 exec 绕过导致的状态丢失,暴露了 observability 进程隔离与 maxSpawnDepth 架构债务的三重漏洞

一句话总结:一次跳过的用户审核,暴露了observability工具的进程隔离漏洞、架构设计的层次限制、以及历史遗留的技术债务。


Flowchart showing broken HITL gate due to exec process isolation

事故回顾

2026年8月5日,kb-writer(详见 [[kb-writer配置与迭代]])在执行博客发布流程时,发生了一次严重的流程违规:

Phase 4 图片生成完成后,直接执行了 Phase 5(发布到 Hugo),跳过了 HITL 2(终稿审核)。

这意味着:

  • 用户没有机会审核最终文章和图片质量
  • 文章已发布到 https://oklife.me
  • 用户审核权被完全绕过

根因分析

1. 执行方式错误:exec 绕过进程隔离(详见 [[blog-pipeline-observability-system]])

事故的直接原因是 observability 工具的调用方式错误。

错误代码:

# ❌ 错误:每次 exec 都是新进程,_hitl_gate 状态丢失
exec("python3 -c \"
import importlib.util
spec = importlib.util.spec_from_file_location('observability', '...')
obs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obs)
# 此时 _hitl_gate 是空字典!
obs.log_phase_end('HITL 2', success=True)
\"")

问题本质:

  • observability.py 的 _hitl_gate 是进程内内存变量
  • 每次 exec("python3 ...") 启动新进程
  • 新进程的 _hitl_gate 是空字典
  • 门控检查 _check_hitl_gate() 永远返回 False
  • 但实际上,因为是新进程,_hitl_gate 根本不存在,检查逻辑失效

2. 架构设计漏洞:Phase 5 无前置检查

observability.py 的门控机制存在设计缺陷:

现有门控(已存在):

def log_phase_end(phase_name, success=True, tokens=None):
    # 只检查 log_phase_end("HITL x") 是否被跳过
    if "HITL" in phase_name and not _check_hitl_gate(phase_name):
        return  # 阻断

漏洞:

  • 只检查 log_phase_end("HITL x") 是否被正确调用
  • 不检查 Phase 5 是否在 HITL 通过后才启动
  • 如果直接调用 log_phase_start("Phase 5"),门控完全无效

攻击向量:

# 可以完全绕过 HITL 2
log_phase_start("Phase 4: 生图")
log_phase_end("Phase 4: 生图", success=True)
# 不调用 mark_hitl_yielded("HITL 2")
# 不调用 log_phase_end("HITL 2")
log_phase_start("Phase 5: 发布")  # 直接启动!
log_phase_end("Phase 5: 发布", success=True)

3. 历史遗留问题:maxSpawnDepth 限制

Architecture comparison: old 3-layer nested vs current 2-layer flat design

blog-pipeline v2.9 的架构设计受到 OpenClaw maxSpawnDepth: 2 限制的影响。

历史架构(已废弃):

主session → 云天枢(orchestrator) → Phase agents
深度: 0 → 1 → 2

问题:

  • 云天枢(详见 [[博客流水线HITL事故复盘]])被 spawn 后是深度 1
  • 云天枢再 spawn Phase agents 是深度 2
  • 如果云天枢的 subagent 再 spawn,就是深度 3
  • 超过 maxSpawnDepth: 2,卡死

当前架构(v2.9):

主session 直接编排 Phase agents(详见 [[OpenClaw多智能体博客流水线v3架构演进]])
深度: 0 → 1

优点:

  • 所有 agent 在深度 1,不触碰限制
  • 避免了嵌套层数问题

缺点:

  • 主session直接控制所有 Phase
  • 缺乏物理隔离保护
  • HITL 依赖"软约束"(代码检查),而非"硬约束"(session隔离)

修复方案

修复 1:新增 check_all_hitl_gates() 函数

在 observability.py 中新增前置检查函数:

def check_all_hitl_gates():
    """检查所有 HITL 门控是否已通过(Phase 5 启动前调用)
    返回 (True, "") 表示通过,(False, "error_msg") 表示阻断
    """
    required_gates = ["HITL 1: 用户审核草稿", "HITL 2: 用户审核终稿"]
    missing = [g for g in required_gates if not _hitl_gate.get(g)]
    if missing:
        msg = f"🚫 Phase 5 阻断:未完成的 HITL 门控:{missing}..."
        logger.error(msg)
        return False, msg
    return True, ""

使用方式:

# Phase 5 启动前强制检查
_hitl_ok, _hitl_msg = check_all_hitl_gates()
if not _hitl_ok:
    raise RuntimeError(_hitl_msg)

修复 2:强制使用 importlib 加载 observability

在 AGENTS.md 和 SKILL.md 中明确禁止 exec 方式:

⚠️ 绝对禁止用 `exec("python3 -c ...")` 调用 observability 函数。
每次 `exec` 都是新进程,`_hitl_gate` 状态丢失,门控检查失效。
必须用 `importlib.util.spec_from_file_location` 在**当前进程**内加载。

正确代码:

import importlib.util
spec = importlib.util.spec_from_file_location(
    "observability", 
    "/home/oklife/.openclaw/skills/blog-pipeline/observability.py"
)
obs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obs)
# 此时 _hitl_gate 在当前进程中持久化
mark_hitl_yielded = obs.mark_hitl_yielded
check_all_hitl_gates = obs.check_all_hitl_gates

修复 3:更新 SKILL.md 文档

同步 observability.py 的最新代码(含门控逻辑):

# 现有代码(旧版,无门控)
def log_phase_end(phase_name, success=True, tokens=None):
    elapsed = time.time() - run_state["phases"][phase_name]["start"]
    # ... 记录结束时间

# 更新后(含门控)
def log_phase_end(phase_name, success=True, tokens=None):
    # HITL 门控:phase_name 包含 HITL 时,必须先通过 yield
    if "HITL" in phase_name and not _check_hitl_gate(phase_name):
        logger.error(f"[BLOCKED] {phase_name} 被 HITL 门控阻断")
        return  # 直接返回,不记录为 success
    # ... 原有逻辑

验证结果

测试 1:未通过 HITL,Phase 5 应被阻断

import importlib.util
spec = importlib.util.spec_from_file_location('observability', '...')
obs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obs)

# 不调用 mark_hitl_yielded,直接检查
ok, msg = obs.check_all_hitl_gates()
print(f'阻断结果:ok={ok}')  # 输出:ok=False ✅

结果: 门控正确阻断,Phase 5 无法启动。

测试 2:通过 HITL 后,Phase 5 应放行

# 先调用 mark_hitl_yielded
obs.mark_hitl_yielded('HITL 1: 用户审核草稿')
obs.mark_hitl_yielded('HITL 2: 用户审核终稿')

# 再检查
ok, msg = obs.check_all_hitl_gates()
print(f'放行结果:ok={ok}')  # 输出:ok=True ✅

结果: 两个 HITL 都通过后,Phase 5 可以启动。


架构讨论

方案对比

方案架构嵌套深度安全性复杂度
当前方案(C)主session直接编排深度 1中(依赖代码检查)
方案 A主session → publish-agent深度 1高(session隔离)
方案 B主session → orchestrator → agents深度 2高(物理隔离)

方案 A:拆 Phase 5 为独立 agent

主session (kb-writer)
  ├── Phase 0-4: 直接执行
  ├── HITL 2 通过后: sessions_yield
  └── 用户回复"通过"后: spawn pub-yuntianxing (深度1)
         └── pub-yuntianxing 执行 Phase 5
              ├── 调用 check_all_hitl_gates()
              ├── 通过 → 执行发布
              └── 不通过 → RuntimeError 终止

优势:

  • Phase 5 在独立 session 执行
  • 主 session 无法跳过门控检查
  • 即使主 session 代码有 bug,独立 agent 有第二重检查

劣势:

  • 需要定义 pub-yuntianxing agent
  • 增加架构复杂度
  • 当前只有 1 个发布平台,价值不明显

方案 B:重构为 orchestrator 架构

主session → 云天枢(orchestrator, 深度1) → Phase agents (深度2)

问题:

  • 云天枢设计用于小说创作,不是博客流水线
  • 需要重写 YAML 工作流
  • 可能重新触发 maxSpawnDepth 限制

结论: 不值得现在重构。


最终决策

Solution comparison table showing architecture, security, and complexity of approaches A, B, and C

保持方案 C(当前架构),不修改 maxSpawnDepth。

理由:

  1. 现有门控已足够check_all_hitl_gates() + sessions_yield + 规则约束
  2. 架构简单:所有 agent 在深度 1,不触碰限制
  3. 维护成本低:不需要引入新的 agent 或重构架构
  4. 需求不明确:多平台发布需求尚未明确,过早抽象是浪费

未来演进路径:

  • 阶段 1(现在):保持现状,依赖代码检查
  • 阶段 2(有明确多平台需求时):Phase 5 拆为独立脚本
  • 阶段 3(平台变多、逻辑复杂时):构建 pub-yuntianxing agent

经验教训

1. 写规则不等于修问题

本次事故前,AGENTS.md 已经写了"禁止跳过 HITL"的规则。但规则只是文字,没有物理强制力。

正确做法: 用代码机制让违规在技术上不可行。

2. 进程隔离是安全的第一道防线(详见 [[blog-pipeline-observability-system]])

exec("python3 ...") 启动新进程,所有内存状态丢失。这是 Python 的基本特性,不是 bug。

教训: 涉及状态持久化的工具,必须用 importlib 在当前进程加载,不能用 exec

3. 架构设计要考虑历史债务

blog-pipeline v2.9 的"主session直接编排"架构,是为了绕过 maxSpawnDepth: 2 限制而做的妥协。这个历史决策影响了当前的架构设计。

反思: 如果当初 maxSpawnDepth 是 3,是否会有不同的架构选择?

4. 过度设计 vs 设计不足

在只有 1 个发布平台时,构建独立 pub-yuntianxing agent 是过度设计。但在有明确多平台需求时,不构建独立 agent 是设计不足。

平衡点: 根据实际需求演进,不超前设计,也不滞后修复。


附录:修改文件清单

文件修改内容
observability.py新增 check_all_hitl_gates() 函数
SKILL.md更新 observability.py 代码示例(含门控逻辑)
AGENTS.md新增"observability 门控绕过铁律"
MEMORY.md记录事故根因和修复方案

参考链接


本文更新于 2026-08-05