Skip to main content

canic_core/api/runtime/
mod.rs

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