1use 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}