# 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 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
限制 可选认证 授权策略 审计"] Gateway --> Adapter["Typed Service Adapter
声明 CommandIntent"] Adapter --> Ledger["CommandLedger
幂等和结果"] 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.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 命令,不会续跑中断前的任务。 ### 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 endpoint; std::shared_ptr 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 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` 和 `RestoreOperationalState` 额外使用独立的 `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。 这些值必须以显式配置和硬上限进入代码,不能以缺省零值表达“无限”或“允许全部”。