use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::{AgentId, CorrelationContext, OperationId}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExecutionOrigin { AdminDraft, AgentSnapshot, } impl ExecutionOrigin { pub fn validate_agent(self, agent_id: Option<&AgentId>) -> Result<(), ExecutionOriginError> { match (self, agent_id) { (Self::AdminDraft, None) | (Self::AgentSnapshot, Some(_)) => Ok(()), (Self::AdminDraft, Some(_)) => Err(ExecutionOriginError::UnexpectedAgent), (Self::AgentSnapshot, None) => Err(ExecutionOriginError::MissingAgent), } } } #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] pub enum ExecutionOriginError { #[error("admin draft execution cannot carry an agent identity")] UnexpectedAgent, #[error("agent snapshot execution requires an agent identity")] MissingAgent, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExecutionStage { Authorization, InputSchema, InputMapping, RequestPreparation, Admission, Adapter, Upstream, OutputMapping, OutputSchema, MandatoryPersistence, Runtime, } impl ExecutionStage { pub const fn as_str(self) -> &'static str { match self { Self::Authorization => "authorization", Self::InputSchema => "input_schema", Self::InputMapping => "input_mapping", Self::RequestPreparation => "request_preparation", Self::Admission => "admission", Self::Adapter => "adapter", Self::Upstream => "upstream", Self::OutputMapping => "output_mapping", Self::OutputSchema => "output_schema", Self::MandatoryPersistence => "mandatory_persistence", Self::Runtime => "runtime", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Retryability { Never, Safe, AfterDelay, ManualReconcile, RequiresConfirmation, } impl Retryability { pub const fn as_str(self) -> &'static str { match self { Self::Never => "never", Self::Safe => "safe", Self::AfterDelay => "after_delay", Self::ManualReconcile => "manual_reconcile", Self::RequiresConfirmation => "requires_confirmation", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OutcomeCertainty { Certain, OutcomeUnknown, } impl OutcomeCertainty { pub const fn as_str(self) -> &'static str { match self { Self::Certain => "certain", Self::OutcomeUnknown => "outcome_unknown", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExecutionErrorCode { AuthorizationDenied, AuthProfileNotFound, SecretNotFound, SecretInvalid, InputSchemaInvalid, InputMappingInvalid, PreparedRequestInvalid, ExecutionOverloaded, SafetyStoreUnavailable, ProtocolUnsupported, ExecutionModeUnsupported, AdapterConfigurationInvalid, OutboundTargetRejected, UpstreamAuthError, UpstreamNotFound, UpstreamRateLimited, UpstreamServerError, UpstreamStatusError, UpstreamTimeout, UpstreamTransportError, UpstreamRequestTooLarge, UpstreamResponseTooLarge, OutputMappingInvalid, OutputSchemaInvalid, PersistenceUnavailable, RuntimeInternal, ConfirmationRequired, ConfirmationInvalid, IdempotencyInProgress, IdempotencyConflict, IdempotencyOutcomeUnknown, } impl ExecutionErrorCode { pub const ALL: [Self; 31] = [ Self::AuthorizationDenied, Self::AuthProfileNotFound, Self::SecretNotFound, Self::SecretInvalid, Self::InputSchemaInvalid, Self::InputMappingInvalid, Self::PreparedRequestInvalid, Self::ExecutionOverloaded, Self::SafetyStoreUnavailable, Self::ProtocolUnsupported, Self::ExecutionModeUnsupported, Self::AdapterConfigurationInvalid, Self::OutboundTargetRejected, Self::UpstreamAuthError, Self::UpstreamNotFound, Self::UpstreamRateLimited, Self::UpstreamServerError, Self::UpstreamStatusError, Self::UpstreamTimeout, Self::UpstreamTransportError, Self::UpstreamRequestTooLarge, Self::UpstreamResponseTooLarge, Self::OutputMappingInvalid, Self::OutputSchemaInvalid, Self::PersistenceUnavailable, Self::RuntimeInternal, Self::ConfirmationRequired, Self::ConfirmationInvalid, Self::IdempotencyInProgress, Self::IdempotencyConflict, Self::IdempotencyOutcomeUnknown, ]; pub const fn as_str(self) -> &'static str { match self { Self::AuthorizationDenied => "authorization_denied", Self::AuthProfileNotFound => "auth_profile_not_found", Self::SecretNotFound => "secret_not_found", Self::SecretInvalid => "secret_invalid", Self::InputSchemaInvalid => "input_schema_invalid", Self::InputMappingInvalid => "input_mapping_invalid", Self::PreparedRequestInvalid => "prepared_request_invalid", Self::ExecutionOverloaded => "execution_overloaded", Self::SafetyStoreUnavailable => "safety_store_unavailable", Self::ProtocolUnsupported => "protocol_unsupported", Self::ExecutionModeUnsupported => "execution_mode_unsupported", Self::AdapterConfigurationInvalid => "adapter_configuration_invalid", Self::OutboundTargetRejected => "outbound_target_rejected", Self::UpstreamAuthError => "upstream_auth_error", Self::UpstreamNotFound => "upstream_not_found", Self::UpstreamRateLimited => "upstream_rate_limited", Self::UpstreamServerError => "upstream_server_error", Self::UpstreamStatusError => "upstream_status_error", Self::UpstreamTimeout => "upstream_timeout", Self::UpstreamTransportError => "upstream_transport_error", Self::UpstreamRequestTooLarge => "upstream_request_too_large", Self::UpstreamResponseTooLarge => "upstream_response_too_large", Self::OutputMappingInvalid => "output_mapping_invalid", Self::OutputSchemaInvalid => "output_schema_invalid", Self::PersistenceUnavailable => "persistence_unavailable", Self::RuntimeInternal => "runtime_internal", Self::ConfirmationRequired => "confirmation_required", Self::ConfirmationInvalid => "confirmation_invalid", Self::IdempotencyInProgress => "idempotency_in_progress", Self::IdempotencyConflict => "idempotency_conflict", Self::IdempotencyOutcomeUnknown => "idempotency_outcome_unknown", } } pub const fn stage(self) -> ExecutionStage { match self { Self::AuthorizationDenied | Self::AuthProfileNotFound | Self::SecretNotFound | Self::SecretInvalid => ExecutionStage::Authorization, Self::InputSchemaInvalid => ExecutionStage::InputSchema, Self::InputMappingInvalid => ExecutionStage::InputMapping, Self::PreparedRequestInvalid => ExecutionStage::RequestPreparation, Self::ExecutionOverloaded | Self::SafetyStoreUnavailable | Self::ConfirmationRequired | Self::ConfirmationInvalid | Self::IdempotencyInProgress | Self::IdempotencyConflict | Self::IdempotencyOutcomeUnknown => ExecutionStage::Admission, Self::ProtocolUnsupported | Self::ExecutionModeUnsupported | Self::AdapterConfigurationInvalid | Self::OutboundTargetRejected => ExecutionStage::Adapter, Self::UpstreamRequestTooLarge => ExecutionStage::RequestPreparation, Self::UpstreamAuthError | Self::UpstreamNotFound | Self::UpstreamRateLimited | Self::UpstreamServerError | Self::UpstreamStatusError | Self::UpstreamTimeout | Self::UpstreamTransportError | Self::UpstreamResponseTooLarge => ExecutionStage::Upstream, Self::OutputMappingInvalid => ExecutionStage::OutputMapping, Self::OutputSchemaInvalid => ExecutionStage::OutputSchema, Self::PersistenceUnavailable => ExecutionStage::MandatoryPersistence, Self::RuntimeInternal => ExecutionStage::Runtime, } } pub const fn retryability(self) -> Retryability { match self { Self::ExecutionOverloaded | Self::SafetyStoreUnavailable | Self::UpstreamRateLimited | Self::UpstreamServerError | Self::UpstreamTimeout | Self::UpstreamTransportError | Self::PersistenceUnavailable | Self::IdempotencyInProgress => Retryability::AfterDelay, Self::ConfirmationRequired => Retryability::RequiresConfirmation, Self::IdempotencyOutcomeUnknown => Retryability::ManualReconcile, _ => Retryability::Never, } } pub const fn outcome_certainty(self) -> OutcomeCertainty { match self { Self::IdempotencyOutcomeUnknown => OutcomeCertainty::OutcomeUnknown, _ => OutcomeCertainty::Certain, } } pub const fn message(self, locale: ExecutionLocale) -> &'static str { match locale { ExecutionLocale::Ru => self.message_ru(), ExecutionLocale::En => self.message_en(), } } const fn message_ru(self) -> &'static str { match self { Self::AuthorizationDenied => "Выполнение операции запрещено.", Self::AuthProfileNotFound => "Профиль авторизации не найден.", Self::SecretNotFound => "Секрет авторизации не найден.", Self::SecretInvalid => "Секрет авторизации имеет неподходящий формат.", Self::InputSchemaInvalid => "Входные параметры не прошли проверку схемы.", Self::InputMappingInvalid => "Не удалось сопоставить входные параметры.", Self::PreparedRequestInvalid => "Не удалось подготовить корректный API-запрос.", Self::ExecutionOverloaded => "Сервис временно перегружен.", Self::SafetyStoreUnavailable => "Обязательное хранилище безопасности недоступно.", Self::ProtocolUnsupported => "Протокол операции не поддерживается.", Self::ExecutionModeUnsupported => "Режим выполнения не поддерживается.", Self::AdapterConfigurationInvalid => "Конфигурация адаптера некорректна.", Self::OutboundTargetRejected => "Целевой адрес отклонён политикой безопасности.", Self::UpstreamAuthError => "Внешний API отклонил авторизацию.", Self::UpstreamNotFound => "Ресурс внешнего API не найден.", Self::UpstreamRateLimited => "Внешний API ограничил частоту запросов.", Self::UpstreamServerError => "Внешний API временно недоступен.", Self::UpstreamStatusError => "Внешний API вернул ошибочный статус.", Self::UpstreamTimeout => "Истекло время ожидания внешнего API.", Self::UpstreamTransportError => "Не удалось подключиться к внешнему API.", Self::UpstreamRequestTooLarge => "Запрос во внешний API превышает лимит.", Self::UpstreamResponseTooLarge => "Ответ внешнего API превышает лимит.", Self::OutputMappingInvalid => "Не удалось сопоставить ответ внешнего API.", Self::OutputSchemaInvalid => "Ответ не прошёл проверку схемы.", Self::PersistenceUnavailable => "Обязательное сохранение результата недоступно.", Self::RuntimeInternal => "Внутренняя ошибка выполнения.", Self::ConfirmationRequired => "Операция требует подтверждения.", Self::ConfirmationInvalid => "Подтверждение недействительно или истекло.", Self::IdempotencyInProgress => "Операция с этим ключом уже выполняется.", Self::IdempotencyConflict => "Ключ идемпотентности использован с другими параметрами.", Self::IdempotencyOutcomeUnknown => { "Результат предыдущего выполнения неизвестен; автоматический повтор запрещён." } } } const fn message_en(self) -> &'static str { match self { Self::AuthorizationDenied => "Operation execution is denied.", Self::AuthProfileNotFound => "Authorization profile was not found.", Self::SecretNotFound => "Authorization secret was not found.", Self::SecretInvalid => "Authorization secret has an invalid format.", Self::InputSchemaInvalid => "Input does not satisfy the operation schema.", Self::InputMappingInvalid => "Input parameters could not be mapped.", Self::PreparedRequestInvalid => "A valid upstream request could not be prepared.", Self::ExecutionOverloaded => "The service is temporarily overloaded.", Self::SafetyStoreUnavailable => "A mandatory safety store is unavailable.", Self::ProtocolUnsupported => "The operation protocol is unsupported.", Self::ExecutionModeUnsupported => "The execution mode is unsupported.", Self::AdapterConfigurationInvalid => "The adapter configuration is invalid.", Self::OutboundTargetRejected => "The target was rejected by the safety policy.", Self::UpstreamAuthError => "The upstream API rejected authorization.", Self::UpstreamNotFound => "The upstream resource was not found.", Self::UpstreamRateLimited => "The upstream API rate-limited the request.", Self::UpstreamServerError => "The upstream API is temporarily unavailable.", Self::UpstreamStatusError => "The upstream API returned an error status.", Self::UpstreamTimeout => "The upstream API timed out.", Self::UpstreamTransportError => "The upstream API could not be reached.", Self::UpstreamRequestTooLarge => "The upstream request exceeded its limit.", Self::UpstreamResponseTooLarge => "The upstream response exceeded its limit.", Self::OutputMappingInvalid => "The upstream response could not be mapped.", Self::OutputSchemaInvalid => "The output does not satisfy the operation schema.", Self::PersistenceUnavailable => "Mandatory result persistence is unavailable.", Self::RuntimeInternal => "Internal execution failure.", Self::ConfirmationRequired => "The operation requires confirmation.", Self::ConfirmationInvalid => "The confirmation is invalid or expired.", Self::IdempotencyInProgress => "The operation is already running for this key.", Self::IdempotencyConflict => "The idempotency key was used with different input.", Self::IdempotencyOutcomeUnknown => { "The previous outcome is unknown; automatic retry is unsafe." } } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ExecutionLocale { Ru, En, } #[derive(Clone, PartialEq, Eq)] pub struct ConfirmationChallenge { token: String, expires_in_ms: u64, } impl ConfirmationChallenge { fn try_new( token: impl Into, expires_in_ms: u64, ) -> Result { let token = token.into(); if token.is_empty() || token.len() > 4_096 || token.chars().any(char::is_control) { return Err(ExecutionFailureBuildError::InvalidConfirmation); } if !(1..=3_600_000).contains(&expires_in_ms) { return Err(ExecutionFailureBuildError::InvalidConfirmation); } Ok(Self { token, expires_in_ms, }) } pub fn token(&self) -> &str { &self.token } pub fn expires_in_ms(&self) -> u64 { self.expires_in_ms } } #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] pub enum ExecutionFailureBuildError { #[error("failure metadata is incompatible with the execution error code")] IncompatibleMetadata, #[error("confirmation metadata is invalid or exceeds its bound")] InvalidConfirmation, #[error("retry metadata is invalid or exceeds its bound")] InvalidRetryAfter, } impl std::fmt::Debug for ConfirmationChallenge { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter .debug_struct("ConfirmationChallenge") .field("token", &"[REDACTED]") .field("expires_in_ms", &self.expires_in_ms) .finish() } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExecutionFailure { code: ExecutionErrorCode, retryability: Retryability, outcome_certainty: OutcomeCertainty, correlation: CorrelationContext, upstream_status: Option, retry_after_ms: Option, confirmation: Option, } impl ExecutionFailure { pub fn new(code: ExecutionErrorCode, correlation: CorrelationContext) -> Self { Self { code, retryability: code.retryability(), outcome_certainty: code.outcome_certainty(), correlation, upstream_status: None, retry_after_ms: None, confirmation: None, } } pub fn error_code(&self) -> ExecutionErrorCode { self.code } pub fn stage(&self) -> ExecutionStage { self.code.stage() } pub fn retryability(&self) -> Retryability { self.retryability } pub fn outcome_certainty(&self) -> OutcomeCertainty { self.outcome_certainty } pub fn correlation(&self) -> &CorrelationContext { &self.correlation } pub fn upstream_status(&self) -> Option { self.upstream_status } pub fn retry_after_ms(&self) -> Option { self.retry_after_ms } pub fn confirmation(&self) -> Option<&ConfirmationChallenge> { self.confirmation.as_ref() } pub fn with_upstream_status(mut self, status: u16) -> Self { self.upstream_status = (100..=599).contains(&status).then_some(status); self } pub fn try_with_retry_after_ms( mut self, retry_after_ms: u64, ) -> Result { if self.retryability != Retryability::AfterDelay { return Err(ExecutionFailureBuildError::IncompatibleMetadata); } if !(1..=3_600_000).contains(&retry_after_ms) { return Err(ExecutionFailureBuildError::InvalidRetryAfter); } self.retry_after_ms = Some(retry_after_ms); Ok(self) } pub fn try_with_confirmation( mut self, token: impl Into, expires_in_ms: u64, ) -> Result { if self.code != ExecutionErrorCode::ConfirmationRequired { return Err(ExecutionFailureBuildError::IncompatibleMetadata); } self.confirmation = Some(ConfirmationChallenge::try_new(token, expires_in_ms)?); Ok(self) } pub fn with_dispatch_uncertainty(mut self) -> Self { if matches!( self.code, ExecutionErrorCode::UpstreamTimeout | ExecutionErrorCode::UpstreamTransportError | ExecutionErrorCode::IdempotencyOutcomeUnknown | ExecutionErrorCode::PersistenceUnavailable ) { self.retryability = Retryability::ManualReconcile; self.outcome_certainty = OutcomeCertainty::OutcomeUnknown; } self } } #[derive(Clone, Debug, PartialEq)] pub struct ExecutionSuccess { pub operation_id: OperationId, pub operation_version: u32, pub origin: ExecutionOrigin, pub correlation: CorrelationContext, pub request_preview: Value, pub output: Value, }