Skip to main content

platform_runtime_operations/
lib.rs

1//! Service-owned Runtime Operations Capability Provider.
2
3use async_trait::async_trait;
4use axum::{
5    Extension, Json,
6    extract::{Path, Query},
7    http::StatusCode,
8};
9use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
10use chrono::{DateTime, Utc};
11use lenso_service::system_plane::{
12    ManagementIntent, RUNTIME_OPERATIONS_FEATURE_EVIDENCE,
13    RUNTIME_OPERATIONS_FEATURE_FUNCTION_RETRY, RUNTIME_OPERATIONS_FEATURE_OUTBOX_RETRY,
14    RUNTIME_OPERATIONS_PATH, RUNTIME_OPERATIONS_PROTOCOL, RuntimeOperationAcknowledgement,
15    RuntimeOperationAvailabilityImpact, RuntimeOperationCompensationSupport,
16    RuntimeOperationDesiredOutcome, RuntimeOperationEvidence, RuntimeOperationEvidencePage,
17    RuntimeOperationPlanReceipt, RuntimeOperationRecovery, RuntimeOperationRisk,
18    RuntimeOperationState, RuntimeOperationSubmission, RuntimeOperationTarget,
19    RuntimeOperationTargetKind, RuntimeOperationTargetSnapshot, RuntimeOperationTargetStatus,
20    management_intent_digest, runtime_operation_plan_digest, runtime_operations_schema_digest,
21};
22use platform_core::Migration;
23use platform_system_plane::{
24    AuthorizedSystemPlaneCaller, SystemPlaneErrorBody, SystemPlaneRejection,
25};
26use serde::{Deserialize, Serialize};
27use sha2::{Digest as _, Sha256};
28use sqlx::{PgPool, Postgres, Transaction};
29use std::{collections::BTreeSet, sync::Arc};
30use utoipa_axum::{router::OpenApiRouter, routes};
31
32pub const RUNTIME_OPERATIONS_MIGRATIONS: &[Migration] = &[Migration {
33    name: "runtime-operations/0001_create_runtime_operations",
34    sql: include_str!("../migrations/0001_create_runtime_operations.sql"),
35}];
36const RUNTIME_OPERATION_EVIDENCE_CURSOR_PROTOCOL: &str =
37    "lenso.system-plane.runtime-operation-evidence-cursor.v1";
38
39#[derive(Debug, Clone)]
40pub struct RuntimeOperationsProvider {
41    pool: PgPool,
42    service_id: String,
43    service_revision: String,
44    authority_verifier: Option<Arc<dyn ManagementAuthorityVerifier>>,
45}
46
47impl RuntimeOperationsProvider {
48    #[must_use]
49    pub fn new(
50        pool: PgPool,
51        service_id: impl Into<String>,
52        service_revision: impl Into<String>,
53    ) -> Self {
54        Self {
55            pool,
56            service_id: service_id.into(),
57            service_revision: service_revision.into(),
58            authority_verifier: None,
59        }
60    }
61
62    #[must_use]
63    pub fn with_authority_verifier(
64        mut self,
65        verifier: Arc<dyn ManagementAuthorityVerifier>,
66    ) -> Self {
67        self.authority_verifier = Some(verifier);
68        self
69    }
70
71    #[must_use]
72    pub fn advertisement() -> lenso_service::system_plane::CapabilityAdvertisement {
73        lenso_service::system_plane::CapabilityAdvertisement {
74            contract_id: RUNTIME_OPERATIONS_PROTOCOL.to_owned(),
75            major_version: 1,
76            feature_ids: BTreeSet::from([
77                RUNTIME_OPERATIONS_FEATURE_EVIDENCE.to_owned(),
78                RUNTIME_OPERATIONS_FEATURE_FUNCTION_RETRY.to_owned(),
79                RUNTIME_OPERATIONS_FEATURE_OUTBOX_RETRY.to_owned(),
80            ]),
81            schema_digest: runtime_operations_schema_digest(),
82            endpoint: RUNTIME_OPERATIONS_PATH.to_owned(),
83        }
84    }
85
86    #[must_use]
87    pub fn service_id(&self) -> &str {
88        &self.service_id
89    }
90
91    #[must_use]
92    pub fn service_revision(&self) -> &str {
93        &self.service_revision
94    }
95
96    #[must_use]
97    pub fn validation_error(&self) -> Option<&'static str> {
98        if self.service_id.trim().is_empty() {
99            return Some("Runtime Operations provider Service identity must not be empty");
100        }
101        if self.service_revision.trim().is_empty() {
102            return Some("Runtime Operations provider Service revision must not be empty");
103        }
104        None
105    }
106
107    pub async fn target_snapshot(
108        &self,
109        target: &RuntimeOperationTarget,
110        now_unix_ms: u64,
111    ) -> Result<RuntimeOperationTargetSnapshot, RuntimeOperationsError> {
112        let row = fetch_target(&self.pool, target).await?;
113        snapshot(&self.service_id, &self.service_revision, row, now_unix_ms)
114    }
115
116    pub async fn plan(
117        &self,
118        intent: &ManagementIntent,
119        now_unix_ms: u64,
120    ) -> Result<RuntimeOperationPlanReceipt, RuntimeOperationsError> {
121        validate_intent(
122            intent,
123            &self.service_id,
124            &self.service_revision,
125            now_unix_ms,
126        )?;
127        let target = self.target_snapshot(&intent.target, now_unix_ms).await?;
128        if target.target_revision != intent.expected_target_revision {
129            return Err(operation_error(
130                RuntimeOperationsErrorCode::StaleTargetRevision,
131                "Management Intent target revision is no longer current",
132            ));
133        }
134        ensure_retryable(target.status)?;
135        let mut receipt = RuntimeOperationPlanReceipt {
136            protocol: RUNTIME_OPERATIONS_PROTOCOL.to_owned(),
137            intent_digest: management_intent_digest(intent),
138            plan_digest: String::new(),
139            service_id: self.service_id.clone(),
140            service_revision: self.service_revision.clone(),
141            target: intent.target.clone(),
142            expected_target_revision: intent.expected_target_revision.clone(),
143            expected_effects: expected_effects(intent.target.kind),
144            risks: operation_risks(intent.target.kind),
145            availability_impact: RuntimeOperationAvailabilityImpact::None,
146            compensation_support: RuntimeOperationCompensationSupport::NotAvailable,
147            approval_required: true,
148            expires_at_unix_ms: intent
149                .deadline_unix_ms
150                .min(now_unix_ms.saturating_add(300_000)),
151        };
152        receipt.plan_digest = runtime_operation_plan_digest(&receipt);
153        Ok(receipt)
154    }
155
156    pub async fn submit(
157        &self,
158        submission: &RuntimeOperationSubmission,
159        caller: &AuthorizedSystemPlaneCaller,
160        now_unix_ms: u64,
161    ) -> Result<RuntimeOperationAcknowledgement, RuntimeOperationsError> {
162        if let Some(acknowledgement) = self.existing_acknowledgement(submission).await? {
163            return Ok(acknowledgement);
164        }
165        validate_intent(
166            &submission.intent,
167            &self.service_id,
168            &self.service_revision,
169            now_unix_ms,
170        )?;
171        validate_submitted_plan(
172            submission,
173            &self.service_id,
174            &self.service_revision,
175            now_unix_ms,
176        )?;
177        let verifier = self.authority_verifier.as_ref().ok_or_else(|| {
178            operation_error(
179                RuntimeOperationsErrorCode::AuthorityUnavailable,
180                "Runtime Operations mutation authority is not configured",
181            )
182        })?;
183        let authority = verifier
184            .verify(ManagementAuthorityRequest {
185                intent: submission.intent.clone(),
186                console_service_principal: caller.service_principal.clone(),
187                authorization_epoch: caller.enrollment.authorization_epoch,
188                enrollment_receipt_digest: caller.enrollment.receipt_digest.clone(),
189                now_unix_ms,
190            })
191            .await?;
192        self.persist_and_retry(submission, caller, authority, now_unix_ms)
193            .await
194    }
195
196    async fn existing_acknowledgement(
197        &self,
198        submission: &RuntimeOperationSubmission,
199    ) -> Result<Option<RuntimeOperationAcknowledgement>, RuntimeOperationsError> {
200        let request_digest = digest_json(submission);
201        let existing = sqlx::query_as::<_, (String, serde_json::Value)>(
202            r#"
203            select request_digest, acknowledgement
204            from platform.system_plane_runtime_operations
205            where service_id = $1 and idempotency_key = $2
206            "#,
207        )
208        .bind(&self.service_id)
209        .bind(&submission.intent.idempotency_key)
210        .fetch_optional(&self.pool)
211        .await
212        .map_err(store_error)?;
213        let Some((stored_request_digest, acknowledgement)) = existing else {
214            return Ok(None);
215        };
216        if stored_request_digest != request_digest {
217            return Err(operation_error(
218                RuntimeOperationsErrorCode::IdempotencyConflict,
219                "Idempotency key is already bound to a different Runtime Operation request",
220            ));
221        }
222        serde_json::from_value(acknowledgement)
223            .map(Some)
224            .map_err(serialization_error)
225    }
226
227    pub async fn evidence(
228        &self,
229        operation_id: &str,
230    ) -> Result<RuntimeOperationEvidence, RuntimeOperationsError> {
231        let evidence = sqlx::query_scalar::<_, serde_json::Value>(
232            r#"
233            select evidence.evidence
234            from platform.system_plane_runtime_operation_evidence evidence
235            join platform.system_plane_runtime_operations operation_record
236              on operation_record.operation_id = evidence.operation_id
237            where evidence.operation_id = $1 and operation_record.service_id = $2
238            order by evidence.sequence desc
239            limit 1
240            "#,
241        )
242        .bind(operation_id)
243        .bind(&self.service_id)
244        .fetch_optional(&self.pool)
245        .await
246        .map_err(store_error)?
247        .ok_or_else(|| {
248            operation_error(
249                RuntimeOperationsErrorCode::NotFound,
250                "Runtime Operation evidence was not found",
251            )
252        })?;
253        serde_json::from_value(evidence).map_err(serialization_error)
254    }
255
256    pub async fn evidence_page(
257        &self,
258        operation_id: &str,
259        cursor: Option<&str>,
260        limit: u32,
261    ) -> Result<RuntimeOperationEvidencePage, RuntimeOperationsError> {
262        if operation_id.trim().is_empty() || !(1..=100).contains(&limit) {
263            return Err(operation_error(
264                RuntimeOperationsErrorCode::InvalidEvidenceQuery,
265                "Operation Evidence queries require an operation identity and a limit from 1 to 100",
266            ));
267        }
268        let after_sequence = if let Some(cursor) = cursor {
269            let cursor = decode_evidence_cursor(cursor)?;
270            if cursor.operation_id != operation_id {
271                return Err(operation_error(
272                    RuntimeOperationsErrorCode::InvalidEvidenceCursor,
273                    "Operation Evidence cursor belongs to a different operation",
274                ));
275            }
276            cursor.after_sequence
277        } else {
278            0
279        };
280        self.ensure_operation_exists(operation_id).await?;
281        let fetch_limit = i64::from(limit) + 1;
282        let mut rows = sqlx::query_as::<_, (i64, serde_json::Value)>(
283            r#"
284            select evidence.sequence, evidence.evidence
285            from platform.system_plane_runtime_operation_evidence evidence
286            join platform.system_plane_runtime_operations operation_record
287              on operation_record.operation_id = evidence.operation_id
288            where evidence.operation_id = $1 and operation_record.service_id = $2
289              and evidence.sequence > $3
290            order by evidence.sequence asc
291            limit $4
292            "#,
293        )
294        .bind(operation_id)
295        .bind(&self.service_id)
296        .bind(to_i64(after_sequence)?)
297        .bind(fetch_limit)
298        .fetch_all(&self.pool)
299        .await
300        .map_err(store_error)?;
301        if rows.is_empty() && after_sequence == 0 {
302            return Err(operation_error(
303                RuntimeOperationsErrorCode::NotFound,
304                "Runtime Operation evidence was not found",
305            ));
306        }
307        let has_more = rows.len() > limit as usize;
308        rows.truncate(limit as usize);
309        let next_cursor = if has_more {
310            rows.last()
311                .map(|(sequence, _)| {
312                    encode_evidence_cursor(&RuntimeOperationEvidenceCursor {
313                        protocol: RUNTIME_OPERATION_EVIDENCE_CURSOR_PROTOCOL.to_owned(),
314                        operation_id: operation_id.to_owned(),
315                        after_sequence: u64::try_from(*sequence).map_err(|_| {
316                            operation_error(
317                                RuntimeOperationsErrorCode::StoreUnavailable,
318                                "Operation Evidence sequence is invalid in the Service Store",
319                            )
320                        })?,
321                    })
322                })
323                .transpose()?
324        } else {
325            None
326        };
327        let items = rows
328            .into_iter()
329            .map(|(_, evidence)| serde_json::from_value(evidence).map_err(serialization_error))
330            .collect::<Result<Vec<_>, _>>()?;
331        Ok(RuntimeOperationEvidencePage {
332            protocol: RUNTIME_OPERATIONS_PROTOCOL.to_owned(),
333            operation_id: operation_id.to_owned(),
334            items,
335            next_cursor,
336        })
337    }
338
339    async fn ensure_operation_exists(
340        &self,
341        operation_id: &str,
342    ) -> Result<(), RuntimeOperationsError> {
343        let exists = sqlx::query_scalar::<_, bool>(
344            r#"
345            select exists (
346                select 1
347                from platform.system_plane_runtime_operations
348                where operation_id = $1 and service_id = $2
349            )
350            "#,
351        )
352        .bind(operation_id)
353        .bind(&self.service_id)
354        .fetch_one(&self.pool)
355        .await
356        .map_err(store_error)?;
357        if exists {
358            Ok(())
359        } else {
360            Err(operation_error(
361                RuntimeOperationsErrorCode::NotFound,
362                "Runtime Operation was not found",
363            ))
364        }
365    }
366
367    pub async fn recover_by_idempotency_key(
368        &self,
369        idempotency_key: &str,
370    ) -> Result<RuntimeOperationRecovery, RuntimeOperationsError> {
371        if idempotency_key.trim().is_empty() {
372            return Err(operation_error(
373                RuntimeOperationsErrorCode::InvalidEvidenceQuery,
374                "Runtime Operation recovery requires an idempotency key",
375            ));
376        }
377        let acknowledgement = sqlx::query_scalar::<_, serde_json::Value>(
378            r#"
379            select acknowledgement
380            from platform.system_plane_runtime_operations
381            where service_id = $1 and idempotency_key = $2
382            "#,
383        )
384        .bind(&self.service_id)
385        .bind(idempotency_key)
386        .fetch_optional(&self.pool)
387        .await
388        .map_err(store_error)?
389        .ok_or_else(|| {
390            operation_error(
391                RuntimeOperationsErrorCode::NotFound,
392                "Runtime Operation acknowledgement was not found",
393            )
394        })?;
395        let acknowledgement: RuntimeOperationAcknowledgement =
396            serde_json::from_value(acknowledgement).map_err(serialization_error)?;
397        let latest_evidence = self.evidence(&acknowledgement.operation_id).await?;
398        Ok(RuntimeOperationRecovery {
399            protocol: RUNTIME_OPERATIONS_PROTOCOL.to_owned(),
400            acknowledgement,
401            latest_evidence,
402        })
403    }
404
405    async fn persist_and_retry(
406        &self,
407        submission: &RuntimeOperationSubmission,
408        caller: &AuthorizedSystemPlaneCaller,
409        authority: VerifiedManagementAuthority,
410        now_unix_ms: u64,
411    ) -> Result<RuntimeOperationAcknowledgement, RuntimeOperationsError> {
412        let intent_digest = management_intent_digest(&submission.intent);
413        let request_digest = digest_json(submission);
414        let operation_id = format!("runtime-operation:{}", &request_digest[7..]);
415        let mut transaction = self.pool.begin().await.map_err(store_error)?;
416        sqlx::query(
417            "select pg_advisory_xact_lock(hashtextextended($1, 0)), pg_advisory_xact_lock(hashtextextended($2, 1))",
418        )
419        .bind(&submission.intent.idempotency_key)
420        .bind(format!(
421            "{}:{}",
422            target_kind_store(submission.intent.target.kind),
423            submission.intent.target.target_id
424        ))
425        .execute(&mut *transaction)
426        .await
427        .map_err(store_error)?;
428        if let Some((stored_request_digest, acknowledgement)) =
429            sqlx::query_as::<_, (String, serde_json::Value)>(
430                r#"
431                select request_digest, acknowledgement
432                from platform.system_plane_runtime_operations
433                where service_id = $1 and idempotency_key = $2
434                "#,
435            )
436            .bind(&self.service_id)
437            .bind(&submission.intent.idempotency_key)
438            .fetch_optional(&mut *transaction)
439            .await
440            .map_err(store_error)?
441        {
442            if stored_request_digest != request_digest {
443                return Err(operation_error(
444                    RuntimeOperationsErrorCode::IdempotencyConflict,
445                    "Idempotency key is already bound to a different Runtime Operation request",
446                ));
447            }
448            let acknowledgement =
449                serde_json::from_value(acknowledgement).map_err(serialization_error)?;
450            transaction.commit().await.map_err(store_error)?;
451            return Ok(acknowledgement);
452        }
453        let before = fetch_target_for_update(&mut transaction, &submission.intent.target).await?;
454        let before_snapshot = snapshot(
455            &self.service_id,
456            &self.service_revision,
457            before,
458            now_unix_ms,
459        )?;
460        if before_snapshot.target_revision != submission.intent.expected_target_revision {
461            return Err(operation_error(
462                RuntimeOperationsErrorCode::StaleTargetRevision,
463                "Runtime Operation target changed before durable acceptance",
464            ));
465        }
466        ensure_retryable(before_snapshot.status)?;
467        let acknowledgement = RuntimeOperationAcknowledgement {
468            protocol: RUNTIME_OPERATIONS_PROTOCOL.to_owned(),
469            operation_id: operation_id.clone(),
470            idempotency_key: submission.intent.idempotency_key.clone(),
471            intent_digest: intent_digest.clone(),
472            plan_digest: submission.plan.plan_digest.clone(),
473            state: RuntimeOperationState::Accepted,
474            accepted_at_unix_ms: now_unix_ms,
475            authorization_epoch: caller.enrollment.authorization_epoch,
476            enrollment_receipt_digest: caller.enrollment.receipt_digest.clone(),
477        };
478        let acknowledgement_json =
479            serde_json::to_value(&acknowledgement).map_err(serialization_error)?;
480        let authority_json = serde_json::to_value(&authority).map_err(serialization_error)?;
481        sqlx::query(
482            r#"
483            insert into platform.system_plane_runtime_operations (
484                operation_id, service_id, idempotency_key, request_digest,
485                intent_digest, plan_digest, target_kind, target_id,
486                target_revision_before, state, authorization_evidence,
487                acknowledgement, accepted_at_unix_ms
488            ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9,
489                      'accepted', $10, $11, $12)
490            "#,
491        )
492        .bind(&operation_id)
493        .bind(&self.service_id)
494        .bind(&submission.intent.idempotency_key)
495        .bind(&request_digest)
496        .bind(&intent_digest)
497        .bind(&submission.plan.plan_digest)
498        .bind(target_kind_store(submission.intent.target.kind))
499        .bind(&submission.intent.target.target_id)
500        .bind(&before_snapshot.target_revision)
501        .bind(authority_json)
502        .bind(acknowledgement_json)
503        .bind(to_i64(now_unix_ms)?)
504        .execute(&mut *transaction)
505        .await
506        .map_err(store_error)?;
507        let accepted_evidence = RuntimeOperationEvidence {
508            protocol: RUNTIME_OPERATIONS_PROTOCOL.to_owned(),
509            operation_id: operation_id.clone(),
510            sequence: 1,
511            state: RuntimeOperationState::Accepted,
512            recorded_at_unix_ms: now_unix_ms,
513            service_id: self.service_id.clone(),
514            service_revision: self.service_revision.clone(),
515            target: submission.intent.target.clone(),
516            target_revision_before: before_snapshot.target_revision.clone(),
517            target_revision_after: None,
518            code: "runtime_operation_accepted".to_owned(),
519            message: "Runtime Operation and authorization evidence were durably accepted"
520                .to_owned(),
521        };
522        sqlx::query(
523            r#"
524            insert into platform.system_plane_runtime_operation_evidence (
525                operation_id, sequence, state, evidence
526            ) values ($1, 1, 'accepted', $2)
527            "#,
528        )
529        .bind(&operation_id)
530        .bind(serde_json::to_value(&accepted_evidence).map_err(serialization_error)?)
531        .execute(&mut *transaction)
532        .await
533        .map_err(store_error)?;
534        let after = retry_target(&mut transaction, &submission.intent.target).await?;
535        let after_snapshot =
536            snapshot(&self.service_id, &self.service_revision, after, now_unix_ms)?;
537        let evidence = RuntimeOperationEvidence {
538            protocol: RUNTIME_OPERATIONS_PROTOCOL.to_owned(),
539            operation_id: operation_id.clone(),
540            sequence: 2,
541            state: RuntimeOperationState::Succeeded,
542            recorded_at_unix_ms: now_unix_ms,
543            service_id: self.service_id.clone(),
544            service_revision: self.service_revision.clone(),
545            target: submission.intent.target.clone(),
546            target_revision_before: before_snapshot.target_revision,
547            target_revision_after: Some(after_snapshot.target_revision.clone()),
548            code: retry_evidence_code(submission.intent.target.kind).to_owned(),
549            message: retry_evidence_message(submission.intent.target.kind).to_owned(),
550        };
551        let evidence_json = serde_json::to_value(&evidence).map_err(serialization_error)?;
552        sqlx::query(
553            r#"
554            insert into platform.system_plane_runtime_operation_evidence (
555                operation_id, sequence, state, evidence
556            ) values ($1, 2, 'succeeded', $2)
557            "#,
558        )
559        .bind(&operation_id)
560        .bind(evidence_json)
561        .execute(&mut *transaction)
562        .await
563        .map_err(store_error)?;
564        sqlx::query(
565            r#"
566            update platform.system_plane_runtime_operations
567            set state = 'succeeded', target_revision_after = $2, updated_at = now()
568            where operation_id = $1
569            "#,
570        )
571        .bind(&operation_id)
572        .bind(&after_snapshot.target_revision)
573        .execute(&mut *transaction)
574        .await
575        .map_err(store_error)?;
576        transaction.commit().await.map_err(store_error)?;
577        Ok(acknowledgement)
578    }
579}
580
581#[derive(Debug, Clone)]
582pub struct ManagementAuthorityRequest {
583    pub intent: ManagementIntent,
584    pub console_service_principal: String,
585    pub authorization_epoch: u64,
586    pub enrollment_receipt_digest: String,
587    pub now_unix_ms: u64,
588}
589
590#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
591#[serde(rename_all = "camelCase", deny_unknown_fields)]
592pub struct VerifiedManagementAuthority {
593    pub actor_subject: String,
594    pub delegated_authority_digest: String,
595    pub approval_digests: Vec<String>,
596    pub console_service_principal: String,
597    pub authorization_epoch: u64,
598    pub enrollment_receipt_digest: String,
599    pub verified_at_unix_ms: u64,
600}
601
602#[async_trait]
603pub trait ManagementAuthorityVerifier: std::fmt::Debug + Send + Sync {
604    async fn verify(
605        &self,
606        request: ManagementAuthorityRequest,
607    ) -> Result<VerifiedManagementAuthority, RuntimeOperationsError>;
608}
609
610#[derive(Debug, Clone)]
611pub struct SystemSandboxManagementAuthorityVerifier;
612
613impl SystemSandboxManagementAuthorityVerifier {
614    pub fn new(environment: &str) -> Result<Self, RuntimeOperationsError> {
615        if !matches!(environment, "local" | "development" | "test") {
616            return Err(operation_error(
617                RuntimeOperationsErrorCode::AuthorityRejected,
618                "System Sandbox Management Authority is forbidden outside local development and tests",
619            ));
620        }
621        Ok(Self)
622    }
623}
624
625#[async_trait]
626impl ManagementAuthorityVerifier for SystemSandboxManagementAuthorityVerifier {
627    async fn verify(
628        &self,
629        request: ManagementAuthorityRequest,
630    ) -> Result<VerifiedManagementAuthority, RuntimeOperationsError> {
631        if request.intent.actor.subject.trim().is_empty()
632            || !canonical_digest(&request.intent.actor.delegated_authority_digest)
633            || request.intent.approvals.is_empty()
634            || request
635                .intent
636                .approvals
637                .iter()
638                .any(|approval| !canonical_digest(&approval.approval_digest))
639            || request.intent.deadline_unix_ms <= request.now_unix_ms
640        {
641            return Err(operation_error(
642                RuntimeOperationsErrorCode::AuthorityRejected,
643                "Management authority, approval evidence, or deadline was rejected",
644            ));
645        }
646        Ok(VerifiedManagementAuthority {
647            actor_subject: request.intent.actor.subject,
648            delegated_authority_digest: request.intent.actor.delegated_authority_digest,
649            approval_digests: request
650                .intent
651                .approvals
652                .into_iter()
653                .map(|approval| approval.approval_digest)
654                .collect(),
655            console_service_principal: request.console_service_principal,
656            authorization_epoch: request.authorization_epoch,
657            enrollment_receipt_digest: request.enrollment_receipt_digest,
658            verified_at_unix_ms: request.now_unix_ms,
659        })
660    }
661}
662
663#[derive(Debug, Clone, Copy, PartialEq, Eq)]
664pub enum RuntimeOperationsErrorCode {
665    InvalidIntent,
666    InvalidEvidenceQuery,
667    InvalidEvidenceCursor,
668    NotFound,
669    TargetNotRetryable,
670    StaleTargetRevision,
671    PlanMismatch,
672    AuthorityUnavailable,
673    AuthorityRejected,
674    IdempotencyConflict,
675    StoreUnavailable,
676}
677
678#[derive(Debug, Clone, PartialEq, Eq)]
679pub struct RuntimeOperationsError {
680    pub code: RuntimeOperationsErrorCode,
681    pub message: String,
682}
683
684#[derive(Debug, Clone, sqlx::FromRow)]
685struct FunctionRunRow {
686    id: String,
687    function_name: String,
688    status: String,
689    attempts: i32,
690    max_attempts: i32,
691    updated_at: DateTime<Utc>,
692}
693
694#[derive(Debug, Clone, sqlx::FromRow)]
695struct OutboxEventRow {
696    id: String,
697    event_name: String,
698    status: String,
699    attempts: i32,
700    max_attempts: i32,
701    available_at: DateTime<Utc>,
702    locked_at: Option<DateTime<Utc>>,
703    published_at: Option<DateTime<Utc>>,
704    created_at: DateTime<Utc>,
705}
706
707#[derive(Debug, Clone)]
708enum RuntimeTargetRow {
709    FunctionRun(FunctionRunRow),
710    OutboxEvent(OutboxEventRow),
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
714#[serde(rename_all = "camelCase", deny_unknown_fields)]
715struct RuntimeOperationEvidenceCursor {
716    protocol: String,
717    operation_id: String,
718    after_sequence: u64,
719}
720
721#[derive(Debug, Deserialize)]
722#[serde(rename_all = "camelCase", deny_unknown_fields)]
723struct RuntimeOperationEvidenceQuery {
724    cursor: Option<String>,
725    limit: Option<u32>,
726}
727
728#[must_use]
729pub fn router<S>(provider: Option<Arc<RuntimeOperationsProvider>>) -> OpenApiRouter<S>
730where
731    S: Clone + Send + Sync + 'static,
732{
733    OpenApiRouter::new()
734        .routes(routes!(get_function_run_target))
735        .routes(routes!(get_outbox_event_target))
736        .routes(routes!(plan_runtime_operation))
737        .routes(routes!(submit_runtime_operation))
738        .routes(routes!(get_runtime_operation_evidence))
739        .routes(routes!(get_runtime_operation_evidence_page))
740        .routes(routes!(recover_runtime_operation))
741        .layer(Extension(provider))
742}
743
744#[utoipa::path(
745    get,
746    path = "/system-plane/v1/runtime-operations/function-runs/{id}",
747    params(("id" = String, Path, description = "Function Run identifier")),
748    responses(
749        (status = 200, body = RuntimeOperationTargetSnapshot),
750        (status = 401, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
751        (status = 403, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
752        (status = 404, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
753        (status = 503, body = SystemPlaneErrorBody, content_type = "application/problem+json")
754    ),
755    security(("bearer_auth" = [])),
756    tag = "system-plane-runtime-operations"
757)]
758async fn get_function_run_target(
759    caller: AuthorizedSystemPlaneCaller,
760    Extension(provider): Extension<Option<Arc<RuntimeOperationsProvider>>>,
761    Path(id): Path<String>,
762) -> Result<Json<RuntimeOperationTargetSnapshot>, SystemPlaneRejection> {
763    let provider = require_provider(provider)?;
764    require_grant(&caller, &[RUNTIME_OPERATIONS_FEATURE_FUNCTION_RETRY])?;
765    provider
766        .target_snapshot(
767            &RuntimeOperationTarget {
768                kind: RuntimeOperationTargetKind::FunctionRun,
769                target_id: id,
770            },
771            now_unix_ms(),
772        )
773        .await
774        .map(Json)
775        .map_err(rejection)
776}
777
778#[utoipa::path(
779    get,
780    path = "/system-plane/v1/runtime-operations/outbox-events/{id}",
781    params(("id" = String, Path, description = "Outbox Event identifier")),
782    responses(
783        (status = 200, body = RuntimeOperationTargetSnapshot),
784        (status = 401, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
785        (status = 403, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
786        (status = 404, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
787        (status = 503, body = SystemPlaneErrorBody, content_type = "application/problem+json")
788    ),
789    security(("bearer_auth" = [])),
790    tag = "system-plane-runtime-operations"
791)]
792async fn get_outbox_event_target(
793    caller: AuthorizedSystemPlaneCaller,
794    Extension(provider): Extension<Option<Arc<RuntimeOperationsProvider>>>,
795    Path(id): Path<String>,
796) -> Result<Json<RuntimeOperationTargetSnapshot>, SystemPlaneRejection> {
797    let provider = require_provider(provider)?;
798    require_grant(&caller, &[RUNTIME_OPERATIONS_FEATURE_OUTBOX_RETRY])?;
799    provider
800        .target_snapshot(
801            &RuntimeOperationTarget {
802                kind: RuntimeOperationTargetKind::OutboxEvent,
803                target_id: id,
804            },
805            now_unix_ms(),
806        )
807        .await
808        .map(Json)
809        .map_err(rejection)
810}
811
812#[utoipa::path(
813    post,
814    path = "/system-plane/v1/runtime-operations/plans",
815    request_body = ManagementIntent,
816    responses(
817        (status = 200, body = RuntimeOperationPlanReceipt),
818        (status = 400, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
819        (status = 401, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
820        (status = 403, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
821        (status = 409, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
822        (status = 503, body = SystemPlaneErrorBody, content_type = "application/problem+json")
823    ),
824    security(("bearer_auth" = [])),
825    tag = "system-plane-runtime-operations"
826)]
827async fn plan_runtime_operation(
828    caller: AuthorizedSystemPlaneCaller,
829    Extension(provider): Extension<Option<Arc<RuntimeOperationsProvider>>>,
830    Json(intent): Json<ManagementIntent>,
831) -> Result<Json<RuntimeOperationPlanReceipt>, SystemPlaneRejection> {
832    let provider = require_provider(provider)?;
833    require_grant(&caller, &[target_feature(intent.target.kind)])?;
834    provider
835        .plan(&intent, now_unix_ms())
836        .await
837        .map(Json)
838        .map_err(rejection)
839}
840
841#[utoipa::path(
842    post,
843    path = "/system-plane/v1/runtime-operations/operations",
844    request_body = RuntimeOperationSubmission,
845    responses(
846        (status = 202, body = RuntimeOperationAcknowledgement),
847        (status = 400, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
848        (status = 401, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
849        (status = 403, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
850        (status = 409, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
851        (status = 503, body = SystemPlaneErrorBody, content_type = "application/problem+json")
852    ),
853    security(("bearer_auth" = [])),
854    tag = "system-plane-runtime-operations"
855)]
856async fn submit_runtime_operation(
857    caller: AuthorizedSystemPlaneCaller,
858    Extension(provider): Extension<Option<Arc<RuntimeOperationsProvider>>>,
859    Json(submission): Json<RuntimeOperationSubmission>,
860) -> Result<(StatusCode, Json<RuntimeOperationAcknowledgement>), SystemPlaneRejection> {
861    let provider = require_provider(provider)?;
862    require_grant(
863        &caller,
864        &[
865            target_feature(submission.intent.target.kind),
866            RUNTIME_OPERATIONS_FEATURE_EVIDENCE,
867        ],
868    )?;
869    provider
870        .submit(&submission, &caller, now_unix_ms())
871        .await
872        .map(|acknowledgement| (StatusCode::ACCEPTED, Json(acknowledgement)))
873        .map_err(rejection)
874}
875
876#[utoipa::path(
877    get,
878    path = "/system-plane/v1/runtime-operations/operations/{id}",
879    params(("id" = String, Path, description = "Service-owned Runtime Operation identifier")),
880    responses(
881        (status = 200, body = RuntimeOperationEvidence),
882        (status = 401, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
883        (status = 403, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
884        (status = 404, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
885        (status = 503, body = SystemPlaneErrorBody, content_type = "application/problem+json")
886    ),
887    security(("bearer_auth" = [])),
888    tag = "system-plane-runtime-operations"
889)]
890async fn get_runtime_operation_evidence(
891    caller: AuthorizedSystemPlaneCaller,
892    Extension(provider): Extension<Option<Arc<RuntimeOperationsProvider>>>,
893    Path(id): Path<String>,
894) -> Result<Json<RuntimeOperationEvidence>, SystemPlaneRejection> {
895    let provider = require_provider(provider)?;
896    require_grant(&caller, &[RUNTIME_OPERATIONS_FEATURE_EVIDENCE])?;
897    provider.evidence(&id).await.map(Json).map_err(rejection)
898}
899
900#[utoipa::path(
901    get,
902    path = "/system-plane/v1/runtime-operations/operations/{id}/evidence",
903    params(
904        ("id" = String, Path, description = "Service-owned Runtime Operation identifier"),
905        ("cursor" = Option<String>, Query, description = "Opaque cursor returned by the prior page"),
906        ("limit" = Option<u32>, Query, description = "Evidence records to return, from 1 to 100")
907    ),
908    responses(
909        (status = 200, body = RuntimeOperationEvidencePage),
910        (status = 400, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
911        (status = 401, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
912        (status = 403, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
913        (status = 404, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
914        (status = 503, body = SystemPlaneErrorBody, content_type = "application/problem+json")
915    ),
916    security(("bearer_auth" = [])),
917    tag = "system-plane-runtime-operations"
918)]
919async fn get_runtime_operation_evidence_page(
920    caller: AuthorizedSystemPlaneCaller,
921    Extension(provider): Extension<Option<Arc<RuntimeOperationsProvider>>>,
922    Path(id): Path<String>,
923    Query(query): Query<RuntimeOperationEvidenceQuery>,
924) -> Result<Json<RuntimeOperationEvidencePage>, SystemPlaneRejection> {
925    let provider = require_provider(provider)?;
926    require_grant(&caller, &[RUNTIME_OPERATIONS_FEATURE_EVIDENCE])?;
927    provider
928        .evidence_page(&id, query.cursor.as_deref(), query.limit.unwrap_or(50))
929        .await
930        .map(Json)
931        .map_err(rejection)
932}
933
934#[utoipa::path(
935    get,
936    path = "/system-plane/v1/runtime-operations/operations/by-idempotency-key/{idempotency_key}",
937    params(("idempotency_key" = String, Path, description = "Management Intent idempotency key")),
938    responses(
939        (status = 200, body = RuntimeOperationRecovery),
940        (status = 400, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
941        (status = 401, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
942        (status = 403, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
943        (status = 404, body = SystemPlaneErrorBody, content_type = "application/problem+json"),
944        (status = 503, body = SystemPlaneErrorBody, content_type = "application/problem+json")
945    ),
946    security(("bearer_auth" = [])),
947    tag = "system-plane-runtime-operations"
948)]
949async fn recover_runtime_operation(
950    caller: AuthorizedSystemPlaneCaller,
951    Extension(provider): Extension<Option<Arc<RuntimeOperationsProvider>>>,
952    Path(idempotency_key): Path<String>,
953) -> Result<Json<RuntimeOperationRecovery>, SystemPlaneRejection> {
954    let provider = require_provider(provider)?;
955    require_grant(&caller, &[RUNTIME_OPERATIONS_FEATURE_EVIDENCE])?;
956    provider
957        .recover_by_idempotency_key(&idempotency_key)
958        .await
959        .map(Json)
960        .map_err(rejection)
961}
962
963fn require_provider(
964    provider: Option<Arc<RuntimeOperationsProvider>>,
965) -> Result<Arc<RuntimeOperationsProvider>, SystemPlaneRejection> {
966    provider.ok_or_else(|| {
967        SystemPlaneRejection::unavailable(
968            "runtime_operations_unavailable",
969            "Runtime Operations capability is not configured for this Service",
970            "configure_runtime_operations",
971        )
972    })
973}
974
975fn require_grant(
976    caller: &AuthorizedSystemPlaneCaller,
977    required_features: &[&str],
978) -> Result<(), SystemPlaneRejection> {
979    caller.require_capability(
980        RUNTIME_OPERATIONS_PROTOCOL,
981        &runtime_operations_schema_digest(),
982        required_features,
983    )
984}
985
986fn target_feature(kind: RuntimeOperationTargetKind) -> &'static str {
987    match kind {
988        RuntimeOperationTargetKind::FunctionRun => RUNTIME_OPERATIONS_FEATURE_FUNCTION_RETRY,
989        RuntimeOperationTargetKind::OutboxEvent => RUNTIME_OPERATIONS_FEATURE_OUTBOX_RETRY,
990    }
991}
992
993fn validate_intent(
994    intent: &ManagementIntent,
995    service_id: &str,
996    service_revision: &str,
997    now_unix_ms: u64,
998) -> Result<(), RuntimeOperationsError> {
999    if intent.protocol != RUNTIME_OPERATIONS_PROTOCOL
1000        || intent.intent_id.trim().is_empty()
1001        || intent.service_id != service_id
1002        || intent.service_revision != service_revision
1003        || intent.target.target_id.trim().is_empty()
1004        || intent.desired_outcome != RuntimeOperationDesiredOutcome::Retry
1005        || !canonical_digest(&intent.expected_target_revision)
1006        || intent.actor.subject.trim().is_empty()
1007        || !canonical_digest(&intent.actor.delegated_authority_digest)
1008        || intent.approvals.is_empty()
1009        || intent.approvals.iter().any(|approval| {
1010            approval.approval_id.trim().is_empty() || !canonical_digest(&approval.approval_digest)
1011        })
1012        || intent.deadline_unix_ms <= now_unix_ms
1013        || intent.idempotency_key.trim().is_empty()
1014        || intent.capability_contract_id != RUNTIME_OPERATIONS_PROTOCOL
1015        || intent.capability_schema_digest != runtime_operations_schema_digest()
1016    {
1017        return Err(operation_error(
1018            RuntimeOperationsErrorCode::InvalidIntent,
1019            "Management Intent is incomplete, expired, or bound to the wrong Service contract",
1020        ));
1021    }
1022    Ok(())
1023}
1024
1025fn validate_submitted_plan(
1026    submission: &RuntimeOperationSubmission,
1027    service_id: &str,
1028    service_revision: &str,
1029    now_unix_ms: u64,
1030) -> Result<(), RuntimeOperationsError> {
1031    let plan = &submission.plan;
1032    let intent = &submission.intent;
1033    if plan.protocol != RUNTIME_OPERATIONS_PROTOCOL
1034        || plan.intent_digest != management_intent_digest(intent)
1035        || plan.plan_digest != runtime_operation_plan_digest(plan)
1036        || plan.service_id != service_id
1037        || plan.service_revision != service_revision
1038        || plan.target != intent.target
1039        || plan.expected_target_revision != intent.expected_target_revision
1040        || plan.expected_effects != expected_effects(intent.target.kind)
1041        || plan.risks != operation_risks(intent.target.kind)
1042        || plan.availability_impact != RuntimeOperationAvailabilityImpact::None
1043        || plan.compensation_support != RuntimeOperationCompensationSupport::NotAvailable
1044        || !plan.approval_required
1045        || plan.expires_at_unix_ms <= now_unix_ms
1046        || plan.expires_at_unix_ms > intent.deadline_unix_ms
1047    {
1048        return Err(operation_error(
1049            RuntimeOperationsErrorCode::PlanMismatch,
1050            "Submitted plan is changed, expired, or no longer bound to its Management Intent",
1051        ));
1052    }
1053    Ok(())
1054}
1055
1056async fn fetch_target(
1057    pool: &PgPool,
1058    target: &RuntimeOperationTarget,
1059) -> Result<RuntimeTargetRow, RuntimeOperationsError> {
1060    match target.kind {
1061        RuntimeOperationTargetKind::FunctionRun => sqlx::query_as::<_, FunctionRunRow>(
1062            r#"
1063            select id, function_name, status, attempts, max_attempts, updated_at
1064            from runtime.function_runs
1065            where id = $1
1066            "#,
1067        )
1068        .bind(&target.target_id)
1069        .fetch_optional(pool)
1070        .await
1071        .map_err(store_error)?
1072        .map(RuntimeTargetRow::FunctionRun)
1073        .ok_or_else(|| target_not_found(target.kind)),
1074        RuntimeOperationTargetKind::OutboxEvent => sqlx::query_as::<_, OutboxEventRow>(
1075            r#"
1076            select id, event_name, status, attempts, max_attempts,
1077                   available_at, locked_at, published_at, created_at
1078            from platform.outbox
1079            where id = $1
1080            "#,
1081        )
1082        .bind(&target.target_id)
1083        .fetch_optional(pool)
1084        .await
1085        .map_err(store_error)?
1086        .map(RuntimeTargetRow::OutboxEvent)
1087        .ok_or_else(|| target_not_found(target.kind)),
1088    }
1089}
1090
1091async fn fetch_target_for_update(
1092    transaction: &mut Transaction<'_, Postgres>,
1093    target: &RuntimeOperationTarget,
1094) -> Result<RuntimeTargetRow, RuntimeOperationsError> {
1095    match target.kind {
1096        RuntimeOperationTargetKind::FunctionRun => sqlx::query_as::<_, FunctionRunRow>(
1097            r#"
1098            select id, function_name, status, attempts, max_attempts, updated_at
1099            from runtime.function_runs
1100            where id = $1
1101            for update
1102            "#,
1103        )
1104        .bind(&target.target_id)
1105        .fetch_optional(&mut **transaction)
1106        .await
1107        .map_err(store_error)?
1108        .map(RuntimeTargetRow::FunctionRun)
1109        .ok_or_else(|| target_not_found(target.kind)),
1110        RuntimeOperationTargetKind::OutboxEvent => sqlx::query_as::<_, OutboxEventRow>(
1111            r#"
1112            select id, event_name, status, attempts, max_attempts,
1113                   available_at, locked_at, published_at, created_at
1114            from platform.outbox
1115            where id = $1
1116            for update
1117            "#,
1118        )
1119        .bind(&target.target_id)
1120        .fetch_optional(&mut **transaction)
1121        .await
1122        .map_err(store_error)?
1123        .map(RuntimeTargetRow::OutboxEvent)
1124        .ok_or_else(|| target_not_found(target.kind)),
1125    }
1126}
1127
1128async fn retry_target(
1129    transaction: &mut Transaction<'_, Postgres>,
1130    target: &RuntimeOperationTarget,
1131) -> Result<RuntimeTargetRow, RuntimeOperationsError> {
1132    let row = match target.kind {
1133        RuntimeOperationTargetKind::FunctionRun => sqlx::query_as::<_, FunctionRunRow>(
1134            r#"
1135            update runtime.function_runs
1136            set status = 'pending',
1137                available_at = now(),
1138                locked_at = null,
1139                locked_by = null,
1140                last_error = null,
1141                updated_at = now()
1142            where id = $1 and status in ('failed', 'dead')
1143            returning id, function_name, status, attempts, max_attempts, updated_at
1144            "#,
1145        )
1146        .bind(&target.target_id)
1147        .fetch_optional(&mut **transaction)
1148        .await
1149        .map_err(store_error)?
1150        .map(RuntimeTargetRow::FunctionRun),
1151        RuntimeOperationTargetKind::OutboxEvent => sqlx::query_as::<_, OutboxEventRow>(
1152            r#"
1153            update platform.outbox
1154            set status = 'pending',
1155                available_at = now(),
1156                locked_at = null,
1157                locked_by = null,
1158                last_error = null
1159            where id = $1 and status in ('failed', 'dead')
1160            returning id, event_name, status, attempts, max_attempts,
1161                      available_at, locked_at, published_at, created_at
1162            "#,
1163        )
1164        .bind(&target.target_id)
1165        .fetch_optional(&mut **transaction)
1166        .await
1167        .map_err(store_error)?
1168        .map(RuntimeTargetRow::OutboxEvent),
1169    };
1170    row.ok_or_else(|| {
1171        operation_error(
1172            RuntimeOperationsErrorCode::StaleTargetRevision,
1173            "Runtime Operation target stopped being retryable before the effect was applied",
1174        )
1175    })
1176}
1177
1178fn target_not_found(kind: RuntimeOperationTargetKind) -> RuntimeOperationsError {
1179    operation_error(
1180        RuntimeOperationsErrorCode::NotFound,
1181        format!("{} target was not found", target_kind_label(kind)),
1182    )
1183}
1184
1185fn target_kind_store(kind: RuntimeOperationTargetKind) -> &'static str {
1186    match kind {
1187        RuntimeOperationTargetKind::FunctionRun => "function_run",
1188        RuntimeOperationTargetKind::OutboxEvent => "outbox_event",
1189    }
1190}
1191
1192fn target_kind_label(kind: RuntimeOperationTargetKind) -> &'static str {
1193    match kind {
1194        RuntimeOperationTargetKind::FunctionRun => "Function Run",
1195        RuntimeOperationTargetKind::OutboxEvent => "Outbox Event",
1196    }
1197}
1198
1199fn expected_effects(kind: RuntimeOperationTargetKind) -> Vec<String> {
1200    vec![format!(
1201        "schedule the exact {} for one additional runtime attempt",
1202        target_kind_label(kind)
1203    )]
1204}
1205
1206fn operation_risks(kind: RuntimeOperationTargetKind) -> Vec<RuntimeOperationRisk> {
1207    match kind {
1208        RuntimeOperationTargetKind::FunctionRun => vec![
1209            RuntimeOperationRisk::DuplicateExternalEffect,
1210            RuntimeOperationRisk::RepeatedBusinessNotification,
1211        ],
1212        RuntimeOperationTargetKind::OutboxEvent => vec![
1213            RuntimeOperationRisk::DuplicateExternalEffect,
1214            RuntimeOperationRisk::RepeatedBusinessNotification,
1215        ],
1216    }
1217}
1218
1219fn retry_evidence_code(kind: RuntimeOperationTargetKind) -> &'static str {
1220    match kind {
1221        RuntimeOperationTargetKind::FunctionRun => "function_run_retry_scheduled",
1222        RuntimeOperationTargetKind::OutboxEvent => "outbox_event_retry_scheduled",
1223    }
1224}
1225
1226fn retry_evidence_message(kind: RuntimeOperationTargetKind) -> &'static str {
1227    match kind {
1228        RuntimeOperationTargetKind::FunctionRun => {
1229            "Function Run was durably scheduled for another runtime attempt"
1230        }
1231        RuntimeOperationTargetKind::OutboxEvent => {
1232            "Outbox Event was durably scheduled for another delivery attempt"
1233        }
1234    }
1235}
1236
1237fn snapshot(
1238    service_id: &str,
1239    service_revision: &str,
1240    row: RuntimeTargetRow,
1241    now_unix_ms: u64,
1242) -> Result<RuntimeOperationTargetSnapshot, RuntimeOperationsError> {
1243    let (target, target_name, status, attempts, max_attempts, target_revision) = match row {
1244        RuntimeTargetRow::FunctionRun(row) => (
1245            RuntimeOperationTarget {
1246                kind: RuntimeOperationTargetKind::FunctionRun,
1247                target_id: row.id.clone(),
1248            },
1249            row.function_name.clone(),
1250            parse_status(RuntimeOperationTargetKind::FunctionRun, &row.status)?,
1251            checked_count(row.attempts, "Function Run attempt count")?,
1252            checked_count(row.max_attempts, "Function Run maximum attempt count")?,
1253            digest_json(&(
1254                &row.id,
1255                &row.function_name,
1256                &row.status,
1257                row.attempts,
1258                row.max_attempts,
1259                row.updated_at.timestamp_micros(),
1260            )),
1261        ),
1262        RuntimeTargetRow::OutboxEvent(row) => (
1263            RuntimeOperationTarget {
1264                kind: RuntimeOperationTargetKind::OutboxEvent,
1265                target_id: row.id.clone(),
1266            },
1267            row.event_name.clone(),
1268            parse_status(RuntimeOperationTargetKind::OutboxEvent, &row.status)?,
1269            checked_count(row.attempts, "Outbox Event attempt count")?,
1270            checked_count(row.max_attempts, "Outbox Event maximum attempt count")?,
1271            digest_json(&(
1272                &row.id,
1273                &row.event_name,
1274                &row.status,
1275                row.attempts,
1276                row.max_attempts,
1277                row.available_at.timestamp_micros(),
1278                row.locked_at.map(|value| value.timestamp_micros()),
1279                row.published_at.map(|value| value.timestamp_micros()),
1280                row.created_at.timestamp_micros(),
1281            )),
1282        ),
1283    };
1284    Ok(RuntimeOperationTargetSnapshot {
1285        protocol: RUNTIME_OPERATIONS_PROTOCOL.to_owned(),
1286        service_id: service_id.to_owned(),
1287        service_revision: service_revision.to_owned(),
1288        target,
1289        target_revision,
1290        observed_at_unix_ms: now_unix_ms,
1291        target_name,
1292        status,
1293        attempts,
1294        max_attempts,
1295    })
1296}
1297
1298fn checked_count(value: i32, label: &str) -> Result<u32, RuntimeOperationsError> {
1299    u32::try_from(value).map_err(|_| {
1300        operation_error(
1301            RuntimeOperationsErrorCode::StoreUnavailable,
1302            format!("{label} is invalid in the Service Store"),
1303        )
1304    })
1305}
1306
1307fn parse_status(
1308    kind: RuntimeOperationTargetKind,
1309    status: &str,
1310) -> Result<RuntimeOperationTargetStatus, RuntimeOperationsError> {
1311    match (kind, status) {
1312        (RuntimeOperationTargetKind::FunctionRun, "pending")
1313        | (RuntimeOperationTargetKind::OutboxEvent, "pending") => {
1314            Ok(RuntimeOperationTargetStatus::Pending)
1315        }
1316        (RuntimeOperationTargetKind::FunctionRun, "processing")
1317        | (RuntimeOperationTargetKind::OutboxEvent, "processing") => {
1318            Ok(RuntimeOperationTargetStatus::Processing)
1319        }
1320        (RuntimeOperationTargetKind::FunctionRun, "running") => {
1321            Ok(RuntimeOperationTargetStatus::Running)
1322        }
1323        (RuntimeOperationTargetKind::FunctionRun, "completed") => {
1324            Ok(RuntimeOperationTargetStatus::Completed)
1325        }
1326        (RuntimeOperationTargetKind::OutboxEvent, "published") => {
1327            Ok(RuntimeOperationTargetStatus::Published)
1328        }
1329        (RuntimeOperationTargetKind::FunctionRun, "failed")
1330        | (RuntimeOperationTargetKind::OutboxEvent, "failed") => {
1331            Ok(RuntimeOperationTargetStatus::Failed)
1332        }
1333        (RuntimeOperationTargetKind::FunctionRun, "dead")
1334        | (RuntimeOperationTargetKind::OutboxEvent, "dead") => {
1335            Ok(RuntimeOperationTargetStatus::Dead)
1336        }
1337        _ => Err(operation_error(
1338            RuntimeOperationsErrorCode::StoreUnavailable,
1339            "Runtime Operation target has an unsupported stored status",
1340        )),
1341    }
1342}
1343
1344fn ensure_retryable(status: RuntimeOperationTargetStatus) -> Result<(), RuntimeOperationsError> {
1345    if matches!(
1346        status,
1347        RuntimeOperationTargetStatus::Failed | RuntimeOperationTargetStatus::Dead
1348    ) {
1349        Ok(())
1350    } else {
1351        Err(operation_error(
1352            RuntimeOperationsErrorCode::TargetNotRetryable,
1353            "Only failed or dead Runtime Operation targets can be retried",
1354        ))
1355    }
1356}
1357
1358fn rejection(error: RuntimeOperationsError) -> SystemPlaneRejection {
1359    let (status, code, next_action) = match error.code {
1360        RuntimeOperationsErrorCode::InvalidIntent
1361        | RuntimeOperationsErrorCode::InvalidEvidenceQuery
1362        | RuntimeOperationsErrorCode::InvalidEvidenceCursor
1363        | RuntimeOperationsErrorCode::PlanMismatch => (
1364            StatusCode::BAD_REQUEST,
1365            "runtime_operation_invalid_request",
1366            "rebuild_runtime_operation_plan",
1367        ),
1368        RuntimeOperationsErrorCode::NotFound => (
1369            StatusCode::NOT_FOUND,
1370            "runtime_operation_not_found",
1371            "refresh_runtime_observation",
1372        ),
1373        RuntimeOperationsErrorCode::TargetNotRetryable
1374        | RuntimeOperationsErrorCode::StaleTargetRevision
1375        | RuntimeOperationsErrorCode::IdempotencyConflict => (
1376            StatusCode::CONFLICT,
1377            "runtime_operation_conflict",
1378            "refresh_runtime_observation",
1379        ),
1380        RuntimeOperationsErrorCode::AuthorityUnavailable => (
1381            StatusCode::SERVICE_UNAVAILABLE,
1382            "runtime_operation_authority_unavailable",
1383            "configure_runtime_operation_authority",
1384        ),
1385        RuntimeOperationsErrorCode::AuthorityRejected => (
1386            StatusCode::FORBIDDEN,
1387            "runtime_operation_authority_rejected",
1388            "obtain_runtime_operation_approval",
1389        ),
1390        RuntimeOperationsErrorCode::StoreUnavailable => (
1391            StatusCode::SERVICE_UNAVAILABLE,
1392            "runtime_operation_store_unavailable",
1393            "restore_service_store",
1394        ),
1395    };
1396    SystemPlaneRejection::new(status, code, error.message, next_action)
1397}
1398
1399fn operation_error(
1400    code: RuntimeOperationsErrorCode,
1401    message: impl Into<String>,
1402) -> RuntimeOperationsError {
1403    RuntimeOperationsError {
1404        code,
1405        message: message.into(),
1406    }
1407}
1408
1409fn store_error(_source: sqlx::Error) -> RuntimeOperationsError {
1410    operation_error(
1411        RuntimeOperationsErrorCode::StoreUnavailable,
1412        "Runtime Operations Store operation failed",
1413    )
1414}
1415
1416fn serialization_error(_source: serde_json::Error) -> RuntimeOperationsError {
1417    operation_error(
1418        RuntimeOperationsErrorCode::StoreUnavailable,
1419        "Runtime Operations Store contains invalid evidence",
1420    )
1421}
1422
1423fn canonical_digest(value: &str) -> bool {
1424    value.strip_prefix("sha256:").is_some_and(|digest| {
1425        digest.len() == 64
1426            && digest
1427                .bytes()
1428                .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
1429    })
1430}
1431
1432fn digest_json<T: Serialize>(value: &T) -> String {
1433    let bytes = serde_json::to_vec(value).expect("Runtime Operations value serializes");
1434    format!("sha256:{}", hex(&Sha256::digest(bytes)))
1435}
1436
1437fn encode_evidence_cursor(
1438    cursor: &RuntimeOperationEvidenceCursor,
1439) -> Result<String, RuntimeOperationsError> {
1440    serde_json::to_vec(cursor)
1441        .map(|bytes| URL_SAFE_NO_PAD.encode(bytes))
1442        .map_err(serialization_error)
1443}
1444
1445fn decode_evidence_cursor(
1446    encoded: &str,
1447) -> Result<RuntimeOperationEvidenceCursor, RuntimeOperationsError> {
1448    if encoded.is_empty() || encoded.len() > 4096 {
1449        return Err(invalid_evidence_cursor());
1450    }
1451    let bytes = URL_SAFE_NO_PAD
1452        .decode(encoded)
1453        .map_err(|_| invalid_evidence_cursor())?;
1454    let cursor: RuntimeOperationEvidenceCursor =
1455        serde_json::from_slice(&bytes).map_err(|_| invalid_evidence_cursor())?;
1456    if cursor.protocol != RUNTIME_OPERATION_EVIDENCE_CURSOR_PROTOCOL
1457        || cursor.operation_id.trim().is_empty()
1458        || cursor.after_sequence == 0
1459        || cursor.after_sequence > i64::MAX as u64
1460    {
1461        return Err(invalid_evidence_cursor());
1462    }
1463    Ok(cursor)
1464}
1465
1466fn invalid_evidence_cursor() -> RuntimeOperationsError {
1467    operation_error(
1468        RuntimeOperationsErrorCode::InvalidEvidenceCursor,
1469        "Operation Evidence cursor is invalid",
1470    )
1471}
1472
1473fn hex(bytes: &[u8]) -> String {
1474    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
1475}
1476
1477fn to_i64(value: u64) -> Result<i64, RuntimeOperationsError> {
1478    i64::try_from(value).map_err(|_| {
1479        operation_error(
1480            RuntimeOperationsErrorCode::InvalidIntent,
1481            "Runtime Operation timestamp exceeds the supported Store range",
1482        )
1483    })
1484}
1485
1486fn now_unix_ms() -> u64 {
1487    std::time::SystemTime::now()
1488        .duration_since(std::time::UNIX_EPOCH)
1489        .map_or(0, |duration| duration.as_millis() as u64)
1490}
1491
1492#[cfg(test)]
1493mod tests {
1494    use super::*;
1495
1496    #[test]
1497    fn outbox_snapshot_is_revisioned_and_uses_outbox_feature_scope() {
1498        let timestamp = DateTime::from_timestamp(1_700_000_000, 0).unwrap();
1499        let snapshot = snapshot(
1500            "support",
1501            "release:sha256:0123456789abcdef",
1502            RuntimeTargetRow::OutboxEvent(OutboxEventRow {
1503                id: "event-1".to_owned(),
1504                event_name: "tickets.notified.v1".to_owned(),
1505                status: "dead".to_owned(),
1506                attempts: 3,
1507                max_attempts: 3,
1508                available_at: timestamp,
1509                locked_at: None,
1510                published_at: None,
1511                created_at: timestamp,
1512            }),
1513            1_700_000_001_000,
1514        )
1515        .unwrap();
1516
1517        assert_eq!(
1518            snapshot.target.kind,
1519            RuntimeOperationTargetKind::OutboxEvent
1520        );
1521        assert_eq!(snapshot.target_name, "tickets.notified.v1");
1522        assert_eq!(snapshot.status, RuntimeOperationTargetStatus::Dead);
1523        assert!(canonical_digest(&snapshot.target_revision));
1524        assert_eq!(
1525            target_feature(snapshot.target.kind),
1526            RUNTIME_OPERATIONS_FEATURE_OUTBOX_RETRY
1527        );
1528        assert_eq!(
1529            expected_effects(snapshot.target.kind),
1530            ["schedule the exact Outbox Event for one additional runtime attempt"]
1531        );
1532    }
1533
1534    #[test]
1535    fn stored_statuses_are_target_kind_specific_and_fail_closed() {
1536        assert_eq!(
1537            parse_status(RuntimeOperationTargetKind::FunctionRun, "completed").unwrap(),
1538            RuntimeOperationTargetStatus::Completed
1539        );
1540        assert_eq!(
1541            parse_status(RuntimeOperationTargetKind::OutboxEvent, "published").unwrap(),
1542            RuntimeOperationTargetStatus::Published
1543        );
1544        assert_eq!(
1545            parse_status(RuntimeOperationTargetKind::FunctionRun, "published")
1546                .unwrap_err()
1547                .code,
1548            RuntimeOperationsErrorCode::StoreUnavailable
1549        );
1550        assert_eq!(
1551            parse_status(RuntimeOperationTargetKind::OutboxEvent, "completed")
1552                .unwrap_err()
1553                .code,
1554            RuntimeOperationsErrorCode::StoreUnavailable
1555        );
1556    }
1557
1558    #[test]
1559    fn evidence_cursor_is_opaque_scoped_and_strict() {
1560        let cursor = RuntimeOperationEvidenceCursor {
1561            protocol: RUNTIME_OPERATION_EVIDENCE_CURSOR_PROTOCOL.to_owned(),
1562            operation_id: "runtime-operation:01".to_owned(),
1563            after_sequence: 2,
1564        };
1565        let encoded = encode_evidence_cursor(&cursor).unwrap();
1566        assert_eq!(decode_evidence_cursor(&encoded).unwrap(), cursor);
1567        assert_eq!(
1568            decode_evidence_cursor("not-a-cursor").unwrap_err().code,
1569            RuntimeOperationsErrorCode::InvalidEvidenceCursor
1570        );
1571
1572        let invalid = RuntimeOperationEvidenceCursor {
1573            protocol: "wrong".to_owned(),
1574            ..cursor
1575        };
1576        assert_eq!(
1577            decode_evidence_cursor(&encode_evidence_cursor(&invalid).unwrap())
1578                .unwrap_err()
1579                .code,
1580            RuntimeOperationsErrorCode::InvalidEvidenceCursor
1581        );
1582    }
1583}