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
78pub struct MemoryRuntimeApi;
83
84impl MemoryRuntimeApi {
85 pub fn bootstrap_registry() -> Result<(), Error> {
87 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
88
89 Ok(())
90 }
91}
92
93pub struct RuntimeIntrospectionApi;
98
99impl RuntimeIntrospectionApi {
100 #[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 #[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 #[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 #[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}