Skip to main content

lenso_module_management/
contracts.rs

1use chrono::{DateTime, Utc};
2use lenso_contracts::{
3    ArtifactReference, ModuleDelivery, ModuleLifecycleState, ModuleVerificationCell,
4    VerificationEvaluation, digest_json,
5};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10pub const DESIRED_MODULE_COMPOSITION_PROTOCOL: &str = "lenso.desired-module-composition.v1";
11pub const APPLICATION_MODULE_LOCK_PROTOCOL: &str = "lenso.application-module-lock.v1";
12pub const MODULE_CHANGE_PLAN_PROTOCOL: &str = "lenso.module-change-plan.v1";
13pub const MODULE_REPAIR_PLAN_PROTOCOL: &str = "lenso.module-repair-plan.v1";
14pub const MODULE_ENVIRONMENT_POLICY_PROTOCOL: &str = "lenso.module-environment-policy.v1";
15pub const MODULE_APPROVAL_PROTOCOL: &str = "lenso.module-approval.v1";
16pub const MODULE_OPERATION_PROTOCOL: &str = "lenso.module-operation.v1";
17pub const MODULE_OPERATION_JOURNAL_PROTOCOL: &str = "lenso.module-operation-journal.v1";
18pub const LINKED_COMPOSITION_SEAM_PROTOCOL: &str = "lenso.linked-composition-seam.v1";
19pub const MODULE_PLANNING_CONTEXT_PROTOCOL: &str = "lenso.module-planning-context.v1";
20pub const MODULE_MANAGEMENT_SNAPSHOT_PROTOCOL: &str = "lenso.module-management-snapshot.v1";
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
23#[serde(deny_unknown_fields)]
24pub struct LinkedCompositionSeam {
25    pub protocol: String,
26    pub host_manifest_path: String,
27    pub host_source_path: String,
28    pub generated_crate_path: String,
29    pub dependency_name: String,
30    pub lenso_version: String,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
34#[serde(deny_unknown_fields)]
35pub struct ModulePlanningContext {
36    pub protocol: String,
37    pub system_id: String,
38    pub application_id: String,
39    pub environment_id: String,
40    pub expected_target_revision: u64,
41    pub catalog_snapshot_digest: String,
42    pub trust_policy_digest: String,
43    pub compatibility_evidence_digest: String,
44    pub resolver_version: String,
45    pub candidates: Vec<crate::ModuleResolutionCandidate>,
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub service_deployments: Vec<ServiceDeploymentBinding>,
48    #[serde(default)]
49    pub cargo_offline: bool,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
53#[serde(rename_all = "snake_case")]
54pub enum ServiceDeploymentAdapterKind {
55    Local,
56    ExternallyManaged,
57    Kubernetes,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
61#[serde(tag = "kind", rename_all = "snake_case")]
62pub enum ServiceDeploymentAction {
63    Command {
64        program: String,
65        #[serde(default, skip_serializing_if = "Vec::is_empty")]
66        args: Vec<String>,
67        #[serde(default, skip_serializing_if = "Option::is_none")]
68        working_directory: Option<String>,
69    },
70    Evidence {
71        receipt: ArtifactReference,
72    },
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
76#[serde(deny_unknown_fields)]
77pub struct ServiceDeploymentBinding {
78    pub service_id: String,
79    pub service_release_digest: String,
80    pub adapter: ServiceDeploymentAdapterKind,
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub installation: Option<crate::ServiceInstallation>,
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub install: Option<ServiceDeploymentAction>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub remove: Option<ServiceDeploymentAction>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub restart: Option<ServiceDeploymentAction>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
92#[serde(rename_all = "snake_case")]
93pub enum ModuleManagementSnapshotStatus {
94    Ready,
95    Unconfigured,
96    Invalid,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
100#[serde(deny_unknown_fields)]
101pub struct ModuleManagementSnapshot {
102    pub protocol: String,
103    pub status: ModuleManagementSnapshotStatus,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub application_id: Option<String>,
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub desired: Option<DesiredModuleComposition>,
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub desired_digest: Option<String>,
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub application_lock: Option<ApplicationModuleLock>,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub application_lock_digest: Option<String>,
114    pub planning_available: bool,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub planning_context_digest: Option<String>,
117    pub execution_available: bool,
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub environment_policy: Option<ModuleEnvironmentPolicy>,
120    #[serde(default, skip_serializing_if = "Vec::is_empty")]
121    pub issues: Vec<String>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
125#[serde(deny_unknown_fields)]
126pub struct DesiredModuleComposition {
127    pub protocol: String,
128    pub application_id: String,
129    pub revision: u64,
130    pub selected: Vec<DesiredModuleSelection>,
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub local_overrides: Vec<LocalModuleOverride>,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
136#[serde(deny_unknown_fields)]
137pub struct DesiredModuleSelection {
138    pub module_id: String,
139    pub version_requirement: String,
140    #[serde(default, skip_serializing_if = "Vec::is_empty")]
141    pub optional_requirements: Vec<String>,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub exact_release_digest: Option<String>,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub delivery_preference: Option<ManagedDeliveryKind>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
149#[serde(deny_unknown_fields)]
150pub struct LocalModuleOverride {
151    pub module_id: String,
152    pub path: String,
153    pub content_digest: String,
154    pub acknowledged_unverified: bool,
155}
156
157#[derive(
158    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
159)]
160#[serde(rename_all = "snake_case")]
161pub enum ManagedDeliveryKind {
162    Linked,
163    Provider,
164    Autonomous,
165}
166
167impl From<&ModuleDelivery> for ManagedDeliveryKind {
168    fn from(value: &ModuleDelivery) -> Self {
169        match value {
170            ModuleDelivery::Linked(_) => Self::Linked,
171            ModuleDelivery::Service(service) => match service.responsibility_profile {
172                lenso_contracts::ServiceResponsibilityProfile::Provider => Self::Provider,
173                lenso_contracts::ServiceResponsibilityProfile::Autonomous => Self::Autonomous,
174            },
175        }
176    }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
180#[serde(deny_unknown_fields)]
181pub struct ApplicationModuleLock {
182    pub protocol: String,
183    pub application_id: String,
184    pub desired_composition_digest: String,
185    pub catalog_snapshot_digest: String,
186    pub trust_policy_digest: String,
187    pub resolver_version: String,
188    pub modules: Vec<LockedModule>,
189    #[serde(default, skip_serializing_if = "Vec::is_empty")]
190    pub capability_bindings: Vec<LockedCapabilityBinding>,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
194#[serde(deny_unknown_fields)]
195pub struct LockedModule {
196    pub module_id: String,
197    pub version: String,
198    pub release_digest: String,
199    pub manifest_digest: String,
200    pub delivery: ModuleDelivery,
201    pub reason: LockedModuleReason,
202    #[serde(default, skip_serializing_if = "Vec::is_empty")]
203    pub dependency_module_ids: Vec<String>,
204    #[serde(default, skip_serializing_if = "Vec::is_empty")]
205    pub crate_features: Vec<String>,
206    #[serde(default, skip_serializing_if = "Vec::is_empty")]
207    pub migration_artifacts: Vec<ArtifactReference>,
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub console_ui_artifact: Option<LockedConsoleUiArtifact>,
210    pub verification: VerificationEvaluation,
211    pub verification_cell: ModuleVerificationCell,
212    pub lifecycle: ModuleLifecycleState,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub local_override_digest: Option<String>,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
218#[serde(rename_all = "snake_case")]
219pub enum LockedModuleReason {
220    Direct,
221    Transitive,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
225#[serde(deny_unknown_fields)]
226pub struct LockedCapabilityBinding {
227    pub capability: String,
228    pub provider_module_id: String,
229    pub consumer_module_id: String,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct LockedConsoleUiArtifact {
235    pub locator: String,
236    pub digest: String,
237    pub format: lenso_contracts::ConsoleUiArtifactFormat,
238    pub protocol_major: u32,
239    pub entry: String,
240    pub entries: Vec<lenso_contracts::ConsoleUiArtifactEntry>,
241    pub style_assets: Vec<lenso_contracts::ConsoleUiArtifactStyleAsset>,
242    pub manifest: lenso_contracts::ConsoleModuleManifest,
243    pub requested_permissions: Vec<lenso_contracts::ConsolePermissionRequest>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
247#[serde(deny_unknown_fields)]
248pub struct ConsoleCompositionArtifact {
249    pub module_id: String,
250    pub module_release_digest: String,
251    pub locator: String,
252    pub digest: String,
253    pub format: lenso_contracts::ConsoleUiArtifactFormat,
254    pub protocol_major: u32,
255    pub entry: String,
256    pub entries: Vec<lenso_contracts::ConsoleUiArtifactEntry>,
257    pub style_assets: Vec<lenso_contracts::ConsoleUiArtifactStyleAsset>,
258    pub manifest: lenso_contracts::ConsoleModuleManifest,
259    pub requested_permissions: Vec<lenso_contracts::ConsolePermissionRequest>,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
263#[serde(tag = "kind", rename_all = "snake_case")]
264pub enum ModuleRootChange {
265    Install {
266        selection: DesiredModuleSelection,
267    },
268    Update {
269        module_id: String,
270        version_requirement: String,
271    },
272    Uninstall {
273        module_id: String,
274    },
275    SelectOptional {
276        module_id: String,
277        requirement: String,
278        selected: bool,
279    },
280    SwitchDelivery {
281        module_id: String,
282        delivery: ManagedDeliveryKind,
283    },
284    Restore {
285        target_lock_digest: String,
286    },
287    Repair {
288        repair_plan_digest: String,
289    },
290}
291
292#[derive(
293    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
294)]
295#[serde(rename_all = "snake_case")]
296pub enum EnvironmentManagementMode {
297    Disabled,
298    ReadOnly,
299    Full,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
303#[serde(deny_unknown_fields)]
304pub struct ModuleEnvironmentPolicy {
305    pub protocol: String,
306    pub policy_id: String,
307    pub revision: String,
308    pub mode: EnvironmentManagementMode,
309    pub require_distinct_approver: bool,
310    pub maximum_approval_age_seconds: u64,
311    pub maximum_lease_seconds: u64,
312    pub require_backup_for_non_local_destructive_effects: bool,
313}
314
315#[derive(
316    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
317)]
318#[serde(rename_all = "snake_case")]
319pub enum ModuleRiskClass {
320    Ordinary,
321    DestructiveMigration,
322    DataDeletion,
323    BackupRestore,
324    BackupWaiver,
325    TrustOverride,
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
329#[serde(deny_unknown_fields)]
330pub struct ModuleApprovalBoundary {
331    pub boundary_id: String,
332    pub risk_class: ModuleRiskClass,
333    pub required_authority: String,
334    pub effect_ids: Vec<String>,
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub backup_evidence_digest: Option<String>,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
340#[serde(deny_unknown_fields)]
341pub struct ModuleApproval {
342    pub protocol: String,
343    pub approval_id: String,
344    pub plan_digest: String,
345    pub application_id: String,
346    pub environment_id: String,
347    pub expected_target_revision: u64,
348    pub boundary_id: String,
349    pub risk_class: ModuleRiskClass,
350    pub actor_id: String,
351    pub verified_authorities: Vec<String>,
352    pub reason: String,
353    pub issued_at: DateTime<Utc>,
354    pub expires_at: DateTime<Utc>,
355    pub nonce: String,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
359#[serde(deny_unknown_fields)]
360pub struct ModuleChangePlan {
361    pub protocol: String,
362    pub plan_id: String,
363    pub plan_digest: String,
364    pub application_id: String,
365    pub environment_id: String,
366    pub expected_target_revision: u64,
367    pub request: ModuleRootChange,
368    pub current_desired_digest: String,
369    pub target_desired: DesiredModuleComposition,
370    pub target_desired_digest: String,
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub current_lock_digest: Option<String>,
373    pub target_lock: ApplicationModuleLock,
374    pub target_lock_digest: String,
375    pub catalog_snapshot_digest: String,
376    pub resolver_version: String,
377    pub trust_policy_digest: String,
378    pub compatibility_evidence_digest: String,
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub cargo_lock_candidate: Option<crate::CargoLockCandidate>,
381    pub read_set: Vec<ModulePathPrecondition>,
382    pub effects: Vec<ModulePlanEffect>,
383    #[serde(default, skip_serializing_if = "Vec::is_empty")]
384    pub approval_boundaries: Vec<ModuleApprovalBoundary>,
385    pub validation_commands: Vec<String>,
386    pub next_actions: Vec<String>,
387    pub created_at: DateTime<Utc>,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
391#[serde(deny_unknown_fields)]
392pub struct ModulePathPrecondition {
393    pub path: String,
394    pub existence: PathExistence,
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub content_digest: Option<String>,
397    pub file_type: ManagedFileType,
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub mode: Option<u32>,
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
403#[serde(rename_all = "snake_case")]
404pub enum PathExistence {
405    Present,
406    Absent,
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
410#[serde(rename_all = "snake_case")]
411pub enum ManagedFileType {
412    Regular,
413    Directory,
414    Absent,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
418#[serde(rename_all = "snake_case")]
419pub enum ModuleFileOwnership {
420    User,
421    Generated,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
425#[serde(tag = "kind", rename_all = "snake_case")]
426pub enum ModulePlanEffect {
427    WorkspaceFile {
428        effect_id: String,
429        path: String,
430        ownership: ModuleFileOwnership,
431        change: ModuleFileChange,
432        #[serde(default, skip_serializing_if = "Option::is_none")]
433        before_digest: Option<String>,
434        #[serde(default, skip_serializing_if = "Option::is_none")]
435        after_digest: Option<String>,
436        #[serde(default, skip_serializing_if = "Option::is_none")]
437        after_content: Option<String>,
438        #[serde(default, skip_serializing_if = "Option::is_none")]
439        after_mode: Option<u32>,
440        patch: String,
441        reversible_before_migration: bool,
442    },
443    Configuration {
444        effect_id: String,
445        config_revision_digest: String,
446        unresolved_required_fields: Vec<String>,
447    },
448    Migration {
449        effect_id: String,
450        module_id: String,
451        release_digest: String,
452        migration_id: String,
453        artifact_locator: String,
454        artifact_digest: String,
455        store_scope: String,
456        execution: MigrationExecutionMode,
457        risk_class: ModuleRiskClass,
458    },
459    Protected {
460        effect_id: String,
461        risk_class: ModuleRiskClass,
462        subject: String,
463        evidence_digest: String,
464    },
465    ConsoleComposition {
466        effect_id: String,
467        console_service_id: String,
468        candidate_lock_digest: String,
469        artifacts: Vec<ConsoleCompositionArtifact>,
470    },
471    ServiceInstallation {
472        effect_id: String,
473        service_id: String,
474        service_release_digest: String,
475        #[serde(default, skip_serializing_if = "Option::is_none")]
476        installation_plan: Option<crate::ServiceInstallationPlan>,
477        #[serde(default, skip_serializing_if = "Option::is_none")]
478        adapter: Option<ServiceDeploymentAdapterKind>,
479        #[serde(default, skip_serializing_if = "Option::is_none")]
480        action: Option<ServiceDeploymentAction>,
481    },
482    ServiceRemoval {
483        effect_id: String,
484        service_id: String,
485        service_release_digest: String,
486        #[serde(default, skip_serializing_if = "Option::is_none")]
487        adapter: Option<ServiceDeploymentAdapterKind>,
488        #[serde(default, skip_serializing_if = "Option::is_none")]
489        action: Option<ServiceDeploymentAction>,
490    },
491    ServiceRestart {
492        effect_id: String,
493        service_id: String,
494        service_release_digest: String,
495        #[serde(default, skip_serializing_if = "Option::is_none")]
496        adapter: Option<ServiceDeploymentAdapterKind>,
497        #[serde(default, skip_serializing_if = "Option::is_none")]
498        action: Option<ServiceDeploymentAction>,
499    },
500    Validate {
501        effect_id: String,
502        command: String,
503        expected_evidence: String,
504    },
505    Restart {
506        effect_id: String,
507        target: String,
508    },
509    Activate {
510        effect_id: String,
511        target_lock_digest: String,
512    },
513}
514
515impl ModulePlanEffect {
516    #[must_use]
517    pub fn effect_id(&self) -> &str {
518        match self {
519            Self::WorkspaceFile { effect_id, .. }
520            | Self::Configuration { effect_id, .. }
521            | Self::Migration { effect_id, .. }
522            | Self::Protected { effect_id, .. }
523            | Self::ConsoleComposition { effect_id, .. }
524            | Self::ServiceInstallation { effect_id, .. }
525            | Self::ServiceRemoval { effect_id, .. }
526            | Self::ServiceRestart { effect_id, .. }
527            | Self::Validate { effect_id, .. }
528            | Self::Restart { effect_id, .. }
529            | Self::Activate { effect_id, .. } => effect_id,
530        }
531    }
532
533    #[must_use]
534    pub fn risk_class(&self) -> ModuleRiskClass {
535        match self {
536            Self::Migration { risk_class, .. } | Self::Protected { risk_class, .. } => *risk_class,
537            _ => ModuleRiskClass::Ordinary,
538        }
539    }
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
543#[serde(rename_all = "snake_case")]
544pub enum ModuleFileChange {
545    Create,
546    Modify,
547    Delete,
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
551#[serde(rename_all = "snake_case")]
552pub enum MigrationExecutionMode {
553    Transactional,
554    IdempotentExternal,
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
558#[serde(deny_unknown_fields)]
559pub struct ModuleRepairPlan {
560    pub protocol: String,
561    pub repair_plan_id: String,
562    pub repair_plan_digest: String,
563    pub original_operation_id: String,
564    pub original_operation_revision: u64,
565    pub application_id: String,
566    pub environment_id: String,
567    pub observed_state_digest: String,
568    pub completed_effect_ids: Vec<String>,
569    pub actions: Vec<ModuleRepairAction>,
570    pub approval_boundaries: Vec<ModuleApprovalBoundary>,
571    pub created_at: DateTime<Utc>,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
575#[serde(tag = "kind", rename_all = "snake_case")]
576pub enum ModuleRepairAction {
577    Resume {
578        effect_ids: Vec<String>,
579    },
580    ForwardFix {
581        module_release_digest: String,
582    },
583    RestoreWorkspace {
584        path_digests: BTreeMap<String, String>,
585    },
586    ReconcileComposition {
587        desired_digest: String,
588        lock_digest: String,
589    },
590    RestoreBackup {
591        backup_evidence_digest: String,
592        store_scope: String,
593    },
594    RecordIntervention {
595        evidence_digest: String,
596    },
597}
598
599#[derive(
600    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
601)]
602#[serde(rename_all = "snake_case")]
603pub enum ModuleOperationState {
604    Planned,
605    AwaitingApproval,
606    Ready,
607    ApplyingFiles,
608    FilesApplied,
609    StagingConfiguration,
610    Migrating,
611    Verifying,
612    Activating,
613    Succeeded,
614    Blocked,
615    Cancelled,
616    Restored,
617    RepairRequired,
618    Reconciled,
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
622#[serde(deny_unknown_fields)]
623pub struct ModuleOperation {
624    pub protocol: String,
625    pub operation_id: String,
626    pub idempotency_key: String,
627    pub application_id: String,
628    pub environment_id: String,
629    pub plan_digest: String,
630    pub operation_kind: ModuleOperationKind,
631    pub expected_target_revision: u64,
632    pub current_lock_digest: Option<String>,
633    pub target_lock_digest: String,
634    pub actor_id: String,
635    pub verified_authorities: Vec<String>,
636    pub policy_revision: String,
637    pub state: ModuleOperationState,
638    pub revision: u64,
639    pub fencing_token: u64,
640    pub attempt: u32,
641    #[serde(default, skip_serializing_if = "Vec::is_empty")]
642    pub approvals: Vec<ModuleApproval>,
643    #[serde(default, skip_serializing_if = "Vec::is_empty")]
644    pub effect_receipts: Vec<ModuleEffectReceipt>,
645    #[serde(default, skip_serializing_if = "Vec::is_empty")]
646    pub workspace_backups: Vec<ModuleWorkspaceBackup>,
647    #[serde(default, skip_serializing_if = "Vec::is_empty")]
648    pub errors: Vec<ModuleOperationError>,
649    #[serde(default, skip_serializing_if = "Vec::is_empty")]
650    pub next_actions: Vec<String>,
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub repair_operation_id: Option<String>,
653    pub updated_at: DateTime<Utc>,
654}
655
656#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
657#[serde(deny_unknown_fields)]
658pub struct ModuleWorkspaceBackup {
659    pub path: String,
660    pub existence: PathExistence,
661    pub file_type: ManagedFileType,
662    #[serde(default, skip_serializing_if = "Option::is_none")]
663    pub content_base64: Option<String>,
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub content_digest: Option<String>,
666    #[serde(default, skip_serializing_if = "Option::is_none")]
667    pub mode: Option<u32>,
668}
669
670#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
671#[serde(rename_all = "snake_case")]
672pub enum ModuleOperationKind {
673    Install,
674    Update,
675    Uninstall,
676    DeliveryTransition,
677    Restore,
678    Repair,
679}
680
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
682#[serde(deny_unknown_fields)]
683pub struct ModuleEffectReceipt {
684    pub receipt_id: String,
685    pub effect_id: String,
686    pub effect_digest: String,
687    pub operation_id: String,
688    pub attempt: u32,
689    pub fencing_token: u64,
690    pub outcome: ModuleEffectOutcome,
691    pub evidence_references: Vec<ArtifactReference>,
692    pub committed_at: DateTime<Utc>,
693}
694
695#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
696#[serde(rename_all = "snake_case")]
697pub enum ModuleEffectOutcome {
698    Applied,
699    AlreadyApplied,
700    Verified,
701    Restored,
702    Activated,
703}
704
705#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
706#[serde(deny_unknown_fields)]
707pub struct ModuleOperationError {
708    pub code: String,
709    pub message: String,
710    pub evidence_references: Vec<ArtifactReference>,
711    pub recorded_at: DateTime<Utc>,
712}
713
714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
715#[serde(deny_unknown_fields)]
716pub struct ModuleOperationLease {
717    pub application_id: String,
718    pub holder_id: String,
719    pub fencing_token: u64,
720    pub revision: u64,
721    pub acquired_at: DateTime<Utc>,
722    pub expires_at: DateTime<Utc>,
723}
724
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
726#[serde(deny_unknown_fields)]
727pub struct ModuleResumeEvidence {
728    pub plan_digest: String,
729    pub observed_target_digest: String,
730    pub completed_effect_ids: Vec<String>,
731    pub next_effect_id: String,
732    pub next_effect_idempotent: bool,
733    pub observed_at: DateTime<Utc>,
734}
735
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
737#[serde(deny_unknown_fields)]
738pub struct ModuleOperationJournalEvent {
739    pub protocol: String,
740    pub event_id: String,
741    pub operation_id: String,
742    pub attempt: u32,
743    pub revision: u64,
744    pub prior_event_digest: Option<String>,
745    pub plan_digest: String,
746    pub actor_id: String,
747    pub fencing_token: u64,
748    pub transition: ModuleOperationTransition,
749    pub outcome_code: String,
750    pub evidence_references: Vec<ArtifactReference>,
751    pub operation_after: ModuleOperation,
752    pub recorded_at: DateTime<Utc>,
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
756#[serde(deny_unknown_fields)]
757pub struct ModuleOperationTransition {
758    pub from: ModuleOperationState,
759    pub to: ModuleOperationState,
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
763#[serde(deny_unknown_fields)]
764pub struct ModuleOperationJournal {
765    pub protocol: String,
766    pub operation_id: String,
767    pub events: Vec<ModuleOperationJournalEvent>,
768}
769
770pub fn desired_composition_digest(
771    composition: &DesiredModuleComposition,
772) -> Result<String, serde_json::Error> {
773    digest_json(composition)
774}
775
776pub fn application_module_lock_digest(
777    module_lock: &ApplicationModuleLock,
778) -> Result<String, serde_json::Error> {
779    digest_json(module_lock)
780}
781
782pub fn module_change_plan_digest(plan: &ModuleChangePlan) -> Result<String, serde_json::Error> {
783    let mut unsigned = plan.clone();
784    unsigned.plan_digest.clear();
785    digest_json(&unsigned)
786}
787
788pub fn module_repair_plan_digest(plan: &ModuleRepairPlan) -> Result<String, serde_json::Error> {
789    let mut unsigned = plan.clone();
790    unsigned.repair_plan_digest.clear();
791    digest_json(&unsigned)
792}
793
794pub fn journal_event_digest(
795    event: &ModuleOperationJournalEvent,
796) -> Result<String, serde_json::Error> {
797    digest_json(event)
798}