ASELSANMicrokernel
S560 · SOURCE-BOUND GATE EVIDENCE

S560 · R1 modem: modem alt sistemi capability ve gözetim modeli

tam S560 implementation modülü → Operations --test hedefi ile bağlı tam focused test → ayrı Operations kaydı Bu sayfa yalnız S560 kapısına aittir; komşu kapıların kaynakları bu kabulün içine katılmaz.

S560Focused kod testiOperations id exactsource SHA exacttest target exact

operation: g8l-s560-r1-modem-subsystem-capability-supervision-model

uygulama/model · focused test · Operations · 3 exact excerpt

sequence-bound=true · implementation-bound=true
01 · Yürütme / doğrulama kodu

Kapının gerçek repository sözleşmesi

tam dosyaL1–L763
kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s560_r1_modem_subsystem_capability_supervision_model.rs::S560 r1 modem subsystem capability supervision model implementation
//! S560 models the R1 modem subsystem as a capability-gated EL0 service with a
//! supervisor watchdog.  It is a pure source/host model for R1 stage 3 (modem,
//! data, call and audio).
//!
//! The model covers: an endpoint capability whose rights are the bitmask
//! `{AT, SMS, DATA, VOICE, AUDIO}`; a bounded request/response schema
//! (opcode, strictly increasing per-client sequence, payload of at most 256
//! bytes with a deterministic FNV-1a digest); the service state machine
//! `Stopped -> Starting -> Ready <-> Degraded -> Restarting -> Starting` with
//! the terminal `Failed` state; a heartbeat watchdog that moves the service to
//! `Restarting` after three consecutive missed heartbeat ticks; a restart
//! budget of three restarts per 60-tick window with exponential backoff
//! (1, 2, 4 ticks); capability revocation when the service reaches `Failed`;
//! and a per-client request rate limit of eight requests per 10-tick window.
//!
//! Everything is fail-closed: an unknown opcode, a missing right, an oversize
//! payload, a revoked or mismatched capability, an out-of-order sequence, a
//! request outside `Ready`/`Degraded`, a start before the backoff elapsed and
//! a rate-limited client are all rejected without mutating the state.  Every
//! accepted step is recorded in a bounded ledger; an exact replay of a
//! published step returns `StepRetained` with the identical receipt and a
//! divergent event at a published step is `PublishedStateDrift`.
//!
//! The gate does NOT claim any hardware: no modem, SIM, AT transport, UART,
//! panel, touch controller, SD card, board, power transition or runtime
//! observation exists for it.  The module has no production callsite and is
//! not wired into any boot path, IRQ path, scheduler or driver; it performs no
//! device operation, emits no UART text and does not rerun or promote the
//! immutable S540/S543 physical RED observations.  Predecessor: S559 (audio
//! route / PCM capability model).  Next gate: S561 (permissioned application
//! launch flow model).

use alloc::vec::Vec;

pub const S560_SEQUENCE: usize = 560;
pub const S560_EXPECTED_PREDECESSOR: usize = 559;
pub const S560_R1_STAGE: u8 = 3;
pub const S560_R1_RANGE_FIRST: usize = 536;
pub const S560_R1_RANGE_LAST: usize = 568;

pub const S560_RIGHT_AT: u8 = 0b0_0001;
pub const S560_RIGHT_SMS: u8 = 0b0_0010;
pub const S560_RIGHT_DATA: u8 = 0b0_0100;
pub const S560_RIGHT_VOICE: u8 = 0b0_1000;
pub const S560_RIGHT_AUDIO: u8 = 0b1_0000;
pub const S560_RIGHTS_ALL: u8 = 0b1_1111;
pub const S560_RIGHT_COUNT: usize = 5;

pub const S560_OPCODE_COUNT: usize = 8;
pub const S560_PAYLOAD_MAX_BYTES: usize = 256;
pub const S560_SEQUENCE_FIRST: u32 = 1;

pub const S560_HEARTBEAT_MISS_LIMIT: u8 = 3;
pub const S560_RESTART_BUDGET_MAX: u8 = 3;
pub const S560_RESTART_WINDOW_TICKS: u32 = 60;
pub const S560_BACKOFF_BASE_TICKS: u32 = 1;
pub const S560_BACKOFF_MAX_TICKS: u32 = 8;

pub const S560_CLIENT_RATE_LIMIT: u8 = 8;
pub const S560_RATE_WINDOW_TICKS: u32 = 10;
pub const S560_MAX_CLIENTS: usize = 4;
pub const S560_ENDPOINT_ID_BASE: u16 = 0x5600;
pub const S560_MAX_STEPS: usize = 64;

pub const S560_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS: usize = 0;
pub const S560_PHYSICAL_OBSERVATIONS: usize = 0;
pub const S560_PHYSICAL_OR_DEVICE_OPERATIONS: usize = 0;
pub const S560_SD_WRITES: usize = 0;
pub const S560_UART_OPENS: usize = 0;
pub const S560_POWER_TRANSITIONS: usize = 0;
pub const S560_NEW_IMMUTABLE_RAW_CAPTURES: usize = 0;
pub const S560_S540_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S560_S543_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S560_AUTOMATIC_PROMOTION: bool = false;
pub const S560_BOOT_TO_UI_PHYSICALLY_OBSERVED: bool = false;
pub const S560_HARDWARE_PRESENT: bool = false;
pub const S560_R1_ACCEPTANCE_COMPLETE: bool = false;
pub const RUNBOOK_EXECUTED_IN_S560: bool = false;

const FNV1A_OFFSET: u32 = 0x811c_9dc5;
const FNV1A_PRIME: u32 = 0x0100_0193;

/// Modem service opcodes.  Each opcode requires exactly one right.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS560ModemOpcode {
    AtCommand,
    SmsSend,
    SmsRead,
    DataAttach,
    DataDetach,
    VoiceDial,
    VoiceHangup,
    AudioRoute,
}

impl G8lS560ModemOpcode {
    pub const ALL: [Self; S560_OPCODE_COUNT] = [
        Self::AtCommand,
        Self::SmsSend,
        Self::SmsRead,
        Self::DataAttach,
        Self::DataDetach,
        Self::VoiceDial,
        Self::VoiceHangup,
        Self::AudioRoute,
    ];

    pub const fn wire(self) -> u8 {
        match self {
            Self::AtCommand => 0x01,
            Self::SmsSend => 0x10,
            Self::SmsRead => 0x11,
            Self::DataAttach => 0x20,
            Self::DataDetach => 0x21,
            Self::VoiceDial => 0x30,
            Self::VoiceHangup => 0x31,
            Self::AudioRoute => 0x40,
        }
    }

    pub const fn from_wire(wire: u8) -> Option<Self> {
        match wire {
            0x01 => Some(Self::AtCommand),
            0x10 => Some(Self::SmsSend),
            0x11 => Some(Self::SmsRead),
            0x20 => Some(Self::DataAttach),
            0x21 => Some(Self::DataDetach),
            0x30 => Some(Self::VoiceDial),
            0x31 => Some(Self::VoiceHangup),
            0x40 => Some(Self::AudioRoute),
            _ => None,
        }
    }

    pub const fn required_right(self) -> u8 {
        match self {
            Self::AtCommand => S560_RIGHT_AT,
            Self::SmsSend | Self::SmsRead => S560_RIGHT_SMS,
            Self::DataAttach | Self::DataDetach => S560_RIGHT_DATA,
            Self::VoiceDial | Self::VoiceHangup => S560_RIGHT_VOICE,
            Self::AudioRoute => S560_RIGHT_AUDIO,
        }
    }

