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