rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

commit d622a3569eda032cc425bc4d853fb1ccdcf4631e
parent b27c561a885b5904d270848eed5c71d8855fc7e4
Author: triesap <tyson@radroots.org>
Date:   Mon, 24 Aug 2026 17:33:38 +0000

doctor: freeze bounded RHI diagnostics

Diffstat:
MAGENTS.md | 8++++++++
MCargo.toml | 3++-
MREADME | 19+++++++++++++++++++
Mcontracts/api_baselines/rhi.txt | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/services_hardening/operator_contract.v1.json | 41++++++++++++++++++++++++++---------------
Asrc/doctor_v1.rs | 647+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 10++++++++++
Msrc/main.rs | 39++++++++++++++++++++++++---------------
Asrc/process_result_v1.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/package_boundary.rs | 95++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atests/services_hardening_doctor.rs | 364+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/services_hardening_operator_contract.rs | 34++++++++++++----------------------
12 files changed, 1356 insertions(+), 54 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -434,6 +434,14 @@ identity/credential, bind/network policy, required source reachability, checkpoint plausibility, leases/backlog, publication invariants, and clock skew without leaking protected details. +- Step 211 freezes the exact fifteen-check doctor inventory, its ordered + per-check deadlines and safe remediation codes, the `pass`, `degraded`, and + `fail` aggregate meanings, and process exit codes zero through six. A required + skipped result is failure; required failure or timeout is exit six; optional + non-pass is degraded success. Dropping a timed-out probe future must stop its + work or leave cleanup owned synchronously by that future, with no detached + probe task. Keep paths, raw errors, arbitrary summaries, and protected values + out of the bounded canonical report and process diagnostics. - Keep plaintext keys, decrypted identity, wrapping credentials, tokens, raw sensitive evidence, private identifiers, paths, upstream errors, and equivalent protected material out of config, logs, status, metrics, audit, diff --git a/Cargo.toml b/Cargo.toml @@ -69,11 +69,12 @@ futures-executor = { version = "0.3" } jsonschema = { version = "0.48.1", default-features = false } nostr = { version = "0.44.7" } rustix = { version = "1", features = ["fs", "process", "std"] } -serde = { version = "1", default-features = false } +serde = { version = "1", default-features = false, features = ["derive"] } serde_json = { version = "1", default-features = false, features = ["raw_value"] } sha2 = { version = "0.10" } sqlx = { version = "0.9.0", default-features = false, features = ["sqlite-bundled"] } thiserror = { version = "2" } +tokio = { version = "1", default-features = false, features = ["time"] } tempfile = { version = "3" } toml = { version = "0.8" } url = "2" diff --git a/README b/README @@ -555,6 +555,25 @@ database-path, worker, environment-file, or arbitrary path-leaf flag. Identity rekey and replacement are not commands; rotation is a create-new offline artifact plus governed configuration apply. +## Bounded active doctor and stable process results + +The doctor runs the governed fifteen checks in exact contract order. Each +check has one fixed deadline, required or optional authority, a closed evidence +scope, and a safe remediation code. Required checks must pass; required +failure, timeout, or an invalid skipped result makes the aggregate `fail`. +Optional non-pass makes the aggregate `degraded`, while every passing check +makes it `pass`. A timed-out probe is cancelled by dropping its future, and no +detached probe work is permitted. + +The compact canonical JSON report is capped at 8,192 UTF-8 bytes. Its summaries +come only from the fixed content-free vocabulary and cannot contain paths, raw +errors, relay text, identifiers, credentials, or other protected material. +Required failure or timeout returns exit code `6`; the complete process-result +contract is the closed zero-through-six inventory in +[`operator_contract.v1.json`](contracts/services_hardening/operator_contract.v1.json). +The executable emits only the stable result code on stderr and never renders a +parser, path-resolution, dependency, or internal error. + ## Unix-admin boundary Step 206 bound the seven common RHI routes for detailed status, redacted diff --git a/contracts/api_baselines/rhi.txt b/contracts/api_baselines/rhi.txt @@ -192,6 +192,54 @@ pub rhi::RhiCredentialResolutionErrorKind::UnsupportedPlatform pub rhi::RhiCredentialResolutionErrorKind::UnsupportedProfile impl rhi::RhiCredentialResolutionErrorKind pub const fn rhi::RhiCredentialResolutionErrorKind::code(self) -> &'static str +pub enum rhi::RhiDoctorAggregateStatus +pub rhi::RhiDoctorAggregateStatus::Degraded +pub rhi::RhiDoctorAggregateStatus::Fail +pub rhi::RhiDoctorAggregateStatus::Pass +pub enum rhi::RhiDoctorCheckId +pub rhi::RhiDoctorCheckId::AdminBindPolicy +pub rhi::RhiDoctorCheckId::ClockSkew +pub rhi::RhiDoctorCheckId::CursorCheckpoint +pub rhi::RhiDoctorCheckId::IdentityBinding +pub rhi::RhiDoctorCheckId::NetworkPolicy +pub rhi::RhiDoctorCheckId::OperationsBindPolicy +pub rhi::RhiDoctorCheckId::PathsPermissions +pub rhi::RhiDoctorCheckId::PublicationInvariants +pub rhi::RhiDoctorCheckId::ReconciliationBacklog +pub rhi::RhiDoctorCheckId::ReconciliationLeases +pub rhi::RhiDoctorCheckId::RequiredSources +pub rhi::RhiDoctorCheckId::SqliteFreeSpace +pub rhi::RhiDoctorCheckId::SqliteIntegrity +pub rhi::RhiDoctorCheckId::SqliteSchema +pub rhi::RhiDoctorCheckId::WriterLock +pub enum rhi::RhiDoctorCheckStatus +pub rhi::RhiDoctorCheckStatus::Fail +pub rhi::RhiDoctorCheckStatus::Pass +pub rhi::RhiDoctorCheckStatus::Skipped +pub rhi::RhiDoctorCheckStatus::Timeout +pub enum rhi::RhiDoctorErrorKind +pub rhi::RhiDoctorErrorKind::Encoding +pub rhi::RhiDoctorErrorKind::OutputTooLarge +pub enum rhi::RhiDoctorObservation +pub rhi::RhiDoctorObservation::Fail +pub rhi::RhiDoctorObservation::Pass +pub rhi::RhiDoctorObservation::Skipped +pub enum rhi::RhiDoctorRemediationCode +pub rhi::RhiDoctorRemediationCode::CorrectAdminBindPolicy +pub rhi::RhiDoctorRemediationCode::CorrectClock +pub rhi::RhiDoctorRemediationCode::CorrectNetworkPolicy +pub rhi::RhiDoctorRemediationCode::CorrectOperationsBindPolicy +pub rhi::RhiDoctorRemediationCode::CorrectPathPolicy +pub rhi::RhiDoctorRemediationCode::FreeStateDiskSpace +pub rhi::RhiDoctorRemediationCode::ReduceReconciliationBacklog +pub rhi::RhiDoctorRemediationCode::ReleaseWriterLock +pub rhi::RhiDoctorRemediationCode::RepairCursorCheckpoint +pub rhi::RhiDoctorRemediationCode::RepairPublicationState +pub rhi::RhiDoctorRemediationCode::RepairReconciliationLeases +pub rhi::RhiDoctorRemediationCode::RepairSchema +pub rhi::RhiDoctorRemediationCode::RestoreIdentityBinding +pub rhi::RhiDoctorRemediationCode::RestoreRequiredSources +pub rhi::RhiDoctorRemediationCode::RestoreVerifiedState pub enum rhi::RhiEncryptedIdentityEnvelopeErrorKind pub rhi::RhiEncryptedIdentityEnvelopeErrorKind::AlreadyExists pub rhi::RhiEncryptedIdentityEnvelopeErrorKind::IdentityMismatch @@ -290,6 +338,18 @@ pub rhi::RhiPresenceTargetState::Submitted pub rhi::RhiPresenceTargetState::Unknown impl rhi::RhiPresenceTargetState pub const fn rhi::RhiPresenceTargetState::code(self) -> &'static str +pub enum rhi::RhiProcessResult +pub rhi::RhiProcessResult::DoctorRequiredCheckFailed +pub rhi::RhiProcessResult::InputOrConfiguration +pub rhi::RhiProcessResult::OperationRejectedOrConflict +pub rhi::RhiProcessResult::ServiceOrDependencyUnavailable +pub rhi::RhiProcessResult::StateOrIdentityUnavailable +pub rhi::RhiProcessResult::Success +pub rhi::RhiProcessResult::UnexpectedInternal +impl rhi::RhiProcessResult +pub const fn rhi::RhiProcessResult::code(self) -> &'static str +pub fn rhi::RhiProcessResult::exit_code(self) -> std::process::ExitCode +pub const fn rhi::RhiProcessResult::exit_code_u8(self) -> u8 pub enum rhi::RhiPublicationAttemptEvidenceErrorKind pub rhi::RhiPublicationAttemptEvidenceErrorKind::InvalidAttemptNumber pub rhi::RhiPublicationAttemptEvidenceErrorKind::InvalidTargetOrdinal @@ -863,6 +923,36 @@ pub const fn rhi::RhiDirtyTradeRepository<'_>::descriptor(&self) -> rhi::RhiStat pub const fn rhi::RhiDirtyTradeRepository<'_>::kind(&self) -> rhi::RhiStateRepositoryKind impl core::fmt::Debug for rhi::RhiDirtyTradeRepository<'_> pub fn rhi::RhiDirtyTradeRepository<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiDoctorCheckDefinition +impl rhi::RhiDoctorCheckDefinition +pub const fn rhi::RhiDoctorCheckDefinition::deadline_ms(self) -> u64 +pub const fn rhi::RhiDoctorCheckDefinition::id(self) -> rhi::RhiDoctorCheckId +pub const fn rhi::RhiDoctorCheckDefinition::remediation_code(self) -> rhi::RhiDoctorRemediationCode +pub const fn rhi::RhiDoctorCheckDefinition::required(self) -> bool +pub const fn rhi::RhiDoctorCheckDefinition::scope(self) -> &'static [&'static str] +pub struct rhi::RhiDoctorCheckResult +impl rhi::RhiDoctorCheckResult +pub const fn rhi::RhiDoctorCheckResult::definition(self) -> rhi::RhiDoctorCheckDefinition +pub const fn rhi::RhiDoctorCheckResult::status(self) -> rhi::RhiDoctorCheckStatus +pub const fn rhi::RhiDoctorCheckResult::summary(self) -> &'static str +pub struct rhi::RhiDoctorError +impl rhi::RhiDoctorError +pub const fn rhi::RhiDoctorError::kind(self) -> rhi::RhiDoctorErrorKind +impl core::error::Error for rhi::RhiDoctorError +impl core::fmt::Debug for rhi::RhiDoctorError +pub fn rhi::RhiDoctorError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for rhi::RhiDoctorError +pub fn rhi::RhiDoctorError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct rhi::RhiDoctorReport +impl rhi::RhiDoctorReport +pub fn rhi::RhiDoctorReport::canonical_json(&self) -> &[u8] +pub fn rhi::RhiDoctorReport::checks(&self) -> &[rhi::RhiDoctorCheckResult] +pub const fn rhi::RhiDoctorReport::exit_code(&self) -> u8 +pub const fn rhi::RhiDoctorReport::instance(&self) -> &radroots_runtime_paths::identifier::InstanceId +pub const fn rhi::RhiDoctorReport::service(&self) -> &'static str +pub const fn rhi::RhiDoctorReport::status(&self) -> rhi::RhiDoctorAggregateStatus +impl core::fmt::Debug for rhi::RhiDoctorReport +pub fn rhi::RhiDoctorReport::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct rhi::RhiEffectiveConfigV1 impl rhi::RhiEffectiveConfigV1 pub fn rhi::RhiEffectiveConfigV1::canonical_json(&self) -> &str @@ -1948,6 +2038,10 @@ pub const rhi::RHI_CONFIG_DOCUMENT_MAX_UTF8_BYTES: usize pub const rhi::RHI_CONFIG_EFFECTIVE_MAX_UTF8_BYTES: usize pub const rhi::RHI_CONFIG_SCHEMA: &str pub const rhi::RHI_CONFIG_SCHEMA_VERSION: u32 +pub const rhi::RHI_DOCTOR_CHECK_COUNT: usize +pub const rhi::RHI_DOCTOR_CONTRACT_VERSION: u32 +pub const rhi::RHI_DOCTOR_REPORT_MAX_UTF8_BYTES: usize +pub const rhi::RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES: usize pub const rhi::RHI_ENCRYPTED_IDENTITY_BACKUP_INCLUDED: bool pub const rhi::RHI_ENCRYPTED_IDENTITY_ENVELOPE_CONTRACT_VERSION: u32 pub const rhi::RHI_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES: usize @@ -2037,6 +2131,8 @@ pub trait rhi::RhiCredentialAccess: core::marker::Send + core::marker::Sync pub fn rhi::RhiCredentialAccess::resolve_existing(&self, &rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiWrappingCredential, rhi::RhiCredentialResolutionError> impl rhi::RhiCredentialAccess for rhi::CanonicalRhiCredentialAccess pub fn rhi::CanonicalRhiCredentialAccess::resolve_existing(&self, &rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiWrappingCredential, rhi::RhiCredentialResolutionError> +pub trait rhi::RhiDoctorProbe: core::marker::Send + core::marker::Sync +pub fn rhi::RhiDoctorProbe::probe(&self, rhi::RhiDoctorCheckDefinition) -> rhi::RhiDoctorFuture<'_> pub trait rhi::RhiExactPresenceSink: core::marker::Send + core::marker::Sync pub fn rhi::RhiExactPresenceSink::submit_exact<'a>(&'a self, &'a rhi::RhiPreparedPresenceAttempt) -> radroots_transport::source::BoxFuture<'a, rhi::RhiPresenceAttemptOutcome> pub trait rhi::RhiExactPublicationSink: core::marker::Send + core::marker::Sync @@ -2067,9 +2163,11 @@ pub fn rhi::provision_rhi_encrypted_identity(&rhi::RhiIdentityEnvelopeBinding, & pub fn rhi::reduce_rhi_reconciliation_manifest(rhi::RhiReconciliationManifest) -> core::result::Result<rhi::RhiReconciliationProjection, rhi::RhiReconciliationReducerError> pub fn rhi::resolve_rhi_runtime_context(&radroots_runtime_paths::roots::RadrootsPathResolver, &rhi::RhiCliInvocationV1) -> core::result::Result<rhi::RhiRuntimeContext, rhi::RhiRuntimeContextError> pub fn rhi::resolve_rhi_wrapping_credential(&rhi::RhiRuntimeContext, &rhi::RhiIdentityEnvelopeBinding) -> core::result::Result<rhi::RhiWrappingCredential, rhi::RhiCredentialResolutionError> +pub const fn rhi::rhi_doctor_check_definitions() -> &'static [rhi::RhiDoctorCheckDefinition; 15] pub fn rhi::rhi_migration_catalog() -> core::result::Result<radroots_service_sqlite::migration::MigrationCatalog, rhi::RhiStateCatalogError> pub fn rhi::rhi_schema_catalog() -> core::result::Result<radroots_service_sqlite::integrity::catalog::SchemaCatalog, rhi::RhiStateCatalogError> pub const fn rhi::rhi_state_repository_descriptors() -> &'static [rhi::RhiStateRepositoryDescriptor; 21] +pub async fn rhi::run_rhi_doctor(&rhi::RhiRuntimeContext, &impl rhi::RhiDoctorProbe + ?core::marker::Sized) -> core::result::Result<rhi::RhiDoctorReport, rhi::RhiDoctorError> pub async fn rhi::stage_rhi_state_restore(&rhi::RhiRuntimeContext, &rhi::RhiStateMetadata, rhi::RhiVerifiedStateBackup) -> core::result::Result<rhi::RhiStagedStateRestore, rhi::RhiStateMaintenanceError> pub fn rhi::trade_mutation_subscription_kinds() -> alloc::vec::Vec<u32> pub fn rhi::validate_rhi_presence_desired_authority(&rhi::RhiConfigDocumentV1, &rhi::RhiPresenceDesiredAuthority) -> core::result::Result<(), rhi::RhiPresenceDesiredError> @@ -2077,5 +2175,6 @@ pub fn rhi::validate_rhi_signed_presence_documents(&rhi::RhiSignedPresenceDocume pub fn rhi::validate_rhi_state_catalogs(&radroots_service_sqlite::migration::MigrationCatalog, &radroots_service_sqlite::integrity::catalog::SchemaCatalog) -> core::result::Result<(), rhi::RhiStateCatalogError> pub fn rhi::verify_rhi_state_backup(&[u8], radroots_service_sqlite::backup::manifest::BackupManifestSha256, &std::path::Path, &rhi::RhiStateMetadata, core::num::nonzero::NonZeroU64) -> core::result::Result<rhi::RhiVerifiedStateBackup, rhi::RhiStateMaintenanceError> pub type rhi::RhiAdminFuture<'a> = core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<rhi::RhiAdminResponseDocument, rhi::RhiAdminHandlerError>> + core::marker::Send + 'a)>> +pub type rhi::RhiDoctorFuture<'a> = core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = rhi::RhiDoctorObservation> + core::marker::Send + 'a)>> pub type rhi::RhiReconciliationCoverage = radroots_trade::evidence::RadrootsTradeEvidenceCoverageV1 pub type rhi::RhiReconciliationOutcome = radroots_trade::evidence::RadrootsTradeEvidenceOutcomeV1 diff --git a/contracts/services_hardening/operator_contract.v1.json b/contracts/services_hardening/operator_contract.v1.json @@ -250,22 +250,33 @@ "doctor": { "shared_schema": "radroots.service.doctor.v1", "contract_version": 1, + "execution": "ordered", + "pass_requires_all_scope": true, + "probe_future_cancellation": "drop_stops_or_owns_cleanup", + "detached_probe_work": false, + "statuses": ["pass", "fail", "timeout", "skipped"], + "aggregate_statuses": ["pass", "degraded", "fail"], + "required_skipped": "forbidden", + "summary_max_utf8_bytes": 256, + "report_max_utf8_bytes": 8192, + "raw_error_or_path_allowed": false, + "required_fail_or_timeout_exit": 6, "checks": [ - { "id": "paths_permissions", "required": true }, - { "id": "writer_lock", "required": true }, - { "id": "sqlite_schema", "required": true }, - { "id": "sqlite_integrity", "required": true }, - { "id": "sqlite_free_space", "required": true }, - { "id": "identity_binding", "required": true }, - { "id": "admin_bind_policy", "required": true }, - { "id": "operations_bind_policy", "required": true }, - { "id": "network_policy", "required": true }, - { "id": "required_sources", "required": true }, - { "id": "cursor_checkpoint", "required": true }, - { "id": "reconciliation_leases", "required": true }, - { "id": "reconciliation_backlog", "required": true }, - { "id": "publication_invariants", "required": true }, - { "id": "clock_skew", "required": false } + { "id": "paths_permissions", "required": true, "deadline_ms": 2000, "remediation_code": "correct_path_policy", "scope": ["resolved_path_containment", "owner", "type", "mode"] }, + { "id": "writer_lock", "required": true, "deadline_ms": 2000, "remediation_code": "release_writer_lock", "scope": ["state_directory_binding", "writer_lock_state"] }, + { "id": "sqlite_schema", "required": true, "deadline_ms": 5000, "remediation_code": "repair_schema", "scope": ["metadata_identity", "migration_history", "schema_catalog"] }, + { "id": "sqlite_integrity", "required": true, "deadline_ms": 15000, "remediation_code": "restore_verified_state", "scope": ["integrity_check", "foreign_key_check"] }, + { "id": "sqlite_free_space", "required": true, "deadline_ms": 2000, "remediation_code": "free_state_disk_space", "scope": ["state_filesystem_capacity", "minimum_free_bytes"] }, + { "id": "identity_binding", "required": true, "deadline_ms": 2000, "remediation_code": "restore_identity_binding", "scope": ["envelope_contract", "credential_reference", "public_identity"] }, + { "id": "admin_bind_policy", "required": true, "deadline_ms": 2000, "remediation_code": "correct_admin_bind_policy", "scope": ["unix_socket_path", "socket_mode", "peer_authorization"] }, + { "id": "operations_bind_policy", "required": true, "deadline_ms": 2000, "remediation_code": "correct_operations_bind_policy", "scope": ["enabled_posture", "listen_address", "bind_policy"] }, + { "id": "network_policy", "required": true, "deadline_ms": 2000, "remediation_code": "correct_network_policy", "scope": ["dns_policy", "tls_policy", "relay_url_policy"] }, + { "id": "required_sources", "required": true, "deadline_ms": 15000, "remediation_code": "restore_required_sources", "scope": ["required_source_inventory", "reachability", "source_deadline"] }, + { "id": "cursor_checkpoint", "required": true, "deadline_ms": 5000, "remediation_code": "repair_cursor_checkpoint", "scope": ["selector_binding", "cursor_plausibility", "completion_evidence"] }, + { "id": "reconciliation_leases", "required": true, "deadline_ms": 5000, "remediation_code": "repair_reconciliation_leases", "scope": ["lease_ownership", "lease_expiry", "retry_state"] }, + { "id": "reconciliation_backlog", "required": true, "deadline_ms": 5000, "remediation_code": "reduce_reconciliation_backlog", "scope": ["queue_bound", "attempt_bound", "schedule_plausibility"] }, + { "id": "publication_invariants", "required": true, "deadline_ms": 5000, "remediation_code": "repair_publication_state", "scope": ["exact_signed_bytes", "target_inventory", "outbox_schedule"] }, + { "id": "clock_skew", "required": false, "deadline_ms": 5000, "remediation_code": "correct_clock", "scope": ["wall_clock_skew"] } ] }, "exit_codes": [ diff --git a/src/doctor_v1.rs b/src/doctor_v1.rs @@ -0,0 +1,647 @@ +//! Bounded active-doctor orchestration and safe structured evidence. + +use core::{fmt, future::Future, pin::Pin, time::Duration}; +use std::error::Error; + +use radroots_runtime_paths::InstanceId; +use serde::Serialize; + +use crate::RhiRuntimeContext; + +/// RHI doctor wire-contract version. +pub const RHI_DOCTOR_CONTRACT_VERSION: u32 = 1; +/// Exact number of governed RHI doctor checks. +pub const RHI_DOCTOR_CHECK_COUNT: usize = 15; +/// Maximum encoded size of one safe summary. +pub const RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES: usize = 256; +/// Maximum encoded size of the complete canonical doctor report. +pub const RHI_DOCTOR_REPORT_MAX_UTF8_BYTES: usize = 8_192; + +const RHI_SERVICE: &str = "rhi"; +const DOCTOR_FAILURE_EXIT_CODE: u8 = 6; +const _: () = { + assert!("check passed".len() <= RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES); + assert!("check failed".len() <= RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES); + assert!("check timed out".len() <= RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES); + assert!("optional check skipped".len() <= RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES); +}; + +/// The closed RHI doctor inventory. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum RhiDoctorCheckId { + PathsPermissions, + WriterLock, + SqliteSchema, + SqliteIntegrity, + SqliteFreeSpace, + IdentityBinding, + AdminBindPolicy, + OperationsBindPolicy, + NetworkPolicy, + RequiredSources, + CursorCheckpoint, + ReconciliationLeases, + ReconciliationBacklog, + PublicationInvariants, + ClockSkew, +} + +impl RhiDoctorCheckId { + const fn as_str(self) -> &'static str { + match self { + Self::PathsPermissions => "paths_permissions", + Self::WriterLock => "writer_lock", + Self::SqliteSchema => "sqlite_schema", + Self::SqliteIntegrity => "sqlite_integrity", + Self::SqliteFreeSpace => "sqlite_free_space", + Self::IdentityBinding => "identity_binding", + Self::AdminBindPolicy => "admin_bind_policy", + Self::OperationsBindPolicy => "operations_bind_policy", + Self::NetworkPolicy => "network_policy", + Self::RequiredSources => "required_sources", + Self::CursorCheckpoint => "cursor_checkpoint", + Self::ReconciliationLeases => "reconciliation_leases", + Self::ReconciliationBacklog => "reconciliation_backlog", + Self::PublicationInvariants => "publication_invariants", + Self::ClockSkew => "clock_skew", + } + } +} + +/// Stable operator action associated with one doctor check. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiDoctorRemediationCode { + CorrectPathPolicy, + ReleaseWriterLock, + RepairSchema, + RestoreVerifiedState, + FreeStateDiskSpace, + RestoreIdentityBinding, + CorrectAdminBindPolicy, + CorrectOperationsBindPolicy, + CorrectNetworkPolicy, + RestoreRequiredSources, + RepairCursorCheckpoint, + RepairReconciliationLeases, + ReduceReconciliationBacklog, + RepairPublicationState, + CorrectClock, +} + +impl RhiDoctorRemediationCode { + const fn as_str(self) -> &'static str { + match self { + Self::CorrectPathPolicy => "correct_path_policy", + Self::ReleaseWriterLock => "release_writer_lock", + Self::RepairSchema => "repair_schema", + Self::RestoreVerifiedState => "restore_verified_state", + Self::FreeStateDiskSpace => "free_state_disk_space", + Self::RestoreIdentityBinding => "restore_identity_binding", + Self::CorrectAdminBindPolicy => "correct_admin_bind_policy", + Self::CorrectOperationsBindPolicy => "correct_operations_bind_policy", + Self::CorrectNetworkPolicy => "correct_network_policy", + Self::RestoreRequiredSources => "restore_required_sources", + Self::RepairCursorCheckpoint => "repair_cursor_checkpoint", + Self::RepairReconciliationLeases => "repair_reconciliation_leases", + Self::ReduceReconciliationBacklog => "reduce_reconciliation_backlog", + Self::RepairPublicationState => "repair_publication_state", + Self::CorrectClock => "correct_clock", + } + } +} + +/// Immutable authority for one check's requirement, deadline, and remediation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RhiDoctorCheckDefinition { + id: RhiDoctorCheckId, + required: bool, + deadline_ms: u64, + remediation_code: RhiDoctorRemediationCode, + scope: &'static [&'static str], +} + +impl RhiDoctorCheckDefinition { + const fn new( + id: RhiDoctorCheckId, + required: bool, + deadline_ms: u64, + remediation_code: RhiDoctorRemediationCode, + scope: &'static [&'static str], + ) -> Self { + Self { + id, + required, + deadline_ms, + remediation_code, + scope, + } + } + + /// Returns the governed check identifier. + #[must_use] + pub const fn id(self) -> RhiDoctorCheckId { + self.id + } + + /// Returns whether a non-pass result fails the doctor command. + #[must_use] + pub const fn required(self) -> bool { + self.required + } + + /// Returns the exact per-check deadline in milliseconds. + #[must_use] + pub const fn deadline_ms(self) -> u64 { + self.deadline_ms + } + + /// Returns the fixed, safe operator remediation classification. + #[must_use] + pub const fn remediation_code(self) -> RhiDoctorRemediationCode { + self.remediation_code + } + + /// Returns the exact safe evidence facets owned by this check. + #[must_use] + pub const fn scope(self) -> &'static [&'static str] { + self.scope + } +} + +const CHECK_DEFINITIONS: [RhiDoctorCheckDefinition; RHI_DOCTOR_CHECK_COUNT] = [ + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::PathsPermissions, + true, + 2_000, + RhiDoctorRemediationCode::CorrectPathPolicy, + &["resolved_path_containment", "owner", "type", "mode"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::WriterLock, + true, + 2_000, + RhiDoctorRemediationCode::ReleaseWriterLock, + &["state_directory_binding", "writer_lock_state"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::SqliteSchema, + true, + 5_000, + RhiDoctorRemediationCode::RepairSchema, + &["metadata_identity", "migration_history", "schema_catalog"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::SqliteIntegrity, + true, + 15_000, + RhiDoctorRemediationCode::RestoreVerifiedState, + &["integrity_check", "foreign_key_check"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::SqliteFreeSpace, + true, + 2_000, + RhiDoctorRemediationCode::FreeStateDiskSpace, + &["state_filesystem_capacity", "minimum_free_bytes"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::IdentityBinding, + true, + 2_000, + RhiDoctorRemediationCode::RestoreIdentityBinding, + &[ + "envelope_contract", + "credential_reference", + "public_identity", + ], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::AdminBindPolicy, + true, + 2_000, + RhiDoctorRemediationCode::CorrectAdminBindPolicy, + &["unix_socket_path", "socket_mode", "peer_authorization"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::OperationsBindPolicy, + true, + 2_000, + RhiDoctorRemediationCode::CorrectOperationsBindPolicy, + &["enabled_posture", "listen_address", "bind_policy"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::NetworkPolicy, + true, + 2_000, + RhiDoctorRemediationCode::CorrectNetworkPolicy, + &["dns_policy", "tls_policy", "relay_url_policy"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::RequiredSources, + true, + 15_000, + RhiDoctorRemediationCode::RestoreRequiredSources, + &[ + "required_source_inventory", + "reachability", + "source_deadline", + ], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::CursorCheckpoint, + true, + 5_000, + RhiDoctorRemediationCode::RepairCursorCheckpoint, + &[ + "selector_binding", + "cursor_plausibility", + "completion_evidence", + ], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::ReconciliationLeases, + true, + 5_000, + RhiDoctorRemediationCode::RepairReconciliationLeases, + &["lease_ownership", "lease_expiry", "retry_state"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::ReconciliationBacklog, + true, + 5_000, + RhiDoctorRemediationCode::ReduceReconciliationBacklog, + &["queue_bound", "attempt_bound", "schedule_plausibility"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::PublicationInvariants, + true, + 5_000, + RhiDoctorRemediationCode::RepairPublicationState, + &["exact_signed_bytes", "target_inventory", "outbox_schedule"], + ), + RhiDoctorCheckDefinition::new( + RhiDoctorCheckId::ClockSkew, + false, + 5_000, + RhiDoctorRemediationCode::CorrectClock, + &["wall_clock_skew"], + ), +]; + +/// Returns the exact ordered doctor inventory. +#[must_use] +pub const fn rhi_doctor_check_definitions() +-> &'static [RhiDoctorCheckDefinition; RHI_DOCTOR_CHECK_COUNT] { + &CHECK_DEFINITIONS +} + +/// A closed result supplied by one bounded check implementation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiDoctorObservation { + Pass, + Fail, + Skipped, +} + +/// Future returned by one doctor probe. +pub type RhiDoctorFuture<'a> = Pin<Box<dyn Future<Output = RhiDoctorObservation> + Send + 'a>>; + +/// Executes each active check without receiving report-construction authority. +/// +/// `Pass` is permitted only after every facet in +/// [`RhiDoctorCheckDefinition::scope`] is proven. Implementations must be +/// cancellation-safe: dropping the future at its deadline must stop work or +/// leave synchronous cleanup owned by that future, never detached mutation. +pub trait RhiDoctorProbe: Send + Sync { + /// Runs one exact check. Raw errors, paths, and arbitrary summaries cannot + /// cross this boundary. + fn probe(&self, definition: RhiDoctorCheckDefinition) -> RhiDoctorFuture<'_>; +} + +/// Stable status of one completed check. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiDoctorCheckStatus { + Pass, + Fail, + Timeout, + Skipped, +} + +impl RhiDoctorCheckStatus { + const fn as_str(self) -> &'static str { + match self { + Self::Pass => "pass", + Self::Fail => "fail", + Self::Timeout => "timeout", + Self::Skipped => "skipped", + } + } + + const fn summary(self) -> &'static str { + match self { + Self::Pass => "check passed", + Self::Fail => "check failed", + Self::Timeout => "check timed out", + Self::Skipped => "optional check skipped", + } + } +} + +/// Stable aggregate doctor status. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiDoctorAggregateStatus { + Pass, + Degraded, + Fail, +} + +impl RhiDoctorAggregateStatus { + const fn as_str(self) -> &'static str { + match self { + Self::Pass => "pass", + Self::Degraded => "degraded", + Self::Fail => "fail", + } + } +} + +/// One sealed structured doctor result. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RhiDoctorCheckResult { + definition: RhiDoctorCheckDefinition, + status: RhiDoctorCheckStatus, +} + +impl RhiDoctorCheckResult { + /// Returns the exact check definition. + #[must_use] + pub const fn definition(self) -> RhiDoctorCheckDefinition { + self.definition + } + + /// Returns the admitted check status. + #[must_use] + pub const fn status(self) -> RhiDoctorCheckStatus { + self.status + } + + /// Returns the fixed content-free summary. + #[must_use] + pub const fn summary(self) -> &'static str { + self.status.summary() + } +} + +/// Stable source-free doctor construction failures. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiDoctorErrorKind { + Encoding, + OutputTooLarge, +} + +impl RhiDoctorErrorKind { + const fn message(self) -> &'static str { + match self { + Self::Encoding => "RHI doctor output encoding failed", + Self::OutputTooLarge => "RHI doctor output exceeds its byte limit", + } + } +} + +/// One redacted doctor construction failure. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct RhiDoctorError { + kind: RhiDoctorErrorKind, +} + +impl RhiDoctorError { + const fn new(kind: RhiDoctorErrorKind) -> Self { + Self { kind } + } + + /// Returns the stable error classification. + #[must_use] + pub const fn kind(self) -> RhiDoctorErrorKind { + self.kind + } +} + +impl fmt::Debug for RhiDoctorError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiDoctorError") + .field("kind", &self.kind) + .finish() + } +} + +impl fmt::Display for RhiDoctorError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.kind.message()) + } +} + +impl Error for RhiDoctorError {} + +/// One immutable, bounded, canonical RHI doctor report. +/// +/// Construction remains inside [`run_rhi_doctor`]: +/// +/// ```compile_fail +/// use rhi::{RhiDoctorAggregateStatus, RhiDoctorReport}; +/// +/// let _ = RhiDoctorReport { +/// instance: todo!(), +/// status: RhiDoctorAggregateStatus::Pass, +/// checks: Box::new([]), +/// canonical_json: Box::new([]), +/// }; +/// ``` +pub struct RhiDoctorReport { + instance: InstanceId, + status: RhiDoctorAggregateStatus, + checks: Box<[RhiDoctorCheckResult]>, + canonical_json: Box<[u8]>, +} + +impl RhiDoctorReport { + /// Returns the fixed service identifier. + #[must_use] + pub const fn service(&self) -> &'static str { + RHI_SERVICE + } + + /// Returns the validated instance identifier admitted into the report. + #[must_use] + pub const fn instance(&self) -> &InstanceId { + &self.instance + } + + /// Returns the aggregate result. + #[must_use] + pub const fn status(&self) -> RhiDoctorAggregateStatus { + self.status + } + + /// Returns the ordered complete check inventory. + #[must_use] + pub fn checks(&self) -> &[RhiDoctorCheckResult] { + &self.checks + } + + /// Returns exact compact UTF-8 JSON in the shared v1 field order. + #[must_use] + pub fn canonical_json(&self) -> &[u8] { + &self.canonical_json + } + + /// Returns exit 6 only when a required check failed or timed out. + #[must_use] + pub const fn exit_code(&self) -> u8 { + match self.status { + RhiDoctorAggregateStatus::Fail => DOCTOR_FAILURE_EXIT_CODE, + RhiDoctorAggregateStatus::Pass | RhiDoctorAggregateStatus::Degraded => 0, + } + } +} + +impl fmt::Debug for RhiDoctorReport { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RhiDoctorReport") + .field("service", &RHI_SERVICE) + .field("instance", &"[redacted]") + .field("status", &self.status) + .field("check_count", &self.checks.len()) + .field("canonical_json", &"[redacted]") + .finish() + } +} + +/// Runs every governed check in exact contract order under its fixed deadline. +/// +/// Probe implementations retain operation-specific filesystem, SQLite, +/// identity, listener, network, source, reconciliation, publication, and clock +/// authority. This orchestrator accepts only a closed result and cannot +/// serialize their paths or raw errors. +pub async fn run_rhi_doctor( + context: &RhiRuntimeContext, + probe: &(impl RhiDoctorProbe + ?Sized), +) -> Result<RhiDoctorReport, RhiDoctorError> { + let mut checks = Vec::with_capacity(RHI_DOCTOR_CHECK_COUNT); + for definition in CHECK_DEFINITIONS { + let status = match tokio::time::timeout( + Duration::from_millis(definition.deadline_ms), + probe.probe(definition), + ) + .await + { + Ok(RhiDoctorObservation::Pass) => RhiDoctorCheckStatus::Pass, + Ok(RhiDoctorObservation::Fail) => RhiDoctorCheckStatus::Fail, + Ok(RhiDoctorObservation::Skipped) if !definition.required => { + RhiDoctorCheckStatus::Skipped + } + Ok(RhiDoctorObservation::Skipped) => RhiDoctorCheckStatus::Fail, + Err(_) => RhiDoctorCheckStatus::Timeout, + }; + checks.push(RhiDoctorCheckResult { definition, status }); + } + let checks = checks.into_boxed_slice(); + let status = aggregate_status(&checks); + let instance = context.context().instance().clone(); + let canonical_json = encode_report(&instance, status, &checks)?; + + Ok(RhiDoctorReport { + instance, + status, + checks, + canonical_json, + }) +} + +fn aggregate_status(checks: &[RhiDoctorCheckResult]) -> RhiDoctorAggregateStatus { + if checks + .iter() + .any(|result| result.definition.required && result.status != RhiDoctorCheckStatus::Pass) + { + RhiDoctorAggregateStatus::Fail + } else if checks + .iter() + .any(|result| result.status != RhiDoctorCheckStatus::Pass) + { + RhiDoctorAggregateStatus::Degraded + } else { + RhiDoctorAggregateStatus::Pass + } +} + +#[derive(Serialize)] +struct DoctorWireReport<'a> { + contract_version: u32, + service: &'static str, + instance: &'a str, + status: &'static str, + checks: Vec<DoctorWireCheck>, +} + +#[derive(Serialize)] +struct DoctorWireCheck { + id: &'static str, + status: &'static str, + required: bool, + deadline_ms: u64, + summary: &'static str, + remediation_code: &'static str, +} + +fn encode_report( + instance: &InstanceId, + status: RhiDoctorAggregateStatus, + checks: &[RhiDoctorCheckResult], +) -> Result<Box<[u8]>, RhiDoctorError> { + let checks = checks + .iter() + .map(|result| DoctorWireCheck { + id: result.definition.id.as_str(), + status: result.status.as_str(), + required: result.definition.required, + deadline_ms: result.definition.deadline_ms, + summary: result.status.summary(), + remediation_code: result.definition.remediation_code.as_str(), + }) + .collect(); + let encoded = serde_json::to_vec(&DoctorWireReport { + contract_version: RHI_DOCTOR_CONTRACT_VERSION, + service: RHI_SERVICE, + instance: instance.as_str(), + status: status.as_str(), + checks, + }) + .map_err(|_| RhiDoctorError::new(RhiDoctorErrorKind::Encoding))?; + if encoded.len() > RHI_DOCTOR_REPORT_MAX_UTF8_BYTES { + return Err(RhiDoctorError::new(RhiDoctorErrorKind::OutputTooLarge)); + } + Ok(encoded.into_boxed_slice()) +} + +#[cfg(test)] +mod tests { + use std::error::Error; + + use super::{RhiDoctorError, RhiDoctorErrorKind}; + + #[test] + fn errors_are_source_free_and_content_free() { + for kind in [ + RhiDoctorErrorKind::Encoding, + RhiDoctorErrorKind::OutputTooLarge, + ] { + let error = RhiDoctorError::new(kind); + assert!(Error::source(&error).is_none()); + let rendered = format!("{error} {error:?}"); + for forbidden in ["/private", "secret", "relay", "sqlite"] { + assert!(!rendered.to_ascii_lowercase().contains(forbidden)); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs @@ -6,11 +6,13 @@ mod adapters; mod admin_v1; mod cli_v1; mod config_v1; +mod doctor_v1; mod features; mod identity_credential; mod identity_envelope; mod presence_desired; mod presence_publication; +mod process_result_v1; mod publication; mod publication_attempt; mod publication_execution; @@ -58,6 +60,13 @@ pub use config_v1::{ RhiConfigV1Error, RhiConfigV1ErrorKind, RhiConfigValueSource, RhiEffectiveConfigV1, RhiRuntimeThreadLimitsV1, parse_rhi_config_v1, }; +pub use doctor_v1::{ + RHI_DOCTOR_CHECK_COUNT, RHI_DOCTOR_CONTRACT_VERSION, RHI_DOCTOR_REPORT_MAX_UTF8_BYTES, + RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES, RhiDoctorAggregateStatus, RhiDoctorCheckDefinition, + RhiDoctorCheckId, RhiDoctorCheckResult, RhiDoctorCheckStatus, RhiDoctorError, + RhiDoctorErrorKind, RhiDoctorFuture, RhiDoctorObservation, RhiDoctorProbe, + RhiDoctorRemediationCode, RhiDoctorReport, rhi_doctor_check_definitions, run_rhi_doctor, +}; pub use features::trade_agreement_attestation::{ RHI_AGREEMENT_ATTESTATION_PROOF_SYSTEM_LOCAL_STATEMENT_HASH, RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID, RHI_AGREEMENT_ATTESTATION_REPORT_VERSION, @@ -96,6 +105,7 @@ pub use presence_publication::{ RhiSignedPresenceDocument, RhiSignedPresenceDocuments, build_rhi_signed_presence_documents, validate_rhi_signed_presence_documents, }; +pub use process_result_v1::RhiProcessResult; pub use publication::{ RHI_PUBLICATION_CONTRACT_VERSION, RHI_PUBLICATION_MAX_ATTEMPTS, RHI_PUBLICATION_MAX_TARGETS, RhiPublicationAuthority, RhiPublicationError, RhiPublicationErrorKind, RhiPublicationMode, diff --git a/src/main.rs b/src/main.rs @@ -4,33 +4,36 @@ use std::path::PathBuf; use std::process::ExitCode; use rhi::{ - RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, parse_rhi_cli_v1_from, - plan_rhi_cli_v1, resolve_rhi_runtime_context, + RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiProcessResult, + parse_rhi_cli_v1_from, plan_rhi_cli_v1, resolve_rhi_runtime_context, }; fn main() -> ExitCode { let invocation = match parse_rhi_cli_v1_from(std::env::args_os()) { Ok(invocation) => invocation, - Err(_) => return ExitCode::FAILURE, + Err(_) => return emit_failure(RhiProcessResult::InputOrConfiguration), }; exit_code_from_run(execute(invocation)) } -fn exit_code_from_run(result: Result<(), ()>) -> ExitCode { +fn exit_code_from_run(result: Result<(), RhiProcessResult>) -> ExitCode { match result { - Ok(()) => ExitCode::SUCCESS, - Err(_) => { - eprintln!("RHI command failed"); - ExitCode::FAILURE - } + Ok(()) => RhiProcessResult::Success.exit_code(), + Err(result) => emit_failure(result), } } -fn execute(invocation: rhi::RhiCliInvocationV1) -> Result<(), ()> { +fn emit_failure(result: RhiProcessResult) -> ExitCode { + eprintln!("RHI command failed: {}", result.code()); + result.exit_code() +} + +fn execute(invocation: rhi::RhiCliInvocationV1) -> Result<(), RhiProcessResult> { let _plan = plan_rhi_cli_v1(&invocation); let resolver = RadrootsPathResolver::new(RadrootsPlatform::current(), host_environment()); - let _context = resolve_rhi_runtime_context(&resolver, &invocation).map_err(|_| ())?; - Err(()) + let _context = resolve_rhi_runtime_context(&resolver, &invocation) + .map_err(|_| RhiProcessResult::InputOrConfiguration)?; + Err(RhiProcessResult::InputOrConfiguration) } fn host_environment() -> RadrootsHostEnvironment { @@ -54,13 +57,16 @@ fn host_environment() -> RadrootsHostEnvironment { #[cfg(test)] mod tests { use super::{execute, exit_code_from_run}; - use rhi::parse_rhi_cli_v1_from; + use rhi::{RhiProcessResult, parse_rhi_cli_v1_from}; use std::process::ExitCode; #[test] fn process_result_is_stable() { assert_eq!(exit_code_from_run(Ok(())), ExitCode::SUCCESS); - assert_eq!(exit_code_from_run(Err(())), ExitCode::FAILURE); + assert_eq!( + exit_code_from_run(Err(RhiProcessResult::UnexpectedInternal)), + ExitCode::FAILURE + ); } #[test] @@ -78,7 +84,10 @@ mod tests { ]) .expect("valid invocation"); - assert_eq!(execute(invocation), Err(())); + assert_eq!( + execute(invocation), + Err(RhiProcessResult::InputOrConfiguration) + ); assert_eq!( std::fs::read_dir(root.path()).expect("read root").count(), 0 diff --git a/src/process_result_v1.rs b/src/process_result_v1.rs @@ -0,0 +1,51 @@ +//! Closed process-result and exit-code contract. + +use std::process::ExitCode; + +/// Exact stable RHI process result and exit-code inventory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RhiProcessResult { + Success, + UnexpectedInternal, + InputOrConfiguration, + ServiceOrDependencyUnavailable, + StateOrIdentityUnavailable, + OperationRejectedOrConflict, + DoctorRequiredCheckFailed, +} + +impl RhiProcessResult { + /// Returns the exact stable process exit code. + #[must_use] + pub const fn exit_code_u8(self) -> u8 { + match self { + Self::Success => 0, + Self::UnexpectedInternal => 1, + Self::InputOrConfiguration => 2, + Self::ServiceOrDependencyUnavailable => 3, + Self::StateOrIdentityUnavailable => 4, + Self::OperationRejectedOrConflict => 5, + Self::DoctorRequiredCheckFailed => 6, + } + } + + /// Returns the stable machine code. + #[must_use] + pub const fn code(self) -> &'static str { + match self { + Self::Success => "success", + Self::UnexpectedInternal => "unexpected_internal", + Self::InputOrConfiguration => "input_or_configuration", + Self::ServiceOrDependencyUnavailable => "service_or_dependency_unavailable", + Self::StateOrIdentityUnavailable => "state_or_identity_unavailable", + Self::OperationRejectedOrConflict => "operation_rejected_or_conflict", + Self::DoctorRequiredCheckFailed => "doctor_required_check_failed", + } + } + + /// Returns the standard-library process exit value. + #[must_use] + pub fn exit_code(self) -> ExitCode { + ExitCode::from(self.exit_code_u8()) + } +} diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -4,7 +4,12 @@ const MANIFEST: &str = include_str!("../Cargo.toml"); const README: &str = include_str!("../README"); const AGENTS: &str = include_str!("../AGENTS.md"); const ROOT: &str = include_str!("../src/lib.rs"); +const MAIN: &str = include_str!("../src/main.rs"); const ADMIN: &str = include_str!("../src/admin_v1.rs"); +const DOCTOR: &str = include_str!("../src/doctor_v1.rs"); +const PROCESS_RESULT: &str = include_str!("../src/process_result_v1.rs"); +const OPERATOR_CONTRACT: &str = + include_str!("../contracts/services_hardening/operator_contract.v1.json"); const ADMIN_IDENTITY_OFFLINE_CONTRACT: &str = include_str!("../contracts/services_hardening/admin_identity_offline.v1.json"); const ADMIN_WAVE_QUALIFICATION_CONTRACT: &str = @@ -78,11 +83,13 @@ const SOURCES: &[&str] = &[ include_str!("../src/admin_v1.rs"), include_str!("../src/cli_v1.rs"), include_str!("../src/config_v1.rs"), + include_str!("../src/doctor_v1.rs"), include_str!("../src/features/trade_agreement_attestation.rs"), include_str!("../src/identity_credential.rs"), include_str!("../src/identity_envelope.rs"), include_str!("../src/presence_desired.rs"), include_str!("../src/presence_publication.rs"), + include_str!("../src/process_result_v1.rs"), include_str!("../src/publication.rs"), include_str!("../src/publication_attempt.rs"), include_str!("../src/publication_execution.rs"), @@ -160,11 +167,13 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { "admin_v1", "cli_v1", "config_v1", + "doctor_v1", "features", "identity_credential", "identity_envelope", "presence_desired", "presence_publication", + "process_result_v1", "publication", "publication_attempt", "publication_execution", @@ -247,6 +256,14 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { "RhiCliAdminOperationV1", "RhiCliExecutionPlanV1", "plan_rhi_cli_v1", + "RhiDoctorCheckId", + "RhiDoctorCheckDefinition", + "RhiDoctorCheckResult", + "RhiDoctorReport", + "RhiDoctorProbe", + "run_rhi_doctor", + "rhi_doctor_check_definitions", + "RhiProcessResult", "RhiPublicationErrorKind", "RhiPublicationMode", "RhiPublicationRetryPolicy", @@ -375,6 +392,77 @@ fn state_catalog_module_is_private_and_root_api_is_curated() { assert!(!PUBLIC_API.contains("rhi::presence_publication::")); assert!(!PUBLIC_API.contains("rhi::admin_v1::")); assert!(!PUBLIC_API.contains("rhi::cli_v1::")); + assert!(!PUBLIC_API.contains("rhi::doctor_v1::")); + assert!(!PUBLIC_API.contains("rhi::process_result_v1::")); +} + +#[test] +fn doctor_and_process_results_are_closed_bounded_and_process_safe() { + let contract: serde_json::Value = + serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract"); + assert_eq!(contract["doctor"]["contract_version"], 1); + assert_eq!(contract["doctor"]["execution"], "ordered"); + assert_eq!(contract["doctor"]["checks"].as_array().unwrap().len(), 15); + assert_eq!(contract["doctor"]["report_max_utf8_bytes"], 8_192); + assert_eq!(contract["doctor"]["required_fail_or_timeout_exit"], 6); + assert_eq!(contract["doctor"]["detached_probe_work"], false); + assert_eq!(contract["exit_codes"].as_array().unwrap().len(), 7); + + for required in [ + "pub const RHI_DOCTOR_CHECK_COUNT: usize = 15", + "pub const RHI_DOCTOR_REPORT_MAX_UTF8_BYTES: usize = 8_192", + "tokio::time::timeout(", + "probe.probe(definition)", + "Ok(RhiDoctorObservation::Skipped) if !definition.required", + "RhiDoctorCheckStatus::Timeout", + "canonical_json: Box<[u8]>", + ".field(\"canonical_json\", &\"[redacted]\")", + ] { + assert!(DOCTOR.contains(required), "doctor is missing {required}"); + } + for forbidden in [ + "std::fs", + "std::net", + "sqlx::", + "tokio::spawn", + "thread::spawn", + "SystemTime", + "serde_json::Value", + "pub fn into_inner", + ] { + assert!( + !DOCTOR.contains(forbidden), + "doctor orchestrator gained forbidden authority {forbidden}" + ); + } + for required in [ + "pub enum RhiProcessResult", + "Self::Success => 0", + "Self::DoctorRequiredCheckFailed => 6", + "pub const fn code(self) -> &'static str", + ] { + assert!( + PROCESS_RESULT.contains(required), + "process result is missing {required}" + ); + } + assert!(MAIN.contains("parse_rhi_cli_v1_from(std::env::args_os())")); + assert!(MAIN.contains("RhiProcessResult::InputOrConfiguration")); + assert!(MAIN.contains("eprintln!(\"RHI command failed: {}\", result.code())")); + for forbidden in ["{error}", "{error:?}", "process::exit", "tokio::runtime"] { + assert!( + !MAIN.contains(forbidden), + "binary exposes forbidden process behavior {forbidden}" + ); + } + assert!( + MANIFEST.contains( + "tokio = { version = \"1\", default-features = false, features = [\"time\"] }" + ) + ); + assert!(MANIFEST.contains( + "serde = { version = \"1\", default-features = false, features = [\"derive\"] }" + )); } #[test] @@ -1035,7 +1123,7 @@ fn public_errors_are_crate_owned_redacted_and_source_free() { .lines() .filter(|line| line.starts_with("pub struct rhi::") && line.ends_with("Error")) .count(); - assert_eq!(public_error_count, 35); + assert_eq!(public_error_count, 36); } #[test] @@ -1530,6 +1618,10 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() { "[`admin_domain.v1.json`](contracts/services_hardening/admin_domain.v1.json)", "[`admin_identity_offline.v1.json`](contracts/services_hardening/admin_identity_offline.v1.json)", "[`admin_wave_qualification.v1.json`](contracts/services_hardening/admin_wave_qualification.v1.json)", + "## Bounded active doctor and stable process results", + "governed fifteen checks in exact contract order", + "Required failure or timeout returns exit code `6`", + "no\ndetached probe work is permitted", ] { assert!(README.contains(required), "README is missing {required}"); } @@ -1558,6 +1650,7 @@ fn readme_freezes_the_root_only_boundary_and_exact_baseline() { "Build service-profile and application-handler presence only through the", "Preserve the\n caller's sealed exact-byte capability", "Recover an expired stale", + "Step 211 freezes the exact fifteen-check doctor inventory", ] { assert!(AGENTS.contains(required), "AGENTS is missing {required}"); } diff --git a/tests/services_hardening_doctor.rs b/tests/services_hardening_doctor.rs @@ -0,0 +1,364 @@ +#![forbid(unsafe_code)] + +use std::{ + collections::{BTreeMap, BTreeSet}, + future::pending, + process::Command, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, +}; + +use rhi::{ + RHI_DOCTOR_CHECK_COUNT, RHI_DOCTOR_CONTRACT_VERSION, RHI_DOCTOR_REPORT_MAX_UTF8_BYTES, + RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES, RadrootsHostEnvironment, RadrootsPathResolver, + RadrootsPlatform, RhiDoctorAggregateStatus, RhiDoctorCheckDefinition, RhiDoctorCheckId, + RhiDoctorCheckStatus, RhiDoctorFuture, RhiDoctorObservation, RhiDoctorProbe, + RhiDoctorRemediationCode, RhiProcessResult, parse_rhi_cli_v1_from, resolve_rhi_runtime_context, + rhi_doctor_check_definitions, run_rhi_doctor, +}; +use sha2::{Digest, Sha256}; + +const OPERATOR_CONTRACT: &str = + include_str!("../contracts/services_hardening/operator_contract.v1.json"); + +struct PendingGuard(Arc<AtomicBool>); + +impl Drop for PendingGuard { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } +} + +struct TestProbe { + outcomes: BTreeMap<RhiDoctorCheckId, RhiDoctorObservation>, + pending: Option<RhiDoctorCheckId>, + calls: Arc<Mutex<Vec<RhiDoctorCheckId>>>, + pending_dropped: Arc<AtomicBool>, +} + +impl TestProbe { + fn all(outcome: RhiDoctorObservation) -> Self { + Self { + outcomes: rhi_doctor_check_definitions() + .iter() + .map(|definition| (definition.id(), outcome)) + .collect(), + pending: None, + calls: Arc::new(Mutex::new(Vec::new())), + pending_dropped: Arc::new(AtomicBool::new(false)), + } + } + + fn with(mut self, id: RhiDoctorCheckId, outcome: RhiDoctorObservation) -> Self { + self.outcomes.insert(id, outcome); + self + } + + fn pending(mut self, id: RhiDoctorCheckId) -> Self { + self.pending = Some(id); + self + } +} + +impl RhiDoctorProbe for TestProbe { + fn probe(&self, definition: RhiDoctorCheckDefinition) -> RhiDoctorFuture<'_> { + let id = definition.id(); + self.calls.lock().expect("calls lock").push(id); + if self.pending == Some(id) { + let dropped = Arc::clone(&self.pending_dropped); + return Box::pin(async move { + let _guard = PendingGuard(dropped); + pending().await + }); + } + let outcome = self.outcomes[&id]; + Box::pin(async move { outcome }) + } +} + +fn runtime() -> (tempfile::TempDir, rhi::RhiRuntimeContext) { + let directory = tempfile::tempdir().expect("temporary root"); + let invocation = parse_rhi_cli_v1_from([ + "rhi", + "--profile", + "repo-local", + "--instance", + "primary", + "--repo-local-root", + directory.path().to_str().expect("UTF-8 path"), + "doctor", + ]) + .expect("doctor invocation"); + let context = resolve_rhi_runtime_context( + &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), + &invocation, + ) + .expect("runtime context"); + (directory, context) +} + +#[test] +fn exact_inventory_and_exit_meanings_match_the_operator_contract() { + let contract: serde_json::Value = + serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract"); + let doctor = contract["doctor"].as_object().expect("doctor"); + assert_eq!( + doctor.keys().map(String::as_str).collect::<BTreeSet<_>>(), + BTreeSet::from([ + "aggregate_statuses", + "checks", + "contract_version", + "detached_probe_work", + "execution", + "pass_requires_all_scope", + "probe_future_cancellation", + "raw_error_or_path_allowed", + "report_max_utf8_bytes", + "required_fail_or_timeout_exit", + "required_skipped", + "shared_schema", + "statuses", + "summary_max_utf8_bytes", + ]) + ); + assert_eq!(RHI_DOCTOR_CONTRACT_VERSION, 1); + assert_eq!(RHI_DOCTOR_CHECK_COUNT, 15); + assert_eq!(RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES, 256); + assert_eq!(RHI_DOCTOR_REPORT_MAX_UTF8_BYTES, 8_192); + assert_eq!(doctor["execution"], "ordered"); + assert_eq!(doctor["pass_requires_all_scope"], true); + assert_eq!( + doctor["probe_future_cancellation"], + "drop_stops_or_owns_cleanup" + ); + assert_eq!(doctor["detached_probe_work"], false); + assert_eq!(doctor["required_skipped"], "forbidden"); + assert_eq!(doctor["raw_error_or_path_allowed"], false); + assert_eq!(doctor["required_fail_or_timeout_exit"], 6); + + let rows = doctor["checks"].as_array().expect("checks"); + assert_eq!(rows.len(), RHI_DOCTOR_CHECK_COUNT); + for (definition, row) in rhi_doctor_check_definitions().iter().zip(rows) { + assert_eq!(row["id"], id_name(definition.id())); + assert_eq!(row["required"], definition.required()); + assert_eq!(row["deadline_ms"], definition.deadline_ms()); + assert_eq!( + row["remediation_code"], + remediation_name(definition.remediation_code()) + ); + assert_eq!( + row["scope"], + serde_json::to_value(definition.scope()).expect("scope") + ); + } + + let process_results = [ + RhiProcessResult::Success, + RhiProcessResult::UnexpectedInternal, + RhiProcessResult::InputOrConfiguration, + RhiProcessResult::ServiceOrDependencyUnavailable, + RhiProcessResult::StateOrIdentityUnavailable, + RhiProcessResult::OperationRejectedOrConflict, + RhiProcessResult::DoctorRequiredCheckFailed, + ]; + for (result, row) in process_results + .into_iter() + .zip(contract["exit_codes"].as_array().expect("exit codes")) + { + assert_eq!(row["code"], result.exit_code_u8()); + assert_eq!(row["name"], result.code()); + } +} + +#[tokio::test] +async fn all_pass_is_canonical_bounded_and_exit_zero() { + let (_directory, context) = runtime(); + let probe = TestProbe::all(RhiDoctorObservation::Pass); + let report = run_rhi_doctor(&context, &probe).await.expect("report"); + assert_eq!(report.service(), "rhi"); + assert_eq!(report.instance().as_str(), "primary"); + assert_eq!(report.status(), RhiDoctorAggregateStatus::Pass); + assert_eq!(report.exit_code(), 0); + assert_eq!(report.checks().len(), RHI_DOCTOR_CHECK_COUNT); + assert!( + report + .checks() + .iter() + .all(|result| result.status() == RhiDoctorCheckStatus::Pass) + ); + assert_eq!( + probe.calls.lock().expect("calls").len(), + RHI_DOCTOR_CHECK_COUNT + ); + + let bytes = report.canonical_json(); + assert!(bytes.len() <= RHI_DOCTOR_REPORT_MAX_UTF8_BYTES); + assert!(!bytes.contains(&b'\n')); + let wire: serde_json::Value = serde_json::from_slice(bytes).expect("JSON"); + assert_eq!(wire["contract_version"], 1); + assert_eq!(wire["service"], "rhi"); + assert_eq!(wire["instance"], "primary"); + assert_eq!(wire["status"], "pass"); + assert_eq!(wire["checks"].as_array().expect("checks").len(), 15); + assert!(String::from_utf8_lossy(bytes).starts_with( + "{\"contract_version\":1,\"service\":\"rhi\",\"instance\":\"primary\",\"status\":\"pass\",\"checks\":[" + )); + assert_eq!( + sha256_hex(bytes), + "5aabfc84927e36bb877c289f8d0ed2be8f4c3115deedad7def3f3e56daab399b" + ); +} + +#[tokio::test] +async fn optional_nonpass_is_degraded_while_required_nonpass_fails() { + let (_directory, context) = runtime(); + for outcome in [RhiDoctorObservation::Fail, RhiDoctorObservation::Skipped] { + let optional = + TestProbe::all(RhiDoctorObservation::Pass).with(RhiDoctorCheckId::ClockSkew, outcome); + let report = run_rhi_doctor(&context, &optional) + .await + .expect("optional report"); + assert_eq!(report.status(), RhiDoctorAggregateStatus::Degraded); + assert_eq!(report.exit_code(), 0); + assert_ne!(report.checks()[14].status(), RhiDoctorCheckStatus::Pass); + + let required = + TestProbe::all(RhiDoctorObservation::Pass).with(RhiDoctorCheckId::WriterLock, outcome); + let report = run_rhi_doctor(&context, &required) + .await + .expect("required report"); + assert_eq!(report.status(), RhiDoctorAggregateStatus::Fail); + assert_eq!(report.exit_code(), 6); + assert_eq!(report.checks()[1].status(), RhiDoctorCheckStatus::Fail); + } +} + +#[tokio::test] +async fn required_timeout_drops_work_and_remaining_checks_continue_in_order() { + let (_directory, context) = runtime(); + let probe = + TestProbe::all(RhiDoctorObservation::Pass).pending(RhiDoctorCheckId::PathsPermissions); + let report = run_rhi_doctor(&context, &probe).await.expect("report"); + assert_eq!(report.status(), RhiDoctorAggregateStatus::Fail); + assert_eq!(report.exit_code(), 6); + assert_eq!(report.checks()[0].status(), RhiDoctorCheckStatus::Timeout); + assert!(probe.pending_dropped.load(Ordering::SeqCst)); + assert_eq!( + *probe.calls.lock().expect("calls"), + rhi_doctor_check_definitions() + .iter() + .map(|definition| definition.id()) + .collect::<Vec<_>>() + ); +} + +#[tokio::test] +async fn report_debug_and_public_errors_retain_no_sensitive_values() { + let directory = tempfile::tempdir().expect("temporary root"); + let root = directory.path().join("secret-root"); + let invocation = parse_rhi_cli_v1_from([ + "rhi", + "--profile", + "repo-local", + "--instance", + "secret-instance", + "--repo-local-root", + root.to_str().expect("UTF-8 path"), + "doctor", + ]) + .expect("doctor invocation"); + let context = resolve_rhi_runtime_context( + &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), + &invocation, + ) + .expect("runtime context"); + let report = run_rhi_doctor(&context, &TestProbe::all(RhiDoctorObservation::Pass)) + .await + .expect("report"); + let debug = format!("{report:?}"); + assert!(!debug.contains("secret-instance")); + assert!(!debug.contains("secret-root")); + for result in report.checks() { + assert!(result.summary().len() <= RHI_DOCTOR_SUMMARY_MAX_UTF8_BYTES); + } + let rendered = format!("{:?}", rhi::RhiDoctorErrorKind::OutputTooLarge); + assert!(!rendered.contains("secret")); +} + +#[test] +fn binary_uses_stable_safe_nonzero_results() { + let canary = "secret-canary-private-key-path-sql-relay-url"; + let invalid = Command::new(env!("CARGO_BIN_EXE_rhi")) + .arg(format!("--credential={canary}")) + .output() + .expect("invalid invocation"); + assert_eq!(invalid.status.code(), Some(2)); + assert!(invalid.stdout.is_empty()); + let stderr = String::from_utf8(invalid.stderr).expect("invalid stderr"); + assert_eq!(stderr, "RHI command failed: input_or_configuration\n"); + assert!(!stderr.contains(canary)); + + let repo_local = tempfile::tempdir().expect("repo-local root"); + let admitted = Command::new(env!("CARGO_BIN_EXE_rhi")) + .args(["--profile", "repo-local", "--instance", "primary"]) + .arg("--repo-local-root") + .arg(repo_local.path()) + .arg("run") + .output() + .expect("admitted invocation"); + assert_eq!(admitted.status.code(), Some(2)); + assert!(admitted.stdout.is_empty()); + assert_eq!( + String::from_utf8(admitted.stderr).expect("admitted stderr"), + "RHI command failed: input_or_configuration\n" + ); +} + +fn sha256_hex(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn id_name(id: RhiDoctorCheckId) -> &'static str { + match id { + RhiDoctorCheckId::PathsPermissions => "paths_permissions", + RhiDoctorCheckId::WriterLock => "writer_lock", + RhiDoctorCheckId::SqliteSchema => "sqlite_schema", + RhiDoctorCheckId::SqliteIntegrity => "sqlite_integrity", + RhiDoctorCheckId::SqliteFreeSpace => "sqlite_free_space", + RhiDoctorCheckId::IdentityBinding => "identity_binding", + RhiDoctorCheckId::AdminBindPolicy => "admin_bind_policy", + RhiDoctorCheckId::OperationsBindPolicy => "operations_bind_policy", + RhiDoctorCheckId::NetworkPolicy => "network_policy", + RhiDoctorCheckId::RequiredSources => "required_sources", + RhiDoctorCheckId::CursorCheckpoint => "cursor_checkpoint", + RhiDoctorCheckId::ReconciliationLeases => "reconciliation_leases", + RhiDoctorCheckId::ReconciliationBacklog => "reconciliation_backlog", + RhiDoctorCheckId::PublicationInvariants => "publication_invariants", + RhiDoctorCheckId::ClockSkew => "clock_skew", + } +} + +fn remediation_name(code: RhiDoctorRemediationCode) -> &'static str { + match code { + RhiDoctorRemediationCode::CorrectPathPolicy => "correct_path_policy", + RhiDoctorRemediationCode::ReleaseWriterLock => "release_writer_lock", + RhiDoctorRemediationCode::RepairSchema => "repair_schema", + RhiDoctorRemediationCode::RestoreVerifiedState => "restore_verified_state", + RhiDoctorRemediationCode::FreeStateDiskSpace => "free_state_disk_space", + RhiDoctorRemediationCode::RestoreIdentityBinding => "restore_identity_binding", + RhiDoctorRemediationCode::CorrectAdminBindPolicy => "correct_admin_bind_policy", + RhiDoctorRemediationCode::CorrectOperationsBindPolicy => "correct_operations_bind_policy", + RhiDoctorRemediationCode::CorrectNetworkPolicy => "correct_network_policy", + RhiDoctorRemediationCode::RestoreRequiredSources => "restore_required_sources", + RhiDoctorRemediationCode::RepairCursorCheckpoint => "repair_cursor_checkpoint", + RhiDoctorRemediationCode::RepairReconciliationLeases => "repair_reconciliation_leases", + RhiDoctorRemediationCode::ReduceReconciliationBacklog => "reduce_reconciliation_backlog", + RhiDoctorRemediationCode::RepairPublicationState => "repair_publication_state", + RhiDoctorRemediationCode::CorrectClock => "correct_clock", + } +} diff --git a/tests/services_hardening_operator_contract.rs b/tests/services_hardening_operator_contract.rs @@ -309,28 +309,18 @@ fn admin_inventory_is_closed_unique_and_model_complete() { fn doctor_exit_and_tcp_contracts_are_exact() { let value = contract(); assert_eq!( - value["doctor"], - serde_json::json!({ - "shared_schema": "radroots.service.doctor.v1", - "contract_version": 1, - "checks": [ - { "id": "paths_permissions", "required": true }, - { "id": "writer_lock", "required": true }, - { "id": "sqlite_schema", "required": true }, - { "id": "sqlite_integrity", "required": true }, - { "id": "sqlite_free_space", "required": true }, - { "id": "identity_binding", "required": true }, - { "id": "admin_bind_policy", "required": true }, - { "id": "operations_bind_policy", "required": true }, - { "id": "network_policy", "required": true }, - { "id": "required_sources", "required": true }, - { "id": "cursor_checkpoint", "required": true }, - { "id": "reconciliation_leases", "required": true }, - { "id": "reconciliation_backlog", "required": true }, - { "id": "publication_invariants", "required": true }, - { "id": "clock_skew", "required": false } - ] - }) + value["doctor"]["shared_schema"], + "radroots.service.doctor.v1" + ); + assert_eq!(value["doctor"]["contract_version"], 1); + assert_eq!(value["doctor"]["execution"], "ordered"); + assert_eq!(value["doctor"]["pass_requires_all_scope"], true); + assert_eq!( + value["doctor"]["checks"] + .as_array() + .expect("doctor checks") + .len(), + 15 ); assert_eq!( value["exit_codes"],