mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-21 13:31:09 +00:00
[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:
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user