Skip to main content

canic_host/fleet_ensure/ops/
platform.rs

1//! Module: fleet_ensure::ops::platform
2//!
3//! Responsibility: mechanically observe and mutate the exact current Fleet through ICP CLI.
4//! Does not own: effect ordering, durable intent, retry policy, or plan approval.
5//! Boundary: the workflow calls one method only after persisting its exact action identity.
6
7use crate::{
8    canister_protocol::{CanisterProtocolError, call_with_candid, query_with_candid},
9    fleet_ensure::{
10        model::{
11            CanisterRuntimeStatus, DesiredCanisterKind, DesiredFleet, EffectRecord, EnsureAction,
12            FleetEnsureStateRecord, FleetObservation, InstallMode, LiveCanister,
13            RetirementTransferBalances, RetirementTransferInvariantError,
14            RetirementTransferReconciliation, RootManagementCanisterObservation,
15            RootManagementObservation, RootOwnedCanisterLifecycle, create_balance_is_terminal,
16            reconcile_retirement_transfer,
17        },
18        ops::{
19            EffectObservation, EffectOutcome, EffectRetry, EnsurePaths, EnsurePlatform,
20            EnsureStateError, TerminalFleetInventory, canic_init, current_protocol,
21            predecessor_root_status, protocol, read_root_start_authority, root_owned_lifecycle,
22            verify_root_start_release_authority,
23        },
24    },
25    icp::{
26        IcpCandidCallError, IcpCanisterStatusReport, IcpCli, IcpCommandError, IcpDiagnostic,
27        IcpManagementCallError, run_status,
28    },
29    icp_config::resolve_icp_build_network_from_root,
30    subnet_catalog::load_mainnet_subnet_catalog,
31};
32use candid::{CandidType, Nat, Principal};
33use canic_core::{
34    cdk::types::Cycles,
35    dto::canister::{CanisterInspectionRequest, CanisterStatusResponse},
36    dto::pool::{CanisterPoolAsset, CanisterPoolResponse, CanisterPoolStatusRequest},
37    ids::BuildNetwork,
38    protocol as canic_protocol,
39};
40use serde::Deserialize;
41use std::{
42    cell::RefCell,
43    collections::{BTreeMap, BTreeSet},
44    path::{Path, PathBuf},
45    time::{SystemTime, UNIX_EPOCH},
46};
47use thiserror::Error as ThisError;
48
49#[derive(CandidType)]
50struct ManagementCanisterStatusRequest {
51    canister_id: Principal,
52}
53
54#[derive(CandidType, Deserialize)]
55struct ManagementCanisterStatusResponse {
56    version: u64,
57    module_hash: Option<Vec<u8>>,
58}
59
60#[derive(Debug, Eq, PartialEq)]
61struct ExactInstallCanisterStatus {
62    canister_version: u64,
63    module_sha256: Option<String>,
64}
65
66#[derive(Debug, Eq, PartialEq)]
67struct RetainedStoreControlBinding {
68    action_root: String,
69    retained_root: Option<String>,
70    retained_store: Option<String>,
71    root_kind: Option<DesiredCanisterKind>,
72    root_parent: Option<String>,
73    store_kind: Option<DesiredCanisterKind>,
74    store_parent: Option<String>,
75}
76
77#[derive(Debug, Eq, PartialEq)]
78struct RetainedStoreControlLiveBinding {
79    root_controllers: Vec<String>,
80    root_module_sha256: Option<String>,
81    store_controllers: Vec<String>,
82    store_module_sha256: Option<String>,
83}
84
85fn retained_store_control_binding(
86    action_root: &str,
87    state: &FleetEnsureStateRecord,
88    root_name: &str,
89    store_name: &str,
90) -> RetainedStoreControlBinding {
91    let root = state.topology.get(root_name);
92    let store = state.topology.get(store_name);
93    RetainedStoreControlBinding {
94        action_root: action_root.to_string(),
95        retained_root: state.principals.get(root_name).cloned(),
96        retained_store: state.principals.get(store_name).cloned(),
97        root_kind: root.map(|topology| topology.kind),
98        root_parent: root.and_then(|topology| topology.parent.clone()),
99        store_kind: store.map(|topology| topology.kind),
100        store_parent: store.and_then(|topology| topology.parent.clone()),
101    }
102}
103
104fn retained_store_control_live_binding(
105    root: LiveCanister,
106    store: LiveCanister,
107) -> RetainedStoreControlLiveBinding {
108    RetainedStoreControlLiveBinding {
109        root_controllers: root.controllers,
110        root_module_sha256: root.module_sha256,
111        store_controllers: store.controllers,
112        store_module_sha256: store.module_sha256,
113    }
114}
115
116fn retained_predecessor_module_matches(
117    retained_module_sha256: Option<&str>,
118    observed_module_sha256: Option<&str>,
119    reviewed_successor_sha256: &str,
120) -> bool {
121    let Some(observed_module_sha256) = observed_module_sha256 else {
122        return false;
123    };
124    observed_module_sha256 != reviewed_successor_sha256
125        && retained_module_sha256.is_none_or(|retained| retained == observed_module_sha256)
126}
127
128fn retained_store_control_live_is_exact(
129    observed: &RetainedStoreControlLiveBinding,
130    root_controllers: &[String],
131    store_controllers: &[String],
132    retained_root_module_sha256: Option<&str>,
133    retained_store_module_sha256: Option<&str>,
134    root_successor_sha256: &str,
135    store_successor_sha256: &str,
136) -> bool {
137    let controllers_match = observed.root_controllers == root_controllers
138        && observed.store_controllers == store_controllers;
139    let root_module_matches = retained_predecessor_module_matches(
140        retained_root_module_sha256,
141        observed.root_module_sha256.as_deref(),
142        root_successor_sha256,
143    );
144    let store_module_matches = retained_predecessor_module_matches(
145        retained_store_module_sha256,
146        observed.store_module_sha256.as_deref(),
147        store_successor_sha256,
148    );
149    controllers_match && root_module_matches && store_module_matches
150}
151
152fn desired_root_by_principal<'a>(
153    desired: &'a DesiredFleet,
154    state: &FleetEnsureStateRecord,
155    principal: &str,
156) -> Option<&'a crate::fleet_ensure::model::DesiredCanister> {
157    desired.canisters.iter().find(|canister| {
158        canister.kind == DesiredCanisterKind::Root
159            && state.principals.get(&canister.name).map(String::as_str) == Some(principal)
160    })
161}
162
163fn is_unallocated_fresh_root(
164    desired: &DesiredFleet,
165    configured: &crate::fleet_ensure::model::DesiredCanister,
166    reviewed_targets: &BTreeSet<String>,
167) -> bool {
168    reviewed_targets.is_empty()
169        && configured.principal.is_none()
170        && desired.bootstrap.as_ref().is_some_and(|bootstrap| {
171            bootstrap.fresh_estate
172                && bootstrap
173                    .roots
174                    .iter()
175                    .any(|root| root.root == configured.name)
176        })
177}
178
179/// Exact input expected by a configured cycle-safe retirement endpoint.
180
181#[derive(CandidType)]
182struct DrainRequest {
183    operation_id: String,
184    destination: Principal,
185    cycles: Nat,
186}
187
188#[derive(CandidType, Deserialize)]
189enum DrainResponse {
190    Accepted { transferred_cycles: Nat },
191    Replayed { transferred_cycles: Nat },
192}
193
194#[derive(CandidType)]
195enum RootPoolStatusRequest {
196    Pool(CanisterPoolStatusRequest),
197}
198
199#[derive(CandidType, Deserialize)]
200enum RootPoolStatusResponse {
201    Pool(Box<CanisterPoolResponse>),
202}
203
204#[derive(CandidType)]
205enum RootInspectionCommand {
206    InspectCanister(CanisterInspectionRequest),
207}
208
209#[derive(CandidType, Deserialize)]
210enum RootInspectionResponse {
211    InspectCanister(CanisterStatusResponse),
212}
213
214#[derive(CandidType)]
215enum ManagedCanisterStatusRequest {
216    CycleBalance,
217}
218
219#[derive(CandidType, Deserialize)]
220enum ManagedCanisterStatusResponse {
221    CycleBalance(canic_core::dto::role::CycleBalanceStatusResponse),
222}
223
224#[derive(CandidType)]
225struct CreateCanisterArgs {
226    amount: Nat,
227    created_at_time: Option<u64>,
228    creation_args: Option<CmcCreateCanisterArgs>,
229    from_subaccount: Option<[u8; 32]>,
230}
231
232#[derive(CandidType)]
233struct CmcCreateCanisterArgs {
234    settings: Option<CanisterSettings>,
235    subnet_selection: Option<SubnetSelection>,
236}
237
238#[derive(CandidType)]
239struct CanisterSettings {
240    compute_allocation: Option<Nat>,
241    controllers: Option<Vec<Principal>>,
242    freezing_threshold: Option<Nat>,
243    memory_allocation: Option<Nat>,
244    reserved_cycles_limit: Option<Nat>,
245}
246
247struct CreateCanisterAuthority<'a> {
248    controller_canisters: &'a [String],
249    controllers: &'a [String],
250    created_at_time: u64,
251    ledger: &'a str,
252    requested_initial_cycles: u128,
253    subnet: &'a str,
254}
255
256#[derive(CandidType)]
257enum SubnetSelection {
258    Subnet { subnet: Principal },
259}
260
261#[derive(CandidType, Deserialize)]
262struct CreateCanisterSuccess {
263    block_id: Nat,
264    canister_id: Principal,
265}
266
267#[derive(CandidType, Deserialize)]
268enum CreateCanisterError {
269    CreatedInFuture {
270        ledger_time: u64,
271    },
272    Duplicate {
273        duplicate_of: Nat,
274        canister_id: Option<Principal>,
275    },
276    FailedToCreate {
277        error: String,
278        fee_block: Option<Nat>,
279        refund_block: Option<Nat>,
280    },
281    GenericError {
282        error_code: Nat,
283        message: String,
284    },
285    InsufficientFunds {
286        balance: Nat,
287    },
288    TemporarilyUnavailable,
289    TooOld,
290}
291
292#[derive(CandidType)]
293struct WithdrawArgs {
294    amount: Nat,
295    created_at_time: Option<u64>,
296    from_subaccount: Option<[u8; 32]>,
297    to: Principal,
298}
299
300#[derive(CandidType, Deserialize)]
301enum WithdrawError {
302    BadFee {
303        expected_fee: Nat,
304    },
305    CreatedInFuture {
306        ledger_time: u64,
307    },
308    Duplicate {
309        duplicate_of: Nat,
310    },
311    FailedToWithdraw {
312        fee_block: Option<Nat>,
313        rejection_code: RejectionCode,
314        rejection_reason: String,
315    },
316    GenericError {
317        error_code: Nat,
318        message: String,
319    },
320    InsufficientFunds {
321        balance: Nat,
322    },
323    InvalidReceiver {
324        receiver: Principal,
325    },
326    TemporarilyUnavailable,
327    TooOld,
328}
329
330#[derive(CandidType, Deserialize)]
331enum RejectionCode {
332    CanisterError,
333    CanisterReject,
334    DestinationInvalid,
335    NoError,
336    SysFatal,
337    SysTransient,
338    Unknown,
339}
340
341/// Typed failure at the current IC effect boundary.
342
343#[derive(Debug, ThisError)]
344pub enum IcpEnsurePlatformError {
345    #[error("cycle arithmetic overflow while executing {0}")]
346    Arithmetic(&'static str),
347
348    #[error("configured Candid file is not a regular file: {}", .0.display())]
349    CandidUnavailable(PathBuf),
350
351    #[error("configured effect artifact is unavailable: {}", .0.display())]
352    ArtifactUnavailable(PathBuf),
353
354    #[error("configured {kind} changed after plan review: expected {expected}, observed {actual}")]
355    ArtifactDigestMismatch {
356        actual: String,
357        expected: String,
358        kind: &'static str,
359    },
360
361    #[error("configured Principal is invalid for {field}: {value}")]
362    InvalidPrincipal { field: &'static str, value: String },
363
364    #[error("Cycles Ledger returned an unsupported fee value: {0}")]
365    InvalidLedgerFee(String),
366
367    #[error("active ICP identity is {actual}, but reviewed Fleet operator is {expected}")]
368    OperatorMismatch { actual: String, expected: String },
369
370    #[error("management status for configured canister {expected} returned identity {actual}")]
371    StatusIdentityMismatch { actual: String, expected: String },
372
373    #[error("retained Root management observation failed: {0}")]
374    RootManagement(String),
375
376    #[error("ICP status has invalid cycle balance for {canister}: {value}")]
377    InvalidStatusCycles { canister: String, value: String },
378
379    #[error("ICP status omitted exact {field} required for controlled canister {canister}")]
380    IncompleteCanisterStatus {
381        canister: String,
382        field: &'static str,
383    },
384
385    #[error(
386        "ICP returned only public non-controller status for {canister}; exact runtime and cycle evidence is unavailable"
387    )]
388    PublicCanisterStatusUnavailable { canister: String },
389
390    #[error(
391        "Root-owned observation for {canister} conflicts with reviewed {field}; no paid effect was repeated"
392    )]
393    RootOwnedObservationAuthorityConflict {
394        canister: String,
395        field: &'static str,
396    },
397
398    #[error(
399        "ICP CLI status JSON omitted canister_version for {canister}, and the exact typed \
400         effective-ID-routed management-canister status call failed: {source}; no install was \
401         issued. Restore the selected controller identity and management-status access, then \
402         resume the same reviewed plan"
403    )]
404    InstallVersionProofUnavailable {
405        canister: String,
406        #[source]
407        source: Box<IcpManagementCallError>,
408    },
409
410    #[error(
411        "completed reinstall proof for {canister} conflicts with current {field}; no install was authorized"
412    )]
413    CompletedReinstallAuthorityConflict {
414        canister: String,
415        field: &'static str,
416    },
417
418    #[error(transparent)]
419    Candid(#[from] IcpCandidCallError),
420
421    #[error(transparent)]
422    Icp(#[from] IcpCommandError),
423
424    #[error("Cycles Ledger create_canister failed: {0}")]
425    LedgerCreate(String),
426
427    #[error("Cycles Ledger duplicate does not yet identify its created canister")]
428    LedgerCreatePending,
429
430    #[error("Cycles Ledger withdraw failed: {0}")]
431    LedgerWithdraw(String),
432
433    #[error(
434        "Root-authorized funding inspection for {canister} conflicts with reviewed {field}; no Ledger withdrawal was repeated"
435    )]
436    FundingInspectionAuthorityConflict {
437        canister: String,
438        field: &'static str,
439    },
440
441    #[error(
442        "canister {canister} retains {actual} cycles above deletion limit {maximum}; left untouched"
443    )]
444    MaterialCycles {
445        actual: u128,
446        canister: String,
447        maximum: u128,
448    },
449
450    #[error("canister {canister} is not stopped; deletion was not attempted")]
451    NotStopped { canister: String },
452
453    #[error("effect references unresolved created canister {0}")]
454    UnresolvedCreated(String),
455
456    #[error("configured retirement endpoint transferred {actual} cycles, expected {expected}")]
457    WrongTransferAmount { actual: Nat, expected: u128 },
458
459    #[error(
460        "retirement transfer for {canister} is not conserved: source debit {source_debit}, treasury credit {destination_credit}, expected credit {expected}, maximum source debit {maximum_source_debit}; source and treasury were left untouched by later retirement steps"
461    )]
462    UnbalancedTransfer {
463        canister: String,
464        destination_credit: u128,
465        expected: u128,
466        maximum_source_debit: u128,
467        source_debit: u128,
468    },
469
470    #[error("retirement transfer for {canister} is missing its durable {side} balance")]
471    MissingTransferBalance {
472        canister: String,
473        side: &'static str,
474    },
475
476    #[error("retirement transfer balance for {canister} moved in an invalid direction")]
477    InvalidTransferBalance { canister: String },
478
479    #[error(transparent)]
480    CurrentProtocol(#[from] current_protocol::CurrentProtocolError),
481
482    #[error(transparent)]
483    CanicInit(#[from] canic_init::CanicInitError),
484
485    #[error(transparent)]
486    Protocol(#[from] protocol::ProtocolEffectError),
487
488    #[error("retained Root status authority is invalid: {0}")]
489    RetainedRootStatusAuthority(#[source] Box<EnsureStateError>),
490
491    #[error("predecessor Root status is invalid: {0}")]
492    PredecessorRootStatus(#[source] Box<predecessor_root_status::PredecessorRootStatusError>),
493}
494
495/// Production ICP adapter for the current desired Fleet.
496pub struct IcpEnsurePlatform {
497    desired: DesiredFleet,
498    icp: IcpCli,
499    recovery_reinstalls: RefCell<BTreeSet<String>>,
500    root: PathBuf,
501}
502
503#[derive(Clone, Copy)]
504enum RetainedRootOwnedObservationMode {
505    DeferredUntilRootStart,
506    ReinstallRecovery,
507}
508
509impl IcpEnsurePlatform {
510    #[must_use]
511    pub fn new(desired: DesiredFleet, icp_executable: &str, root: &Path) -> Self {
512        let icp = IcpCli::new(icp_executable, Some(desired.environment.clone()))
513            .with_cwd(root.to_path_buf());
514        Self {
515            desired,
516            icp,
517            recovery_reinstalls: RefCell::new(BTreeSet::new()),
518            root: root.to_path_buf(),
519        }
520    }
521
522    fn require_operator(&self) -> Result<(), IcpEnsurePlatformError> {
523        let actual = self.icp.identity_principal_text()?;
524        if actual != self.desired.operator {
525            return Err(IcpEnsurePlatformError::OperatorMismatch {
526                actual,
527                expected: self.desired.operator.clone(),
528            });
529        }
530        Ok(())
531    }
532
533    fn current_principal<'a>(
534        &'a self,
535        state: &'a FleetEnsureStateRecord,
536        name: &str,
537    ) -> Option<&'a str> {
538        state
539            .pending_principals
540            .get(name)
541            .or_else(|| state.principals.get(name))
542            .map(String::as_str)
543            .or_else(|| {
544                self.desired
545                    .canisters
546                    .iter()
547                    .find(|configured| configured.name == name)
548                    .and_then(|configured| configured.principal.as_deref())
549            })
550    }
551
552    fn protocol_principals(&self, state: &FleetEnsureStateRecord) -> BTreeMap<String, String> {
553        self.desired
554            .canisters
555            .iter()
556            .filter_map(|configured| {
557                state
558                    .pending_principals
559                    .get(&configured.name)
560                    .or_else(|| state.principals.get(&configured.name))
561                    .or(configured.principal.as_ref())
562                    .map(|principal| (configured.name.clone(), principal.clone()))
563            })
564            .collect()
565    }
566
567    fn resolved_controllers(
568        &self,
569        state: &FleetEnsureStateRecord,
570        controllers: &[String],
571        controller_canisters: &[String],
572    ) -> Result<Vec<String>, IcpEnsurePlatformError> {
573        let mut resolved = controllers.to_vec();
574        for name in controller_canisters {
575            resolved.push(
576                self.current_principal(state, name)
577                    .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(name.clone()))?
578                    .to_string(),
579            );
580        }
581        resolved.sort();
582        resolved.dedup();
583        Ok(resolved)
584    }
585
586    fn retained_store_control_replan_is_exact(
587        &self,
588        operation_id: &str,
589        action: &EnsureAction,
590        state: &FleetEnsureStateRecord,
591    ) -> Result<bool, IcpEnsurePlatformError> {
592        let EnsureAction::FleetProtocol {
593            action: current_action,
594            name,
595            principal,
596            ..
597        } = action
598        else {
599            return Ok(false);
600        };
601        let crate::fleet_ensure::model::CurrentFleetProtocolAction::AdoptStore { request } =
602            current_action.as_ref()
603        else {
604            return Ok(false);
605        };
606        let requested_root = request.authority.fleet_subnet_root.to_text();
607        let Some(root) = desired_root_by_principal(&self.desired, state, &requested_root) else {
608            return Ok(false);
609        };
610        let root_name = root.name.as_str();
611        if name != &format!("root-store-control:{root_name}") {
612            return Ok(false);
613        }
614        if !current_protocol::retained_store_control_request_is_exact(
615            &self.root,
616            &self.desired,
617            operation_id,
618            state,
619            root_name,
620            request,
621        )? {
622            return Ok(false);
623        }
624        let principals = self.protocol_principals(state);
625        let Some((_name, expected)) =
626            canic_init::compile_root_authorities(&self.root, &self.desired, &principals)?
627                .into_iter()
628                .find(|(name, _authority)| name == root_name)
629        else {
630            return Ok(false);
631        };
632        let root_principal = expected.binding.fleet_subnet_root.to_text();
633        let store_principal = expected.wasm_store_authority.wasm_store.to_text();
634        let root_topology = state.topology.get(root_name);
635        let store = self.desired.canisters.iter().find(|canister| {
636            canister.kind == DesiredCanisterKind::Store
637                && canister.parent.as_deref() == Some(root_name)
638        });
639        let Some(store) = store else {
640            return Ok(false);
641        };
642        let store_topology = state.topology.get(&store.name);
643        let observed_binding =
644            retained_store_control_binding(principal, state, root_name, &store.name);
645        let expected_binding = RetainedStoreControlBinding {
646            action_root: root_principal.clone(),
647            retained_root: Some(root_principal.clone()),
648            retained_store: Some(store_principal.clone()),
649            root_kind: Some(DesiredCanisterKind::Root),
650            root_parent: root.parent.clone(),
651            store_kind: Some(DesiredCanisterKind::Store),
652            store_parent: Some(root_name.to_string()),
653        };
654        if observed_binding != expected_binding {
655            return Ok(false);
656        }
657
658        let Some(root_live) = self.install_status_optional(&root_principal)? else {
659            return Ok(false);
660        };
661        let Some(store_live) = self.install_status_optional(&store_principal)? else {
662            return Ok(false);
663        };
664        let Some(root_wasm) = root.wasm.as_ref() else {
665            return Ok(false);
666        };
667        let Some(store_wasm) = store.wasm.as_ref() else {
668            return Ok(false);
669        };
670        let root_successor = artifact_hash(&resolve_path(&self.root, root_wasm))?;
671        let store_successor = artifact_hash(&resolve_path(&self.root, store_wasm))?;
672        let root_controllers =
673            self.resolved_controllers(state, &root.controllers, &root.controller_canisters)?;
674        let store_controllers =
675            self.resolved_controllers(state, &store.controllers, &store.controller_canisters)?;
676        let observed_live = retained_store_control_live_binding(root_live, store_live);
677        Ok(retained_store_control_live_is_exact(
678            &observed_live,
679            &root_controllers,
680            &store_controllers,
681            root_topology.and_then(|topology| topology.module_hash.as_deref()),
682            store_topology.and_then(|topology| topology.module_hash.as_deref()),
683            &root_successor,
684            &store_successor,
685        ))
686    }
687
688    fn observed_protocol_action(
689        &self,
690        step: &crate::fleet_ensure::model::DesiredProtocolStep,
691        principal: String,
692    ) -> Result<EnsureAction, IcpEnsurePlatformError> {
693        Ok(EnsureAction::Protocol {
694            candid: step.candid.clone(),
695            candid_sha256: artifact_hash(&resolve_path(&self.root, &step.candid))?,
696            command_args: step.command_args.clone(),
697            command_args_sha256: artifact_hash(&resolve_path(&self.root, &step.command_args))?,
698            command_method: step.command_method.clone(),
699            expected_status: step.expected_status.clone(),
700            expected_status_sha256: artifact_hash(&resolve_path(
701                &self.root,
702                &step.expected_status,
703            ))?,
704            maximum_execution_burn_cycles: step
705                .maximum_execution_burn_cycles
706                .parse()
707                .map_err(|_| IcpEnsurePlatformError::Arithmetic("protocol burn"))?,
708            name: step.name.clone(),
709            principal,
710            status_args: step.status_args.clone(),
711            status_args_sha256: artifact_hash(&resolve_path(&self.root, &step.status_args))?,
712            status_method: step.status_method.clone(),
713        })
714    }
715
716    fn action_principal<'a>(
717        state: &'a FleetEnsureStateRecord,
718        principal: &'a str,
719    ) -> Result<&'a str, IcpEnsurePlatformError> {
720        if let Some(name) = principal.strip_prefix("created:") {
721            return state
722                .pending_principals
723                .get(name)
724                .map(String::as_str)
725                .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(name.to_string()));
726        }
727        Ok(principal)
728    }
729
730    fn status_optional(
731        &self,
732        principal: &str,
733    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
734        let report = match self.icp.canister_status_report(principal) {
735            Ok(report) => report,
736            Err(error)
737                if matches!(
738                    error.diagnostic(),
739                    Some(IcpDiagnostic::CanisterNotFound { .. })
740                ) =>
741            {
742                return Ok(None);
743            }
744            Err(error) => return Err(error.into()),
745        };
746        if report.id != principal {
747            return Err(IcpEnsurePlatformError::StatusIdentityMismatch {
748                actual: report.id,
749                expected: principal.to_string(),
750            });
751        }
752        reject_public_status_projection(&report, principal)?;
753        let status_text =
754            report
755                .status
756                .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
757                    canister: principal.to_string(),
758                    field: "status",
759                })?;
760        let cycles_text =
761            report
762                .cycles
763                .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
764                    canister: principal.to_string(),
765                    field: "cycles",
766                })?;
767        let cycles = parse_status_cycles(&cycles_text).ok_or_else(|| {
768            IcpEnsurePlatformError::InvalidStatusCycles {
769                canister: principal.to_string(),
770                value: cycles_text,
771            }
772        })?;
773        let mut controllers = report
774            .settings
775            .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
776                canister: principal.to_string(),
777                field: "controllers",
778            })?
779            .controllers;
780        controllers.sort();
781        let status = match status_text.to_ascii_lowercase().as_str() {
782            "stopped" => CanisterRuntimeStatus::Stopped,
783            "stopping" => CanisterRuntimeStatus::Stopping,
784            _ => CanisterRuntimeStatus::Running,
785        };
786        Ok(Some(LiveCanister {
787            canister_version: report.canister_version,
788            controllers,
789            cycles,
790            module_sha256: report.module_hash.map(|hash| normalize_hash(&hash)),
791            principal: principal.to_string(),
792            reinstall_required: false,
793            root_owned_lifecycle: None,
794            status,
795        }))
796    }
797
798    fn inspect_root_owned_canister(
799        &self,
800        configured: &crate::fleet_ensure::model::DesiredCanister,
801        principal: &str,
802        state: &FleetEnsureStateRecord,
803    ) -> Result<Option<CanisterStatusResponse>, IcpEnsurePlatformError> {
804        let parent = configured.parent.as_deref().ok_or_else(|| {
805            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
806                canister: configured.name.clone(),
807                field: "Root parent",
808            }
809        })?;
810        let root = self.current_principal(state, parent).ok_or_else(|| {
811            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
812                canister: configured.name.clone(),
813                field: "Root Principal",
814            }
815        })?;
816        let root_configured = self
817            .desired
818            .canisters
819            .iter()
820            .find(|candidate| {
821                candidate.name == parent && candidate.kind == DesiredCanisterKind::Root
822            })
823            .ok_or_else(
824                || IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
825                    canister: configured.name.clone(),
826                    field: "Root desired authority",
827                },
828            )?;
829        let root_wasm = root_configured.wasm.as_deref().ok_or_else(|| {
830            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
831                canister: configured.name.clone(),
832                field: "Root artifact",
833            }
834        })?;
835        let expected_root_module = artifact_hash(&resolve_path(&self.root, root_wasm))?;
836        let root_live = self.status_optional(root)?.ok_or_else(|| {
837            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
838                canister: configured.name.clone(),
839                field: "live Root",
840            }
841        })?;
842        let expected_root_controllers = self.resolved_controllers(
843            state,
844            &root_configured.controllers,
845            &root_configured.controller_canisters,
846        )?;
847        if root_live.controllers != expected_root_controllers {
848            return Err(
849                IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
850                    canister: configured.name.clone(),
851                    field: "Root controllers",
852                },
853            );
854        }
855        let Some(root_module) = root_live.module_sha256.as_deref() else {
856            return Ok(None);
857        };
858        if root_module != expected_root_module {
859            return Err(
860                IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
861                    canister: configured.name.clone(),
862                    field: "Root module SHA-256",
863                },
864            );
865        }
866        if root_live.status != CanisterRuntimeStatus::Running {
867            return Ok(None);
868        }
869        self.require_operator()?;
870        let response: RootInspectionResponse = call_with_candid(
871            &self.icp,
872            &self.root_protocol_candid()?,
873            parse_principal("Fleet Subnet Root", root)?,
874            canic_protocol::CANIC_ROOT_COMMAND,
875            &RootInspectionCommand::InspectCanister(CanisterInspectionRequest {
876                canister_id: parse_principal("Root-owned canister", principal)?,
877            }),
878        )
879        .map_err(current_protocol::CurrentProtocolError::from)?;
880        let RootInspectionResponse::InspectCanister(response) = response;
881        Ok(Some(response))
882    }
883
884    fn created_canister_cycles(
885        &self,
886        name: &str,
887        principal: &str,
888        state: &FleetEnsureStateRecord,
889    ) -> Result<(Option<u128>, bool), IcpEnsurePlatformError> {
890        match self.status_optional(principal) {
891            Ok(live) => return Ok((live.map(|live| live.cycles), false)),
892            Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable { .. }) => {}
893            Err(error) => return Err(error),
894        }
895        let configured = self
896            .desired
897            .canisters
898            .iter()
899            .find(|configured| configured.name == name)
900            .ok_or_else(
901                || IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
902                    canister: name.to_string(),
903                    field: "desired canister",
904                },
905            )?;
906        if configured.kind != DesiredCanisterKind::Pool {
907            return Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
908                canister: principal.to_string(),
909            });
910        }
911        let Some(response) = self.inspect_root_owned_canister(configured, principal, state)? else {
912            return Ok((None, true));
913        };
914        let parent = configured.parent.as_deref().ok_or_else(|| {
915            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
916                canister: name.to_string(),
917                field: "Root parent",
918            }
919        })?;
920        let root = self.current_principal(state, parent).ok_or_else(|| {
921            IcpEnsurePlatformError::RootOwnedObservationAuthorityConflict {
922                canister: name.to_string(),
923                field: "Root Principal",
924            }
925        })?;
926        let cycles = validate_root_funding_inspection(
927            name,
928            configured.kind,
929            root,
930            &response.settings.controllers,
931            response.module_hash.as_deref(),
932            &response.cycles,
933        )?;
934        Ok((Some(cycles), false))
935    }
936
937    fn install_status_optional(
938        &self,
939        principal: &str,
940    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
941        let Some(mut live) = self.status_optional(principal)? else {
942            return Ok(None);
943        };
944        let exact = exact_install_canister_status(
945            &self.icp,
946            principal,
947            live.canister_version,
948            live.module_sha256.clone(),
949        )?;
950        live.canister_version = Some(exact.canister_version);
951        live.module_sha256 = exact.module_sha256;
952        Ok(Some(live))
953    }
954
955    fn has_stopped_retained_protocol_owner(
956        &self,
957        state: &FleetEnsureStateRecord,
958    ) -> Result<bool, IcpEnsurePlatformError> {
959        for configured in self.desired.canisters.iter().filter(|configured| {
960            configured.presence == crate::fleet_ensure::model::DesiredPresence::Present
961                && matches!(
962                    configured.kind,
963                    DesiredCanisterKind::Coordinator
964                        | DesiredCanisterKind::Root
965                        | DesiredCanisterKind::Store
966                )
967        }) {
968            let Some(principal) = self.current_principal(state, &configured.name) else {
969                continue;
970            };
971            let Some(live) = self.status_optional(principal)? else {
972                continue;
973            };
974            if live.status != CanisterRuntimeStatus::Running {
975                return Ok(true);
976            }
977        }
978        Ok(false)
979    }
980
981    fn observe_configured_canister(
982        &self,
983        configured: &crate::fleet_ensure::model::DesiredCanister,
984        principal: &str,
985        state: &FleetEnsureStateRecord,
986    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
987        if configured.kind == DesiredCanisterKind::Pool {
988            return self.observe_root_owned_canister(configured, principal, state);
989        }
990        match self.status_optional(principal) {
991            Ok(observed) => Ok(observed),
992            Err(error)
993                if matches!(
994                    configured.kind,
995                    DesiredCanisterKind::Store | DesiredCanisterKind::Pool
996                ) && !matches!(
997                    error,
998                    IcpEnsurePlatformError::Icp(ref source)
999                        if matches!(
1000                            source.diagnostic(),
1001                            Some(IcpDiagnostic::CanisterNotFound { .. })
1002                        )
1003                ) =>
1004            {
1005                self.observe_root_owned_canister(configured, principal, state)
1006            }
1007            Err(error) => Err(error),
1008        }
1009    }
1010
1011    fn observe_root_owned_canister(
1012        &self,
1013        configured: &crate::fleet_ensure::model::DesiredCanister,
1014        principal: &str,
1015        state: &FleetEnsureStateRecord,
1016    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1017        let candid = self.root_protocol_candid()?;
1018        let parent = configured.parent.as_deref().ok_or_else(|| {
1019            current_protocol::CurrentProtocolError::Configuration(format!(
1020                "Root-owned canister {} has no Root parent",
1021                configured.name
1022            ))
1023        })?;
1024        let root = self.current_principal(state, parent).ok_or_else(|| {
1025            current_protocol::CurrentProtocolError::Configuration(format!(
1026                "Root-owned canister {} has no resolved Root",
1027                configured.name
1028            ))
1029        })?;
1030        let retained_observation = |mode| {
1031            self.retained_root_owned_observation(configured, principal, parent, root, state, mode)
1032        };
1033        if self.required_root_status(&configured.name, root)? == CanisterRuntimeStatus::Stopped {
1034            return retained_observation(RetainedRootOwnedObservationMode::DeferredUntilRootStart);
1035        }
1036        let predecessor_status = self.predecessor_root_status_authorized(parent, root)?;
1037        if predecessor_status {
1038            self.record_recovery_reinstalls(parent);
1039        }
1040        let target = parse_principal("Root-owned canister", principal)?;
1041        let mut start_after = None;
1042        loop {
1043            let page = if predecessor_status {
1044                predecessor_root_status::query_pool(
1045                    &self.icp,
1046                    parse_principal("Fleet Subnet Root", root)?,
1047                    start_after,
1048                    256,
1049                )
1050                .map_err(|error| IcpEnsurePlatformError::PredecessorRootStatus(Box::new(error)))?
1051            } else {
1052                let response: Result<RootPoolStatusResponse, CanisterProtocolError> =
1053                    query_with_candid(
1054                        &self.icp,
1055                        &candid,
1056                        parse_principal("Fleet Subnet Root", root)?,
1057                        canic_protocol::CANIC_STATUS,
1058                        &RootPoolStatusRequest::Pool(CanisterPoolStatusRequest {
1059                            start_after,
1060                            limit: 256,
1061                        }),
1062                    );
1063                let response = match response {
1064                    Ok(response) => response,
1065                    Err(error) if recoverable_root_status_error(&error) => {
1066                        return retained_observation(
1067                            RetainedRootOwnedObservationMode::ReinstallRecovery,
1068                        );
1069                    }
1070                    Err(error) => {
1071                        return Err(current_protocol::CurrentProtocolError::from(error).into());
1072                    }
1073                };
1074                let RootPoolStatusResponse::Pool(page) = response;
1075                *page
1076            };
1077            if let Some(asset) = page
1078                .entries
1079                .into_iter()
1080                .find(|asset| asset.canister_id == target)
1081            {
1082                if matches!(
1083                    asset.status,
1084                    canic_core::dto::pool::CanisterPoolAssetStatus::PendingReset
1085                ) && asset.cycles.to_u128() == 0
1086                {
1087                    return retained_observation(
1088                        RetainedRootOwnedObservationMode::ReinstallRecovery,
1089                    );
1090                }
1091                return Self::observed_root_owned_asset(configured, principal, root, asset);
1092            }
1093            let next = page.next_start_after;
1094            if next.is_none() {
1095                return Ok(None);
1096            }
1097            if next == start_after {
1098                return Err(
1099                    current_protocol::CurrentProtocolError::Configuration(format!(
1100                        "Root {parent} pool cursor did not advance"
1101                    ))
1102                    .into(),
1103                );
1104            }
1105            start_after = next;
1106        }
1107    }
1108
1109    fn inspect_root_owned_funding_balance(
1110        &self,
1111        configured: &crate::fleet_ensure::model::DesiredCanister,
1112        principal: &str,
1113        state: &FleetEnsureStateRecord,
1114    ) -> Result<u128, IcpEnsurePlatformError> {
1115        let parent = configured.parent.as_deref().ok_or_else(|| {
1116            IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1117                canister: configured.name.clone(),
1118                field: "Root parent",
1119            }
1120        })?;
1121        let root = self.current_principal(state, parent).ok_or_else(|| {
1122            IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1123                canister: configured.name.clone(),
1124                field: "Root Principal",
1125            }
1126        })?;
1127        if self.required_root_status(&configured.name, root)? != CanisterRuntimeStatus::Running {
1128            return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
1129                canister: configured.name.clone(),
1130                field: "running Root",
1131            });
1132        }
1133        self.require_operator()?;
1134        let target = parse_principal("Root-owned funding target", principal)?;
1135        let response: RootInspectionResponse = call_with_candid(
1136            &self.icp,
1137            &self.root_protocol_candid()?,
1138            parse_principal("Fleet Subnet Root", root)?,
1139            canic_protocol::CANIC_ROOT_COMMAND,
1140            &RootInspectionCommand::InspectCanister(CanisterInspectionRequest {
1141                canister_id: target,
1142            }),
1143        )
1144        .map_err(current_protocol::CurrentProtocolError::from)?;
1145        let RootInspectionResponse::InspectCanister(response) = response;
1146        validate_root_funding_inspection(
1147            &configured.name,
1148            configured.kind,
1149            root,
1150            &response.settings.controllers,
1151            response.module_hash.as_deref(),
1152            &response.cycles,
1153        )
1154    }
1155
1156    fn observed_root_owned_asset(
1157        configured: &crate::fleet_ensure::model::DesiredCanister,
1158        principal: &str,
1159        root: &str,
1160        asset: CanisterPoolAsset,
1161    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1162        let Some(root_owned_lifecycle) = root_owned_lifecycle(configured.kind, &asset.status)
1163        else {
1164            return Err(
1165                current_protocol::CurrentProtocolError::Configuration(format!(
1166                    "Root-owned canister {} has a live pool role incompatible with desired {:?}",
1167                    configured.name, configured.kind
1168                ))
1169                .into(),
1170            );
1171        };
1172        let status = match root_owned_lifecycle {
1173            RootOwnedCanisterLifecycle::Store | RootOwnedCanisterLifecycle::Workload => {
1174                CanisterRuntimeStatus::Running
1175            }
1176            RootOwnedCanisterLifecycle::Claimed
1177            | RootOwnedCanisterLifecycle::Idle
1178            | RootOwnedCanisterLifecycle::Retained => CanisterRuntimeStatus::Stopped,
1179        };
1180        Ok(Some(LiveCanister {
1181            canister_version: None,
1182            controllers: vec![root.to_string()],
1183            cycles: asset.cycles.to_u128(),
1184            module_sha256: None,
1185            principal: principal.to_string(),
1186            reinstall_required: false,
1187            root_owned_lifecycle: Some(root_owned_lifecycle),
1188            status,
1189        }))
1190    }
1191
1192    fn predecessor_root_status_authorized(
1193        &self,
1194        root_name: &str,
1195        root: &str,
1196    ) -> Result<bool, IcpEnsurePlatformError> {
1197        let paths = EnsurePaths::under(&self.root, &self.desired.environment, &self.desired.fleet);
1198        let Some(authority) = read_root_start_authority(&paths).map_err(|error| {
1199            IcpEnsurePlatformError::RetainedRootStatusAuthority(Box::new(error))
1200        })?
1201        else {
1202            return Ok(false);
1203        };
1204        let Some(binding) = authority
1205            .roots
1206            .iter()
1207            .find(|binding| binding.principal == root)
1208        else {
1209            return Ok(false);
1210        };
1211        let live = self.status_optional(root)?.ok_or_else(|| {
1212            current_protocol::CurrentProtocolError::Configuration(format!(
1213                "retained predecessor Root {root_name} is unavailable"
1214            ))
1215        })?;
1216        if live.module_sha256.as_deref() != Some(binding.predecessor_module_sha256.as_str()) {
1217            return Ok(false);
1218        }
1219        let bootstrap = self.desired.bootstrap.as_ref().ok_or_else(|| {
1220            current_protocol::CurrentProtocolError::Configuration(
1221                "predecessor Root status requires current Fleet bootstrap authority".to_string(),
1222            )
1223        })?;
1224        let configured = self
1225            .desired
1226            .canisters
1227            .iter()
1228            .find(|configured| {
1229                configured.name == root_name && configured.kind == DesiredCanisterKind::Root
1230            })
1231            .ok_or_else(|| {
1232                current_protocol::CurrentProtocolError::Configuration(format!(
1233                    "predecessor Root {root_name} is absent from desired topology"
1234                ))
1235            })?;
1236        let successor_wasm = configured.wasm.as_deref().ok_or_else(|| {
1237            current_protocol::CurrentProtocolError::Configuration(format!(
1238                "predecessor Root {root_name} has no desired successor artifact"
1239            ))
1240        })?;
1241        let successor_module = artifact_hash(&resolve_path(&self.root, successor_wasm))?;
1242        let identity_matches = authority.environment == self.desired.environment
1243            && authority.fleet == self.desired.fleet
1244            && authority.fleet_id == bootstrap.fleet_id
1245            && authority.release_build_id == bootstrap.release_build_id
1246            && authority.successor_module_sha256 == successor_module
1247            && binding.name == configured.name
1248            && configured.principal.as_deref() == Some(root)
1249            && binding.subnet == configured.subnet
1250            && binding.controllers == configured.controllers
1251            && live.controllers == configured.controllers;
1252        if !identity_matches {
1253            return Err(
1254                current_protocol::CurrentProtocolError::Configuration(format!(
1255                    "predecessor Root {root_name} conflicts with sealed successor authority"
1256                ))
1257                .into(),
1258            );
1259        }
1260        verify_root_start_release_authority(&self.root, &authority).map_err(|error| {
1261            IcpEnsurePlatformError::RetainedRootStatusAuthority(Box::new(error))
1262        })?;
1263        Ok(true)
1264    }
1265
1266    fn root_protocol_candid(&self) -> Result<PathBuf, IcpEnsurePlatformError> {
1267        let protocol = self.desired.protocol.as_ref().ok_or_else(|| {
1268            current_protocol::CurrentProtocolError::Configuration(
1269                "Root-owned observation requires typed Fleet protocol".to_string(),
1270            )
1271        })?;
1272        Ok(resolve_path(&self.root, &protocol.root_candid))
1273    }
1274
1275    fn required_root_status(
1276        &self,
1277        configured_name: &str,
1278        root: &str,
1279    ) -> Result<CanisterRuntimeStatus, IcpEnsurePlatformError> {
1280        self.status_optional(root)?
1281            .map(|live| live.status)
1282            .ok_or_else(|| {
1283                current_protocol::CurrentProtocolError::Configuration(format!(
1284                    "Root-owned canister {configured_name} has no live Root"
1285                ))
1286                .into()
1287            })
1288    }
1289
1290    fn retained_root_owned_observation(
1291        &self,
1292        configured: &crate::fleet_ensure::model::DesiredCanister,
1293        principal: &str,
1294        parent: &str,
1295        root: &str,
1296        state: &FleetEnsureStateRecord,
1297        mode: RetainedRootOwnedObservationMode,
1298    ) -> Result<Option<LiveCanister>, IcpEnsurePlatformError> {
1299        let Some(retained_topology) = exact_retained_root_owned_topology(
1300            state,
1301            &configured.name,
1302            configured.kind,
1303            parent,
1304            principal,
1305            root,
1306        ) else {
1307            return Err(
1308                current_protocol::CurrentProtocolError::Configuration(format!(
1309                    "Root-owned canister {} has no exact retained topology authority",
1310                    configured.name
1311                ))
1312                .into(),
1313            );
1314        };
1315        let cycles = self
1316            .public_cycle_balance(principal)
1317            .or_else(|| state.retained_cycles_by_principal.get(principal).copied())
1318            .ok_or_else(|| {
1319                current_protocol::CurrentProtocolError::Configuration(format!(
1320                    "Root-owned canister {} has no current or retained exact native balance",
1321                    configured.name
1322                ))
1323            })?;
1324        if !matches!(
1325            configured.kind,
1326            DesiredCanisterKind::Pool | DesiredCanisterKind::Store
1327        ) || configured.presence != crate::fleet_ensure::model::DesiredPresence::Present
1328            || configured.replace
1329            || configured.drain.is_some()
1330            || (configured.kind == DesiredCanisterKind::Pool && configured.wasm.is_some())
1331            || (configured.kind == DesiredCanisterKind::Store && configured.wasm.is_none())
1332        {
1333            return Err(
1334                current_protocol::CurrentProtocolError::Configuration(format!(
1335                    "Root-owned recovery evidence for {} cannot authorize a mutation",
1336                    configured.name
1337                ))
1338                .into(),
1339            );
1340        }
1341        let root_config = self
1342            .desired
1343            .canisters
1344            .iter()
1345            .find(|canister| canister.name == parent && canister.kind == DesiredCanisterKind::Root)
1346            .ok_or_else(|| {
1347                current_protocol::CurrentProtocolError::Configuration(format!(
1348                    "Root-owned canister {} has no exact configured Root",
1349                    configured.name
1350                ))
1351            })?;
1352        let live_root = self.status_optional(root)?.ok_or_else(|| {
1353            current_protocol::CurrentProtocolError::Configuration(format!(
1354                "Root-owned canister {} has no live Root",
1355                configured.name
1356            ))
1357        })?;
1358        let mut expected_controllers = root_config.controllers.clone();
1359        expected_controllers.sort();
1360        if live_root.controllers != expected_controllers {
1361            return Err(
1362                current_protocol::CurrentProtocolError::Configuration(format!(
1363                    "Root-owned canister {} Root controllers drifted",
1364                    configured.name
1365                ))
1366                .into(),
1367            );
1368        }
1369        if matches!(mode, RetainedRootOwnedObservationMode::ReinstallRecovery) {
1370            self.record_recovery_reinstalls(parent);
1371        }
1372        Ok(Some(LiveCanister {
1373            canister_version: None,
1374            controllers: vec![root.to_string()],
1375            cycles,
1376            module_sha256: retained_topology.module_hash.clone(),
1377            principal: principal.to_string(),
1378            reinstall_required: false,
1379            root_owned_lifecycle: Some(RootOwnedCanisterLifecycle::Retained),
1380            status: if configured.kind == DesiredCanisterKind::Store {
1381                CanisterRuntimeStatus::Running
1382            } else {
1383                CanisterRuntimeStatus::Stopped
1384            },
1385        }))
1386    }
1387
1388    fn record_recovery_reinstalls(&self, root_name: &str) {
1389        let mut recovery = self.recovery_reinstalls.borrow_mut();
1390        recovery.insert(root_name.to_string());
1391        recovery.extend(
1392            self.desired
1393                .canisters
1394                .iter()
1395                .filter(|canister| {
1396                    canister.kind == DesiredCanisterKind::Coordinator
1397                        || (canister.kind == DesiredCanisterKind::Store
1398                            && canister.parent.as_deref() == Some(root_name))
1399                })
1400                .map(|canister| canister.name.clone()),
1401        );
1402    }
1403
1404    fn completed_reinstall_is_current(
1405        &self,
1406        state: &FleetEnsureStateRecord,
1407        name: &str,
1408        live: &LiveCanister,
1409    ) -> Result<bool, IcpEnsurePlatformError> {
1410        if state.completed_reinstall_operation_id.is_none()
1411            || !state.completed_reinstall_action_sha256.contains_key(name)
1412        {
1413            return Ok(false);
1414        }
1415        let Some(pre_canister_version) = state.completed_reinstalls.get(name) else {
1416            return Ok(false);
1417        };
1418        let Some(configured) = self
1419            .desired
1420            .canisters
1421            .iter()
1422            .find(|configured| configured.name == name)
1423        else {
1424            return Ok(false);
1425        };
1426        let Some(wasm) = configured.wasm.as_deref() else {
1427            return Ok(false);
1428        };
1429        let principal_matches = state
1430            .principals
1431            .get(name)
1432            .is_some_and(|principal| principal == &live.principal);
1433        let retained_topology = state.topology.get(name);
1434        let topology_matches = retained_topology.is_some_and(|topology| {
1435            topology.kind == configured.kind && topology.parent == configured.parent
1436        });
1437        let desired_module_sha256 = artifact_hash(&resolve_path(&self.root, wasm))?;
1438        let root_owned_store_module = (live.module_sha256.is_none()
1439            && live.root_owned_lifecycle == Some(RootOwnedCanisterLifecycle::Store))
1440        .then(|| {
1441            configured
1442                .parent
1443                .as_deref()
1444                .and_then(|parent| state.principals.get(parent))
1445                .filter(|root| live.controllers.as_slice() == [root.as_str()])
1446                .and(retained_topology)
1447                .and_then(|topology| topology.module_hash.as_deref())
1448        })
1449        .flatten();
1450        completed_reinstall_continuity(
1451            *pre_canister_version,
1452            principal_matches,
1453            topology_matches,
1454            live.module_sha256.as_deref().or(root_owned_store_module),
1455            &desired_module_sha256,
1456            live.canister_version,
1457        )
1458        .map_err(
1459            |field| IcpEnsurePlatformError::CompletedReinstallAuthorityConflict {
1460                canister: name.to_string(),
1461                field,
1462            },
1463        )?;
1464        Ok(true)
1465    }
1466
1467    fn public_cycle_balance(&self, principal: &str) -> Option<u128> {
1468        let response: Result<ManagedCanisterStatusResponse, canic_core::dto::error::Error> = self
1469            .icp
1470            .canister_query_candid(
1471                principal,
1472                canic_protocol::CANIC_STATUS,
1473                &ManagedCanisterStatusRequest::CycleBalance,
1474                None,
1475            )
1476            .ok()?;
1477        let ManagedCanisterStatusResponse::CycleBalance(balance) = response.ok()?;
1478        Some(balance.cycles)
1479    }
1480
1481    fn apply_create(
1482        &self,
1483        authority: CreateCanisterAuthority<'_>,
1484        state: &FleetEnsureStateRecord,
1485    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
1486        let creation_fee = self
1487            .desired
1488            .management_creation_fee_cycles
1489            .parse::<Cycles>()
1490            .map(|cycles| cycles.to_u128())
1491            .map_err(|_| IcpEnsurePlatformError::Arithmetic("management creation fee"))?;
1492        let amount = authority
1493            .requested_initial_cycles
1494            .checked_add(creation_fee)
1495            .ok_or(IcpEnsurePlatformError::Arithmetic("creation amount"))?;
1496        let controllers = self
1497            .resolved_controllers(state, authority.controllers, authority.controller_canisters)?
1498            .iter()
1499            .map(|value| parse_principal("controller", value))
1500            .collect::<Result<Vec<_>, _>>()?;
1501        let request = CreateCanisterArgs {
1502            amount: Nat::from(amount),
1503            created_at_time: Some(authority.created_at_time),
1504            creation_args: Some(CmcCreateCanisterArgs {
1505                settings: Some(CanisterSettings {
1506                    compute_allocation: None,
1507                    controllers: Some(controllers),
1508                    freezing_threshold: None,
1509                    memory_allocation: None,
1510                    reserved_cycles_limit: None,
1511                }),
1512                subnet_selection: Some(SubnetSelection::Subnet {
1513                    subnet: parse_principal("subnet", authority.subnet)?,
1514                }),
1515            }),
1516            from_subaccount: None,
1517        };
1518        let response: Result<CreateCanisterSuccess, CreateCanisterError> = self
1519            .icp
1520            .canister_call_candid(authority.ledger, "create_canister", &request, None)?;
1521        create_canister_response_outcome(response, authority.requested_initial_cycles)
1522    }
1523
1524    fn apply_fund(
1525        &self,
1526        amount: u128,
1527        created_at_time: u64,
1528        ledger: &str,
1529        principal: &str,
1530    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
1531        let request = WithdrawArgs {
1532            amount: Nat::from(amount),
1533            created_at_time: Some(created_at_time),
1534            from_subaccount: None,
1535            to: parse_principal("funding target", principal)?,
1536        };
1537        let response: Result<Nat, WithdrawError> = self
1538            .icp
1539            .canister_call_candid(ledger, "withdraw", &request, None)?;
1540        match response {
1541            Ok(block) => Ok(EffectOutcome {
1542                created_principal: None,
1543                post_cycles: None,
1544                receipt: Some(block.to_string()),
1545            }),
1546            Err(WithdrawError::Duplicate { duplicate_of }) => Ok(EffectOutcome {
1547                created_principal: None,
1548                post_cycles: None,
1549                receipt: Some(duplicate_of.to_string()),
1550            }),
1551            Err(error) => Err(IcpEnsurePlatformError::LedgerWithdraw(
1552                render_withdraw_error(error),
1553            )),
1554        }
1555    }
1556
1557    #[expect(
1558        clippy::too_many_arguments,
1559        reason = "the install boundary verifies one complete immutable action tuple"
1560    )]
1561    fn apply_install(
1562        &self,
1563        operation_id: &str,
1564        principals: &BTreeMap<String, String>,
1565        canic_init: Option<&crate::fleet_ensure::model::DesiredCanisterInit>,
1566        init_arg: Option<&str>,
1567        init_arg_sha256: Option<&str>,
1568        init_candid: Option<&str>,
1569        init_candid_sha256: Option<&str>,
1570        mode: InstallMode,
1571        principal: &str,
1572        wasm: &str,
1573        wasm_sha256: &str,
1574    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
1575        verify_artifact(&resolve_path(&self.root, wasm), wasm_sha256, "Wasm")?;
1576        let mut command = self.icp.canister_command();
1577        command.args(["install", principal, "--mode"]);
1578        command.arg(match mode {
1579            InstallMode::Install => "install",
1580            InstallMode::Reinstall => "reinstall",
1581        });
1582        command.args(["--yes", "--wasm"]);
1583        command.arg(resolve_path(&self.root, wasm));
1584        let generated_init = if let Some(canic_init) = canic_init {
1585            let path = canic_init::write_arguments(canic_init::CanicInitRequest {
1586                desired: &self.desired,
1587                init: canic_init,
1588                operation_id,
1589                principals,
1590                root: &self.root,
1591                wasm,
1592                wasm_sha256,
1593            })?;
1594            command.arg("--args-file");
1595            command.arg(&path);
1596            command.args(["--args-format", "bin"]);
1597            Some(path)
1598        } else if let Some(init_arg) = init_arg {
1599            let expected =
1600                init_arg_sha256.ok_or_else(|| IcpEnsurePlatformError::ArtifactDigestMismatch {
1601                    actual: "missing".to_string(),
1602                    expected: "reviewed init argument identity".to_string(),
1603                    kind: "init argument",
1604                })?;
1605            let init_candid =
1606                init_candid.ok_or_else(|| IcpEnsurePlatformError::ArtifactDigestMismatch {
1607                    actual: "missing".to_string(),
1608                    expected: "reviewed init Candid".to_string(),
1609                    kind: "init Candid",
1610                })?;
1611            let init_candid_sha256 = init_candid_sha256.ok_or_else(|| {
1612                IcpEnsurePlatformError::ArtifactDigestMismatch {
1613                    actual: "missing".to_string(),
1614                    expected: "reviewed init Candid identity".to_string(),
1615                    kind: "init Candid",
1616                }
1617            })?;
1618            let path = protocol::write_init_arguments(
1619                &self.root,
1620                operation_id,
1621                principals,
1622                init_candid,
1623                init_candid_sha256,
1624                init_arg,
1625                expected,
1626            )?;
1627            command.arg("--args-file");
1628            command.arg(&path);
1629            command.args(["--args-format", "bin"]);
1630            Some(path)
1631        } else {
1632            command.args(["--args", "()"]);
1633            None
1634        };
1635        self.icp.add_target_args(&mut command);
1636        let result = run_status(&mut command).map_err(IcpEnsurePlatformError::from);
1637        if let Some(path) = generated_init {
1638            std::fs::remove_file(&path)
1639                .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path))?;
1640        }
1641        result?;
1642        Ok(empty_outcome())
1643    }
1644
1645    fn apply_controllers(
1646        &self,
1647        principal: &str,
1648        controllers: &[String],
1649    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
1650        let mut command = self.icp.canister_command();
1651        command.args([
1652            "settings",
1653            "update",
1654            principal,
1655            "--force",
1656            "--remove-all-controllers",
1657        ]);
1658        for controller in controllers {
1659            parse_principal("controller", controller)?;
1660            command.args(["--add-controller", controller]);
1661        }
1662        self.icp.add_target_args(&mut command);
1663        run_status(&mut command)?;
1664        Ok(empty_outcome())
1665    }
1666
1667    #[expect(
1668        clippy::too_many_arguments,
1669        reason = "one typed transfer effect carries the complete reviewed authority tuple"
1670    )]
1671    fn apply_transfer(
1672        &self,
1673        amount: u128,
1674        candid: &str,
1675        candid_sha256: &str,
1676        destination: &str,
1677        method: &str,
1678        operation_id: &str,
1679        principal: &str,
1680    ) -> Result<EffectOutcome, IcpEnsurePlatformError> {
1681        let candid = resolve_path(&self.root, candid);
1682        if !candid.is_file() {
1683            return Err(IcpEnsurePlatformError::CandidUnavailable(candid));
1684        }
1685        verify_artifact(&candid, candid_sha256, "drain Candid")?;
1686        let response: DrainResponse = self.icp.canister_call_candid(
1687            principal,
1688            method,
1689            &DrainRequest {
1690                operation_id: operation_id.to_string(),
1691                destination: parse_principal("treasury", destination)?,
1692                cycles: Nat::from(amount),
1693            },
1694            Some(&candid),
1695        )?;
1696        let transferred = match response {
1697            DrainResponse::Accepted { transferred_cycles }
1698            | DrainResponse::Replayed { transferred_cycles } => transferred_cycles,
1699        };
1700        if transferred != amount {
1701            return Err(IcpEnsurePlatformError::WrongTransferAmount {
1702                actual: transferred,
1703                expected: amount,
1704            });
1705        }
1706        Ok(EffectOutcome {
1707            created_principal: None,
1708            post_cycles: None,
1709            receipt: Some(operation_id.to_string()),
1710        })
1711    }
1712}
1713
1714impl EnsurePlatform for IcpEnsurePlatform {
1715    type Error = IcpEnsurePlatformError;
1716
1717    fn bind_reviewed_desired(&mut self, desired: &DesiredFleet) -> Result<(), Self::Error> {
1718        self.desired = desired.clone();
1719        Ok(())
1720    }
1721
1722    fn observe_root_management(
1723        &mut self,
1724        state: &FleetEnsureStateRecord,
1725        reviewed_targets: &BTreeSet<String>,
1726    ) -> Result<Option<RootManagementObservation>, Self::Error> {
1727        let configured_roots = self
1728            .desired
1729            .canisters
1730            .iter()
1731            .filter(|configured| {
1732                configured.kind == DesiredCanisterKind::Root
1733                    && configured.presence == crate::fleet_ensure::model::DesiredPresence::Present
1734            })
1735            .collect::<Vec<_>>();
1736        if configured_roots.is_empty() {
1737            return Ok(None);
1738        }
1739        self.require_operator()?;
1740        let mut observed_roots = Vec::new();
1741        for configured in configured_roots {
1742            let Some(principal) = self.current_principal(state, &configured.name) else {
1743                if is_unallocated_fresh_root(&self.desired, configured, reviewed_targets) {
1744                    continue;
1745                }
1746                return Err(IcpEnsurePlatformError::RootManagement(format!(
1747                    "configured Root {} has no exact Principal",
1748                    configured.name
1749                )));
1750            };
1751            let live = self.status_optional(principal)?.ok_or_else(|| {
1752                IcpEnsurePlatformError::RootManagement(format!(
1753                    "configured Root {} is unavailable",
1754                    configured.name
1755                ))
1756            })?;
1757            observed_roots.push((configured, live));
1758        }
1759        if reviewed_targets.is_empty()
1760            && observed_roots
1761                .iter()
1762                .all(|(_, live)| live.status == CanisterRuntimeStatus::Running)
1763        {
1764            return Ok(None);
1765        }
1766        let network = resolve_icp_build_network_from_root(&self.root, &self.desired.environment)
1767            .map_err(|error| IcpEnsurePlatformError::RootManagement(error.to_string()))?;
1768        let catalog = if network == BuildNetwork::Ic {
1769            let now = SystemTime::now()
1770                .duration_since(UNIX_EPOCH)
1771                .map_err(|_| {
1772                    IcpEnsurePlatformError::RootManagement(
1773                        "system clock is before the Unix epoch".to_string(),
1774                    )
1775                })?
1776                .as_secs();
1777            Some(
1778                load_mainnet_subnet_catalog(&self.root, now)
1779                    .map_err(|error| IcpEnsurePlatformError::RootManagement(error.to_string()))?,
1780            )
1781        } else {
1782            None
1783        };
1784        let mut roots = BTreeMap::new();
1785        for (configured, live) in observed_roots {
1786            let principal = live.principal.clone();
1787            let subnet = catalog.as_ref().map_or_else(
1788                || Ok(configured.subnet.clone()),
1789                |catalog| {
1790                    catalog
1791                        .catalog
1792                        .resolve_canister_route(&principal)
1793                        .map(|route| route.subnet.to_text())
1794                        .map_err(|error| IcpEnsurePlatformError::RootManagement(error.to_string()))
1795                },
1796            )?;
1797            let name = configured.name.clone();
1798            if roots
1799                .insert(
1800                    name.clone(),
1801                    RootManagementCanisterObservation { live, name, subnet },
1802                )
1803                .is_some()
1804            {
1805                return Err(IcpEnsurePlatformError::RootManagement(
1806                    "configured Root names are not unique".to_string(),
1807                ));
1808            }
1809        }
1810        let operator_cycles = self
1811            .icp
1812            .identity_cycles_balance()
1813            .map_err(|error| IcpEnsurePlatformError::LedgerWithdraw(error.to_string()))?;
1814        Ok(Some(RootManagementObservation {
1815            operator_cycles,
1816            roots,
1817        }))
1818    }
1819
1820    fn observe(
1821        &mut self,
1822        operation_id: &str,
1823        state: &FleetEnsureStateRecord,
1824    ) -> Result<FleetObservation, Self::Error> {
1825        self.require_operator()?;
1826        self.recovery_reinstalls.borrow_mut().clear();
1827        let mut canisters = BTreeMap::new();
1828        for configured in &self.desired.canisters {
1829            let observed = self
1830                .current_principal(state, &configured.name)
1831                .map(|principal| self.observe_configured_canister(configured, principal, state))
1832                .transpose()?
1833                .flatten();
1834            canisters.insert(configured.name.clone(), observed);
1835        }
1836        let mut completed_reinstalls = BTreeSet::new();
1837        for name in self.recovery_reinstalls.borrow().iter() {
1838            let Some(live) = canisters.get_mut(name).and_then(Option::as_mut) else {
1839                continue;
1840            };
1841            if self.completed_reinstall_is_current(state, name, live)? {
1842                if live.module_sha256.is_none()
1843                    && live.root_owned_lifecycle == Some(RootOwnedCanisterLifecycle::Store)
1844                {
1845                    live.module_sha256 = state
1846                        .topology
1847                        .get(name)
1848                        .and_then(|topology| topology.module_hash.clone());
1849                }
1850                completed_reinstalls.insert(name.clone());
1851            }
1852        }
1853        self.recovery_reinstalls
1854            .borrow_mut()
1855            .retain(|name| !completed_reinstalls.contains(name));
1856        for name in self.recovery_reinstalls.borrow().iter() {
1857            if let Some(live) = canisters.get_mut(name).and_then(Option::as_mut) {
1858                live.reinstall_required = true;
1859            }
1860        }
1861        let principals = self.protocol_principals(state);
1862        let protocol_ready = self
1863            .desired
1864            .protocol_steps
1865            .iter()
1866            .map(|step| {
1867                let Some(principal) = principals.get(&step.canister) else {
1868                    return Ok((step.name.clone(), false));
1869                };
1870                let live = canisters.get(&step.canister).and_then(Option::as_ref);
1871                let Some(live) = live else {
1872                    return Ok((step.name.clone(), false));
1873                };
1874                let configured = self
1875                    .desired
1876                    .canisters
1877                    .iter()
1878                    .find(|configured| configured.name == step.canister)
1879                    .expect("protocol target was validated against desired canisters");
1880                if let Some(wasm) = &configured.wasm {
1881                    let desired_hash = artifact_hash(&resolve_path(&self.root, wasm))?;
1882                    if live.module_sha256.as_deref() != Some(desired_hash.as_str()) {
1883                        return Ok((step.name.clone(), false));
1884                    }
1885                }
1886                let action = self.observed_protocol_action(step, principal.clone())?;
1887                protocol::observe(&self.icp, &self.root, operation_id, &principals, &action)
1888                    .map(|observation| (step.name.clone(), observation.applied))
1889                    .map_err(IcpEnsurePlatformError::from)
1890            })
1891            .collect::<Result<BTreeMap<_, _>, _>>()?;
1892        Ok(FleetObservation {
1893            additional_controlled_cycles: BTreeMap::new(),
1894            canisters,
1895            ledger_fee_cycles: ledger_fee_cycles(self.icp.canister_query_candid(
1896                &self.desired.cycles_ledger,
1897                "icrc1_fee",
1898                &(),
1899                None,
1900            )?)?,
1901            operator_cycles: self
1902                .icp
1903                .identity_cycles_balance()
1904                .map_err(|error| IcpEnsurePlatformError::LedgerWithdraw(error.to_string()))?,
1905            protocol_ready,
1906        })
1907    }
1908
1909    fn protocol_actions(
1910        &mut self,
1911        operation_id: &str,
1912        state: &FleetEnsureStateRecord,
1913    ) -> Result<Vec<EnsureAction>, Self::Error> {
1914        if self.desired.protocol.is_none() {
1915            return Ok(Vec::new());
1916        }
1917        current_protocol::validate_component_pool_capacity(&self.root, &self.desired)?;
1918        if self.has_stopped_retained_protocol_owner(state)? {
1919            return Ok(Vec::new());
1920        }
1921        if !self.recovery_reinstalls.borrow().is_empty() {
1922            return Ok(Vec::new());
1923        }
1924        let coordinator = self
1925            .desired
1926            .canisters
1927            .iter()
1928            .find(|configured| {
1929                configured.presence == crate::fleet_ensure::model::DesiredPresence::Present
1930                    && configured.kind
1931                        == crate::fleet_ensure::model::DesiredCanisterKind::Coordinator
1932            })
1933            .expect("typed topology validation requires one Coordinator");
1934        let Some(principal) = self.current_principal(state, &coordinator.name) else {
1935            return Ok(Vec::new());
1936        };
1937        let Some(live) = self.status_optional(principal)? else {
1938            return Ok(Vec::new());
1939        };
1940        if let Some(wasm) = &coordinator.wasm {
1941            let expected = artifact_hash(&resolve_path(&self.root, wasm))?;
1942            if live.module_sha256.as_deref() != Some(expected.as_str()) {
1943                return Ok(Vec::new());
1944            }
1945        }
1946        current_protocol::compile(&self.icp, &self.root, &self.desired, operation_id, state)
1947            .map_err(Into::into)
1948    }
1949
1950    fn terminal_inventory(
1951        &mut self,
1952        operation_id: &str,
1953        state: &FleetEnsureStateRecord,
1954    ) -> Result<TerminalFleetInventory, Self::Error> {
1955        if self.desired.protocol.is_none() {
1956            if state.active_registry.is_some() {
1957                return Err(current_protocol::CurrentProtocolError::Configuration(
1958                    "managed terminal Fleet cannot drop its typed protocol intent".to_string(),
1959                )
1960                .into());
1961            }
1962            return Ok(TerminalFleetInventory::default());
1963        }
1964        super::current_inventory::terminal_inventory(
1965            &self.icp,
1966            &self.root,
1967            &self.desired,
1968            operation_id,
1969            state,
1970        )
1971        .map_err(Into::into)
1972    }
1973
1974    #[expect(
1975        clippy::too_many_lines,
1976        reason = "the platform keeps every effect's exact live terminal predicate in one exhaustive match"
1977    )]
1978    fn observe_effect(
1979        &mut self,
1980        operation_id: &str,
1981        action: &EnsureAction,
1982        record: &EffectRecord,
1983        state: &FleetEnsureStateRecord,
1984    ) -> Result<EffectObservation, Self::Error> {
1985        let mut retry = EffectRetry::None;
1986        let mut post_cycles = None;
1987        let (applied, progress_identity) = match action {
1988            EnsureAction::Create {
1989                name,
1990                requested_initial_cycles,
1991                ..
1992            } => {
1993                let (live_cycles, deferred) =
1994                    if record.created_principal.is_some() && record.receipt.is_some() {
1995                        self.created_canister_cycles(
1996                            name,
1997                            record
1998                                .created_principal
1999                                .as_deref()
2000                                .ok_or(IcpEnsurePlatformError::LedgerCreatePending)?,
2001                            state,
2002                        )?
2003                    } else {
2004                        (None, false)
2005                    };
2006                post_cycles = live_cycles;
2007                let maximum_observation_burn_cycles = self
2008                    .desired
2009                    .maximum_observation_burn_cycles
2010                    .parse::<Cycles>()
2011                    .map(|cycles| cycles.to_u128())
2012                    .map_err(|_| IcpEnsurePlatformError::Arithmetic("observation burn"))?;
2013                let applied = create_balance_is_terminal(
2014                    live_cycles,
2015                    *requested_initial_cycles,
2016                    maximum_observation_burn_cycles,
2017                );
2018                if live_cycles.is_some() && !applied {
2019                    retry = EffectRetry::ReplanRequiredAfterCreateBalanceDrift;
2020                } else if deferred {
2021                    retry = EffectRetry::DeferUntilControllerObservation;
2022                }
2023                (
2024                    applied,
2025                    format!(
2026                        "created:{:?}:actual:{live_cycles:?}:requested:{requested_initial_cycles}",
2027                        record.created_principal,
2028                    ),
2029                )
2030            }
2031            EnsureAction::Delete { principal, .. } => {
2032                let live = self.status_optional(Self::action_principal(state, principal)?)?;
2033                (live.is_none(), format!("delete:{live:?}"))
2034            }
2035            EnsureAction::Fund {
2036                amount,
2037                expected_post_cycles,
2038                funding_deficit_cycles,
2039                funding_margin_cycles,
2040                name,
2041                principal,
2042                ..
2043            } => {
2044                let live_cycles = if record.receipt.is_some() {
2045                    let configured = self
2046                        .desired
2047                        .canisters
2048                        .iter()
2049                        .find(|configured| configured.name == *name)
2050                        .ok_or_else(|| {
2051                            current_protocol::CurrentProtocolError::Configuration(format!(
2052                                "funding target {name} is absent from desired topology"
2053                            ))
2054                        })?;
2055                    if configured.kind == DesiredCanisterKind::Pool {
2056                        Some(self.inspect_root_owned_funding_balance(
2057                            configured,
2058                            Self::action_principal(state, principal)?,
2059                            state,
2060                        )?)
2061                    } else {
2062                        self.action_cycles(action, state)?
2063                    }
2064                } else {
2065                    self.action_cycles(action, state)?
2066                };
2067                post_cycles = live_cycles;
2068                (
2069                    record.receipt.is_some()
2070                        && native_funding_applied(NativeFundingObservation {
2071                            amount: *amount,
2072                            expected_post_cycles: *expected_post_cycles,
2073                            funding_deficit_cycles: *funding_deficit_cycles,
2074                            funding_margin_cycles: *funding_margin_cycles,
2075                            live_cycles,
2076                            pre_cycles: record.pre_cycles,
2077                        }),
2078                    format!(
2079                        "native-topup:ledger-withdraw:{}:actual:{live_cycles:?}:expected:{expected_post_cycles}:margin:{funding_margin_cycles}",
2080                        record.receipt.as_deref().unwrap_or("pending"),
2081                    ),
2082                )
2083            }
2084            EnsureAction::Install {
2085                mode,
2086                principal,
2087                wasm_sha256,
2088                ..
2089            } => {
2090                let live =
2091                    self.install_status_optional(Self::action_principal(state, principal)?)?;
2092                let applied = live.as_ref().is_some_and(|live| {
2093                    install_effect_applied(
2094                        *mode,
2095                        wasm_sha256,
2096                        live.module_sha256.as_deref(),
2097                        record.pre_canister_version,
2098                        live.canister_version,
2099                    )
2100                });
2101                let progress_identity = live.as_ref().map_or_else(
2102                    || "install:missing".to_string(),
2103                    |live| {
2104                        format!(
2105                            "install:{:?}:version:{:?}",
2106                            live.module_sha256, live.canister_version
2107                        )
2108                    },
2109                );
2110                (applied, progress_identity)
2111            }
2112            EnsureAction::FleetProtocol {
2113                action: current_action,
2114                ..
2115            } => {
2116                let observation = match current_protocol::observe(&self.icp, &self.root, action) {
2117                    Ok(observation) => observation,
2118                    Err(error)
2119                        if matches!(
2120                            current_action.as_ref(),
2121                            crate::fleet_ensure::model::CurrentFleetProtocolAction::AdoptStore { .. }
2122                        ) && predecessor_store_control_rejection(&error) =>
2123                    {
2124                        if !self.retained_store_control_replan_is_exact(
2125                            operation_id,
2126                            action,
2127                            state,
2128                        )? {
2129                            return Err(error.into());
2130                        }
2131                        EffectObservation {
2132                            applied: false,
2133                            post_cycles: None,
2134                            progress_identity: "store-adoption:replan-required:diagnostic:132"
2135                                .to_string(),
2136                            retry: EffectRetry::ReplanRequiredAfterRejectedPrerequisite,
2137                        }
2138                    }
2139                    Err(error)
2140                        if matches!(
2141                            current_action.as_ref(),
2142                            crate::fleet_ensure::model::CurrentFleetProtocolAction::AdoptStore { .. }
2143                        ) && recoverable_current_protocol_error(&error) =>
2144                    {
2145                        EffectObservation {
2146                            applied: false,
2147                            post_cycles: None,
2148                            progress_identity: "store-adoption:protected-status-unavailable"
2149                                .to_string(),
2150                            retry: EffectRetry::None,
2151                        }
2152                    }
2153                    Err(error) => return Err(error.into()),
2154                };
2155                retry = observation.retry;
2156                (observation.applied, observation.progress_identity)
2157            }
2158            EnsureAction::Protocol { .. } => {
2159                let observation = protocol::observe(
2160                    &self.icp,
2161                    &self.root,
2162                    operation_id,
2163                    &self.protocol_principals(state),
2164                    &resolved_protocol_action(action, state)?,
2165                )?;
2166                (observation.applied, observation.progress_identity)
2167            }
2168            EnsureAction::SetControllers {
2169                controller_canisters,
2170                controllers,
2171                name,
2172                principal,
2173                ..
2174            } => {
2175                let expected =
2176                    self.resolved_controllers(state, controllers, controller_canisters)?;
2177                let principal = Self::action_principal(state, principal)?;
2178                let configured = self
2179                    .desired
2180                    .canisters
2181                    .iter()
2182                    .find(|configured| configured.name == *name);
2183                let mut observed_controllers = if let Some(configured) =
2184                    configured.filter(|configured| configured.kind == DesiredCanisterKind::Pool)
2185                {
2186                    self.inspect_root_owned_canister(configured, principal, state)?
2187                        .map(|response| {
2188                            response
2189                                .settings
2190                                .controllers
2191                                .into_iter()
2192                                .map(|controller| controller.to_text())
2193                                .collect::<Vec<_>>()
2194                        })
2195                } else {
2196                    self.status_optional(principal)?
2197                        .map(|live| live.controllers)
2198                };
2199                if let Some(controllers) = &mut observed_controllers {
2200                    controllers.sort();
2201                    controllers.dedup();
2202                }
2203                (
2204                    observed_controllers.as_ref() == Some(&expected),
2205                    format!("controllers:{observed_controllers:?}"),
2206                )
2207            }
2208            EnsureAction::Start { principal, .. } => {
2209                let live = self.status_optional(Self::action_principal(state, principal)?)?;
2210                (
2211                    live.as_ref()
2212                        .is_some_and(|live| live.status == CanisterRuntimeStatus::Running),
2213                    format!("start:{:?}", live.map(|live| live.status)),
2214                )
2215            }
2216            EnsureAction::Stop { principal, .. } => {
2217                let live = self.status_optional(Self::action_principal(state, principal)?)?;
2218                (
2219                    live.as_ref()
2220                        .is_some_and(|live| live.status == CanisterRuntimeStatus::Stopped),
2221                    format!("stop:{:?}", live.map(|live| live.status)),
2222                )
2223            }
2224            EnsureAction::Transfer {
2225                amount,
2226                maximum_execution_burn_cycles,
2227                name,
2228                principal,
2229                destination,
2230                ..
2231            } => {
2232                let source = self
2233                    .status_optional(Self::action_principal(state, principal)?)?
2234                    .map(|live| live.cycles)
2235                    .ok_or_else(|| IcpEnsurePlatformError::MissingTransferBalance {
2236                        canister: name.clone(),
2237                        side: "live source",
2238                    })?;
2239                let destination = self
2240                    .status_optional(self.current_principal(state, destination).ok_or_else(
2241                        || IcpEnsurePlatformError::UnresolvedCreated(destination.clone()),
2242                    )?)?
2243                    .map(|live| live.cycles)
2244                    .ok_or_else(|| IcpEnsurePlatformError::MissingTransferBalance {
2245                        canister: name.clone(),
2246                        side: "live treasury",
2247                    })?;
2248                let source_pre = record.pre_cycles.ok_or_else(|| {
2249                    IcpEnsurePlatformError::MissingTransferBalance {
2250                        canister: name.clone(),
2251                        side: "retained source",
2252                    }
2253                })?;
2254                let destination_pre = record.destination_pre_cycles.ok_or_else(|| {
2255                    IcpEnsurePlatformError::MissingTransferBalance {
2256                        canister: name.clone(),
2257                        side: "retained treasury",
2258                    }
2259                })?;
2260                let reconciliation = reconcile_retirement_transfer(RetirementTransferBalances {
2261                    destination_after: destination,
2262                    destination_before: destination_pre,
2263                    maximum_execution_burn: *maximum_execution_burn_cycles,
2264                    source_after: source,
2265                    source_before: source_pre,
2266                    transfer_amount: *amount,
2267                });
2268                let (applied, source_debit, destination_credit) = match reconciliation {
2269                    Ok(RetirementTransferReconciliation::Pending) => (false, 0, 0),
2270                    Ok(RetirementTransferReconciliation::Conserved {
2271                        destination_credit,
2272                        source_debit,
2273                    }) => (true, source_debit, destination_credit),
2274                    Err(RetirementTransferInvariantError::ArithmeticOverflow) => {
2275                        return Err(IcpEnsurePlatformError::Arithmetic(
2276                            "retirement transfer debit",
2277                        ));
2278                    }
2279                    Err(RetirementTransferInvariantError::BalanceMovedInInvalidDirection) => {
2280                        return Err(IcpEnsurePlatformError::InvalidTransferBalance {
2281                            canister: name.clone(),
2282                        });
2283                    }
2284                    Err(RetirementTransferInvariantError::Unbalanced {
2285                        destination_credit,
2286                        maximum_source_debit,
2287                        source_debit,
2288                    }) => {
2289                        return Err(IcpEnsurePlatformError::UnbalancedTransfer {
2290                            canister: name.clone(),
2291                            destination_credit,
2292                            expected: *amount,
2293                            maximum_source_debit,
2294                            source_debit,
2295                        });
2296                    }
2297                };
2298                (
2299                    applied,
2300                    format!("transfer:{source_debit}:{destination_credit}"),
2301                )
2302            }
2303        };
2304        Ok(EffectObservation {
2305            applied,
2306            post_cycles,
2307            progress_identity,
2308            retry,
2309        })
2310    }
2311
2312    #[expect(
2313        clippy::too_many_lines,
2314        reason = "the platform keeps every approved single-step effect in one exhaustive match"
2315    )]
2316    fn apply(
2317        &mut self,
2318        operation_id: &str,
2319        action: &EnsureAction,
2320        _record: &EffectRecord,
2321        state: &FleetEnsureStateRecord,
2322    ) -> Result<EffectOutcome, Self::Error> {
2323        self.require_operator()?;
2324        match action {
2325            EnsureAction::Create {
2326                controller_canisters,
2327                controllers,
2328                created_at_time,
2329                ledger,
2330                requested_initial_cycles,
2331                subnet,
2332                ..
2333            } => self.apply_create(
2334                CreateCanisterAuthority {
2335                    controller_canisters,
2336                    controllers,
2337                    created_at_time: *created_at_time,
2338                    ledger,
2339                    requested_initial_cycles: *requested_initial_cycles,
2340                    subnet,
2341                },
2342                state,
2343            ),
2344            EnsureAction::Delete {
2345                maximum_remaining_cycles,
2346                principal,
2347                ..
2348            } => {
2349                let principal = Self::action_principal(state, principal)?;
2350                if let Some(live) = self.status_optional(principal)? {
2351                    if live.cycles > *maximum_remaining_cycles {
2352                        return Err(IcpEnsurePlatformError::MaterialCycles {
2353                            actual: live.cycles,
2354                            canister: principal.to_string(),
2355                            maximum: *maximum_remaining_cycles,
2356                        });
2357                    }
2358                    if live.status != CanisterRuntimeStatus::Stopped {
2359                        return Err(IcpEnsurePlatformError::NotStopped {
2360                            canister: principal.to_string(),
2361                        });
2362                    }
2363                    self.icp.delete_canister_without_cycle_recovery(principal)?;
2364                }
2365                Ok(empty_outcome())
2366            }
2367            EnsureAction::Fund {
2368                amount,
2369                created_at_time,
2370                ledger,
2371                principal,
2372                ..
2373            } => self.apply_fund(
2374                *amount,
2375                *created_at_time,
2376                ledger,
2377                Self::action_principal(state, principal)?,
2378            ),
2379            EnsureAction::Install {
2380                canic_init,
2381                init_arg,
2382                init_arg_sha256,
2383                init_candid,
2384                init_candid_sha256,
2385                mode,
2386                principal,
2387                wasm,
2388                wasm_sha256,
2389                ..
2390            } => self.apply_install(
2391                operation_id,
2392                &self.protocol_principals(state),
2393                canic_init.as_ref(),
2394                init_arg.as_deref(),
2395                init_arg_sha256.as_deref(),
2396                init_candid.as_deref(),
2397                init_candid_sha256.as_deref(),
2398                *mode,
2399                Self::action_principal(state, principal)?,
2400                wasm,
2401                wasm_sha256,
2402            ),
2403            EnsureAction::FleetProtocol { .. } => {
2404                current_protocol::apply(&self.icp, &self.root, action).map_err(Into::into)
2405            }
2406            EnsureAction::Protocol { .. } => {
2407                let action = resolved_protocol_action(action, state)?;
2408                protocol::apply(
2409                    &self.icp,
2410                    &self.root,
2411                    operation_id,
2412                    &self.protocol_principals(state),
2413                    &action,
2414                )?;
2415                Ok(EffectOutcome {
2416                    created_principal: None,
2417                    post_cycles: None,
2418                    receipt: Some(operation_id.to_string()),
2419                })
2420            }
2421            EnsureAction::SetControllers {
2422                controller_canisters,
2423                controllers,
2424                principal,
2425                ..
2426            } => self.apply_controllers(
2427                Self::action_principal(state, principal)?,
2428                &self.resolved_controllers(state, controllers, controller_canisters)?,
2429            ),
2430            EnsureAction::Start { principal, .. } => {
2431                self.icp
2432                    .start_canister(Self::action_principal(state, principal)?)?;
2433                Ok(empty_outcome())
2434            }
2435            EnsureAction::Stop { principal, .. } => {
2436                self.icp
2437                    .stop_canister(Self::action_principal(state, principal)?)?;
2438                Ok(empty_outcome())
2439            }
2440            EnsureAction::Transfer {
2441                amount,
2442                candid,
2443                candid_sha256,
2444                destination,
2445                method,
2446                principal,
2447                ..
2448            } => self.apply_transfer(
2449                *amount,
2450                candid,
2451                candid_sha256,
2452                self.current_principal(state, destination).ok_or_else(|| {
2453                    IcpEnsurePlatformError::UnresolvedCreated(destination.clone())
2454                })?,
2455                method,
2456                operation_id,
2457                Self::action_principal(state, principal)?,
2458            ),
2459        }
2460    }
2461
2462    fn action_cycles(
2463        &mut self,
2464        action: &EnsureAction,
2465        state: &FleetEnsureStateRecord,
2466    ) -> Result<Option<u128>, Self::Error> {
2467        let (name, principal) = match action {
2468            EnsureAction::Create { .. } => return Ok(None),
2469            EnsureAction::Delete {
2470                name, principal, ..
2471            }
2472            | EnsureAction::FleetProtocol {
2473                name, principal, ..
2474            }
2475            | EnsureAction::Fund {
2476                name, principal, ..
2477            }
2478            | EnsureAction::Install {
2479                name, principal, ..
2480            }
2481            | EnsureAction::Protocol {
2482                name, principal, ..
2483            }
2484            | EnsureAction::SetControllers {
2485                name, principal, ..
2486            }
2487            | EnsureAction::Start {
2488                name, principal, ..
2489            }
2490            | EnsureAction::Stop {
2491                name, principal, ..
2492            }
2493            | EnsureAction::Transfer {
2494                name, principal, ..
2495            } => (name, Self::action_principal(state, principal)?),
2496        };
2497        let observed = self
2498            .desired
2499            .canisters
2500            .iter()
2501            .find(|configured| configured.name == *name)
2502            .map_or_else(
2503                || self.status_optional(principal),
2504                |configured| self.observe_configured_canister(configured, principal, state),
2505            )?;
2506        Ok(observed.map(|live| live.cycles))
2507    }
2508
2509    fn action_destination_cycles(
2510        &mut self,
2511        action: &EnsureAction,
2512        state: &FleetEnsureStateRecord,
2513    ) -> Result<Option<u128>, Self::Error> {
2514        let EnsureAction::Transfer { destination, .. } = action else {
2515            return Ok(None);
2516        };
2517        let destination = self
2518            .current_principal(state, destination)
2519            .ok_or_else(|| IcpEnsurePlatformError::UnresolvedCreated(destination.clone()))?;
2520        Ok(self.status_optional(destination)?.map(|live| live.cycles))
2521    }
2522
2523    fn action_canister_version(
2524        &mut self,
2525        action: &EnsureAction,
2526        state: &FleetEnsureStateRecord,
2527    ) -> Result<Option<u64>, Self::Error> {
2528        let EnsureAction::Install { principal, .. } = action else {
2529            return Ok(None);
2530        };
2531        let principal = Self::action_principal(state, principal)?;
2532        let version = self
2533            .install_status_optional(principal)?
2534            .and_then(|live| live.canister_version)
2535            .ok_or_else(|| IcpEnsurePlatformError::IncompleteCanisterStatus {
2536                canister: principal.to_string(),
2537                field: "canister_version",
2538            })?;
2539        Ok(Some(version))
2540    }
2541}
2542
2543fn exact_install_canister_status(
2544    icp: &IcpCli,
2545    canister: &str,
2546    projected_canister_version: Option<u64>,
2547    projected_module_sha256: Option<String>,
2548) -> Result<ExactInstallCanisterStatus, IcpEnsurePlatformError> {
2549    exact_install_canister_status_with(
2550        canister,
2551        projected_canister_version,
2552        projected_module_sha256,
2553        |canister_id| {
2554            icp.management_canister_status_candid::<_, ManagementCanisterStatusResponse>(
2555                canister_id,
2556                &ManagementCanisterStatusRequest { canister_id },
2557            )
2558        },
2559    )
2560}
2561
2562fn exact_install_canister_status_with(
2563    canister: &str,
2564    projected_canister_version: Option<u64>,
2565    projected_module_sha256: Option<String>,
2566    management_status: impl FnOnce(
2567        Principal,
2568    )
2569        -> Result<ManagementCanisterStatusResponse, IcpManagementCallError>,
2570) -> Result<ExactInstallCanisterStatus, IcpEnsurePlatformError> {
2571    if let Some(canister_version) = projected_canister_version {
2572        return Ok(ExactInstallCanisterStatus {
2573            canister_version,
2574            module_sha256: projected_module_sha256,
2575        });
2576    }
2577    let canister_id = parse_principal("install target", canister)?;
2578    let response = management_status(canister_id).map_err(|source| {
2579        IcpEnsurePlatformError::InstallVersionProofUnavailable {
2580            canister: canister.to_string(),
2581            source: Box::new(source),
2582        }
2583    })?;
2584    Ok(ExactInstallCanisterStatus {
2585        canister_version: response.version,
2586        module_sha256: response
2587            .module_hash
2588            .map(|hash| canic_core::cdk::utils::hash::hex_bytes(&hash)),
2589    })
2590}
2591
2592fn exact_retained_root_owned_topology<'a>(
2593    state: &'a FleetEnsureStateRecord,
2594    name: &str,
2595    kind: DesiredCanisterKind,
2596    parent: &str,
2597    principal: &str,
2598    root: &str,
2599) -> Option<&'a crate::fleet_ensure::model::FleetEnsureTopologyRecord> {
2600    let child_identity_matches = retained_state_principal_matches(state, name, principal);
2601    let root_identity_matches = retained_state_principal_matches(state, parent, root);
2602    let topology = state
2603        .topology
2604        .get(name)
2605        .filter(|topology| topology.kind == kind && topology.parent.as_deref() == Some(parent));
2606    (child_identity_matches && root_identity_matches)
2607        .then_some(topology)
2608        .flatten()
2609}
2610
2611fn retained_state_principal_matches(
2612    state: &FleetEnsureStateRecord,
2613    name: &str,
2614    principal: &str,
2615) -> bool {
2616    let pending = state.pending_principals.get(name).map(String::as_str);
2617    let terminal = state.principals.get(name).map(String::as_str);
2618    let retained = pending.is_some() || terminal.is_some();
2619    retained
2620        && pending.is_none_or(|retained| retained == principal)
2621        && terminal.is_none_or(|retained| retained == principal)
2622}
2623
2624fn completed_reinstall_continuity(
2625    pre_canister_version: u64,
2626    principal_matches: bool,
2627    topology_matches: bool,
2628    live_module_sha256: Option<&str>,
2629    desired_module_sha256: &str,
2630    live_canister_version: Option<u64>,
2631) -> Result<(), &'static str> {
2632    if !principal_matches {
2633        return Err("Principal");
2634    }
2635    if !topology_matches {
2636        return Err("Root/parent/kind topology");
2637    }
2638    if live_module_sha256 != Some(desired_module_sha256) {
2639        return Err("module SHA-256");
2640    }
2641    if live_canister_version.is_some_and(|version| version <= pre_canister_version) {
2642        return Err("canister version");
2643    }
2644    Ok(())
2645}
2646
2647fn recoverable_root_status_error(error: &CanisterProtocolError) -> bool {
2648    error.is_rejected_with(canic_core::diagnostics::codes::STATE_CONFLICT)
2649        || error.is_rejected_with(canic_core::diagnostics::codes::STATE_UNAVAILABLE)
2650}
2651
2652fn recoverable_current_protocol_error(error: &current_protocol::CurrentProtocolError) -> bool {
2653    matches!(
2654        error,
2655        current_protocol::CurrentProtocolError::Transport(source)
2656            if recoverable_root_status_error(source)
2657    )
2658}
2659
2660fn predecessor_store_control_rejection(error: &current_protocol::CurrentProtocolError) -> bool {
2661    matches!(
2662        error,
2663        current_protocol::CurrentProtocolError::Transport(source)
2664            if source.is_rejected_with(canic_core::diagnostics::codes::STATE_CONFLICT)
2665    )
2666}
2667
2668pub fn install_effect_applied(
2669    mode: InstallMode,
2670    expected_hash: &str,
2671    live_hash: Option<&str>,
2672    pre_canister_version: Option<u64>,
2673    live_canister_version: Option<u64>,
2674) -> bool {
2675    if live_hash != Some(expected_hash) {
2676        return false;
2677    }
2678    match mode {
2679        InstallMode::Install => true,
2680        InstallMode::Reinstall => pre_canister_version
2681            .zip(live_canister_version)
2682            .is_some_and(|(before, after)| after > before),
2683    }
2684}
2685
2686/// Exact retained and live evidence for one Cycles Ledger withdrawal.
2687#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2688pub struct NativeFundingObservation {
2689    pub amount: u128,
2690    pub expected_post_cycles: u128,
2691    pub funding_deficit_cycles: u128,
2692    pub funding_margin_cycles: u128,
2693    pub live_cycles: Option<u128>,
2694    pub pre_cycles: Option<u128>,
2695}
2696
2697pub const fn native_funding_applied(observation: NativeFundingObservation) -> bool {
2698    let Some(pre_cycles) = observation.pre_cycles else {
2699        return false;
2700    };
2701    let Some(live_cycles) = observation.live_cycles else {
2702        return false;
2703    };
2704    let Some(expected_from_amount) = pre_cycles.checked_add(observation.amount) else {
2705        return false;
2706    };
2707    let Some(minimum_live_cycles) = observation
2708        .expected_post_cycles
2709        .checked_sub(observation.funding_margin_cycles)
2710    else {
2711        return false;
2712    };
2713    let Some(minimum_from_deficit) = pre_cycles.checked_add(observation.funding_deficit_cycles)
2714    else {
2715        return false;
2716    };
2717    observation.funding_deficit_cycles > 0
2718        && expected_from_amount == observation.expected_post_cycles
2719        && minimum_live_cycles == minimum_from_deficit
2720        && minimum_live_cycles > pre_cycles
2721        && live_cycles >= minimum_live_cycles
2722}
2723
2724fn validate_root_funding_inspection(
2725    canister: &str,
2726    kind: DesiredCanisterKind,
2727    root: &str,
2728    controllers: &[Principal],
2729    module_hash: Option<&[u8]>,
2730    cycles: &Nat,
2731) -> Result<u128, IcpEnsurePlatformError> {
2732    if controllers.len() != 1 || controllers[0].to_text() != root {
2733        return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2734            canister: canister.to_string(),
2735            field: "Root-only controllers",
2736        });
2737    }
2738    if kind == DesiredCanisterKind::Pool && module_hash.is_some() {
2739        return Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict {
2740            canister: canister.to_string(),
2741            field: "module-free pool asset",
2742        });
2743    }
2744    u128::try_from(cycles.0.clone()).map_err(|_| IcpEnsurePlatformError::InvalidStatusCycles {
2745        canister: canister.to_string(),
2746        value: cycles.to_string(),
2747    })
2748}
2749
2750fn created_canister_outcome(
2751    canister_id: Principal,
2752    receipt: String,
2753    requested_initial_cycles: u128,
2754) -> EffectOutcome {
2755    EffectOutcome {
2756        created_principal: Some(canister_id.to_text()),
2757        post_cycles: Some(requested_initial_cycles),
2758        receipt: Some(receipt),
2759    }
2760}
2761
2762fn create_canister_response_outcome(
2763    response: Result<CreateCanisterSuccess, CreateCanisterError>,
2764    requested_initial_cycles: u128,
2765) -> Result<EffectOutcome, IcpEnsurePlatformError> {
2766    match response {
2767        Ok(success) => Ok(created_canister_outcome(
2768            success.canister_id,
2769            success.block_id.to_string(),
2770            requested_initial_cycles,
2771        )),
2772        Err(CreateCanisterError::Duplicate {
2773            duplicate_of,
2774            canister_id: Some(canister_id),
2775        }) => Ok(created_canister_outcome(
2776            canister_id,
2777            duplicate_of.to_string(),
2778            requested_initial_cycles,
2779        )),
2780        Err(CreateCanisterError::Duplicate {
2781            canister_id: None, ..
2782        }) => Err(IcpEnsurePlatformError::LedgerCreatePending),
2783        Err(error) => Err(IcpEnsurePlatformError::LedgerCreate(render_create_error(
2784            error,
2785        ))),
2786    }
2787}
2788
2789fn ledger_fee_cycles(value: Nat) -> Result<u128, IcpEnsurePlatformError> {
2790    let rendered = value.to_string();
2791    u128::try_from(value.0).map_err(|_| IcpEnsurePlatformError::InvalidLedgerFee(rendered))
2792}
2793
2794const fn empty_outcome() -> EffectOutcome {
2795    EffectOutcome {
2796        created_principal: None,
2797        post_cycles: None,
2798        receipt: None,
2799    }
2800}
2801
2802fn parse_status_cycles(value: &str) -> Option<u128> {
2803    value.replace('_', "").trim().parse().ok()
2804}
2805
2806fn normalize_hash(value: &str) -> String {
2807    value
2808        .strip_prefix("0x")
2809        .unwrap_or(value)
2810        .to_ascii_lowercase()
2811}
2812
2813fn parse_principal(field: &'static str, value: &str) -> Result<Principal, IcpEnsurePlatformError> {
2814    Principal::from_text(value).map_err(|_| IcpEnsurePlatformError::InvalidPrincipal {
2815        field,
2816        value: value.to_string(),
2817    })
2818}
2819
2820fn reject_public_status_projection(
2821    report: &IcpCanisterStatusReport,
2822    principal: &str,
2823) -> Result<(), IcpEnsurePlatformError> {
2824    if report.status.is_none()
2825        && report.settings.is_none()
2826        && report.cycles.is_none()
2827        && report.public_controllers.is_some()
2828    {
2829        return Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
2830            canister: principal.to_string(),
2831        });
2832    }
2833    Ok(())
2834}
2835
2836fn resolve_path(root: &Path, path: &str) -> PathBuf {
2837    let path = Path::new(path);
2838    if path.is_absolute() {
2839        path.to_path_buf()
2840    } else {
2841        root.join(path)
2842    }
2843}
2844
2845fn verify_artifact(
2846    path: &Path,
2847    expected: &str,
2848    kind: &'static str,
2849) -> Result<(), IcpEnsurePlatformError> {
2850    let bytes = std::fs::read(path)
2851        .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path.to_path_buf()))?;
2852    let actual = canic_core::cdk::utils::hash::sha256_hex(&bytes);
2853    if actual != expected {
2854        return Err(IcpEnsurePlatformError::ArtifactDigestMismatch {
2855            actual,
2856            expected: expected.to_string(),
2857            kind,
2858        });
2859    }
2860    Ok(())
2861}
2862
2863fn artifact_hash(path: &Path) -> Result<String, IcpEnsurePlatformError> {
2864    let bytes = std::fs::read(path)
2865        .map_err(|_| IcpEnsurePlatformError::ArtifactUnavailable(path.to_path_buf()))?;
2866    Ok(canic_core::cdk::utils::hash::sha256_hex(&bytes))
2867}
2868
2869fn resolved_protocol_action(
2870    action: &EnsureAction,
2871    state: &FleetEnsureStateRecord,
2872) -> Result<EnsureAction, IcpEnsurePlatformError> {
2873    let mut resolved = action.clone();
2874    if let EnsureAction::Protocol { principal, .. } = &mut resolved {
2875        *principal = IcpEnsurePlatform::action_principal(state, principal)?.to_string();
2876        return Ok(resolved);
2877    }
2878    Err(IcpEnsurePlatformError::Protocol(
2879        protocol::ProtocolEffectError::WrongAction,
2880    ))
2881}
2882
2883fn render_create_error(error: CreateCanisterError) -> String {
2884    match error {
2885        CreateCanisterError::CreatedInFuture { ledger_time } => {
2886            format!("created in future of ledger time {ledger_time}")
2887        }
2888        CreateCanisterError::Duplicate { duplicate_of, .. } => {
2889            format!("duplicate block {duplicate_of}")
2890        }
2891        CreateCanisterError::FailedToCreate {
2892            error,
2893            fee_block,
2894            refund_block,
2895        } => format!("failed: {error}; fee_block={fee_block:?}; refund_block={refund_block:?}"),
2896        CreateCanisterError::GenericError {
2897            error_code,
2898            message,
2899        } => format!("error {error_code}: {message}"),
2900        CreateCanisterError::InsufficientFunds { balance } => {
2901            format!("insufficient funds: balance={balance}")
2902        }
2903        CreateCanisterError::TemporarilyUnavailable => "temporarily unavailable".to_string(),
2904        CreateCanisterError::TooOld => "request is too old".to_string(),
2905    }
2906}
2907
2908fn render_withdraw_error(error: WithdrawError) -> String {
2909    match error {
2910        WithdrawError::BadFee { expected_fee } => format!("bad fee; expected {expected_fee}"),
2911        WithdrawError::CreatedInFuture { ledger_time } => {
2912            format!("created in future of ledger time {ledger_time}")
2913        }
2914        WithdrawError::Duplicate { duplicate_of } => format!("duplicate block {duplicate_of}"),
2915        WithdrawError::FailedToWithdraw {
2916            fee_block,
2917            rejection_reason,
2918            ..
2919        } => format!("withdraw failed: {rejection_reason}; fee_block={fee_block:?}"),
2920        WithdrawError::GenericError {
2921            error_code,
2922            message,
2923        } => format!("error {error_code}: {message}"),
2924        WithdrawError::InsufficientFunds { balance } => {
2925            format!("insufficient funds: balance={balance}")
2926        }
2927        WithdrawError::InvalidReceiver { receiver } => format!("invalid receiver {receiver}"),
2928        WithdrawError::TemporarilyUnavailable => "temporarily unavailable".to_string(),
2929        WithdrawError::TooOld => "request is too old".to_string(),
2930    }
2931}
2932
2933#[expect(
2934    dead_code,
2935    reason = "Candid variant is retained for exact response decoding"
2936)]
2937const fn rejection_code_name(code: RejectionCode) -> &'static str {
2938    match code {
2939        RejectionCode::CanisterError => "canister_error",
2940        RejectionCode::CanisterReject => "canister_reject",
2941        RejectionCode::DestinationInvalid => "destination_invalid",
2942        RejectionCode::NoError => "no_error",
2943        RejectionCode::SysFatal => "sys_fatal",
2944        RejectionCode::SysTransient => "sys_transient",
2945        RejectionCode::Unknown => "unknown",
2946    }
2947}
2948
2949#[cfg(test)]
2950mod tests {
2951    use super::*;
2952    use canic_core::dto::pool::CanisterPoolAssetStatus;
2953
2954    #[cfg(unix)]
2955    #[test]
2956    fn icp_1_3_public_non_controller_status_is_typed_unavailable_evidence() {
2957        use std::{fs, os::unix::fs::PermissionsExt};
2958
2959        let canister = "rrkah-fqaaa-aaaaa-aaaaq-cai";
2960        let root = crate::test_support::temp_dir("canic-public-status-projection");
2961        fs::create_dir_all(&root).expect("create public status fixture");
2962        let executable = root.join("icp");
2963        let commands = root.join("commands.log");
2964        let script = format!(
2965            "#!/bin/sh\n\
2966             if [ \"$1\" = \"--version\" ]; then echo 'icp 1.3.0'; exit 0; fi\n\
2967             printf '%s\\n' \"$*\" >> '{}'\n\
2968             printf '%s\\n' '{{\"id\":\"{canister}\",\"controllers\":[\"rdmx6-jaaaa-aaaaa-aaadq-cai\"],\"module_hash\":null}}'\n",
2969            commands.display(),
2970        );
2971        fs::write(&executable, script).expect("write fake ICP 1.3.0");
2972        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755))
2973            .expect("make fake ICP executable");
2974        let desired = DesiredFleet {
2975            bootstrap: None,
2976            canisters: Vec::new(),
2977            cycles_ledger: "um5iw-rqaaa-aaaaq-qaaba-cai".to_string(),
2978            environment: "local".to_string(),
2979            fleet: "public-status".to_string(),
2980            ledger_fee_cycles: "100M".to_string(),
2981            management_creation_fee_cycles: "500B".to_string(),
2982            material_cycle_threshold: "1B".to_string(),
2983            maximum_observation_burn_cycles: "1B".to_string(),
2984            maximum_stalled_observations: 4,
2985            maximum_update_burn_cycles: "1B".to_string(),
2986            operator: "rdmx6-jaaaa-aaaaa-aaadq-cai".to_string(),
2987            protocol: None,
2988            protocol_steps: Vec::new(),
2989            schema_version: 1,
2990            treasury: "treasury".to_string(),
2991        };
2992        let platform =
2993            IcpEnsurePlatform::new(desired, executable.to_string_lossy().as_ref(), &root);
2994
2995        assert!(matches!(
2996            platform.status_optional(canister),
2997            Err(IcpEnsurePlatformError::PublicCanisterStatusUnavailable {
2998                canister: unavailable,
2999            }) if unavailable == canister
3000        ));
3001        assert!(
3002            fs::read_to_string(commands)
3003                .expect("read fake ICP commands")
3004                .contains("canister status rrkah-fqaaa-aaaaa-aaaaq-cai --json")
3005        );
3006    }
3007
3008    #[test]
3009    fn retained_predecessor_modules_are_observed_and_optionally_cross_checked() {
3010        let root_controllers = vec!["root-controller".to_string()];
3011        let store_controllers = vec!["root-controller".to_string(), "operator".to_string()];
3012        let observed = RetainedStoreControlLiveBinding {
3013            root_controllers: root_controllers.clone(),
3014            root_module_sha256: Some("root-predecessor".to_string()),
3015            store_controllers: store_controllers.clone(),
3016            store_module_sha256: Some("store-predecessor".to_string()),
3017        };
3018
3019        assert!(retained_store_control_live_is_exact(
3020            &observed,
3021            &root_controllers,
3022            &store_controllers,
3023            None,
3024            None,
3025            "root-successor",
3026            "store-successor",
3027        ));
3028        assert!(retained_store_control_live_is_exact(
3029            &observed,
3030            &root_controllers,
3031            &store_controllers,
3032            Some("root-predecessor"),
3033            Some("store-predecessor"),
3034            "root-successor",
3035            "store-successor",
3036        ));
3037        assert!(!retained_store_control_live_is_exact(
3038            &observed,
3039            &root_controllers,
3040            &store_controllers,
3041            Some("wrong-root-predecessor"),
3042            Some("store-predecessor"),
3043            "root-successor",
3044            "store-successor",
3045        ));
3046
3047        let missing_live_module = RetainedStoreControlLiveBinding {
3048            root_controllers: root_controllers.clone(),
3049            root_module_sha256: None,
3050            store_controllers: store_controllers.clone(),
3051            store_module_sha256: Some("store-predecessor".to_string()),
3052        };
3053        assert!(!retained_store_control_live_is_exact(
3054            &missing_live_module,
3055            &root_controllers,
3056            &store_controllers,
3057            None,
3058            None,
3059            "root-successor",
3060            "store-successor",
3061        ));
3062
3063        let mut successor_is_already_live = observed;
3064        successor_is_already_live.store_module_sha256 = Some("store-successor".to_string());
3065        assert!(!retained_store_control_live_is_exact(
3066            &successor_is_already_live,
3067            &root_controllers,
3068            &store_controllers,
3069            None,
3070            None,
3071            "root-successor",
3072            "store-successor",
3073        ));
3074    }
3075
3076    #[cfg(unix)]
3077    #[test]
3078    fn versionless_icp_status_uses_exact_typed_management_version() {
3079        #[derive(CandidType)]
3080        struct CanonicalManagementCanisterStatusFixture {
3081            version: u64,
3082            module_hash: Option<Vec<u8>>,
3083        }
3084
3085        use std::{fs, os::unix::fs::PermissionsExt};
3086
3087        let root = crate::test_support::temp_dir("canic-install-version-fallback");
3088        fs::create_dir_all(&root).expect("create version fallback fixture");
3089        let executable = root.join("icp");
3090        let commands = root.join("commands.log");
3091        let canister = "rrkah-fqaaa-aaaaa-aaaaq-cai";
3092        let status_json = format!(
3093            r#"{{"id":"{canister}","name":"coordinator","status":"Running","settings":{{"controllers":["rdmx6-jaaaa-aaaaa-aaadq-cai"]}},"module_hash":"0x{}","memory_size":"1","cycles":"1000000000000","query_stats":{{}}}}"#,
3094            "11".repeat(32),
3095        );
3096        let response_bytes = candid::encode_one(CanonicalManagementCanisterStatusFixture {
3097            version: 42,
3098            module_hash: Some(vec![0x22; 32]),
3099        })
3100        .expect("encode independently modelled management status");
3101        let script = format!(
3102            "#!/bin/sh\n\
3103             if [ \"$1\" = \"--version\" ]; then printf '%s\\n' 'icp 1.3.0'; exit 0; fi\n\
3104             printf '%s\\n' \"$*\" >> '{}'\n\
3105             case \"$*\" in\n\
3106               *\"canister status {canister}\"*) printf '%s\\n' '{}' ;;\n\
3107               *) printf '%s\\n' 'unexpected fake ICP command' >&2; exit 23 ;;\n\
3108             esac\n",
3109            commands.display(),
3110            status_json,
3111        );
3112        fs::write(&executable, script).expect("write fake ICP 1.3.0");
3113        fs::set_permissions(&executable, fs::Permissions::from_mode(0o755))
3114            .expect("make fake ICP executable");
3115        let icp = IcpCli::new(executable.to_string_lossy(), Some("ic".to_string()));
3116
3117        let projected = icp
3118            .canister_status_report(canister)
3119            .expect("read ICP 1.3.0 status projection");
3120        assert_eq!(projected.canister_version, None);
3121        let exact = exact_install_canister_status_with(
3122            canister,
3123            projected.canister_version,
3124            projected.module_hash.map(|hash| normalize_hash(&hash)),
3125            |effective_canister_id| {
3126                assert_eq!(
3127                    effective_canister_id,
3128                    canister.parse().expect("target Principal")
3129                );
3130                candid::decode_one(&response_bytes).map_err(IcpManagementCallError::CandidResponse)
3131            },
3132        )
3133        .expect("obtain exact typed management status");
3134        assert_eq!(exact.canister_version, 42);
3135        assert_eq!(exact.module_sha256, Some("22".repeat(32)));
3136        let commands = fs::read_to_string(&commands).expect("read fake ICP commands");
3137        assert!(commands.contains("canister status rrkah-fqaaa-aaaaa-aaaaq-cai"));
3138        assert!(!commands.contains("canister call aaaaa-aa canister_status"));
3139
3140        fs::write(
3141            &executable,
3142            "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'icp 1.3.0'; exit 0; fi\necho '{\"not_response_bytes\":true}'\n",
3143        )
3144        .expect("replace unavailable management fixture");
3145        let error = exact_install_canister_status_with(canister, None, None, |_| {
3146            Err(IcpManagementCallError::MissingEnvironment)
3147        })
3148        .expect_err("missing projected and typed version must fail closed");
3149        assert!(matches!(
3150            error,
3151            IcpEnsurePlatformError::InstallVersionProofUnavailable { .. }
3152        ));
3153        assert!(error.to_string().contains("no install was issued"));
3154        assert!(error.to_string().contains("resume the same reviewed plan"));
3155
3156        fs::remove_dir_all(root).expect("remove version fallback fixture");
3157    }
3158
3159    #[test]
3160    fn root_owned_observation_classifies_bootstrap_and_workload_lifecycle() {
3161        assert_eq!(
3162            root_owned_lifecycle(DesiredCanisterKind::Store, &CanisterPoolAssetStatus::Store),
3163            Some(RootOwnedCanisterLifecycle::Store)
3164        );
3165        assert_eq!(
3166            root_owned_lifecycle(DesiredCanisterKind::Pool, &CanisterPoolAssetStatus::Ready),
3167            Some(RootOwnedCanisterLifecycle::Idle)
3168        );
3169        assert_eq!(
3170            root_owned_lifecycle(
3171                DesiredCanisterKind::Pool,
3172                &CanisterPoolAssetStatus::Claimed {
3173                    claim: canic_core::dto::pool::CanisterPoolClaim {
3174                        component: canic_core::ids::ComponentInstanceId::from_generated_bytes(
3175                            [1; 32]
3176                        ),
3177                        operation_id: [2; 32],
3178                    },
3179                }
3180            ),
3181            Some(RootOwnedCanisterLifecycle::Claimed)
3182        );
3183        assert_eq!(
3184            root_owned_lifecycle(
3185                DesiredCanisterKind::Pool,
3186                &CanisterPoolAssetStatus::Workload {
3187                    claim: canic_core::dto::pool::CanisterPoolClaim {
3188                        component: canic_core::ids::ComponentInstanceId::from_generated_bytes(
3189                            [1; 32]
3190                        ),
3191                        operation_id: [2; 32],
3192                    },
3193                }
3194            ),
3195            Some(RootOwnedCanisterLifecycle::Workload)
3196        );
3197        assert_eq!(
3198            root_owned_lifecycle(
3199                DesiredCanisterKind::Pool,
3200                &CanisterPoolAssetStatus::HandingOff {
3201                    recipient: Principal::anonymous(),
3202                }
3203            ),
3204            None
3205        );
3206        assert_eq!(
3207            root_owned_lifecycle(
3208                DesiredCanisterKind::Store,
3209                &CanisterPoolAssetStatus::PendingReset
3210            ),
3211            None
3212        );
3213    }
3214
3215    #[test]
3216    #[expect(
3217        clippy::too_many_lines,
3218        reason = "the binding regression mutates each authority field independently"
3219    )]
3220    fn retained_root_owned_topology_requires_exact_child_root_and_parent_binding() {
3221        let mut state = FleetEnsureStateRecord {
3222            active_registry: None,
3223            completed_reinstall_action_sha256: BTreeMap::new(),
3224            completed_reinstall_operation_id: None,
3225            completed_reinstalls: BTreeMap::new(),
3226            fleet: "fleet".to_string(),
3227            pending_principals: BTreeMap::new(),
3228            principals: BTreeMap::from([
3229                ("root".to_string(), "root-principal".to_string()),
3230                ("store".to_string(), "store-principal".to_string()),
3231            ]),
3232            retained_cycles_by_principal: BTreeMap::new(),
3233            schema_version: crate::fleet_ensure::model::FLEET_ENSURE_SCHEMA_VERSION,
3234            topology: BTreeMap::from([(
3235                "store".to_string(),
3236                crate::fleet_ensure::model::FleetEnsureTopologyRecord {
3237                    kind: DesiredCanisterKind::Store,
3238                    module_hash: Some("11".repeat(32)),
3239                    parent: Some("root".to_string()),
3240                    protocol_binding: None,
3241                    role: None,
3242                },
3243            )]),
3244        };
3245
3246        assert!(
3247            exact_retained_root_owned_topology(
3248                &state,
3249                "store",
3250                DesiredCanisterKind::Store,
3251                "root",
3252                "store-principal",
3253                "root-principal",
3254            )
3255            .is_some()
3256        );
3257        assert!(
3258            exact_retained_root_owned_topology(
3259                &state,
3260                "store",
3261                DesiredCanisterKind::Store,
3262                "root",
3263                "foreign-store",
3264                "root-principal",
3265            )
3266            .is_none()
3267        );
3268        state
3269            .principals
3270            .insert("store".to_string(), "store-principal".to_string());
3271        state
3272            .principals
3273            .insert("root".to_string(), "foreign-root".to_string());
3274        assert!(
3275            exact_retained_root_owned_topology(
3276                &state,
3277                "store",
3278                DesiredCanisterKind::Store,
3279                "root",
3280                "store-principal",
3281                "root-principal",
3282            )
3283            .is_none()
3284        );
3285        state
3286            .principals
3287            .insert("root".to_string(), "root-principal".to_string());
3288        state
3289            .topology
3290            .get_mut("store")
3291            .expect("Store topology")
3292            .kind = DesiredCanisterKind::Pool;
3293        assert!(
3294            exact_retained_root_owned_topology(
3295                &state,
3296                "store",
3297                DesiredCanisterKind::Store,
3298                "root",
3299                "store-principal",
3300                "root-principal",
3301            )
3302            .is_none()
3303        );
3304        state
3305            .topology
3306            .get_mut("store")
3307            .expect("Store topology")
3308            .kind = DesiredCanisterKind::Store;
3309        state
3310            .topology
3311            .get_mut("store")
3312            .expect("Store topology")
3313            .parent = Some("foreign-root".to_string());
3314        assert!(
3315            exact_retained_root_owned_topology(
3316                &state,
3317                "store",
3318                DesiredCanisterKind::Store,
3319                "root",
3320                "store-principal",
3321                "root-principal",
3322            )
3323            .is_none()
3324        );
3325        state
3326            .topology
3327            .get_mut("store")
3328            .expect("Store topology")
3329            .parent = Some("root".to_string());
3330        state.principals.clear();
3331        state.pending_principals = BTreeMap::from([
3332            ("root".to_string(), "root-principal".to_string()),
3333            ("store".to_string(), "store-principal".to_string()),
3334        ]);
3335        assert!(
3336            exact_retained_root_owned_topology(
3337                &state,
3338                "store",
3339                DesiredCanisterKind::Store,
3340                "root",
3341                "store-principal",
3342                "root-principal",
3343            )
3344            .is_some()
3345        );
3346        state
3347            .principals
3348            .insert("store".to_string(), "foreign-store".to_string());
3349        assert!(
3350            exact_retained_root_owned_topology(
3351                &state,
3352                "store",
3353                DesiredCanisterKind::Store,
3354                "root",
3355                "store-principal",
3356                "root-principal",
3357            )
3358            .is_none()
3359        );
3360    }
3361
3362    #[test]
3363    fn completed_reinstall_requires_exact_continuity_when_ordinary_status_has_no_version() {
3364        let desired = "11".repeat(32);
3365        assert_eq!(
3366            completed_reinstall_continuity(7, true, true, Some(&desired), &desired, None,),
3367            Ok(())
3368        );
3369        assert_eq!(
3370            completed_reinstall_continuity(7, true, true, Some(&desired), &desired, Some(8),),
3371            Ok(())
3372        );
3373        for (rejected, field) in [
3374            (
3375                completed_reinstall_continuity(7, false, true, Some(&desired), &desired, None),
3376                "Principal",
3377            ),
3378            (
3379                completed_reinstall_continuity(7, true, false, Some(&desired), &desired, None),
3380                "Root/parent/kind topology",
3381            ),
3382            (
3383                completed_reinstall_continuity(
3384                    7,
3385                    true,
3386                    true,
3387                    Some(&"22".repeat(32)),
3388                    &desired,
3389                    None,
3390                ),
3391                "module SHA-256",
3392            ),
3393            (
3394                completed_reinstall_continuity(7, true, true, Some(&desired), &desired, Some(7)),
3395                "canister version",
3396            ),
3397        ] {
3398            assert_eq!(rejected, Err(field));
3399        }
3400    }
3401
3402    #[test]
3403    fn toko_fresh_fleet_create_responses_retain_the_exact_requested_balance() {
3404        let canister = Principal::from_slice(&[9; 29]);
3405        let canister_text = canister.to_text();
3406        for (response, receipt) in [
3407            (
3408                Ok(CreateCanisterSuccess {
3409                    block_id: Nat::from(41_u8),
3410                    canister_id: canister,
3411                }),
3412                "41",
3413            ),
3414            (
3415                Err(CreateCanisterError::Duplicate {
3416                    duplicate_of: Nat::from(41_u8),
3417                    canister_id: Some(canister),
3418                }),
3419                "41",
3420            ),
3421        ] {
3422            let outcome = create_canister_response_outcome(response, 5_000)
3423                .expect("successful or duplicate-with-Principal Create response");
3424            assert_eq!(
3425                outcome.created_principal.as_deref(),
3426                Some(canister_text.as_str())
3427            );
3428            assert_eq!(outcome.post_cycles, Some(5_000));
3429            assert_eq!(outcome.receipt.as_deref(), Some(receipt));
3430        }
3431    }
3432
3433    #[test]
3434    fn root_funding_inspection_requires_exact_controller_and_pool_shape() {
3435        let root = Principal::from_slice(&[7; 29]);
3436        let root_text = root.to_text();
3437        let cycles = Nat::from(2_898_749_313_788_u128);
3438        assert_eq!(
3439            validate_root_funding_inspection(
3440                "pool-0",
3441                DesiredCanisterKind::Pool,
3442                &root_text,
3443                &[root],
3444                None,
3445                &cycles,
3446            )
3447            .expect("exact Root-authorized pool inspection"),
3448            2_898_749_313_788,
3449        );
3450
3451        let foreign = Principal::from_slice(&[8; 29]);
3452        for rejected in [
3453            validate_root_funding_inspection(
3454                "pool-0",
3455                DesiredCanisterKind::Pool,
3456                &root_text,
3457                &[foreign],
3458                None,
3459                &cycles,
3460            ),
3461            validate_root_funding_inspection(
3462                "pool-0",
3463                DesiredCanisterKind::Pool,
3464                &root_text,
3465                &[root],
3466                Some(&[1]),
3467                &cycles,
3468            ),
3469        ] {
3470            assert!(matches!(
3471                rejected,
3472                Err(IcpEnsurePlatformError::FundingInspectionAuthorityConflict { .. })
3473            ));
3474        }
3475    }
3476}