cmvr-es/docs/device_safety_control_plane_architecture.md

1824 lines
86 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 | 厂商结果语义仍需逐台真机校准 |
| 2 | 软件完成 | DeviceManager 持有 SafetyManager、legacy participant、GetSafetyState、启动 coverage 校验、Shadow/Enforce 配置 | 真实运行 Shadow 日志评审 |
| 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 另有一条设备内硬件语义:真实硬件急停曾有效、随后输入消失且控制器重新报告
`Normal/ReducedMode` 时,驱动会自动上电到 `Idle`、清理旧队列、执行 `startup()`,并在
`Running` 下再次确认 quiescent 后解除该硬件锁存,使新的 gRPC 指令可以重新准入;
软件 `emergencyStop()` 使用独立 `SoftwareEmergencyStop` 锁存,即使它与硬件急停重叠也绝不被
硬件输入释放自动清除。自动流程不 resume、不重放旧目标;显式 Stop/PowerOff 会取消本轮自动上电。
## 1. 决策摘要
本设计采用以下核心决策:
1. `DeviceManager` 继续负责设备注册和生命周期,并持有一个独立、可测试的
`SafetyManager`。状态机代码不直接堆入 `DeviceManager`。
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 时,
只能替换安全网关组件,不修改设备、`SafetyManager` 或业务 Proto。
12. 该软件控制面不替代独立物理急停,也不声明 SIL、PL 或其他功能安全等级。
## 2. 改造前基础与需要保留的行为
项目已有一些经过并发测试的能力,应当迁移和复用,而不是重新实现:
| 当前能力 | 目标用法 |
| --- | --- |
| `ControlAuthorityManager` 的 lease、generation、dispatch fence、quarantine | 作为 `SafetyManager` 内部控制权组件 |
| `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`;
- 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`;
- 设备生命周期和快照:`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`,重试只能查询该结果,不能再次下发。
8. 通用 `RecoverSafetyState` 成功只表示软件准入可重新评估,不表示设备被上电、使能、
解除急停或自动运动。AUBO 物理急停释放后的自动上电是独立、显式配置的设备内策略。
9. `SafetyManager` 持有内部锁时不得调用设备、网络或可能阻塞的 participant 方法。
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/>幂等和结果"]
Adapter --> Coordinator["DeviceManager::SafetyManager"]
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
cmvr-es/manager/safety_manager/
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
include/safety_manager.h
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/...
```
新增 CMake target:`cmvr_es::safety_manager`。它可以依赖通用类型和
`ControlAuthorityManager`,但不能依赖 gRPC、具体设备后端或厂商 SDK。
### 5.2 `DeviceManager` 的职责变化
`DeviceManager` 增加:
```cpp
SafetyManager& safetyManager() noexcept;
const SafetyManager& safetyManager() const noexcept;
```
它负责:
- 在设备创建后注册设备描述、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&
SafetyManager&
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
-> construct DeviceManager core and SafetyManager
-> 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
```
- 配置损坏、所选安全 Profile 的身份材料无效或 SafetyManager 核心构造失败仍使进程启动失败;
- `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 返回冲突。
5. Service 使用固定 `CommandDescriptor` 调用 `SafetyManager::admit()`。
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。
### 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;本设计
保证的是不再修改 SafetyManager、StopAll 和 Recover 的设备类型分支。
### 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;
};
```
participant 可以代表设备,也可以代表 ActionQueue、MediaSourceManager、Motor session registry 等
跨设备活动域。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 所有权。
`SafetyManager` 不依赖 gRPC 类型。Service 只把从 `RequestContext` 派生的稳定
`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 前清零。
`RecoverSafetyState` 额外使用独立的 `RecoveryExposure`:
| 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;
- `DeviceManager`、`SafetyManager`、Sensor/Control policy;
- `DeviceSafetyEndpoint`、`SafetyParticipant` 和厂商驱动;
- handler 内的命令准入、StopAll 或 Recover 业务分支。
安全 Profile 只能在重启时切换。重启会生成新的 service instance ID,进程内 anonymous ledger
自然失效;不做运行期 `anonymous -> authenticated principal` 账本迁移,也不允许认证加载失败时
保留旧监听并降级运行。
## 17. 配置设计
### 17.1 SafetyManagerConfig
建议在 `DeviceManagerConfig` 中增加:
```protobuf
message SafetyManagerConfig {
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/恢复要点 |
| --- | --- | --- | --- |
| 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 |
| 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 状态当作手部运动状态 |
| Camera capture | Sensor | opened、streaming、worker generation | 复用 MediaSourceManager;gRPC/QUIC 和直接 startStreaming 在启动设备 producer 前取得 Sensor/StartActivity dispatch guard |
| 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 仍权威 |
| 2 | SafetyManager shadow | 否,只比较决策 |
| 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:统一类型、快照、错误与幂等契约
#### 目标
建立后续状态机所需的数据契约,但不改变现有命令准入结果。
#### 实施项
1. 新建 `manager/safety_manager` target 和 `safety_types.h`。
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。
### 阶段 2:SafetyManager 影子运行
#### 目标
在不改变线上允许/拒绝结果的情况下,对全部命令计算新策略结果并验证分类完整性。
#### 实施项
1. DeviceManager 构造并持有 SafetyManager。
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 的普通命令由
SafetyManager 权威准入,并统一幂等与执行结果语义。
#### 迁移顺序
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 定位并进入恢复流程;
- 新控制设备接入安全层不需要修改 SafetyManager switch。
#### 回滚边界
按 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;
7. SafetyManager shadow 和 GetSafetyState;
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。
这些值必须以显式配置和硬上限进入代码,不能以缺省零值表达“无限”或“允许全部”。