lenso-service 0.1.21

Public contracts for Lenso Providers and Autonomous Services.
Documentation
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::{Digest as _, Sha256};
use utoipa::ToSchema;

pub const RUNTIME_OPERATIONS_PROTOCOL: &str = "lenso.system-plane.runtime-operations.v1";
pub const RUNTIME_OPERATIONS_PATH: &str = "/system-plane/v1/runtime-operations";
pub const RUNTIME_OPERATIONS_FEATURE_FUNCTION_RETRY: &str = "function-run-retry";
pub const RUNTIME_OPERATIONS_FEATURE_OUTBOX_RETRY: &str = "outbox-event-retry";
pub const RUNTIME_OPERATIONS_FEATURE_EVIDENCE: &str = "operation-evidence";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeOperationTargetKind {
    FunctionRun,
    OutboxEvent,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeOperationTargetStatus {
    Pending,
    Processing,
    Running,
    Completed,
    Published,
    Failed,
    Dead,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeOperationDesiredOutcome {
    Retry,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ManagementActorKind {
    Operator,
    Automation,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeOperationTarget {
    pub kind: RuntimeOperationTargetKind,
    pub target_id: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeOperationTargetSnapshot {
    pub protocol: String,
    pub service_id: String,
    pub service_revision: String,
    pub target: RuntimeOperationTarget,
    pub target_revision: String,
    pub observed_at_unix_ms: u64,
    pub target_name: String,
    pub status: RuntimeOperationTargetStatus,
    pub attempts: u32,
    pub max_attempts: u32,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ManagementActor {
    pub kind: ManagementActorKind,
    pub subject: String,
    pub delegated_authority_digest: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ManagementApproval {
    pub approval_id: String,
    pub approval_digest: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ManagementIntent {
    pub protocol: String,
    pub intent_id: String,
    pub service_id: String,
    pub service_revision: String,
    pub target: RuntimeOperationTarget,
    pub desired_outcome: RuntimeOperationDesiredOutcome,
    pub expected_target_revision: String,
    pub actor: ManagementActor,
    pub approvals: Vec<ManagementApproval>,
    pub deadline_unix_ms: u64,
    pub idempotency_key: String,
    pub capability_contract_id: String,
    pub capability_schema_digest: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeOperationRisk {
    DuplicateExternalEffect,
    RepeatedBusinessNotification,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeOperationAvailabilityImpact {
    None,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeOperationCompensationSupport {
    NotAvailable,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeOperationPlanReceipt {
    pub protocol: String,
    pub intent_digest: String,
    pub plan_digest: String,
    pub service_id: String,
    pub service_revision: String,
    pub target: RuntimeOperationTarget,
    pub expected_target_revision: String,
    pub expected_effects: Vec<String>,
    pub risks: Vec<RuntimeOperationRisk>,
    pub availability_impact: RuntimeOperationAvailabilityImpact,
    pub compensation_support: RuntimeOperationCompensationSupport,
    pub approval_required: bool,
    pub expires_at_unix_ms: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeOperationSubmission {
    pub intent: ManagementIntent,
    pub plan: RuntimeOperationPlanReceipt,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeOperationState {
    Accepted,
    Succeeded,
    Rejected,
    Failed,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeOperationAcknowledgement {
    pub protocol: String,
    pub operation_id: String,
    pub idempotency_key: String,
    pub intent_digest: String,
    pub plan_digest: String,
    pub state: RuntimeOperationState,
    pub accepted_at_unix_ms: u64,
    pub authorization_epoch: u64,
    pub enrollment_receipt_digest: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeOperationEvidence {
    pub protocol: String,
    pub operation_id: String,
    pub sequence: u64,
    pub state: RuntimeOperationState,
    pub recorded_at_unix_ms: u64,
    pub service_id: String,
    pub service_revision: String,
    pub target: RuntimeOperationTarget,
    pub target_revision_before: String,
    pub target_revision_after: Option<String>,
    pub code: String,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeOperationEvidencePage {
    pub protocol: String,
    pub operation_id: String,
    pub items: Vec<RuntimeOperationEvidence>,
    pub next_cursor: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RuntimeOperationRecovery {
    pub protocol: String,
    pub acknowledgement: RuntimeOperationAcknowledgement,
    pub latest_evidence: RuntimeOperationEvidence,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", content = "document", rename_all = "snake_case")]
pub enum RuntimeOperationsMessage {
    TargetSnapshot(RuntimeOperationTargetSnapshot),
    Intent(ManagementIntent),
    PlanReceipt(RuntimeOperationPlanReceipt),
    Submission(RuntimeOperationSubmission),
    Acknowledgement(RuntimeOperationAcknowledgement),
    Evidence(RuntimeOperationEvidence),
    EvidencePage(RuntimeOperationEvidencePage),
    Recovery(RuntimeOperationRecovery),
}

#[must_use]
pub fn management_intent_digest(intent: &ManagementIntent) -> String {
    digest_json(intent)
}

#[must_use]
pub fn runtime_operation_plan_digest(plan: &RuntimeOperationPlanReceipt) -> String {
    let mut unsigned = plan.clone();
    unsigned.plan_digest.clear();
    digest_json(&unsigned)
}

#[must_use]
pub fn runtime_operations_schema() -> Value {
    let mut schema = serde_json::to_value(schemars::schema_for!(RuntimeOperationsMessage))
        .expect("Runtime Operations schema serializes");
    schema["$id"] = Value::String(
        "https://contracts.lenso.local/system-plane/lenso.system-plane.runtime-operations.v1.schema.json"
            .to_owned(),
    );
    schema["title"] = Value::String("Lenso Runtime Operations Messages".to_owned());
    for definition in [
        "RuntimeOperationTargetSnapshot",
        "ManagementIntent",
        "RuntimeOperationPlanReceipt",
        "RuntimeOperationAcknowledgement",
        "RuntimeOperationEvidence",
        "RuntimeOperationEvidencePage",
        "RuntimeOperationRecovery",
    ] {
        schema["$defs"][definition]["properties"]["protocol"] =
            json!({ "const": RUNTIME_OPERATIONS_PROTOCOL });
    }
    patch_digest(&mut schema, "ManagementActor", "delegatedAuthorityDigest");
    patch_digest(&mut schema, "ManagementApproval", "approvalDigest");
    patch_digest(&mut schema, "ManagementIntent", "expectedTargetRevision");
    patch_digest(&mut schema, "ManagementIntent", "capabilitySchemaDigest");
    patch_digest(&mut schema, "RuntimeOperationPlanReceipt", "intentDigest");
    patch_digest(&mut schema, "RuntimeOperationPlanReceipt", "planDigest");
    patch_digest(
        &mut schema,
        "RuntimeOperationPlanReceipt",
        "expectedTargetRevision",
    );
    patch_digest(
        &mut schema,
        "RuntimeOperationAcknowledgement",
        "intentDigest",
    );
    patch_digest(&mut schema, "RuntimeOperationAcknowledgement", "planDigest");
    patch_digest(
        &mut schema,
        "RuntimeOperationAcknowledgement",
        "enrollmentReceiptDigest",
    );
    patch_digest(
        &mut schema,
        "RuntimeOperationEvidence",
        "targetRevisionBefore",
    );
    schema
}

#[must_use]
pub fn runtime_operations_schema_digest() -> String {
    digest_bytes(
        &serde_json::to_vec(&runtime_operations_schema())
            .expect("Runtime Operations schema serializes to bytes"),
    )
}

fn patch_digest(schema: &mut Value, definition: &str, field: &str) {
    schema["$defs"][definition]["properties"][field]["pattern"] = json!("^sha256:[0-9a-f]{64}$");
}

fn digest_json<T: Serialize>(value: &T) -> String {
    digest_bytes(&serde_json::to_vec(value).expect("Runtime Operations document serializes"))
}

fn digest_bytes(bytes: &[u8]) -> String {
    format!("sha256:{}", hex(&Sha256::digest(bytes)))
}

fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}