2026-08-17 08:34:44 +08:00
|
|
|
|
# CMVR-ES 统一设备安全控制面架构设计
|
|
|
|
|
|
|
|
|
|
|
|
状态:Implemented in software;默认 Shadow rollout;真实硬件与发布验收待完成
|
|
|
|
|
|
|
|
|
|
|
|
修订:2026-08-14,具体认证实现延后,保留统一安全扩展边界并收紧匿名 Recover 暴露
|
|
|
|
|
|
|
|
|
|
|
|
范围:设备命令准入、控制权、StopAll、软件锁止恢复、gRPC 请求上下文与安全扩展边界、命令幂等、设备安全状态上报
|
|
|
|
|
|
|
|
|
|
|
|
实施方式:阶段 0 到阶段 5,共六个阶段
|
|
|
|
|
|
|
|
|
|
|
|
## 当前实施状态(2026-08-14)
|
|
|
|
|
|
|
|
|
|
|
|
当前分支已经完成六阶段所需的软件骨架和主要入口迁移,但没有把“代码已具备能力”误写成
|
|
|
|
|
|
“生产安全验收已完成”。源码默认配置仍使用 `SHADOW`、`INSECURE + DISABLED` 和
|
|
|
|
|
|
`RECOVERY_DISABLED`;切换 `ENFORCE_SELECTED/ENFORCE_ALL` 或开放本机恢复前,必须先完成
|
|
|
|
|
|
对应设备和部署环境的验收。
|
|
|
|
|
|
|
|
|
|
|
|
| 阶段 | 当前状态 | 已落地内容 | 尚未完成 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| 0 | 软件完成 | 统一 RequestContext、完整 method policy/call guard、anonymous principal、显式 Insecure/Disabled 配置、RecoveryExposure 和持久审计边界 | TLS、Token、JWT、mTLS provider 按本轮决策不实现 |
|
|
|
|
|
|
| 1 | 软件完成 | safety types、SnapshotStore、设备 adapter、公共 reason/execution state、service instance、CommandLedger | 厂商结果语义仍需逐台真机校准 |
|
2026-08-17 09:48:42 +08:00
|
|
|
|
| 2 | 软件完成 | DeviceManager 持有 SafetyManager、legacy participant、GetSafetyState、启动 coverage 校验、Shadow/Enforce 配置 | 真实运行 Shadow 日志评审 |
|
2026-08-17 08:34:44 +08:00
|
|
|
|
| 3 | 软件完成 | Arm、ArmTeleop、AGV、Motor、DexHand、PTZ、ActionQueue 统一准入;dispatch 前 permit/final check;PTZ START/STOP 服务端派生 lane | 每类设备真实停车确认和 ACK/断网故障注入 |
|
|
|
|
|
|
| 4 | 软件完成 | 通用 participant StopAll、RecoveryLedger、RecoverSafetyState、LocalOnly/Authorized policy、审计失败 fail-closed、shutdown quiesce | LocalOnly 现场入口和审计文件运维验收 |
|
|
|
|
|
|
| 5 | 部分完成 | Camera/Microphone/Speaker/BioHead/HLC、TouchScreenTask、媒体 Hub/QUIC Sensor start 已迁移;Enforce 启动覆盖校验已实现 | 默认切换 EnforceAll、删除兼容 gate、capability manifest、安装产物 smoke、TSAN 和真机台架 |
|
|
|
|
|
|
|
|
|
|
|
|
认证和证书不是本次整改的运行前提。当前只实现无证书的兼容 Profile;配置选择尚未实现的
|
|
|
|
|
|
认证或 TLS 模式会启动失败,不会静默回退。匿名部署的恢复默认关闭,只有显式配置
|
|
|
|
|
|
`RECOVERY_LOCAL_ONLY`、服务端确认实际 peer 为 loopback/Unix socket 且持久审计可写时才可开放。
|
|
|
|
|
|
|
|
|
|
|
|
AUBO 另有一条设备内硬件语义:真实硬件急停曾有效、随后输入消失且控制器重新报告
|
2026-08-17 12:16:20 +08:00
|
|
|
|
`Normal/ReducedMode` 时,驱动会自动上电到 `Idle`、清理旧队列、执行 `startup()`,并在
|
|
|
|
|
|
`Running` 下再次确认 quiescent 后解除该硬件锁存,使新的 gRPC 指令可以重新准入;
|
2026-08-17 08:34:44 +08:00
|
|
|
|
软件 `emergencyStop()` 使用独立 `SoftwareEmergencyStop` 锁存,即使它与硬件急停重叠也绝不被
|
2026-08-17 12:16:20 +08:00
|
|
|
|
硬件输入释放自动清除。自动流程不 resume、不重放旧目标;显式 Stop/PowerOff 会取消本轮自动上电。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
|
|
|
|
|
|
## 1. 决策摘要
|
|
|
|
|
|
|
|
|
|
|
|
本设计采用以下核心决策:
|
|
|
|
|
|
|
|
|
|
|
|
1. `DeviceManager` 继续负责设备注册和生命周期,并持有一个独立、可测试的
|
2026-08-17 09:48:42 +08:00
|
|
|
|
`SafetyManager`。状态机代码不直接堆入 `DeviceManager`。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
2. Service 不再自行组合 StopAll gate、设备状态和控制权判断。所有会改变设备或
|
|
|
|
|
|
活动状态的命令必须声明 `CommandIntent`,通过统一准入获得短生命周期的
|
|
|
|
|
|
`AdmissionPermit`。
|
|
|
|
|
|
3. 设备层不把厂商硬件规则上移。设备或设备适配器持续发布硬件事实,并在真正下发
|
|
|
|
|
|
命令前执行最后一次硬件安全检查。
|
|
|
|
|
|
4. 生命周期、健康状态和安全状态保持正交。`Running`、`Healthy` 都不等于当前可以
|
|
|
|
|
|
接受执行器命令。
|
|
|
|
|
|
5. 策略分为 `SensorPolicy` 和 `ControlPolicy` 两个默认族,但最终按命令意图判定。
|
|
|
|
|
|
DexHand、Camera PTZ 等混合设备不能只按整个设备归类。
|
|
|
|
|
|
6. Stop、状态查询和安全恢复使用独立安全通道,不受普通命令 gate 阻塞。
|
|
|
|
|
|
7. 对外接口命名为 `RecoverSafetyState`。它只在重新验证硬件事实后清除软件准入锁止,
|
|
|
|
|
|
不能忽略急停、保护停、未知状态或仍未确认的运动。
|
|
|
|
|
|
8. 普通 unary 控制命令使用统一幂等账本;流式控制使用 session epoch 和严格递增的
|
|
|
|
|
|
sequence。任何 `OUTCOME_UNKNOWN` 都不得自动重发。
|
|
|
|
|
|
9. 当前小范围部署不把 TLS、Token、mTLS 等具体认证实现作为设备安全架构的前置条件,
|
|
|
|
|
|
但阶段 0 必须先建立统一 `RequestContext`、认证提供者、授权策略和审计扩展边界。
|
|
|
|
|
|
10. 认证关闭时由服务端注入固定的 `anonymous` principal,不能接受客户端自报身份或角色;
|
|
|
|
|
|
`RecoverSafetyState` 默认禁用,只有本机受限模式可以显式开放。
|
|
|
|
|
|
11. 传输加密、身份认证和方法授权是三个正交层。后续增加静态 Token、JWT 或 mTLS 时,
|
2026-08-17 09:48:42 +08:00
|
|
|
|
只能替换安全网关组件,不修改设备、`SafetyManager` 或业务 Proto。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
12. 该软件控制面不替代独立物理急停,也不声明 SIL、PL 或其他功能安全等级。
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 改造前基础与需要保留的行为
|
|
|
|
|
|
|
|
|
|
|
|
项目已有一些经过并发测试的能力,应当迁移和复用,而不是重新实现:
|
|
|
|
|
|
|
|
|
|
|
|
| 当前能力 | 目标用法 |
|
|
|
|
|
|
| --- | --- |
|
2026-08-17 09:48:42 +08:00
|
|
|
|
| `ControlAuthorityManager` 的 lease、generation、dispatch fence、quarantine | 作为 `SafetyManager` 内部控制权组件 |
|
2026-08-17 08:34:44 +08:00
|
|
|
|
| `StopAllAdmissionGate` 的并发 round 和失败后 fail-closed | 作为阶段 2、3 的兼容参与者,最终由统一状态机接管 |
|
|
|
|
|
|
| `ActionQueueExecutor` 的 `action_id`、service instance 和精确 retired ID 账本 | 作为普通命令幂等账本的语义模板 |
|
|
|
|
|
|
| Motor、Media、Camera activity coordinator | 先包装为 `SafetyParticipant`,最后逐步合并 |
|
|
|
|
|
|
| `StopOperationDispatcher` 的有界异步停止和生命周期管理 | 作为统一 StopAll 执行器的基础 |
|
|
|
|
|
|
| `DeviceManager::inventorySnapshot()` | StopAll 枚举设备时继续使用,禁止持锁进入驱动 |
|
|
|
|
|
|
| `Runtime` 的启动失败回滚和退出时 Task/Device 停止 | 保留,并在退出前增加全局安全准入关闭 |
|
|
|
|
|
|
|
|
|
|
|
|
整改开始时的主要缺口是:
|
|
|
|
|
|
|
|
|
|
|
|
- gRPC 默认全网卡明文监听,没有认证和授权;当前阶段可以接受该部署风险,但必须通过显式配置、
|
|
|
|
|
|
网络隔离和恢复接口限制控制暴露范围;
|
|
|
|
|
|
- 普通控制 RPC 缺少 `command_id` 和统一重试语义;
|
|
|
|
|
|
- StopAll 在 `grpc_system_service.cpp` 内按设备类型硬编码;
|
|
|
|
|
|
- Arm、AGV、Motor、Media、ActionQueue 分别维护 gate 和 generation;
|
|
|
|
|
|
- 只有少数设备实现 `healthSnapshot()`,且健康状态不能表示硬件安全事实;
|
|
|
|
|
|
- `DeviceManager::snapshot()` 仍会在调用线程逐个调用设备方法;
|
|
|
|
|
|
- 普通 service 直接取得设备并调用驱动,无法保证所有入口都经过同一准入;
|
|
|
|
|
|
- Aubo 等驱动存在“硬件可能已接受,但软件未观察到执行 ID”的不确定结果窗口;
|
|
|
|
|
|
- 错误类型没有统一映射,客户端容易对不应重试的命令进行重试。
|
|
|
|
|
|
|
|
|
|
|
|
当前实现锚点:
|
|
|
|
|
|
|
|
|
|
|
|
- gRPC 监听和 credentials:`cmvr-es/task/grpc_server_task/src/grpc_server_task.cpp`;
|
2026-08-17 09:48:42 +08:00
|
|
|
|
- SystemService StopAll:`cmvr-es/service/grpc/server/src/grpc_system_service.cpp`;
|
|
|
|
|
|
- 全局 gate:`cmvr-es/service/grpc/stop_all/include/stop_all_admission_gate.h`;
|
|
|
|
|
|
- 控制权:`cmvr-es/manager/control_authority_manager/include/control_authority_manager.h`;
|
2026-08-17 08:34:44 +08:00
|
|
|
|
- 设备生命周期和快照:`cmvr-es/manager/device_manager/`;
|
|
|
|
|
|
- 通用请求头:`protos/cmvr/api/common.proto`;
|
|
|
|
|
|
- System API:`protos/cmvr/api/system_service.proto` 和 `system_command.proto`。
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 目标、非目标与安全不变量
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 目标
|
|
|
|
|
|
|
|
|
|
|
|
- 所有 gRPC、ActionQueue、Teleoperation 和未来 QUIC 控制入口共享同一安全准入。
|
|
|
|
|
|
- 新增设备时,安全层只要求注册能力和策略,不修改 StopAll 或 SystemService 主流程。
|
|
|
|
|
|
- 单个驱动、网络或状态刷新阻塞不能拖住其他设备的 Stop、Status 或 Recover。
|
|
|
|
|
|
- StopAll、设备级不确定结果和进程重启都能通过 generation 使旧命令永久失效。
|
|
|
|
|
|
- 锁止原因、状态新鲜度、阻塞设备和恢复结果能够被机器读取和审计。
|
|
|
|
|
|
- 迁移期间保持现有 API wire compatibility,并允许按设备逐步启用 enforce。
|
|
|
|
|
|
- 具体认证实现延后时,所有请求仍经过稳定的安全上下文边界;未来启用认证不需要改动
|
|
|
|
|
|
Service 业务分支、状态机、驱动接口或业务 Proto。
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 非目标
|
|
|
|
|
|
|
|
|
|
|
|
- 不把厂商协议、关节限制、工作空间或设备专用故障码搬入 `DeviceManager`。
|
|
|
|
|
|
- 不允许远程接口绕过真实硬件急停或保护停。
|
|
|
|
|
|
- 不保证所有设备在一个阶段内同时完成迁移。
|
|
|
|
|
|
- 不用软件 StopAll 替代机器人、PLC、驱动器或安全控制器的独立安全回路。
|
|
|
|
|
|
- 不在本设计中实现运行期卸载设备插件;只消除安全协调器中的设备类型硬编码。
|
|
|
|
|
|
- 首轮六阶段实施不要求交付证书、静态 Token、JWT 或 mTLS;这些能力作为独立的后续安全加固项。
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3 必须始终成立的不变量
|
|
|
|
|
|
|
|
|
|
|
|
1. `Stop`、`Status`、`GetSafetyState` 和 `RecoverSafetyState` 不依赖普通准入为 OPEN。
|
|
|
|
|
|
2. 一次 StopAll 开始后,旧 epoch 的命令不能再进入设备下发点。
|
|
|
|
|
|
3. StopAll 与正常命令竞态时,命令只能是“未下发”或“已被登记并纳入停止”,不能存在
|
|
|
|
|
|
未登记的第三种状态。
|
|
|
|
|
|
4. 状态过期、设备失联、安全能力缺失对控制命令一律 fail-closed。
|
|
|
|
|
|
5. 同一 effective principal 的同一 `command_id` 最多触发一次硬件提交;认证关闭时所有调用者
|
|
|
|
|
|
共享服务端生成的 `anonymous` principal,因此 command ID 必须在整个匿名部署内唯一。
|
|
|
|
|
|
6. 相同 ID、不同语义 payload 必须返回冲突,不能覆盖旧记录。
|
|
|
|
|
|
7. 硬件结果不确定时保存 `OUTCOME_UNKNOWN`,重试只能查询该结果,不能再次下发。
|
2026-08-17 12:16:20 +08:00
|
|
|
|
8. 通用 `RecoverSafetyState` 成功只表示软件准入可重新评估,不表示设备被上电、使能、
|
|
|
|
|
|
解除急停或自动运动。AUBO 物理急停释放后的自动上电是独立、显式配置的设备内策略。
|
2026-08-17 09:48:42 +08:00
|
|
|
|
9. `SafetyManager` 持有内部锁时不得调用设备、网络或可能阻塞的 participant 方法。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
10. 驱动最终安全检查失败时,即使已经获得 permit,也不能下发设备命令。
|
|
|
|
|
|
11. 进程重启后,控制设备在新鲜状态确认完成前不能自动恢复到可控制状态。
|
|
|
|
|
|
12. 所有安全状态转换都增加 epoch、产生事件,并记录明确 reason code。
|
|
|
|
|
|
13. 匿名网络调用不能执行 `RecoverSafetyState`;认证关闭时只能显式禁用恢复,或仅允许
|
|
|
|
|
|
服务端判定为 loopback/Unix Domain Socket 的本机调用。该限制不能由请求字段覆盖。
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 统一概念模型
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 三个互不替代的状态维度
|
|
|
|
|
|
|
|
|
|
|
|
| 维度 | 说明 | 示例 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| Lifecycle | 进程对设备对象的生命周期管理 | Initializing、Running、Stopped、Error |
|
|
|
|
|
|
| Health | 设备是否正常工作和是否存在故障 | Healthy、Degraded、Fault、Unknown |
|
|
|
|
|
|
| Safety | 当前事实是否足以接受某一类命令 | Nominal、Restricted、Unsafe、Unknown |
|
|
|
|
|
|
|
|
|
|
|
|
`Lifecycle=Running` 只说明设备 worker 已启动;`Health=Healthy` 只说明没有已知故障;
|
|
|
|
|
|
两者都不能替代 Safety 中的状态新鲜度、急停、运动、使能和代际信息。
|
|
|
|
|
|
|
|
|
|
|
|
### 4.2 命令意图
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
enum class CommandIntent {
|
|
|
|
|
|
Observe, // 只读状态、图像、传感数据
|
|
|
|
|
|
StartActivity, // 启动采集、录制、播放等非运动活动
|
|
|
|
|
|
Configure, // 修改参数、地图、非安全 IO 等
|
|
|
|
|
|
Actuate, // 运动、速度、力、位置、使能、扭矩输出
|
|
|
|
|
|
Stop, // 停车、取消、torque-off、停止活动
|
|
|
|
|
|
ResetFault, // 设备级、不会自动产生运动的故障复位
|
|
|
|
|
|
RecoverAdmission, // 系统级软件锁止恢复
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
每个 handler 必须在代码中声明固定意图。客户端不能通过请求字段自行选择意图。
|
|
|
|
|
|
|
|
|
|
|
|
### 4.3 两个默认策略族
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
enum class SafetyPolicyFamily {
|
|
|
|
|
|
Sensor,
|
|
|
|
|
|
Control,
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- `Sensor` 表示非运动型采集或外围活动,不要求它在字面上一定是传感器。Camera、
|
|
|
|
|
|
Microphone、Battery、普通 Speaker activity 可使用该策略。
|
|
|
|
|
|
- `Control` 表示能够导致位置、速度、力、扭矩或机械结构变化的能力。Arm、AGV、Motor、
|
|
|
|
|
|
DexHand 动作、Camera PTZ、BioHead 表情和仿真执行器使用该策略。
|
|
|
|
|
|
- 混合设备按命令选择策略。例如 Camera 图像流是 Sensor,PTZ START 是 Control,
|
|
|
|
|
|
PTZ STOP 是 Stop。
|
|
|
|
|
|
|
|
|
|
|
|
### 4.4 硬件事实使用三值逻辑
|
|
|
|
|
|
|
|
|
|
|
|
安全事实不能把“未实现”解释为 false:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
enum class TriState { Unknown, False, True };
|
|
|
|
|
|
|
|
|
|
|
|
enum class SafetyCondition {
|
|
|
|
|
|
Nominal, // 已知正常,可继续按具体命令规则判断
|
|
|
|
|
|
Restricted, // 已知静止或受硬件抑制,但不满足普通执行条件
|
|
|
|
|
|
Unsafe, // 已观察到不允许继续控制的状态
|
|
|
|
|
|
Unknown, // 缺失、过期、失联或无法解释
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
推荐的进程内快照:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
struct DeviceSafetySnapshot {
|
|
|
|
|
|
std::string device_id;
|
|
|
|
|
|
SafetyCondition condition{SafetyCondition::Unknown};
|
|
|
|
|
|
std::uint64_t device_generation{0};
|
|
|
|
|
|
std::uint64_t sample_sequence{0};
|
|
|
|
|
|
std::chrono::steady_clock::time_point observed_at;
|
|
|
|
|
|
std::uint64_t observed_at_unix_ms{0};
|
|
|
|
|
|
|
|
|
|
|
|
TriState connected{TriState::Unknown};
|
|
|
|
|
|
TriState operational_ready{TriState::Unknown};
|
|
|
|
|
|
TriState quiescent{TriState::Unknown};
|
|
|
|
|
|
TriState motion_active{TriState::Unknown};
|
|
|
|
|
|
TriState actuator_enabled{TriState::Unknown};
|
|
|
|
|
|
TriState emergency_stop_active{TriState::Unknown};
|
|
|
|
|
|
TriState protective_stop_active{TriState::Unknown};
|
|
|
|
|
|
TriState fault_active{TriState::Unknown};
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<SafetyBlocker> blockers;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`SafetyBlocker` 必须包含稳定 reason、来源和作用域:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
enum class BlockerScope { Device, System };
|
|
|
|
|
|
enum class RecoveryRequirement {
|
|
|
|
|
|
RefreshOnly,
|
|
|
|
|
|
ClearSoftwareLatch,
|
|
|
|
|
|
HardwareReleaseRequired,
|
|
|
|
|
|
ManualInspectionRequired,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct SafetyBlocker {
|
|
|
|
|
|
SafetyReason reason;
|
|
|
|
|
|
BlockerScope scope;
|
|
|
|
|
|
RecoveryRequirement recovery_requirement;
|
|
|
|
|
|
std::string source_id;
|
|
|
|
|
|
std::string operation_id;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
设备本地急停可以只阻塞该设备;连接到整机安全链的急停必须发布 `System` scope,并阻止
|
|
|
|
|
|
全局准入恢复。作用域由经过评审的设备/安全链 adapter 固定,不能由客户端或普通配置修改。
|
|
|
|
|
|
|
|
|
|
|
|
安全判断使用本机 monotonic age。UTC 只用于日志和跨系统诊断,不能参与准入时序。
|
|
|
|
|
|
|
|
|
|
|
|
`device_generation` 在以下情况增加:
|
|
|
|
|
|
|
|
|
|
|
|
- 设备后端重新初始化;
|
|
|
|
|
|
- SDK、总线或网络 session 重连;
|
|
|
|
|
|
- 驱动状态机执行需要使旧命令失效的 reset;
|
|
|
|
|
|
- 设备被重新注册。
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 目标架构与所有权
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart LR
|
|
|
|
|
|
Client["gRPC / QUIC / Local Task"] --> Gateway["Request Context Gateway<br/>限制 可选认证 授权策略 审计"]
|
|
|
|
|
|
Gateway --> Adapter["Typed Service Adapter<br/>声明 CommandIntent"]
|
|
|
|
|
|
Adapter --> Ledger["CommandLedger<br/>幂等和结果"]
|
2026-08-17 09:48:42 +08:00
|
|
|
|
Adapter --> Coordinator["DeviceManager::SafetyManager"]
|
2026-08-17 08:34:44 +08:00
|
|
|
|
Coordinator --> Policy["SensorPolicy / ControlPolicy"]
|
|
|
|
|
|
Coordinator --> Authority["ControlAuthorityManager"]
|
|
|
|
|
|
Coordinator --> Participants["SafetyParticipant Registry"]
|
|
|
|
|
|
Coordinator --> Cache["SafetySnapshotStore"]
|
|
|
|
|
|
Coordinator --> Permit["AdmissionPermit"]
|
|
|
|
|
|
Permit --> Driver["DeviceSafetyEndpoint + Driver"]
|
|
|
|
|
|
Driver --> Cache
|
|
|
|
|
|
StopRecover["StopAll / Recover 安全通道"] --> Coordinator
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 建议目录
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
2026-08-17 09:48:42 +08:00
|
|
|
|
cmvr-es/manager/safety_manager/
|
2026-08-17 08:34:44 +08:00
|
|
|
|
include/safety_types.h
|
|
|
|
|
|
include/safety_reason.h
|
|
|
|
|
|
include/device_safety_endpoint.h
|
|
|
|
|
|
include/safety_participant.h
|
|
|
|
|
|
include/safety_snapshot_store.h
|
|
|
|
|
|
include/command_admission_controller.h
|
|
|
|
|
|
include/command_ledger.h
|
|
|
|
|
|
include/safety_operation_orchestrator.h
|
2026-08-17 09:48:42 +08:00
|
|
|
|
include/safety_manager.h
|
2026-08-17 08:34:44 +08:00
|
|
|
|
src/...
|
|
|
|
|
|
tests/...
|
|
|
|
|
|
|
|
|
|
|
|
cmvr-es/service/grpc/security/
|
|
|
|
|
|
include/grpc_request_context.h
|
|
|
|
|
|
include/grpc_authentication_provider.h
|
|
|
|
|
|
include/grpc_authorization_policy.h
|
|
|
|
|
|
include/grpc_method_policy_registry.h
|
|
|
|
|
|
include/grpc_security_interceptor.h
|
|
|
|
|
|
src/...
|
|
|
|
|
|
tests/...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-17 09:48:42 +08:00
|
|
|
|
新增 CMake target:`cmvr_es::safety_manager`。它可以依赖通用类型和
|
2026-08-17 08:34:44 +08:00
|
|
|
|
`ControlAuthorityManager`,但不能依赖 gRPC、具体设备后端或厂商 SDK。
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 `DeviceManager` 的职责变化
|
|
|
|
|
|
|
|
|
|
|
|
`DeviceManager` 增加:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
2026-08-17 09:48:42 +08:00
|
|
|
|
SafetyManager& safetyManager() noexcept;
|
|
|
|
|
|
const SafetyManager& safetyManager() const noexcept;
|
2026-08-17 08:34:44 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
它负责:
|
|
|
|
|
|
|
|
|
|
|
|
- 在设备创建后注册设备描述、Safety endpoint 和快照槽位;
|
|
|
|
|
|
- 在设备销毁前注销 participant;
|
|
|
|
|
|
- 在 lifecycle start/stop/restart 时推进 device generation;
|
|
|
|
|
|
- 在 Runtime shutdown 开始时先关闭全局准入;
|
|
|
|
|
|
- 对外提供纯内存的 Manager + Safety 联合快照。
|
|
|
|
|
|
|
|
|
|
|
|
它不负责:
|
|
|
|
|
|
|
|
|
|
|
|
- 解析 Aubo、Huayan、SEER、Modbus 等厂商状态;
|
|
|
|
|
|
- 根据 `DeviceKind` 写具体停止逻辑;
|
|
|
|
|
|
- 在 Manager mutex 下执行硬件 I/O;
|
|
|
|
|
|
- 判断具体运动目标是否超限。
|
|
|
|
|
|
|
|
|
|
|
|
### 5.3 依赖注入
|
|
|
|
|
|
|
|
|
|
|
|
最终状态下,gRPC service 构造函数显式接收:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
DeviceManager&
|
2026-08-17 09:48:42 +08:00
|
|
|
|
SafetyManager&
|
2026-08-17 08:34:44 +08:00
|
|
|
|
CommandLedger&
|
|
|
|
|
|
GrpcSecurityGateway&
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
每个 handler 通过一个统一 call guard 取得不可变 `RequestContext`。认证 metadata 的解析、
|
|
|
|
|
|
peer 归一化、角色生成和恢复接口暴露检查只存在于 `GrpcSecurityGateway`;业务 Service 不直接
|
|
|
|
|
|
解析 metadata,也不从 request message 读取 principal 或 role。具体实现可以由 interceptor
|
|
|
|
|
|
完成前置检查,再由 call guard 取得同一次调用的上下文,但不能依赖 thread-local 传递上下文。
|
|
|
|
|
|
|
|
|
|
|
|
阶段 2 可以保留无参构造函数作为兼容入口,但它只能转发到上述依赖。阶段 5 删除业务
|
|
|
|
|
|
代码对进程级 safety singleton 的直接访问。测试使用独立 coordinator,不再依赖
|
|
|
|
|
|
`clearForTesting()` 清理全局状态。
|
|
|
|
|
|
|
|
|
|
|
|
### 5.4 管理面与设备运行面分离
|
|
|
|
|
|
|
|
|
|
|
|
“Stop/Status/Recover 始终可达”不仅要求它们绕过普通 gate,也要求单个设备初始化失败时
|
|
|
|
|
|
gRPC 管理面仍能启动。目标 Runtime 启动顺序为:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
load config and logging
|
2026-08-17 09:48:42 +08:00
|
|
|
|
-> construct DeviceManager core and SafetyManager
|
2026-08-17 08:34:44 +08:00
|
|
|
|
-> validate selected gRPC security profile and start management-plane services
|
|
|
|
|
|
-> initialize/start devices
|
|
|
|
|
|
-> reconcile required control snapshots
|
|
|
|
|
|
-> start operational tasks
|
|
|
|
|
|
-> Open with device-level Blocked, or global Latched
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-17 09:48:42 +08:00
|
|
|
|
- 配置损坏、所选安全 Profile 的身份材料无效或 SafetyManager 核心构造失败仍使进程启动失败;
|
2026-08-17 08:34:44 +08:00
|
|
|
|
- `AuthenticationMode::Disabled` 是显式兼容模式,不伪装成“已认证”;如果使用非 loopback
|
|
|
|
|
|
明文监听,启动日志、GetSystemInfo 和指标必须持续暴露该风险;
|
|
|
|
|
|
- 单个设备 create/init/start 失败记录在 inventory,并使相关资源 Blocked;
|
|
|
|
|
|
- Device scope 的动态 Unknown/Unsafe 使相关资源 Blocked,System scope blocker 使全局 Latched,
|
|
|
|
|
|
两者都不停止 SystemService;
|
|
|
|
|
|
- 普通设备 service 可以继续注册,访问失败设备时返回结构化 DEVICE_UNAVAILABLE;
|
|
|
|
|
|
- TaskManager 需要区分 ManagementPlane 和 Operational task,后者失败不能回滚前者;
|
|
|
|
|
|
- 物理急停和本机停机手段始终独立于该网络管理面。
|
|
|
|
|
|
|
|
|
|
|
|
在完成该拆分前,“始终可达”只保证 gRPC server 已成功运行期间的行为,不能覆盖当前
|
|
|
|
|
|
Runtime 在任一 required device 启动失败时直接退出的情况。
|
|
|
|
|
|
|
|
|
|
|
|
## 6. 快照发布与线程模型
|
|
|
|
|
|
|
|
|
|
|
|
### 6.1 发布模型
|
|
|
|
|
|
|
|
|
|
|
|
正常准入和状态查询只能读取 `SafetySnapshotStore`,不能同步查询驱动。
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
driver callback / polling worker
|
|
|
|
|
|
|
|
|
|
|
|
|
v
|
|
|
|
|
|
DeviceSafetyEndpoint normalizes vendor facts
|
|
|
|
|
|
|
|
|
|
|
|
|
v
|
|
|
|
|
|
SafetySnapshotPublisher::publish(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
v
|
|
|
|
|
|
SafetySnapshotStore atomic value slot
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
现有设备可以先通过独立 adapter worker 拉取状态;新设备应优先在自己的 SDK callback 或
|
|
|
|
|
|
状态轮询线程中发布。Manager heartbeat、gRPC Status 和准入只复制值。
|
|
|
|
|
|
|
|
|
|
|
|
现有 `healthSnapshot()` 在迁移期只能由 DeviceMonitor 后台 worker 调用,并使用每设备隔离和
|
|
|
|
|
|
超时;不能继续由 `DeviceManager::snapshot()` 的调用线程执行。目标状态是设备同时发布缓存的
|
|
|
|
|
|
Health 和 Safety,两者仍保持独立字段。
|
|
|
|
|
|
|
|
|
|
|
|
### 6.2 Freshness
|
|
|
|
|
|
|
|
|
|
|
|
每个 `DeviceSafetyDescriptor` 声明默认最大状态年龄。配置只允许将阈值收紧,不能在生产模式下
|
|
|
|
|
|
放宽超过代码定义的硬上限。
|
|
|
|
|
|
|
|
|
|
|
|
- Sensor Observe 可在业务允许时返回带 `stale=true` 的历史值;
|
|
|
|
|
|
- Sensor StartActivity 在 Missing/Stale 时拒绝;
|
|
|
|
|
|
- Control Actuate、Configure 和 ResetFault 在 Missing/Stale 时拒绝;
|
|
|
|
|
|
- Stop 不因状态过期而拒绝,仍直接尝试安全停止;
|
|
|
|
|
|
- Recover 必须获得一个晚于本次 refresh request 的新样本。
|
|
|
|
|
|
|
|
|
|
|
|
### 6.3 主动刷新
|
|
|
|
|
|
|
|
|
|
|
|
`RecoverSafetyState` 需要主动复核,但仍不在 gRPC 线程直接做设备 I/O:
|
|
|
|
|
|
|
|
|
|
|
|
1. 记录当前 `sample_sequence`;
|
|
|
|
|
|
2. 调用 endpoint 的非阻塞 `requestSafetyRefresh()`;
|
|
|
|
|
|
3. 在专用 recovery executor 等待 sequence 增加;
|
|
|
|
|
|
4. 超时则产生 `SAFETY_STATE_STALE` blocker,保持锁止。
|
|
|
|
|
|
|
|
|
|
|
|
### 6.4 锁和执行器约束
|
|
|
|
|
|
|
|
|
|
|
|
- Coordinator 锁只保护状态转换、epoch、slot 和 participant registry;
|
|
|
|
|
|
- Snapshot slot 使用原子 shared snapshot 或短持有独立 mutex;
|
|
|
|
|
|
- `beginDispatch()` 只建立 fence 和 in-flight 计数,不等待物理运动结束;
|
|
|
|
|
|
- 设备调用、停止和刷新均在锁外执行;
|
|
|
|
|
|
- Stop/Recover 使用有界 worker pool,不为每次调用创建 detached thread;
|
|
|
|
|
|
- 每个 legacy health probe 最多允许一个 in-flight worker,卡住后只标记 stale,不重复创建线程;
|
|
|
|
|
|
- participant 注销前必须等待自己的 callback 和 stop handle 退出。
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 状态机
|
|
|
|
|
|
|
|
|
|
|
|
### 7.1 系统准入状态
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
stateDiagram-v2
|
|
|
|
|
|
[*] --> Starting
|
|
|
|
|
|
Starting --> Open: required snapshots reconciled
|
|
|
|
|
|
Starting --> Latched: unknown active control or startup failure
|
|
|
|
|
|
Open --> Stopping: StopAll
|
|
|
|
|
|
Stopping --> Open: all participants quiescent
|
|
|
|
|
|
Stopping --> Latched: timeout or unconfirmed stop
|
|
|
|
|
|
Latched --> Recovering: authorized recovery
|
|
|
|
|
|
Recovering --> Open: every required latch cleared
|
|
|
|
|
|
Recovering --> Latched: blocker remains
|
|
|
|
|
|
Starting --> ShuttingDown: shutdown
|
|
|
|
|
|
Open --> ShuttingDown: shutdown
|
|
|
|
|
|
Stopping --> ShuttingDown: shutdown
|
|
|
|
|
|
Latched --> ShuttingDown: shutdown
|
|
|
|
|
|
Recovering --> ShuttingDown: shutdown
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
每次进入 `Stopping`、`Latched`、`Recovering` 或 `ShuttingDown` 都增加 `safety_epoch`。
|
|
|
|
|
|
|
|
|
|
|
|
### 7.2 设备准入状态
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
enum class DeviceAdmissionState {
|
|
|
|
|
|
Observing, // 已注册,等待第一份有效快照
|
|
|
|
|
|
Open,
|
|
|
|
|
|
Blocked, // 确定的硬件或生命周期条件不满足
|
|
|
|
|
|
Quarantined, // 命令或停止结果不确定,需要人工恢复流程
|
|
|
|
|
|
Recovering,
|
|
|
|
|
|
Removed,
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- `Blocked` 可以在新鲜硬件事实改善后自动回到 Open,例如设备故障被合法清除;
|
|
|
|
|
|
- `Quarantined` 不能仅靠一份健康快照自动清除,必须经过 recovery transaction;
|
|
|
|
|
|
- 一个普通设备命令的结果不确定时,默认只 quarantine 对应 control resource;
|
|
|
|
|
|
- StopAll 无法确认全部参与者时,全局状态进入 Latched。
|
|
|
|
|
|
|
|
|
|
|
|
### 7.3 命令生命周期
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
RECEIVED
|
|
|
|
|
|
-> RESERVED
|
|
|
|
|
|
-> REJECTED_BEFORE_DISPATCH
|
|
|
|
|
|
-> ADMITTED
|
|
|
|
|
|
-> DISPATCHING
|
|
|
|
|
|
-> ACCEPTED_BY_HARDWARE
|
|
|
|
|
|
-> COMPLETED | FAILED | CANCELED | OUTCOME_UNKNOWN
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`ACCEPTED_BY_HARDWARE` 的含义必须由每个 adapter 明确定义。SDK 返回码、queue ID、任务 ID、
|
|
|
|
|
|
ACK 或写入总线分别可能具有不同强度,不能统一解释成“运动完成”。
|
|
|
|
|
|
|
|
|
|
|
|
## 8. 命令准入算法
|
|
|
|
|
|
|
|
|
|
|
|
### 8.1 普通 unary 控制命令
|
|
|
|
|
|
|
|
|
|
|
|
1. Request Context Gateway 完成大小限制、correlation ID、上下文构造和授权策略检查;只有
|
|
|
|
|
|
当前 Profile 启用 AuthN 时才执行身份认证。
|
|
|
|
|
|
2. 解析 `command_id`、service instance、device generation 和本地有效期。
|
|
|
|
|
|
3. CommandLedger 按 effective principal 预留 ID,并计算语义 payload hash;Disabled 模式使用
|
|
|
|
|
|
服务端固定的 `anonymous` namespace。
|
|
|
|
|
|
4. 若存在同 ID 记录:相同 hash 返回/等待原结果;不同 hash 返回冲突。
|
2026-08-17 09:48:42 +08:00
|
|
|
|
5. Service 使用固定 `CommandDescriptor` 调用 `SafetyManager::admit()`。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
6. Coordinator 在短锁内读取 global state、safety epoch、device slot 和 cached snapshot。
|
|
|
|
|
|
7. Policy 判断意图、freshness、硬件事实、设备状态和调用角色。
|
|
|
|
|
|
8. Control intent 获取或校验 `ControlAuthorityManager` lease。
|
|
|
|
|
|
9. 返回 move-only `AdmissionPermit`,其中固定所有 generation 和 deadline。
|
|
|
|
|
|
10. Service 调用 `beginDispatch(permit)`。这里是 StopAll 与命令提交的线性化点。
|
|
|
|
|
|
11. 驱动在自己的串行化上下文中执行最终硬件检查。
|
|
|
|
|
|
12. 通过检查后调用 SDK,并把结果写入 ledger;失败或异常也必须终结 ledger 记录。
|
|
|
|
|
|
|
|
|
|
|
|
`AdmissionPermit` 至少包含:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
struct AdmissionPermit {
|
|
|
|
|
|
std::string command_id;
|
|
|
|
|
|
std::string device_id;
|
|
|
|
|
|
CommandIntent intent;
|
|
|
|
|
|
std::uint64_t safety_epoch;
|
|
|
|
|
|
std::uint64_t device_generation;
|
|
|
|
|
|
std::uint64_t authority_generation;
|
|
|
|
|
|
std::chrono::steady_clock::time_point deadline;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 8.2 Stop 与 ResetFault
|
|
|
|
|
|
|
|
|
|
|
|
- Stop 走安全通道,不需要普通 permit,也不受全局 Latched 阻塞;
|
|
|
|
|
|
- Stop 仍需经过 RequestContext、当前 Profile 的访问策略、参数校验、超时和审计;认证关闭时
|
|
|
|
|
|
不额外拒绝 Stop,因为停止能力必须保持可达;
|
|
|
|
|
|
- Stop 不因 expected service instance、device generation 或 safety epoch 过期而拒绝;这些字段
|
|
|
|
|
|
对 Stop 仅用于诊断,因为安全停止必须优先于防重放限制;
|
|
|
|
|
|
- 重复 Stop 应加入当前 stop operation 或执行幂等停止,不能因为缺少 command ID 而拒绝;
|
|
|
|
|
|
- ResetFault 可在 Latched 下执行,但必须声明为不会上电、使能或恢复运动;
|
|
|
|
|
|
- 如果某厂商的 clear-fault 同时会 resume、enable 或移动,必须拆成两个 typed 操作,
|
|
|
|
|
|
不能把它登记成 ResetFault;
|
|
|
|
|
|
- ResetFault 成功只触发安全快照刷新,不直接清除 central quarantine。
|
|
|
|
|
|
|
2026-08-18 15:03:17 +08:00
|
|
|
|
### 8.2.1 整机运行态恢复
|
|
|
|
|
|
|
|
|
|
|
|
`SystemService.RestoreOperationalState` 是面向上层平台的整机恢复事务,与
|
|
|
|
|
|
`RecoverSafetyState` 的软件准入恢复语义分离:
|
|
|
|
|
|
|
|
|
|
|
|
- 请求必须携带唯一 `recovery_id`、非空 `reason`,并通过与 Recover 相同的
|
|
|
|
|
|
`RecoveryExposure` 和持久审计策略;
|
|
|
|
|
|
- 事务固定覆盖全部已注册设备,并与 StopAll、其他恢复事务通过 process operation mutex
|
|
|
|
|
|
串行;事务期间系统处于 Recovering,拒绝新的普通运动命令;
|
|
|
|
|
|
- 硬件恢复前必须主动取得新鲜快照,确认设备已连接;Control 设备还必须确认物理急停已释放
|
|
|
|
|
|
且已经静止。条件未知、急停仍有效、设备仍运动或审计授权失败时,不调用设备恢复能力;
|
|
|
|
|
|
- 对尚未达到 Nominal/operational-ready 的支持设备调用
|
|
|
|
|
|
`DeviceSafetyEndpoint::restoreOperationalState()`。Legacy Arm 和 AGV adapter 分别调用设备
|
|
|
|
|
|
`clearFault()`;该操作允许按设备实现执行上电、使能和故障复位,但不得恢复旧轨迹、导航、
|
|
|
|
|
|
teleop session、control lease 或 ActionQueue;
|
|
|
|
|
|
- 每个硬件恢复动作完成后必须再次主动刷新,确认连接、新鲜度、急停、保护停、fault、静止和
|
|
|
|
|
|
operational-ready。任一设备失败时系统保持 Latched,并返回逐设备失败原因;中央 quarantine、
|
|
|
|
|
|
retained barrier 和全局 gate 不做部分释放;
|
|
|
|
|
|
- 只有全部必需设备验证成功后,才协调释放软件 latch 和 retained barrier,将系统切回 Open。
|
|
|
|
|
|
调用成功只表示整机已使能、空闲并可接受新的 gRPC 命令,不会续跑中断前的任务。
|
|
|
|
|
|
|
2026-08-17 08:34:44 +08:00
|
|
|
|
### 8.3 流式控制
|
|
|
|
|
|
|
|
|
|
|
|
Teleoperation、Motor cyclic stream 和未来连续控制不为每个 setpoint 写 unary ledger:
|
|
|
|
|
|
|
|
|
|
|
|
1. OPEN frame 生成 `control_session_id`,绑定 service instance、device generation、
|
|
|
|
|
|
safety epoch 和 control lease;
|
|
|
|
|
|
2. setpoint sequence 必须严格递增,且带本地可解释的 `valid_for`;
|
|
|
|
|
|
3. 每个 setpoint 下发前轻量校验 session epoch、permit epoch、watchdog 和快照 freshness;
|
|
|
|
|
|
4. StopAll 或 Recover 增加 epoch 后,旧 stream 立即进入 HOLD/STOP,不可自动续接;
|
|
|
|
|
|
5. reconnect 必须创建新 session,不能恢复旧 ACTIVE 状态;
|
|
|
|
|
|
6. stream 结束、write/read 失败和 watchdog 均执行可验证的安全停止。
|
|
|
|
|
|
|
|
|
|
|
|
### 8.4 ActionQueue
|
|
|
|
|
|
|
|
|
|
|
|
- 保留现有 ActionQueue `action_id` 和 service instance;
|
|
|
|
|
|
- 预校验阶段确认所有 step 已分类,但不提前获取可长期持有的 permit;
|
|
|
|
|
|
- 每个 step 真正开始前重新准入并取得当前 epoch 的 permit;
|
|
|
|
|
|
- step 的内部 command ID 由 `action_id + step_id` 确定性派生,不能要求嵌套请求再提供一个
|
|
|
|
|
|
可与 action 身份冲突的独立 ID;
|
|
|
|
|
|
- StopAll 仍能抢占队列和活动 step;
|
|
|
|
|
|
- 迁移完成后 ActionQueue 不再拥有独立的普通准入 generation,只保留队列执行 generation;
|
|
|
|
|
|
- 队列结果和单步 CommandLedger 记录使用关联 ID,但不得造成一次 step 两套独立重试语义。
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 策略矩阵
|
|
|
|
|
|
|
|
|
|
|
|
### 9.1 SensorPolicy
|
|
|
|
|
|
|
|
|
|
|
|
| Intent | Global Latched | Snapshot 要求 | 默认结果 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| Observe | 允许 | 可返回 stale 标记;不能伪造为 fresh | Allow |
|
|
|
|
|
|
| StartActivity | 拒绝或按设备 Blocked | connected、fresh、无已知 fault | Conditional |
|
|
|
|
|
|
| Configure | 拒绝 | connected、fresh、配置操作已分类 | Conditional |
|
|
|
|
|
|
| Stop | 允许 | 不要求 fresh | Allow safety lane |
|
|
|
|
|
|
| ResetFault | 仅明确支持的设备 | fresh,且操作不会启动活动 | Conditional |
|
|
|
|
|
|
| RecoverAdmission | SafetyAdmin only | fresh sample required | Safety lane |
|
|
|
|
|
|
|
|
|
|
|
|
### 9.2 ControlPolicy
|
|
|
|
|
|
|
|
|
|
|
|
| Intent | 必要条件 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| Observe | 始终允许读取缓存;返回 freshness 和 blocker |
|
|
|
|
|
|
| Configure | 全局 Open、设备非 Quarantined、fresh、命令不会隐式 Actuate |
|
|
|
|
|
|
| Actuate | 全局 Open、设备 Open、condition=Nominal、fresh、generation 匹配、lease 有效、deadline 有效、最终硬件检查通过 |
|
|
|
|
|
|
| Stop | 无条件进入安全通道;状态未知仍尝试 stop |
|
|
|
|
|
|
| ResetFault | 允许在 Blocked/Latched 下执行,但只允许非使能、非运动的 typed reset |
|
|
|
|
|
|
| RecoverAdmission | SafetyAdmin、expected epoch 匹配、主动刷新完成、quiescent=true、没有未知 stop worker |
|
|
|
|
|
|
|
|
|
|
|
|
`emergency_stop_active=true` 或 `protective_stop_active=true` 可以表示设备被硬件抑制且物理上
|
|
|
|
|
|
静止,但不能被 Recover 清除。Device scope blocker 继续阻止该设备 Actuate;System scope
|
|
|
|
|
|
blocker 继续保持全局 Latched。只有硬件被合法处理并发布新的事实后,恢复事务才可清理对应
|
|
|
|
|
|
软件 latch。
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 设备安全能力接口
|
|
|
|
|
|
|
|
|
|
|
|
### 10.1 描述与注册
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
struct DeviceSafetyDescriptor {
|
|
|
|
|
|
std::string device_id;
|
|
|
|
|
|
DeviceKind kind;
|
|
|
|
|
|
SafetyPolicyFamily default_policy;
|
|
|
|
|
|
std::chrono::milliseconds maximum_snapshot_age;
|
|
|
|
|
|
bool requires_safe_stop;
|
|
|
|
|
|
bool supports_active_refresh;
|
|
|
|
|
|
bool supports_non_enabling_fault_reset;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct DeviceSafetyRegistration {
|
|
|
|
|
|
DeviceSafetyDescriptor descriptor;
|
|
|
|
|
|
std::shared_ptr<DeviceSafetyEndpoint> endpoint;
|
|
|
|
|
|
std::shared_ptr<SafetyParticipant> participant;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
控制设备在 enforce 模式下缺少 endpoint、fresh snapshot 或 safe-stop participant 时:
|
|
|
|
|
|
|
|
|
|
|
|
- 启动阶段标记设备 safety capability invalid;
|
|
|
|
|
|
- 设备可以继续出现在 inventory 和诊断接口中;
|
|
|
|
|
|
- 所有 Actuate 命令拒绝;
|
|
|
|
|
|
- 配置要求严格启动时,可以直接使 Runtime 初始化失败。
|
|
|
|
|
|
|
|
|
|
|
|
新增全新 DeviceKind 仍可能需要修改现有 DeviceFactory、配置 Proto 和对外业务 API;本设计
|
2026-08-17 09:48:42 +08:00
|
|
|
|
保证的是不再修改 SafetyManager、StopAll 和 Recover 的设备类型分支。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
|
|
|
|
|
|
### 10.2 Endpoint
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
class DeviceSafetyEndpoint {
|
|
|
|
|
|
public:
|
|
|
|
|
|
virtual ~DeviceSafetyEndpoint() = default;
|
|
|
|
|
|
|
|
|
|
|
|
virtual DeviceSafetyDescriptor descriptor() const = 0;
|
|
|
|
|
|
virtual void bindPublisher(SafetySnapshotPublisher publisher) = 0;
|
|
|
|
|
|
virtual void requestSafetyRefresh() noexcept = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 在驱动自己的串行化上下文内执行。不得更改设备状态。
|
|
|
|
|
|
virtual HardwareCheckResult validateBeforeDispatch(
|
|
|
|
|
|
const AdmissionPermit&) = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 只协调驱动内部软件状态,不得上电、使能或启动运动。
|
|
|
|
|
|
virtual RecoveryCheckResult reconcileAdmissionState(
|
|
|
|
|
|
const RecoveryContext&) = 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
如果最终检查需要厂商 I/O,它必须有设备级 deadline,并在驱动 executor 中执行;Manager
|
|
|
|
|
|
不持锁等待。超时返回 Unknown,不允许继续下发。
|
|
|
|
|
|
|
|
|
|
|
|
### 10.3 Safe-stop participant
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
class SafetyParticipant {
|
|
|
|
|
|
public:
|
|
|
|
|
|
virtual ParticipantDescriptor descriptor() const = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 纯内存、快速关闭本 participant 的新工作准入。
|
|
|
|
|
|
virtual BarrierToken beginBarrier(const SafetyOperationContext&) = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 非阻塞提交停止,返回可等待 handle。
|
|
|
|
|
|
virtual StopHandle requestQuiesce(
|
|
|
|
|
|
const BarrierToken&, const SafetyOperationContext&) = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 验证“旧 generation 不会继续、当前输出已静止”。
|
|
|
|
|
|
virtual QuiescenceResult verifyQuiescent(
|
|
|
|
|
|
const BarrierToken&, const SafetyOperationContext&) = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 只清理软件 gate、retired holder 或内部 session。
|
|
|
|
|
|
virtual RecoveryCheckResult recoverAdmission(
|
|
|
|
|
|
const BarrierToken&, const RecoveryContext&) = 0;
|
|
|
|
|
|
|
|
|
|
|
|
virtual void releaseBarrier(const BarrierToken&) noexcept = 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-08-17 09:48:42 +08:00
|
|
|
|
participant 可以代表设备,也可以代表 ActionQueue、MediaSourceManager、Motor session registry 等
|
2026-08-17 08:34:44 +08:00
|
|
|
|
跨设备活动域。StopAll 不再知道具体 C++ 设备类型。
|
|
|
|
|
|
|
|
|
|
|
|
## 11. CommandLedger 与结果语义
|
|
|
|
|
|
|
|
|
|
|
|
### 11.1 Key 和 payload hash
|
|
|
|
|
|
|
|
|
|
|
|
推荐 key:`(effective_principal_id, command_id)`。
|
|
|
|
|
|
|
|
|
|
|
|
`effective_principal_id` 只能由服务端安全网关产生:认证启用时取认证结果中的稳定 principal ID;
|
|
|
|
|
|
认证关闭时固定为 `anonymous`。不能使用 request、普通 metadata 中自报的 client ID,也不使用
|
|
|
|
|
|
易变化的 TCP source port。匿名模式因此要求 command ID 在整个部署内全局唯一。
|
|
|
|
|
|
|
|
|
|
|
|
payload hash 包含:
|
|
|
|
|
|
|
|
|
|
|
|
- 完整 gRPC method 名;
|
|
|
|
|
|
- device/resource ID;
|
|
|
|
|
|
- deterministic protobuf semantic payload;
|
|
|
|
|
|
- expected service instance 和 device generation;
|
|
|
|
|
|
- 不包含诊断 timestamp、认证 metadata 和 command ID 本身。
|
|
|
|
|
|
|
|
|
|
|
|
同一 effective principal 内 command ID 必须全局唯一。不同 method 复用同一 ID 会因 hash 不同而冲突。
|
|
|
|
|
|
|
|
|
|
|
|
### 11.2 记录内容
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
struct CommandRecord {
|
|
|
|
|
|
CommandKey key;
|
|
|
|
|
|
PayloadHash payload_hash;
|
|
|
|
|
|
CommandLifecycle lifecycle;
|
|
|
|
|
|
CommandOutcome outcome;
|
|
|
|
|
|
std::uint64_t safety_epoch;
|
|
|
|
|
|
std::uint64_t device_generation;
|
|
|
|
|
|
bool hardware_submission_possible;
|
|
|
|
|
|
steady_clock::time_point accepted_at;
|
|
|
|
|
|
steady_clock::time_point terminal_at;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- In-flight 和近期 terminal 结果保留完整响应;
|
|
|
|
|
|
- 淘汰完整结果后保留精确 retired-ID tombstone;
|
|
|
|
|
|
- 达到硬容量后拒绝新 ID,不淘汰仍可能被重放的 tombstone;
|
|
|
|
|
|
- 账本耗尽使用 `RESOURCE_EXHAUSTED/LEDGER_EXHAUSTED`,不能降级成无幂等执行;
|
|
|
|
|
|
- 进程内账本不要求落盘,但客户端必须携带 expected service instance;
|
|
|
|
|
|
- 进程重启后旧 instance 请求拒绝,控制设备先完成 startup reconciliation 才可 Open。
|
|
|
|
|
|
|
|
|
|
|
|
### 11.3 RPC 取消
|
|
|
|
|
|
|
|
|
|
|
|
- 在 RESERVED/ADMITTED 且未 dispatch 时取消:终结为 `CANCELED_BEFORE_DISPATCH`;
|
|
|
|
|
|
- 硬件已接受后客户端断开:不能假设命令取消,继续记录实际结果;
|
|
|
|
|
|
- 需要“断线即停”的命令必须显式使用 session/watchdog 协议;
|
|
|
|
|
|
- retry 相同 ID 只能加入原执行或读取原结果。
|
|
|
|
|
|
|
|
|
|
|
|
## 12. StopAll 统一事务
|
|
|
|
|
|
|
|
|
|
|
|
### 12.1 Participant 分组
|
|
|
|
|
|
|
|
|
|
|
|
建议固定阶段而非依赖注册顺序:
|
|
|
|
|
|
|
|
|
|
|
|
1. `Ingress`:关闭 gRPC/QUIC/local task 普通准入;
|
|
|
|
|
|
2. `Scheduler`:取消 ActionQueue 和待执行作业;
|
|
|
|
|
|
3. `ControlSession`:撤销 teleop、motor cyclic、velocity 等连续控制 session;
|
|
|
|
|
|
4. `Actuator`:Arm、AGV、Motor、DexHand、PTZ、BioHead 请求安全停止;
|
|
|
|
|
|
5. `PeripheralActivity`:Camera、Microphone、Speaker、recording 和媒体 producer;
|
|
|
|
|
|
6. `Verification`:等待每个 required participant 的 quiescence 证明。
|
|
|
|
|
|
|
|
|
|
|
|
组内可以并行,组间顺序固定。每个 participant 有独立 timeout,外层还有总 deadline。
|
|
|
|
|
|
|
|
|
|
|
|
### 12.2 算法
|
|
|
|
|
|
|
|
|
|
|
|
1. 使用 process-wide operation mutex 创建或加入当前 StopAll round;
|
|
|
|
|
|
2. 状态设为 Stopping,增加 safety epoch,关闭全局准入;
|
|
|
|
|
|
3. 对当前 participant registry 建立不可变快照;
|
|
|
|
|
|
4. 对所有 participant 调用 `beginBarrier()`;
|
|
|
|
|
|
5. 撤销普通 control lease,并等待已进入 dispatch fence 的短提交退出;
|
|
|
|
|
|
6. 分阶段提交 `requestQuiesce()`;
|
|
|
|
|
|
7. 等待 handle,并调用 `verifyQuiescent()`;
|
|
|
|
|
|
8. 任一 required participant 超时、异常或 Unknown:保留其 barrier,记录 blocker,进入 Latched;
|
|
|
|
|
|
9. 全部成功:清理旧 retired holders,释放本轮 barrier,状态回到 Open;
|
|
|
|
|
|
10. 返回结构化 per-participant 结果和新 epoch。
|
|
|
|
|
|
|
|
|
|
|
|
多个 StopAll 调用加入同一 round,并各自得到同一最终结果。RPC waiter 取消不能取消已经开始的
|
|
|
|
|
|
系统停止事务。
|
|
|
|
|
|
|
|
|
|
|
|
成功 StopAll 是一个“已经在本轮确认全部静止”的同步屏障,不是持续维护锁。状态回到 Open 后,
|
|
|
|
|
|
其他通过当前访问策略的客户端可以提交新命令,甚至可能在 StopAll 调用方收到响应前完成准入。需要长期禁止
|
|
|
|
|
|
控制时应设计独立的 maintenance lock,不复用 StopAll 或 Recover 语义。
|
|
|
|
|
|
|
|
|
|
|
|
### 12.3 迟到结果
|
|
|
|
|
|
|
|
|
|
|
|
- 旧 round 的 worker 完成后只允许更新该 round 的诊断记录;
|
|
|
|
|
|
- worker 不能凭旧 token 重新打开准入;
|
|
|
|
|
|
- participant 的 release 必须验证 operation ID 和 epoch;
|
|
|
|
|
|
- worker 仍运行时 participant 保持 blocker,Recover 不得跳过它。
|
|
|
|
|
|
|
|
|
|
|
|
## 13. RecoverSafetyState 事务
|
|
|
|
|
|
|
|
|
|
|
|
### 13.1 语义边界
|
|
|
|
|
|
|
|
|
|
|
|
`RecoverSafetyState` 的准确含义是:
|
|
|
|
|
|
|
|
|
|
|
|
> 在独立安全通道中重新确认设备和活动域已经静止,并清除由软件 StopAll、超时、取消、
|
|
|
|
|
|
> 旧 session 或不确定结果留下的准入锁止。
|
|
|
|
|
|
|
|
|
|
|
|
它不执行:
|
|
|
|
|
|
|
|
|
|
|
|
- 解除物理急停;
|
|
|
|
|
|
- 自动解除保护停;
|
|
|
|
|
|
- torque-on、power-on、brake-release;
|
|
|
|
|
|
- 自动继续旧轨迹、导航、ActionQueue 或 teleop session;
|
|
|
|
|
|
- 把 Unknown 解释为 Safe;
|
|
|
|
|
|
- 调用测试接口 `clearForTesting()`。
|
|
|
|
|
|
|
|
|
|
|
|
### 13.2 恢复算法
|
|
|
|
|
|
|
|
|
|
|
|
1. Request Context Gateway 先执行恢复暴露策略:认证模式要求 `SafetyAdmin`;认证关闭时默认
|
|
|
|
|
|
禁用,只有 `LOCAL_ONLY` 且服务端确认 peer 为 loopback/Unix Domain Socket 才允许继续;
|
|
|
|
|
|
随后校验 `recovery_id`、scope、reason 和 deadline;
|
|
|
|
|
|
2. RecoveryLedger 对 `recovery_id` 做幂等处理;
|
|
|
|
|
|
3. 与 StopAll 串行化;expected safety epoch 不匹配立即拒绝;
|
|
|
|
|
|
4. 状态转为 Recovering 并增加 epoch,使所有旧 permit/session 失效;
|
|
|
|
|
|
5. 选取 scope 内处于 Blocked/Quarantined 的 device 和 subsystem participant;
|
|
|
|
|
|
6. 确认没有旧 stop worker、dispatch fence 或 active command 仍未退出;
|
|
|
|
|
|
7. 对设备发起 active refresh,等待晚于本次请求的新 snapshot;
|
|
|
|
|
|
8. 控制设备要求 `quiescent=true`,且 condition 不能是 Unsafe/Unknown;任何 System scope
|
|
|
|
|
|
hardware blocker 都保持全局锁止;
|
|
|
|
|
|
9. 调用 endpoint/participant 的非使能 `reconcileAdmissionState()` 和 `recoverAdmission()`;
|
|
|
|
|
|
10. 清理已经验证的 quarantine、retired safety holder 和软件 gate;
|
|
|
|
|
|
11. scope 外 blocker 或失败 participant 继续保留;只有全部 required latch 清除才回到 Open;
|
|
|
|
|
|
12. 返回 previous/new epoch、每个对象的 before/after 状态和 blocker。
|
|
|
|
|
|
|
|
|
|
|
|
### 13.3 部分恢复
|
|
|
|
|
|
|
|
|
|
|
|
- 请求可以只恢复指定 device,但不能偷偷排除 subsystem blocker;
|
|
|
|
|
|
- 设备级 quarantine 可以单独清除;
|
|
|
|
|
|
- 如果 global gate 仍被其他 participant 持有,系统状态仍为 Latched;
|
|
|
|
|
|
- response 必须区分 `RECOVERED`、`VERIFIED_BUT_STILL_BLOCKED`、`BLOCKER_REMAINS`、
|
|
|
|
|
|
`EPOCH_MISMATCH` 和 `NOTHING_TO_RECOVER`。
|
|
|
|
|
|
|
|
|
|
|
|
## 14. gRPC API 设计
|
|
|
|
|
|
|
|
|
|
|
|
### 14.1 扩展公共命令头
|
|
|
|
|
|
|
|
|
|
|
|
保持字段 1、2 不变,使用新 tag 增量扩展:
|
|
|
|
|
|
|
|
|
|
|
|
```protobuf
|
|
|
|
|
|
message CommandHeader {
|
|
|
|
|
|
message Request {
|
|
|
|
|
|
string device_id = 1;
|
|
|
|
|
|
google.protobuf.Timestamp timestamp = 2; // 仅诊断
|
|
|
|
|
|
string command_id = 3;
|
|
|
|
|
|
string expected_service_instance_id = 4;
|
|
|
|
|
|
optional uint64 expected_device_generation = 5;
|
|
|
|
|
|
uint32 valid_for_ms = 6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message Feedback {
|
|
|
|
|
|
bool success = 1;
|
|
|
|
|
|
string error_message = 2;
|
|
|
|
|
|
google.protobuf.Timestamp timestamp = 3;
|
|
|
|
|
|
CommandReasonCode reason_code = 4;
|
|
|
|
|
|
string command_id = 5;
|
|
|
|
|
|
string service_instance_id = 6;
|
|
|
|
|
|
uint64 safety_epoch = 7;
|
|
|
|
|
|
uint64 device_generation = 8;
|
|
|
|
|
|
CommandExecutionState execution_state = 9;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
兼容阶段中旧客户端字段为空:
|
|
|
|
|
|
|
|
|
|
|
|
- Legacy/Shadow 模式允许,但记录 `missing_command_identity`;
|
|
|
|
|
|
- EnforceSelected 只对已迁移设备要求;
|
|
|
|
|
|
- EnforceAll 下所有普通 mutating unary 命令必须提供;
|
|
|
|
|
|
- Observe 不要求 command ID;安全 Stop 为保持可达性不把 ID/epoch 作为前置条件,但有 ID 时
|
|
|
|
|
|
用于合并结果和审计;Recover 必须提供 recovery ID 和 expected safety epoch。
|
|
|
|
|
|
|
|
|
|
|
|
`valid_for_ms` 从服务端收到请求的 monotonic time 开始计算,不能信任跨机器 timestamp。
|
|
|
|
|
|
Safety Stop 忽略已经过期的普通命令有效期,但仍受服务端 stop operation 总 deadline 约束。
|
|
|
|
|
|
|
|
|
|
|
|
- `command_id` 建议使用 UUID,服务端至少限制字符集和最大长度;
|
|
|
|
|
|
- `valid_for_ms=0` 在 Legacy/Shadow 下使用有界服务端默认值,不能表示无限;
|
|
|
|
|
|
- EnforceSelected/EnforceAll 可以要求 Actuate 显式提供非零有效期;
|
|
|
|
|
|
- 服务端对所有客户端有效期施加硬上限,retry 不能延长原 ledger 记录的 deadline;
|
|
|
|
|
|
- expected service instance 为空只在兼容模式接受。
|
|
|
|
|
|
|
|
|
|
|
|
### 14.2 新增安全 API
|
|
|
|
|
|
|
|
|
|
|
|
建议新增 `protos/cmvr/api/safety_command.proto`,并在现有 `SystemService` 增加:
|
|
|
|
|
|
|
|
|
|
|
|
```protobuf
|
|
|
|
|
|
rpc GetSafetyState(GetSafetyStateCommand.Request)
|
|
|
|
|
|
returns (GetSafetyStateCommand.Feedback);
|
|
|
|
|
|
|
|
|
|
|
|
rpc RecoverSafetyState(RecoverSafetyStateCommand.Request)
|
|
|
|
|
|
returns (RecoverSafetyStateCommand.Feedback);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
建议核心消息:
|
|
|
|
|
|
|
|
|
|
|
|
```protobuf
|
|
|
|
|
|
message DeviceIdList {
|
|
|
|
|
|
repeated string device_ids = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message SafetyScope {
|
|
|
|
|
|
oneof target {
|
|
|
|
|
|
bool all_devices = 1; // 必须显式为 true
|
|
|
|
|
|
DeviceIdList devices = 2; // 必须非空且无重复
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message RecoverSafetyStateCommand {
|
|
|
|
|
|
enum Mode {
|
|
|
|
|
|
MODE_UNSPECIFIED = 0;
|
|
|
|
|
|
VERIFY_ONLY = 1;
|
|
|
|
|
|
CLEAR_SOFTWARE_LATCH = 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message Request {
|
|
|
|
|
|
string recovery_id = 1;
|
|
|
|
|
|
SafetyScope scope = 2;
|
|
|
|
|
|
uint64 expected_safety_epoch = 3;
|
|
|
|
|
|
Mode mode = 4;
|
|
|
|
|
|
string reason = 5;
|
|
|
|
|
|
uint32 timeout_ms = 6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message Feedback {
|
|
|
|
|
|
CommandHeader.Feedback header = 1;
|
|
|
|
|
|
RecoveryResult result = 2;
|
|
|
|
|
|
uint64 previous_safety_epoch = 3;
|
|
|
|
|
|
uint64 current_safety_epoch = 4;
|
|
|
|
|
|
SystemAdmissionState system_state = 5;
|
|
|
|
|
|
repeated RecoveryTargetResult targets = 6;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
不要增加 `force=true` 或 `ignore_hardware_state=true`。
|
|
|
|
|
|
|
|
|
|
|
|
### 14.3 GetSafetyState
|
|
|
|
|
|
|
|
|
|
|
|
至少返回:
|
|
|
|
|
|
|
|
|
|
|
|
- global admission state、safety epoch、control service instance ID;
|
|
|
|
|
|
- device lifecycle、health、device admission state;
|
|
|
|
|
|
- SafetyCondition、snapshot fresh、sample age、device generation;
|
|
|
|
|
|
- active control owner 的脱敏标识;
|
|
|
|
|
|
- blocker reason code、首次发生时间、最后更新时间、来源 operation/command ID;
|
|
|
|
|
|
- 当前 StopAll/Recover operation 的阶段和 deadline;
|
|
|
|
|
|
- subsystem participant 状态。
|
|
|
|
|
|
|
|
|
|
|
|
该接口只读内存,即使设备失联或 driver worker 卡住也必须及时返回。
|
|
|
|
|
|
|
|
|
|
|
|
### 14.4 gRPC status 与业务结果
|
|
|
|
|
|
|
|
|
|
|
|
- 没有产生有效业务结果时使用非 OK status:认证失败、权限不足、格式错误、服务关闭;
|
|
|
|
|
|
- command ID 已预留后,执行结果使用 `grpc::Status::OK + CommandOutcome`,便于相同 ID
|
|
|
|
|
|
重放完整结果;
|
|
|
|
|
|
- Recover 的部分失败也返回 OK 和 per-target result;
|
|
|
|
|
|
- `INTERNAL` 只表示代码异常或不变量破坏;
|
|
|
|
|
|
- 兼容旧服务时继续填充 `success/error_message`,客户端应迁移到 reason code。
|
|
|
|
|
|
|
|
|
|
|
|
## 15. 稳定错误模型
|
|
|
|
|
|
|
|
|
|
|
|
建议公共 reason code 至少包括:
|
|
|
|
|
|
|
|
|
|
|
|
| Reason | gRPC 映射 | 是否可用同 ID 重试 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| INVALID_ARGUMENT | INVALID_ARGUMENT | 否,修正后使用新 ID |
|
|
|
|
|
|
| UNAUTHENTICATED | UNAUTHENTICATED | 认证后重新请求 |
|
|
|
|
|
|
| PERMISSION_DENIED | PERMISSION_DENIED | 否 |
|
|
|
|
|
|
| RECOVERY_RPC_DISABLED | FAILED_PRECONDITION | 是;未进入 RecoveryLedger,本机改配置并重启后可重试 |
|
|
|
|
|
|
| DEVICE_NOT_FOUND | NOT_FOUND | 否 |
|
|
|
|
|
|
| UNSUPPORTED_COMMAND | UNIMPLEMENTED | 否 |
|
|
|
|
|
|
| SYSTEM_STOPPING | ABORTED | 原 ID 查询,不重新下发 |
|
|
|
|
|
|
| SAFETY_LATCHED | FAILED_PRECONDITION | 恢复后新 ID |
|
|
|
|
|
|
| SAFETY_STATE_STALE | UNAVAILABLE | 状态刷新后新 ID |
|
|
|
|
|
|
| HARDWARE_UNSAFE | FAILED_PRECONDITION | 处理硬件后新 ID |
|
|
|
|
|
|
| EMERGENCY_STOP_ACTIVE | FAILED_PRECONDITION | 物理处理后新 ID |
|
|
|
|
|
|
| PROTECTIVE_STOP_ACTIVE | FAILED_PRECONDITION | 合法复位后新 ID |
|
|
|
|
|
|
| DEVICE_DISCONNECTED | UNAVAILABLE | 重连并校验 generation 后新 ID |
|
|
|
|
|
|
| CONTROL_BUSY | RESOURCE_EXHAUSTED | 释放控制权后新 ID |
|
|
|
|
|
|
| GENERATION_MISMATCH | ABORTED | 刷新状态后新 ID |
|
|
|
|
|
|
| COMMAND_ID_CONFLICT | ALREADY_EXISTS | 否 |
|
|
|
|
|
|
| LEDGER_EXHAUSTED | RESOURCE_EXHAUSTED | 稍后提交新 ID |
|
|
|
|
|
|
| BACKPRESSURE | RESOURCE_EXHAUSTED | 仅确认未接收后使用新 ID |
|
|
|
|
|
|
| DEADLINE_EXCEEDED_BEFORE_DISPATCH | DEADLINE_EXCEEDED | 新 ID |
|
|
|
|
|
|
| OUTCOME_UNKNOWN | ABORTED | 不得自动重下发 |
|
|
|
|
|
|
| INTERNAL_ERROR | INTERNAL | 不得自动重下发运动 |
|
|
|
|
|
|
|
|
|
|
|
|
response 可以附带 retry directive,但 `retryable=true` 不能用于 `OUTCOME_UNKNOWN`。
|
|
|
|
|
|
|
|
|
|
|
|
## 16. gRPC 请求上下文与安全扩展设计
|
|
|
|
|
|
|
|
|
|
|
|
### 16.1 延后认证的边界
|
|
|
|
|
|
|
|
|
|
|
|
本轮设备安全改造允许不实现 TLS、静态 Token、JWT 和 mTLS,但不能把“暂不认证”等同于
|
|
|
|
|
|
“不设计认证边界”。现在必须固定以下三层接口,后续安全加固只能替换实现:
|
|
|
|
|
|
|
|
|
|
|
|
| 层 | 当前小范围部署 | 后续可选实现 | 业务层是否感知 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| Transport security | Insecure,可受限到 loopback/隔离网 | Server TLS、mTLS、Unix Domain Socket | 否 |
|
|
|
|
|
|
| Authentication | Disabled,服务端生成 `anonymous` | Static Token、JWT/OIDC、TLS client certificate、Unix peer credential | 否 |
|
|
|
|
|
|
| Authorization | Compatibility policy + Recover exposure policy | 基于 role/capability 的 policy | 只接收允许/拒绝结果 |
|
|
|
|
|
|
|
|
|
|
|
|
这种调整只表示认证交付可以延后,不表示明文匿名网络具备安全性。使用非 loopback 明文监听时,
|
|
|
|
|
|
安全依赖部署网络、主机防火墙和物理访问控制;该风险必须在配置、启动日志、SystemInfo 和指标中
|
|
|
|
|
|
保持可见。
|
|
|
|
|
|
|
|
|
|
|
|
认证信息继续使用 gRPC metadata 或 transport auth context,不进入业务 request Proto。这样不会
|
|
|
|
|
|
污染设备 API,也避免以后为了增加 Token 给所有命令消息增加字段。
|
|
|
|
|
|
|
|
|
|
|
|
### 16.2 `RequestContext` 与扩展接口
|
|
|
|
|
|
|
|
|
|
|
|
建议定义与设备层无关的不可变上下文:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
enum class AuthenticationMethod {
|
|
|
|
|
|
Disabled,
|
|
|
|
|
|
StaticToken,
|
|
|
|
|
|
Jwt,
|
|
|
|
|
|
TlsClientCertificate,
|
|
|
|
|
|
UnixPeerCredential,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct Principal {
|
|
|
|
|
|
std::string id; // 由服务端生成
|
|
|
|
|
|
AuthenticationMethod method;
|
|
|
|
|
|
bool authenticated;
|
|
|
|
|
|
std::vector<std::string> roles; // Disabled 模式只能是 Anonymous
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct RequestContext {
|
|
|
|
|
|
std::string correlation_id;
|
|
|
|
|
|
std::string full_method_name;
|
|
|
|
|
|
std::string peer;
|
|
|
|
|
|
Principal principal;
|
|
|
|
|
|
bool transport_encrypted;
|
|
|
|
|
|
bool local_peer;
|
|
|
|
|
|
std::chrono::steady_clock::time_point received_at;
|
|
|
|
|
|
std::chrono::steady_clock::time_point deadline;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
核心扩展接口:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
class GrpcAuthenticationProvider {
|
|
|
|
|
|
public:
|
|
|
|
|
|
virtual ~GrpcAuthenticationProvider() = default;
|
|
|
|
|
|
virtual AuthenticationResult authenticate(const GrpcCallFacts&) = 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
class GrpcAuthorizationPolicy {
|
|
|
|
|
|
public:
|
|
|
|
|
|
virtual ~GrpcAuthorizationPolicy() = default;
|
|
|
|
|
|
virtual AuthorizationDecision authorize(
|
|
|
|
|
|
const RequestContext&, const GrpcMethodPolicy&) const = 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
class GrpcSecurityGateway {
|
|
|
|
|
|
public:
|
|
|
|
|
|
virtual GrpcCallGuard beginCall(
|
|
|
|
|
|
grpc::ServerContext&, const GrpcMethodPolicy&) = 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
当前提供 `DisabledAuthenticationProvider`:它不读取客户端自报身份,始终产生
|
|
|
|
|
|
`{id="anonymous", authenticated=false, roles=[Anonymous]}`。同时提供可注入的 fake provider,
|
|
|
|
|
|
用于证明未来切换认证实现时不需要修改 handler。
|
|
|
|
|
|
|
|
|
|
|
|
每个 gRPC handler 在入口取得 `GrpcCallGuard`,之后只使用其中的 `RequestContext`。call guard
|
|
|
|
|
|
负责上下文生命周期、统一拒绝状态和审计结束事件。可以用 server interceptor 完成全局前置检查,
|
|
|
|
|
|
但不能依赖 thread-local 在 interceptor 和 handler 之间传递身份;同步、异步和 callback RPC 都必须
|
|
|
|
|
|
具有明确的 per-call 所有权。
|
|
|
|
|
|
|
2026-08-17 09:48:42 +08:00
|
|
|
|
`SafetyManager` 不依赖 gRPC 类型。Service 只把从 `RequestContext` 派生的稳定
|
2026-08-17 08:34:44 +08:00
|
|
|
|
`CommandActor`/capability 传给准入和 ledger;驱动层完全不可见认证方式。
|
|
|
|
|
|
|
|
|
|
|
|
### 16.3 部署 Profile
|
|
|
|
|
|
|
|
|
|
|
|
建议提供以下验证 Profile。Profile 是一组配置约束,不是散落在 Service 中的条件分支:
|
|
|
|
|
|
|
|
|
|
|
|
| Profile | 监听与传输 | AuthN | Recover | 适用范围 |
|
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
|
| `LOCAL_COMPATIBILITY` | loopback 或 Unix Domain Socket,可明文 | Disabled | 默认 Disabled,可显式 LocalOnly | 单机开发和维护 |
|
|
|
|
|
|
| `TRUSTED_NETWORK_COMPATIBILITY` | 显式受控网卡,可明文 | Disabled | Disabled | 当前隔离的小范围运行 |
|
|
|
|
|
|
| `LIGHTWEIGHT_AUTHENTICATED` | Server TLS | Static Token | SafetyAdmin | 客户端无需证书的轻量方案 |
|
|
|
|
|
|
| `PRODUCTION_AUTHENTICATED` | Server TLS 或 mTLS | JWT/Token/client certificate | SafetyAdmin | 后续正式部署 |
|
|
|
|
|
|
|
|
|
|
|
|
本轮只要求实现前两个 Compatibility Profile 和后两个 Profile 的配置校验占位;选择尚未编译的
|
|
|
|
|
|
认证 provider 必须启动失败,不能静默退回 Disabled。
|
|
|
|
|
|
|
|
|
|
|
|
约束如下:
|
|
|
|
|
|
|
|
|
|
|
|
- Disabled 必须是显式模式,不能因为证书或 Token 文件加载失败而自动进入;
|
|
|
|
|
|
- 非 loopback 的 Insecure + Disabled 必须额外配置 `allow_insecure_non_loopback=true`,并产生
|
|
|
|
|
|
高可见度持续告警;
|
|
|
|
|
|
- Profile 只能通过本机配置和进程重启改变,不提供远程降级接口;
|
|
|
|
|
|
- QUIC heartbeat 和 GetSystemInfo 发布实际 transport/authentication/recovery exposure,不能
|
|
|
|
|
|
根据配置意图伪报;
|
|
|
|
|
|
- 后续 Static Token 客户端通过统一 client interceptor 添加 `authorization: Bearer ...`,
|
|
|
|
|
|
业务调用点不变化;
|
|
|
|
|
|
- 启用了 TLS 时才校验证书、私钥、CA、有效期和文件权限。
|
|
|
|
|
|
|
|
|
|
|
|
### 16.4 授权与 Recover 暴露策略
|
|
|
|
|
|
|
|
|
|
|
|
方法权限仍预先分类,作为未来认证启用后的稳定契约:
|
|
|
|
|
|
|
|
|
|
|
|
| Role | 权限 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| Anonymous | Compatibility Profile 中除 Recover 外的现有兼容行为 |
|
|
|
|
|
|
| Observer | GetSystemInfo、GetDeviceList、GetSafetyState、只读状态和传感流 |
|
|
|
|
|
|
| Operator | Observer + 普通控制 + 设备 Stop + StopAll |
|
|
|
|
|
|
| SafetyAdmin | Operator + RecoverSafetyState + 安全配置诊断 |
|
|
|
|
|
|
|
|
|
|
|
|
`GrpcMethodPolicyRegistry` 至少保存完整 method 名、read/mutate/stop/recover 分类、
|
|
|
|
|
|
`CommandIntent` 和最低 role。未知方法在 Authenticated Profile 中 fail-closed;Compatibility
|
|
|
|
|
|
Profile 可以只为已有 RPC 保留当前行为,但仍必须产生 `unclassified_method` 告警并在阶段 2 前清零。
|
|
|
|
|
|
|
2026-08-18 15:03:17 +08:00
|
|
|
|
`RecoverSafetyState` 和 `RestoreOperationalState` 额外使用独立的
|
|
|
|
|
|
`RecoveryExposure`:
|
2026-08-17 08:34:44 +08:00
|
|
|
|
|
|
|
|
|
|
| RecoveryExposure | 行为 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| `DISABLED` | 返回稳定的 `RECOVERY_RPC_DISABLED`,不进入 RecoveryLedger |
|
|
|
|
|
|
| `LOCAL_ONLY` | 仅接受服务端从实际 peer 判定的 loopback/Unix Domain Socket 调用 |
|
|
|
|
|
|
| `AUTHORIZED` | 要求 authenticated principal 且具有 SafetyAdmin |
|
|
|
|
|
|
|
|
|
|
|
|
校验规则:
|
|
|
|
|
|
|
|
|
|
|
|
- Authentication Disabled + Recovery Authorized 是非法配置;
|
|
|
|
|
|
- `TRUSTED_NETWORK_COMPATIBILITY` 不能配置 LocalOnly 后再信任代理转发的 IP/header;只有 gRPC
|
|
|
|
|
|
连接的实际 peer 可以用于本机判定;
|
|
|
|
|
|
- LocalOnly 是部署范围限制,不宣称调用者身份已经认证;优先使用 Unix Domain Socket,
|
|
|
|
|
|
平台支持时再校验 UID/GID;
|
|
|
|
|
|
- TCP loopback 的 LocalOnly 等价于信任主机上的所有进程。多用户主机、共享容器宿主机或存在
|
|
|
|
|
|
不可信本地进程时必须保持 Disabled,或改用具有文件权限/peer credential 的 Unix Domain Socket;
|
|
|
|
|
|
- request 中的 role、principal、`force=true` 或类似字段一律不能改变该策略;
|
|
|
|
|
|
- Stop/StopAll 始终走独立安全通道,不受普通 safety gate 或审计 sink 故障阻塞;Authenticated
|
|
|
|
|
|
Profile 仍执行其访问策略,独立物理急停不能依赖网络认证服务。
|
|
|
|
|
|
|
|
|
|
|
|
### 16.5 调用链与状态码
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
request limits -> correlation ID -> selected authentication provider
|
|
|
|
|
|
-> immutable RequestContext -> method/recovery authorization policy
|
|
|
|
|
|
-> audit begin -> service handler -> audit outcome
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- Disabled provider 成功结果仍标记 `authenticated=false`,不能伪造为 Observer/Operator;
|
|
|
|
|
|
- 认证信息无效返回 `UNAUTHENTICATED`,身份有效但权限不足返回 `PERMISSION_DENIED`;
|
|
|
|
|
|
- Recover 被部署配置关闭返回业务 reason `RECOVERY_RPC_DISABLED`;
|
|
|
|
|
|
- LocalOnly 收到非本机 peer 返回 `PERMISSION_DENIED`;
|
|
|
|
|
|
- reflection 独立配置。Authenticated Profile 默认关闭或仅向 SafetyAdmin 开放;Compatibility
|
|
|
|
|
|
Profile 保持显式开关,不能依靠 reflection 状态表示访问安全。
|
|
|
|
|
|
|
|
|
|
|
|
### 16.6 审计字段与失败策略
|
|
|
|
|
|
|
|
|
|
|
|
- effective principal ID、authentication method、authenticated、roles、peer;
|
|
|
|
|
|
- transport encrypted、security profile、recovery exposure;
|
|
|
|
|
|
- gRPC method、device/resource、intent;
|
|
|
|
|
|
- correlation ID、command/recovery/stop operation ID;
|
|
|
|
|
|
- payload hash,不记录 Token、Authorization metadata、原始音视频和敏感大 payload;
|
|
|
|
|
|
- 准入结果、reason code、safety/device/authority generation;
|
|
|
|
|
|
- 硬件提交状态、终态、耗时;
|
|
|
|
|
|
- Recover reason、before/after blocker;
|
|
|
|
|
|
- 审计写入失败的处理策略。
|
|
|
|
|
|
|
|
|
|
|
|
阶段 0 可以先把统一审计事件接入现有日志 sink,但事件 schema 必须稳定。阶段 4 开放任何形式的
|
|
|
|
|
|
Recover 前必须具备持久本地审计;Recover 审计无法写入时 fail-closed。Stop/StopAll 不能因审计
|
|
|
|
|
|
sink 不可用而被拒绝,实现应保留本地应急日志并继续停止。普通控制是否因审计失败而拒绝由
|
|
|
|
|
|
Profile 决定。
|
|
|
|
|
|
|
|
|
|
|
|
### 16.7 后续启用认证的变更面
|
|
|
|
|
|
|
|
|
|
|
|
阶段 0 边界完成后,从 Disabled 升级到 Static Token/JWT/mTLS 只允许修改或新增:
|
|
|
|
|
|
|
|
|
|
|
|
- `GrpcServerTask` 的 credential/provider builder;
|
|
|
|
|
|
- `GrpcAuthenticationProvider` 实现、secret/identity 配置加载和角色映射;
|
|
|
|
|
|
- 客户端统一 metadata interceptor 或 channel credential;
|
|
|
|
|
|
- 对应 Profile 的集成测试、密钥轮换和部署 Runbook。
|
|
|
|
|
|
|
|
|
|
|
|
以下内容不应因认证升级而修改:
|
|
|
|
|
|
|
|
|
|
|
|
- 设备命令 request/feedback Proto;
|
2026-08-17 09:48:42 +08:00
|
|
|
|
- `DeviceManager`、`SafetyManager`、Sensor/Control policy;
|
2026-08-17 08:34:44 +08:00
|
|
|
|
- `DeviceSafetyEndpoint`、`SafetyParticipant` 和厂商驱动;
|
|
|
|
|
|
- handler 内的命令准入、StopAll 或 Recover 业务分支。
|
|
|
|
|
|
|
|
|
|
|
|
安全 Profile 只能在重启时切换。重启会生成新的 service instance ID,进程内 anonymous ledger
|
|
|
|
|
|
自然失效;不做运行期 `anonymous -> authenticated principal` 账本迁移,也不允许认证加载失败时
|
|
|
|
|
|
保留旧监听并降级运行。
|
|
|
|
|
|
|
|
|
|
|
|
## 17. 配置设计
|
|
|
|
|
|
|
2026-08-17 09:48:42 +08:00
|
|
|
|
### 17.1 SafetyManagerConfig
|
2026-08-17 08:34:44 +08:00
|
|
|
|
|
|
|
|
|
|
建议在 `DeviceManagerConfig` 中增加:
|
|
|
|
|
|
|
|
|
|
|
|
```protobuf
|
2026-08-17 09:48:42 +08:00
|
|
|
|
message SafetyManagerConfig {
|
2026-08-17 08:34:44 +08:00
|
|
|
|
enum EnforcementMode {
|
|
|
|
|
|
ENFORCEMENT_MODE_UNSPECIFIED = 0;
|
|
|
|
|
|
LEGACY = 1;
|
|
|
|
|
|
SHADOW = 2;
|
|
|
|
|
|
ENFORCE_SELECTED = 3;
|
|
|
|
|
|
ENFORCE_ALL = 4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EnforcementMode mode = 1;
|
|
|
|
|
|
repeated string enforced_device_ids = 2;
|
|
|
|
|
|
uint32 stop_all_timeout_ms = 3;
|
|
|
|
|
|
uint32 recovery_timeout_ms = 4;
|
|
|
|
|
|
uint32 command_ledger_result_capacity = 5;
|
|
|
|
|
|
uint32 command_ledger_total_id_capacity = 6;
|
|
|
|
|
|
uint32 event_history_capacity = 7;
|
|
|
|
|
|
bool fail_startup_on_missing_control_capability = 8;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
阶段 1 到阶段 4 允许旧配置缺失并进入 Legacy/Shadow,同时产生高可见度告警。阶段 5 的
|
|
|
|
|
|
Production 配置若仍为 UNSPECIFIED 或 LEGACY,启动失败。
|
|
|
|
|
|
|
|
|
|
|
|
### 17.2 设备级覆盖
|
|
|
|
|
|
|
|
|
|
|
|
设备 entry 可增加:
|
|
|
|
|
|
|
|
|
|
|
|
- 是否纳入当前 enforce rollout;
|
|
|
|
|
|
- 更短的 snapshot freshness;
|
|
|
|
|
|
- 更短的 stop/recovery timeout;
|
|
|
|
|
|
- 是否为启动所必需。
|
|
|
|
|
|
|
|
|
|
|
|
配置不能:
|
|
|
|
|
|
|
|
|
|
|
|
- 把 Control endpoint 改成 Sensor;
|
|
|
|
|
|
- 把 required safe-stop 改成 optional;
|
|
|
|
|
|
- 允许 Unknown 通过;
|
|
|
|
|
|
- 关闭驱动最终硬件检查;
|
|
|
|
|
|
- 远程修改 enforce 为 legacy。
|
|
|
|
|
|
|
|
|
|
|
|
策略族和 capability 由编译后的 adapter 注册,配置只能收紧。
|
|
|
|
|
|
|
|
|
|
|
|
该启动失败开关只针对结构性缺陷,例如控制设备没有 endpoint 或 safe-stop capability。运行时
|
|
|
|
|
|
设备断线、急停或动态 Unknown 应进入 Blocked/Latched,并保持管理面在线。
|
|
|
|
|
|
|
|
|
|
|
|
### 17.3 GRPCSecurityConfig
|
|
|
|
|
|
|
|
|
|
|
|
建议在现有 `GRPCServerConfig` 中增加独立安全配置。Transport 和 Authentication 不合并成一个
|
|
|
|
|
|
布尔值,避免以后只能通过客户端证书获得加密连接:
|
|
|
|
|
|
|
|
|
|
|
|
```protobuf
|
|
|
|
|
|
message GRPCSecurityConfig {
|
|
|
|
|
|
enum TransportMode {
|
|
|
|
|
|
TRANSPORT_MODE_UNSPECIFIED = 0;
|
|
|
|
|
|
INSECURE = 1;
|
|
|
|
|
|
SERVER_TLS = 2;
|
|
|
|
|
|
MUTUAL_TLS = 3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum AuthenticationMode {
|
|
|
|
|
|
AUTHENTICATION_MODE_UNSPECIFIED = 0;
|
|
|
|
|
|
DISABLED = 1;
|
|
|
|
|
|
STATIC_TOKEN = 2;
|
|
|
|
|
|
JWT = 3;
|
|
|
|
|
|
TLS_CLIENT_CERTIFICATE = 4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum RecoveryExposure {
|
|
|
|
|
|
RECOVERY_EXPOSURE_UNSPECIFIED = 0;
|
|
|
|
|
|
RECOVERY_DISABLED = 1;
|
|
|
|
|
|
RECOVERY_LOCAL_ONLY = 2;
|
|
|
|
|
|
RECOVERY_AUTHORIZED = 3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TransportMode transport_mode = 1;
|
|
|
|
|
|
AuthenticationMode authentication_mode = 2;
|
|
|
|
|
|
RecoveryExposure recovery_exposure = 3;
|
|
|
|
|
|
bool allow_insecure_non_loopback = 4;
|
|
|
|
|
|
bool enable_reflection = 5;
|
|
|
|
|
|
|
|
|
|
|
|
string server_certificate_file = 6;
|
|
|
|
|
|
string server_private_key_file = 7;
|
|
|
|
|
|
string client_ca_file = 8;
|
|
|
|
|
|
string static_token_file = 9;
|
|
|
|
|
|
string jwt_issuer = 10;
|
|
|
|
|
|
string jwt_audience = 11;
|
|
|
|
|
|
string audit_file = 12;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
阶段 0 只实现 `INSECURE + DISABLED`,以及 `RECOVERY_DISABLED/RECOVERY_LOCAL_ONLY` 的策略;
|
|
|
|
|
|
其他枚举值先形成稳定配置契约,选择未编译能力时返回明确启动错误。旧配置缺失该 message 时可有
|
|
|
|
|
|
一个发布周期映射到当前行为,但必须告警;迁移窗口结束后 UNSPECIFIED 一律启动失败。
|
|
|
|
|
|
|
|
|
|
|
|
后续 provider 实现后的组合约束:
|
|
|
|
|
|
|
|
|
|
|
|
| Transport | Authentication | 是否允许 | 说明 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| Insecure/Unix socket | Disabled | 是 | Compatibility;按监听范围和 RecoveryExposure 限制 |
|
|
|
|
|
|
| Insecure TCP | StaticToken/JWT | 否 | 凭据可被监听和重放 |
|
|
|
|
|
|
| Server TLS | Disabled | 是 | 只加密,不识别客户端,仍属于 Compatibility |
|
|
|
|
|
|
| Server TLS | StaticToken/JWT | 是 | 推荐的轻量远程认证 |
|
|
|
|
|
|
| Mutual TLS | TLSClientCertificate | 是 | 后续强身份 Profile |
|
|
|
|
|
|
| Mutual TLS | Disabled | 否 | 要求客户端证书却丢弃身份没有明确语义 |
|
|
|
|
|
|
|
|
|
|
|
|
首轮不实现多因素组合;如果未来需要 mTLS + Token,必须定义唯一 principal、角色合并和审计规则,
|
|
|
|
|
|
不能简单拼接两个 provider 的结果。
|
|
|
|
|
|
|
|
|
|
|
|
配置只保存凭据文件路径,不保存 Token 明文。后续实现静态 Token 时使用权限受限的独立文件,
|
|
|
|
|
|
日志和审计只能记录 token ID,不能记录 secret。所有组合在启动时集中校验,并把实际生效值写入
|
|
|
|
|
|
capability manifest/SystemInfo。
|
|
|
|
|
|
|
|
|
|
|
|
## 18. 当前设备迁移映射
|
|
|
|
|
|
|
|
|
|
|
|
| 设备/入口 | 策略 | 关键安全事实 | Stop/恢复要点 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
2026-08-17 12:16:20 +08:00
|
|
|
|
| Aubo Arm | Control | connected、robot mode、exec/queue、power、硬件/软件 EStop、protective stop、fault | 硬件 EStop 释放后在 Normal/Reduced 下自动 poweron/startup,Running 且队列清空、quiescent 后才重新准入;显式 Stop/PowerOff 优先;软件 EStop 独立锁存;无法确认 exec 时 OutcomeUnknown |
|
2026-08-17 08:34:44 +08:00
|
|
|
|
| Huayan Arm | Control | lifecycle generation、motion state、fault、stop confirmation | 保留已强化的 fail-closed 生命周期,映射为统一 endpoint |
|
|
|
|
|
|
| MotorRobotArm | Control | group atomicity、joint freshness、bus generation | 不具备原子 group servo 时继续拒绝 teleop capability |
|
|
|
|
|
|
| UME RobotArm | Control | CAN session、watchdog、torque enable、feedback freshness | reconnect 不恢复 torque;本地 haptic loop 不做网络调用 |
|
|
|
|
|
|
| ArmTeleopService | Control(stream) | session ID、sequence、watchdog、lease、safety epoch | OPEN 时准入;每帧校验 epoch;StopAll 后必须新 session |
|
|
|
|
|
|
| SEER AGV | Control | controller session、navigation terminal、velocity、EStop、fault | cancel ACK 不等于停稳;连续零速度采样后才 quiescent |
|
|
|
|
|
|
| MotorManager/Motor | Control | bus session epoch、CiA402 state、enabled、quick-stop、actual velocity | 每个 motor resource 注册;Quick Stop 未确认则 quarantine |
|
|
|
|
|
|
| DexHand control | Control | hand lifecycle、command generation、actuator idle | tactile stream 与控制命令分开分类;stopOperationalActivity 必须可证明 |
|
|
|
|
|
|
| DexHand tactile | Sensor | polling worker、sample freshness | StopAll 可停 stream,但不能把 stream 状态当作手部运动状态 |
|
2026-08-17 09:48:42 +08:00
|
|
|
|
| Camera capture | Sensor | opened、streaming、worker generation | 复用 MediaSourceManager;gRPC/QUIC 和直接 startStreaming 在启动设备 producer 前取得 Sensor/StartActivity dispatch guard |
|
2026-08-17 08:34:44 +08:00
|
|
|
|
| Camera PTZ | Control | PTZ activity generation、stop ACK | 服务端按已校验 action 派生 START=Actuate、STOP=安全通道,锁止时 STOP 仍可下发 |
|
|
|
|
|
|
| Microphone | Sensor | capture lifecycle、sample freshness | activity participant |
|
|
|
|
|
|
| Speaker | Sensor | playback lifecycle、worker generation | Stop 始终允许;不视为机械执行器 |
|
|
|
|
|
|
| BioHead | Control | expression/speech activity、hardware fault | 表情可能产生机械运动,不能归入纯媒体 |
|
|
|
|
|
|
| Battery | Sensor | sample freshness、communication state | 只读;不参与 actuator StopAll |
|
|
|
|
|
|
| MuJoCo control | Control | simulation generation、active command | 使用与真机相同策略,便于故障注入,但不能代替真机验收 |
|
|
|
|
|
|
| HLC/touch | Control | 服务端解析真实 arm ID、任务 safety session、每次 arm submission 复核 | `touch` 为 Actuate;每次 move/speed/servo 前依次复核 permit、control authority、Coordinator dispatch 和设备最终检查;撤销后走 Stop lane |
|
|
|
|
|
|
| QUIC media | Sensor | Camera/Microphone source ID、snapshot freshness、media generation | 不提供执行器控制;全局 Hub 的设备 producer 启动使用与 gRPC 相同的 Sensor/StartActivity dispatch guard |
|
|
|
|
|
|
|
|
|
|
|
|
Aubo JSON `get_di/get_do` 可以归类 Observe;`set_do` 必须归类 Configure/Control,并最终迁移
|
|
|
|
|
|
为 typed RPC。未知 JSON command 在 Control 设备上默认拒绝。
|
|
|
|
|
|
|
|
|
|
|
|
## 19. 六阶段实施方案
|
|
|
|
|
|
|
|
|
|
|
|
| 阶段 | 核心产物 | 是否改变生产准入 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 0 | RequestContext、AuthN/AuthZ/Audit 扩展边界 | 否,显式保持现有兼容行为 |
|
|
|
|
|
|
| 1 | 类型、快照、错误、幂等契约 | 否,旧 gate 仍权威 |
|
2026-08-17 09:48:42 +08:00
|
|
|
|
| 2 | SafetyManager shadow | 否,只比较决策 |
|
2026-08-17 08:34:44 +08:00
|
|
|
|
| 3 | 高风险设备逐个 enforce | 仅改变选中设备 |
|
|
|
|
|
|
| 4 | 泛化 StopAll、正式 Recover | 改变系统安全事务 |
|
|
|
|
|
|
| 5 | EnforceAll、真机签字、移除旧路径 | 全量切换 |
|
|
|
|
|
|
|
|
|
|
|
|
每个阶段只有满足退出条件后才能进入下一阶段;不能为了尽快提供 Recover 而跳过阶段 0、1、
|
|
|
|
|
|
2 或高风险设备迁移。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段 0:建立控制面安全扩展边界
|
|
|
|
|
|
|
|
|
|
|
|
#### 目标
|
|
|
|
|
|
|
|
|
|
|
|
在不增加现有客户端证书或 Token 配置负担的前提下,建立统一请求上下文和可替换的安全网关,
|
|
|
|
|
|
使以后增加认证只替换 provider/policy,不横向修改所有 Service。当前明文匿名暴露被显式记录,
|
|
|
|
|
|
但本阶段不改变已有普通 RPC 的允许/拒绝行为。
|
|
|
|
|
|
|
|
|
|
|
|
#### 实施项
|
|
|
|
|
|
|
|
|
|
|
|
1. 扩展 `grpc_server_config.proto`,增加 Transport、Authentication、RecoveryExposure 和
|
|
|
|
|
|
`allow_insecure_non_loopback`;本阶段只实现 Insecure + Disabled。
|
|
|
|
|
|
2. 定义不可变 `RequestContext`、`Principal`、与 gRPC 无关的 `CommandActor`,以及明确的
|
|
|
|
|
|
effective principal 规则。
|
|
|
|
|
|
3. 实现 `DisabledAuthenticationProvider`、`GrpcAuthorizationPolicy`、
|
|
|
|
|
|
`GrpcSecurityGateway/GrpcCallGuard` 和可注入 fake provider。
|
|
|
|
|
|
4. 建立完整 method 名驱动的 `GrpcMethodPolicyRegistry`,先固定 read/mutate/stop/recover、
|
|
|
|
|
|
最低未来 role;CommandIntent 最迟在阶段 2 补齐。
|
|
|
|
|
|
5. 所有现有 handler 统一通过 call guard 取得上下文;Service 不直接读取认证 metadata,
|
|
|
|
|
|
不从 request 读取 principal/role。
|
|
|
|
|
|
6. Compatibility policy 保持现有 RPC 行为。`RecoverSafetyState` 标记为独立 Recovery policy,
|
|
|
|
|
|
此阶段只预留,不注册实现。
|
|
|
|
|
|
7. 增加 correlation ID 和统一审计事件 schema,先接入现有日志 sink;保留 error logging
|
|
|
|
|
|
interceptor,并明确多个 interceptor/call guard 的生命周期顺序。
|
|
|
|
|
|
8. GrpcServerTask 集中校验配置组合;选择 StaticToken/JWT/mTLS 等未实现 provider 时启动失败,
|
|
|
|
|
|
不允许静默退回 Disabled。
|
|
|
|
|
|
9. QUIC heartbeat、GetSystemInfo、启动日志和指标发布实际 transport、authentication 和
|
|
|
|
|
|
recovery exposure;非 loopback 明文匿名模式持续告警。
|
|
|
|
|
|
10. 为旧配置提供一个发布周期的兼容映射,并更新部署文档和显式示例配置。
|
|
|
|
|
|
|
|
|
|
|
|
#### 测试
|
|
|
|
|
|
|
|
|
|
|
|
- Disabled provider 忽略伪造的 principal/role metadata,结果始终是未认证 `anonymous`;
|
|
|
|
|
|
- 现有客户端不增加 metadata 仍可调用已有 RPC,允许/拒绝结果不变;
|
|
|
|
|
|
- IPv4/IPv6 loopback、Unix Domain Socket 和非本机 peer 分类;
|
|
|
|
|
|
- Disabled + Authorized Recovery、未实现 provider、非 loopback insecure 未显式确认等非法组合;
|
|
|
|
|
|
- fake authenticated provider 下 Observer/Operator/SafetyAdmin 的 method policy;
|
|
|
|
|
|
- sync、stream、callback、deadline/cancellation 下 RequestContext 生命周期和清理;
|
|
|
|
|
|
- correlation ID、审计字段脱敏、audit sink 异常和 interceptor 顺序;
|
|
|
|
|
|
- 旧配置迁移及实际 security capability 上报。
|
|
|
|
|
|
|
|
|
|
|
|
#### 退出条件
|
|
|
|
|
|
|
|
|
|
|
|
- 每个现有 gRPC method 都经过统一 gateway,并有 access class;
|
|
|
|
|
|
- `anonymous` 只能由服务端生成,伪造 metadata 不会获得 role;
|
|
|
|
|
|
- 当前兼容模式行为未改变,但明文/匿名/监听范围在配置和运行状态中可见;
|
|
|
|
|
|
- 非本机匿名 Recover 没有可执行路径;
|
|
|
|
|
|
- fake provider 测试证明启用认证不需要修改业务 handler、Coordinator 或驱动;
|
|
|
|
|
|
- 部署文档、风险说明和显式示例配置已更新。
|
|
|
|
|
|
|
|
|
|
|
|
#### 回滚边界
|
|
|
|
|
|
|
|
|
|
|
|
可以让 Compatibility policy 继续保持旧行为,但 RequestContext、method registry 和配置字段
|
|
|
|
|
|
不能删除;否则会重新引入后续横向改造。任何安全 Profile 变化只能通过本机配置和进程重启,
|
|
|
|
|
|
不能提供远程降级 RPC。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段 1:统一类型、快照、错误与幂等契约
|
|
|
|
|
|
|
|
|
|
|
|
#### 目标
|
|
|
|
|
|
|
|
|
|
|
|
建立后续状态机所需的数据契约,但不改变现有命令准入结果。
|
|
|
|
|
|
|
|
|
|
|
|
#### 实施项
|
|
|
|
|
|
|
2026-08-17 09:48:42 +08:00
|
|
|
|
1. 新建 `manager/safety_manager` target 和 `safety_types.h`。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
2. 定义 CommandIntent、SafetyCondition、TriState、SafetyBlocker、SafetySnapshot。
|
|
|
|
|
|
3. 定义 DeviceSafetyDescriptor、Endpoint、Participant 接口。
|
|
|
|
|
|
4. 在 DeviceManager 中建立 `SafetySnapshotStore`,Manager snapshot 只读缓存。
|
|
|
|
|
|
5. 为现有设备建立 adapter;未支持设备发布 Unknown,不伪造安全。
|
|
|
|
|
|
6. 扩展 `CommandHeader` 和 Feedback;新增 reason code 和 execution state。
|
|
|
|
|
|
7. 生成统一 control service instance ID,并通过 GetSystemInfo 暴露。
|
|
|
|
|
|
8. 实现进程内 CommandLedger,复用 ActionQueue 的容量和 retired-ID 设计原则。
|
|
|
|
|
|
9. 建立 vendor result 到公共 reason code 的映射层。
|
|
|
|
|
|
10. 给 AbstractDevice 的宽松默认生命周期能力增加弃用标记;Control 注册要求显式能力。
|
|
|
|
|
|
11. 更新 Manager/Service/Proto 文档中已经过时的生命周期说明。
|
|
|
|
|
|
|
|
|
|
|
|
#### 测试
|
|
|
|
|
|
|
|
|
|
|
|
- snapshot 并发发布/读取、stale 计算、generation 单调性;
|
|
|
|
|
|
- endpoint 未实现、设备重连、设备重新注册;
|
|
|
|
|
|
- ledger 同 ID 同 payload、不同 payload、in-flight join、淘汰和容量耗尽;
|
|
|
|
|
|
- deterministic payload hash;
|
|
|
|
|
|
- reason code 映射完整性;
|
|
|
|
|
|
- protobuf 旧 client payload 解析和旧配置解析。
|
|
|
|
|
|
|
|
|
|
|
|
#### 退出条件
|
|
|
|
|
|
|
|
|
|
|
|
- gRPC 状态查询和 QUIC heartbeat 不再调用设备方法;
|
|
|
|
|
|
- 所有 Control 类型至少有显式 Unknown adapter,不再依赖默认 Healthy;
|
|
|
|
|
|
- 新字段保持 wire compatible;
|
|
|
|
|
|
- ledger 的 OutcomeUnknown 测试证明不会二次 dispatch;
|
|
|
|
|
|
- 此阶段生产行为仍由旧 gate 决定。
|
|
|
|
|
|
|
|
|
|
|
|
#### 回滚边界
|
|
|
|
|
|
|
|
|
|
|
|
新 Proto 字段不可删除或复用;可以停止使用新字段,但必须保留 wire schema。SnapshotStore
|
|
|
|
|
|
可以退回仅诊断模式,不影响旧 gate。
|
|
|
|
|
|
|
2026-08-17 09:48:42 +08:00
|
|
|
|
### 阶段 2:SafetyManager 影子运行
|
2026-08-17 08:34:44 +08:00
|
|
|
|
|
|
|
|
|
|
#### 目标
|
|
|
|
|
|
|
|
|
|
|
|
在不改变线上允许/拒绝结果的情况下,对全部命令计算新策略结果并验证分类完整性。
|
|
|
|
|
|
|
|
|
|
|
|
#### 实施项
|
|
|
|
|
|
|
2026-08-17 09:48:42 +08:00
|
|
|
|
1. DeviceManager 构造并持有 SafetyManager。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
2. 在阶段 0 的 GrpcMethodPolicyRegistry 中补齐 CommandIntent,并引入 typed CommandDescriptor。
|
|
|
|
|
|
3. gRPC service 增加可注入构造函数;GrpcServerTask 统一传入 coordinator/ledger,沿用既有
|
|
|
|
|
|
GrpcSecurityGateway。
|
|
|
|
|
|
4. 每个 handler 在旧 gate 前后调用 shadow evaluation,记录旧/新决策差异。
|
|
|
|
|
|
5. 把 StopAll gate、Motor、Media、Camera registry、ActionQueue 包装成 legacy participant。
|
|
|
|
|
|
6. 新增只读 `GetSafetyState`,先发布 shadow decision、freshness 和 blocker。
|
|
|
|
|
|
7. 增加 admission latency、decision mismatch、unknown snapshot、unclassified method 指标。
|
|
|
|
|
|
8. 启动状态使用 Starting;只做诊断,不因 shadow 结果阻止旧业务。
|
|
|
|
|
|
9. 将 Runtime/TaskManager 划分为 management-plane 和 operational 启动组;设备动态故障不再
|
|
|
|
|
|
使已经通过所选安全 Profile 配置校验的 SystemService 一并退出。
|
|
|
|
|
|
|
|
|
|
|
|
#### 影子比较分类
|
|
|
|
|
|
|
|
|
|
|
|
| 旧结果 | 新结果 | 处理 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| Allow | Allow | 正常 |
|
|
|
|
|
|
| Deny | Deny | 正常,比较 reason |
|
|
|
|
|
|
| Allow | Deny | 记录 `would_deny`,优先修正快照或旧行为 |
|
|
|
|
|
|
| Deny | Allow | 高风险 `would_allow`,在进入阶段 3 前必须归零或有书面解释 |
|
|
|
|
|
|
|
|
|
|
|
|
#### 测试
|
|
|
|
|
|
|
|
|
|
|
|
- 所有 protobuf service method 都在 method access policy 和 command-intent registry 中;
|
|
|
|
|
|
- shadow evaluation 无硬件 I/O;
|
|
|
|
|
|
- coordinator 销毁时 participant 全部注销且无 callback;
|
|
|
|
|
|
- StopAll 与 shadow admit 并发不改变旧 gate 行为;
|
|
|
|
|
|
- GetSafetyState 在设备 endpoint 阻塞时仍快速返回。
|
|
|
|
|
|
- 单个设备 create/init/start 失败时,GetSafetyState 仍可按当前安全 Profile 访问。
|
|
|
|
|
|
|
|
|
|
|
|
#### 退出条件
|
|
|
|
|
|
|
|
|
|
|
|
- 所有 mutating method 均有固定 intent;
|
|
|
|
|
|
- 所有 Control 设备都有 DeviceSafetyDescriptor;
|
|
|
|
|
|
- 没有未解释的 `legacy deny / new allow`;
|
|
|
|
|
|
- Shadow 准入 p99 只包含内存操作,不受硬件 RTT 影响;
|
|
|
|
|
|
- 至少完成一轮真实运行日志评审。
|
|
|
|
|
|
- 管理面 degraded-start 和正常 shutdown 路径都有生命周期测试。
|
|
|
|
|
|
|
|
|
|
|
|
#### 回滚边界
|
|
|
|
|
|
|
|
|
|
|
|
可关闭 shadow evaluation,但保留 snapshot 和 method registry。旧 gate 仍是唯一 authority。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段 3:迁移高风险控制设备
|
|
|
|
|
|
|
|
|
|
|
|
#### 目标
|
|
|
|
|
|
|
|
|
|
|
|
让 Arm、Teleoperation、AGV、Motor、DexHand control 和 Camera PTZ 的普通命令由
|
2026-08-17 09:48:42 +08:00
|
|
|
|
SafetyManager 权威准入,并统一幂等与执行结果语义。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
|
|
|
|
|
|
#### 迁移顺序
|
|
|
|
|
|
|
|
|
|
|
|
1. Aubo/Huayan unary Arm;
|
|
|
|
|
|
2. ArmTeleopService 和 UME session;
|
|
|
|
|
|
3. AGV navigation/velocity;
|
|
|
|
|
|
4. Motor unary 和 cyclic stream;
|
|
|
|
|
|
5. DexHand control;
|
|
|
|
|
|
6. Camera PTZ;
|
|
|
|
|
|
7. ActionQueue step dispatch。
|
|
|
|
|
|
|
|
|
|
|
|
#### 单设备迁移步骤
|
|
|
|
|
|
|
|
|
|
|
|
1. 完成 endpoint 和新鲜 snapshot;
|
|
|
|
|
|
2. 明确每个 SDK 返回值的 Accepted/Completed/Rejected/Unknown 语义;
|
|
|
|
|
|
3. 实现 safe-stop request 和 quiescence verification;
|
|
|
|
|
|
4. handler 先通过 Coordinator,再保留 legacy gate 作为第二道 deny-only adapter;
|
|
|
|
|
|
5. dispatch 前执行 permit revalidation 和 driver final check;
|
|
|
|
|
|
6. unary mutating 命令接入 CommandLedger;
|
|
|
|
|
|
7. 流式命令绑定 safety epoch、device generation 和 lease generation;
|
|
|
|
|
|
8. 开启该设备 `ENFORCE_SELECTED`;
|
|
|
|
|
|
9. 通过 fake、并发和真机测试后,删除该设备 handler 中重复的旧状态判断。
|
|
|
|
|
|
|
|
|
|
|
|
#### 驱动执行契约
|
|
|
|
|
|
|
|
|
|
|
|
- Aubo queue full 只有在 SDK 能证明命令未接收时才能返回 Backpressure;
|
|
|
|
|
|
- 未及时观察到 exec ID 但无法证明未接收时返回 OutcomeUnknown,并 quarantine arm;
|
|
|
|
|
|
- Huayan 保留已有的停止确认和生命周期 generation;
|
|
|
|
|
|
- AGV cancel/zero command ACK 后继续等待导航终态和连续零速度样本;
|
|
|
|
|
|
- Motor Quick Stop 未确认时保留 motor resource quarantine;
|
|
|
|
|
|
- Teleop/stream 的 reconnect 总是新 session,旧轨迹不续跑;
|
|
|
|
|
|
- DexHand void 返回接口逐步改为结构化 result,不能只靠日志判断成功。
|
|
|
|
|
|
|
|
|
|
|
|
#### 测试
|
|
|
|
|
|
|
|
|
|
|
|
- admit 与 StopAll 的线性化竞态;
|
|
|
|
|
|
- snapshot 在 admit 后、dispatch 前过期;
|
|
|
|
|
|
- lease 在 dispatch 前被抢占;
|
|
|
|
|
|
- ACK 丢失、队列满、控制器断线、进程重连;
|
|
|
|
|
|
- 同 command ID 并发请求;
|
|
|
|
|
|
- stream sequence 重复、倒退、watchdog、断线;
|
|
|
|
|
|
- Stop 后旧 session/permit 无法恢复;
|
|
|
|
|
|
- 每个设备的真实停车确认。
|
|
|
|
|
|
|
|
|
|
|
|
#### 退出条件
|
|
|
|
|
|
|
|
|
|
|
|
- 上述高风险入口不存在绕过 Coordinator 的设备调用;
|
|
|
|
|
|
- 每类命令都有结构化结果,Internal 不再承载所有业务错误;
|
|
|
|
|
|
- 旧 gate 只作为兼容 deny,不再能单独 reopen 新 Coordinator;
|
|
|
|
|
|
- 设备级 OutcomeUnknown 可通过 GetSafetyState 定位并进入恢复流程;
|
2026-08-17 09:48:42 +08:00
|
|
|
|
- 新控制设备接入安全层不需要修改 SafetyManager switch。
|
2026-08-17 08:34:44 +08:00
|
|
|
|
|
|
|
|
|
|
#### 回滚边界
|
|
|
|
|
|
|
|
|
|
|
|
按 device ID 从 EnforceSelected 退回 Shadow。已产生的 quarantine 不能因回滚配置自动清除,
|
|
|
|
|
|
必须 StopAll 成功或通过当前 RecoveryExposure 允许的恢复流程处理。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段 4:泛化 StopAll 并开放正式恢复 RPC
|
|
|
|
|
|
|
|
|
|
|
|
#### 目标
|
|
|
|
|
|
|
|
|
|
|
|
删除 SystemService 中的设备类型停止编排,使 StopAll 和 Recover 成为统一、可观察、可重试的
|
|
|
|
|
|
安全事务。
|
|
|
|
|
|
|
|
|
|
|
|
#### 实施项
|
|
|
|
|
|
|
|
|
|
|
|
1. 实现 `SafetyOperationOrchestrator` 和 participant 分阶段执行。
|
|
|
|
|
|
2. 所有 legacy coordinator 注册为 participant,保留其已有 barrier 语义。
|
|
|
|
|
|
3. 设备通过注册的 safe-stop participant 加入,不再由 SystemService dynamic cast。
|
|
|
|
|
|
4. `gRPCSystemServiceImpl::StopAll` 缩减为 RequestContext、参数适配和结果映射。
|
|
|
|
|
|
5. StopAll 请求增加 operation ID、诊断用 service instance 和结构化 per-target response;旧或
|
|
|
|
|
|
缺失 instance 不能阻止安全停止。
|
|
|
|
|
|
6. 实现 RecoveryLedger、active refresh、quiescence check 和软件 latch reconcile。
|
|
|
|
|
|
7. 在 SystemService 注册 `GetSafetyState` 和 `RecoverSafetyState`。
|
|
|
|
|
|
8. Recover 统一执行 RecoveryExposure:Disabled 拒绝、LocalOnly 校验实际本机 peer、
|
|
|
|
|
|
Authorized 要求 authenticated SafetyAdmin;所有允许路径都要求非空 reason 并写持久审计。
|
|
|
|
|
|
9. `clearForTesting()` 保持测试可见或移入 test support,运行代码无法调用。
|
|
|
|
|
|
10. 调整 Runtime shutdown:先进入 ShuttingDown/关闭准入,再在 participant 存活时执行
|
|
|
|
|
|
有界 quiesce,之后停止 TaskManager 和 DeviceManager。
|
|
|
|
|
|
|
|
|
|
|
|
#### 并发规则
|
|
|
|
|
|
|
|
|
|
|
|
| 并发场景 | 规则 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| StopAll vs normal command | epoch 线性化;命令要么被拒绝,要么被登记并停止 |
|
|
|
|
|
|
| StopAll vs StopAll | 加入同一 round,返回同一结果 |
|
|
|
|
|
|
| Recover vs normal command | Recovering 全程关闭普通准入 |
|
|
|
|
|
|
| Recover vs StopAll | process operation mutex 串行化,Stop 优先 |
|
|
|
|
|
|
| Recover vs Recover | 相同 recovery ID join;不同 ID 串行 |
|
|
|
|
|
|
| RPC cancellation vs Stop/Recover | 只取消 waiter,不取消已开始的系统安全事务 |
|
|
|
|
|
|
| late worker vs new epoch | 只能写旧 operation 诊断,不能 release 新 barrier |
|
|
|
|
|
|
| shutdown vs Recover | shutdown 终止新 recovery,保持 gate 关闭并执行 quiesce |
|
|
|
|
|
|
|
|
|
|
|
|
#### 测试
|
|
|
|
|
|
|
|
|
|
|
|
- participant 注册顺序随机但执行阶段稳定;
|
|
|
|
|
|
- participant throw、timeout、永不返回、返回 Unknown;
|
|
|
|
|
|
- 部分恢复和 global blocker;
|
|
|
|
|
|
- Recover 时硬件 EStop、protective stop、fault、stale、disconnect、still moving;
|
|
|
|
|
|
- 恢复过程中状态再次变坏;
|
|
|
|
|
|
- expected epoch mismatch 和 recovery ID payload conflict;
|
|
|
|
|
|
- Disabled/LocalOnly/Authorized 三种 RecoveryExposure,尤其是非本机伪造 metadata 无法绕过;
|
|
|
|
|
|
- audit 写失败;
|
|
|
|
|
|
- service 析构、dispatcher join 和 Runtime shutdown。
|
|
|
|
|
|
|
|
|
|
|
|
#### 退出条件
|
|
|
|
|
|
|
|
|
|
|
|
- SystemService StopAll 不包含具体 DeviceKind 停止分支;
|
|
|
|
|
|
- StopAll/Recover 都返回 per-participant 结构化结果;
|
|
|
|
|
|
- 任何失败路径都保持明确 Latched/Quarantined 状态;
|
|
|
|
|
|
- Recover 无法绕过 Unknown、未结束 worker 或未确认运动;
|
|
|
|
|
|
- 一次成功 Recover 使系统能接受新命令,但不会恢复旧命令或自动使能硬件;
|
|
|
|
|
|
- Recover RPC 默认 Disabled;匿名部署只有 LocalOnly 验收通过才能开放,远程开放必须启用
|
|
|
|
|
|
authenticated SafetyAdmin;任何开放模式都要求持久审计验收通过。
|
|
|
|
|
|
|
|
|
|
|
|
#### 回滚边界
|
|
|
|
|
|
|
|
|
|
|
|
保留旧 StopAll adapter 一个发布周期,可通过本地启动配置切换实现。正式 Recover 已产生的
|
|
|
|
|
|
epoch 和审计不能回滚;禁止退回运行期无检查 clear。
|
|
|
|
|
|
|
|
|
|
|
|
### 阶段 5:全量 enforce、真机验证和旧路径移除
|
|
|
|
|
|
|
|
|
|
|
|
#### 目标
|
|
|
|
|
|
|
|
|
|
|
|
覆盖剩余外围设备和所有命令入口,验证发布产物与真实硬件,并删除重复全局状态。
|
|
|
|
|
|
|
|
|
|
|
|
#### 实施项
|
|
|
|
|
|
|
|
|
|
|
|
1. 迁移 Camera/Microphone/Speaker/BioHead/HLC 和剩余 local task;
|
|
|
|
|
|
2. 对所有 protobuf method 做 descriptor 驱动的 access-policy/intent 完整性测试;
|
|
|
|
|
|
3. Production 强制 `ENFORCE_ALL`,Control Unknown 拒绝;
|
|
|
|
|
|
4. 删除 service 对 `globalStopAllAdmissionGate()`、global media/motor registry 的业务依赖;
|
|
|
|
|
|
5. 保留必要 registry 的领域功能,但准入 authority 只属于 Coordinator;
|
|
|
|
|
|
6. 建立 build capability manifest,包含已编译的 transport/auth provider、MsQuic、硬件 SDK、
|
|
|
|
|
|
Safety schema version;
|
|
|
|
|
|
7. GetSystemInfo 和启动日志发布实际 capability,不以源码存在推断二进制能力;
|
|
|
|
|
|
8. 建立安装产物 smoke test,而不只测试 build tree;
|
|
|
|
|
|
9. 建立真实设备台架和故障注入矩阵;
|
|
|
|
|
|
10. 完成运维 Runbook:StopAll、blocker 诊断、硬件处理、dry-run、Recover 和审计查询。
|
|
|
|
|
|
|
|
|
|
|
|
#### 真机矩阵
|
|
|
|
|
|
|
|
|
|
|
|
至少覆盖:
|
|
|
|
|
|
|
|
|
|
|
|
- Aubo/Huayan:运动中断网、ACK 丢失、queue full、保护停、急停释放、SDK reconnect;
|
|
|
|
|
|
- AGV:导航取消 ACK 后仍移动、速度反馈丢失、地图切换中 StopAll;
|
|
|
|
|
|
- Motor:Modbus/EtherCAT 断线、bus epoch 改变、Quick Stop 未确认、stream watchdog;
|
|
|
|
|
|
- DexHand/PTZ/BioHead:连续命令中 StopAll、stop ACK 丢失、sensor stream 并发;
|
|
|
|
|
|
- 进程:控制中 SIGTERM、任务启动失败、服务重启后硬件仍活动;
|
|
|
|
|
|
- 网络:RPC 超时后相同 ID 重试、不同 payload 冲突、匿名 namespace、实际 peer 分类;如果构建
|
|
|
|
|
|
包含认证 provider,再覆盖 Token/证书轮换和权限变化;
|
|
|
|
|
|
- 多平台 QUIC:重连和多 heartbeat 不能改变本地控制 safety epoch。
|
|
|
|
|
|
|
|
|
|
|
|
#### 退出条件
|
|
|
|
|
|
|
|
|
|
|
|
- 所有 mutating 入口都经过 Coordinator 或显式安全通道;
|
|
|
|
|
|
- 所有 Control capability 都有 fresh snapshot、final check、safe-stop 和真机签字;
|
|
|
|
|
|
- 进程重启不会自动继续运动或接受旧请求;
|
|
|
|
|
|
- install artifact capability 与配置一致;
|
|
|
|
|
|
- 全量自动测试、TSAN/并发测试和硬件台架通过;
|
|
|
|
|
|
- 运维人员能只依赖结构化状态完成一次故障定位和恢复;
|
|
|
|
|
|
- 删除遗留 runtime clear 和无主 global gate。
|
|
|
|
|
|
|
|
|
|
|
|
#### 回滚边界
|
|
|
|
|
|
|
|
|
|
|
|
单设备可以通过本地配置从 EnforceSelected 退回 Shadow,但 RequestContext 安全边界、幂等和
|
|
|
|
|
|
Stop/Recover epoch 不能关闭。RecoveryExposure 或网络暴露范围的任何放宽都要求重启、审计和
|
|
|
|
|
|
现场审批,不能通过远程 RPC 修改。
|
|
|
|
|
|
|
|
|
|
|
|
## 20. 测试体系
|
|
|
|
|
|
|
|
|
|
|
|
### 20.1 单元测试
|
|
|
|
|
|
|
|
|
|
|
|
- Sensor/Control policy 全状态表;
|
|
|
|
|
|
- safety state transition 和非法 transition;
|
|
|
|
|
|
- snapshot freshness 和 generation;
|
|
|
|
|
|
- permit move/expiry/revalidation;
|
|
|
|
|
|
- ledger 和 recovery ledger;
|
|
|
|
|
|
- reason code 与 gRPC 映射;
|
|
|
|
|
|
- participant barrier/token epoch;
|
|
|
|
|
|
- RequestContext 构造、anonymous effective principal、provider/policy 组合;
|
|
|
|
|
|
- RecoveryExposure 和 actual peer classifier。
|
|
|
|
|
|
|
|
|
|
|
|
### 20.2 并发与性质测试
|
|
|
|
|
|
|
|
|
|
|
|
建议用可控 scheduler/fake clock 重复验证:
|
|
|
|
|
|
|
|
|
|
|
|
1. StopAll 和 dispatch 的所有交错最终满足“不丢失活动”;
|
|
|
|
|
|
2. Recover 和 late stop callback 的所有交错都不能提前 reopen;
|
|
|
|
|
|
3. 相同 command ID 任意并发度下 dispatch count 最大为 1;
|
|
|
|
|
|
4. generation 单调增加,旧 permit 永远无法重新合法;
|
|
|
|
|
|
5. participant 异常不会跳过其他 required stop;
|
|
|
|
|
|
6. Coordinator 销毁后不存在访问已释放对象的 callback。
|
|
|
|
|
|
|
|
|
|
|
|
### 20.3 Service 集成测试
|
|
|
|
|
|
|
|
|
|
|
|
- Disabled/Fake provider、method policy 和伪造 metadata;
|
|
|
|
|
|
- legacy/new protobuf client;
|
|
|
|
|
|
- handler 分类完整性;
|
|
|
|
|
|
- Stop/Status 在 Latched 时可调用,Recover 在 exposure 允许时不受普通 gate 阻塞;
|
|
|
|
|
|
- Recover per-target response;
|
|
|
|
|
|
- gRPC deadline/cancellation 与 operation 生命周期分离。
|
|
|
|
|
|
|
|
|
|
|
|
### 20.4 故障注入
|
|
|
|
|
|
|
|
|
|
|
|
| 故障点 | 期望结果 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| Admit 后 snapshot 过期 | final check 拒绝,无 SDK dispatch |
|
|
|
|
|
|
| SDK 调用超时且接收状态未知 | OutcomeUnknown + device quarantine |
|
|
|
|
|
|
| Stop worker 超时 | global Latched,worker token 保留 |
|
|
|
|
|
|
| Recovery refresh 超时 | blocker remains,不清 gate |
|
|
|
|
|
|
| 相同 ID 不同 payload | conflict,无第二次 dispatch |
|
|
|
|
|
|
| 进程重启期间硬件仍运动 | Starting/Latched,不自动 Open |
|
|
|
|
|
|
| 审计落盘失败 | Recover fail-closed |
|
|
|
|
|
|
| 一个设备 health worker 卡住 | 其他 Status/Stop/Recover 仍可执行 |
|
|
|
|
|
|
|
|
|
|
|
|
### 20.5 发布产物验证
|
|
|
|
|
|
|
|
|
|
|
|
- 对安装目录启动二进制;
|
|
|
|
|
|
- 校验 capability manifest;
|
|
|
|
|
|
- Insecure + Disabled 的 loopback 和显式 trusted-network grpcurl smoke;
|
|
|
|
|
|
- Recovery Disabled/LocalOnly 的实际 peer smoke;
|
|
|
|
|
|
- reflection 与安全 Profile 的显式配置 smoke;
|
|
|
|
|
|
- 配置要求未编译 AuthN/Transport provider、MsQuic 或 driver capability 时启动失败;
|
|
|
|
|
|
- 构建包含可选 Token/TLS/mTLS provider 时,才执行对应认证矩阵;
|
|
|
|
|
|
- 生成 C++ 和 Java API 兼容测试。
|
|
|
|
|
|
|
|
|
|
|
|
## 21. 可观测性与运维
|
|
|
|
|
|
|
|
|
|
|
|
### 21.1 指标
|
|
|
|
|
|
|
|
|
|
|
|
建议至少提供:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
cmvr_safety_admission_decisions_total{device,intent,decision,reason}
|
|
|
|
|
|
cmvr_safety_snapshot_age_ms{device}
|
|
|
|
|
|
cmvr_safety_device_state{device,state}
|
|
|
|
|
|
cmvr_safety_global_state{state}
|
|
|
|
|
|
cmvr_safety_stop_duration_ms{participant,result}
|
|
|
|
|
|
cmvr_safety_recovery_attempts_total{result}
|
|
|
|
|
|
cmvr_command_dedup_total{status}
|
|
|
|
|
|
cmvr_command_outcome_unknown_total{device,method}
|
|
|
|
|
|
cmvr_control_lease_conflicts_total{resource}
|
|
|
|
|
|
cmvr_grpc_authz_denied_total{method,role}
|
|
|
|
|
|
cmvr_grpc_requests_total{auth_method,authenticated}
|
|
|
|
|
|
cmvr_grpc_insecure_listener{non_loopback}
|
|
|
|
|
|
cmvr_grpc_recovery_access_denied_total{exposure,peer_kind,reason}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 21.2 事件历史
|
|
|
|
|
|
|
|
|
|
|
|
Coordinator 保存有界内存事件环,并将关键事件写入持久审计:
|
|
|
|
|
|
|
|
|
|
|
|
- global/device transition;
|
|
|
|
|
|
- snapshot stale/fresh、generation changed;
|
|
|
|
|
|
- quarantine 创建和清除;
|
|
|
|
|
|
- StopAll/Recover phase;
|
|
|
|
|
|
- permit reject、final check reject;
|
|
|
|
|
|
- OutcomeUnknown;
|
|
|
|
|
|
- participant timeout/late completion。
|
|
|
|
|
|
|
|
|
|
|
|
错误字符串只用于人读,自动化必须使用 reason code 和结构化 blocker。
|
|
|
|
|
|
|
|
|
|
|
|
### 21.3 Runbook 顺序
|
|
|
|
|
|
|
|
|
|
|
|
1. 调用 GetSafetyState 获取 epoch 和 blocker;
|
|
|
|
|
|
2. 确认当前 RecoveryExposure;Disabled 模式不能尝试网络绕过,需按本机变更流程处理;
|
|
|
|
|
|
3. 在现场确认物理环境和设备状态;
|
|
|
|
|
|
4. 必要时调用 typed ResetFault,不能直接 Recover 代替硬件处理;
|
|
|
|
|
|
5. 使用符合 LocalOnly/Authorized 策略的调用方执行 RecoverSafetyState `VERIFY_ONLY`;
|
|
|
|
|
|
6. blocker 全部满足后调用 `CLEAR_SOFTWARE_LATCH`;
|
|
|
|
|
|
7. 重新读取 SafetyState,确认新 epoch 和 Open/设备级状态;
|
|
|
|
|
|
8. 使用新的 command ID、service instance 和 device generation 下发后续命令。
|
|
|
|
|
|
|
|
|
|
|
|
## 22. 建议 PR 拆分
|
|
|
|
|
|
|
|
|
|
|
|
为避免一次性改动所有设备,建议按以下独立可回滚单元提交:
|
|
|
|
|
|
|
|
|
|
|
|
1. 设计文档和安全不变量;
|
|
|
|
|
|
2. GRPCSecurityConfig、RequestContext、Disabled/Fake provider、method policy 和 call guard;
|
|
|
|
|
|
3. correlation/audit schema、实际 security capability 上报和 Compatibility Profile 测试;
|
|
|
|
|
|
4. Safety types、snapshot store 和 tests;
|
|
|
|
|
|
5. CommandHeader/reason code additive Proto;
|
|
|
|
|
|
6. CommandLedger;
|
2026-08-17 09:48:42 +08:00
|
|
|
|
7. SafetyManager shadow 和 GetSafetyState;
|
2026-08-17 08:34:44 +08:00
|
|
|
|
8. legacy participant adapters;
|
|
|
|
|
|
9. Arm + ArmTeleop migration;
|
|
|
|
|
|
10. AGV migration;
|
|
|
|
|
|
11. Motor migration;
|
|
|
|
|
|
12. DexHand/PTZ/ActionQueue migration;
|
|
|
|
|
|
13. generic StopAll;
|
|
|
|
|
|
14. RecoverSafetyState;
|
|
|
|
|
|
15. peripheral migration、EnforceAll 和 legacy removal;
|
|
|
|
|
|
16. capability manifest、install smoke 和硬件验证报告;
|
|
|
|
|
|
17. 可选后续:Server TLS + Static Token provider 和客户端 interceptor;
|
|
|
|
|
|
18. 可选后续:JWT/OIDC 或 mTLS provider、正式身份生命周期和密钥轮换。
|
|
|
|
|
|
|
|
|
|
|
|
每个迁移 PR 必须包含:命令分类表、snapshot 定义、stop verification、错误映射、竞态测试和
|
|
|
|
|
|
回滚开关。不得只把 handler 前的一个 `if` 移到 Coordinator 就宣称完成迁移。
|
|
|
|
|
|
|
|
|
|
|
|
## 23. 最终验收清单
|
|
|
|
|
|
|
|
|
|
|
|
- [x] 所有 gRPC handler 都通过统一 RequestContext/call guard,业务代码不解析认证 metadata
|
|
|
|
|
|
- [x] Disabled 模式始终产生未认证 anonymous principal,实际安全 Profile 和监听风险可观测
|
|
|
|
|
|
- [x] 所有方法都有 access policy 和 CommandIntent,未知方法不会绕过安全网关
|
|
|
|
|
|
- [x] Recover 默认 Disabled;匿名模式只允许 LocalOnly,远程模式只允许 authenticated SafetyAdmin
|
|
|
|
|
|
- [x] Recover 所有开放方式都要求持久审计,审计失败时 fail-closed
|
|
|
|
|
|
- [ ] 启用 Authenticated Profile 时,传输/AuthN/AuthZ 按该 Profile 的独立验收矩阵通过
|
|
|
|
|
|
- [x] 生命周期、健康和安全状态是独立字段
|
|
|
|
|
|
- [x] Manager snapshot/status 路径只读 Manager 缓存,不执行设备 I/O
|
|
|
|
|
|
- [x] 普通 mutating unary 命令使用 command ID、service instance 和统一 ledger 语义
|
|
|
|
|
|
- [x] 流式控制绑定 session、sequence、watchdog 和 safety epoch
|
|
|
|
|
|
- [x] 已迁移的 Control 命令经过统一准入和驱动最终检查
|
|
|
|
|
|
- [x] Stop/Status 在软件锁止状态下可达;Recover 在 exposure 允许时不受普通 gate 阻塞
|
|
|
|
|
|
- [x] SystemService StopAll 不按 DeviceKind 硬编码设备停止逻辑
|
|
|
|
|
|
- [x] StopAll 失败留下结构化 blocker 和不可绕过 latch
|
|
|
|
|
|
- [x] Recover 不能忽略 Unknown、EStop、未确认运动或旧 worker
|
|
|
|
|
|
- [x] Recover 不上电、不使能、不恢复旧轨迹或旧 session
|
|
|
|
|
|
- [x] 同一 command ID 最大一次 dispatch,OutcomeUnknown 不自动重发
|
|
|
|
|
|
- [x] 新设备只需注册 DeviceSafetyDescriptor、Endpoint、Participant 和 method intent
|
|
|
|
|
|
- [x] startup/shutdown 与 safety epoch 有明确顺序
|
|
|
|
|
|
- [x] 单个设备动态故障不会关闭按当前安全 Profile 启动的 SystemService 管理面
|
|
|
|
|
|
- [ ] 所有发布 capability 来自实际安装二进制
|
|
|
|
|
|
- [ ] fake、并发、故障注入、安装产物和真机台架全部完成
|
|
|
|
|
|
|
|
|
|
|
|
## 24. 实施前需要确定的工程参数
|
|
|
|
|
|
|
|
|
|
|
|
以下参数需要通过设备手册和台架测量确定,但不改变架构:
|
|
|
|
|
|
|
|
|
|
|
|
- 各设备 snapshot polling 周期和 maximum age;
|
|
|
|
|
|
- StopAll 总 deadline 与每类 participant timeout;
|
|
|
|
|
|
- 连续零速度/静止确认的阈值和样本数;
|
|
|
|
|
|
- Aubo、Huayan、SEER 等 SDK 对“已接受命令”的精确定义;
|
|
|
|
|
|
- ledger 容量和结果保留时间;
|
|
|
|
|
|
- 审计文件保留、轮转和上传策略;
|
|
|
|
|
|
- 当前部署允许监听的网卡/CIDR、主机防火墙规则和 `allow_insecure_non_loopback` 责任人;
|
|
|
|
|
|
- RecoveryExposure 初始选择 Disabled 还是 LocalOnly,以及本机维护入口;
|
|
|
|
|
|
- 旧安全配置兼容窗口长度,以及启用 Static Token/Server TLS 的触发条件;
|
|
|
|
|
|
- 后续认证启用时的 principal 命名、Token/证书轮换和角色绑定;
|
|
|
|
|
|
- 哪些设备是启动时 required control device。
|
|
|
|
|
|
|
|
|
|
|
|
这些值必须以显式配置和硬上限进入代码,不能以缺省零值表达“无限”或“允许全部”。
|