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:
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);
+ }
+}