    /// Bounded response payload length per opcode.  `AtCommand` echoes the
    /// request length; every value stays within `S560_PAYLOAD_MAX_BYTES`.
    pub const fn response_payload_len(self, request_payload_len: usize) -> usize {
        match self {
            Self::AtCommand => request_payload_len,
            Self::SmsSend => 4,
            Self::SmsRead => 176,
            Self::DataAttach => 16,
            Self::DataDetach => 0,
            Self::VoiceDial => 8,
            Self::VoiceHangup => 0,
            Self::AudioRoute => 4,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS560ModemServiceState {
    Stopped,
    Starting,
    Ready,
    Degraded,
    Restarting,
    Failed,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS560ModemEndpointCapability {
    pub endpoint_id: u16,
    pub client_id: u8,
    pub rights: u8,
    pub generation: u8,
    pub revoked: bool,
}

impl G8lS560ModemEndpointCapability {
    pub const fn grants(self, right: u8) -> bool {
        !self.revoked && self.rights & right == right
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS560ModemRequest {
    pub client_id: u8,
    pub endpoint_id: u16,
    pub opcode: u8,
    pub sequence: u32,
    pub payload_len: usize,
    pub payload_digest: u32,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS560ModemResponseStatus {
    Accepted,
    AcceptedDegraded,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS560ModemResponse {
    pub endpoint_id: u16,
    pub opcode: u8,
    pub sequence: u32,
    pub status: G8lS560ModemResponseStatus,
    pub payload_len: usize,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS560ModemEvent {
    GrantCapability { client_id: u8, rights: u8 },
    RevokeCapability { client_id: u8 },
    Start,
    ReadyReported,
    Stop,
    Heartbeat { seen: bool },
    Tick { ticks: u32 },
    Request(G8lS560ModemRequest),
}

impl G8lS560ModemEvent {
    pub const fn kind(self) -> u8 {
        match self {
            Self::GrantCapability { .. } => 1,
            Self::RevokeCapability { .. } => 2,
            Self::Start => 3,
            Self::ReadyReported => 4,
            Self::Stop => 5,
            Self::Heartbeat { .. } => 6,
            Self::Tick { .. } => 7,
            Self::Request(..) => 8,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS560ModemSupervisionReceipt {
    pub sequence: usize,
    pub predecessor_sequence: usize,
    pub r1_stage: u8,
    pub step: usize,
    pub event_kind: u8,
    pub tick: u32,
    pub from_state: G8lS560ModemServiceState,
    pub to_state: G8lS560ModemServiceState,
    pub heartbeat_misses: u8,
    pub restarts_in_window: u8,
    pub backoff_ticks: u32,
    pub active_capabilities: u8,
    pub revoked_capabilities: u8,
    pub response: Option<G8lS560ModemResponse>,
    pub hardware_present: bool,
    pub physical_observations: usize,
    pub runbook_executed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS560ModemSupervisionOutcome {
    StepPublished(G8lS560ModemSupervisionReceipt),
    StepRetained(G8lS560ModemSupervisionReceipt),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS560ModemSupervisionError {
    StepOutOfOrder,
    LedgerFull,
    PublishedStateDrift,
    UnknownOpcode,
    PayloadOversize,
    UnknownClient,
    EndpointMismatch,
    CapabilityRevoked,
    RightsMissing,
    ServiceNotReady,
    ServiceFailed,
    ClientRateLimited,
    SequenceOutOfOrder,
    SequenceOverflow,
    InvalidTransition,
    BackoffNotElapsed,
    ClientTableFull,
    DuplicateClient,
    EmptyRights,
    InvalidRights,
    TickOverflow,
    ZeroTick,
}

impl G8lS560ModemSupervisionError {
    pub const fn diagnostic_code(self) -> u64 {
        match self {
            Self::StepOutOfOrder => 1,
            Self::LedgerFull => 2,
            Self::PublishedStateDrift => 3,
            Self::UnknownOpcode => 4,
            Self::PayloadOversize => 5,
            Self::UnknownClient => 6,
            Self::EndpointMismatch => 7,
            Self::CapabilityRevoked => 8,
            Self::RightsMissing => 9,
            Self::ServiceNotReady => 10,
            Self::ServiceFailed => 11,
            Self::ClientRateLimited => 12,
            Self::SequenceOutOfOrder => 13,
            Self::SequenceOverflow => 14,
            Self::InvalidTransition => 15,
            Self::BackoffNotElapsed => 16,
            Self::ClientTableFull => 17,
            Self::DuplicateClient => 18,
            Self::EmptyRights => 19,
            Self::InvalidRights => 20,
            Self::TickOverflow => 21,
            Self::ZeroTick => 22,
        }
    }
}

/// Deterministic FNV-1a digest of a request payload.  The multiply wraps by
/// definition of the hash; it is not an arithmetic budget.
pub fn s560_payload_digest(payload: &[u8]) -> u32 {
    let mut digest = FNV1A_OFFSET;
    for byte in payload {
        digest ^= u32::from(*byte);
        digest = digest.wrapping_mul(FNV1A_PRIME);
    }
    digest
}

/// Builds a bounded request.  Unknown opcodes and payloads above 256 bytes
/// fail closed before any state is touched.
pub fn encode_s560_request(
    client_id: u8,
    endpoint_id: u16,
    opcode: u8,
    sequence: u32,
    payload: &[u8],
) -> Result<G8lS560ModemRequest, G8lS560ModemSupervisionError> {
    if G8lS560ModemOpcode::from_wire(opcode).is_none() {
        return Err(G8lS560ModemSupervisionError::UnknownOpcode);
    }
    if payload.len() > S560_PAYLOAD_MAX_BYTES {
        return Err(G8lS560ModemSupervisionError::PayloadOversize);
    }
    Ok(G8lS560ModemRequest {
        client_id,
        endpoint_id,
        opcode,
        sequence,
        payload_len: payload.len(),
        payload_digest: s560_payload_digest(payload),
    })
}

/// Exponential backoff for the n-th restart inside the window: 1, 2, 4 ticks,
/// capped at `S560_BACKOFF_MAX_TICKS`.
pub const fn s560_backoff_ticks(restarts_in_window: u8) -> u32 {
    if restarts_in_window == 0 {
        return 0;
    }
    let shift = restarts_in_window - 1;
    if shift >= 31 {
        return S560_BACKOFF_MAX_TICKS;
    }
    let ticks = S560_BACKOFF_BASE_TICKS << shift;
    if ticks > S560_BACKOFF_MAX_TICKS {
        S560_BACKOFF_MAX_TICKS
    } else {
        ticks
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ClientSlot {
    capability: G8lS560ModemEndpointCapability,
    last_sequence: u32,
    rate_window: u32,
    rate_count: u8,
}

#[derive(Clone, Debug)]
pub struct G8lS560ModemSupervisionState {
    service: G8lS560ModemServiceState,
    tick: u32,
    heartbeat_misses: u8,
    restart_ticks: [Option<u32>; S560_RESTART_BUDGET_MAX as usize],
    restarts_in_window: u8,
    backoff_ticks: u32,
    restarting_since: u32,
    clients: [Option<ClientSlot>; S560_MAX_CLIENTS],
    ledger: Vec<(G8lS560ModemEvent, G8lS560ModemSupervisionReceipt)>,
}

impl G8lS560ModemSupervisionState {
    pub const fn new() -> Self {
        Self {
            service: G8lS560ModemServiceState::Stopped,
            tick: 0,
            heartbeat_misses: 0,
            restart_ticks: [None; S560_RESTART_BUDGET_MAX as usize],
            restarts_in_window: 0,
            backoff_ticks: 0,
            restarting_since: 0,
            clients: [None; S560_MAX_CLIENTS],
            ledger: Vec::new(),
        }
    }

    pub const fn service(&self) -> G8lS560ModemServiceState {
        self.service
    }

    pub const fn tick(&self) -> u32 {
        self.tick
    }

    pub const fn heartbeat_misses(&self) -> u8 {
        self.heartbeat_misses
    }

    pub fn step_count(&self) -> usize {
        self.ledger.len()
    }

    pub fn receipt(&self, step: usize) -> Option<G8lS560ModemSupervisionReceipt> {
        self.ledger.get(step).map(|(_, receipt)| *receipt)
    }

    pub fn capability(&self, client_id: u8) -> Option<G8lS560ModemEndpointCapability> {
        self.clients
            .iter()
            .flatten()
            .find(|slot| slot.capability.client_id == client_id)
            .map(|slot| slot.capability)
    }

    fn slot_index(&self, client_id: u8) -> Option<usize> {
        self.clients
            .iter()
            .position(|slot| matches!(slot, Some(slot) if slot.capability.client_id == client_id))
    }

    fn capability_counts(&self) -> (u8, u8) {
        let mut active = 0u8;
        let mut revoked = 0u8;
        for slot in self.clients.iter().flatten() {
            if slot.capability.revoked {
                revoked = revoked.saturating_add(1);
            } else {
                active = active.saturating_add(1);
            }
        }
        (active, revoked)
    }

    fn restarts_within_window(&self, now: u32) -> u8 {
        let floor = now.saturating_sub(S560_RESTART_WINDOW_TICKS);
        let mut count = 0u8;
        for recorded in self.restart_ticks.iter().flatten() {
            if *recorded >= floor {
                count = count.saturating_add(1);
            }
        }
        count
    }

    fn revoke_all(&mut self) {
        for slot in self.clients.iter_mut().flatten() {
            slot.capability.revoked = true;
        }
    }
}

impl Default for G8lS560ModemSupervisionState {
    fn default() -> Self {
        Self::new()
    }
}

/// Applies one supervisor/client event at ledger position `step`.  Every
/// invalid input returns `Err` and leaves the state untouched; an exact
/// replay of a published step returns `StepRetained` with the same receipt.
pub fn service_s560_model_supervise(
    state: &mut G8lS560ModemSupervisionState,
    step: usize,
    event: G8lS560ModemEvent,
) -> Result<G8lS560ModemSupervisionOutcome, G8lS560ModemSupervisionError> {
    if let Some((published_event, published_receipt)) = state.ledger.get(step) {
        if *published_event != event {
            return Err(G8lS560ModemSupervisionError::PublishedStateDrift);
        }
        return Ok(G8lS560ModemSupervisionOutcome::StepRetained(
            *published_receipt,
        ));
    }
    if step != state.ledger.len() {
        return Err(G8lS560ModemSupervisionError::StepOutOfOrder);
    }
    if state.ledger.len() >= S560_MAX_STEPS {
        return Err(G8lS560ModemSupervisionError::LedgerFull);
    }

    let from_state = state.service;
    let mut next = state.clone();
    let mut response = None;

    match event {
        G8lS560ModemEvent::GrantCapability { client_id, rights } => {
            if next.service == G8lS560ModemServiceState::Failed {
                return Err(G8lS560ModemSupervisionError::ServiceFailed);
            }
            if rights == 0 {
                return Err(G8lS560ModemSupervisionError::EmptyRights);
            }
            if rights & !S560_RIGHTS_ALL != 0 {
                return Err(G8lS560ModemSupervisionError::InvalidRights);
            }
            match next.slot_index(client_id) {
                Some(index) => {
                    let slot = next.clients[index]
                        .as_mut()
                        .ok_or(G8lS560ModemSupervisionError::UnknownClient)?;
                    if !slot.capability.revoked {
                        return Err(G8lS560ModemSupervisionError::DuplicateClient);
                    }
                    let generation = slot
                        .capability
                        .generation
                        .checked_add(1)
                        .ok_or(G8lS560ModemSupervisionError::SequenceOverflow)?;
                    slot.capability = G8lS560ModemEndpointCapability {
                        endpoint_id: slot.capability.endpoint_id,
                        client_id,
                        rights,
                        generation,
                        revoked: false,
                    };
                }
                None => {
                    let index = next
                        .clients
                        .iter()
                        .position(Option::is_none)
                        .ok_or(G8lS560ModemSupervisionError::ClientTableFull)?;
                    let endpoint_id = S560_ENDPOINT_ID_BASE
                        .checked_add(index as u16)
                        .ok_or(G8lS560ModemSupervisionError::SequenceOverflow)?;
                    next.clients[index] = Some(ClientSlot {
                        capability: G8lS560ModemEndpointCapability {
                            endpoint_id,
                            client_id,
                            rights,
                            generation: 1,
                            revoked: false,
                        },
                        last_sequence: 0,
                        rate_window: 0,
                        rate_count: 0,
                    });
                }
            }
        }
        G8lS560ModemEvent::RevokeCapability { client_id } => {
            let index = next
                .slot_index(client_id)
                .ok_or(G8lS560ModemSupervisionError::UnknownClient)?;
            let slot = next.clients[index]
                .as_mut()
                .ok_or(G8lS560ModemSupervisionError::UnknownClient)?;
            if slot.capability.revoked {
                return Err(G8lS560ModemSupervisionError::CapabilityRevoked);
            }
            slot.capability.revoked = true;
        }
        G8lS560ModemEvent::Start => match next.service {
            G8lS560ModemServiceState::Stopped => {
                next.service = G8lS560ModemServiceState::Starting;
                next.heartbeat_misses = 0;
            }
            G8lS560ModemServiceState::Restarting => {
                let elapsed = next
                    .tick
                    .checked_sub(next.restarting_since)
                    .ok_or(G8lS560ModemSupervisionError::TickOverflow)?;
                if elapsed < next.backoff_ticks {
                    return Err(G8lS560ModemSupervisionError::BackoffNotElapsed);
                }
                next.service = G8lS560ModemServiceState::Starting;
                next.heartbeat_misses = 0;
            }
            G8lS560ModemServiceState::Failed => {
                return Err(G8lS560ModemSupervisionError::ServiceFailed)
            }
            _ => return Err(G8lS560ModemSupervisionError::InvalidTransition),
        },
        G8lS560ModemEvent::ReadyReported => match next.service {
            G8lS560ModemServiceState::Starting => {
                next.service = G8lS560ModemServiceState::Ready;
                next.heartbeat_misses = 0;
            }
            G8lS560ModemServiceState::Failed => {
                return Err(G8lS560ModemSupervisionError::ServiceFailed)
            }
            _ => return Err(G8lS560ModemSupervisionError::InvalidTransition),
        },
        G8lS560ModemEvent::Stop => match next.service {
            G8lS560ModemServiceState::Starting
            | G8lS560ModemServiceState::Ready
            | G8lS560ModemServiceState::Degraded => {
                next.service = G8lS560ModemServiceState::Stopped;
                next.heartbeat_misses = 0;
            }
            G8lS560ModemServiceState::Failed => {
                return Err(G8lS560ModemSupervisionError::ServiceFailed)
            }
            _ => return Err(G8lS560ModemSupervisionError::InvalidTransition),
        },
        G8lS560ModemEvent::Heartbeat { seen } => {
            match next.service {
                G8lS560ModemServiceState::Ready | G8lS560ModemServiceState::Degraded => {}
                G8lS560ModemServiceState::Failed => {
                    return Err(G8lS560ModemSupervisionError::ServiceFailed)
                }
                _ => return Err(G8lS560ModemSupervisionError::InvalidTransition),
            }
            next.tick = next
                .tick
                .checked_add(1)
                .ok_or(G8lS560ModemSupervisionError::TickOverflow)?;
            if seen {
                next.heartbeat_misses = 0;
                next.service = G8lS560ModemServiceState::Ready;
            } else {
                next.heartbeat_misses = next
                    .heartbeat_misses
                    .checked_add(1)
                    .ok_or(G8lS560ModemSupervisionError::TickOverflow)?;
                if next.heartbeat_misses >= S560_HEARTBEAT_MISS_LIMIT {
                    let now = next.tick;
                    let in_window = next.restarts_within_window(now);
                    if in_window >= S560_RESTART_BUDGET_MAX {
                        next.service = G8lS560ModemServiceState::Failed;
                        next.restarts_in_window = in_window;
                        next.backoff_ticks = 0;
                        next.revoke_all();
                    } else {
                        let restarts = in_window
                            .checked_add(1)
                            .ok_or(G8lS560ModemSupervisionError::TickOverflow)?;
                        let floor = now.saturating_sub(S560_RESTART_WINDOW_TICKS);
                        let slot = next
                            .restart_ticks
                            .iter()
                            .position(|recorded| match recorded {
                                None => true,
                                Some(recorded) => *recorded < floor,
                            })
                            .ok_or(G8lS560ModemSupervisionError::InvalidTransition)?;
                        next.restart_ticks[slot] = Some(now);
                        next.restarts_in_window = restarts;
                        next.backoff_ticks = s560_backoff_ticks(restarts);
                        next.restarting_since = now;
                        next.service = G8lS560ModemServiceState::Restarting;
                    }
                } else {
                    next.service = G8lS560ModemServiceState::Degraded;
                }
            }
        }
        G8lS560ModemEvent::Tick { ticks } => {
            if ticks == 0 {
                return Err(G8lS560ModemSupervisionError::ZeroTick);
            }
            next.tick = next
                .tick
                .checked_add(ticks)
                .ok_or(G8lS560ModemSupervisionError::TickOverflow)?;
        }
        G8lS560ModemEvent::Request(request) => {
            let opcode = G8lS560ModemOpcode::from_wire(request.opcode)
                .ok_or(G8lS560ModemSupervisionError::UnknownOpcode)?;
            if request.payload_len > S560_PAYLOAD_MAX_BYTES {
                return Err(G8lS560ModemSupervisionError::PayloadOversize);
            }
            let index = next
                .slot_index(request.client_id)
                .ok_or(G8lS560ModemSupervisionError::UnknownClient)?;
            let slot = next.clients[index]
                .as_mut()
                .ok_or(G8lS560ModemSupervisionError::UnknownClient)?;
            if slot.capability.endpoint_id != request.endpoint_id {
                return Err(G8lS560ModemSupervisionError::EndpointMismatch);
            }
            let status = match next.service {
                G8lS560ModemServiceState::Ready => G8lS560ModemResponseStatus::Accepted,
                G8lS560ModemServiceState::Degraded => G8lS560ModemResponseStatus::AcceptedDegraded,
                G8lS560ModemServiceState::Failed => {
                    return Err(G8lS560ModemSupervisionError::ServiceFailed)
                }
                _ => return Err(G8lS560ModemSupervisionError::ServiceNotReady),
            };
            if slot.capability.revoked {
                return Err(G8lS560ModemSupervisionError::CapabilityRevoked);
            }
            if !slot.capability.grants(opcode.required_right()) {
                return Err(G8lS560ModemSupervisionError::RightsMissing);
            }
            let window = next.tick / S560_RATE_WINDOW_TICKS;
            let count = if slot.rate_window == window {
                slot.rate_count
            } else {
                0
            };
            if count >= S560_CLIENT_RATE_LIMIT {
                return Err(G8lS560ModemSupervisionError::ClientRateLimited);
            }
            if slot.last_sequence == u32::MAX {
                return Err(G8lS560ModemSupervisionError::SequenceOverflow);
            }
            if request.sequence < S560_SEQUENCE_FIRST || request.sequence <= slot.last_sequence {
                return Err(G8lS560ModemSupervisionError::SequenceOutOfOrder);
            }
            slot.rate_window = window;
            slot.rate_count = count
                .checked_add(1)
                .ok_or(G8lS560ModemSupervisionError::ClientRateLimited)?;
            slot.last_sequence = request.sequence;
            response = Some(G8lS560ModemResponse {
                endpoint_id: request.endpoint_id,
                opcode: request.opcode,
                sequence: request.sequence,
                status,
                payload_len: opcode.response_payload_len(request.payload_len),
            });
        }
    }

    let (active_capabilities, revoked_capabilities) = next.capability_counts();
    let receipt = G8lS560ModemSupervisionReceipt {
        sequence: S560_SEQUENCE,
        predecessor_sequence: S560_EXPECTED_PREDECESSOR,
        r1_stage: S560_R1_STAGE,
        step,
        event_kind: event.kind(),
        tick: next.tick,
        from_state,
        to_state: next.service,
        heartbeat_misses: next.heartbeat_misses,
        restarts_in_window: next.restarts_in_window,
        backoff_ticks: next.backoff_ticks,
        active_capabilities,
        revoked_capabilities,
        response,
        hardware_present: S560_HARDWARE_PRESENT,
        physical_observations: S560_PHYSICAL_OBSERVATIONS,
        runbook_executed: RUNBOOK_EXECUTED_IN_S560,
    };
    next.ledger.push((event, receipt));
    *state = next;
    Ok(G8lS560ModemSupervisionOutcome::StepPublished(receipt))
}
snippet sha256: e9beae450e35file sha256: e9beae450e35
02 · Doğrulayan test kodu

Operations komutuna bağlı focused test

tam dosyaL1–L819
simulation/tests/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s560_r1_modem_subsystem_capability_supervision_model.rs::S560 r1 modem subsystem capability supervision model focused tests
use aselsan_microkernel_simulation::g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s560_r1_modem_subsystem_capability_supervision_model::*;
use std::collections::BTreeSet;

const SOURCE: &str = include_str!(
    "../../kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s560_r1_modem_subsystem_capability_supervision_model.rs"
);
const MAIN: &str = include_str!("../../kernel/src/main.rs");
const SIMULATION_LIB: &str = include_str!("../src/lib.rs");

const CLIENT: u8 = 1;
const ENDPOINT: u16 = S560_ENDPOINT_ID_BASE;

type State = G8lS560ModemSupervisionState;
type Event = G8lS560ModemEvent;
type Outcome = G8lS560ModemSupervisionOutcome;
type Error = G8lS560ModemSupervisionError;
type Service = G8lS560ModemServiceState;

fn publish(state: &mut State, step: usize, event: Event) -> G8lS560ModemSupervisionReceipt {
    match service_s560_model_supervise(state, step, event) {
        Ok(Outcome::StepPublished(receipt)) => receipt,
        other => panic!("step {step} must publish, got {other:?}"),
    }
}

fn request(client_id: u8, endpoint_id: u16, opcode: u8, sequence: u32, payload: &[u8]) -> Event {
    Event::Request(encode_s560_request(client_id, endpoint_id, opcode, sequence, payload).unwrap())
}

/// Grant client 1 all rights, start, report ready.  Returns the next step.
fn bring_up(state: &mut State) -> usize {
    publish(
        state,
        0,
        Event::GrantCapability {
            client_id: CLIENT,
            rights: S560_RIGHTS_ALL,
        },
    );
    publish(state, 1, Event::Start);
    let receipt = publish(state, 2, Event::ReadyReported);
    assert_eq!(receipt.to_state, Service::Ready);
    3
}

/// Miss three heartbeats from Ready/Degraded; returns the next step and the
/// receipt of the third miss.
fn miss_three(state: &mut State, mut step: usize) -> (usize, G8lS560ModemSupervisionReceipt) {
    let mut last = None;
    for _ in 0..S560_HEARTBEAT_MISS_LIMIT {
        last = Some(publish(state, step, Event::Heartbeat { seen: false }));
        step += 1;
    }
    (step, last.unwrap())
}

#[test]
fn sequence_scope_and_nonpromotion_are_exact() {
    assert_eq!(S560_SEQUENCE, 560);
    assert_eq!(S560_EXPECTED_PREDECESSOR, 559);
    assert_eq!(S560_R1_STAGE, 3);
    assert_eq!(S560_R1_RANGE_FIRST, 536);
    assert_eq!(S560_R1_RANGE_LAST, 568);
    assert_eq!(S560_RIGHTS_ALL, 0x1f);
    assert_eq!(S560_RIGHT_COUNT, 5);
    assert_eq!(S560_OPCODE_COUNT, 8);
    assert_eq!(S560_PAYLOAD_MAX_BYTES, 256);
    assert_eq!(S560_HEARTBEAT_MISS_LIMIT, 3);
    assert_eq!(S560_RESTART_BUDGET_MAX, 3);
    assert_eq!(S560_RESTART_WINDOW_TICKS, 60);
    assert_eq!(S560_BACKOFF_BASE_TICKS, 1);
    assert_eq!(S560_BACKOFF_MAX_TICKS, 8);
    assert_eq!(S560_CLIENT_RATE_LIMIT, 8);
    assert_eq!(S560_RATE_WINDOW_TICKS, 10);
    assert_eq!(S560_MAX_CLIENTS, 4);
    assert_eq!(S560_MAX_STEPS, 64);
    assert_eq!(S560_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS, 0);
    assert_eq!(S560_PHYSICAL_OBSERVATIONS, 0);
    assert_eq!(S560_PHYSICAL_OR_DEVICE_OPERATIONS, 0);
    assert_eq!(S560_SD_WRITES, 0);
    assert_eq!(S560_UART_OPENS, 0);
    assert_eq!(S560_POWER_TRANSITIONS, 0);
    assert_eq!(S560_NEW_IMMUTABLE_RAW_CAPTURES, 0);
    assert!(S560_S540_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(S560_S543_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(!S560_AUTOMATIC_PROMOTION);
    assert!(!S560_BOOT_TO_UI_PHYSICALLY_OBSERVED);
    assert!(!S560_HARDWARE_PRESENT);
    assert!(!S560_R1_ACCEPTANCE_COMPLETE);
    assert!(!RUNBOOK_EXECUTED_IN_S560);
}

#[test]
fn module_is_registered_in_kernel_and_simulation() {
    let module = "g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s560_r1_modem_subsystem_capability_supervision_model";
    assert!(MAIN.contains(&format!("mod {module};")));
    assert!(SIMULATION_LIB.contains(&format!("pub mod {module};")));
}

#[test]
fn source_has_no_device_execution_or_uart_emission_surface() {
    for forbidden in [
        "unsafe",
        "asm!",
        "write_volatile",
        "crate::uart",
        "crate::arch",
        "#[no_mangle]",
        "spin::",
        "std::",
        "kprintln!",
    ] {
        assert!(!SOURCE.contains(forbidden), "forbidden token: {forbidden}");
    }
    assert!(SOURCE.contains("no production callsite"));
    assert!(SOURCE.contains("performs no\n//! device operation"));
    assert!(SOURCE.contains("S560_HARDWARE_PRESENT: bool = false"));
    assert!(SOURCE.contains("RUNBOOK_EXECUTED_IN_S560: bool = false"));
}

#[test]
fn diagnostic_codes_are_nonzero_and_unique() {
    let errors = [
        Error::StepOutOfOrder,
        Error::LedgerFull,
        Error::PublishedStateDrift,
        Error::UnknownOpcode,
        Error::PayloadOversize,
        Error::UnknownClient,
        Error::EndpointMismatch,
        Error::CapabilityRevoked,
        Error::RightsMissing,
        Error::ServiceNotReady,
        Error::ServiceFailed,
        Error::ClientRateLimited,
        Error::SequenceOutOfOrder,
        Error::SequenceOverflow,
        Error::InvalidTransition,
        Error::BackoffNotElapsed,
        Error::ClientTableFull,
        Error::DuplicateClient,
        Error::EmptyRights,
        Error::InvalidRights,
        Error::TickOverflow,
        Error::ZeroTick,
    ];
    let codes: BTreeSet<_> = errors.into_iter().map(Error::diagnostic_code).collect();
    assert_eq!(codes.len(), errors.len());
    assert_eq!(codes.len(), 22);
    assert!(!codes.contains(&0));
}

#[test]
fn exact_replay_retains_the_same_receipt() {
    let mut state = State::new();
    let step = bring_up(&mut state);
    let event = request(CLIENT, ENDPOINT, 0x01, 1, b"AT+CSQ");
    let receipt = publish(&mut state, step, event);
    assert_eq!(
        service_s560_model_supervise(&mut state, step, event),
        Ok(Outcome::StepRetained(receipt))
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, 1, Event::Start),
        Ok(Outcome::StepRetained(state.receipt(1).unwrap()))
    );
    assert_eq!(state.step_count(), step + 1);
    assert_eq!(state.receipt(step), Some(receipt));
}

#[test]
fn divergent_input_after_publication_fails_closed() {
    let mut state = State::new();
    let step = bring_up(&mut state);
    publish(&mut state, step, request(CLIENT, ENDPOINT, 0x01, 1, b"AT"));
    assert_eq!(
        service_s560_model_supervise(
            &mut state,
            step,
            request(CLIENT, ENDPOINT, 0x01, 1, b"AT+CSQ")
        ),
        Err(Error::PublishedStateDrift)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, 1, Event::Stop),
        Err(Error::PublishedStateDrift)
    );
    assert_eq!(state.step_count(), step + 1);
    assert_eq!(state.service(), Service::Ready);
}

#[test]
fn opcode_table_binds_each_opcode_to_one_right_and_bounded_response() {
    let mut wires = BTreeSet::new();
    for opcode in G8lS560ModemOpcode::ALL {
        assert_eq!(G8lS560ModemOpcode::from_wire(opcode.wire()), Some(opcode));
        assert!(wires.insert(opcode.wire()));
        let right = opcode.required_right();
        assert_eq!(right.count_ones(), 1);
        assert_eq!(right & S560_RIGHTS_ALL, right);
        assert!(opcode.response_payload_len(S560_PAYLOAD_MAX_BYTES) <= S560_PAYLOAD_MAX_BYTES);
    }
    assert_eq!(wires.len(), S560_OPCODE_COUNT);
    for unknown in [0x00u8, 0x02, 0x12, 0x22, 0x32, 0x41, 0x7f, 0xff] {
        assert_eq!(G8lS560ModemOpcode::from_wire(unknown), None);
    }
    assert_eq!(G8lS560ModemOpcode::AtCommand.required_right(), S560_RIGHT_AT);
    assert_eq!(G8lS560ModemOpcode::SmsSend.required_right(), S560_RIGHT_SMS);
    assert_eq!(G8lS560ModemOpcode::DataAttach.required_right(), S560_RIGHT_DATA);
    assert_eq!(G8lS560ModemOpcode::VoiceDial.required_right(), S560_RIGHT_VOICE);
    assert_eq!(G8lS560ModemOpcode::AudioRoute.required_right(), S560_RIGHT_AUDIO);
    assert_eq!(
        S560_RIGHT_AT | S560_RIGHT_SMS | S560_RIGHT_DATA | S560_RIGHT_VOICE | S560_RIGHT_AUDIO,
        S560_RIGHTS_ALL
    );
    assert_eq!(s560_payload_digest(b""), 0x811c_9dc5);
    assert_ne!(s560_payload_digest(b"AT"), s560_payload_digest(b"TA"));
}

#[test]
fn backoff_table_is_exponential_and_capped() {
    assert_eq!(s560_backoff_ticks(0), 0);
    assert_eq!(s560_backoff_ticks(1), 1);
    assert_eq!(s560_backoff_ticks(2), 2);
    assert_eq!(s560_backoff_ticks(3), 4);
    assert_eq!(s560_backoff_ticks(4), 8);
    assert_eq!(s560_backoff_ticks(5), 8);
    assert_eq!(s560_backoff_ticks(u8::MAX), S560_BACKOFF_MAX_TICKS);
}

#[test]
fn stopped_to_ready_bring_up_publishes_receipts_in_order() {
    let mut state = State::new();
    assert_eq!(state.service(), Service::Stopped);
    let grant = publish(
        &mut state,
        0,
        Event::GrantCapability {
            client_id: CLIENT,
            rights: S560_RIGHT_AT | S560_RIGHT_SMS,
        },
    );
    assert_eq!(grant.event_kind, 1);
    assert_eq!((grant.from_state, grant.to_state), (Service::Stopped, Service::Stopped));
    assert_eq!(grant.active_capabilities, 1);
    assert_eq!(
        state.capability(CLIENT),
        Some(G8lS560ModemEndpointCapability {
            endpoint_id: ENDPOINT,
            client_id: CLIENT,
            rights: S560_RIGHT_AT | S560_RIGHT_SMS,
            generation: 1,
            revoked: false,
        })
    );
    let start = publish(&mut state, 1, Event::Start);
    assert_eq!((start.from_state, start.to_state), (Service::Stopped, Service::Starting));
    let ready = publish(&mut state, 2, Event::ReadyReported);
    assert_eq!((ready.from_state, ready.to_state), (Service::Starting, Service::Ready));
    assert_eq!(ready.sequence, S560_SEQUENCE);
    assert_eq!(ready.predecessor_sequence, S560_EXPECTED_PREDECESSOR);
    assert_eq!(ready.r1_stage, 3);
    assert_eq!(ready.step, 2);
    assert_eq!(ready.tick, 0);
    assert_eq!(ready.heartbeat_misses, 0);
    assert_eq!(ready.restarts_in_window, 0);
    assert_eq!(ready.backoff_ticks, 0);
    assert_eq!(ready.response, None);
    assert!(!ready.hardware_present);
    assert_eq!(ready.physical_observations, 0);
    assert!(!ready.runbook_executed);
    let seen = publish(&mut state, 3, Event::Heartbeat { seen: true });
    assert_eq!(seen.tick, 1);
    assert_eq!(seen.to_state, Service::Ready);
    let stop = publish(&mut state, 4, Event::Stop);
    assert_eq!(stop.to_state, Service::Stopped);
    assert_eq!(state.step_count(), 5);
}

#[test]
fn granted_capability_accepts_matching_request_and_echoes_bounded_response() {
    let mut state = State::new();
    let step = bring_up(&mut state);
    let receipt = publish(&mut state, step, request(CLIENT, ENDPOINT, 0x01, 1, b"AT+CSQ"));
    assert_eq!(receipt.event_kind, 8);
    assert_eq!(
        receipt.response,
        Some(G8lS560ModemResponse {
            endpoint_id: ENDPOINT,
            opcode: 0x01,
            sequence: 1,
            status: G8lS560ModemResponseStatus::Accepted,
            payload_len: 6,
        })
    );
    let sms = publish(&mut state, step + 1, request(CLIENT, ENDPOINT, 0x11, 2, b""));
    assert_eq!(sms.response.unwrap().payload_len, 176);
    let detach = publish(&mut state, step + 2, request(CLIENT, ENDPOINT, 0x21, 3, b"x"));
    assert_eq!(detach.response.unwrap().payload_len, 0);
    let dial = publish(&mut state, step + 3, request(CLIENT, ENDPOINT, 0x30, 4, b"+90"));
    assert_eq!(dial.response.unwrap().payload_len, 8);
    let audio = publish(&mut state, step + 4, request(CLIENT, ENDPOINT, 0x40, 5, b"ear"));
    assert_eq!(audio.response.unwrap().payload_len, 4);
    assert_eq!(audio.to_state, Service::Ready);
    assert_eq!(audio.tick, 0);
}

#[test]
fn unknown_opcode_missing_right_and_oversize_payload_fail_closed() {
    let mut state = State::new();
    publish(
        &mut state,
        0,
        Event::GrantCapability {
            client_id: CLIENT,
            rights: S560_RIGHT_AT,
        },
    );
    publish(&mut state, 1, Event::Start);
    publish(&mut state, 2, Event::ReadyReported);
    assert_eq!(
        encode_s560_request(CLIENT, ENDPOINT, 0xff, 1, b""),
        Err(Error::UnknownOpcode)
    );
    let raw_unknown = G8lS560ModemRequest {
        client_id: CLIENT,
        endpoint_id: ENDPOINT,
        opcode: 0x02,
        sequence: 1,
        payload_len: 0,
        payload_digest: 0,
    };
    assert_eq!(
        service_s560_model_supervise(&mut state, 3, Event::Request(raw_unknown)),
        Err(Error::UnknownOpcode)
    );
    for opcode in [0x10u8, 0x20, 0x30, 0x40] {
        assert_eq!(
            service_s560_model_supervise(&mut state, 3, request(CLIENT, ENDPOINT, opcode, 1, b"")),
            Err(Error::RightsMissing)
        );
    }
    let oversize = [0u8; S560_PAYLOAD_MAX_BYTES + 1];
    assert_eq!(
        encode_s560_request(CLIENT, ENDPOINT, 0x01, 1, &oversize),
        Err(Error::PayloadOversize)
    );
    let raw_oversize = G8lS560ModemRequest {
        payload_len: S560_PAYLOAD_MAX_BYTES + 1,
        ..raw_unknown
    };
    let raw_oversize = G8lS560ModemRequest {
        opcode: 0x01,
        ..raw_oversize
    };
    assert_eq!(
        service_s560_model_supervise(&mut state, 3, Event::Request(raw_oversize)),
        Err(Error::PayloadOversize)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, 3, request(2, ENDPOINT, 0x01, 1, b"")),
        Err(Error::UnknownClient)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, 3, request(CLIENT, ENDPOINT + 1, 0x01, 1, b"")),
        Err(Error::EndpointMismatch)
    );
    assert_eq!(state.step_count(), 3);
    assert_eq!(state.service(), Service::Ready);
    assert_eq!(state.capability(CLIENT).unwrap().rights, S560_RIGHT_AT);
}

#[test]
fn payload_boundary_256_is_accepted_and_257_rejected() {
    let mut state = State::new();
    let step = bring_up(&mut state);
    let exact = [0xa5u8; S560_PAYLOAD_MAX_BYTES];
    let encoded = encode_s560_request(CLIENT, ENDPOINT, 0x01, 1, &exact).unwrap();
    assert_eq!(encoded.payload_len, 256);
    assert_eq!(encoded.payload_digest, s560_payload_digest(&exact));
    let receipt = publish(&mut state, step, Event::Request(encoded));
    assert_eq!(receipt.response.unwrap().payload_len, 256);
    let over = [0xa5u8; S560_PAYLOAD_MAX_BYTES + 1];
    assert_eq!(
        encode_s560_request(CLIENT, ENDPOINT, 0x01, 2, &over),
        Err(Error::PayloadOversize)
    );
    let empty = publish(&mut state, step + 1, request(CLIENT, ENDPOINT, 0x01, 2, b""));
    assert_eq!(empty.response.unwrap().payload_len, 0);
}

#[test]
fn three_missed_heartbeats_restart_with_exponential_backoff() {
    let mut state = State::new();
    let step = bring_up(&mut state);
    let first = publish(&mut state, step, Event::Heartbeat { seen: false });
    assert_eq!((first.to_state, first.heartbeat_misses, first.tick), (Service::Degraded, 1, 1));
    let recovered = publish(&mut state, step + 1, Event::Heartbeat { seen: true });
    assert_eq!((recovered.to_state, recovered.heartbeat_misses), (Service::Ready, 0));
    let (mut step, restart) = miss_three(&mut state, step + 2);
    assert_eq!(restart.to_state, Service::Restarting);
    assert_eq!(restart.heartbeat_misses, 3);
    assert_eq!(restart.restarts_in_window, 1);
    assert_eq!(restart.backoff_ticks, 1);
    assert_eq!(restart.tick, 5);
    assert_eq!(
        service_s560_model_supervise(&mut state, step, Event::Start),
        Err(Error::BackoffNotElapsed)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, step, Event::Heartbeat { seen: true }),
        Err(Error::InvalidTransition)
    );
    publish(&mut state, step, Event::Tick { ticks: 1 });
    let started = publish(&mut state, step + 1, Event::Start);
    assert_eq!((started.from_state, started.to_state), (Service::Restarting, Service::Starting));
    assert_eq!(started.heartbeat_misses, 0);
    publish(&mut state, step + 2, Event::ReadyReported);
    step += 3;
    let (mut step, second) = miss_three(&mut state, step);
    assert_eq!((second.restarts_in_window, second.backoff_ticks), (2, 2));
    publish(&mut state, step, Event::Tick { ticks: 1 });
    assert_eq!(
        service_s560_model_supervise(&mut state, step + 1, Event::Start),
        Err(Error::BackoffNotElapsed)
    );
    publish(&mut state, step + 1, Event::Tick { ticks: 1 });
    publish(&mut state, step + 2, Event::Start);
    publish(&mut state, step + 3, Event::ReadyReported);
    step += 4;
    let (step, third) = miss_three(&mut state, step);
    assert_eq!((third.restarts_in_window, third.backoff_ticks), (3, 4));
    publish(&mut state, step, Event::Tick { ticks: 3 });
    assert_eq!(
        service_s560_model_supervise(&mut state, step + 1, Event::Start),
        Err(Error::BackoffNotElapsed)
    );
    publish(&mut state, step + 1, Event::Tick { ticks: 1 });
    publish(&mut state, step + 2, Event::Start);
    assert_eq!(state.service(), Service::Starting);
    assert!(state.capability(CLIENT).unwrap().grants(S560_RIGHT_AT));
}

#[test]
fn restart_budget_exhaustion_fails_and_revokes_capabilities() {
    let mut state = State::new();
    let mut step = bring_up(&mut state);
    publish(
        &mut state,
        step,
        Event::GrantCapability {
            client_id: 2,
            rights: S560_RIGHT_VOICE,
        },
    );
    step += 1;
    for expected_backoff in [1u32, 2, 4] {
        let (next, restart) = miss_three(&mut state, step);
        assert_eq!(restart.to_state, Service::Restarting);
        assert_eq!(restart.backoff_ticks, expected_backoff);
        publish(&mut state, next, Event::Tick { ticks: expected_backoff });
        publish(&mut state, next + 1, Event::Start);
        publish(&mut state, next + 2, Event::ReadyReported);
        step = next + 3;
    }
    let (step, failed) = miss_three(&mut state, step);
    assert_eq!(failed.to_state, Service::Failed);
    assert_eq!(failed.restarts_in_window, 3);
    assert_eq!(failed.backoff_ticks, 0);
    assert_eq!(failed.active_capabilities, 0);
    assert_eq!(failed.revoked_capabilities, 2);
    assert!(failed.tick < S560_RESTART_WINDOW_TICKS);
    assert!(state.capability(CLIENT).unwrap().revoked);
    assert!(state.capability(2).unwrap().revoked);
    for event in [
        Event::Start,
        Event::ReadyReported,
        Event::Stop,
        Event::Heartbeat { seen: true },
        Event::GrantCapability {
            client_id: 3,
            rights: S560_RIGHT_AT,
        },
        request(CLIENT, ENDPOINT, 0x01, 1, b"AT"),
    ] {
        assert_eq!(
            service_s560_model_supervise(&mut state, step, event),
            Err(Error::ServiceFailed)
        );
    }
    let tick = publish(&mut state, step, Event::Tick { ticks: 100 });
    assert_eq!(tick.to_state, Service::Failed);
    assert_eq!(
        service_s560_model_supervise(&mut state, step + 1, Event::Start),
        Err(Error::ServiceFailed)
    );
}

#[test]
fn restart_window_expiry_resets_the_budget() {
    let mut state = State::new();
    let mut step = bring_up(&mut state);
    for backoff in [1u32, 2, 4] {
        let (next, _) = miss_three(&mut state, step);
        publish(&mut state, next, Event::Tick { ticks: backoff });
        publish(&mut state, next + 1, Event::Start);
        publish(&mut state, next + 2, Event::ReadyReported);
        step = next + 3;
    }
    let before = state.tick();
    publish(&mut state, step, Event::Tick { ticks: S560_RESTART_WINDOW_TICKS });
    let (next, restart) = miss_three(&mut state, step + 1);
    assert_eq!(restart.to_state, Service::Restarting);
    assert_eq!(restart.restarts_in_window, 1);
    assert_eq!(restart.backoff_ticks, 1);
    assert_eq!(restart.tick, before + S560_RESTART_WINDOW_TICKS + 3);
    assert_eq!(restart.active_capabilities, 1);
    assert_eq!(restart.revoked_capabilities, 0);
    assert_eq!(state.step_count(), next);
}

#[test]
fn per_client_rate_limit_rejects_ninth_request_in_window() {
    let mut state = State::new();
    let mut step = bring_up(&mut state);
    publish(
        &mut state,
        step,
        Event::GrantCapability {
            client_id: 2,
            rights: S560_RIGHT_AT,
        },
    );
    step += 1;
    for sequence in 1..=u32::from(S560_CLIENT_RATE_LIMIT) {
        publish(&mut state, step, request(CLIENT, ENDPOINT, 0x01, sequence, b"AT"));
        step += 1;
    }
    assert_eq!(
        service_s560_model_supervise(&mut state, step, request(CLIENT, ENDPOINT, 0x01, 9, b"AT")),
        Err(Error::ClientRateLimited)
    );
    let other = publish(&mut state, step, request(2, ENDPOINT + 1, 0x01, 1, b"AT"));
    assert_eq!(other.response.unwrap().endpoint_id, ENDPOINT + 1);
    step += 1;
    publish(&mut state, step, Event::Tick { ticks: S560_RATE_WINDOW_TICKS - 1 });
    step += 1;
    assert_eq!(
        service_s560_model_supervise(&mut state, step, request(CLIENT, ENDPOINT, 0x01, 9, b"AT")),
        Err(Error::ClientRateLimited)
    );
    publish(&mut state, step, Event::Tick { ticks: 1 });
    step += 1;
    let ninth = publish(&mut state, step, request(CLIENT, ENDPOINT, 0x01, 9, b"AT"));
    assert_eq!(ninth.tick, S560_RATE_WINDOW_TICKS);
    assert_eq!(ninth.response.unwrap().sequence, 9);
}

#[test]
fn sequence_must_strictly_increase_and_overflow_is_rejected() {
    let mut state = State::new();
    let step = bring_up(&mut state);
    assert_eq!(
        service_s560_model_supervise(&mut state, step, request(CLIENT, ENDPOINT, 0x01, 0, b"")),
        Err(Error::SequenceOutOfOrder)
    );
    publish(&mut state, step, request(CLIENT, ENDPOINT, 0x01, 5, b""));
    for stale in [1u32, 4, 5] {
        assert_eq!(
            service_s560_model_supervise(
                &mut state,
                step + 1,
                request(CLIENT, ENDPOINT, 0x01, stale, b"")
            ),
            Err(Error::SequenceOutOfOrder)
        );
    }
    let last = publish(&mut state, step + 1, request(CLIENT, ENDPOINT, 0x01, u32::MAX, b""));
    assert_eq!(last.response.unwrap().sequence, u32::MAX);
    assert_eq!(
        service_s560_model_supervise(
            &mut state,
            step + 2,
            request(CLIENT, ENDPOINT, 0x01, u32::MAX, b"")
        ),
        Err(Error::SequenceOverflow)
    );
    assert_eq!(state.step_count(), step + 2);
}

#[test]
fn requests_outside_ready_or_degraded_fail_closed() {
    let mut state = State::new();
    publish(
        &mut state,
        0,
        Event::GrantCapability {
            client_id: CLIENT,
            rights: S560_RIGHTS_ALL,
        },
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, 1, request(CLIENT, ENDPOINT, 0x01, 1, b"")),
        Err(Error::ServiceNotReady)
    );
    publish(&mut state, 1, Event::Start);
    assert_eq!(
        service_s560_model_supervise(&mut state, 2, request(CLIENT, ENDPOINT, 0x01, 1, b"")),
        Err(Error::ServiceNotReady)
    );
    publish(&mut state, 2, Event::ReadyReported);
    publish(&mut state, 3, Event::Heartbeat { seen: false });
    assert_eq!(state.service(), Service::Degraded);
    let degraded = publish(&mut state, 4, request(CLIENT, ENDPOINT, 0x20, 1, b"apn"));
    assert_eq!(
        degraded.response.unwrap().status,
        G8lS560ModemResponseStatus::AcceptedDegraded
    );
    assert_eq!(degraded.response.unwrap().payload_len, 16);
    publish(&mut state, 5, Event::Heartbeat { seen: false });
    publish(&mut state, 6, Event::Heartbeat { seen: false });
    assert_eq!(state.service(), Service::Restarting);
    assert_eq!(
        service_s560_model_supervise(&mut state, 7, request(CLIENT, ENDPOINT, 0x01, 2, b"")),
        Err(Error::ServiceNotReady)
    );
    assert_eq!(state.step_count(), 7);
}

#[test]
fn explicit_revocation_and_regrant_rotate_generation() {
    let mut state = State::new();
    let step = bring_up(&mut state);
    let revoked = publish(&mut state, step, Event::RevokeCapability { client_id: CLIENT });
    assert_eq!(revoked.event_kind, 2);
    assert_eq!((revoked.active_capabilities, revoked.revoked_capabilities), (0, 1));
    assert_eq!(
        service_s560_model_supervise(
            &mut state,
            step + 1,
            request(CLIENT, ENDPOINT, 0x01, 1, b"AT")
        ),
        Err(Error::CapabilityRevoked)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, step + 1, Event::RevokeCapability { client_id: CLIENT }),
        Err(Error::CapabilityRevoked)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, step + 1, Event::RevokeCapability { client_id: 9 }),
        Err(Error::UnknownClient)
    );
    let regrant = publish(
        &mut state,
        step + 1,
        Event::GrantCapability {
            client_id: CLIENT,
            rights: S560_RIGHT_SMS,
        },
    );
    assert_eq!((regrant.active_capabilities, regrant.revoked_capabilities), (1, 0));
    assert_eq!(
        state.capability(CLIENT),
        Some(G8lS560ModemEndpointCapability {
            endpoint_id: ENDPOINT,
            client_id: CLIENT,
            rights: S560_RIGHT_SMS,
            generation: 2,
            revoked: false,
        })
    );
    assert_eq!(
        service_s560_model_supervise(
            &mut state,
            step + 2,
            request(CLIENT, ENDPOINT, 0x01, 1, b"AT")
        ),
        Err(Error::RightsMissing)
    );
    let sms = publish(&mut state, step + 2, request(CLIENT, ENDPOINT, 0x10, 1, b"pdu"));
    assert_eq!(sms.response.unwrap().payload_len, 4);
}

#[test]
fn client_table_bounds_and_rights_validation_fail_closed() {
    let mut state = State::new();
    for (step, client_id) in (10u8..10 + S560_MAX_CLIENTS as u8).enumerate() {
        let receipt = publish(
            &mut state,
            step,
            Event::GrantCapability {
                client_id,
                rights: S560_RIGHT_AT,
            },
        );
        assert_eq!(receipt.active_capabilities as usize, step + 1);
        assert_eq!(
            state.capability(client_id).unwrap().endpoint_id,
            S560_ENDPOINT_ID_BASE + step as u16
        );
    }
    let step = S560_MAX_CLIENTS;
    assert_eq!(
        service_s560_model_supervise(
            &mut state,
            step,
            Event::GrantCapability {
                client_id: 99,
                rights: S560_RIGHT_AT,
            }
        ),
        Err(Error::ClientTableFull)
    );
    assert_eq!(
        service_s560_model_supervise(
            &mut state,
            step,
            Event::GrantCapability {
                client_id: 10,
                rights: S560_RIGHT_AT,
            }
        ),
        Err(Error::DuplicateClient)
    );
    assert_eq!(
        service_s560_model_supervise(
            &mut state,
            step,
            Event::GrantCapability {
                client_id: 99,
                rights: 0,
            }
        ),
        Err(Error::EmptyRights)
    );
    for rights in [0x20u8, 0x3f, 0x80, 0xff] {
        assert_eq!(
            service_s560_model_supervise(
                &mut state,
                step,
                Event::GrantCapability {
                    client_id: 99,
                    rights,
                }
            ),
            Err(Error::InvalidRights)
        );
    }
    assert_eq!(state.step_count(), S560_MAX_CLIENTS);
    assert_eq!(state.capability(99), None);
}

#[test]
fn step_out_of_order_and_ledger_full_are_rejected() {
    let mut state = State::new();
    assert_eq!(
        service_s560_model_supervise(&mut state, 1, Event::Tick { ticks: 1 }),
        Err(Error::StepOutOfOrder)
    );
    for step in 0..S560_MAX_STEPS {
        publish(&mut state, step, Event::Tick { ticks: 1 });
    }
    assert_eq!(state.tick(), S560_MAX_STEPS as u32);
    assert_eq!(
        service_s560_model_supervise(&mut state, S560_MAX_STEPS, Event::Tick { ticks: 1 }),
        Err(Error::LedgerFull)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, S560_MAX_STEPS + 1, Event::Tick { ticks: 1 }),
        Err(Error::StepOutOfOrder)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, 7, Event::Tick { ticks: 1 }),
        Ok(Outcome::StepRetained(state.receipt(7).unwrap()))
    );
    assert_eq!(state.step_count(), S560_MAX_STEPS);
}

#[test]
fn invalid_transitions_zero_tick_and_tick_overflow_are_rejected() {
    let mut state = State::new();
    for event in [Event::ReadyReported, Event::Stop, Event::Heartbeat { seen: true }] {
        assert_eq!(
            service_s560_model_supervise(&mut state, 0, event),
            Err(Error::InvalidTransition)
        );
    }
    assert_eq!(
        service_s560_model_supervise(&mut state, 0, Event::Tick { ticks: 0 }),
        Err(Error::ZeroTick)
    );
    publish(&mut state, 0, Event::Start);
    assert_eq!(
        service_s560_model_supervise(&mut state, 1, Event::Start),
        Err(Error::InvalidTransition)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, 1, Event::Heartbeat { seen: false }),
        Err(Error::InvalidTransition)
    );
    publish(&mut state, 1, Event::ReadyReported);
    assert_eq!(
        service_s560_model_supervise(&mut state, 2, Event::ReadyReported),
        Err(Error::InvalidTransition)
    );
    publish(&mut state, 2, Event::Tick { ticks: u32::MAX });
    assert_eq!(state.tick(), u32::MAX);
    assert_eq!(
        service_s560_model_supervise(&mut state, 3, Event::Tick { ticks: 1 }),
        Err(Error::TickOverflow)
    );
    assert_eq!(
        service_s560_model_supervise(&mut state, 3, Event::Heartbeat { seen: true }),
        Err(Error::TickOverflow)
    );
    assert_eq!(state.step_count(), 3);
    assert_eq!(state.service(), Service::Ready);
}
snippet sha256: 1c71dd45bcd7file sha256: 1c71dd45bcd7
03 · Kapı kimlik kaydı

Operations sıra, kimlik ve başlık bağı

tam Operations kaydıL2306–L2364
website/src/lib/operations.ts::g8l-s560-r1-modem-subsystem-capability-supervision-model
  {
    id: "g8l-s560-r1-modem-subsystem-capability-supervision-model",
    date: "2026-08-30",
    sequence: 560,
    status: "passed",
    umbrella_status: "partial",
    title: "S560 · R1 modem: modem alt sistemi capability ve gözetim modeli",
    summary:
      "S560 kaynak/host model kapısı PASS'tir: R1 3. aşama (modem, veri, arama ve ses) için modem alt sistemi, capability-gated bir EL0 servisi ve supervisor watchdog olarak saf bir model biçiminde yazıldı. Model {AT, SMS, DATA, VOICE, AUDIO} haklarıyla endpoint capability'yi, opcode + kesin artan sequence + en çok 256 B payload'lı sınırlı istek/yanıt şemasını, Stopped/Starting/Ready/Degraded/Restarting/Failed durum makinesini, 3 kaçırılan heartbeat sonrası Restarting'i, 60 tick penceresinde en çok 3 restart bütçesini ve 1/2/4 tick üstel backoff'u, Failed'da tüm capability'lerin iptalini ve istemci başına 10 tick'te 8 istek hız sınırını kapsar; bilinmeyen opcode, eksik hak ve büyük payload fail-closed reddedilir. Focused 22/22 PASS'tir. S540 ve S543 fiziksel raw/verdict değişmez RED kalır; hiçbir modem, SIM, UART, panel veya board yoktur; physical observation=0, SD/UART/power/new-raw=0/0/0/0, Boot-to-UI=false ve R1 acceptance=false'dur. RUNBOOK_EXECUTED_IN_S560=NO. S561 host-only izinli uygulama başlatma akışı modeli kapısıdır.",
    evidence: [
      "S560, S559'dan ayrı saf model kernel modülü, 22-test focused binary, proof, status manifest, Operations kaydı ve complete Code kartına sahiptir; production callsite yoktur ve modül hiçbir boot, IRQ, scheduler veya driver yoluna bağlı değildir.",
      "Dar S560 source/host status=PASS; R1 umbrella=PARTIAL ve S540/S543 physical gate status=RED olarak ayrı tutulur.",
      "Endpoint capability hak maskesi AT=0x01, SMS=0x02, DATA=0x04, VOICE=0x08, AUDIO=0x10 ve ALL=0x1f'tir; rights=0 EmptyRights, 0x1f dışındaki her bit InvalidRights ile fail-closed reddedilir.",
      "En çok 4 istemci slotu vardır ve endpoint id'ler 0x5600+slot'tur; canlı istemciye ikinci grant DuplicateClient, beşinci istemci ClientTableFull verir; iptal edilmiş istemciye regrant endpoint id'yi korur ve generation'ı artırır.",
      "Opcode tablosu 8 girişlidir ve her opcode tam bir hak ister: AtCommand=0x01 (AT), SmsSend=0x10/SmsRead=0x11 (SMS), DataAttach=0x20/DataDetach=0x21 (DATA), VoiceDial=0x30/VoiceHangup=0x31 (VOICE), AudioRoute=0x40 (AUDIO); diğer her wire değeri UnknownOpcode'dur.",
      "Payload 256 B ile sınırlıdır: 256 B kabul, 257 B hem encoder'da hem servis adımında PayloadOversize'dır; encoder deterministik FNV-1a digest kaydeder ve istemci sequence'ı kesin artmalıdır (SequenceOutOfOrder, u32::MAX sonrası SequenceOverflow).",
      "Yanıt endpoint id, opcode, sequence, Accepted/AcceptedDegraded ve tablo güdümlü sınırlı uzunluk taşır: AtCommand istek uzunluğunu yansıtır, SmsRead 176, DataAttach 16, VoiceDial 8, SmsSend/AudioRoute 4, DataDetach/VoiceHangup 0.",
      "Durum makinesi Stopped→Starting→Ready, Ready↔Degraded, Degraded→Restarting→Starting ve terminal Failed'dır; heartbeat yalnız Ready/Degraded'da geçerlidir, kaçırılan her heartbeat Degraded'a götürür ve üçüncü ardışık kaçırma restart tetikler.",
      "Restart bütçesi 60 tick penceresinde en çok 3'tür ve n'inci restart 1/2/4 tick (üst sınır 8) üstel backoff ile Restarting'e girer; backoff dolmadan Start BackoffNotElapsed verir, pencere içindeki dördüncü restart servisi Failed yapar ve tüm capability'leri iptal eder, pencere dolunca bütçe sıfırlanır.",
      "İstemci başına hız sınırı 10 tick penceresinde 8 istektir; dokuzuncu istek ClientRateLimited ile reddedilir ve diğer istemciler etkilenmez.",
      "Ledger en çok 64 adım tutar; her kabul edilen adım tick, from/to durum, heartbeat kaçırma, pencere içi restart, backoff, capability sayıları ve opsiyonel yanıt içeren bir receipt üretir; exact replay StepRetained ile aynı receipt'i döndürür, yayınlanmış adımda farklı olay PublishedStateDrift, sıra dışı adım StepOutOfOrder verir.",
      "22 hata kodu sıfırdan farklı ve tekildir; kaynakta unsafe, asm!, write_volatile, crate::uart, crate::arch, #[no_mangle] ve spin:: yüzeyi yoktur ve tick aritmetiği checked_add ile yapılır.",
      "Focused target 1 grup / 22 passed / 0 failed / 0 ignored / 0 filtered verdi.",
      "Implementation 27621 B / e9beae450e35fa8a13e45c8a46258c2452667d5cd138f0f5babf973c826af7d2; focused test 29745 B / 1c71dd45bcd7e547bcc383105e1e29f2f6d3602b599e5822a0d565690b288caf SHA-256'dır.",
      "Proof 6407 B'dir.",
      "S540 immutable raw 20525 B ve S543 immutable raw 20509 B fiziksel RED olarak byte-exact korunur; automatic promotion=false ve rerun=false'dur.",
      "S560 sırasında modem, SIM, AT transport, SD write/read-back/eject, UART open/capture, power transition, fiziksel koşu veya yeni immutable raw üretimi yapılmadı.",
      "RUNBOOK_EXECUTED_IN_S560=NO; supported-profile runtime observations=0, physical observations=0, hardware present=false, Boot-to-UI physically observed=false ve R1 acceptance=false'dur.",
      "S561 yalnız host üzerinde izinli uygulama başlatma akışı modelini yazacaktır; aygıt veya fiziksel koşu yetkisi değildir.",
    ],
    commands: [
      "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s560_r1_modem_subsystem_capability_supervision_model -- --test-threads=1",
    ],
    terminalSessions: [
      {
        id: "s560-focused",
        title: "S560 modem alt sistemi capability/gözetim modeli focused",
        commandLines: [
          "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s560_r1_modem_subsystem_capability_supervision_model -- --test-threads=1",
        ],
        outputLines: [
          "test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s",
          "S560 focused=1 group / 22 passed / 0 failed",
          "hardware=none physical=0 runbook=NO",
        ],
        exitCode: 0,
        outputMode: "complete",
      },
    ],
    terminalSessionsNote:
      "S560 kaynak/host model PASS'tir; supported-profile runtime, modem veya fiziksel PASS değildir. S540 ve S543 RED raw ve kararları değişmez.",
    limitations: [
      "S560 saf bir kaynak/host modelidir; hiçbir donanım/panel/modem/board gözlemi yoktur ve gerçek bir modem servisi, heartbeat'i veya restart'ı gözlenmemiştir.",
      "Modülün production callsite'ı yoktur; gerçek EL0 servisi, IPC endpoint bağlantısı, AT transport ve modem sürücüsü bu kapının dışındadır.",
      "S540 ve S543 fiziksel RED immutable kalır; S546 fiziksel koşusunun kararı bu kapıda varsayılmaz.",
      "BOOT_TO_UI_READY gerçek UART'ta görülmedi; Boot-to-UI ve R1 acceptance false kalır.",
      "S561 host-only izinli uygulama başlatma akışı modelidir; yeni SD/UART/power koşusu ayrı kapı, fresh target revalidation, açık operatör yetkisi ve yeni immutable raw ister.",
    ],
  },
snippet sha256: 23f3f43de2f0file sha256: 9726dbf00f84
Focused test komutu
CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s560_r1_modem_subsystem_capability_supervision_model -- --test-threads=1
proof: docs/M8.1-RPi5-G8l-S560-R1-Modem-Subsystem-Capability-Supervision-Model-Proof.md
Registry schema v5 · generator website/scripts/generate-code-gates.mjs · Tam SHA-256: 3050638b71a684d8f8f947a8a6faa237a17fa8db5dc0db04fb207b668b462af9