# ADR-0004：安全门禁放在控制平面，Agent 保持"笨"

- 日期：2026-07-27
- 状态：已接受

## 背景

危险动作有三类：刷固件、Format/Sanitize、断电。任何一次误盘 = PoC 判定失败
（方案 §9.3：0 次错盘、0 次系统盘破坏）。

门禁可以放在 Agent（离设备近，判断准）或控制平面（离决策近，可审计）。

## 决定

**全部放控制平面**，且拒绝发生在**下发之前**。Agent 只执行已批准的命令模板 +
schema 校验过的参数，自己不做任何"这条命令安不安全"的判断。

Agent 侧保留的唯一防线是 `precheck`（设备在不在、路径对不对），
它是**执行前自检**，不是安全决策。

## 理由

- **可审计**：审批记录、模板版本、操作者、参数必须与决策在同一个地方，
  否则审计链断在 Agent 上。客户安全评审第一个问的就是这个。
- **Agent 可信度低**：Agent 跑在客户的测试主机上，那台机器会蓝屏、会被人手动改配置、
  会装乱七八糟的工具。安全判断不能依赖一个随时可能处于异常状态的环境。
- **升级路径**：安全规则改了，改控制平面就全网生效。要靠 Agent 判断，
  就得推送 Agent 升级到每台客户主机——在实验室里这是以周计的事。
- **可测试**：门禁是纯函数（`safety/gates.py`），误盘场景可以在单测里穷举，
  不需要真的接一块盘来试。

## 具体门禁（`safety/gates.py`）

1. **DUT 绑定**：序列号 + 设备路径 + `allow_destructive` 标签三信号一致才放行。
   任一不匹配 → 拒绝，记 `SAFETY_REJECTED` 事件。
2. **系统盘保护**：解析目标设备的挂载点/启动标志/分区表，命中系统盘立即拒绝。
   这一条**没有** override 开关，代码里不给绕过的口子。
3. **命令模板白名单**：危险命令只能来自 `safety/templates.py` 注册表，
   参数受 JSON Schema 约束。不接受自由文本命令行。
4. **环境指纹一致性**：A/B 对比的两次运行环境指纹不一致 → 结论标记为不可比。

## 代价

- 控制平面必须掌握足够的设备信息才能判断（依赖 Agent 上报的设备探针数据）。
  上报延迟 → 判断基于稍旧的数据。缓解：危险步骤执行前强制刷新一次探针，
  Agent 的 `precheck` 再做一次现场核对，两者不一致直接拒绝。
- 多一次往返。危险动作本来就不该快。

## 什么时候推翻

不推翻。可以增强（加人工审批门、加双人复核），但不下放到 Agent。
