# 架构:五层 → 代码目录的逐层映射 > **实现状态(2026-07-27)**:本文描述目标架构。当前已落地控制平面的模型、 > 安全门禁、事件记录、Run 转移表、恢复决策、最小证据包、内置模拟执行器,以及 > 独立 Host Agent 的注册、token、心跳和 Host 状态老化链路;Step 租约、Adapter > 执行、消息总线替换和生产对象存储仍未落地。 方案 §5.1 定义了五层架构。这里给出每一层**落到哪个目录、边界在哪、谁不能调用谁**。 ``` ┌─────────────────────────────────────────────────────────────┐ │ 交互与集成层 apps/console (Next.js) · CLI · Open API │ ├─────────────────────────────────────────────────────────────┤ │ 控制平面 services/control-plane/flashops_control/ │ │ services/ 编排 safety/ 门禁 api/ 接口 │ ├─────────────────────────────────────────────────────────────┤ │ 执行平面 services/host-agent/flashops_agent/ │ │ adapters/ 工具适配器 │ ├─────────────────────────────────────────────────────────────┤ │ 带外平面 services/host-agent/flashops_agent/oob/ │ │ (独立进程/独立网络,模拟实现在 simulator/) │ ├─────────────────────────────────────────────────────────────┤ │ 证据与智能层 control-plane/.../evidence/ · signatures/ │ │ events/ 事件总线 │ └─────────────────────────────────────────────────────────────┘ ``` ## 依赖方向(单向,不可逆) ``` console ──HTTP/SSE──▶ control-plane ◀──HTTP──── host-agent │ │ │ ├─▶ adapters ─▶ 客户工具 ▼ │ 数据库 └─▶ oob ─▶ 带外控制器 ``` **硬规则:** - `host-agent` **不得**导入 `control-plane` 的任何模块。两者只通过 `docs/agent-protocol.md` 定义的 HTTP 契约通信。理由:Agent 要能单文件分发到客户 Windows 机器, 控制平面代码不进客户测试主机。 - `control-plane` **不得**主动连接 Agent(Agent 是拉模式)。理由:客户测试主机在 内网/NAT 后面,反向连接在真实实验室里活不下来。 - `engine/` **不得**直接读写数据库。它只操作 `Snapshot` 值对象,返回 `Effect` 列表。 持久化由 `runtime.py` 施加。理由:状态机要能不起数据库单测。 - `api/` **不得**包含业务判断。路由只做校验 + 调用 `engine`/`services`。 - 任何危险动作(刷写、Format、Sanitize、断电)**必须**先过 `safety/`, 且 `safety/` 的拒绝发生在下发给 Agent 之前。 ## 控制平面内部分层 ``` flashops_control/ ├─ config.py 配置(产品名、DSN、路径、超时策略) ├─ db.py 引擎/会话/建表 ├─ models/ SQLAlchemy 表定义(贫血模型,只管持久化) ├─ schemas.py Pydantic 出入参(当前最小 API 契约) ├─ events/ │ ├─ bus.py 进程内事件总线(生产换 NATS/Redis,接口不变) │ └─ recorder.py 事件溯源写入:唯一允许改 Run 状态的入口 ├─ engine/ │ ├─ states.py Run/Step 状态枚举 + 转移表(纯数据) │ ├─ recovery.py 五级恢复阶梯策略(纯函数) │ └─ (目标)planner/runtime:独立 Agent 接入后补齐 ├─ safety/ │ ├─ gates.py DUT 绑定 / 系统盘保护 / 破坏性白名单 │ └─ templates.py 签名命令模板注册表 + 参数 schema 校验 ├─ signatures/ (目标)失败签名(8 要素哈希)与聚类 ├─ evidence/ 最小 Evidence Bundle(manifest + 事件流) ├─ services/runs.py 当前内置模拟编排(后续拆成 Agent + runtime) ├─ seed.py 可重复执行的模拟资产与工作流 └─ api/ FastAPI 路由 ``` **`engine/` 的纯度是刻意的。** 当前 `states.py` / `recovery.py` 不导入 SQLAlchemy、不导入 FastAPI、不做 I/O、不看时钟 (时间从 `Snapshot.now` 传入)。这让状态机的全部分支都能用普通 pytest 覆盖, 不需要起库、不需要 sleep。后面几波会不断往状态机里加分支(新故障类型、 新恢复策略、多工位调度),这条边界是唯一能防止它烂掉的东西。 ## 开发形态 vs 生产形态 | 组件 | 开发(`make dev`) | 生产(`docker-compose`) | 换的时候动哪 | |---|---|---|---| | 数据库 | SQLite (aiosqlite) | PostgreSQL 16 | 只改 `DATABASE_URL` | | 队列/总线 | 进程内 asyncio | Redis / NATS | `events/bus.py` 换实现 | | 对象存储 | `var/objects/` 本地目录 | MinIO | `storage.py` 换实现 | | Agent | 内置模拟执行器 + 独立 Agent 心跳 | 客户主机上的 systemd/Windows 服务 | 补齐 Step lease/runtime | | 带外 | 模拟控制器 | 树莓派 / JetKVM / 智能 PDU | `oob/` 换实现 | 三处"换实现"都在接口后面,且开发态实现本身就是测试替身——不存在"生产才发现跑不通"。 ## 为什么不用 Temporal / Airflow 见 [ADR-0003](decisions/0003-own-state-machine-over-temporal.md)。一句话: 恢复语义(带外断电、冻结现场、误盘防护)是本产品的差异化本身, 不能交给通用工作流引擎的重试模型。等到多工位跨节点调度成为瓶颈时再评估 Temporal, `runtime.py` 是唯一需要替换的文件。