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