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
7mod serialization;
8
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11
12pub const FLEET_ENSURE_SCHEMA_VERSION: u16 = 1;
13pub const MAX_FLEET_ENSURE_CANISTERS: usize = 4_096;
14pub const MAX_FLEET_ENSURE_PROTOCOL_STEPS: usize = 4_096;
15
16/// One exact observed canister state used by planning and effect reconciliation.
17
18#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
19pub struct LiveCanister {
20    pub canister_version: Option<u64>,
21    pub controllers: Vec<String>,
22    #[serde(with = "u128_text")]
23    pub cycles: u128,
24    pub module_sha256: Option<String>,
25    pub principal: String,
26    pub reinstall_required: bool,
27    pub root_owned_lifecycle: Option<RootOwnedCanisterLifecycle>,
28    pub status: CanisterRuntimeStatus,
29}
30
31/// Root-owned lifecycle of one explicitly seeded paid canister.
32
33#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
34#[serde(rename_all = "snake_case")]
35pub enum RootOwnedCanisterLifecycle {
36    Claimed,
37    Idle,
38    /// Last exact current-generation balance retained while Root status is fenced.
39    Retained,
40    Store,
41    Workload,
42}
43
44impl RootOwnedCanisterLifecycle {
45    #[must_use]
46    pub const fn label(self) -> &'static str {
47        match self {
48            Self::Claimed => "claimed",
49            Self::Idle => "idle",
50            Self::Retained => "retained",
51            Self::Store => "store",
52            Self::Workload => "workload",
53        }
54    }
55}
56
57/// Management status relevant to reconciliation.
58
59#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
60#[serde(rename_all = "snake_case")]
61pub enum CanisterRuntimeStatus {
62    Running,
63    Stopped,
64    Stopping,
65}
66
67/// Complete live snapshot of the explicitly controlled Fleet estate.
68
69#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
70pub struct FleetObservation {
71    pub additional_controlled_cycles: BTreeMap<String, u128>,
72    pub canisters: BTreeMap<String, Option<LiveCanister>>,
73    pub estate_funding_domains: BTreeMap<String, EstateFundingDomainObservation>,
74    #[serde(with = "u128_text")]
75    pub ledger_fee_cycles: u128,
76    #[serde(with = "u128_text")]
77    pub operator_cycles: u128,
78    pub protocol_ready: BTreeMap<String, bool>,
79}
80
81/// Exact read-only observation of one Root-owned Cycles Ledger account.
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
83#[serde(deny_unknown_fields)]
84pub struct EstateFundingDomainObservation {
85    #[serde(with = "option_u128_text")]
86    pub balance_cycles: Option<u128>,
87    pub cycles_ledger: String,
88    pub pool: Option<EstatePoolInventoryObservation>,
89    pub root_principal: Option<String>,
90}
91
92/// Complete protected Root-pool observation used for capacity and conservation.
93#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
94#[serde(deny_unknown_fields)]
95pub struct EstatePoolInventoryObservation {
96    pub assets: Vec<EstatePoolAssetObservation>,
97    pub maximum_size: u32,
98    pub minimum_size: u32,
99    pub pending_creation: Option<EstatePoolPendingCreationObservation>,
100    #[serde(with = "u128_text")]
101    pub readiness_floor_cycles: u128,
102    #[serde(with = "u128_text")]
103    pub creation_execution_margin_cycles: u128,
104}
105
106/// One non-Store physical asset retained by a Root pool.
107#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
108#[serde(deny_unknown_fields)]
109pub struct EstatePoolAssetObservation {
110    pub creation_receipt: Option<EstatePoolCreationReceiptObservation>,
111    #[serde(with = "u128_text")]
112    pub cycles: u128,
113    pub lifecycle: EstatePoolAssetLifecycle,
114    pub origin: EstatePoolAssetOrigin,
115    pub principal: String,
116}
117
118/// Exact autonomous-creation receipt exposed by the protected Root inventory.
119#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
120#[serde(deny_unknown_fields)]
121pub struct EstatePoolCreationReceiptObservation {
122    pub block_index: u64,
123    pub operation_id: String,
124    pub cycles_ledger: String,
125    #[serde(with = "u128_text")]
126    pub ledger_amount_cycles: u128,
127    #[serde(with = "u128_text")]
128    pub ledger_fee_cycles: u128,
129    #[serde(with = "u128_text")]
130    pub readiness_floor_cycles: u128,
131    #[serde(with = "u128_text")]
132    pub creation_execution_margin_cycles: u128,
133    #[serde(with = "u128_text")]
134    pub management_creation_fee_cycles: u128,
135    #[serde(with = "option_u128_text")]
136    pub first_observed_cycles: Option<u128>,
137}
138
139/// Protected provenance of one Root-owned pool asset.
140#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
141#[serde(rename_all = "snake_case")]
142pub enum EstatePoolAssetOrigin {
143    InfrastructureStore,
144    Created,
145    Imported,
146    Recycled,
147}
148
149/// Reviewed Root authority and lifecycle of a native pool funding target.
150#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
151#[serde(deny_unknown_fields)]
152pub struct PoolFundingAuthority {
153    pub root: String,
154    pub lifecycle: EstatePoolAssetLifecycle,
155}
156
157/// Capacity-relevant lifecycle of one Root-owned pool asset.
158#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
159#[serde(rename_all = "snake_case")]
160pub enum EstatePoolAssetLifecycle {
161    Claimed,
162    Failed,
163    HandingOff,
164    PendingReset,
165    Ready,
166    Recycling,
167    Workload,
168}
169
170/// Exact in-flight creation occupying one slot in the Root pool.
171#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172#[serde(deny_unknown_fields)]
173pub struct EstatePoolPendingCreationObservation {
174    pub attempt_count: u32,
175    #[serde(with = "option_u128_text")]
176    pub available_cycles: Option<u128>,
177    #[serde(with = "u128_text")]
178    pub creation_amount_cycles: u128,
179    pub created_principal: Option<String>,
180    pub diagnostic: Option<EstatePoolCreationDiagnostic>,
181    pub last_attempt_at_ns: Option<u64>,
182    pub operation_id: String,
183    #[serde(with = "option_u128_text")]
184    pub required_cycles: Option<u128>,
185    pub retry_at_ns: Option<u64>,
186    #[serde(with = "option_u128_text")]
187    pub shortfall_cycles: Option<u128>,
188    pub uncertain_result: bool,
189}
190
191/// Typed reason retained by an autonomous pool creation that cannot advance.
192#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
193#[serde(rename_all = "snake_case")]
194pub enum EstatePoolCreationDiagnostic {
195    FundingRequired,
196    LedgerCreationFailed,
197    LedgerRejected,
198    UnresolvedAfterLedgerWindow,
199}
200
201/// Maintained disposition of one configured canister.
202
203#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
204#[serde(rename_all = "snake_case")]
205pub enum DesiredPresence {
206    Absent,
207    Present,
208}
209
210/// Exact method used by a controlled source to transfer its cycles before retirement.
211
212#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
213#[serde(deny_unknown_fields)]
214pub struct DrainAuthority {
215    pub candid: String,
216    pub method: String,
217    pub destination: String,
218    pub maximum_execution_burn_cycles: String,
219}
220
221/// Current desired state for one controlled canister.
222
223#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
224#[serde(deny_unknown_fields)]
225pub struct DesiredCanister {
226    #[serde(default)]
227    pub canic_init: Option<DesiredCanisterInit>,
228    #[serde(default)]
229    pub controller_canisters: Vec<String>,
230    pub controllers: Vec<String>,
231    pub drain: Option<DrainAuthority>,
232    pub initial_cycles: String,
233    pub init_arg: Option<String>,
234    pub init_candid: Option<String>,
235    pub kind: DesiredCanisterKind,
236    pub minimum_cycles: String,
237    pub name: String,
238    pub parent: Option<String>,
239    pub presence: DesiredPresence,
240    pub principal: Option<String>,
241    #[serde(skip)]
242    pub(crate) protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
243    pub replace: bool,
244    pub subnet: String,
245    pub wasm: Option<String>,
246}
247
248/// Canic-owned role class used to derive Fleet control-plane choreography.
249#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
250#[serde(rename_all = "snake_case")]
251pub enum DesiredCanisterKind {
252    Auxiliary,
253    Component,
254    Coordinator,
255    Pool,
256    Root,
257    Store,
258}
259
260/// Canic-owned typed initialization role for one infrastructure canister.
261#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
262#[serde(rename_all = "snake_case", tag = "role")]
263pub enum DesiredCanisterInit {
264    Coordinator,
265    Root { root: String },
266    Store { root: String },
267}
268
269/// One current-only idempotent protocol transition owned by the Fleet journal.
270///
271/// The three argument documents use Candid text. They may bind a dynamically
272/// created canister with `{{principal:<logical-name>}}`, the retained operation
273/// string with `{{operation_id}}`, or its 32-byte value with
274/// `{{operation_id_blob}}`.
275#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
276#[serde(deny_unknown_fields)]
277pub(crate) struct DesiredProtocolStep {
278    pub canister: String,
279    pub candid: String,
280    pub command_args: String,
281    pub command_method: String,
282    pub expected_status: String,
283    pub maximum_execution_burn_cycles: String,
284    pub name: String,
285    pub status_args: String,
286    pub status_method: String,
287}
288
289/// Current-only desired Fleet contract consumed by `canic fleet ensure`.
290
291#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
292#[serde(deny_unknown_fields)]
293pub struct DesiredFleet {
294    #[serde(default)]
295    pub bootstrap: Option<DesiredFleetBootstrap>,
296    pub canisters: Vec<DesiredCanister>,
297    pub cycles_ledger: String,
298    pub environment: String,
299    pub fleet: String,
300    pub ledger_fee_cycles: String,
301    pub management_creation_fee_cycles: String,
302    pub material_cycle_threshold: String,
303    pub maximum_observation_burn_cycles: String,
304    pub maximum_stalled_observations: u32,
305    pub maximum_update_burn_cycles: String,
306    pub operator: String,
307    #[serde(default)]
308    pub protocol: Option<DesiredFleetProtocol>,
309    #[serde(skip)]
310    pub(crate) protocol_steps: Vec<DesiredProtocolStep>,
311    pub schema_version: u16,
312    /// Logical name of the present controlled canister that owns retirement receipts.
313    pub treasury: String,
314}
315
316/// Complete current-generation Canic initialization authority generated from App and Fleet input.
317#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
318#[serde(deny_unknown_fields)]
319pub struct DesiredFleetBootstrap {
320    pub admission: canic_core::ids::FleetAdmissionPolicyTemplate,
321    pub app: canic_core::ids::AppId,
322    pub canonical_network_id: canic_core::ids::CanonicalNetworkId,
323    pub component_deployment_configuration:
324        canic_core::control_plane_support::config::ComponentDeploymentConfiguration,
325    pub coordinator: String,
326    pub coordinator_subnet: canic_core::ids::SubnetId,
327    pub fleet_id: canic_core::ids::FleetId,
328    pub fresh_estate: bool,
329    pub release_build_id: canic_core::ids::ReleaseBuildId,
330    pub root_funding: Option<canic_core::ids::FleetCoordinatorRootFundingPolicy>,
331    pub roots: Vec<DesiredFleetBootstrapRoot>,
332}
333
334/// One Root/Store pair's generated immutable initialization authority.
335#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
336#[serde(deny_unknown_fields)]
337pub struct DesiredFleetBootstrapRoot {
338    pub canister_pool_imports: Vec<String>,
339    pub component_admissions: Vec<canic_core::ids::ComponentSpecAdmission>,
340    pub component_topology_digest: canic_core::ids::ComponentTopologyDigest,
341    pub funding: canic_core::ids::FleetSubnetRootFundingAuthority,
342    pub limits: canic_core::ids::FleetSubnetRootLimits,
343    pub placement_subnet: canic_core::ids::SubnetId,
344    pub root: String,
345    pub store: String,
346}
347
348/// Current Canic-owned control-plane intent compiled from one App topology.
349#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
350#[serde(deny_unknown_fields)]
351pub struct DesiredFleetProtocol {
352    /// Canonical application `canic.toml` used to compile Component topology.
353    pub app_config: String,
354    /// Exact initial group placements assigned to declared Root roles.
355    pub component_group_placements: Vec<DesiredComponentGroupPlacement>,
356    /// Exact Coordinator Candid used only by Canic's typed protocol adapter.
357    pub coordinator_candid: String,
358    /// Exact Root Candid used only by Canic's typed protocol adapter.
359    pub root_candid: String,
360    /// Exact Store Candid used only by Canic's typed protocol adapter.
361    pub store_candid: String,
362}
363
364/// One typed initial Component Group placement owned by Canic policy.
365#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
366#[serde(deny_unknown_fields)]
367pub struct DesiredComponentGroupPlacement {
368    pub deployment: String,
369    pub ordinal: u32,
370    pub root: String,
371}
372
373/// Content identities resolved from current desired-state artifact paths by ops.
374#[derive(Clone, Debug, Default, Eq, PartialEq)]
375pub struct DesiredFleetArtifacts {
376    pub continuation: Option<FleetEnsureContinuationAuthority>,
377    pub drain_candid_sha256_by_canister: BTreeMap<String, String>,
378    pub init_arg_sha256_by_canister: BTreeMap<String, String>,
379    pub init_candid_sha256_by_canister: BTreeMap<String, String>,
380    pub wasm_sha256_by_canister: BTreeMap<String, String>,
381    pub(crate) protocol_by_step: BTreeMap<String, ProtocolArtifactDigests>,
382}
383
384/// Content identities for one declarative protocol transition.
385#[derive(Clone, Debug, Default, Eq, PartialEq)]
386pub(crate) struct ProtocolArtifactDigests {
387    pub candid_sha256: String,
388    pub command_args_sha256: String,
389    pub expected_status_sha256: String,
390    pub status_args_sha256: String,
391}
392
393/// Validated numeric funding bounds for one desired canister.
394
395#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
396pub struct CanisterCyclePolicy {
397    #[serde(with = "u128_text")]
398    pub initial_cycles: u128,
399    #[serde(with = "u128_text")]
400    pub minimum_cycles: u128,
401}
402
403/// Exact Root authority and Candid contract for replicated reinstall observations.
404#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
405#[serde(deny_unknown_fields)]
406pub struct ReinstallHistoryWitness {
407    /// Exact installed module retained before this reviewed reinstall.
408    pub prior_module_sha256: String,
409    pub authority: RootManagementBinding,
410    pub candid: String,
411    pub candid_sha256: String,
412}
413
414/// Exact effect kind retained before execution.
415#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
416#[serde(rename_all = "snake_case", tag = "kind")]
417pub enum EnsureAction {
418    /// Seal one authority's mutation and timer owners before inventory capture.
419    SealAuthority {
420        candid: String,
421        candid_sha256: String,
422        authority_kind: DesiredCanisterKind,
423        name: String,
424        principal: String,
425    },
426    Create {
427        controller_canisters: Vec<String>,
428        controllers: Vec<String>,
429        created_at_time: u64,
430        ledger: String,
431        name: String,
432        #[serde(with = "u128_text")]
433        requested_initial_cycles: u128,
434        subnet: String,
435    },
436    Delete {
437        #[serde(with = "u128_text")]
438        maximum_remaining_cycles: u128,
439        name: String,
440        principal: String,
441    },
442    Fund {
443        /// Exact Root and lifecycle for protected inspection of a pool funding target.
444        #[serde(deserialize_with = "serialization::required_option")]
445        pool_funding: Option<PoolFundingAuthority>,
446        #[serde(with = "u128_text")]
447        amount: u128,
448        created_at_time: u64,
449        #[serde(with = "u128_text")]
450        expected_post_cycles: u128,
451        #[serde(with = "u128_text")]
452        funding_deficit_cycles: u128,
453        #[serde(with = "u128_text")]
454        funding_margin_cycles: u128,
455        ledger: String,
456        name: String,
457        principal: String,
458    },
459    FundEstate {
460        #[serde(with = "u128_text")]
461        amount: u128,
462        created_at_time: u64,
463        #[serde(with = "u128_text")]
464        expected_post_cycles: u128,
465        ledger: String,
466        #[serde(with = "u128_text")]
467        ledger_fee_cycles: u128,
468        name: String,
469        principal: String,
470    },
471    Install {
472        canic_init: Option<DesiredCanisterInit>,
473        #[serde(deserialize_with = "serialization::required_option")]
474        reinstall_witness: Option<Box<ReinstallHistoryWitness>>,
475        init_arg: Option<String>,
476        init_arg_sha256: Option<String>,
477        init_candid: Option<String>,
478        init_candid_sha256: Option<String>,
479        mode: InstallMode,
480        name: String,
481        principal: String,
482        wasm: String,
483        wasm_sha256: String,
484    },
485    FleetProtocol {
486        action: Box<CurrentFleetProtocolAction>,
487        candid: String,
488        candid_sha256: String,
489        #[serde(with = "u128_text")]
490        maximum_execution_burn_cycles: u128,
491        name: String,
492        principal: String,
493    },
494    Protocol {
495        candid: String,
496        candid_sha256: String,
497        command_args: String,
498        command_args_sha256: String,
499        command_method: String,
500        expected_status: String,
501        expected_status_sha256: String,
502        #[serde(with = "u128_text")]
503        maximum_execution_burn_cycles: u128,
504        name: String,
505        principal: String,
506        status_args: String,
507        status_args_sha256: String,
508        status_method: String,
509    },
510    SetControllers {
511        controller_canisters: Vec<String>,
512        controllers: Vec<String>,
513        name: String,
514        principal: String,
515    },
516    Start {
517        name: String,
518        principal: String,
519    },
520    Stop {
521        name: String,
522        principal: String,
523    },
524    Transfer {
525        #[serde(with = "u128_text")]
526        amount: u128,
527        candid: String,
528        candid_sha256: String,
529        destination: String,
530        #[serde(with = "u128_text")]
531        maximum_execution_burn_cycles: u128,
532        method: String,
533        name: String,
534        principal: String,
535    },
536}
537
538impl EnsureAction {
539    #[must_use]
540    pub fn name(&self) -> &str {
541        match self {
542            Self::SealAuthority { name, .. }
543            | Self::Create { name, .. }
544            | Self::Delete { name, .. }
545            | Self::FleetProtocol { name, .. }
546            | Self::Fund { name, .. }
547            | Self::FundEstate { name, .. }
548            | Self::Install { name, .. }
549            | Self::Protocol { name, .. }
550            | Self::SetControllers { name, .. }
551            | Self::Start { name, .. }
552            | Self::Stop { name, .. }
553            | Self::Transfer { name, .. } => name,
554        }
555    }
556}
557
558/// Closed current-only Canic choreography emitted from roles and topology.
559#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
560#[serde(rename_all = "snake_case", tag = "kind")]
561#[expect(
562    clippy::large_enum_variant,
563    reason = "each exact typed action is boxed by EnsureAction before durable retention"
564)]
565pub enum CurrentFleetProtocolAction {
566    /// Verify host-created local supply without authorizing mainnet-only refill.
567    ObservePoolReadiness {
568        minimum_ready: u32,
569        readiness_floor: canic_core::cdk::types::Cycles,
570    },
571    MaintainPoolReadiness {
572        maximum_updates: u32,
573        minimum_ready: u32,
574        readiness_floor: canic_core::cdk::types::Cycles,
575    },
576    ReconcilePoolAsset {
577        request: canic_core::dto::pool::PoolCanisterRequest,
578        minimum_cycles: canic_core::cdk::types::Cycles,
579    },
580    ActivateRegistry {
581        expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
582        expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
583        request: canic_core::dto::fleet_registry::FleetRegistryActivationRequest,
584    },
585    ActivateRegistryMirror {
586        expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistryMirrorActivationResponse,
587        request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
588    },
589    AdoptStore {
590        request: canic_core::dto::fleet_subnet_root::FleetSubnetWasmStoreAdoptionRequest,
591    },
592    BootstrapStore {
593        expected: canic_core::dto::root_store::RootStoreBootstrapResponse,
594        request: canic_core::dto::root_store::RootStoreBootstrapRequest,
595    },
596    JoinRoot {
597        expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
598        expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
599        request: canic_core::dto::fleet_registry::FleetSubnetRootJoinRequest,
600    },
601    PrepareStoreChunkSet {
602        request: canic_control_plane::dto::template::TemplateChunkSetPrepareInput,
603    },
604    PrepareComponentRegistry {
605        expected: canic_core::dto::component_registry::RootComponentRegistryStatusResponse,
606        request: canic_core::dto::component_registry::RootComponentRegistryPreparationRequest,
607    },
608    ProvisionComponents {
609        plan_hash: [u8; 32],
610        request: canic_core::dto::component_provisioning::FleetComponentProvisioningPrepareRequest,
611    },
612    PublishStoreChunk {
613        request: canic_control_plane::dto::template::TemplateChunkInput,
614    },
615    StageStoreManifest {
616        request: canic_control_plane::dto::template::TemplateManifestInput,
617    },
618    SynchronizeRegistry {
619        expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncResponse,
620        request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
621    },
622}
623
624impl CurrentFleetProtocolAction {
625    /// Role that exclusively owns this current control-plane transition.
626    #[must_use]
627    pub const fn target_kind(&self) -> DesiredCanisterKind {
628        match self {
629            Self::ActivateRegistry { .. }
630            | Self::JoinRoot { .. }
631            | Self::ProvisionComponents { .. } => DesiredCanisterKind::Coordinator,
632            Self::ActivateRegistryMirror { .. }
633            | Self::ReconcilePoolAsset { .. }
634            | Self::MaintainPoolReadiness { .. }
635            | Self::ObservePoolReadiness { .. }
636            | Self::AdoptStore { .. }
637            | Self::BootstrapStore { .. }
638            | Self::PrepareComponentRegistry { .. }
639            | Self::SynchronizeRegistry { .. } => DesiredCanisterKind::Root,
640            Self::PrepareStoreChunkSet { .. }
641            | Self::PublishStoreChunk { .. }
642            | Self::StageStoreManifest { .. } => DesiredCanisterKind::Store,
643        }
644    }
645
646    /// Durable operation identity when this transition has a protocol-owned replay key.
647    #[must_use]
648    pub const fn operation_id(&self) -> Option<[u8; 32]> {
649        match self {
650            Self::ActivateRegistry { .. }
651            | Self::ReconcilePoolAsset { .. }
652            | Self::MaintainPoolReadiness { .. }
653            | Self::ObservePoolReadiness { .. }
654            | Self::JoinRoot { .. }
655            | Self::PrepareComponentRegistry { .. }
656            | Self::PrepareStoreChunkSet { .. }
657            | Self::PublishStoreChunk { .. }
658            | Self::StageStoreManifest { .. } => None,
659            Self::ActivateRegistryMirror { request, .. }
660            | Self::SynchronizeRegistry { request, .. } => Some(request.operation_id),
661            Self::AdoptStore { request } => Some(request.operation_id),
662            Self::BootstrapStore { request, .. } => Some(request.operation_id),
663            Self::ProvisionComponents { request, .. } => Some(request.operation_id),
664        }
665    }
666}
667
668/// Explicit Wasm installation mode selected from live state.
669
670#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
671#[serde(rename_all = "snake_case")]
672pub enum InstallMode {
673    Install,
674    Reinstall,
675}
676
677/// Planned disposition and effects for one controlled canister.
678
679#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
680#[serde(deny_unknown_fields)]
681pub struct CanisterPlan {
682    pub actions: Vec<EnsureAction>,
683    pub disposition: CanisterDisposition,
684    pub name: String,
685    #[serde(with = "u128_text")]
686    pub observed_cycles: u128,
687    #[serde(deserialize_with = "serialization::required_option")]
688    pub principal: Option<String>,
689}
690
691/// Operator-visible canister disposition.
692
693#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
694#[serde(rename_all = "snake_case")]
695pub enum CanisterDisposition {
696    Create,
697    Delete,
698    Replace,
699    Reinstall,
700    Reuse,
701}
702
703/// Exact cycle conservation proof reviewed before apply.
704
705#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
706pub struct CycleConservation {
707    pub estate_funding_domains: Vec<EstateFundingDomainPlan>,
708    #[serde(with = "u128_text")]
709    pub expected_post_operation_cycles: u128,
710    #[serde(with = "u128_text")]
711    pub maximum_execution_burn_cycles: u128,
712    #[serde(with = "u128_text")]
713    pub maximum_new_funding_cycles: u128,
714    #[serde(with = "u128_text")]
715    pub maximum_operator_debit_cycles: u128,
716    #[serde(with = "u128_text")]
717    pub maximum_unavoidable_fee_cycles: u128,
718    #[serde(with = "u128_text")]
719    pub observed_controlled_cycles: u128,
720    #[serde(with = "u128_text")]
721    pub retained_in_reused_canisters_cycles: u128,
722    #[serde(with = "u128_text")]
723    pub scheduled_transfer_cycles: u128,
724}
725
726/// Reviewed autonomous-creation exposure for one Root-owned Cycles Ledger account.
727#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
728#[serde(deny_unknown_fields)]
729pub struct EstateFundingDomainPlan {
730    pub allocated_workloads: u32,
731    #[serde(with = "option_u128_text")]
732    pub available_cycles: Option<u128>,
733    pub available_pool_slots: u32,
734    #[serde(with = "u128_text")]
735    pub creation_amount_cycles: u128,
736    pub cycles_ledger: String,
737    #[serde(with = "u128_text")]
738    pub creation_execution_margin_cycles: u128,
739    #[serde(with = "u128_text")]
740    pub readiness_floor_cycles: u128,
741    pub eligible_ready_pool_assets: u32,
742    pub initial_pool_assets: Vec<String>,
743    #[serde(with = "u128_text")]
744    pub ledger_fee_cycles: u128,
745    #[serde(with = "u128_text")]
746    pub management_creation_fee_cycles: u128,
747    #[serde(with = "u128_text")]
748    pub maximum_creation_debit_cycles: u128,
749    #[serde(with = "u128_text")]
750    pub maximum_creation_fee_cycles: u128,
751    #[serde(with = "u128_text")]
752    pub maximum_funding_cycles: u128,
753    pub occupied_pool_assets: u32,
754    pub pending_creation_count: u32,
755    pub pending_creation: Option<EstatePoolPendingCreationObservation>,
756    pub pool_maximum_size: u32,
757    pub planned_initial_workloads: u32,
758    pub required_creation_count: u32,
759    pub root: String,
760    pub root_principal: Option<String>,
761    #[serde(with = "u128_text")]
762    pub shortfall_cycles: u128,
763}
764
765/// Durable no-effect pause for one underfunded Root-owned creation domain.
766#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
767#[serde(deny_unknown_fields)]
768pub struct EstateFundingRequiredRecord {
769    pub attempt_count: Option<u32>,
770    #[serde(with = "u128_text")]
771    pub available_cycles: u128,
772    #[serde(with = "u128_text")]
773    pub creation_amount_cycles: u128,
774    pub cycles_ledger: String,
775    #[serde(with = "u128_text")]
776    pub creation_execution_margin_cycles: u128,
777    #[serde(with = "u128_text")]
778    pub readiness_floor_cycles: u128,
779    #[serde(with = "u128_text")]
780    pub ledger_fee_cycles: u128,
781    #[serde(with = "u128_text")]
782    pub management_creation_fee_cycles: u128,
783    #[serde(with = "u128_text")]
784    pub maximum_creation_debit_cycles: u128,
785    #[serde(with = "u128_text")]
786    pub maximum_creation_fee_cycles: u128,
787    pub operation_id: String,
788    pub pending_creation_operation_id: Option<String>,
789    pub plan_sha256: String,
790    pub required_creation_count: u32,
791    pub root: String,
792    pub root_principal: String,
793    pub last_attempt_at_ns: Option<u64>,
794    pub retry_at_ns: Option<u64>,
795    #[serde(with = "u128_text")]
796    pub shortfall_cycles: u128,
797}
798
799/// Terminal measured conservation result from the exact applied operation.
800#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
801pub struct ActualCycleConservation {
802    #[serde(with = "u128_text")]
803    pub estate_funding_cycles: u128,
804    #[serde(with = "u128_text")]
805    pub exact_estate_creation_fee_cycles: u128,
806    #[serde(with = "u128_text")]
807    pub exact_unavoidable_fee_cycles: u128,
808    #[serde(with = "u128_text")]
809    pub final_controlled_cycles: u128,
810    #[serde(with = "u128_text")]
811    pub measured_execution_burn_cycles: u128,
812    #[serde(with = "u128_text")]
813    pub observed_starting_cycles: u128,
814    #[serde(with = "u128_text")]
815    pub operator_debit_cycles: u128,
816    #[serde(with = "u128_text")]
817    pub received_new_funding_cycles: u128,
818}
819
820/// Sum the separately reviewed Create execution and first-observation bounds.
821/// Overflow cannot grant an unbounded allowance.
822pub(crate) const fn creation_observation_burn(observation: u128, update: u128) -> Option<u128> {
823    observation.checked_add(update)
824}
825
826/// Whether a newly created canister's first balance is covered by its reviewed
827/// Create execution plus first-observation allowance. The Ledger receipt owns
828/// the requested amount; live status owns the exact retained balance.
829pub(crate) const fn create_balance_is_terminal(
830    actual_cycles: Option<u128>,
831    requested_initial_cycles: u128,
832    maximum_observation_burn_cycles: u128,
833) -> bool {
834    let Some(actual_cycles) = actual_cycles else {
835        return false;
836    };
837    if actual_cycles > requested_initial_cycles {
838        return false;
839    }
840    requested_initial_cycles - actual_cycles <= maximum_observation_burn_cycles
841}
842
843/// Exact normalized desired input retained by one in-progress operation.
844///
845/// The public desired document cannot declare internal protocol steps. This
846/// wrapper persists those Canic-owned steps without making them an input
847/// surface and restores them before any resumed observation or effect.
848#[derive(Clone, Debug, Eq, PartialEq)]
849pub struct ReviewedDesiredFleetRecord {
850    desired: DesiredFleet,
851}
852
853impl ReviewedDesiredFleetRecord {
854    pub(crate) fn capture(desired: &DesiredFleet) -> Self {
855        Self {
856            desired: desired.clone(),
857        }
858    }
859
860    /// Borrow the complete normalized desired input, including internal steps.
861    #[must_use]
862    pub const fn desired(&self) -> &DesiredFleet {
863        &self.desired
864    }
865
866    /// Consume the retained authority as the complete normalized desired input.
867    #[must_use]
868    pub fn into_desired(self) -> DesiredFleet {
869        self.desired
870    }
871}
872
873#[derive(Deserialize, Serialize)]
874struct ReviewedDesiredFleetRecordProjection {
875    desired: DesiredFleet,
876    protocol_steps: Vec<DesiredProtocolStep>,
877}
878
879impl Serialize for ReviewedDesiredFleetRecord {
880    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
881    where
882        S: serde::Serializer,
883    {
884        ReviewedDesiredFleetRecordProjection {
885            desired: self.desired.clone(),
886            protocol_steps: self.desired.protocol_steps.clone(),
887        }
888        .serialize(serializer)
889    }
890}
891
892impl<'de> Deserialize<'de> for ReviewedDesiredFleetRecord {
893    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
894    where
895        D: serde::Deserializer<'de>,
896    {
897        let mut projection = ReviewedDesiredFleetRecordProjection::deserialize(deserializer)?;
898        projection.desired.protocol_steps = projection.protocol_steps;
899        Ok(Self {
900            desired: projection.desired,
901        })
902    }
903}
904
905/// Immutable reviewed plan produced from desired state plus one live observation.
906#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
907#[serde(deny_unknown_fields)]
908pub struct FleetEnsurePlan {
909    /// Informational dependent work; grants no funding or effect authority.
910    #[serde(deserialize_with = "serialization::required_option")]
911    pub recovery_review: Option<Box<FleetRecoveryReview>>,
912    /// One explicit same-release wipe, retained through preparation and full convergence.
913    #[serde(deserialize_with = "serialization::required_option")]
914    pub reinstall: Option<Box<FleetReinstallRecord>>,
915    #[serde(deserialize_with = "serialization::required_option")]
916    pub continuation: Option<FleetEnsureContinuationAuthority>,
917    pub canisters: Vec<CanisterPlan>,
918    pub conservation: CycleConservation,
919    pub desired_sha256: String,
920    pub environment: String,
921    pub fleet: String,
922    pub operation_id: String,
923    pub plan_sha256: String,
924    pub planned_at_time: u64,
925    pub protocol_actions: Vec<EnsureAction>,
926    #[serde(deserialize_with = "serialization::required_option")]
927    pub root_start_authority: Option<Box<RetainedRootStartAuthorityRecord>>,
928    /// Exact installed Root authority approved for replacement by this plan.
929    pub root_reinstall_bindings: Vec<RootManagementBinding>,
930    #[serde(deserialize_with = "serialization::required_option")]
931    pub reviewed_desired: Option<Box<ReviewedDesiredFleetRecord>>,
932    pub schema_version: u16,
933    pub scope: FleetEnsurePlanScope,
934    /// Exact terminal operation that owns the currently active Registry observation.
935    #[serde(deserialize_with = "serialization::required_option")]
936    pub terminal_inventory_operation_id: Option<String>,
937}
938
939/// Exact authority scope of one current Fleet Ensure plan.
940#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
941#[serde(rename_all = "snake_case")]
942pub enum FleetEnsurePlanScope {
943    /// Complete desired-state convergence after all protected roles are observable.
944    #[default]
945    Full,
946    /// Seal current allocation before reviewing the complete physical reset closure.
947    ReinstallPreparation,
948    /// Exact reviewed Root reset before current protected interfaces become available.
949    RootReinstallPrerequisite,
950    /// Exact management-authorized Root Start prerequisite.
951    RootStartPrerequisite,
952}
953
954impl FleetEnsurePlanScope {
955    /// Stable operator-facing name shared by text and JSON reports.
956    #[must_use]
957    pub const fn as_str(self) -> &'static str {
958        match self {
959            Self::Full => "full",
960            Self::ReinstallPreparation => "reinstall_preparation",
961            Self::RootReinstallPrerequisite => "root_reinstall_prerequisite",
962            Self::RootStartPrerequisite => "root_start_prerequisite",
963        }
964    }
965}
966
967/// Exact operation-scoped authority for one requested same-release database wipe.
968#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
969#[serde(deny_unknown_fields)]
970pub struct FleetReinstallRecord {
971    #[serde(deserialize_with = "serialization::required_option")]
972    pub activation_reset: Option<Box<FleetActivationResetRecord>>,
973    pub operation_id: String,
974    pub source_operation_id: String,
975    pub authorities: Vec<RootManagementBinding>,
976    /// Empty during preparation; the full plan binds every sealed physical asset.
977    pub assets: Vec<FleetReinstallAssetRecord>,
978}
979
980/// Exact retained source bytes and issued protocol effects inspected for activation recovery.
981/// The source plan label is a journal reference, not executable replacement authority.
982
983#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
984#[serde(deny_unknown_fields)]
985pub struct FleetActivationSourceRecord {
986    pub infrastructure: Vec<RootManagementBinding>,
987    pub operator: String,
988    pub cycles_ledger: String,
989    #[serde(with = "u128_text")]
990    pub initial_controlled_cycles: u128,
991    #[serde(with = "u128_text")]
992    pub maximum_execution_burn_cycles: u128,
993    #[serde(with = "u128_text_map")]
994    pub initial_estate_funding_cycles_by_root: BTreeMap<String, u128>,
995    pub operation_id: String,
996    pub plan_sha256: String,
997    pub plan_document_sha256: String,
998    pub journal_document_sha256: String,
999    pub state_document_sha256: String,
1000    pub provisioning: EnsureAction,
1001    pub registry_preparations: Vec<EnsureAction>,
1002    pub stores: Vec<FleetProtocolReadRecord>,
1003}
1004
1005/// Reviewed inactive activation evidence retained through preparation and reset.
1006#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1007#[serde(deny_unknown_fields)]
1008pub struct FleetActivationResetRecord {
1009    #[serde(deserialize_with = "serialization::required_option")]
1010    pub preparation: Option<ActivationPreparationEvidenceRecord>,
1011    pub source: FleetActivationSourceRecord,
1012    pub roots: Vec<RootActivationResetRecord>,
1013}
1014
1015/// Exact completed preparation documents retained before the separate Root-reset review.
1016#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1017#[serde(deny_unknown_fields)]
1018pub struct ActivationPreparationEvidenceRecord {
1019    pub plan_document_sha256: String,
1020    pub journal_document_sha256: String,
1021}
1022
1023/// Durable local handoff intent; completion is recorded before remote effects can run.
1024#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1025#[serde(deny_unknown_fields)]
1026pub struct ActivationResetAdoptionRecord {
1027    pub source_plan_sha256: String,
1028    pub source_journal_sha256: String,
1029    pub source_state_sha256: String,
1030    pub replacement_plan_sha256: String,
1031    pub replacement_journal_sha256: String,
1032    pub complete: bool,
1033}
1034
1035/// Exact retained Candid and target used only to observe a source protocol owner.
1036
1037#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1038#[serde(deny_unknown_fields)]
1039pub struct FleetProtocolReadRecord {
1040    pub candid: String,
1041    pub candid_sha256: String,
1042    pub principal: String,
1043}
1044
1045/// Inactive Root identity and sealed initial membership observed before a reset.
1046
1047#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1048#[serde(deny_unknown_fields)]
1049pub struct RootActivationResetRecord {
1050    pub root: String,
1051    pub activation_operation_id: [u8; 32],
1052    pub inventory_hash: [u8; 32],
1053    pub provisioning_receipt_hash: [u8; 32],
1054    pub component_count: u32,
1055    pub managed_descendants: u32,
1056}
1057
1058/// One controlled physical asset in the sealed reset closure.
1059#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1060#[serde(deny_unknown_fields)]
1061pub struct FleetReinstallAssetRecord {
1062    pub controllers: Vec<String>,
1063    #[serde(deserialize_with = "serialization::required_option")]
1064    pub module_sha256: Option<String>,
1065    pub principal: String,
1066    pub root: String,
1067    pub subnet: String,
1068}
1069
1070/// Exact management-canister observation of one configured Fleet Subnet Root.
1071#[derive(Clone, Debug, Eq, PartialEq)]
1072pub struct RootManagementCanisterObservation {
1073    pub live: LiveCanister,
1074    pub name: String,
1075    pub subnet: String,
1076}
1077
1078/// Read-only management evidence available before protected Root ingress can run.
1079#[derive(Clone, Debug, Eq, PartialEq)]
1080pub struct RootManagementObservation {
1081    pub operator_cycles: u128,
1082    pub roots: BTreeMap<String, RootManagementCanisterObservation>,
1083}
1084
1085/// Exact installed Root identity, module and controller authority observed through management.
1086#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1087#[serde(deny_unknown_fields)]
1088pub struct RootManagementBinding {
1089    pub controllers: Vec<String>,
1090    pub name: String,
1091    pub module_sha256: String,
1092    pub principal: String,
1093    pub subnet: String,
1094}
1095
1096/// Generator-owned authority for management-bound Root prerequisites before protected observation.
1097///
1098/// The separately reviewed plan permits only Start for the exact observed module.
1099/// It cannot authorize replacement, installation or reuse across releases.
1100#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1101#[serde(deny_unknown_fields)]
1102pub struct RetainedRootStartAuthorityRecord {
1103    pub authority_sha256: String,
1104    pub environment: String,
1105    pub fleet: String,
1106    pub fleet_id: canic_core::ids::FleetId,
1107    pub roots: Vec<RootManagementBinding>,
1108    pub schema_version: u16,
1109}
1110
1111impl RetainedRootStartAuthorityRecord {
1112    pub(crate) fn seal(&mut self) {
1113        self.authority_sha256 = self.expected_sha256();
1114    }
1115
1116    pub(crate) fn has_valid_digest(&self) -> bool {
1117        self.authority_sha256 == self.expected_sha256()
1118    }
1119
1120    fn expected_sha256(&self) -> String {
1121        let mut canonical = self.clone();
1122        canonical.authority_sha256.clear();
1123        let bytes = serde_json::to_vec(&canonical)
1124            .expect("retained Root-start authority is JSON serializable");
1125        let mut framed = b"canic:fleet-ensure:root-start-authority:v1".to_vec();
1126        framed.extend_from_slice(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
1127        framed.extend_from_slice(&bytes);
1128        canic_core::cdk::utils::hash::sha256_hex(&framed)
1129    }
1130}
1131
1132/// Durable state of one planned effect.
1133
1134#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1135#[serde(rename_all = "snake_case")]
1136pub enum EffectState {
1137    Applied,
1138    Intent,
1139    Issued,
1140}
1141
1142/// Durable outcome of one reviewed Fleet ensure operation.
1143#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1144#[serde(rename_all = "snake_case")]
1145pub enum FleetEnsureCompletion {
1146    Converged,
1147    /// Authorities are sealed; the full reset still requires review and execution.
1148    Prepared,
1149    InProgress,
1150    ReplanRequired,
1151}
1152
1153/// Durable intent/result record for one action.
1154
1155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1156#[serde(deny_unknown_fields)]
1157pub struct EffectRecord {
1158    /// Root maintenance calls whose intent was persisted, including lost responses.
1159    pub maintenance_attempts: u32,
1160    pub action_sha256: String,
1161    #[serde(deserialize_with = "serialization::required_option")]
1162    pub created_principal: Option<String>,
1163    #[serde(with = "option_u128_text")]
1164    pub destination_post_cycles: Option<u128>,
1165    #[serde(with = "option_u128_text")]
1166    pub destination_pre_cycles: Option<u128>,
1167    #[serde(with = "option_u128_text")]
1168    pub post_cycles: Option<u128>,
1169    #[serde(with = "option_u128_text")]
1170    pub pre_cycles: Option<u128>,
1171    #[serde(deserialize_with = "serialization::required_option")]
1172    pub pre_canister_version: Option<u64>,
1173    #[serde(deserialize_with = "serialization::required_option")]
1174    pub progress_identity: Option<String>,
1175    #[serde(deserialize_with = "serialization::required_option")]
1176    pub receipt: Option<String>,
1177    pub state: EffectState,
1178}
1179
1180/// Complete retained/live balance tuple for one exact retirement transfer.
1181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1182pub struct RetirementTransferBalances {
1183    pub destination_after: u128,
1184    pub destination_before: u128,
1185    pub maximum_execution_burn: u128,
1186    pub source_after: u128,
1187    pub source_before: u128,
1188    pub transfer_amount: u128,
1189}
1190
1191/// Model-owned result of reconciling both controlled sides of a retirement transfer.
1192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1193pub enum RetirementTransferReconciliation {
1194    Conserved {
1195        destination_credit: u128,
1196        source_debit: u128,
1197    },
1198    Pending,
1199}
1200
1201/// Exact invariant failure while reconciling a retirement transfer.
1202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1203pub enum RetirementTransferInvariantError {
1204    ArithmeticOverflow,
1205    BalanceMovedInInvalidDirection,
1206    Unbalanced {
1207        destination_credit: u128,
1208        maximum_source_debit: u128,
1209        source_debit: u128,
1210    },
1211}
1212
1213/// Require a matching source debit and treasury credit before retirement may continue.
1214pub fn reconcile_retirement_transfer(
1215    balances: RetirementTransferBalances,
1216) -> Result<RetirementTransferReconciliation, RetirementTransferInvariantError> {
1217    let source_debit = balances
1218        .source_before
1219        .checked_sub(balances.source_after)
1220        .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
1221    let destination_credit = balances
1222        .destination_after
1223        .checked_sub(balances.destination_before)
1224        .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
1225    let maximum_source_debit = balances
1226        .transfer_amount
1227        .checked_add(balances.maximum_execution_burn)
1228        .ok_or(RetirementTransferInvariantError::ArithmeticOverflow)?;
1229    if source_debit == 0 && destination_credit == 0 {
1230        return Ok(RetirementTransferReconciliation::Pending);
1231    }
1232    if source_debit >= balances.transfer_amount
1233        && source_debit <= maximum_source_debit
1234        && destination_credit == balances.transfer_amount
1235    {
1236        return Ok(RetirementTransferReconciliation::Conserved {
1237            destination_credit,
1238            source_debit,
1239        });
1240    }
1241    Err(RetirementTransferInvariantError::Unbalanced {
1242        destination_credit,
1243        maximum_source_debit,
1244        source_debit,
1245    })
1246}
1247
1248/// Current discovered identities retained only to seed live observation.
1249
1250#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1251#[serde(deny_unknown_fields)]
1252pub struct FleetEnsureStateRecord {
1253    #[serde(deserialize_with = "serialization::required_option")]
1254    pub active_registry: Option<canic_core::dto::fleet_registry::FleetRegistry>,
1255    /// Exact applied action identities for the adjacent completed reinstalls.
1256    pub completed_reinstall_action_sha256: BTreeMap<String, String>,
1257    /// Exact operation owning the adjacent completed-reinstall thresholds.
1258    #[serde(deserialize_with = "serialization::required_option")]
1259    pub completed_reinstall_operation_id: Option<String>,
1260    /// Pre-effect version thresholds for exact journal-proved reinstalls in the
1261    /// immediately preceding nonterminal operation.
1262    pub completed_reinstalls: BTreeMap<String, u64>,
1263    pub fleet: String,
1264    pub pending_principals: BTreeMap<String, String>,
1265    pub principals: BTreeMap<String, String>,
1266    pub retained_cycles_by_principal: BTreeMap<String, u128>,
1267    pub schema_version: u16,
1268    pub topology: BTreeMap<String, FleetEnsureTopologyRecord>,
1269}
1270
1271/// Current typed topology retained independently from any historical install evidence.
1272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1273#[serde(deny_unknown_fields)]
1274pub struct FleetEnsureTopologyRecord {
1275    pub kind: DesiredCanisterKind,
1276    #[serde(deserialize_with = "serialization::required_option")]
1277    pub module_hash: Option<String>,
1278    #[serde(deserialize_with = "serialization::required_option")]
1279    pub parent: Option<String>,
1280    #[serde(deserialize_with = "serialization::required_option")]
1281    pub protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
1282    #[serde(deserialize_with = "serialization::required_option")]
1283    pub role: Option<String>,
1284}
1285
1286/// Sole current-generation host journal for one Fleet ensure operation.
1287
1288#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1289#[serde(deny_unknown_fields)]
1290pub struct FleetEnsureJournalRecord {
1291    pub funding_reviews: Vec<EstateFundingReviewRecord>,
1292    pub successor_phases: Vec<FleetEnsureSuccessorPhaseRecord>,
1293    pub completion: FleetEnsureCompletion,
1294    #[serde(deserialize_with = "serialization::required_option")]
1295    pub estate_funding_required: Option<EstateFundingRequiredRecord>,
1296    pub effects: Vec<EffectRecord>,
1297    pub fleet: String,
1298    #[serde(with = "u128_text")]
1299    pub initial_controlled_cycles: u128,
1300    #[serde(with = "u128_text_map")]
1301    pub initial_estate_funding_cycles_by_root: BTreeMap<String, u128>,
1302    #[serde(with = "u128_text")]
1303    pub initial_operator_cycles: u128,
1304    pub operation_id: String,
1305    pub plan_sha256: String,
1306    pub schema_version: u16,
1307    pub stalled_observations: u32,
1308}
1309
1310/// Reviewed input identities and finite protocol expansion for a fresh Fleet.
1311#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1312#[serde(deny_unknown_fields)]
1313pub struct FleetEnsureContinuationAuthority {
1314    pub app_config_sha256: String,
1315    pub application_artifact_union_sha256: String,
1316    pub coordinator_candid_sha256: String,
1317    pub maximum_successor_actions: u32,
1318    pub root_candid_sha256: String,
1319    pub store_candid_sha256: String,
1320}
1321
1322/// One immutable successor plan retained before its first effect intent.
1323#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1324#[serde(deny_unknown_fields)]
1325pub struct FleetEnsureSuccessorPhaseRecord {
1326    #[serde(with = "u128_text")]
1327    pub execution_burn_before_phase: u128,
1328    pub plan_sha256: String,
1329    /// Hydrated by the owning file reader; durable JSON contains only the digest.
1330    #[serde(skip)]
1331    pub plan: Option<Box<FleetEnsurePlan>>,
1332}
1333
1334/// Typed boundary requiring a new review instead of automatic protocol continuation.
1335#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1336#[serde(rename_all = "snake_case")]
1337pub enum FleetEnsureSuccessorReviewReason {
1338    AdditionalEffect,
1339    BudgetExceeded,
1340    ChangedInputs,
1341    PhaseBound,
1342    ProtocolAuthority,
1343}
1344
1345/// Result returned by one plan-only or apply invocation.
1346
1347#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1348pub struct FleetEnsureReport {
1349    pub funding_review: Option<EstateFundingReviewRecord>,
1350    pub actual_conservation: Option<ActualCycleConservation>,
1351    pub effects_applied: u32,
1352    pub plan: FleetEnsurePlan,
1353    pub terminal: bool,
1354}
1355
1356/// Exact additional transfer reviewed within an existing Fleet operation.
1357/// The nullable effect is the durable approval boundary; planning never sets it.
1358#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1359#[serde(deny_unknown_fields)]
1360pub struct EstateFundingReviewRecord {
1361    pub action: EnsureAction,
1362    #[serde(deserialize_with = "serialization::required_option")]
1363    pub effect: Option<EffectRecord>,
1364    pub pause: EstateFundingRequiredRecord,
1365    pub review_sha256: String,
1366}
1367
1368mod u128_text {
1369    use serde::{Deserialize, Deserializer, Serializer};
1370
1371    pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
1372    where
1373        S: Serializer,
1374    {
1375        serializer.serialize_str(&value.to_string())
1376    }
1377
1378    pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
1379    where
1380        D: Deserializer<'de>,
1381    {
1382        String::deserialize(deserializer)?
1383            .parse()
1384            .map_err(serde::de::Error::custom)
1385    }
1386}
1387
1388mod option_u128_text {
1389    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1390
1391    #[expect(
1392        clippy::ref_option,
1393        reason = "serde(with) requires the serializer to accept the field by reference"
1394    )]
1395    pub fn serialize<S>(value: &Option<u128>, serializer: S) -> Result<S::Ok, S::Error>
1396    where
1397        S: Serializer,
1398    {
1399        value.map(|cycles| cycles.to_string()).serialize(serializer)
1400    }
1401
1402    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
1403    where
1404        D: Deserializer<'de>,
1405    {
1406        Option::<String>::deserialize(deserializer)?
1407            .map(|value| value.parse().map_err(serde::de::Error::custom))
1408            .transpose()
1409    }
1410}
1411
1412mod u128_text_map {
1413    use std::collections::BTreeMap;
1414
1415    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1416
1417    pub fn serialize<S>(values: &BTreeMap<String, u128>, serializer: S) -> Result<S::Ok, S::Error>
1418    where
1419        S: Serializer,
1420    {
1421        values
1422            .iter()
1423            .map(|(key, value)| (key, value.to_string()))
1424            .collect::<BTreeMap<_, _>>()
1425            .serialize(serializer)
1426    }
1427
1428    pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<String, u128>, D::Error>
1429    where
1430        D: Deserializer<'de>,
1431    {
1432        BTreeMap::<String, String>::deserialize(deserializer)?
1433            .into_iter()
1434            .map(|(key, value)| {
1435                value
1436                    .parse()
1437                    .map(|value| (key, value))
1438                    .map_err(serde::de::Error::custom)
1439            })
1440            .collect()
1441    }
1442}
1443
1444/// Conservative base and continuation allowances, with separately reviewed dependent funding.
1445#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1446#[serde(deny_unknown_fields)]
1447pub struct FleetRecoveryReview {
1448    #[serde(with = "u128_text")]
1449    pub base_execution_burn_cycles: u128,
1450    #[serde(with = "u128_text")]
1451    pub continuation_reserve_cycles: u128,
1452    #[serde(with = "u128_text")]
1453    pub whole_continuation_ceiling_cycles: u128,
1454    pub known_pool_funding: Vec<PoolRecoveryFunding>,
1455    /// Current protocol installation and fresh inventory may reveal more work or debit.
1456    pub discovery: RecoveryDiscovery,
1457}
1458
1459/// Availability of authoritative dependent-work observations at review time.
1460#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1461#[serde(rename_all = "snake_case")]
1462pub enum RecoveryDiscovery {
1463    PendingCurrentProtocol,
1464}
1465
1466/// Exact current pool reset top-up estimate; execution requires its own reviewed Fund action.
1467#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1468#[serde(deny_unknown_fields)]
1469pub struct PoolRecoveryFunding {
1470    pub principal: String,
1471    pub root: String,
1472    #[serde(with = "u128_text")]
1473    pub amount_cycles: u128,
1474    #[serde(with = "u128_text")]
1475    pub ledger_fee_cycles: u128,
1476    #[serde(with = "u128_text")]
1477    pub funding_deficit_cycles: u128,
1478    #[serde(with = "u128_text")]
1479    pub funding_margin_cycles: u128,
1480    #[serde(with = "u128_text")]
1481    pub expected_post_cycles: u128,
1482}
1483
1484/// Informational difference requiring another plan review; never grants effect authority.
1485#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1486pub struct FleetSuccessorReview {
1487    pub actions: Vec<FleetReviewAction>,
1488    #[serde(with = "u128_text")]
1489    pub maximum_additional_debit_cycles: u128,
1490    pub next_review_command: String,
1491}
1492
1493/// Exact target and current action kind in a newly observed phase.
1494#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1495pub struct FleetReviewAction {
1496    pub name: String,
1497    pub principal: Option<String>,
1498    pub kind: String,
1499}
1500
1501impl std::fmt::Display for FleetSuccessorReview {
1502    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1503        write!(f, "remaining actions: ")?;
1504        for (index, action) in self.actions.iter().enumerate() {
1505            if index > 0 {
1506                write!(f, ", ")?;
1507            }
1508            write!(
1509                f,
1510                "{} {} ({})",
1511                action.kind,
1512                action.name,
1513                action.principal.as_deref().unwrap_or("not yet created")
1514            )?;
1515        }
1516        write!(
1517            f,
1518            "; maximum additional debit including fees: {} cycles; next review: {}",
1519            self.maximum_additional_debit_cycles, self.next_review_command
1520        )
1521    }
1522}