mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-23 15:51:10 +00:00
[AZ-676] [AZ-677] [AZ-678] [AZ-679] telemetry+operator foundation
Batch 15 ships the four foundation tickets sitting on top of AZ-675 (gRPC server) and AZ-667 (mapobjects_store hydrate): * AZ-676: telemetry_stream video path (rtsp_forward + bytes_inline) with ai_locked atomic + session counter, SubscribeVideo RPC. * AZ-677: MapObjects snapshot-on-subscribe + diff broadcast + reconnect-resync (StartThen stream-prepend pattern). * AZ-678: HmacOperatorValidator with per-session monotonic seq, in-process session registry + TTL, constant-time HMAC compare, rejection-reason counters, sliding 60 s sig-failure red-health gate. Trait OperatorCommandValidator in shared::contracts::operator_auth. * AZ-679: PoiSurfaceMapper produces OperatorPoiEvent per architecture §7.10; PoiDequeued events on rotate/age-out/complete; pushed via new TelemetrySink::push_operator_event extension on Topic::OperatorEvent. Cross-task wiring: TelemetrySink trait extended with push_operator_event; OperatorBridge gets optional builder methods with_telemetry_sink / with_validator (composition root wires in AZ-680). Workspace deps: hmac = "0.12"; per-crate adds bytes, serde_json, parking_lot, chrono, uuid, sha2, thiserror. Tests: 14/14 ACs verified locally (4 + 3 + 5 + 3 by AC) plus 6 supporting unit tests + 7 integration tests + 2 shared serde roundtrips. cargo clippy clean on touched crates. Cumulative review for batches 13-15 produced; verdict PASS_WITH_WARNINGS (0 Critical, 0 High, 1 Medium, 4 Low — all carry-overs or deferred-producer notes for AZ-680/AZ-684). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
//! AZ-678 — operator command authentication contract.
|
||||
//!
|
||||
//! `OperatorCommandValidator` is the boundary every operator-bound
|
||||
//! command crosses before any business logic runs. The default
|
||||
//! implementation (`HmacOperatorValidator` in
|
||||
//! `operator_bridge::internal::auth`) uses HMAC-SHA256 over
|
||||
//! `(session_token || sequence_number || payload_bytes)`. The trait
|
||||
//! lives here so the dispatch surface (`scan_controller`,
|
||||
//! `mission_executor`) can depend on the contract without importing
|
||||
//! `operator_bridge`.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::models::operator::{OperatorCommand, OperatorCommandKind};
|
||||
|
||||
/// A command as it arrives over the operator-link, prior to any
|
||||
/// authentication. Mirrors the validated `OperatorCommand` shape
|
||||
/// closely so a successful `validate` is a near-identity transform.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SignedCommand {
|
||||
pub session_token: String,
|
||||
pub sequence_number: u64,
|
||||
pub kind: OperatorCommandKind,
|
||||
pub payload: serde_json::Value,
|
||||
/// HMAC over `(session_token || sequence_number || canonical
|
||||
/// JSON of payload)`. Length depends on the scheme; for HMAC-SHA256
|
||||
/// this is exactly 32 bytes.
|
||||
pub signature: Vec<u8>,
|
||||
/// Wall-clock time the Ground Station stamped the command. Carried
|
||||
/// through `validate` for downstream audit logging; not used by
|
||||
/// the auth check itself.
|
||||
pub issued_at_wallclock: chrono::DateTime<chrono::Utc>,
|
||||
pub command_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl SignedCommand {
|
||||
/// Convert into a canonical [`OperatorCommand`] once validation
|
||||
/// has succeeded. The signature is retained on the result for
|
||||
/// downstream audit logging.
|
||||
pub fn into_command(self) -> OperatorCommand {
|
||||
OperatorCommand {
|
||||
command_id: self.command_id,
|
||||
session_token: self.session_token,
|
||||
sequence_number: self.sequence_number,
|
||||
issued_at_wallclock: self.issued_at_wallclock,
|
||||
kind: self.kind,
|
||||
payload: self.payload,
|
||||
signature: self.signature,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validated command. Returned by `OperatorCommandValidator::validate`
|
||||
/// on the happy path. Holding a `ValidatedCommand` is the proof that
|
||||
/// dispatching the inner command is safe.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValidatedCommand {
|
||||
pub command: OperatorCommand,
|
||||
}
|
||||
|
||||
/// Why an operator command was rejected. Each variant maps 1-1 to a
|
||||
/// `auth_rejections_total{reason}` metric counter and to a structured
|
||||
/// log line. Order MUST match
|
||||
/// `operator_bridge::internal::auth::REJECTION_REASONS` for the
|
||||
/// counter array layout.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||
pub enum AuthError {
|
||||
#[error("signature does not match computed HMAC")]
|
||||
SignatureInvalid,
|
||||
#[error("replay detected — sequence number not greater than last seen")]
|
||||
ReplayDetected,
|
||||
#[error("session token unknown or never established")]
|
||||
SessionUnknown,
|
||||
#[error("session token expired (TTL elapsed)")]
|
||||
SessionExpired,
|
||||
}
|
||||
|
||||
impl AuthError {
|
||||
/// Stable kebab-case label for the rejection-reason metric.
|
||||
pub fn reason_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::SignatureInvalid => "signature_invalid",
|
||||
Self::ReplayDetected => "replay_detected",
|
||||
Self::SessionUnknown => "session_unknown",
|
||||
Self::SessionExpired => "session_expired",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contract every operator-command validator must satisfy. The
|
||||
/// default `HmacOperatorValidator` lives in `operator_bridge`; other
|
||||
/// schemes (e.g. Q9 resolution to a JWS-based one) implement the
|
||||
/// same trait and can be swapped behind the same callsite.
|
||||
pub trait OperatorCommandValidator: Send + Sync {
|
||||
/// Validate one signed command. On `Ok`, the per-session
|
||||
/// sequence-number tracker advances; on `Err`, it does NOT
|
||||
/// advance (so the rejected `seq` does not poison the session).
|
||||
fn validate(&self, cmd: SignedCommand) -> Result<ValidatedCommand, AuthError>;
|
||||
}
|
||||
Reference in New Issue
Block a user