[AZ-680] [AZ-681] operator_bridge command dispatch + safety lane

Add the operator-command dispatcher behind a typed CommandAck:
60 s per-command-id idempotency cache, surfaced-POI registry with
unknown_poi_id + expired gates, BIT-degraded ack severity check, and
SafetyOverride forwarding to mission_executor with structured audit
log (redacts signature + session_token).

Cross-layer wiring goes through three new traits in shared::contracts
(ScanCommandRouter, MissionSafetyRouter, BitReportSeverityLookup) so
operator_bridge stays free of direct scan_controller / mission_executor
imports. scan_controller::ScanControllerHandle implements the scan
router; a new mission_executor::SafetyDispatchHandle wraps the BIT
ack channel + battery monitor handle and implements the safety router;
BitControllerHandle gains a bounded (16-entry) report-severity cache
for the lookup trait.

scan_controller also picks up ConfirmPoi handling: PoiQueue::confirm
removes the entry and SubmitOutcome::Confirmed carries the typed
(target_mgrs, target_class) hint for AZ-684/AZ-686 downstream.

Tests: 9 new integration tests in operator_bridge/tests/dispatcher.rs
cover AZ-680 AC-1..AC-5 + AZ-681 AC-1..AC-4. scan_controller adds 2
ConfirmPoi tests. All modified-crate suites green; one pre-existing
mission_executor state-machine test flake (already documented in
_docs/_process_leftovers) updated to note ac1 also affected.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-20 17:32:38 +03:00
parent aa4282f9f8
commit c4eff40dbc
24 changed files with 2017 additions and 53 deletions
+60
View File
@@ -83,6 +83,66 @@ pub trait OperatorCommandSink: Send + Sync {
async fn dispatch(&self, command: OperatorCommand) -> Result<()>;
}
/// AZ-680 — route a validated `OperatorCommand` into `scan_controller`.
///
/// Lives in `shared::contracts` so `operator_bridge` (Layer 3) can
/// depend on the trait without importing `scan_controller` (Layer 4).
/// `scan_controller` implements this for its public `Handle`.
///
/// The trait name uses `route` instead of `submit_operator_cmd` to
/// avoid a name collision with the inherent method on
/// `ScanControllerHandle`. Implementations forward to the inherent
/// method.
#[async_trait]
pub trait ScanCommandRouter: Send + Sync {
async fn route(&self, command: OperatorCommand) -> Result<()>;
}
/// AZ-681 — forward safety-critical operator commands (BIT acks,
/// safety overrides) into `mission_executor`.
///
/// `operator_bridge` (Layer 3) cannot import `mission_executor`
/// (Layer 3 sibling). The composition root constructs a concrete
/// impl that wraps the executor's BIT ack channel + battery monitor
/// handle.
#[async_trait]
pub trait MissionSafetyRouter: Send + Sync {
/// Forward a signed BIT-degraded acknowledgement. The
/// `report_id` identifies the originating BIT report that
/// produced the `Degraded` verdict. `operator_id` is carried for
/// the executor's structured-log trail.
async fn acknowledge_bit_degraded(
&self,
report_id: uuid::Uuid,
operator_id: Option<String>,
) -> Result<()>;
/// Apply a signed safety override. The override is bounded by
/// `duration_secs`; the receiving subsystem (e.g. battery
/// monitor) is responsible for enforcing the deadline.
async fn apply_safety_override(
&self,
scope: crate::models::operator::SafetyOverrideScope,
duration_secs: u32,
operator_id: String,
rationale: String,
) -> Result<()>;
}
/// AZ-681 — look up the severity of a previously-generated BIT report
/// by id. `operator_bridge` consults this before forwarding a BIT-
/// degraded ack: a `Fail` severity is never acknowledgeable (per
/// AC-2).
///
/// Returns `Some(true)` when the report exists and is acknowledgeable
/// (severity is NOT `Fail`); `Some(false)` when known and `Fail`;
/// `None` when the report id has never been generated (or has aged
/// out of the lookup cache).
#[async_trait]
pub trait BitReportSeverityLookup: Send + Sync {
async fn is_acknowledgeable(&self, report_id: uuid::Uuid) -> Option<bool>;
}
#[cfg(test)]
mod tests {
use super::*;
+25
View File
@@ -20,6 +20,31 @@ pub enum OperatorCommandKind {
MissionAbort,
}
/// AZ-681 — scope of a `SafetyOverride` command. Each variant maps to
/// a specific failsafe family in `mission_executor` that the operator
/// is suppressing for a bounded duration (architecture.md §F10).
///
/// Marked `#[non_exhaustive]` so adding `LinkLost` / `Geofence` later
/// is a non-breaking change to downstream matchers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SafetyOverrideScope {
/// Suppress battery-RTL until the override deadline elapses. The
/// `hard_floor` land-now is NEVER suppressible regardless of
/// override (per `architecture.md §F10`).
BatteryRtl,
}
impl SafetyOverrideScope {
/// Stable kebab-case label for audit logs and metrics.
pub fn label(self) -> &'static str {
match self {
Self::BatteryRtl => "battery_rtl",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperatorCommand {
pub command_id: Uuid,