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: e9beae450e35…file 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: 1c71dd45bcd7…file 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: 23f3f43de2f0…file 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=1proof: 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