myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit 294ee8941f3eab74488f1c7c8144f4e5bfa28a50
parent ad591fe0961202521118124fa583cd2c626ab500
Author: triesap <tyson@radroots.org>
Date:   Sat, 22 Aug 2026 07:17:09 +0000

feat(myc): own critical task supervision

Diffstat:
MAGENTS.md | 4++++
MREADME | 8+++++++-
Mcontracts/api_baselines/myc.txt | 48++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/services_hardening/runtime_supervision.v1.json | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 6++++++
Asrc/runtime_supervision.rs | 288+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/package_boundary.rs | 98++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atests/services_hardening_runtime_supervision.rs | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 686 insertions(+), 2 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -202,6 +202,10 @@ failpoints. Supervise and join every authoritative task; panic, error, or unexpected successful return from a critical task must coordinate shutdown and produce a nonzero process result. +- Keep the Myc critical-task graph bounded and sealed. A task receives only its + cooperative cancellation observer; callers cannot name tasks, extract task + handles, detach work, install signals, or select process exits through this + library boundary. Step 159 owns signal and forced-shutdown composition. ## 6. Admission, commit, and publication invariants diff --git a/README b/README @@ -117,7 +117,13 @@ credential, secret, or decrypted content. Every public Myc error remains a crate-owned source-free classification, so ordinary `Display`, `Debug`, and whole-chain traversal cannot bypass redaction. Current binary dispatch reports invalid input as exit 2 and the intentionally unavailable later executor as -exit 3; Steps 158 and 159 own real daemon/task/signal execution. +exit 3. The Myc runtime now owns one sealed, bounded critical-task graph over +the shared supervisor: task error, panic, join failure, unexpected cancellation, +or success before cancellation cancels peers, joins every task, and maps to the +fixed unexpected-internal process result. Tasks receive only a cooperative +cancellation observer; task names and handles remain internal. Step 159 owns +the process panic hook, signal installation, externally requested and forced +shutdown, the configured grace deadline, and durability-aware ordered drain. `parse_myc_config_v1` caps original bytes before decoding, checks the schema header before closed contract admission, rejects duplicate, null, unknown, and diff --git a/contracts/api_baselines/myc.txt b/contracts/api_baselines/myc.txt @@ -630,6 +630,17 @@ pub myc::MycRuntimeReadinessReason::SignerProviderUnavailable pub myc::MycRuntimeReadinessReason::SubscriberNotActive impl myc::MycRuntimeReadinessReason pub const fn myc::MycRuntimeReadinessReason::as_str(self) -> &'static str +pub enum myc::MycRuntimeSupervisionErrorKind +pub myc::MycRuntimeSupervisionErrorKind::EmptyTaskSet +pub myc::MycRuntimeSupervisionErrorKind::JoinFailed +pub myc::MycRuntimeSupervisionErrorKind::TaskPanicked +pub myc::MycRuntimeSupervisionErrorKind::TaskRegistration +pub myc::MycRuntimeSupervisionErrorKind::TaskReturnedError +pub myc::MycRuntimeSupervisionErrorKind::TooManyTasks +pub myc::MycRuntimeSupervisionErrorKind::UnexpectedCancellation +pub myc::MycRuntimeSupervisionErrorKind::UnexpectedCompletion +impl myc::MycRuntimeSupervisionErrorKind +pub const fn myc::MycRuntimeSupervisionErrorKind::code(self) -> &'static str pub enum myc::MycServicePhase pub myc::MycServicePhase::Degraded pub myc::MycServicePhase::Failed @@ -1003,6 +1014,17 @@ impl core::fmt::Debug for myc::MycCredentialResolutionError pub fn myc::MycCredentialResolutionError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::fmt::Display for myc::MycCredentialResolutionError pub fn myc::MycCredentialResolutionError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct myc::MycCriticalTask +impl myc::MycCriticalTask +pub fn myc::MycCriticalTask::new<F, Fut>(F) -> Self where F: core::ops::function::FnOnce(myc::MycTaskCancellation) -> Fut + core::marker::Send + 'static, Fut: core::future::future::Future<Output = core::result::Result<(), myc::MycCriticalTaskError>> + core::marker::Send + 'static +impl core::fmt::Debug for myc::MycCriticalTask +pub fn myc::MycCriticalTask::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct myc::MycCriticalTaskError +impl myc::MycCriticalTaskError +pub const fn myc::MycCriticalTaskError::failed() -> Self +impl core::error::Error for myc::MycCriticalTaskError +impl core::fmt::Display for myc::MycCriticalTaskError +pub fn myc::MycCriticalTaskError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct myc::MycDecryptedIdentity impl myc::MycDecryptedIdentity pub fn myc::MycDecryptedIdentity::public_identity(&self) -> &myc::MycProviderPublicIdentity @@ -1694,6 +1716,17 @@ pub fn myc::MycRuntimeReadiness::required(&self) -> &[myc::MycRuntimePrerequisit pub fn myc::MycRuntimeReadiness::satisfied(&self) -> &[myc::MycRuntimePrerequisite] impl core::fmt::Debug for myc::MycRuntimeReadiness pub fn myc::MycRuntimeReadiness::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct myc::MycRuntimeSupervisionError +impl myc::MycRuntimeSupervisionError +pub const fn myc::MycRuntimeSupervisionError::code(self) -> &'static str +pub const fn myc::MycRuntimeSupervisionError::diagnostic(self) -> myc::MycLogRecord +pub const fn myc::MycRuntimeSupervisionError::kind(self) -> myc::MycRuntimeSupervisionErrorKind +pub const fn myc::MycRuntimeSupervisionError::process_result(self) -> myc::MycProcessResult +impl core::error::Error for myc::MycRuntimeSupervisionError +impl core::fmt::Debug for myc::MycRuntimeSupervisionError +pub fn myc::MycRuntimeSupervisionError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for myc::MycRuntimeSupervisionError +pub fn myc::MycRuntimeSupervisionError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct myc::MycSignerCorrelationId(_) impl myc::MycSignerCorrelationId pub const fn myc::MycSignerCorrelationId::as_bytes(&self) -> &[u8; 32] @@ -1906,6 +1939,19 @@ pub struct myc::MycStatusUnixSeconds(_) impl myc::MycStatusUnixSeconds pub const fn myc::MycStatusUnixSeconds::get(self) -> u64 pub fn myc::MycStatusUnixSeconds::new(u64) -> core::result::Result<Self, myc::MycStatusError> +pub struct myc::MycSupervisedRuntime +impl myc::MycSupervisedRuntime +pub fn myc::MycSupervisedRuntime::new(impl core::iter::traits::collect::IntoIterator<Item = myc::MycCriticalTask>) -> core::result::Result<Self, myc::MycRuntimeSupervisionError> +pub async fn myc::MycSupervisedRuntime::run(self) -> core::result::Result<(), myc::MycRuntimeSupervisionError> +pub fn myc::MycSupervisedRuntime::task_count(&self) -> usize +impl core::fmt::Debug for myc::MycSupervisedRuntime +pub fn myc::MycSupervisedRuntime::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct myc::MycTaskCancellation +impl myc::MycTaskCancellation +pub async fn myc::MycTaskCancellation::cancelled(&self) +pub fn myc::MycTaskCancellation::is_cancelled(&self) -> bool +impl core::fmt::Debug for myc::MycTaskCancellation +pub fn myc::MycTaskCancellation::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct myc::MycUntrustedProviderOutput(_) impl myc::MycUntrustedProviderOutput pub fn myc::MycUntrustedProviderOutput::as_bytes(&self) -> &[u8] @@ -1963,6 +2009,7 @@ pub const myc::MYC_CONFIG_DOCUMENT_MAX_UTF8_BYTES: usize pub const myc::MYC_CONFIG_SCHEMA: &str pub const myc::MYC_CONFIG_SCHEMA_VERSION: u32 pub const myc::MYC_CONNECTION_PERMISSION_MAX_COUNT: usize +pub const myc::MYC_CRITICAL_TASK_MAX_COUNT: usize pub const myc::MYC_DELIVERY_ATTEMPT_MAX_COUNT: u32 pub const myc::MYC_DELIVERY_RECOVERY_BATCH_MAX_COUNT: usize pub const myc::MYC_DELIVERY_RELAY_ID_MAX_BYTES: usize @@ -2007,6 +2054,7 @@ pub const myc::MYC_RATE_RETENTION_MAX_MS: u64 pub const myc::MYC_RATE_WINDOW_MAX_MS: u64 pub const myc::MYC_READYZ_PATH: &str pub const myc::MYC_RUNTIME_FOUNDATION_CONTRACT_VERSION: u32 +pub const myc::MYC_RUNTIME_SUPERVISION_CONTRACT_VERSION: u32 pub const myc::MYC_SIGNER_STATUS_CONTRACT_VERSION: u32 pub const myc::MYC_STATE_APPLICATION_ID: u32 pub const myc::MYC_STATE_BASE_SCHEMA_VERSION: u32 diff --git a/contracts/services_hardening/runtime_supervision.v1.json b/contracts/services_hardening/runtime_supervision.v1.json @@ -0,0 +1,56 @@ +{ + "schema": "radroots.myc.runtime-supervision.v1", + "contract_version": 1, + "step": 158, + "task_set": { + "minimum_count": 1, + "maximum_count": 32, + "classification": "critical", + "shutdown_phase_assignment": "deferred_to_step_159", + "names": "internal_bounded_ordinals", + "caller_named_tasks": false, + "task_handles_exposed": false, + "detached_tasks": false + }, + "task_boundary": { + "input": "cooperative_cancellation_observer_only", + "output": "success_or_source_free_failure", + "raw_service_host_types_exposed": false, + "raw_join_error_exposed": false + }, + "fatal_outcomes": [ + "task_returned_error", + "task_panicked", + "unexpected_completion", + "unexpected_cancellation", + "join_failed" + ], + "fatal_effect": { + "peer_cancellation": "coordinated", + "all_task_joins_observed_before_return": true, + "process_result": "unexpected_internal", + "diagnostic": "critical_task_failed" + }, + "resource_policy": { + "iterator_ingestion": "maximum_plus_one", + "unbounded_queue": false, + "runtime_creation": false, + "ambient_time": false, + "ambient_entropy": false + }, + "deferred": [ + "process_panic_hook", + "signal_installation", + "first_signal_graceful_shutdown", + "second_signal_forced_shutdown", + "shutdown_grace_deadline", + "ordered_durability_drain" + ], + "nonclaims": [ + "concrete_relay_task_implementation", + "provider_execution", + "rcld_promotion", + "nix", + "oci" + ] +} diff --git a/src/lib.rs b/src/lib.rs @@ -26,6 +26,7 @@ mod provider_local_signer; mod provider_verification; mod runtime_context; mod runtime_foundation; +mod runtime_supervision; mod state_catalog; mod state_completion; mod state_connection; @@ -141,6 +142,11 @@ pub use runtime_foundation::{ MycRuntimeFoundationErrorKind, MycRuntimePrerequisite, MycRuntimeReadiness, MycRuntimeReadinessReason, open_myc_runtime_foundation, }; +pub use runtime_supervision::{ + MYC_CRITICAL_TASK_MAX_COUNT, MYC_RUNTIME_SUPERVISION_CONTRACT_VERSION, MycCriticalTask, + MycCriticalTaskError, MycRuntimeSupervisionError, MycRuntimeSupervisionErrorKind, + MycSupervisedRuntime, MycTaskCancellation, +}; pub use state_catalog::{ MYC_MIGRATION_CATALOG_SHA256, MYC_STATE_BASE_SCHEMA_VERSION, MYC_STATE_SCHEMA_CATALOG_SHA256, MYC_STATE_SCHEMA_VERSION, MYC_STATE_SCHEMA_VERSION_1_OBJECT_COUNT, diff --git a/src/runtime_supervision.rs b/src/runtime_supervision.rs @@ -0,0 +1,288 @@ +//! Sealed, bounded Myc critical-task supervision over the shared host runner. + +use core::{fmt, future::Future, pin::Pin}; +use std::error::Error; + +use radroots_service_host::{ + CancellationToken, HostError, HostErrorKind, ShutdownPhase, SupervisionFailureKind, + TaskClassification, TaskMetadata, TaskName, TaskSupervisor, +}; + +use crate::{MycLogRecord, MycProcessResult}; + +/// Exact Myc runtime-supervision contract version. +pub const MYC_RUNTIME_SUPERVISION_CONTRACT_VERSION: u32 = 1; +/// Maximum number of critical tasks admitted into one Myc runtime graph. +pub const MYC_CRITICAL_TASK_MAX_COUNT: usize = 32; + +type CriticalTaskFuture = + Pin<Box<dyn Future<Output = Result<(), MycCriticalTaskError>> + Send + 'static>>; +type CriticalTaskFactory = + Box<dyn FnOnce(MycTaskCancellation) -> CriticalTaskFuture + Send + 'static>; + +// TaskMetadata requires a phase for critical work, but the Step 158 supervisor +// does not execute ordered shutdown. Step 159 replaces this inert placeholder +// while composing the exact durability-aware phase inventory. +const STEP_158_PLACEHOLDER_SHUTDOWN_PHASE: ShutdownPhase = ShutdownPhase::DrainOperations; + +/// Read-only cooperative cancellation evidence supplied to one critical task. +#[derive(Clone)] +pub struct MycTaskCancellation { + inner: CancellationToken, +} + +impl MycTaskCancellation { + /// Returns whether coordinated cancellation has already been requested. + #[must_use] + pub fn is_cancelled(&self) -> bool { + self.inner.is_cancelled() + } + + /// Waits until the owning Myc supervisor requests coordinated cancellation. + pub async fn cancelled(&self) { + self.inner.cancelled().await; + } +} + +impl fmt::Debug for MycTaskCancellation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("MycTaskCancellation([sealed])") + } +} + +/// Source-free failure returned by one caller-supplied critical task. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct MycCriticalTaskError; + +impl MycCriticalTaskError { + /// Constructs the sole safe task-failure classification. + #[must_use] + pub const fn failed() -> Self { + Self + } +} + +impl fmt::Display for MycCriticalTaskError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Myc critical task failed") + } +} + +impl Error for MycCriticalTaskError {} + +/// One sealed critical task without a caller-controlled name or detachable handle. +pub struct MycCriticalTask { + factory: CriticalTaskFactory, +} + +impl MycCriticalTask { + /// Wraps one authoritative task for owned supervision. + pub fn new<F, Fut>(task: F) -> Self + where + F: FnOnce(MycTaskCancellation) -> Fut + Send + 'static, + Fut: Future<Output = Result<(), MycCriticalTaskError>> + Send + 'static, + { + Self { + factory: Box::new(move |cancellation| Box::pin(task(cancellation))), + } + } +} + +impl fmt::Debug for MycCriticalTask { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("MycCriticalTask([sealed])") + } +} + +/// Stable source-free failure classes for the Myc critical-task graph. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MycRuntimeSupervisionErrorKind { + EmptyTaskSet, + TooManyTasks, + TaskRegistration, + TaskReturnedError, + TaskPanicked, + UnexpectedCompletion, + UnexpectedCancellation, + JoinFailed, +} + +impl MycRuntimeSupervisionErrorKind { + /// Returns the stable machine-readable failure code. + #[must_use] + pub const fn code(self) -> &'static str { + match self { + Self::EmptyTaskSet => "runtime_task_set_empty", + Self::TooManyTasks => "runtime_task_set_too_large", + Self::TaskRegistration => "runtime_task_registration_failed", + Self::TaskReturnedError => "runtime_task_returned_error", + Self::TaskPanicked => "runtime_task_panicked", + Self::UnexpectedCompletion => "runtime_task_completed_early", + Self::UnexpectedCancellation => "runtime_task_cancelled_unexpectedly", + Self::JoinFailed => "runtime_task_join_failed", + } + } +} + +/// Redacted failure returned only after the shared supervisor joins owned work. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MycRuntimeSupervisionError { + kind: MycRuntimeSupervisionErrorKind, +} + +impl MycRuntimeSupervisionError { + const fn new(kind: MycRuntimeSupervisionErrorKind) -> Self { + Self { kind } + } + + /// Returns the stable failure classification. + #[must_use] + pub const fn kind(self) -> MycRuntimeSupervisionErrorKind { + self.kind + } + + /// Returns the stable machine-readable failure code. + #[must_use] + pub const fn code(self) -> &'static str { + self.kind.code() + } + + /// Returns the fixed nonzero process result for every fatal task-graph outcome. + #[must_use] + pub const fn process_result(self) -> MycProcessResult { + MycProcessResult::UnexpectedInternal + } + + /// Returns the fixed safe diagnostic for every fatal task-graph outcome. + #[must_use] + pub const fn diagnostic(self) -> MycLogRecord { + MycLogRecord::critical_task_failed() + } +} + +impl fmt::Display for MycRuntimeSupervisionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Myc critical-task supervision failed") + } +} + +impl fmt::Debug for MycRuntimeSupervisionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MycRuntimeSupervisionError") + .field("kind", &self.kind) + .finish() + } +} + +impl Error for MycRuntimeSupervisionError {} + +/// One nonforgeable, bounded set of critical tasks awaiting owned execution. +#[must_use = "the supervised runtime must be run so authoritative tasks are joined"] +pub struct MycSupervisedRuntime { + tasks: Box<[MycCriticalTask]>, +} + +impl MycSupervisedRuntime { + /// Validates and retains between one and 32 critical tasks. + /// + /// Iterator ingestion stops after the maximum plus one item. + pub fn new( + tasks: impl IntoIterator<Item = MycCriticalTask>, + ) -> Result<Self, MycRuntimeSupervisionError> { + let mut bounded = Vec::with_capacity(MYC_CRITICAL_TASK_MAX_COUNT); + for task in tasks.into_iter().take(MYC_CRITICAL_TASK_MAX_COUNT + 1) { + if bounded.len() == MYC_CRITICAL_TASK_MAX_COUNT { + return Err(MycRuntimeSupervisionError::new( + MycRuntimeSupervisionErrorKind::TooManyTasks, + )); + } + bounded.push(task); + } + if bounded.is_empty() { + return Err(MycRuntimeSupervisionError::new( + MycRuntimeSupervisionErrorKind::EmptyTaskSet, + )); + } + Ok(Self { + tasks: bounded.into_boxed_slice(), + }) + } + + /// Returns the exact number of retained authoritative tasks. + #[must_use] + pub fn task_count(&self) -> usize { + self.tasks.len() + } + + /// Runs the single owned graph until a fatal outcome coordinates peer + /// cancellation and every task join has been observed. + pub async fn run(self) -> Result<(), MycRuntimeSupervisionError> { + let metadata = (0..self.tasks.len()) + .map(task_metadata) + .collect::<Result<Vec<_>, _>>()?; + let mut supervisor = TaskSupervisor::new(); + for (metadata, task) in metadata.into_iter().zip(self.tasks.into_vec()) { + let factory = task.factory; + if supervisor + .spawn(metadata, move |cancellation| async move { + factory(MycTaskCancellation { + inner: cancellation, + }) + .await + .map_err(|_| HostError::new(HostErrorKind::TaskFailure)) + }) + .is_err() + { + supervisor.request_cancellation(); + let _ = supervisor.supervise().await; + return Err(MycRuntimeSupervisionError::new( + MycRuntimeSupervisionErrorKind::TaskRegistration, + )); + } + } + supervisor + .supervise() + .await + .map(|_| ()) + .map_err(|failure| MycRuntimeSupervisionError::new(map_failure_kind(failure.kind()))) + } +} + +impl fmt::Debug for MycSupervisedRuntime { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MycSupervisedRuntime") + .field("task_count", &self.tasks.len()) + .field("tasks", &"[sealed]") + .finish() + } +} + +fn task_metadata(index: usize) -> Result<TaskMetadata, MycRuntimeSupervisionError> { + let name = TaskName::new(format!("critical_task_{index:02}")).map_err(|_| { + MycRuntimeSupervisionError::new(MycRuntimeSupervisionErrorKind::TaskRegistration) + })?; + TaskMetadata::new( + name, + TaskClassification::Critical, + Some(STEP_158_PLACEHOLDER_SHUTDOWN_PHASE), + ) + .map_err(|_| MycRuntimeSupervisionError::new(MycRuntimeSupervisionErrorKind::TaskRegistration)) +} + +const fn map_failure_kind(kind: SupervisionFailureKind) -> MycRuntimeSupervisionErrorKind { + match kind { + SupervisionFailureKind::TaskReturnedError => { + MycRuntimeSupervisionErrorKind::TaskReturnedError + } + SupervisionFailureKind::TaskPanicked => MycRuntimeSupervisionErrorKind::TaskPanicked, + SupervisionFailureKind::UnexpectedCompletion => { + MycRuntimeSupervisionErrorKind::UnexpectedCompletion + } + SupervisionFailureKind::UnexpectedCancellation => { + MycRuntimeSupervisionErrorKind::UnexpectedCancellation + } + SupervisionFailureKind::JoinFailed => MycRuntimeSupervisionErrorKind::JoinFailed, + } +} diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs @@ -23,6 +23,9 @@ const DIAGNOSTICS_CONTRACT: &str = include_str!("../contracts/services_hardening/diagnostics.v1.json"); const MAIN: &str = include_str!("../src/main.rs"); const STATUS_V1: &str = include_str!("../src/status_v1.rs"); +const RUNTIME_SUPERVISION: &str = include_str!("../src/runtime_supervision.rs"); +const RUNTIME_SUPERVISION_CONTRACT: &str = + include_str!("../contracts/services_hardening/runtime_supervision.v1.json"); const STATUS_CONTRACT: &str = include_str!("../contracts/services_hardening/status_cache.v1.json"); const OPERATIONS_V1: &str = include_str!("../src/operations_v1.rs"); const OPERATIONS_CONTRACT: &str = @@ -64,6 +67,7 @@ const SOURCES: &[&str] = &[ include_str!("../src/provider_verification.rs"), include_str!("../src/runtime_context.rs"), include_str!("../src/runtime_foundation.rs"), + include_str!("../src/runtime_supervision.rs"), include_str!("../src/status_v1.rs"), include_str!("../src/state_catalog.rs"), include_str!("../src/state_completion.rs"), @@ -109,6 +113,7 @@ fn implementation_modules_are_private_and_rustdoc_uses_the_reviewed_readme() { "provider_verification", "runtime_context", "runtime_foundation", + "runtime_supervision", "status_v1", "state_catalog", "state_completion", @@ -234,6 +239,12 @@ fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() { "pub fn myc::verify_myc_nip46_event", "pub fn myc::verify_myc_nip46_request", "pub async fn myc::open_myc_runtime_foundation", + "pub struct myc::MycCriticalTask", + "pub struct myc::MycCriticalTaskError", + "pub struct myc::MycRuntimeSupervisionError", + "pub enum myc::MycRuntimeSupervisionErrorKind", + "pub struct myc::MycSupervisedRuntime", + "pub struct myc::MycTaskCancellation", ] { assert!( PUBLIC_API.contains(required), @@ -263,6 +274,7 @@ fn reviewed_api_is_root_only_and_exposes_no_implementation_authority() { "provider_verification", "runtime_context", "runtime_foundation", + "runtime_supervision", "status_v1", "state_catalog", "state_completion", @@ -533,6 +545,90 @@ fn step157_control_plane_wave_is_machine_bound_native_and_test_only() { } #[test] +fn step158_runtime_supervision_is_one_owned_bounded_redacted_graph() { + let contract: serde_json::Value = serde_json::from_str(RUNTIME_SUPERVISION_CONTRACT) + .expect("Step 158 runtime-supervision contract"); + assert_eq!(contract["schema"], "radroots.myc.runtime-supervision.v1"); + assert_eq!(contract["contract_version"], 1); + assert_eq!(contract["step"], 158); + assert_eq!(contract["task_set"]["minimum_count"], 1); + assert_eq!(contract["task_set"]["maximum_count"], 32); + assert_eq!(contract["task_set"]["classification"], "critical"); + assert_eq!( + contract["task_set"]["shutdown_phase_assignment"], + "deferred_to_step_159" + ); + assert_eq!(contract["task_set"]["detached_tasks"], false); + assert_eq!( + contract["fatal_outcomes"], + serde_json::json!([ + "task_returned_error", + "task_panicked", + "unexpected_completion", + "unexpected_cancellation", + "join_failed" + ]) + ); + assert_eq!( + contract["fatal_effect"]["all_task_joins_observed_before_return"], + true + ); + assert_eq!( + contract["deferred"], + serde_json::json!([ + "process_panic_hook", + "signal_installation", + "first_signal_graceful_shutdown", + "second_signal_forced_shutdown", + "shutdown_grace_deadline", + "ordered_durability_drain" + ]) + ); + for required in [ + "MYC_CRITICAL_TASK_MAX_COUNT: usize = 32", + ".take(MYC_CRITICAL_TASK_MAX_COUNT + 1)", + "TaskClassification::Critical", + "supervisor.request_cancellation()", + "supervisor.supervise().await", + "MycProcessResult::UnexpectedInternal", + "MycLogRecord::critical_task_failed()", + "MycTaskCancellation([sealed])", + "MycCriticalTask([sealed])", + ] { + assert!( + RUNTIME_SUPERVISION.contains(required), + "Step 158 implementation is missing `{required}`" + ); + } + for forbidden in [ + "pub use radroots_service_host", + "pub fn cancellation_token", + "pub fn request_cancellation", + "JoinHandle", + "tokio::spawn", + "spawn_blocking", + "signal::", + "process::exit", + "std::time::SystemTime", + "rand::", + "getrandom", + "fn source(", + ] { + assert!( + !RUNTIME_SUPERVISION.contains(forbidden), + "Step 158 boundary gained forbidden authority `{forbidden}`" + ); + } + for required in [ + "one sealed, bounded critical-task graph", + "task names and handles remain internal", + "Step 159 owns\nthe process panic hook, signal installation", + ] { + assert!(README.contains(required), "README is missing `{required}`"); + } +} + +#[test] fn step148_response_commit_is_one_atomic_exact_byte_authority() { let contract: serde_json::Value = serde_json::from_str(NIP46_RESPONSE_CONTRACT).expect("Step 148 contract"); @@ -878,7 +974,7 @@ fn public_errors_remain_crate_owned_redacted_and_source_free() { .lines() .filter(|line| line.starts_with("pub struct myc::") && line.ends_with("Error")) .count(); - assert_eq!(public_error_count, 30); + assert_eq!(public_error_count, 32); assert!(PUBLIC_API.contains("pub struct myc::MycDoctorError")); assert!(!PUBLIC_API.contains("pub struct myc::MycRuntimeFoundation {")); assert!(!PUBLIC_API.contains("pub struct myc::MycStateHost {")); diff --git a/tests/services_hardening_runtime_supervision.rs b/tests/services_hardening_runtime_supervision.rs @@ -0,0 +1,180 @@ +#![forbid(unsafe_code)] + +use std::{ + error::Error, + sync::{ + Arc, + atomic::{AtomicBool, AtomicUsize, Ordering}, + }, +}; + +use myc::{ + MYC_CRITICAL_TASK_MAX_COUNT, MycCriticalTask, MycCriticalTaskError, MycLogRecord, + MycProcessResult, MycRuntimeSupervisionErrorKind, MycSupervisedRuntime, +}; + +fn waiting_peer(joined: Arc<AtomicBool>) -> MycCriticalTask { + MycCriticalTask::new(move |cancellation| async move { + cancellation.cancelled().await; + assert!(cancellation.is_cancelled()); + joined.store(true, Ordering::SeqCst); + Ok(()) + }) +} + +#[test] +fn task_inventory_is_nonempty_bounded_and_stops_at_maximum_plus_one() { + let empty = MycSupervisedRuntime::new([]).expect_err("empty graph must fail closed"); + assert_eq!(empty.kind(), MycRuntimeSupervisionErrorKind::EmptyTaskSet); + + let maximum = + (0..MYC_CRITICAL_TASK_MAX_COUNT).map(|_| MycCriticalTask::new(|_| async { Ok(()) })); + assert_eq!( + MycSupervisedRuntime::new(maximum) + .expect("exact maximum") + .task_count(), + MYC_CRITICAL_TASK_MAX_COUNT + ); + + let generated = Arc::new(AtomicUsize::new(0)); + let count = Arc::clone(&generated); + let unbounded = std::iter::repeat_with(move || { + count.fetch_add(1, Ordering::SeqCst); + MycCriticalTask::new(|_| async { Ok(()) }) + }); + let too_many = MycSupervisedRuntime::new(unbounded).expect_err("maximum plus one"); + assert_eq!( + too_many.kind(), + MycRuntimeSupervisionErrorKind::TooManyTasks + ); + assert_eq!( + generated.load(Ordering::SeqCst), + MYC_CRITICAL_TASK_MAX_COUNT + 1 + ); +} + +#[test] +fn registration_without_a_tokio_runtime_fails_before_any_task_can_detach() { + let runtime = MycSupervisedRuntime::new([MycCriticalTask::new(|_| async { Ok(()) })]) + .expect("bounded graph"); + let error = futures_executor::block_on(runtime.run()).expect_err("Tokio runtime required"); + assert_eq!( + error.kind(), + MycRuntimeSupervisionErrorKind::TaskRegistration + ); +} + +#[tokio::test] +async fn task_error_coordinates_peer_cancellation_and_observes_every_join() { + let peer_joined = Arc::new(AtomicBool::new(false)); + let runtime = MycSupervisedRuntime::new([ + waiting_peer(Arc::clone(&peer_joined)), + MycCriticalTask::new(|_| async { Err(MycCriticalTaskError::failed()) }), + ]) + .expect("bounded graph"); + + let error = runtime.run().await.expect_err("critical task failed"); + assert_eq!( + error.kind(), + MycRuntimeSupervisionErrorKind::TaskReturnedError + ); + assert_eq!(error.process_result(), MycProcessResult::UnexpectedInternal); + assert_eq!(error.diagnostic(), MycLogRecord::critical_task_failed()); + assert!(peer_joined.load(Ordering::SeqCst)); + assert!(Error::source(&error).is_none()); +} + +#[tokio::test] +async fn early_success_and_panic_are_fatal_and_join_their_cancelled_peer() { + let early_peer = Arc::new(AtomicBool::new(false)); + let early = MycSupervisedRuntime::new([ + waiting_peer(Arc::clone(&early_peer)), + MycCriticalTask::new(|_| async { Ok(()) }), + ]) + .expect("bounded graph") + .run() + .await + .expect_err("critical task returned before cancellation"); + assert_eq!( + early.kind(), + MycRuntimeSupervisionErrorKind::UnexpectedCompletion + ); + assert!(early_peer.load(Ordering::SeqCst)); + + let panic_peer = Arc::new(AtomicBool::new(false)); + let panicked = MycSupervisedRuntime::new([ + waiting_peer(Arc::clone(&panic_peer)), + MycCriticalTask::new(|_| async { + panic!("fixed critical-task test panic"); + #[allow(unreachable_code)] + Ok(()) + }), + ]) + .expect("bounded graph") + .run() + .await + .expect_err("critical task panicked"); + assert_eq!( + panicked.kind(), + MycRuntimeSupervisionErrorKind::TaskPanicked + ); + assert!(panic_peer.load(Ordering::SeqCst)); +} + +#[test] +fn task_and_error_diagnostics_are_source_free_and_redacted() { + let secret = "caller-task-secret"; + let task = MycCriticalTask::new(move |_| async move { + let _ = secret; + Ok(()) + }); + let runtime = MycSupervisedRuntime::new([task]).expect("bounded graph"); + assert_eq!( + format!("{runtime:?}"), + "MycSupervisedRuntime { task_count: 1, tasks: \"[sealed]\" }" + ); + assert!(!format!("{runtime:?}").contains(secret)); + assert_eq!( + format!("{:?}", MycCriticalTaskError::failed()), + "MycCriticalTaskError" + ); + assert!(Error::source(&MycCriticalTaskError::failed()).is_none()); + + let codes = [ + ( + MycRuntimeSupervisionErrorKind::EmptyTaskSet, + "runtime_task_set_empty", + ), + ( + MycRuntimeSupervisionErrorKind::TooManyTasks, + "runtime_task_set_too_large", + ), + ( + MycRuntimeSupervisionErrorKind::TaskRegistration, + "runtime_task_registration_failed", + ), + ( + MycRuntimeSupervisionErrorKind::TaskReturnedError, + "runtime_task_returned_error", + ), + ( + MycRuntimeSupervisionErrorKind::TaskPanicked, + "runtime_task_panicked", + ), + ( + MycRuntimeSupervisionErrorKind::UnexpectedCompletion, + "runtime_task_completed_early", + ), + ( + MycRuntimeSupervisionErrorKind::UnexpectedCancellation, + "runtime_task_cancelled_unexpectedly", + ), + ( + MycRuntimeSupervisionErrorKind::JoinFailed, + "runtime_task_join_failed", + ), + ]; + for (kind, code) in codes { + assert_eq!(kind.code(), code); + } +}