91 lines
4.6 KiB
Markdown
91 lines
4.6 KiB
Markdown
# 控制平面 ↔ Host Agent 协议
|
||
|
||
> **当前状态(2026-07-27)**:独立 Host Agent 已实现注册、注册口令、bearer token、
|
||
> 本地 `0600` 凭据文件、主机指纹和心跳;控制平面只保存 token SHA-256 摘要。
|
||
> 心跳迟到 15 秒会把 Host 降级为 `DEGRADED`,丢失 30 秒会标记为 `OFFLINE`;
|
||
> 安全预检会先刷新该状态,避免失联 Host 被误判为可用。
|
||
> Step 租约、事件、完成与产物端点仍是下一阶段契约。`make demo` 暂时继续使用
|
||
> 控制平面内置模拟执行器验证状态、恢复与证据链。
|
||
|
||
Agent 是**拉模式**。控制平面永远不主动连接测试主机。
|
||
|
||
理由:客户测试主机在内网/NAT 后面,很多实验室根本不给外部反连;
|
||
拉模式还顺带解决了 Agent 重启后的重新接入问题。
|
||
|
||
## 端点
|
||
|
||
| 状态 | 方法 | 路径 | 用途 |
|
||
|---|---|---|---|
|
||
| 已实现 | POST | `/api/v1/agent/register` | 首次注册 / token 轮换;上报主机指纹与工具能力 |
|
||
| 已实现 | POST | `/api/v1/agent/{agent_id}/heartbeat` | bearer 心跳,上报健康和设备探针;返回待办指令 |
|
||
| 已实现 | GET | `/api/v1/agent/hosts` | 查看 Host 与 Agent 在线投影,不返回 token |
|
||
| 待实现 | POST | `/api/v1/agent/{agent_id}/lease` | 领取下一个步骤(长轮询,最多挂 20s) |
|
||
| 待实现 | POST | `/api/v1/agent/{agent_id}/steps/{step_id}/events` | 批量上报执行事件与日志片段 |
|
||
| 待实现 | POST | `/api/v1/agent/{agent_id}/steps/{step_id}/complete` | 上报步骤终态 + UnifiedResult + 产物清单 |
|
||
| 待实现 | POST | `/api/v1/agent/{agent_id}/artifacts` | 上传产物(日志 / 结果文件 / 画面) |
|
||
|
||
带外控制器走独立端点(它必须能在主机死后独立上报):
|
||
|
||
| 方法 | 路径 | 用途 |
|
||
|---|---|---|
|
||
| POST | `/api/v1/oob/{controller_id}/heartbeat` | 带外心跳 + 电源状态 + 温度 + 画面指纹 |
|
||
| POST | `/api/v1/oob/{controller_id}/observation` | 主动上报观测(蓝屏画面、掉电、按钮触发) |
|
||
|
||
## 心跳返回的指令
|
||
|
||
心跳响应已经带 `commands` 字段;当前固定为空数组。命令队列接入后,Agent 收到后立即执行:
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"server_time": "2026-07-27T03:14:15Z",
|
||
"commands": [
|
||
{"kind": "CANCEL_STEP", "step_id": "..."},
|
||
{"kind": "SOFT_RESTART", "reason": "recovery_L1"},
|
||
{"kind": "EMERGENCY_STOP", "run_id": "..."},
|
||
{"kind": "REFRESH_ASSETS"}
|
||
]
|
||
}
|
||
```
|
||
|
||
紧急停止走心跳而不是新连接——心跳是唯一能保证到达的通道。
|
||
最坏情况延迟 = 心跳间隔(5s)。真正需要毫秒级的停止靠带外物理急停,不靠软件。
|
||
|
||
## 租约(lease)语义
|
||
|
||
```
|
||
Agent ──lease──▶ 控制平面
|
||
├─ 有活:分配步骤,step → DISPATCHED,写租约(TTL 60s)
|
||
└─ 没活:长轮询挂起最多 20s,返回 204
|
||
Agent ──ack───▶ step → RUNNING
|
||
(租约到期未 ack 或未 complete)→ step 回 PENDING,重新派发
|
||
```
|
||
|
||
租约 TTL 必须**大于**步骤超时?不——恰恰相反:租约由 Agent 在执行期间通过心跳续期。
|
||
Agent 死了 → 心跳停 → 租约到期 → 步骤回收。这是 Agent 崩溃能被发现的机制之一
|
||
(另一条是心跳超时判定,见 state-machine.md §5)。
|
||
|
||
## 认证
|
||
|
||
首次注册带 `X-FlashOps-Enrollment-Token`。开发态未配置注册口令时允许本机演示;
|
||
生产态未配置时注册接口返回 503。注册成功下发 bearer token,之后每个请求带
|
||
`Authorization: Bearer <token>`。`agent_id` 与 `test_host_id` 绑定,重新注册会轮换 token。
|
||
|
||
原始 token 仅存于 Agent 本机 `0600` state 文件;控制平面数据库只保存 SHA-256 摘要。
|
||
Agent 客户端保留可选 gateway Basic Auth 能力,供未来重新启用边界认证或部署在其他网关后使用;
|
||
当前 `flashops.imagebrewing.com` 预览环境未启用这层认证。
|
||
|
||
**下一波必须升级**:方案 §6.2 要求"每个 Agent 使用唯一证书或密钥"。
|
||
bearer token 在客户内网够用,但要进客户安全评审得上 mTLS。
|
||
升级点在 `api/deps.py::require_agent()` 一处。
|
||
|
||
## 幂等(待实现)
|
||
|
||
Step 租约接入时,所有 Agent → 控制平面的 Step 写请求必须带
|
||
`Idempotency-Key`(Agent 生成的 UUID)。控制平面需要记录并拒绝重复副作用;
|
||
`run_events` 已有唯一约束 `(run_id, idempotency_key)`,但当前注册 / 心跳链路尚未实现
|
||
通用的请求幂等存储。
|
||
|
||
这一条在真实实验室里不是可选项:主机重启、网线松动、Wi-Fi 掉线是常态,
|
||
没有幂等就会出现"一次故障记了三条"的时间线,取证时说不清。
|