Skip to main content

canic_host/fleet_ensure/model/
mod.rs

1//! Module: fleet_ensure::model
2//!
3//! Responsibility: own current-generation Fleet reconciliation state and conservation records.
4//! Does not own: transport parsing, policy decisions, persistence, or IC effects.
5//! Boundary: workflow persists these records before and after every effect.
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10pub const FLEET_ENSURE_SCHEMA_VERSION: u16 = 1;
11pub const MAX_FLEET_ENSURE_CANISTERS: usize = 4_096;
12pub const MAX_FLEET_ENSURE_PROTOCOL_STEPS: usize = 4_096;
13
14/// One exact observed canister state used by planning and effect reconciliation.
15
16#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
17pub struct LiveCanister {
18    pub canister_version: Option<u64>,
19    pub controllers: Vec<String>,
20    #[serde(with = "u128_text")]
21    pub cycles: u128,
22    pub module_sha256: Option<String>,
23    pub principal: String,
24    pub reinstall_required: bool,
25    pub root_owned_lifecycle: Option<RootOwnedCanisterLifecycle>,
26    pub status: CanisterRuntimeStatus,
27}
28
29/// Root-owned lifecycle of one explicitly seeded paid canister.
30
31#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum RootOwnedCanisterLifecycle {
34    Claimed,
35    Idle,
36    /// Last exact current-generation balance retained while Root status is fenced.
37    Retained,
38    Store,
39    Workload,
40}
41
42/// Management status relevant to reconciliation.
43
44#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
45#[serde(rename_all = "snake_case")]
46pub enum CanisterRuntimeStatus {
47    Running,
48    Stopped,
49    Stopping,
50}
51
52/// Complete live snapshot of the explicitly controlled Fleet estate.
53
54#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
55pub struct FleetObservation {
56    pub additional_controlled_cycles: BTreeMap<String, u128>,
57    pub canisters: BTreeMap<String, Option<LiveCanister>>,
58    #[serde(with = "u128_text")]
59    pub ledger_fee_cycles: u128,
60    #[serde(with = "u128_text")]
61    pub operator_cycles: u128,
62    pub protocol_ready: BTreeMap<String, bool>,
63}
64
65/// Maintained disposition of one configured canister.
66
67#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
68#[serde(rename_all = "snake_case")]
69pub enum DesiredPresence {
70    Absent,
71    Present,
72}
73
74/// Exact method used by a controlled source to transfer its cycles before retirement.
75
76#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77#[serde(deny_unknown_fields)]
78pub struct DrainAuthority {
79    pub candid: String,
80    pub method: String,
81    pub destination: String,
82    pub maximum_execution_burn_cycles: String,
83}
84
85/// Current desired state for one controlled canister.
86
87#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
88#[serde(deny_unknown_fields)]
89pub struct DesiredCanister {
90    #[serde(default)]
91    pub canic_init: Option<DesiredCanisterInit>,
92    #[serde(default)]
93    pub controller_canisters: Vec<String>,
94    pub controllers: Vec<String>,
95    pub drain: Option<DrainAuthority>,
96    pub initial_cycles: String,
97    pub init_arg: Option<String>,
98    pub init_candid: Option<String>,
99    pub kind: DesiredCanisterKind,
100    pub minimum_cycles: String,
101    pub name: String,
102    pub parent: Option<String>,
103    pub presence: DesiredPresence,
104    pub principal: Option<String>,
105    #[serde(skip)]
106    pub(crate) protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
107    pub replace: bool,
108    pub subnet: String,
109    pub wasm: Option<String>,
110}
111
112/// Canic-owned role class used to derive Fleet control-plane choreography.
113#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
114#[serde(rename_all = "snake_case")]
115pub enum DesiredCanisterKind {
116    Auxiliary,
117    Component,
118    Coordinator,
119    Pool,
120    Root,
121    Store,
122}
123
124/// Canic-owned typed initialization role for one infrastructure canister.
125#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
126#[serde(rename_all = "snake_case", tag = "role")]
127pub enum DesiredCanisterInit {
128    Coordinator,
129    Root { root: String },
130    Store { root: String },
131}
132
133/// One current-only idempotent protocol transition owned by the Fleet journal.
134///
135/// The three argument documents use Candid text. They may bind a dynamically
136/// created canister with `{{principal:<logical-name>}}`, the retained operation
137/// string with `{{operation_id}}`, or its 32-byte value with
138/// `{{operation_id_blob}}`.
139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140#[serde(deny_unknown_fields)]
141pub(crate) struct DesiredProtocolStep {
142    pub canister: String,
143    pub candid: String,
144    pub command_args: String,
145    pub command_method: String,
146    pub expected_status: String,
147    pub maximum_execution_burn_cycles: String,
148    pub name: String,
149    pub status_args: String,
150    pub status_method: String,
151}
152
153/// Current-only desired Fleet contract consumed by `canic fleet ensure`.
154
155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156#[serde(deny_unknown_fields)]
157pub struct DesiredFleet {
158    #[serde(default)]
159    pub bootstrap: Option<DesiredFleetBootstrap>,
160    pub canisters: Vec<DesiredCanister>,
161    pub cycles_ledger: String,
162    pub environment: String,
163    pub fleet: String,
164    pub ledger_fee_cycles: String,
165    pub management_creation_fee_cycles: String,
166    pub material_cycle_threshold: String,
167    pub maximum_observation_burn_cycles: String,
168    pub maximum_stalled_observations: u32,
169    pub maximum_update_burn_cycles: String,
170    pub operator: String,
171    #[serde(default)]
172    pub protocol: Option<DesiredFleetProtocol>,
173    #[serde(skip)]
174    pub(crate) protocol_steps: Vec<DesiredProtocolStep>,
175    pub schema_version: u16,
176    /// Logical name of the present controlled canister that owns retirement receipts.
177    pub treasury: String,
178}
179
180/// Complete current-generation Canic initialization authority generated from App and Fleet input.
181#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
182#[serde(deny_unknown_fields)]
183pub struct DesiredFleetBootstrap {
184    pub admission: canic_core::ids::FleetAdmissionPolicyTemplate,
185    pub app: canic_core::ids::AppId,
186    pub canonical_network_id: canic_core::ids::CanonicalNetworkId,
187    pub component_deployment_configuration:
188        canic_core::control_plane_support::config::ComponentDeploymentConfiguration,
189    pub coordinator: String,
190    pub coordinator_subnet: canic_core::ids::SubnetId,
191    pub fleet_id: canic_core::ids::FleetId,
192    pub fresh_estate: bool,
193    pub release_build_id: canic_core::ids::ReleaseBuildId,
194    pub root_funding: Option<canic_core::ids::FleetCoordinatorRootFundingPolicy>,
195    pub roots: Vec<DesiredFleetBootstrapRoot>,
196}
197
198/// One Root/Store pair's generated immutable initialization authority.
199#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
200#[serde(deny_unknown_fields)]
201pub struct DesiredFleetBootstrapRoot {
202    pub canister_pool_imports: Vec<String>,
203    pub component_admissions: Vec<canic_core::ids::ComponentSpecAdmission>,
204    pub component_topology_digest: canic_core::ids::ComponentTopologyDigest,
205    pub funding: canic_core::ids::FleetSubnetRootFundingAuthority,
206    pub limits: canic_core::ids::FleetSubnetRootLimits,
207    pub placement_subnet: canic_core::ids::SubnetId,
208    pub root: String,
209    pub store: String,
210}
211
212/// Current Canic-owned control-plane intent compiled from one App topology.
213#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
214#[serde(deny_unknown_fields)]
215pub struct DesiredFleetProtocol {
216    /// Canonical application `canic.toml` used to compile Component topology.
217    pub app_config: String,
218    /// Exact initial group placements assigned to declared Root roles.
219    pub component_group_placements: Vec<DesiredComponentGroupPlacement>,
220    /// Exact Coordinator Candid used only by Canic's typed protocol adapter.
221    pub coordinator_candid: String,
222    /// Exact Root Candid used only by Canic's typed protocol adapter.
223    pub root_candid: String,
224    /// Exact Store Candid used only by Canic's typed protocol adapter.
225    pub store_candid: String,
226}
227
228/// One typed initial Component Group placement owned by Canic policy.
229#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct DesiredComponentGroupPlacement {
232    pub deployment: String,
233    pub ordinal: u32,
234    pub root: String,
235}
236
237/// Content identities resolved from current desired-state artifact paths by ops.
238#[derive(Clone, Debug, Default, Eq, PartialEq)]
239pub struct DesiredFleetArtifacts {
240    pub drain_candid_sha256_by_canister: BTreeMap<String, String>,
241    pub init_arg_sha256_by_canister: BTreeMap<String, String>,
242    pub init_candid_sha256_by_canister: BTreeMap<String, String>,
243    pub wasm_sha256_by_canister: BTreeMap<String, String>,
244    pub(crate) protocol_by_step: BTreeMap<String, ProtocolArtifactDigests>,
245}
246
247/// Content identities for one declarative protocol transition.
248#[derive(Clone, Debug, Default, Eq, PartialEq)]
249pub(crate) struct ProtocolArtifactDigests {
250    pub candid_sha256: String,
251    pub command_args_sha256: String,
252    pub expected_status_sha256: String,
253    pub status_args_sha256: String,
254}
255
256/// Validated numeric funding bounds for one desired canister.
257
258#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
259pub struct CanisterCyclePolicy {
260    #[serde(with = "u128_text")]
261    pub initial_cycles: u128,
262    #[serde(with = "u128_text")]
263    pub minimum_cycles: u128,
264}
265
266/// Exact effect kind retained before execution.
267
268#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
269#[serde(rename_all = "snake_case", tag = "kind")]
270pub enum EnsureAction {
271    Create {
272        controller_canisters: Vec<String>,
273        controllers: Vec<String>,
274        created_at_time: u64,
275        ledger: String,
276        name: String,
277        #[serde(with = "u128_text")]
278        requested_initial_cycles: u128,
279        subnet: String,
280    },
281    Delete {
282        #[serde(with = "u128_text")]
283        maximum_remaining_cycles: u128,
284        name: String,
285        principal: String,
286    },
287    Fund {
288        #[serde(with = "u128_text")]
289        amount: u128,
290        created_at_time: u64,
291        #[serde(default, with = "u128_text", skip_serializing_if = "is_zero_u128")]
292        expected_post_cycles: u128,
293        #[serde(default, with = "u128_text", skip_serializing_if = "is_zero_u128")]
294        funding_deficit_cycles: u128,
295        #[serde(default, with = "u128_text", skip_serializing_if = "is_zero_u128")]
296        funding_margin_cycles: u128,
297        ledger: String,
298        name: String,
299        principal: String,
300    },
301    Install {
302        canic_init: Option<DesiredCanisterInit>,
303        init_arg: Option<String>,
304        init_arg_sha256: Option<String>,
305        init_candid: Option<String>,
306        init_candid_sha256: Option<String>,
307        mode: InstallMode,
308        name: String,
309        principal: String,
310        wasm: String,
311        wasm_sha256: String,
312    },
313    FleetProtocol {
314        action: Box<CurrentFleetProtocolAction>,
315        candid: String,
316        candid_sha256: String,
317        #[serde(with = "u128_text")]
318        maximum_execution_burn_cycles: u128,
319        name: String,
320        principal: String,
321    },
322    Protocol {
323        candid: String,
324        candid_sha256: String,
325        command_args: String,
326        command_args_sha256: String,
327        command_method: String,
328        expected_status: String,
329        expected_status_sha256: String,
330        #[serde(with = "u128_text")]
331        maximum_execution_burn_cycles: u128,
332        name: String,
333        principal: String,
334        status_args: String,
335        status_args_sha256: String,
336        status_method: String,
337    },
338    SetControllers {
339        controller_canisters: Vec<String>,
340        controllers: Vec<String>,
341        name: String,
342        principal: String,
343    },
344    Start {
345        name: String,
346        principal: String,
347    },
348    Stop {
349        name: String,
350        principal: String,
351    },
352    Transfer {
353        #[serde(with = "u128_text")]
354        amount: u128,
355        candid: String,
356        candid_sha256: String,
357        destination: String,
358        #[serde(with = "u128_text")]
359        maximum_execution_burn_cycles: u128,
360        method: String,
361        name: String,
362        principal: String,
363    },
364}
365
366impl EnsureAction {
367    #[must_use]
368    pub fn name(&self) -> &str {
369        match self {
370            Self::Create { name, .. }
371            | Self::Delete { name, .. }
372            | Self::FleetProtocol { name, .. }
373            | Self::Fund { name, .. }
374            | Self::Install { name, .. }
375            | Self::Protocol { name, .. }
376            | Self::SetControllers { name, .. }
377            | Self::Start { name, .. }
378            | Self::Stop { name, .. }
379            | Self::Transfer { name, .. } => name,
380        }
381    }
382}
383
384/// Closed current-only Canic choreography emitted from roles and topology.
385#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
386#[serde(rename_all = "snake_case", tag = "kind")]
387#[expect(
388    clippy::large_enum_variant,
389    reason = "each exact typed action is boxed by EnsureAction before durable retention"
390)]
391pub enum CurrentFleetProtocolAction {
392    ActivateRegistry {
393        expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
394        expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
395        request: canic_core::dto::fleet_registry::FleetRegistryActivationRequest,
396    },
397    ActivateRegistryMirror {
398        expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistryMirrorActivationResponse,
399        request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
400    },
401    AdoptStore {
402        request: canic_core::dto::fleet_subnet_root::FleetSubnetWasmStoreAdoptionRequest,
403    },
404    BootstrapStore {
405        expected: canic_core::dto::root_store::RootStoreBootstrapResponse,
406        request: canic_core::dto::root_store::RootStoreBootstrapRequest,
407    },
408    JoinRoot {
409        expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
410        expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
411        request: canic_core::dto::fleet_registry::FleetSubnetRootJoinRequest,
412    },
413    PrepareStoreChunkSet {
414        request: canic_control_plane::dto::template::TemplateChunkSetPrepareInput,
415    },
416    PrepareComponentRegistry {
417        expected: canic_core::dto::component_registry::RootComponentRegistryStatusResponse,
418        request: canic_core::dto::component_registry::RootComponentRegistryPreparationRequest,
419    },
420    ProvisionComponents {
421        plan_hash: [u8; 32],
422        request: canic_core::dto::component_provisioning::FleetComponentProvisioningPrepareRequest,
423    },
424    RecoverPoolLedger {
425        request: canic_core::dto::pool::PoolLedgerRecoveryRequest,
426    },
427    PublishStoreChunk {
428        request: canic_control_plane::dto::template::TemplateChunkInput,
429    },
430    StageStoreManifest {
431        request: canic_control_plane::dto::template::TemplateManifestInput,
432    },
433    SynchronizeRegistry {
434        expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncResponse,
435        request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
436    },
437}
438
439impl CurrentFleetProtocolAction {
440    /// Role that exclusively owns this current control-plane transition.
441    #[must_use]
442    pub const fn target_kind(&self) -> DesiredCanisterKind {
443        match self {
444            Self::ActivateRegistry { .. }
445            | Self::JoinRoot { .. }
446            | Self::ProvisionComponents { .. } => DesiredCanisterKind::Coordinator,
447            Self::ActivateRegistryMirror { .. }
448            | Self::AdoptStore { .. }
449            | Self::BootstrapStore { .. }
450            | Self::PrepareComponentRegistry { .. }
451            | Self::RecoverPoolLedger { .. }
452            | Self::SynchronizeRegistry { .. } => DesiredCanisterKind::Root,
453            Self::PrepareStoreChunkSet { .. }
454            | Self::PublishStoreChunk { .. }
455            | Self::StageStoreManifest { .. } => DesiredCanisterKind::Store,
456        }
457    }
458
459    /// Durable operation identity when this transition has a protocol-owned replay key.
460    #[must_use]
461    pub const fn operation_id(&self) -> Option<[u8; 32]> {
462        match self {
463            Self::ActivateRegistry { .. }
464            | Self::JoinRoot { .. }
465            | Self::PrepareComponentRegistry { .. }
466            | Self::PrepareStoreChunkSet { .. }
467            | Self::PublishStoreChunk { .. }
468            | Self::StageStoreManifest { .. } => None,
469            Self::ActivateRegistryMirror { request, .. }
470            | Self::SynchronizeRegistry { request, .. } => Some(request.operation_id),
471            Self::AdoptStore { request } => Some(request.operation_id),
472            Self::BootstrapStore { request, .. } => Some(request.operation_id),
473            Self::ProvisionComponents { request, .. } => Some(request.operation_id),
474            Self::RecoverPoolLedger { request } => Some(request.operation_id),
475        }
476    }
477}
478
479/// Explicit Wasm installation mode selected from live state.
480
481#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
482#[serde(rename_all = "snake_case")]
483pub enum InstallMode {
484    Install,
485    Reinstall,
486}
487
488/// Planned disposition and effects for one controlled canister.
489
490#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
491pub struct CanisterPlan {
492    pub actions: Vec<EnsureAction>,
493    pub disposition: CanisterDisposition,
494    pub name: String,
495    #[serde(with = "u128_text")]
496    pub observed_cycles: u128,
497    pub principal: Option<String>,
498}
499
500/// Operator-visible canister disposition.
501
502#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
503#[serde(rename_all = "snake_case")]
504pub enum CanisterDisposition {
505    Create,
506    Delete,
507    Replace,
508    Reinstall,
509    Reuse,
510}
511
512/// Exact cycle conservation proof reviewed before apply.
513
514#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
515pub struct CycleConservation {
516    #[serde(with = "u128_text")]
517    pub expected_post_operation_cycles: u128,
518    #[serde(with = "u128_text")]
519    pub maximum_execution_burn_cycles: u128,
520    #[serde(with = "u128_text")]
521    pub maximum_new_funding_cycles: u128,
522    #[serde(with = "u128_text")]
523    pub maximum_operator_debit_cycles: u128,
524    #[serde(with = "u128_text")]
525    pub maximum_unavoidable_fee_cycles: u128,
526    #[serde(with = "u128_text")]
527    pub observed_controlled_cycles: u128,
528    #[serde(with = "u128_text")]
529    pub retained_in_reused_canisters_cycles: u128,
530    #[serde(with = "u128_text")]
531    pub scheduled_transfer_cycles: u128,
532}
533
534/// Terminal measured conservation result from the exact applied operation.
535#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
536pub struct ActualCycleConservation {
537    #[serde(with = "u128_text")]
538    pub exact_unavoidable_fee_cycles: u128,
539    #[serde(with = "u128_text")]
540    pub final_controlled_cycles: u128,
541    #[serde(with = "u128_text")]
542    pub measured_execution_burn_cycles: u128,
543    #[serde(with = "u128_text")]
544    pub observed_starting_cycles: u128,
545    #[serde(with = "u128_text")]
546    pub operator_debit_cycles: u128,
547    #[serde(with = "u128_text")]
548    pub received_new_funding_cycles: u128,
549}
550
551/// Exact normalized desired input retained by one in-progress operation.
552///
553/// The public desired document cannot declare internal protocol steps. This
554/// wrapper persists those Canic-owned steps without making them an input
555/// surface and restores them before any resumed observation or effect.
556#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct ReviewedDesiredFleetRecord {
558    desired: DesiredFleet,
559}
560
561impl ReviewedDesiredFleetRecord {
562    pub(crate) fn capture(desired: &DesiredFleet) -> Self {
563        Self {
564            desired: desired.clone(),
565        }
566    }
567
568    /// Borrow the complete normalized desired input, including internal steps.
569    #[must_use]
570    pub const fn desired(&self) -> &DesiredFleet {
571        &self.desired
572    }
573
574    /// Consume the retained authority as the complete normalized desired input.
575    #[must_use]
576    pub fn into_desired(self) -> DesiredFleet {
577        self.desired
578    }
579}
580
581#[derive(Deserialize, Serialize)]
582struct ReviewedDesiredFleetRecordProjection {
583    desired: DesiredFleet,
584    protocol_steps: Vec<DesiredProtocolStep>,
585}
586
587impl Serialize for ReviewedDesiredFleetRecord {
588    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
589    where
590        S: serde::Serializer,
591    {
592        ReviewedDesiredFleetRecordProjection {
593            desired: self.desired.clone(),
594            protocol_steps: self.desired.protocol_steps.clone(),
595        }
596        .serialize(serializer)
597    }
598}
599
600impl<'de> Deserialize<'de> for ReviewedDesiredFleetRecord {
601    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
602    where
603        D: serde::Deserializer<'de>,
604    {
605        let mut projection = ReviewedDesiredFleetRecordProjection::deserialize(deserializer)?;
606        projection.desired.protocol_steps = projection.protocol_steps;
607        Ok(Self {
608            desired: projection.desired,
609        })
610    }
611}
612
613/// Immutable reviewed plan produced from desired state plus one live observation.
614#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
615pub struct FleetEnsurePlan {
616    pub canisters: Vec<CanisterPlan>,
617    pub conservation: CycleConservation,
618    pub desired_sha256: String,
619    pub environment: String,
620    pub fleet: String,
621    pub operation_id: String,
622    pub plan_sha256: String,
623    pub planned_at_time: u64,
624    pub protocol_actions: Vec<EnsureAction>,
625    #[serde(default, skip_serializing_if = "Option::is_none")]
626    pub reviewed_desired: Option<Box<ReviewedDesiredFleetRecord>>,
627    pub schema_version: u16,
628}
629
630/// Durable state of one planned effect.
631
632#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
633#[serde(rename_all = "snake_case")]
634pub enum EffectState {
635    Applied,
636    Intent,
637    Issued,
638}
639
640/// Durable outcome of one reviewed Fleet ensure operation.
641#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
642#[serde(rename_all = "snake_case")]
643pub enum FleetEnsureCompletion {
644    Converged,
645    InProgress,
646    ReplanRequired,
647}
648
649/// Durable intent/result record for one action.
650
651#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
652pub struct EffectRecord {
653    pub action_sha256: String,
654    pub created_principal: Option<String>,
655    #[serde(with = "option_u128_text")]
656    pub destination_post_cycles: Option<u128>,
657    #[serde(with = "option_u128_text")]
658    pub destination_pre_cycles: Option<u128>,
659    #[serde(with = "option_u128_text")]
660    pub post_cycles: Option<u128>,
661    #[serde(with = "option_u128_text")]
662    pub pre_cycles: Option<u128>,
663    #[serde(default)]
664    pub pre_canister_version: Option<u64>,
665    pub progress_identity: Option<String>,
666    pub receipt: Option<String>,
667    pub state: EffectState,
668}
669
670/// Complete retained/live balance tuple for one exact retirement transfer.
671#[derive(Clone, Copy, Debug, Eq, PartialEq)]
672pub struct RetirementTransferBalances {
673    pub destination_after: u128,
674    pub destination_before: u128,
675    pub maximum_execution_burn: u128,
676    pub source_after: u128,
677    pub source_before: u128,
678    pub transfer_amount: u128,
679}
680
681/// Model-owned result of reconciling both controlled sides of a retirement transfer.
682#[derive(Clone, Copy, Debug, Eq, PartialEq)]
683pub enum RetirementTransferReconciliation {
684    Conserved {
685        destination_credit: u128,
686        source_debit: u128,
687    },
688    Pending,
689}
690
691/// Exact invariant failure while reconciling a retirement transfer.
692#[derive(Clone, Copy, Debug, Eq, PartialEq)]
693pub enum RetirementTransferInvariantError {
694    ArithmeticOverflow,
695    BalanceMovedInInvalidDirection,
696    Unbalanced {
697        destination_credit: u128,
698        maximum_source_debit: u128,
699        source_debit: u128,
700    },
701}
702
703/// Require a matching source debit and treasury credit before retirement may continue.
704pub fn reconcile_retirement_transfer(
705    balances: RetirementTransferBalances,
706) -> Result<RetirementTransferReconciliation, RetirementTransferInvariantError> {
707    let source_debit = balances
708        .source_before
709        .checked_sub(balances.source_after)
710        .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
711    let destination_credit = balances
712        .destination_after
713        .checked_sub(balances.destination_before)
714        .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
715    let maximum_source_debit = balances
716        .transfer_amount
717        .checked_add(balances.maximum_execution_burn)
718        .ok_or(RetirementTransferInvariantError::ArithmeticOverflow)?;
719    if source_debit == 0 && destination_credit == 0 {
720        return Ok(RetirementTransferReconciliation::Pending);
721    }
722    if source_debit >= balances.transfer_amount
723        && source_debit <= maximum_source_debit
724        && destination_credit == balances.transfer_amount
725    {
726        return Ok(RetirementTransferReconciliation::Conserved {
727            destination_credit,
728            source_debit,
729        });
730    }
731    Err(RetirementTransferInvariantError::Unbalanced {
732        destination_credit,
733        maximum_source_debit,
734        source_debit,
735    })
736}
737
738/// Current discovered identities retained only to seed live observation.
739
740#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
741pub struct FleetEnsureStateRecord {
742    pub active_registry: Option<canic_core::dto::fleet_registry::FleetRegistry>,
743    #[serde(default)]
744    pub completed_reinstalls: BTreeMap<String, u64>,
745    pub fleet: String,
746    pub pending_principals: BTreeMap<String, String>,
747    pub principals: BTreeMap<String, String>,
748    #[serde(default)]
749    pub retained_cycles_by_principal: BTreeMap<String, u128>,
750    pub schema_version: u16,
751    pub topology: BTreeMap<String, FleetEnsureTopologyRecord>,
752}
753
754/// Current typed topology retained independently from any historical install evidence.
755#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
756pub struct FleetEnsureTopologyRecord {
757    pub kind: DesiredCanisterKind,
758    pub module_hash: Option<String>,
759    pub parent: Option<String>,
760    pub protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
761    pub role: Option<String>,
762}
763
764/// Sole current-generation host journal for one Fleet ensure operation.
765
766#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
767pub struct FleetEnsureJournalRecord {
768    pub completion: FleetEnsureCompletion,
769    pub effects: Vec<EffectRecord>,
770    pub fleet: String,
771    #[serde(with = "u128_text")]
772    pub initial_controlled_cycles: u128,
773    #[serde(with = "u128_text")]
774    pub initial_operator_cycles: u128,
775    pub operation_id: String,
776    pub plan_sha256: String,
777    pub schema_version: u16,
778    pub stalled_observations: u32,
779}
780
781/// Result returned by one plan-only or apply invocation.
782
783#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
784pub struct FleetEnsureReport {
785    pub actual_conservation: Option<ActualCycleConservation>,
786    pub effects_applied: u32,
787    pub plan: FleetEnsurePlan,
788    pub terminal: bool,
789}
790
791const fn is_zero_u128(value: &u128) -> bool {
792    *value == 0
793}
794
795mod u128_text {
796    use serde::{Deserialize, Deserializer, Serializer};
797
798    pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
799    where
800        S: Serializer,
801    {
802        serializer.serialize_str(&value.to_string())
803    }
804
805    pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
806    where
807        D: Deserializer<'de>,
808    {
809        String::deserialize(deserializer)?
810            .parse()
811            .map_err(serde::de::Error::custom)
812    }
813}
814
815mod option_u128_text {
816    use serde::{Deserialize, Deserializer, Serialize, Serializer};
817
818    #[expect(
819        clippy::ref_option,
820        reason = "serde(with) requires the serializer to accept the field by reference"
821    )]
822    pub fn serialize<S>(value: &Option<u128>, serializer: S) -> Result<S::Ok, S::Error>
823    where
824        S: Serializer,
825    {
826        value.map(|cycles| cycles.to_string()).serialize(serializer)
827    }
828
829    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
830    where
831        D: Deserializer<'de>,
832    {
833        Option::<String>::deserialize(deserializer)?
834            .map(|value| value.parse().map_err(serde::de::Error::custom))
835            .transpose()
836    }
837}