Skip to main content

canic_core/api/runtime/
mod.rs

1pub mod install;
2pub mod root_funding;
3#[cfg(feature = "internal-test-fixtures")]
4pub mod topup_fixture;
5
6use crate::{
7    InternalError,
8    cdk::types::Principal,
9    domain::runtime::{
10        FailureSeverity, HealthStatus, ReadinessStatus, RuntimeCheckStatus,
11        RuntimeDiagnosticSeverity, RuntimeFieldVisibility, RuntimeStateDomainStatus, RuntimeStatus,
12    },
13    dto::{
14        error::Error,
15        runtime::{
16            CanicHealthStatus, CanicReadinessStatus, CanicRuntimeStatus, CanisterTimerStatus,
17            RUNTIME_INTROSPECTION_SCHEMA_VERSION, RuntimeAuthStatusSummary,
18            RuntimeBlobStorageStatusSummary, RuntimeBuildInfo, RuntimeCheck, RuntimeDiagnostic,
19            RuntimeFeatureStatus, RuntimeReceiptCapacityStatus, RuntimeStateDomainSummary,
20            RuntimeStateSummary, RuntimeTopologyStatus, RuntimeVisibilityEntry,
21            TimerCallbackPerformanceStatus, TimerMemoryPageExtentStatus,
22            TimerMemoryPageSampleStatus,
23        },
24    },
25    ops::{
26        ic::{IcOps, build_network::BuildNetworkOps},
27        runtime::{
28            env::EnvOps,
29            memory::MemoryRegistryOps,
30            ready::ReadyOps,
31            recent_failure::{RecentFailureInput, RecentFailureOps},
32        },
33        storage::intent::{RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD, ReceiptBackedIntentOps},
34    },
35    state_contract::{STATE_MANIFEST_SCHEMA_VERSION, canic_state_descriptors},
36    workflow::runtime::timer::TimerAuthorityWorkflow,
37};
38const RUNTIME_FEATURE_SOURCE: &str = "compile_feature";
39const RUNTIME_FEATURE_FLAGS: [(&str, bool); 11] = [
40    (
41        "auth-chain-key-ecdsa",
42        cfg!(feature = "auth-chain-key-ecdsa"),
43    ),
44    (
45        "auth-chain-key-root-sign",
46        cfg!(feature = "auth-chain-key-root-sign"),
47    ),
48    (
49        "auth-delegated-token-verify",
50        cfg!(feature = "auth-delegated-token-verify"),
51    ),
52    (
53        "auth-issuer-canister-sig-create",
54        cfg!(feature = "auth-issuer-canister-sig-create"),
55    ),
56    (
57        "auth-issuer-canister-sig-verify",
58        cfg!(feature = "auth-issuer-canister-sig-verify"),
59    ),
60    (
61        "auth-local-application-authorization",
62        cfg!(feature = "auth-local-application-authorization"),
63    ),
64    (
65        "auth-root-canister-sig-create",
66        cfg!(feature = "auth-root-canister-sig-create"),
67    ),
68    (
69        "auth-root-canister-sig-verify",
70        cfg!(feature = "auth-root-canister-sig-verify"),
71    ),
72    ("blob-storage", cfg!(feature = "blob-storage")),
73    (
74        "blob-storage-billing",
75        cfg!(feature = "blob-storage-billing"),
76    ),
77    ("sharding", cfg!(feature = "sharding")),
78];
79
80///
81/// MemoryRuntimeApi
82///
83
84pub struct MemoryRuntimeApi;
85
86impl MemoryRuntimeApi {
87    /// Bootstrap Canic's stable-memory declaration snapshot.
88    pub fn bootstrap_registry() -> Result<(), Error> {
89        MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
90
91        Ok(())
92    }
93}
94
95///
96/// RuntimeIntrospectionApi
97///
98
99pub struct RuntimeIntrospectionApi;
100
101impl RuntimeIntrospectionApi {
102    /// Return the minimal health status for a canister that answered the query.
103    #[must_use]
104    pub fn health(observed_at_ns: Option<u64>) -> CanicHealthStatus {
105        CanicHealthStatus {
106            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
107            status: HealthStatus::Healthy,
108            observed_at_ns,
109            checks: vec![RuntimeCheck {
110                category: "health".to_string(),
111                code: "canister_responsive".to_string(),
112                status: RuntimeCheckStatus::Pass,
113                subject: "canister".to_string(),
114                detail: "canister returned a health response".to_string(),
115                next: None,
116                source: "runtime_observed".to_string(),
117            }],
118        }
119    }
120
121    /// Return guarded readiness status for the local Canic role.
122    #[must_use]
123    pub fn readiness(observed_at_ns: u64) -> CanicReadinessStatus {
124        let ready = ReadyOps::is_ready();
125        let role = EnvOps::canister_role()
126            .ok()
127            .map(crate::ids::CanisterRole::into_string);
128
129        let (status, check_status, detail, next) = if ready {
130            (
131                ReadinessStatus::Ready,
132                RuntimeCheckStatus::Pass,
133                "runtime readiness barrier is marked ready",
134                None,
135            )
136        } else {
137            (
138                ReadinessStatus::NotReady,
139                RuntimeCheckStatus::Fail,
140                "runtime readiness barrier is not ready",
141                Some("wait for bootstrap to complete or inspect the role-owned Overview status"),
142            )
143        };
144
145        let readiness_check = RuntimeCheck {
146            category: "readiness".to_string(),
147            code: "runtime_ready_barrier".to_string(),
148            status: check_status,
149            subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
150            detail: detail.to_string(),
151            next: next.map(str::to_string),
152            source: "runtime_observed".to_string(),
153        };
154
155        let blockers = if ready {
156            Vec::new()
157        } else {
158            vec![RuntimeDiagnostic {
159                category: "readiness".to_string(),
160                code: "runtime_not_ready".to_string(),
161                severity: RuntimeDiagnosticSeverity::Blocked,
162                subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
163                detail: "runtime readiness barrier has not completed".to_string(),
164                next: Some(
165                    "inspect bootstrap status before treating the role as ready".to_string(),
166                ),
167                source: "runtime_observed".to_string(),
168            }]
169        };
170
171        let fixture = crate::workflow::fixture_provisioning::status();
172        let fixture_ready = crate::workflow::fixture_provisioning::is_ready(&fixture);
173        let mut checks = vec![readiness_check];
174        let mut blockers = blockers;
175        checks.push(RuntimeCheck {
176            category: "readiness".to_string(),
177            code: "fixture_receipt".to_string(),
178            status: if fixture_ready {
179                RuntimeCheckStatus::Pass
180            } else {
181                RuntimeCheckStatus::Fail
182            },
183            subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
184            detail: "application fixture receipt for the installed selection".to_string(),
185            next: None,
186            source: "application_durable_progress".to_string(),
187        });
188        if !fixture_ready {
189            blockers.push(RuntimeDiagnostic {
190                category: "readiness".to_string(),
191                code: "fixture_not_ready".to_string(),
192                severity: RuntimeDiagnosticSeverity::Blocked,
193                subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
194                detail: "required fixture has no validated completion receipt".to_string(),
195                next: Some(
196                    "inspect the protected fixture result for progress or failure".to_string(),
197                ),
198                source: "application_durable_progress".to_string(),
199            });
200        }
201        CanicReadinessStatus {
202            fixture,
203            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
204            role,
205            status: if fixture_ready {
206                status
207            } else {
208                ReadinessStatus::NotReady
209            },
210            observed_at_ns,
211            checks,
212            blockers,
213            warnings: Vec::new(),
214        }
215    }
216
217    /// Return guarded runtime status for the local Canic role.
218    #[must_use]
219    pub fn runtime_status_for(
220        canister_id: Principal,
221        observed_at_ns: u64,
222        package_name: &str,
223        package_version: &str,
224        canic_version: &str,
225        canister_version: u64,
226    ) -> CanicRuntimeStatus {
227        let readiness = Self::readiness(observed_at_ns);
228        let role = readiness.role.clone();
229        let state = state_summary(role.as_deref());
230        let root = EnvOps::root_pid().ok();
231        let parent = EnvOps::parent_pid().ok();
232        let subnet = EnvOps::subnet_pid().ok();
233        let receipt_capacity_result = runtime_receipt_capacity();
234        let receipt_capacity_status = receipt_capacity_result
235            .as_ref()
236            .ok()
237            .map(|capacity| capacity.status);
238        let timer_observation =
239            timer_status_observation(TimerAuthorityWorkflow::statuses(), observed_at_ns);
240        let status = aggregate_runtime_status(
241            readiness.status,
242            receipt_capacity_status,
243            timer_observation.check.status,
244        );
245        let (receipt_capacity, receipt_failure) = match receipt_capacity_result {
246            Ok(capacity) => (Some(capacity), None),
247            Err(err) => {
248                let code = err.code();
249                (
250                    None,
251                    Some(RecentFailureInput {
252                        occurred_at_ns: observed_at_ns,
253                        subsystem: "intent_capacity".to_string(),
254                        code: code.to_string(),
255                        severity: FailureSeverity::Error,
256                        summary: format!("diagnostic={code}"),
257                        correlation_id: None,
258                    }),
259                )
260            }
261        };
262        let recent_failures = RecentFailureOps::snapshot_with_many(
263            [timer_observation.failure, receipt_failure]
264                .into_iter()
265                .flatten(),
266        );
267
268        CanicRuntimeStatus {
269            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
270            observed_at_ns,
271            canister_id,
272            role,
273            root,
274            build_network: BuildNetworkOps::build_network(),
275            build: RuntimeBuildInfo {
276                package_name: package_name.to_string(),
277                package_version: package_version.to_string(),
278                canic_version: canic_version.to_string(),
279                canister_version,
280            },
281            features: runtime_features(),
282            topology: Some(RuntimeTopologyStatus {
283                root,
284                parent,
285                subnet,
286                source: "runtime_observed".to_string(),
287            }),
288            timers: timer_observation.timers,
289            timer_inventory: timer_observation.check,
290            state,
291            auth: Some(runtime_auth_status()),
292            blob_storage: runtime_blob_storage_status(),
293            receipt_capacity,
294            recent_failures,
295            visibility: runtime_visibility(),
296            readiness,
297            status,
298        }
299    }
300
301    /// Return guarded runtime status using ambient IC runtime values.
302    #[must_use]
303    pub fn runtime_status(
304        observed_at_ns: u64,
305        package_name: &str,
306        package_version: &str,
307        canic_version: &str,
308        canister_version: u64,
309    ) -> CanicRuntimeStatus {
310        Self::runtime_status_for(
311            IcOps::canister_self(),
312            observed_at_ns,
313            package_name,
314            package_version,
315            canic_version,
316            canister_version,
317        )
318    }
319}
320
321fn runtime_features() -> Vec<RuntimeFeatureStatus> {
322    RUNTIME_FEATURE_FLAGS
323        .into_iter()
324        .map(|(name, enabled)| runtime_feature_status(name, enabled))
325        .collect()
326}
327
328fn runtime_feature_status(name: &str, enabled: bool) -> RuntimeFeatureStatus {
329    RuntimeFeatureStatus {
330        name: name.to_string(),
331        enabled,
332        visibility: RuntimeFieldVisibility::OperatorOnly,
333        source: RUNTIME_FEATURE_SOURCE.to_string(),
334    }
335}
336
337fn runtime_auth_status() -> RuntimeAuthStatusSummary {
338    RuntimeAuthStatusSummary {
339        auth_features: RUNTIME_FEATURE_FLAGS
340            .into_iter()
341            .filter(|(name, _)| name.starts_with("auth-"))
342            .map(|(name, enabled)| runtime_feature_status(name, enabled))
343            .collect(),
344    }
345}
346
347fn runtime_receipt_capacity() -> Result<RuntimeReceiptCapacityStatus, InternalError> {
348    let capacity = ReceiptBackedIntentOps::receipt_capacity()?;
349    Ok(RuntimeReceiptCapacityStatus {
350        status: receipt_capacity_condition(
351            capacity.remaining_record_headroom,
352            capacity.remaining_resource_total_headroom,
353        ),
354        receipt_records: capacity.total_records,
355        application_receipt_records: capacity.application_records,
356        canic_owned_receipt_records: capacity.canic_owned_records,
357        pending_application_receipt_records: capacity.pending_records,
358        terminal_application_receipt_records: capacity.terminal_records,
359        receipt_record_limit: capacity.record_limit,
360        remaining_receipt_record_headroom: capacity.remaining_record_headroom,
361        resource_total_records: capacity.resource_total_records,
362        resource_total_record_limit: capacity.resource_total_record_limit,
363        remaining_resource_total_headroom: capacity.remaining_resource_total_headroom,
364        warning_headroom_threshold: RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD,
365        reserved_terminal_slots: capacity.reserved_terminal_slots,
366        reserved_terminal_pages: capacity.reserved_terminal_pages,
367        next_terminal_eligibility_at_ns: capacity.next_eligibility_at_ns,
368        source: "intent_storage".to_string(),
369    })
370}
371
372const fn receipt_capacity_condition(
373    remaining_receipt_records: u64,
374    remaining_resource_totals: u64,
375) -> RuntimeCheckStatus {
376    let minimum_headroom = if remaining_receipt_records < remaining_resource_totals {
377        remaining_receipt_records
378    } else {
379        remaining_resource_totals
380    };
381    if minimum_headroom == 0 {
382        RuntimeCheckStatus::Fail
383    } else if minimum_headroom <= RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD {
384        RuntimeCheckStatus::Warn
385    } else {
386        RuntimeCheckStatus::Pass
387    }
388}
389
390const fn aggregate_runtime_status(
391    readiness: ReadinessStatus,
392    receipt_capacity: Option<RuntimeCheckStatus>,
393    timer_inventory: RuntimeCheckStatus,
394) -> RuntimeStatus {
395    if matches!(readiness, ReadinessStatus::NotReady)
396        || matches!(
397            timer_inventory,
398            RuntimeCheckStatus::Fail | RuntimeCheckStatus::NotEvaluated
399        )
400        || matches!(
401            receipt_capacity,
402            None | Some(RuntimeCheckStatus::Fail | RuntimeCheckStatus::NotEvaluated)
403        )
404    {
405        RuntimeStatus::Failing
406    } else if matches!(
407        readiness,
408        ReadinessStatus::Degraded | ReadinessStatus::NotEvaluated
409    ) || matches!(receipt_capacity, Some(RuntimeCheckStatus::Warn))
410        || matches!(timer_inventory, RuntimeCheckStatus::Warn)
411    {
412        RuntimeStatus::Degraded
413    } else {
414        RuntimeStatus::Ok
415    }
416}
417
418fn runtime_blob_storage_status() -> Option<RuntimeBlobStorageStatusSummary> {
419    let blob_storage_enabled = cfg!(feature = "blob-storage");
420    let billing_enabled = cfg!(feature = "blob-storage-billing");
421
422    (blob_storage_enabled || billing_enabled).then(|| RuntimeBlobStorageStatusSummary {
423        blob_storage_features: [
424            ("blob-storage", blob_storage_enabled),
425            ("blob-storage-billing", billing_enabled),
426        ]
427        .into_iter()
428        .map(|(name, enabled)| runtime_feature_status(name, enabled))
429        .collect(),
430    })
431}
432
433struct TimerStatusObservation {
434    check: RuntimeCheck,
435    timers: Vec<CanisterTimerStatus>,
436    failure: Option<RecentFailureInput>,
437}
438
439fn timer_status_observation(
440    snapshots: Result<Vec<ic_timers::TimerSnapshot>, crate::api::timer::TimerError>,
441    observed_at_ns: u64,
442) -> TimerStatusObservation {
443    match snapshots {
444        Ok(snapshots) => TimerStatusObservation {
445            check: RuntimeCheck {
446                category: "runtime".to_string(),
447                code: "timer_inventory_available".to_string(),
448                status: RuntimeCheckStatus::Pass,
449                subject: "shared_timer_registry".to_string(),
450                detail: "the complete canister-local timer inventory was observed".to_string(),
451                next: None,
452                source: "ic_timers".to_string(),
453            },
454            timers: timer_statuses_from(snapshots),
455            failure: None,
456        },
457        Err(error) => TimerStatusObservation {
458            check: RuntimeCheck {
459                category: "runtime".to_string(),
460                code: "timer_inventory_unavailable".to_string(),
461                status: RuntimeCheckStatus::Fail,
462                subject: "shared_timer_registry".to_string(),
463                detail: "the canister-local timer inventory could not be observed".to_string(),
464                next: Some(
465                    "retry runtime observation; inspect recent failures if unavailable persists"
466                        .to_string(),
467                ),
468                source: "ic_timers".to_string(),
469            },
470            timers: Vec::new(),
471            failure: Some(RecentFailureInput {
472                occurred_at_ns: observed_at_ns,
473                subsystem: "timer_runtime".to_string(),
474                code: "timer_inventory_unavailable".to_string(),
475                severity: FailureSeverity::Error,
476                summary: error.to_string(),
477                correlation_id: None,
478            }),
479        },
480    }
481}
482
483fn timer_statuses_from(snapshots: Vec<ic_timers::TimerSnapshot>) -> Vec<CanisterTimerStatus> {
484    let mut timers = snapshots
485        .into_iter()
486        .map(|snapshot| {
487            let identity = snapshot.identity();
488            let observability = snapshot.observability();
489            let outcomes = observability.outcomes();
490            let counters = observability.counters();
491            let performance = observability.performance();
492            let condition = timer_process_condition(snapshot.process_condition());
493            CanisterTimerStatus {
494                name: identity.name().to_string(),
495                owner: identity.owner().to_string(),
496                subsystem: identity.subsystem().to_string(),
497                scheduling_mode: timer_scheduling_mode(snapshot.scheduling_mode()),
498                registration: timer_registration_status(snapshot.registration_status()),
499                condition,
500                enabled: condition != crate::domain::runtime::TimerProcessCondition::Disabled,
501                generation: snapshot.generation(),
502                next_due_at_ns: snapshot.next_deadline_ns(),
503                last_outcome: outcomes.last_outcome().map(timer_execution_outcome),
504                last_work_count: outcomes.last_work_count().unwrap_or_default(),
505                last_success_at_ns: outcomes.last_success_at_ns(),
506                last_failure_at_ns: outcomes.last_failure_at_ns(),
507                consecutive_expected_failures: outcomes.consecutive_expected_failures(),
508                schedules_since_runtime_start: counters.wakeups_armed(),
509                executions_since_runtime_start: counters.work_started(),
510                successes_since_runtime_start: counters
511                    .succeeded()
512                    .saturating_add(counters.no_work()),
513                expected_failures_since_runtime_start: counters.retryable_failure(),
514                invariant_failures_since_runtime_start: counters.invariant_failure(),
515                stale_callbacks_since_runtime_start: counters
516                    .stale_wakeups()
517                    .saturating_add(counters.stale_work()),
518                scheduler_performance: timer_callback_performance_status(
519                    performance.scheduler_instructions(),
520                    performance.scheduler_memory_pages(),
521                ),
522                work_performance: timer_callback_performance_status(
523                    performance.work_instructions(),
524                    performance.work_memory_pages(),
525                ),
526            }
527        })
528        .collect::<Vec<_>>();
529    timers.sort_by(|left, right| {
530        left.owner
531            .cmp(&right.owner)
532            .then_with(|| left.subsystem.cmp(&right.subsystem))
533            .then_with(|| left.name.cmp(&right.name))
534    });
535    timers
536}
537
538fn timer_callback_performance_status(
539    instructions: ic_timers::MeasurementSummary,
540    memory: ic_timers::MemoryPageSummary,
541) -> TimerCallbackPerformanceStatus {
542    TimerCallbackPerformanceStatus {
543        instruction_samples_since_runtime_start: instructions.samples(),
544        instructions_latest: instructions.latest(),
545        instructions_maximum: instructions.maximum(),
546        instructions_total_since_runtime_start: instructions.total(),
547        memory_page_samples_since_runtime_start: memory.samples(),
548        memory_pages_latest: memory.latest().map(timer_memory_page_sample_status),
549        maximum_wasm_memory_growth_pages: memory.maximum_wasm_growth_pages(),
550        maximum_stable_memory_growth_pages: memory.maximum_stable_growth_pages(),
551    }
552}
553
554const fn timer_memory_page_sample_status(
555    sample: ic_timers::MemoryPageSample,
556) -> TimerMemoryPageSampleStatus {
557    TimerMemoryPageSampleStatus {
558        start: timer_memory_page_extent_status(sample.start()),
559        end: timer_memory_page_extent_status(sample.end()),
560    }
561}
562
563const fn timer_memory_page_extent_status(
564    extent: ic_timers::MemoryPageExtent,
565) -> TimerMemoryPageExtentStatus {
566    TimerMemoryPageExtentStatus {
567        wasm_pages: extent.wasm_pages(),
568        stable_pages: extent.stable_pages(),
569    }
570}
571
572const fn timer_scheduling_mode(
573    value: ic_timers::TimerSchedulingMode,
574) -> crate::domain::runtime::TimerSchedulingMode {
575    use crate::domain::runtime::TimerSchedulingMode as Canic;
576    use ic_timers::TimerSchedulingMode as Shared;
577    match value {
578        Shared::Once => Canic::Once,
579        Shared::AfterCompletion => Canic::AfterCompletion,
580        Shared::Deadline => Canic::Deadline,
581        Shared::Retry => Canic::Retry,
582        Shared::Continuation => Canic::Continuation,
583        Shared::Watchdog => Canic::Watchdog,
584    }
585}
586
587const fn timer_registration_status(
588    value: ic_timers::TimerRegistrationStatus,
589) -> crate::domain::runtime::TimerRegistrationStatus {
590    use crate::domain::runtime::TimerRegistrationStatus as Canic;
591    match value {
592        ic_timers::TimerRegistrationStatus::Unregistered => Canic::Unregistered,
593        ic_timers::TimerRegistrationStatus::Scheduled => Canic::Scheduled,
594        ic_timers::TimerRegistrationStatus::Running => Canic::Running,
595    }
596}
597
598const fn timer_process_condition(
599    value: ic_timers::TimerProcessCondition,
600) -> crate::domain::runtime::TimerProcessCondition {
601    use crate::domain::runtime::TimerProcessCondition as Canic;
602    match value {
603        ic_timers::TimerProcessCondition::Disabled => Canic::Disabled,
604        ic_timers::TimerProcessCondition::Idle => Canic::Idle,
605        ic_timers::TimerProcessCondition::Active => Canic::Active,
606        ic_timers::TimerProcessCondition::Retrying => Canic::Retrying,
607        ic_timers::TimerProcessCondition::Failed => Canic::Failed,
608    }
609}
610
611const fn timer_execution_outcome(
612    value: ic_timers::TimerLastOutcome,
613) -> crate::domain::runtime::TimerExecutionOutcome {
614    use crate::domain::runtime::TimerExecutionOutcome as Canic;
615    match value {
616        ic_timers::TimerLastOutcome::Completed(completion) => match completion {
617            ic_timers::TimerCompletionOutcome::Success => Canic::Success,
618            ic_timers::TimerCompletionOutcome::NoWork => Canic::NoWork,
619            ic_timers::TimerCompletionOutcome::RetryableFailure => Canic::RetryableFailure,
620            ic_timers::TimerCompletionOutcome::InvariantFailure => Canic::InvariantFailure,
621        },
622        ic_timers::TimerLastOutcome::Unacknowledged => Canic::Unacknowledged,
623    }
624}
625
626fn state_summary(role: Option<&str>) -> Option<RuntimeStateSummary> {
627    let memory_ids = MemoryRegistryOps::ledger_snapshot()
628        .ok()?
629        .memories
630        .into_iter()
631        .map(|memory| memory.memory_manager_id)
632        .collect::<std::collections::BTreeSet<_>>();
633    state_summary_for_memory_ids(role, &memory_ids)
634}
635
636fn state_summary_for_memory_ids(
637    role: Option<&str>,
638    memory_ids: &std::collections::BTreeSet<u8>,
639) -> Option<RuntimeStateSummary> {
640    role?;
641    let mut domains = canic_state_descriptors()
642        .into_iter()
643        .flat_map(|descriptor| descriptor.state)
644        .filter(|domain| domain.memory_id.is_some_and(|id| memory_ids.contains(&id)))
645        .map(|domain| RuntimeStateDomainSummary {
646            domain: domain.domain,
647            version: domain.version,
648            storage: domain.storage.as_str().to_string(),
649            memory_id: domain.memory_id,
650            status: RuntimeStateDomainStatus::Ok,
651        })
652        .collect::<Vec<_>>();
653    domains.sort_by(|left, right| left.domain.cmp(&right.domain));
654
655    if domains.is_empty() {
656        return None;
657    }
658
659    Some(RuntimeStateSummary {
660        manifest_schema_version: u32::from(STATE_MANIFEST_SCHEMA_VERSION),
661        domains,
662        total_stable_memory_pages: None,
663    })
664}
665
666fn runtime_visibility() -> Vec<RuntimeVisibilityEntry> {
667    [
668        ("schema_version", RuntimeFieldVisibility::PublicSafe),
669        ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
670        ("canister_id", RuntimeFieldVisibility::OperatorOnly),
671        ("role", RuntimeFieldVisibility::OperatorOnly),
672        ("root", RuntimeFieldVisibility::OperatorOnly),
673        ("build_network", RuntimeFieldVisibility::OperatorOnly),
674        ("build", RuntimeFieldVisibility::OperatorOnly),
675        ("features", RuntimeFieldVisibility::OperatorOnly),
676        ("topology", RuntimeFieldVisibility::ControllerOnly),
677        ("timers", RuntimeFieldVisibility::OperatorOnly),
678        ("timer_inventory", RuntimeFieldVisibility::OperatorOnly),
679        ("state", RuntimeFieldVisibility::OperatorOnly),
680        ("auth", RuntimeFieldVisibility::OperatorOnly),
681        ("blob_storage", RuntimeFieldVisibility::FeatureGated),
682        ("receipt_capacity", RuntimeFieldVisibility::OperatorOnly),
683        ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
684        ("readiness", RuntimeFieldVisibility::OperatorOnly),
685        ("status", RuntimeFieldVisibility::OperatorOnly),
686        ("visibility", RuntimeFieldVisibility::OperatorOnly),
687    ]
688    .into_iter()
689    .map(|(field, visibility)| RuntimeVisibilityEntry {
690        field: field.to_string(),
691        visibility,
692    })
693    .collect()
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699    use crate::ids::IntentResourceKey;
700    use crate::ops::runtime::bootstrap::{BootstrapPhaseLabel, BootstrapStatusOps};
701    use crate::ops::runtime::recent_failure::RecentFailureOps;
702    use crate::ops::storage::intent::{
703        INTENT_RESOURCE_TOTAL_RECORD_LIMIT, IntentStoreOps, RECEIPT_BACKED_INTENT_RECORD_LIMIT,
704    };
705    use crate::storage::stable::intent::{IntentResourceTotalsRecord, IntentStore};
706
707    #[test]
708    fn health_is_minimal_and_schema_versioned() {
709        let health = RuntimeIntrospectionApi::health(Some(42));
710
711        assert_eq!(health.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
712        assert_eq!(health.status, HealthStatus::Healthy);
713        assert_eq!(health.observed_at_ns, Some(42));
714        assert_eq!(health.checks.len(), 1);
715        assert_eq!(health.checks[0].code, "canister_responsive");
716    }
717
718    #[test]
719    fn unavailable_timer_inventory_is_explicit_and_fails_runtime_status() {
720        let observation =
721            timer_status_observation(Err(crate::api::timer::TimerError::CustodyBusy), 101);
722
723        assert_eq!(observation.check.status, RuntimeCheckStatus::Fail);
724        assert!(observation.timers.is_empty());
725        assert_eq!(
726            observation
727                .failure
728                .as_ref()
729                .map(|failure| failure.code.as_str()),
730            Some("timer_inventory_unavailable")
731        );
732        assert_eq!(
733            aggregate_runtime_status(
734                ReadinessStatus::Ready,
735                Some(RuntimeCheckStatus::Pass),
736                observation.check.status,
737            ),
738            RuntimeStatus::Failing
739        );
740    }
741
742    #[test]
743    fn runtime_status_embeds_guarded_readiness_and_build_info() {
744        let status = RuntimeIntrospectionApi::runtime_status_for(
745            Principal::anonymous(),
746            100,
747            "test-canister",
748            "1.2.3",
749            "0.81.0",
750            7,
751        );
752
753        assert_eq!(status.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
754        assert_eq!(status.observed_at_ns, 100);
755        assert_eq!(status.canister_id, Principal::anonymous());
756        assert_eq!(status.build_network, BuildNetworkOps::build_network());
757        assert_eq!(status.build.package_name, "test-canister");
758        assert_eq!(status.build.package_version, "1.2.3");
759        assert_eq!(status.build.canic_version, "0.81.0");
760        assert_eq!(status.build.canister_version, 7);
761        assert_eq!(status.readiness.observed_at_ns, 100);
762        assert!(
763            status
764                .visibility
765                .iter()
766                .any(|entry| entry.field == "topology"
767                    && entry.visibility == RuntimeFieldVisibility::ControllerOnly)
768        );
769    }
770
771    #[test]
772    fn runtime_status_classifies_each_top_level_field_visibility() {
773        let status = RuntimeIntrospectionApi::runtime_status_for(
774            Principal::anonymous(),
775            100,
776            "test-canister",
777            "1.2.3",
778            "0.81.0",
779            7,
780        );
781        let expected = [
782            ("schema_version", RuntimeFieldVisibility::PublicSafe),
783            ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
784            ("canister_id", RuntimeFieldVisibility::OperatorOnly),
785            ("role", RuntimeFieldVisibility::OperatorOnly),
786            ("root", RuntimeFieldVisibility::OperatorOnly),
787            ("build_network", RuntimeFieldVisibility::OperatorOnly),
788            ("build", RuntimeFieldVisibility::OperatorOnly),
789            ("features", RuntimeFieldVisibility::OperatorOnly),
790            ("topology", RuntimeFieldVisibility::ControllerOnly),
791            ("timers", RuntimeFieldVisibility::OperatorOnly),
792            ("timer_inventory", RuntimeFieldVisibility::OperatorOnly),
793            ("state", RuntimeFieldVisibility::OperatorOnly),
794            ("auth", RuntimeFieldVisibility::OperatorOnly),
795            ("blob_storage", RuntimeFieldVisibility::FeatureGated),
796            ("receipt_capacity", RuntimeFieldVisibility::OperatorOnly),
797            ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
798            ("readiness", RuntimeFieldVisibility::OperatorOnly),
799            ("status", RuntimeFieldVisibility::OperatorOnly),
800            ("visibility", RuntimeFieldVisibility::OperatorOnly),
801        ];
802
803        assert_eq!(status.visibility.len(), expected.len());
804        for (index, (field, visibility)) in expected.into_iter().enumerate() {
805            assert_eq!(status.visibility[index].field, field);
806            assert_eq!(status.visibility[index].visibility, visibility);
807        }
808    }
809
810    #[test]
811    fn runtime_status_projects_empty_receipt_capacity() {
812        IntentStoreOps::reset_for_tests();
813
814        let status = RuntimeIntrospectionApi::runtime_status_for(
815            Principal::anonymous(),
816            100,
817            "test-canister",
818            "1.2.3",
819            "0.96.6",
820            7,
821        );
822        let capacity = status.receipt_capacity.expect("receipt capacity");
823
824        assert_eq!(capacity.status, RuntimeCheckStatus::Pass);
825        assert_eq!(capacity.receipt_records, 0);
826        assert_eq!(
827            capacity.receipt_record_limit,
828            RECEIPT_BACKED_INTENT_RECORD_LIMIT
829        );
830        assert_eq!(capacity.resource_total_records, 0);
831        assert_eq!(
832            capacity.resource_total_record_limit,
833            INTENT_RESOURCE_TOTAL_RECORD_LIMIT
834        );
835        assert_eq!(
836            capacity.warning_headroom_threshold,
837            RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD
838        );
839        assert_eq!(capacity.source, "intent_storage");
840    }
841
842    #[test]
843    fn receipt_capacity_condition_has_exact_warning_and_failure_boundaries() {
844        assert_eq!(
845            receipt_capacity_condition(RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD + 1, u64::MAX,),
846            RuntimeCheckStatus::Pass
847        );
848        assert_eq!(
849            receipt_capacity_condition(RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD, u64::MAX),
850            RuntimeCheckStatus::Warn
851        );
852        assert_eq!(
853            receipt_capacity_condition(u64::MAX, 1),
854            RuntimeCheckStatus::Warn
855        );
856        assert_eq!(
857            receipt_capacity_condition(u64::MAX, 0),
858            RuntimeCheckStatus::Fail
859        );
860        assert_eq!(
861            aggregate_runtime_status(
862                ReadinessStatus::Ready,
863                Some(RuntimeCheckStatus::Warn),
864                RuntimeCheckStatus::Pass,
865            ),
866            RuntimeStatus::Degraded
867        );
868        assert_eq!(
869            aggregate_runtime_status(ReadinessStatus::Ready, None, RuntimeCheckStatus::Pass,),
870            RuntimeStatus::Failing
871        );
872        assert_eq!(
873            aggregate_runtime_status(
874                ReadinessStatus::Ready,
875                Some(RuntimeCheckStatus::Pass),
876                RuntimeCheckStatus::Fail,
877            ),
878            RuntimeStatus::Failing
879        );
880    }
881
882    #[test]
883    fn runtime_status_fails_closed_with_typed_capacity_diagnostic() {
884        IntentStoreOps::reset_for_tests();
885        RecentFailureOps::reset();
886        for value in 0..=INTENT_RESOURCE_TOTAL_RECORD_LIMIT {
887            IntentStore::set_totals(
888                IntentResourceKey::new(format!("runtime-capacity:{value}")),
889                IntentResourceTotalsRecord {
890                    reserved_qty: 0,
891                    committed_qty: 1,
892                    pending_count: 0,
893                },
894            );
895        }
896
897        let status = RuntimeIntrospectionApi::runtime_status_for(
898            Principal::anonymous(),
899            100,
900            "test-canister",
901            "1.2.3",
902            "0.96.6",
903            7,
904        );
905
906        assert_eq!(status.status, RuntimeStatus::Failing);
907        assert!(status.receipt_capacity.is_none());
908        let expected_code = crate::diagnostics::codes::CAPACITY_LIMIT.to_string();
909        let failure = status
910            .recent_failures
911            .iter()
912            .find(|failure| failure.code == expected_code)
913            .expect("current capacity failure diagnostic");
914        assert_eq!(failure.subsystem, "intent_capacity");
915        assert_eq!(failure.code, expected_code);
916        assert_eq!(failure.severity, FailureSeverity::Error);
917        assert_eq!(
918            failure.summary,
919            format!("diagnostic={}", crate::diagnostics::codes::CAPACITY_LIMIT)
920        );
921        assert!(RecentFailureOps::snapshot().is_empty());
922
923        IntentStoreOps::reset_for_tests();
924        RecentFailureOps::reset();
925    }
926
927    #[test]
928    fn runtime_status_reports_compile_features_deterministically() {
929        let status = RuntimeIntrospectionApi::runtime_status_for(
930            Principal::anonymous(),
931            100,
932            "test-canister",
933            "1.2.3",
934            "0.81.0",
935            7,
936        );
937        assert_eq!(status.features.len(), RUNTIME_FEATURE_FLAGS.len());
938        for (index, (name, enabled)) in RUNTIME_FEATURE_FLAGS.into_iter().enumerate() {
939            assert_eq!(status.features[index].name, name);
940            assert_eq!(status.features[index].enabled, enabled);
941            assert_eq!(
942                status.features[index].visibility,
943                RuntimeFieldVisibility::OperatorOnly
944            );
945            assert_eq!(status.features[index].source, RUNTIME_FEATURE_SOURCE);
946        }
947    }
948
949    #[test]
950    fn runtime_status_reports_auth_and_blob_storage_feature_summaries() {
951        let status = RuntimeIntrospectionApi::runtime_status_for(
952            Principal::anonymous(),
953            100,
954            "test-canister",
955            "1.2.3",
956            "0.81.0",
957            7,
958        );
959
960        let auth = status.auth.expect("auth feature summary");
961        assert!(
962            auth.auth_features
963                .windows(2)
964                .all(|features| features[0].name <= features[1].name)
965        );
966        assert_runtime_feature(
967            &auth.auth_features,
968            "auth-chain-key-ecdsa",
969            cfg!(feature = "auth-chain-key-ecdsa"),
970        );
971        assert_runtime_feature(
972            &auth.auth_features,
973            "auth-delegated-token-verify",
974            cfg!(feature = "auth-delegated-token-verify"),
975        );
976        assert_runtime_feature(
977            &auth.auth_features,
978            "auth-issuer-canister-sig-create",
979            cfg!(feature = "auth-issuer-canister-sig-create"),
980        );
981
982        if cfg!(any(
983            feature = "blob-storage",
984            feature = "blob-storage-billing"
985        )) {
986            let blob_storage = status.blob_storage.expect("blob-storage feature summary");
987            assert_runtime_feature(
988                &blob_storage.blob_storage_features,
989                "blob-storage",
990                cfg!(feature = "blob-storage"),
991            );
992            assert_runtime_feature(
993                &blob_storage.blob_storage_features,
994                "blob-storage-billing",
995                cfg!(feature = "blob-storage-billing"),
996            );
997        } else {
998            assert!(status.blob_storage.is_none());
999        }
1000    }
1001
1002    fn assert_runtime_feature(
1003        features: &[RuntimeFeatureStatus],
1004        name: &str,
1005        expected_enabled: bool,
1006    ) {
1007        let feature = features
1008            .iter()
1009            .find(|feature| feature.name == name)
1010            .unwrap_or_else(|| panic!("expected runtime feature {name}"));
1011
1012        assert_eq!(feature.enabled, expected_enabled);
1013        assert_eq!(feature.visibility, RuntimeFieldVisibility::OperatorOnly);
1014        assert_eq!(feature.source, RUNTIME_FEATURE_SOURCE);
1015    }
1016
1017    #[test]
1018    fn state_summary_joins_runtime_memory_ids_to_owner_metadata() {
1019        let summary = state_summary_for_memory_ids(
1020            Some("root"),
1021            &std::collections::BTreeSet::from([
1022                crate::role_contract::allocation::memory::runtime::RUNTIME_BINDINGS_ID,
1023            ]),
1024        )
1025        .expect("runtime state declarations");
1026
1027        assert_eq!(
1028            summary.manifest_schema_version,
1029            u32::from(crate::state_contract::STATE_MANIFEST_SCHEMA_VERSION)
1030        );
1031        assert!(summary.total_stable_memory_pages.is_none());
1032        assert!(summary.domains.iter().any(|domain| {
1033            domain.domain == "runtime_bindings"
1034                && domain.storage == "stable_memory"
1035                && domain.status == RuntimeStateDomainStatus::Ok
1036        }));
1037        assert!(state_summary_for_memory_ids(None, &std::collections::BTreeSet::new()).is_none());
1038    }
1039
1040    #[test]
1041    fn runtime_status_includes_recent_failure_snapshot() {
1042        RecentFailureOps::reset();
1043        RecentFailureOps::record(RecentFailureInput {
1044            occurred_at_ns: 77,
1045            subsystem: "runtime".to_string(),
1046            code: "readiness_failed".to_string(),
1047            severity: FailureSeverity::Error,
1048            summary: "bounded failure summary".to_string(),
1049            correlation_id: Some("runtime-check".to_string()),
1050        });
1051
1052        let status = RuntimeIntrospectionApi::runtime_status_for(
1053            Principal::anonymous(),
1054            100,
1055            "test-canister",
1056            "1.2.3",
1057            "0.81.0",
1058            7,
1059        );
1060
1061        let failure = status
1062            .recent_failures
1063            .iter()
1064            .find(|failure| failure.code == "readiness_failed")
1065            .expect("retained recent failure");
1066        assert_eq!(failure.occurred_at_ns, 77);
1067        assert_eq!(failure.subsystem, "runtime");
1068
1069        RecentFailureOps::reset();
1070    }
1071
1072    #[test]
1073    fn runtime_status_includes_bootstrap_failure_metadata() {
1074        RecentFailureOps::reset();
1075        BootstrapStatusOps::set_phase(BootstrapPhaseLabel::ROOT_INIT);
1076        BootstrapStatusOps::mark_failed("raw bootstrap failure detail");
1077
1078        let status = RuntimeIntrospectionApi::runtime_status_for(
1079            Principal::anonymous(),
1080            100,
1081            "test-canister",
1082            "1.2.3",
1083            "0.81.0",
1084            7,
1085        );
1086
1087        let failure = status
1088            .recent_failures
1089            .iter()
1090            .find(|failure| failure.code == "bootstrap_failed")
1091            .expect("bootstrap failure metadata");
1092
1093        assert_eq!(failure.subsystem, "runtime_bootstrap");
1094        assert_eq!(failure.severity, FailureSeverity::Error);
1095        assert_eq!(failure.correlation_id.as_deref(), Some("root:init"));
1096        assert!(
1097            !failure.summary.contains("raw bootstrap failure detail"),
1098            "runtime status recent failures should not mirror raw bootstrap errors"
1099        );
1100
1101        RecentFailureOps::reset();
1102    }
1103}