lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 1871733edd8c81a1a2f907cafb1fae1ef270fdfc
parent 1db1a0d7047463c39ceb7b6cc4a551d3ed1abd7b
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 09:12:13 +0000

runtime-distribution: define hardened service targets

- freeze exact metadata-only Myc and RHI target contracts
- require explicit instance contexts and keep manager inspection non-I/O
- reject Myc and RHI artifact rows until distribution authority lands
- add durable fixtures, boundary tests, and breaking-contract guidance

Diffstat:
MAGENTS.md | 8++++++++
MCargo.lock | 2++
Mcrates/runtime_distribution/Cargo.toml | 1+
Mcrates/runtime_distribution/README | 7+++++++
Mcrates/runtime_distribution/src/error.rs | 80+++++++++++++++++++++++++++++++++----------------------------------------------
Mcrates/runtime_distribution/src/lib.rs | 316+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mcrates/runtime_distribution/src/model.rs | 11+++++++++++
Mcrates/runtime_distribution/src/resolve.rs | 139++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Acrates/runtime_distribution/src/service.rs | 306+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime_distribution/tests/fixtures/hardened_service_targets.v1.toml | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime_distribution/tests/package_boundary.rs | 34++++++++++++++++++++++++++++++++++
Mcrates/runtime_manager/Cargo.toml | 1+
Mcrates/runtime_manager/README | 11+++++++----
Mcrates/runtime_manager/src/error.rs | 8++++++++
Mcrates/runtime_manager/src/lib.rs | 77++++++++++++++++++++++-------------------------------------------------------
Mcrates/runtime_manager/src/managed.rs | 360++++++++++++++++++++++++++++++++++++-------------------------------------------
Mcrates/runtime_manager/src/model.rs | 42+++++++++++++++++++++++++++++++-----------
Mcrates/runtime_manager/src/paths.rs | 1+
Mcrates/runtime_manager/src/registry.rs | 2++
Acrates/runtime_manager/tests/fixtures/hardened_service_management.v1.toml | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime_manager/tests/service_target_boundary.rs | 25+++++++++++++++++++++++++
21 files changed, 1124 insertions(+), 448 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -167,6 +167,14 @@ Before editing code: names are validated single path components, and ordinary manager errors and `Debug` output must not expose filesystem paths, file contents, or raw dependency-owned causes. +- Runtime-distribution and runtime-management service metadata is the sealed + exact Myc/RHI v1 inventory. Both services support multiple validated + instances, one TOML config, explicit initialization with existing-only run, + detailed HTTP/1.1-over-Unix local administration, cached + `/livez`/`readyz`/`metrics`, and only Linux x86_64/aarch64 Tier-1 eligibility + in `target` posture. This metadata does not authorize service registration, + PID/config/log probing, lifecycle actions, artifact names, channels, archive + resolution, or a `qualified` support claim. - Library code must not initialize a tracing subscriber, parse a process CLI, read service configuration from environment variables, install signal handlers, create a Tokio runtime, call `process::exit`, or spawn arbitrary diff --git a/Cargo.lock b/Cargo.lock @@ -3788,6 +3788,7 @@ dependencies = [ name = "radroots_runtime_distribution" version = "0.1.0-alpha" dependencies = [ + "radroots_runtime_paths", "serde", "thiserror 1.0.69", "toml 0.8.23", @@ -3798,6 +3799,7 @@ name = "radroots_runtime_manager" version = "0.1.0-alpha" dependencies = [ "flate2", + "radroots_runtime_distribution", "radroots_runtime_paths", "serde", "tar", diff --git a/crates/runtime_distribution/Cargo.toml b/crates/runtime_distribution/Cargo.toml @@ -13,6 +13,7 @@ documentation = "https://docs.rs/radroots_runtime_distribution" readme = "README" [dependencies] +radroots_runtime_paths = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } toml = { workspace = true } diff --git a/crates/runtime_distribution/README b/crates/runtime_distribution/README @@ -9,8 +9,15 @@ distribution contract resolution for the `radroots` core libraries. archive formats; * a schema constant and resolver for selecting a matching runtime artifact; * request and resolved-artifact types used by bootstrap and installer code; + * an exact, typed Myc/RHI service-target inventory for multiple-instance TOML + services with explicit existing-state startup, Unix local administration, + cached operations endpoints, and Linux x86_64/aarch64 Tier-1 eligibility; * TOML-backed contract handling for modular runtime deployment metadata. +The hardened service-target inventory is metadata-only. It deliberately does +not define Myc or RHI binaries, packages, archives, channels, artifact names, +or qualified support claims. + ## Copyright Except as otherwise noted, all files in the `radroots_runtime_distribution` diff --git a/crates/runtime_distribution/src/error.rs b/crates/runtime_distribution/src/error.rs @@ -1,51 +1,37 @@ use thiserror::Error; -#[derive(Debug, Error, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum RadrootsRuntimeDistributionError { - #[error("parse runtime distribution contract: {0}")] - Parse(String), - #[error("runtime distribution schema `{found}` does not match `{expected}`")] - UnexpectedSchema { - expected: &'static str, - found: String, - }, - #[error("runtime `{0}` not found in distribution contract")] - UnknownRuntime(String), - #[error("runtime `{0}` is not installable through the local runtime distribution contract")] - RuntimeNotInstallable(String), - #[error("runtime `{0}` has no target set in the distribution contract")] - MissingTargetSet(String), - #[error("runtime `{runtime_id}` references unknown artifact adapter `{adapter_id}`")] - UnknownArtifactAdapter { - runtime_id: String, - adapter_id: String, - }, - #[error("channel `{0}` is not defined in the runtime distribution contract")] - UnknownChannel(String), - #[error("channel `{0}` is defined but not active in the runtime distribution contract")] - InactiveChannel(String), - #[error( - "target set `{target_set_id}` for runtime `{runtime_id}` references unknown target `{target_id}`" - )] - UnknownTarget { - runtime_id: String, - target_set_id: String, - target_id: String, - }, - #[error("runtime `{runtime_id}` does not support os `{os}` arch `{arch}`")] - UnsupportedPlatform { - runtime_id: String, - os: String, - arch: String, - }, - #[error("target `{target_id}` references unknown archive format `{archive_format_id}`")] - UnknownArchiveFormat { - target_id: String, - archive_format_id: String, - }, - #[error("target `{target_id}` for runtime `{runtime_id}` does not define an archive format")] - MissingArchiveFormat { - runtime_id: String, - target_id: String, - }, + #[error("parse runtime distribution contract failed")] + Parse, + #[error("runtime distribution schema is unsupported")] + UnexpectedSchema, + #[error("runtime distribution schema version is unsupported")] + UnexpectedSchemaVersion, + #[error("runtime is not present in the distribution contract")] + UnknownRuntime, + #[error("runtime is not installable through the distribution contract")] + RuntimeNotInstallable, + #[error("hardened service artifact authority is deferred")] + HardenedServiceArtifactDeferred, + #[error("runtime has no target set in the distribution contract")] + MissingTargetSet, + #[error("runtime references an unknown artifact adapter")] + UnknownArtifactAdapter, + #[error("channel is not defined in the distribution contract")] + UnknownChannel, + #[error("channel is defined but not active in the distribution contract")] + InactiveChannel, + #[error("runtime target set references an unknown target")] + UnknownTarget, + #[error("runtime does not support the requested platform")] + UnsupportedPlatform, + #[error("target references an unknown archive format")] + UnknownArchiveFormat, + #[error("target does not define an archive format")] + MissingArchiveFormat, + #[error("service is not a hardened distribution target")] + UnsupportedService, + #[error("service target is not eligible for Tier-1 qualification")] + UnsupportedServiceTarget, } diff --git a/crates/runtime_distribution/src/lib.rs b/crates/runtime_distribution/src/lib.rs @@ -3,6 +3,7 @@ pub mod error; pub mod model; pub mod resolve; +pub mod service; pub use error::RadrootsRuntimeDistributionError; pub use model::{ @@ -10,19 +11,34 @@ pub use model::{ RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSet, TargetSpec, }; pub use resolve::{ - RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionResolver, ResolvedRuntimeArtifact, - RuntimeArtifactRequest, + RUNTIME_DISTRIBUTION_SCHEMA, RUNTIME_DISTRIBUTION_SCHEMA_VERSION, + RadrootsRuntimeDistributionResolver, ResolvedRuntimeArtifact, ResolvedServiceTarget, + RuntimeArtifactRequest, ServiceTargetRequest, +}; +pub use service::{ + HardenedServiceTarget, HardenedServiceTargets, ServiceAdminBasePath, ServiceAdminTransport, + ServiceConfigurationFormat, ServiceInstanceSupport, ServiceOperationsSurface, + ServiceRunStatePolicy, ServiceStateInitialization, ServiceStatusSurface, ServiceSupportPosture, + ServiceTier1Target, }; #[cfg(test)] mod tests { + use radroots_runtime_paths::ServiceId; use toml::Value; use super::{ - RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionError, - RadrootsRuntimeDistributionResolver, RuntimeArtifactRequest, + HardenedServiceTarget, RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionContract, + RadrootsRuntimeDistributionError, RadrootsRuntimeDistributionResolver, + RuntimeArtifactRequest, RuntimeDistributionEntry, ServiceConfigurationFormat, + ServiceInstanceSupport, ServiceOperationsSurface, ServiceRunStatePolicy, + ServiceStateInitialization, ServiceSupportPosture, ServiceTargetRequest, + ServiceTier1Target, }; + const HARDENED_SERVICE_CONTRACT: &str = + include_str!("../tests/fixtures/hardened_service_targets.v1.toml"); + const CONTRACT: &str = r#" schema = "radroots-runtime-distribution" schema_version = 1 @@ -184,6 +200,34 @@ artifact_adapter = "mojo_workspace_archive" target_set = "mojo_workspace_default" default_channel = "stable" human_installable = false + +[service_targets.myc] +service_id = "myc" +instance_support = "multiple" +config_format = "toml" +state_initialization = "explicit" +run_state_policy = "existing_only" +admin_transport = "http11_over_unix_domain_socket" +admin_base_path = "/v1" +admin_contract_version = 1 +status_surface = "local_admin_service_status_v1" +operations_surface = "cached_livez_readyz_metrics" +support_posture = "target" +tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] + +[service_targets.rhi] +service_id = "rhi" +instance_support = "multiple" +config_format = "toml" +state_initialization = "explicit" +run_state_policy = "existing_only" +admin_transport = "http11_over_unix_domain_socket" +admin_base_path = "/v1" +admin_contract_version = 1 +status_surface = "local_admin_service_status_v1" +operations_surface = "cached_livez_readyz_metrics" +support_posture = "target" +tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] "#; fn contract_value() -> Value { @@ -217,10 +261,7 @@ human_installable = false fn parse_str_rejects_invalid_toml() { let err = RadrootsRuntimeDistributionResolver::parse_str("schema = [") .expect_err("invalid toml should fail"); - assert_eq!( - std::mem::discriminant(&err), - std::mem::discriminant(&RadrootsRuntimeDistributionError::Parse(String::new())) - ); + assert_eq!(err, RadrootsRuntimeDistributionError::Parse); } #[test] @@ -232,13 +273,7 @@ human_installable = false let err = RadrootsRuntimeDistributionResolver::parse_str(&raw) .expect_err("unexpected schema should fail"); - assert_eq!( - err, - RadrootsRuntimeDistributionError::UnexpectedSchema { - expected: RUNTIME_DISTRIBUTION_SCHEMA, - found: "wrong-schema".to_string(), - } - ); + assert_eq!(err, RadrootsRuntimeDistributionError::UnexpectedSchema); } #[test] @@ -319,12 +354,7 @@ human_installable = false }) .expect_err("mobile runtime should not be installable"); - assert_eq!( - err, - RadrootsRuntimeDistributionError::RuntimeNotInstallable( - "community-app-ios".to_string() - ) - ); + assert_eq!(err, RadrootsRuntimeDistributionError::RuntimeNotInstallable); } #[test] @@ -342,10 +372,7 @@ human_installable = false }) .expect_err("bootstrap runtime should not be installable"); - assert_eq!( - err, - RadrootsRuntimeDistributionError::RuntimeNotInstallable("hyf".to_string()) - ); + assert_eq!(err, RadrootsRuntimeDistributionError::RuntimeNotInstallable); } #[test] @@ -363,10 +390,7 @@ human_installable = false }) .expect_err("candidate channel should be inactive"); - assert_eq!( - err, - RadrootsRuntimeDistributionError::InactiveChannel("candidate".to_string()) - ); + assert_eq!(err, RadrootsRuntimeDistributionError::InactiveChannel); } #[test] @@ -385,10 +409,7 @@ human_installable = false }, ); - assert_eq!( - err, - RadrootsRuntimeDistributionError::UnknownRuntime("missing-runtime".to_string()) - ); + assert_eq!(err, RadrootsRuntimeDistributionError::UnknownRuntime); } #[test] @@ -407,10 +428,7 @@ human_installable = false }, ); - assert_eq!( - err, - RadrootsRuntimeDistributionError::UnknownChannel("beta".to_string()) - ); + assert_eq!(err, RadrootsRuntimeDistributionError::UnknownChannel); } #[test] @@ -428,14 +446,7 @@ human_installable = false }) .expect_err("windows target should be unsupported"); - assert_eq!( - err, - RadrootsRuntimeDistributionError::UnsupportedPlatform { - runtime_id: "radrootsd".to_string(), - os: "windows".to_string(), - arch: "amd64".to_string(), - } - ); + assert_eq!(err, RadrootsRuntimeDistributionError::UnsupportedPlatform); } #[test] @@ -461,10 +472,7 @@ human_installable = false }, ); - assert_eq!( - err, - RadrootsRuntimeDistributionError::MissingTargetSet("community-app-ios".to_string()) - ); + assert_eq!(err, RadrootsRuntimeDistributionError::MissingTargetSet); } #[test] @@ -492,10 +500,7 @@ human_installable = false assert_eq!( err, - RadrootsRuntimeDistributionError::UnknownArtifactAdapter { - runtime_id: "cli".to_string(), - adapter_id: "missing_adapter".to_string(), - } + RadrootsRuntimeDistributionError::UnknownArtifactAdapter ); } @@ -522,14 +527,7 @@ human_installable = false }, ); - assert_eq!( - err, - RadrootsRuntimeDistributionError::UnsupportedPlatform { - runtime_id: "cli".to_string(), - os: "linux".to_string(), - arch: "amd64".to_string(), - } - ); + assert_eq!(err, RadrootsRuntimeDistributionError::UnsupportedPlatform); } #[test] @@ -550,14 +548,7 @@ human_installable = false }, ); - assert_eq!( - err, - RadrootsRuntimeDistributionError::UnknownTarget { - runtime_id: "cli".to_string(), - target_set_id: "cli_default".to_string(), - target_id: "missing-target".to_string(), - } - ); + assert_eq!(err, RadrootsRuntimeDistributionError::UnknownTarget); } #[test] @@ -603,13 +594,7 @@ human_installable = false }, ); - assert_eq!( - err, - RadrootsRuntimeDistributionError::UnknownArchiveFormat { - target_id: "x86_64-unknown-linux-gnu".to_string(), - archive_format_id: "tar.xz".to_string(), - } - ); + assert_eq!(err, RadrootsRuntimeDistributionError::UnknownArchiveFormat); } #[test] @@ -632,12 +617,181 @@ human_installable = false }, ); - assert_eq!( - err, - RadrootsRuntimeDistributionError::MissingArchiveFormat { - runtime_id: "community-app-desktop".to_string(), - target_id: "aarch64-apple-darwin".to_string(), + assert_eq!(err, RadrootsRuntimeDistributionError::MissingArchiveFormat); + } + + #[test] + fn durable_contract_resolves_exact_hardened_service_metadata() { + let resolver = RadrootsRuntimeDistributionResolver::parse_str(HARDENED_SERVICE_CONTRACT) + .expect("hardened service contract"); + + for service in ["myc", "rhi"] { + let service_id = ServiceId::new(service).expect("service id"); + let metadata = resolver + .service_target(&service_id) + .expect("service metadata"); + assert_eq!(metadata.service_id(), &service_id); + assert_eq!( + metadata.instance_support(), + ServiceInstanceSupport::Multiple + ); + assert_eq!(metadata.config_format(), ServiceConfigurationFormat::Toml); + assert_eq!( + metadata.state_initialization(), + ServiceStateInitialization::Explicit + ); + assert_eq!( + metadata.run_state_policy(), + ServiceRunStatePolicy::ExistingOnly + ); + assert_eq!( + metadata.operations_surface(), + ServiceOperationsSurface::CachedLivezReadyzMetrics + ); + assert_eq!( + metadata.operations_surface().routes(), + ["/livez", "/readyz", "/metrics"] + ); + assert_eq!(metadata.support_posture(), ServiceSupportPosture::Target); + assert_eq!(metadata.tier_1_targets(), ServiceTier1Target::ALL); + + for target in ServiceTier1Target::ALL { + let resolved = resolver + .resolve_service_target(&ServiceTargetRequest { + service_id: &service_id, + target_id: target.as_str(), + }) + .expect("eligible target"); + assert_eq!(resolved.service_id(), &service_id); + assert_eq!(resolved.target(), target); } + } + } + + #[test] + fn hardened_services_are_metadata_only_and_reject_unsupported_targets() { + let resolver = RadrootsRuntimeDistributionResolver::parse_str(HARDENED_SERVICE_CONTRACT) + .expect("hardened service contract"); + let myc = ServiceId::new("myc").expect("myc"); + + for target_id in ["aarch64-apple-darwin", "x86_64-pc-windows-msvc", "linux-64"] { + assert_eq!( + resolver.resolve_service_target(&ServiceTargetRequest { + service_id: &myc, + target_id, + }), + Err(RadrootsRuntimeDistributionError::UnsupportedServiceTarget) + ); + } + assert_eq!( + resolver.resolve_artifact(&RuntimeArtifactRequest { + runtime_id: "myc", + os: "linux", + arch: "amd64", + version: "1.0.0", + channel: None, + }), + Err(RadrootsRuntimeDistributionError::UnknownRuntime) ); } + + #[test] + fn hardened_services_reject_parsed_and_direct_artifact_authority() { + let raw = format!( + "{HARDENED_SERVICE_CONTRACT}\n\ + [[runtime]]\n\ + id = \"myc\"\n\ + distribution_state = \"defined\"\n\ + release_unit = \"myc\"\n\ + package_name = \"radroots_myc\"\n\ + artifact_adapter = \"rust_binary_archive\"\n\ + default_channel = \"stable\"\n\ + human_installable = true\n" + ); + assert_eq!( + RadrootsRuntimeDistributionResolver::parse_str(&raw).expect_err("parsed bypass"), + RadrootsRuntimeDistributionError::HardenedServiceArtifactDeferred + ); + + let mut contract = + toml::from_str::<RadrootsRuntimeDistributionContract>(HARDENED_SERVICE_CONTRACT) + .expect("direct contract"); + contract.runtime.push(RuntimeDistributionEntry { + id: "rhi".to_owned(), + distribution_state: "defined".to_owned(), + release_unit: "rhi".to_owned(), + package_name: "radroots_rhi".to_owned(), + binary_name: None, + artifact_adapter: "rust_binary_archive".to_owned(), + target_set: None, + default_channel: "stable".to_owned(), + human_installable: true, + notes: None, + }); + assert_eq!( + RadrootsRuntimeDistributionResolver::new(contract).expect_err("direct bypass"), + RadrootsRuntimeDistributionError::HardenedServiceArtifactDeferred + ); + } + + #[test] + fn hardened_service_contract_rejects_schema_drift_unknown_fields_and_inventory_drift() { + for raw in [ + HARDENED_SERVICE_CONTRACT.replace("schema_version = 1", "schema_version = 2"), + format!("{HARDENED_SERVICE_CONTRACT}\nunknown = true\n"), + HARDENED_SERVICE_CONTRACT.replace("service_id = \"rhi\"", "service_id = \"other\""), + HARDENED_SERVICE_CONTRACT.replace( + " \"aarch64-unknown-linux-gnu\",\n]", + " \"aarch64-apple-darwin\",\n]", + ), + ] { + assert!(RadrootsRuntimeDistributionResolver::parse_str(&raw).is_err()); + } + } + + #[test] + fn standalone_hardened_service_target_rejects_contract_drift() { + let contract: Value = + toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); + let target = + toml::to_string(&contract["service_targets"]["myc"]).expect("standalone target"); + let parsed = toml::from_str::<HardenedServiceTarget>(&target).expect("valid target"); + assert_eq!(parsed.service_id().as_str(), "myc"); + + for raw in [ + target.replace("admin_contract_version = 1", "admin_contract_version = 99"), + target.replace("service_id = \"myc\"", "service_id = \"unsupported\""), + target.replace( + "\"x86_64-unknown-linux-gnu\", \"aarch64-unknown-linux-gnu\"", + "\"aarch64-unknown-linux-gnu\"", + ), + ] { + assert!(toml::from_str::<HardenedServiceTarget>(&raw).is_err()); + } + } + + #[test] + fn distribution_errors_do_not_expose_contract_values_or_parser_causes() { + use std::error::Error as _; + + for (raw, secret) in [ + ( + HARDENED_SERVICE_CONTRACT.replace( + "schema = \"radroots-runtime-distribution\"", + "schema = \"secret-contract-value\"", + ), + "secret-contract-value", + ), + ( + "credential = 'secret-value'\ninvalid = [".to_owned(), + "secret-value", + ), + ] { + let error = + RadrootsRuntimeDistributionResolver::parse_str(&raw).expect_err("invalid contract"); + let rendered = format!("{error} {error:?}"); + assert!(!rendered.contains(secret)); + assert!(error.source().is_none()); + } + } } diff --git a/crates/runtime_distribution/src/model.rs b/crates/runtime_distribution/src/model.rs @@ -2,7 +2,10 @@ use std::collections::BTreeMap; use serde::Deserialize; +use crate::service::HardenedServiceTargets; + #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct RadrootsRuntimeDistributionContract { pub schema: String, pub schema_version: u32, @@ -20,9 +23,11 @@ pub struct RadrootsRuntimeDistributionContract { pub targets: BTreeMap<String, TargetSpec>, #[serde(default)] pub runtime: Vec<RuntimeDistributionEntry>, + pub service_targets: HardenedServiceTargets, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct DistributionFamily { pub id: String, pub canonical_installer_engine: String, @@ -34,6 +39,7 @@ pub struct DistributionFamily { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct ChannelSet { #[serde(default)] pub active: Vec<String>, @@ -42,6 +48,7 @@ pub struct ChannelSet { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct ArtifactAdapter { pub kind: String, #[serde(default)] @@ -50,6 +57,7 @@ pub struct ArtifactAdapter { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct ArchiveFormat { pub extension: String, #[serde(default)] @@ -57,12 +65,14 @@ pub struct ArchiveFormat { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct TargetSet { #[serde(default)] pub targets: Vec<String>, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct TargetSpec { pub os: String, pub arch: String, @@ -70,6 +80,7 @@ pub struct TargetSpec { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct RuntimeDistributionEntry { pub id: String, pub distribution_state: String, diff --git a/crates/runtime_distribution/src/resolve.rs b/crates/runtime_distribution/src/resolve.rs @@ -2,8 +2,35 @@ use crate::error::RadrootsRuntimeDistributionError; use crate::model::{ ArtifactAdapter, RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSpec, }; +use crate::service::{HardenedServiceTarget, ServiceTier1Target}; +use radroots_runtime_paths::ServiceId; pub const RUNTIME_DISTRIBUTION_SCHEMA: &str = "radroots-runtime-distribution"; +pub const RUNTIME_DISTRIBUTION_SCHEMA_VERSION: u32 = 1; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServiceTargetRequest<'a> { + pub service_id: &'a ServiceId, + pub target_id: &'a str, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedServiceTarget { + service_id: ServiceId, + target: ServiceTier1Target, +} + +impl ResolvedServiceTarget { + #[must_use] + pub fn service_id(&self) -> &ServiceId { + &self.service_id + } + + #[must_use] + pub const fn target(&self) -> ServiceTier1Target { + self.target + } +} #[derive(Debug, Clone, PartialEq, Eq)] pub struct RuntimeArtifactRequest<'a> { @@ -40,7 +67,7 @@ pub struct RadrootsRuntimeDistributionResolver { impl RadrootsRuntimeDistributionResolver { pub fn parse_str(raw: &str) -> Result<Self, RadrootsRuntimeDistributionError> { let contract = toml::from_str::<RadrootsRuntimeDistributionContract>(raw) - .map_err(|err| RadrootsRuntimeDistributionError::Parse(err.to_string()))?; + .map_err(|_| RadrootsRuntimeDistributionError::Parse)?; Self::new(contract) } @@ -48,10 +75,18 @@ impl RadrootsRuntimeDistributionResolver { contract: RadrootsRuntimeDistributionContract, ) -> Result<Self, RadrootsRuntimeDistributionError> { if contract.schema != RUNTIME_DISTRIBUTION_SCHEMA { - return Err(RadrootsRuntimeDistributionError::UnexpectedSchema { - expected: RUNTIME_DISTRIBUTION_SCHEMA, - found: contract.schema.clone(), - }); + return Err(RadrootsRuntimeDistributionError::UnexpectedSchema); + } + if contract.schema_version != RUNTIME_DISTRIBUTION_SCHEMA_VERSION { + return Err(RadrootsRuntimeDistributionError::UnexpectedSchemaVersion); + } + if contract.runtime.iter().any(|runtime| { + contract + .service_targets + .iter() + .any(|(_, service)| runtime.id == service.service_id().as_str()) + }) { + return Err(RadrootsRuntimeDistributionError::HardenedServiceArtifactDeferred); } Ok(Self { contract }) } @@ -60,6 +95,30 @@ impl RadrootsRuntimeDistributionResolver { &self.contract } + pub fn service_target( + &self, + service_id: &ServiceId, + ) -> Result<&HardenedServiceTarget, RadrootsRuntimeDistributionError> { + self.contract + .service_targets + .get(service_id) + .ok_or(RadrootsRuntimeDistributionError::UnsupportedService) + } + + pub fn resolve_service_target( + &self, + request: &ServiceTargetRequest<'_>, + ) -> Result<ResolvedServiceTarget, RadrootsRuntimeDistributionError> { + let service = self.service_target(request.service_id)?; + let target = ServiceTier1Target::parse(request.target_id) + .filter(|target| service.tier_1_targets().contains(target)) + .ok_or(RadrootsRuntimeDistributionError::UnsupportedServiceTarget)?; + Ok(ResolvedServiceTarget { + service_id: request.service_id.clone(), + target, + }) + } + pub fn resolve_artifact( &self, request: &RuntimeArtifactRequest<'_>, @@ -69,33 +128,25 @@ impl RadrootsRuntimeDistributionResolver { .runtime .iter() .find(|runtime| runtime.id == request.runtime_id) - .ok_or_else(|| { - RadrootsRuntimeDistributionError::UnknownRuntime(request.runtime_id.to_string()) - })?; + .ok_or(RadrootsRuntimeDistributionError::UnknownRuntime)?; if !runtime.human_installable { - return Err(RadrootsRuntimeDistributionError::RuntimeNotInstallable( - runtime.id.clone(), - )); + return Err(RadrootsRuntimeDistributionError::RuntimeNotInstallable); } let channel = request.channel.unwrap_or(runtime.default_channel.as_str()); self.ensure_channel_is_active(channel)?; - let target_set_id = runtime.target_set.as_ref().ok_or_else(|| { - RadrootsRuntimeDistributionError::MissingTargetSet(runtime.id.clone()) - })?; + let target_set_id = runtime + .target_set + .as_ref() + .ok_or(RadrootsRuntimeDistributionError::MissingTargetSet)?; let adapter = self .contract .artifact_adapters .get(&runtime.artifact_adapter) - .ok_or_else( - || RadrootsRuntimeDistributionError::UnknownArtifactAdapter { - runtime_id: runtime.id.clone(), - adapter_id: runtime.artifact_adapter.clone(), - }, - )?; + .ok_or(RadrootsRuntimeDistributionError::UnknownArtifactAdapter)?; let (target_id, target) = self.select_target(runtime, target_set_id, request.os, request.arch)?; @@ -105,10 +156,7 @@ impl RadrootsRuntimeDistributionResolver { .contract .archive_formats .get(&normalized_contract_key(archive_format_id)) - .ok_or_else(|| RadrootsRuntimeDistributionError::UnknownArchiveFormat { - target_id: target_id.to_string(), - archive_format_id: archive_format_id.to_string(), - })?; + .ok_or(RadrootsRuntimeDistributionError::UnknownArchiveFormat)?; let artifact_stem = format!("{}-{}-{}", runtime.release_unit, request.version, target_id); let artifact_file_name = format!("{artifact_stem}{}", archive_format.extension); @@ -142,9 +190,7 @@ impl RadrootsRuntimeDistributionResolver { .iter() .any(|entry| entry == channel) { - return Err(RadrootsRuntimeDistributionError::UnknownChannel( - channel.to_string(), - )); + return Err(RadrootsRuntimeDistributionError::UnknownChannel); } if !self .contract @@ -153,16 +199,14 @@ impl RadrootsRuntimeDistributionResolver { .iter() .any(|entry| entry == channel) { - return Err(RadrootsRuntimeDistributionError::InactiveChannel( - channel.to_string(), - )); + return Err(RadrootsRuntimeDistributionError::InactiveChannel); } Ok(()) } fn select_target<'a>( &'a self, - runtime: &RuntimeDistributionEntry, + _runtime: &RuntimeDistributionEntry, target_set_id: &str, os: &str, arch: &str, @@ -171,21 +215,15 @@ impl RadrootsRuntimeDistributionResolver { .contract .target_sets .get(target_set_id) - .ok_or_else(|| RadrootsRuntimeDistributionError::UnsupportedPlatform { - runtime_id: runtime.id.clone(), - os: os.to_string(), - arch: arch.to_string(), - })?; + .ok_or(RadrootsRuntimeDistributionError::UnsupportedPlatform)?; let mut found_match = None; for target_id in &target_set.targets { - let target = self.contract.targets.get(target_id).ok_or_else(|| { - RadrootsRuntimeDistributionError::UnknownTarget { - runtime_id: runtime.id.clone(), - target_set_id: target_set_id.to_string(), - target_id: target_id.clone(), - } - })?; + let target = self + .contract + .targets + .get(target_id) + .ok_or(RadrootsRuntimeDistributionError::UnknownTarget)?; if target.os == os && target.arch == arch { found_match = Some((target_id.as_str(), target)); @@ -193,17 +231,13 @@ impl RadrootsRuntimeDistributionResolver { } } - found_match.ok_or_else(|| RadrootsRuntimeDistributionError::UnsupportedPlatform { - runtime_id: runtime.id.clone(), - os: os.to_string(), - arch: arch.to_string(), - }) + found_match.ok_or(RadrootsRuntimeDistributionError::UnsupportedPlatform) } fn resolve_archive_format_id<'a>( &self, - runtime: &RuntimeDistributionEntry, - target_id: &'a str, + _runtime: &RuntimeDistributionEntry, + _target_id: &'a str, target: &'a TargetSpec, adapter: &'a ArtifactAdapter, ) -> Result<&'a str, RadrootsRuntimeDistributionError> { @@ -215,10 +249,7 @@ impl RadrootsRuntimeDistributionResolver { return Ok(adapter.supported_archive_formats[0].as_str()); } - Err(RadrootsRuntimeDistributionError::MissingArchiveFormat { - runtime_id: runtime.id.clone(), - target_id: target_id.to_string(), - }) + Err(RadrootsRuntimeDistributionError::MissingArchiveFormat) } } diff --git a/crates/runtime_distribution/src/service.rs b/crates/runtime_distribution/src/service.rs @@ -0,0 +1,306 @@ +use std::collections::BTreeMap; + +use radroots_runtime_paths::ServiceId; +use serde::Deserialize; + +/// Instance cardinality supported by a hardened service target. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceInstanceSupport { + Multiple, +} + +/// Configuration document format supported by a hardened service target. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceConfigurationFormat { + Toml, +} + +impl ServiceConfigurationFormat { + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Toml => "toml", + } + } +} + +/// State initialization policy supported by a hardened service target. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceStateInitialization { + Explicit, +} + +/// Daemon state-open policy supported by a hardened service target. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceRunStatePolicy { + ExistingOnly, +} + +/// Detailed local-administration transport supported by a hardened service. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceAdminTransport { + Http11OverUnixDomainSocket, +} + +/// Versioned base path for detailed local administration. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +pub enum ServiceAdminBasePath { + #[serde(rename = "/v1")] + V1, +} + +/// Detailed status surface supported by a hardened service. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceStatusSurface { + LocalAdminServiceStatusV1, +} + +/// Public operations surface supported by a hardened service. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceOperationsSurface { + CachedLivezReadyzMetrics, +} + +impl ServiceOperationsSurface { + pub const ROUTES: [&str; 3] = ["/livez", "/readyz", "/metrics"]; + + #[must_use] + pub const fn routes(self) -> [&'static str; 3] { + match self { + Self::CachedLivezReadyzMetrics => Self::ROUTES, + } + } +} + +/// Current evidence posture for an eligible service target. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceSupportPosture { + Target, +} + +/// Exact Linux target triples eligible for future Tier-1 qualification. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +pub enum ServiceTier1Target { + #[serde(rename = "x86_64-unknown-linux-gnu")] + X86_64UnknownLinuxGnu, + #[serde(rename = "aarch64-unknown-linux-gnu")] + Aarch64UnknownLinuxGnu, +} + +impl ServiceTier1Target { + pub const ALL: [Self; 2] = [Self::X86_64UnknownLinuxGnu, Self::Aarch64UnknownLinuxGnu]; + + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::X86_64UnknownLinuxGnu => "x86_64-unknown-linux-gnu", + Self::Aarch64UnknownLinuxGnu => "aarch64-unknown-linux-gnu", + } + } + + pub(crate) fn parse(value: &str) -> Option<Self> { + Self::ALL + .into_iter() + .find(|target| target.as_str() == value) + } +} + +/// Closed metadata for one hardened standalone service target. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(try_from = "HardenedServiceTargetWire")] +pub struct HardenedServiceTarget { + service_id: ServiceId, + instance_support: ServiceInstanceSupport, + config_format: ServiceConfigurationFormat, + state_initialization: ServiceStateInitialization, + run_state_policy: ServiceRunStatePolicy, + admin_transport: ServiceAdminTransport, + admin_base_path: ServiceAdminBasePath, + admin_contract_version: u32, + status_surface: ServiceStatusSurface, + operations_surface: ServiceOperationsSurface, + support_posture: ServiceSupportPosture, + tier_1_targets: Vec<ServiceTier1Target>, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct HardenedServiceTargetWire { + service_id: ServiceId, + instance_support: ServiceInstanceSupport, + config_format: ServiceConfigurationFormat, + state_initialization: ServiceStateInitialization, + run_state_policy: ServiceRunStatePolicy, + admin_transport: ServiceAdminTransport, + admin_base_path: ServiceAdminBasePath, + admin_contract_version: u32, + status_surface: ServiceStatusSurface, + operations_surface: ServiceOperationsSurface, + support_posture: ServiceSupportPosture, + tier_1_targets: Vec<ServiceTier1Target>, +} + +impl HardenedServiceTarget { + #[must_use] + pub fn service_id(&self) -> &ServiceId { + &self.service_id + } + + #[must_use] + pub const fn instance_support(&self) -> ServiceInstanceSupport { + self.instance_support + } + + #[must_use] + pub const fn config_format(&self) -> ServiceConfigurationFormat { + self.config_format + } + + #[must_use] + pub const fn state_initialization(&self) -> ServiceStateInitialization { + self.state_initialization + } + + #[must_use] + pub const fn run_state_policy(&self) -> ServiceRunStatePolicy { + self.run_state_policy + } + + #[must_use] + pub const fn admin_transport(&self) -> ServiceAdminTransport { + self.admin_transport + } + + #[must_use] + pub const fn admin_base_path(&self) -> ServiceAdminBasePath { + self.admin_base_path + } + + #[must_use] + pub const fn admin_contract_version(&self) -> u32 { + self.admin_contract_version + } + + #[must_use] + pub const fn status_surface(&self) -> ServiceStatusSurface { + self.status_surface + } + + #[must_use] + pub const fn operations_surface(&self) -> ServiceOperationsSurface { + self.operations_surface + } + + #[must_use] + pub const fn support_posture(&self) -> ServiceSupportPosture { + self.support_posture + } + + #[must_use] + pub fn tier_1_targets(&self) -> &[ServiceTier1Target] { + &self.tier_1_targets + } + + fn has_exact_common_contract(&self) -> bool { + self.instance_support == ServiceInstanceSupport::Multiple + && self.config_format == ServiceConfigurationFormat::Toml + && self.state_initialization == ServiceStateInitialization::Explicit + && self.run_state_policy == ServiceRunStatePolicy::ExistingOnly + && self.admin_transport == ServiceAdminTransport::Http11OverUnixDomainSocket + && self.admin_base_path == ServiceAdminBasePath::V1 + && self.admin_contract_version == 1 + && self.status_surface == ServiceStatusSurface::LocalAdminServiceStatusV1 + && self.operations_surface == ServiceOperationsSurface::CachedLivezReadyzMetrics + && self.support_posture == ServiceSupportPosture::Target + && self.tier_1_targets == ServiceTier1Target::ALL + } +} + +impl TryFrom<HardenedServiceTargetWire> for HardenedServiceTarget { + type Error = &'static str; + + fn try_from(wire: HardenedServiceTargetWire) -> Result<Self, Self::Error> { + let target = Self { + service_id: wire.service_id, + instance_support: wire.instance_support, + config_format: wire.config_format, + state_initialization: wire.state_initialization, + run_state_policy: wire.run_state_policy, + admin_transport: wire.admin_transport, + admin_base_path: wire.admin_base_path, + admin_contract_version: wire.admin_contract_version, + status_surface: wire.status_surface, + operations_surface: wire.operations_surface, + support_posture: wire.support_posture, + tier_1_targets: wire.tier_1_targets, + }; + if !matches!(target.service_id.as_str(), "myc" | "rhi") + || !target.has_exact_common_contract() + { + return Err("hardened service target does not match the v1 contract"); + } + Ok(target) + } +} + +/// Validated exact Myc/RHI service-target inventory. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(try_from = "BTreeMap<String, HardenedServiceTarget>")] +pub struct HardenedServiceTargets(BTreeMap<String, HardenedServiceTarget>); + +impl HardenedServiceTargets { + #[must_use] + pub fn get(&self, service_id: &ServiceId) -> Option<&HardenedServiceTarget> { + self.0.get(service_id.as_str()) + } + + pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &HardenedServiceTarget)> { + self.0.iter().map(|(key, value)| (key.as_str(), value)) + } + + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl TryFrom<BTreeMap<String, HardenedServiceTarget>> for HardenedServiceTargets { + type Error = &'static str; + + fn try_from(targets: BTreeMap<String, HardenedServiceTarget>) -> Result<Self, Self::Error> { + const REQUIRED_SERVICES: [&str; 2] = ["myc", "rhi"]; + + if targets.len() != REQUIRED_SERVICES.len() { + return Err("hardened service target inventory must contain exactly Myc and RHI"); + } + for service in REQUIRED_SERVICES { + let Some(target) = targets.get(service) else { + return Err("hardened service target inventory is incomplete"); + }; + if target.service_id.as_str() != service || !target.has_exact_common_contract() { + return Err("hardened service target metadata does not match the v1 contract"); + } + } + if targets + .iter() + .any(|(key, target)| key != target.service_id.as_str()) + { + return Err("hardened service target key does not match its service identifier"); + } + + Ok(Self(targets)) + } +} diff --git a/crates/runtime_distribution/tests/fixtures/hardened_service_targets.v1.toml b/crates/runtime_distribution/tests/fixtures/hardened_service_targets.v1.toml @@ -0,0 +1,51 @@ +schema = "radroots-runtime-distribution" +schema_version = 1 +owner_doc = "service-hardening-v1" +runtime_registry = "registry.toml" + +[family] +id = "radroots_runtime-family" +canonical_installer_engine = "single_runtime_selected" +human_install_facade = "delivery_publication_only" +tooling_consumption = "shared_distribution_library" +independent_runtime_versions = true +version_resolution = "runtime_scoped_channel_latest" +artifact_verification_required = true + +[channels] +active = [] +defined = [] + +[service_targets.myc] +service_id = "myc" +instance_support = "multiple" +config_format = "toml" +state_initialization = "explicit" +run_state_policy = "existing_only" +admin_transport = "http11_over_unix_domain_socket" +admin_base_path = "/v1" +admin_contract_version = 1 +status_surface = "local_admin_service_status_v1" +operations_surface = "cached_livez_readyz_metrics" +support_posture = "target" +tier_1_targets = [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", +] + +[service_targets.rhi] +service_id = "rhi" +instance_support = "multiple" +config_format = "toml" +state_initialization = "explicit" +run_state_policy = "existing_only" +admin_transport = "http11_over_unix_domain_socket" +admin_base_path = "/v1" +admin_contract_version = 1 +status_surface = "local_admin_service_status_v1" +operations_surface = "cached_livez_readyz_metrics" +support_posture = "target" +tier_1_targets = [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", +] diff --git a/crates/runtime_distribution/tests/package_boundary.rs b/crates/runtime_distribution/tests/package_boundary.rs @@ -0,0 +1,34 @@ +const SERVICE_SOURCE: &str = include_str!("../src/service.rs"); +const SERVICE_FIXTURE: &str = include_str!("fixtures/hardened_service_targets.v1.toml"); + +#[test] +fn hardened_service_metadata_has_no_artifact_or_runtime_authority() { + for forbidden in [ + "binary_name", + "package_name", + "artifact_adapter", + "default_channel", + "[[runtime]]", + "qualified", + ] { + assert!( + !SERVICE_SOURCE.contains(forbidden) && !SERVICE_FIXTURE.contains(forbidden), + "hardened service metadata contains deferred authority `{forbidden}`" + ); + } + + for forbidden in [ + "std::fs", + "std::process", + "tokio", + "hyper", + "reqwest", + "UnixListener", + "TcpListener", + ] { + assert!( + !SERVICE_SOURCE.contains(forbidden), + "service metadata owns forbidden runtime behavior `{forbidden}`" + ); + } +} diff --git a/crates/runtime_manager/Cargo.toml b/crates/runtime_manager/Cargo.toml @@ -14,6 +14,7 @@ readme = "README" [dependencies] flate2 = { workspace = true } +radroots_runtime_distribution = { workspace = true } radroots_runtime_paths = { workspace = true } serde = { workspace = true, features = ["derive"] } tar = { workspace = true } diff --git a/crates/runtime_manager/README b/crates/runtime_manager/README @@ -8,16 +8,19 @@ runtime lifecycle and inspection helpers for the `radroots` core libraries. * schema-checked management contract parsing and shared management constants; * manager-owned shared and per-instance tracking paths kept separate from canonical service-instance paths supplied by a sealed `RuntimeContext`; - * typed, deterministic registry load, save, lookup, and upsert helpers that + * typed, deterministic registry load, save, and lookup helpers that persist service/instance identities without duplicating service paths or secrets; * lifecycle helpers for contained archive install, process start and stop, and context-bound non-secret configuration writes; the manager has no credential read/write authority; - * context-bound runtime inspection and target-resolution helpers for status, - logs, config, and action availability; and + * sealed Myc/RHI target metadata and context resolution without service + registration, lifecycle admission, or PID/config/log probing; and * cleanup behavior limited to manager-owned install and tracking artifacts, - preserving canonical service state and secrets. +preserving canonical service state and secrets. + +Myc and RHI remain metadata-only management targets until their public CLI, +Unix-admin, status, and artifact integrations are separately implemented. ## Copyright diff --git a/crates/runtime_manager/src/error.rs b/crates/runtime_manager/src/error.rs @@ -11,10 +11,18 @@ pub enum RadrootsRuntimeManagerError { Parse, #[error("runtime management schema is unsupported")] UnexpectedSchema, + #[error("runtime management schema version is unsupported")] + UnexpectedSchemaVersion, + #[error("runtime management contract violates the hardened service inventory")] + InvalidContract, #[error("management mode does not support the selected profile")] UnsupportedProfile, #[error("runtime has no bootstrap entry in runtime management contract")] UnknownBootstrapRuntime, + #[error("runtime is not a hardened service target")] + UnsupportedServiceTarget, + #[error("hardened service target is metadata-only until service integration is complete")] + MetadataOnlyServiceTarget, #[error("runtime context does not share the manager path scope")] RuntimeContextMismatch, #[error("read runtime instance registry failed: {kind}")] diff --git a/crates/runtime_manager/src/lib.rs b/crates/runtime_manager/src/lib.rs @@ -31,6 +31,10 @@ pub use paths::{ManagedRuntimeInstancePaths, ManagedRuntimeSharedPaths, bootstra pub use registry::{instance, load_registry, save_registry}; pub const RUNTIME_MANAGEMENT_SCHEMA: &str = "radroots-runtime-management"; +pub const RUNTIME_MANAGEMENT_SCHEMA_VERSION: u32 = 1; + +pub(crate) const HARDENED_MANAGEMENT_CONTRACT: &str = + include_str!("../tests/fixtures/hardened_service_management.v1.toml"); pub fn parse_contract_str( raw: &str, @@ -40,69 +44,32 @@ pub fn parse_contract_str( if contract.schema != RUNTIME_MANAGEMENT_SCHEMA { return Err(RadrootsRuntimeManagerError::UnexpectedSchema); } + if contract.schema_version != RUNTIME_MANAGEMENT_SCHEMA_VERSION { + return Err(RadrootsRuntimeManagerError::UnexpectedSchemaVersion); + } + validate_hardened_management_contract(&contract)?; Ok(contract) } +pub(crate) fn validate_hardened_management_contract( + contract: &RadrootsRuntimeManagementContract, +) -> Result<(), RadrootsRuntimeManagerError> { + let expected = + toml::from_str::<RadrootsRuntimeManagementContract>(HARDENED_MANAGEMENT_CONTRACT) + .map_err(|_| RadrootsRuntimeManagerError::InvalidContract)?; + if contract != &expected { + return Err(RadrootsRuntimeManagerError::InvalidContract); + } + Ok(()) +} + #[cfg(test)] mod tests { use std::error::Error as _; - use super::{RUNTIME_MANAGEMENT_SCHEMA, parse_contract_str}; - - const CONTRACT: &str = r#" -schema = "radroots-runtime-management" -schema_version = 1 -owner_doc = "owner" -runtime_registry = "registry" -distribution_contract = "distribution" -capabilities_contract = "capabilities" - -[defaults] -instance_cardinality = "multiple" -managed_runtime_lookup = "typed_instance_registry" -explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true -global_path_mutation_forbidden = true - -[management_clients] -active = ["cli"] - -[managed_runtime_targets] -defined = ["myc", "rhi"] - -[lifecycle] -actions = ["status"] -health_states = ["running"] - -[mode.interactive] -contract_state = "active" -platforms = ["linux"] -supported_profiles = ["repo_local"] -service_manager_integration = false -uses_absolute_binary_paths = true -default_instance_cardinality = "multiple" - -[paths.interactive] -shared_namespace = "obsolete" -instance_registry_root_class = "config" -instance_registry_rel = "obsolete" -artifact_cache_root_class = "cache" -artifact_cache_rel = "obsolete" -install_root_class = "data" -install_root_rel = "obsolete" -state_root_class = "data" -state_root_rel = "obsolete" -logs_root_class = "logs" -logs_root_rel = "obsolete" -run_root_class = "run" -run_root_rel = "obsolete" -secrets_root_class = "secrets" -secrets_namespace_rel = "obsolete" - -[instance_metadata] -required_fields = ["service_id", "instance_id"] + use super::{HARDENED_MANAGEMENT_CONTRACT, RUNTIME_MANAGEMENT_SCHEMA, parse_contract_str}; -[bootstrap] -"#; + const CONTRACT: &str = HARDENED_MANAGEMENT_CONTRACT; #[test] fn contract_parser_accepts_only_the_expected_schema() { diff --git a/crates/runtime_manager/src/managed.rs b/crates/runtime_manager/src/managed.rs @@ -1,10 +1,10 @@ use core::fmt; use std::path::Path; +use radroots_runtime_distribution::HardenedServiceTarget; use radroots_runtime_paths::{RadrootsPathProfile, RuntimeContext, RuntimeContextSource}; -use crate::paths::{resolve_instance_paths, resolve_shared_paths}; -use crate::registry::{remove_instance, upsert_instance}; +use crate::paths::resolve_shared_paths; use crate::{ BootstrapRuntimeContract, ManagedRuntimeHealthState, ManagedRuntimeInstallState, ManagedRuntimeInstancePaths, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry, @@ -44,14 +44,13 @@ impl ManagedRuntimeContext { pub fn register_instance( &mut self, runtime_context: &RuntimeContext, - install_state: ManagedRuntimeInstallState, + _install_state: ManagedRuntimeInstallState, ) -> Result<(), RadrootsRuntimeManagerError> { ensure_context_scope(&self.manager_context, runtime_context)?; - upsert_instance( - &mut self.registry, - ManagedRuntimeInstanceRecord::new(runtime_context, install_state), - ); - Ok(()) + match self.contract.service_targets.get(runtime_context.service()) { + Some(_) => Err(RadrootsRuntimeManagerError::MetadataOnlyServiceTarget), + None => Err(RadrootsRuntimeManagerError::UnsupportedServiceTarget), + } } pub fn remove_instance( @@ -59,11 +58,10 @@ impl ManagedRuntimeContext { runtime_context: &RuntimeContext, ) -> Result<Option<ManagedRuntimeInstanceRecord>, RadrootsRuntimeManagerError> { ensure_context_scope(&self.manager_context, runtime_context)?; - Ok(remove_instance( - &mut self.registry, - runtime_context.service(), - runtime_context.instance(), - )) + match self.contract.service_targets.get(runtime_context.service()) { + Some(_) => Err(RadrootsRuntimeManagerError::MetadataOnlyServiceTarget), + None => Err(RadrootsRuntimeManagerError::UnsupportedServiceTarget), + } } } @@ -129,6 +127,7 @@ pub struct ManagedRuntimeTarget { context: RuntimeContext, instance_source: RuntimeContextSource, runtime_group: ManagedRuntimeGroup, + service_target: HardenedServiceTarget, management_mode: Option<String>, mode_contract: Option<ManagementModeContract>, bootstrap: Option<BootstrapRuntimeContract>, @@ -153,6 +152,11 @@ impl ManagedRuntimeTarget { } #[must_use] + pub fn service_target(&self) -> &HardenedServiceTarget { + &self.service_target + } + + #[must_use] pub fn management_mode(&self) -> Option<&str> { self.management_mode.as_deref() } @@ -184,6 +188,7 @@ impl fmt::Debug for ManagedRuntimeTarget { .debug_struct("ManagedRuntimeTarget") .field("context", &self.context) .field("runtime_group", &self.runtime_group) + .field("service_target", &self.service_target) .field("predicted_paths", &self.predicted_paths) .finish_non_exhaustive() } @@ -293,6 +298,7 @@ pub fn load_management_context( contract: RadrootsRuntimeManagementContract, manager_context: RuntimeContext, ) -> Result<ManagedRuntimeContext, RadrootsRuntimeManagerError> { + crate::validate_hardened_management_contract(&contract)?; active_management_mode_for_profile(&contract, manager_context.profile())?; let shared_paths = resolve_shared_paths(&manager_context); let registry = load_registry(shared_paths.instance_registry_path())?; @@ -330,29 +336,28 @@ pub fn resolve_runtime_target( ensure_context_scope(&context.manager_context, &runtime_context)?; let runtime_id = runtime_context.service().as_str(); let runtime_group = runtime_group(&context.contract, runtime_id); + let service_target = context + .contract + .service_targets + .get(runtime_context.service()) + .cloned() + .ok_or(RadrootsRuntimeManagerError::UnsupportedServiceTarget)?; let bootstrap = context.contract.bootstrap.get(runtime_id).cloned(); - let management_mode = bootstrap - .as_ref() - .map(|entry| entry.management_mode.clone()); + let management_mode = Some( + active_management_mode_for_profile(&context.contract, runtime_context.profile())? + .to_owned(), + ); let mode_contract = management_mode .as_ref() .and_then(|mode_id| context.contract.mode.get(mode_id).cloned()); - let instance_record = context - .registry - .instances - .iter() - .find(|record| record.matches_context(&runtime_context)) - .cloned(); - let predicted_paths = matches!( - runtime_group, - ManagedRuntimeGroup::ActiveManagedTarget | ManagedRuntimeGroup::DefinedManagedTarget - ) - .then(|| resolve_instance_paths(&context.shared_paths, &runtime_context)); + let instance_record = None; + let predicted_paths = None; Ok(ManagedRuntimeTarget { instance_source: runtime_context.sources().instance(), context: runtime_context, runtime_group, + service_target, management_mode, mode_contract, bootstrap, @@ -400,11 +405,7 @@ pub fn inspect_runtime_status( target: &ManagedRuntimeTarget, lifecycle_actions: &[String], ) -> ManagedRuntimeInspection<ManagedRuntimeStatusInspection> { - let availability = if target.runtime_group == ManagedRuntimeGroup::Unknown { - ManagedRuntimeInspectionAvailability::Unconfigured - } else { - ManagedRuntimeInspectionAvailability::Success - }; + let availability = managed_inspection_availability(target); let (health_state, health_source) = infer_health_state(target); ManagedRuntimeInspection { @@ -430,7 +431,7 @@ pub fn inspect_runtime_status( preferred_cli_binding: target .bootstrap .as_ref() - .map(|entry| entry.preferred_cli_binding), + .map(BootstrapRuntimeContract::preferred_cli_binding), install_state: target .instance_record .as_ref() @@ -453,14 +454,16 @@ pub fn inspect_runtime_logs( target: &ManagedRuntimeTarget, ) -> ManagedRuntimeInspection<ManagedRuntimeLogsInspection> { let availability = managed_inspection_availability(target); - let stdout_log_present = target - .predicted_paths - .as_ref() - .is_some_and(|paths| paths.stdout_log_path().exists()); - let stderr_log_present = target - .predicted_paths - .as_ref() - .is_some_and(|paths| paths.stderr_log_path().exists()); + let stdout_log_present = (availability == ManagedRuntimeInspectionAvailability::Success) + && target + .predicted_paths + .as_ref() + .is_some_and(|paths| paths.stdout_log_path().exists()); + let stderr_log_present = (availability == ManagedRuntimeInspectionAvailability::Success) + && target + .predicted_paths + .as_ref() + .is_some_and(|paths| paths.stderr_log_path().exists()); ManagedRuntimeInspection { availability, @@ -483,12 +486,15 @@ pub fn inspect_runtime_config( target: &ManagedRuntimeTarget, ) -> ManagedRuntimeInspection<ManagedRuntimeConfigInspection> { let availability = managed_inspection_availability(target); - let config_path = target.instance_record.as_ref().and_then(|_| { - target - .predicted_paths - .as_ref() - .map(ManagedRuntimeInstancePaths::config_path) - }); + let config_path = (availability == ManagedRuntimeInspectionAvailability::Success) + .then_some(()) + .and(target.instance_record.as_ref()) + .and_then(|_| { + target + .predicted_paths + .as_ref() + .map(ManagedRuntimeInstancePaths::config_path) + }); let config_present = config_path.as_deref().is_some_and(Path::exists); ManagedRuntimeInspection { @@ -507,23 +513,11 @@ pub fn inspect_runtime_config( }, source: "runtime context + typed instance registry".to_owned(), detail: config_detail(target, config_path.is_some()), - config_format: target - .bootstrap - .as_ref() - .map(|entry| entry.config_format.clone()), + config_format: Some(target.service_target.config_format().as_str().to_owned()), config_present, - requires_bootstrap_secret: target - .bootstrap - .as_ref() - .map(|entry| entry.requires_bootstrap_secret), - requires_config_bootstrap: target - .bootstrap - .as_ref() - .map(|entry| entry.requires_config_bootstrap), - requires_signer_provider: target - .bootstrap - .as_ref() - .map(|entry| entry.requires_signer_provider), + requires_bootstrap_secret: None, + requires_config_bootstrap: Some(true), + requires_signer_provider: None, }, } } @@ -693,6 +687,12 @@ fn unknown_runtime_detail(target: &ManagedRuntimeTarget) -> String { } fn infer_health_state(target: &ManagedRuntimeTarget) -> (&'static str, &'static str) { + if target.runtime_group != ManagedRuntimeGroup::ActiveManagedTarget { + return ( + health_state_label(ManagedRuntimeHealthState::NotInstalled), + "metadata_only", + ); + } let Some(record) = &target.instance_record else { return ( health_state_label(ManagedRuntimeHealthState::NotInstalled), @@ -795,82 +795,12 @@ mod tests { inspect_runtime_logs, inspect_runtime_status, load_management_context, resolve_runtime_target, runtime_group, }; - use crate::{ManagedRuntimeInstallState, RadrootsRuntimeManagerError, parse_contract_str}; - - const CONTRACT: &str = r#" -schema = "radroots-runtime-management" -schema_version = 1 -owner_doc = "owner" -runtime_registry = "registry.toml" -distribution_contract = "distribution.toml" -capabilities_contract = "capabilities.toml" - -[defaults] -instance_cardinality = "multiple" -managed_runtime_lookup = "typed_instance_registry" -explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true -global_path_mutation_forbidden = true - -[management_clients] -active = ["cli"] - -[managed_runtime_targets] -active = ["radrootsd"] -defined = ["myc", "rhi"] -bootstrap_only = ["hyf"] - -[lifecycle] -actions = ["install", "start"] -health_states = ["not_installed", "running"] - -[mode.interactive_user_managed] -contract_state = "active" -platforms = ["linux"] -supported_profiles = ["repo_local"] -service_manager_integration = false -uses_absolute_binary_paths = true -default_instance_cardinality = "multiple" - -[mode.service_host_managed] -contract_state = "defined" -platforms = ["linux"] -supported_profiles = ["service_host"] -service_manager_integration = true -uses_absolute_binary_paths = true -default_instance_cardinality = "multiple" - -[paths.interactive_user_managed] -shared_namespace = "obsolete" -instance_registry_root_class = "config" -instance_registry_rel = "obsolete" -artifact_cache_root_class = "cache" -artifact_cache_rel = "obsolete" -install_root_class = "data" -install_root_rel = "obsolete" -state_root_class = "data" -state_root_rel = "obsolete" -logs_root_class = "logs" -logs_root_rel = "obsolete" -run_root_class = "run" -run_root_rel = "obsolete" -secrets_root_class = "secrets" -secrets_namespace_rel = "obsolete" - -[instance_metadata] -required_fields = ["service_id", "instance_id"] - -[bootstrap.radrootsd] -runtime_id = "radrootsd" -management_mode = "interactive_user_managed" -default_instance_id = "local" -install_strategy = "archive_unpack" -config_format = "toml" -requires_bootstrap_secret = true -requires_config_bootstrap = true -requires_signer_provider = false -health_surface = "jsonrpc_status" -preferred_cli_binding = true -"#; + use crate::{ + HARDENED_MANAGEMENT_CONTRACT, ManagedRuntimeInstallState, RadrootsRuntimeManagerError, + parse_contract_str, + }; + + const CONTRACT: &str = HARDENED_MANAGEMENT_CONTRACT; fn context(service: &str, instance: &str, root: &std::path::Path) -> RuntimeContext { RuntimeContext::resolve( @@ -918,9 +848,10 @@ preferred_cli_binding = true .expect("active mode"), "interactive_user_managed" ); - assert!( + assert_eq!( active_management_mode_for_profile(&contract, RadrootsPathProfile::ServiceHost) - .is_err() + .expect("service-host mode"), + "service_host_managed" ); } @@ -930,26 +861,32 @@ preferred_cli_binding = true let mut manager = manager(dir.path()); let primary = context("myc", "primary", dir.path()); let secondary = context("myc", "secondary", dir.path()); - manager - .register_instance(&primary, ManagedRuntimeInstallState::Configured) - .expect("register primary"); let primary_target = resolve_runtime_target(&manager, primary.clone()).expect("primary"); let secondary_target = resolve_runtime_target(&manager, secondary.clone()).expect("secondary"); - assert!(primary_target.instance_record.is_some()); + assert!(primary_target.instance_record.is_none()); assert!(secondary_target.instance_record.is_none()); assert_eq!(primary_target.context, primary); assert_eq!(secondary_target.context, secondary); assert_ne!( - primary_target.predicted_paths, - secondary_target.predicted_paths + primary_target.context.paths(), + secondary_target.context.paths() ); assert_eq!( primary_target.runtime_group, ManagedRuntimeGroup::DefinedManagedTarget ); - assert!(primary_target.predicted_paths.is_some()); + assert!(primary_target.predicted_paths.is_none()); + assert_eq!(primary_target.service_target().service_id().as_str(), "myc"); + assert_eq!( + manager.register_instance(&primary, ManagedRuntimeInstallState::Configured), + Err(RadrootsRuntimeManagerError::MetadataOnlyServiceTarget) + ); + assert_eq!( + manager.remove_instance(&primary), + Err(RadrootsRuntimeManagerError::MetadataOnlyServiceTarget) + ); } #[test] @@ -977,46 +914,41 @@ preferred_cli_binding = true let contract = manager.contract(); assert_eq!( runtime_group(contract, "radrootsd"), - ManagedRuntimeGroup::ActiveManagedTarget + ManagedRuntimeGroup::Unknown ); assert_eq!( runtime_group(contract, "myc"), ManagedRuntimeGroup::DefinedManagedTarget ); - assert_eq!( - runtime_group(contract, "hyf"), - ManagedRuntimeGroup::BootstrapOnly - ); + assert_eq!(runtime_group(contract, "hyf"), ManagedRuntimeGroup::Unknown); assert_eq!( runtime_group(contract, "unknown"), ManagedRuntimeGroup::Unknown ); - let unknown = resolve_runtime_target(&manager, context("unknown", "default", dir.path())) - .expect("unknown target"); - assert!(unknown.predicted_paths.is_none()); assert_eq!( - inspect_runtime_status(&unknown, &[]).availability, - ManagedRuntimeInspectionAvailability::Unconfigured + resolve_runtime_target(&manager, context("unknown", "default", dir.path())) + .expect_err("unknown target"), + RadrootsRuntimeManagerError::UnsupportedServiceTarget ); } #[test] - fn status_uses_manager_tracking_without_disclosing_paths() { + fn status_is_metadata_only_without_probing_manager_tracking() { let dir = tempdir().expect("tempdir"); - let mut manager = manager(dir.path()); - let service = context("radrootsd", "local", dir.path()); - manager - .register_instance(&service, ManagedRuntimeInstallState::Configured) - .expect("register service"); + let manager = manager(dir.path()); + let service = context("myc", "primary", dir.path()); let target = resolve_runtime_target(&manager, service).expect("target"); - let paths = target.predicted_paths.as_ref().expect("paths"); - fs::create_dir_all(paths.run_dir()).expect("run dir"); - fs::write(paths.pid_file_path(), std::process::id().to_string()).expect("pid"); + assert!(target.predicted_paths().is_none()); let status = inspect_runtime_status(&target, &["start".to_owned()]); - assert_eq!(status.view.health_state, "running"); - assert_eq!(status.view.health_source, "process_probe"); + assert_eq!( + status.availability, + ManagedRuntimeInspectionAvailability::Unsupported + ); + assert_eq!(status.view.health_state, "not_installed"); + assert_eq!(status.view.health_source, "metadata_only"); + assert!(status.view.lifecycle_actions.is_empty()); assert_eq!( status.view.instance_source, RuntimeContextSource::BootstrapCli @@ -1027,26 +959,35 @@ preferred_cli_binding = true } #[test] - fn log_and_config_inspections_use_manager_and_service_context_paths() { + fn log_and_config_inspections_remain_non_io_for_metadata_only_services() { let dir = tempdir().expect("tempdir"); - let mut manager = manager(dir.path()); - let service = context("radrootsd", "local", dir.path()); - manager - .register_instance(&service, ManagedRuntimeInstallState::Configured) - .expect("register service"); + let manager = manager(dir.path()); + let service = context("rhi", "default", dir.path()); let target = resolve_runtime_target(&manager, service).expect("target"); - let paths = target.predicted_paths.as_ref().expect("paths"); - fs::create_dir_all(paths.logs_dir()).expect("logs"); - fs::write(paths.stdout_log_path(), "stdout").expect("stdout"); - let config_path = paths.config_path(); - fs::create_dir_all(config_path.parent().expect("config parent")).expect("config parent"); - fs::write(&config_path, "enabled = true").expect("config"); + assert!(target.predicted_paths().is_none()); + fs::create_dir_all(target.context().paths().logs()).expect("service logs"); + fs::write(target.context().paths().logs().join("stdout.log"), "stdout") + .expect("service stdout"); + fs::create_dir_all(target.context().paths().config()).expect("service config"); + fs::write( + target.context().paths().config().join("config.toml"), + "enabled = true", + ) + .expect("service config"); let logs = inspect_runtime_logs(&target); - assert!(logs.view.stdout_log_present); + assert_eq!( + logs.availability, + ManagedRuntimeInspectionAvailability::Unsupported + ); + assert!(!logs.view.stdout_log_present); assert!(!logs.view.stderr_log_present); let config = inspect_runtime_config(&target); - assert!(config.view.config_present); + assert_eq!( + config.availability, + ManagedRuntimeInspectionAvailability::Unsupported + ); + assert!(!config.view.config_present); assert_eq!(config.view.config_format.as_deref(), Some("toml")); for rendered in [format!("{logs:?}"), format!("{config:?}")] { assert!(!rendered.contains(dir.path().to_string_lossy().as_ref())); @@ -1057,24 +998,51 @@ preferred_cli_binding = true fn actions_do_not_mutate_bindings_for_any_group() { let dir = tempdir().expect("tempdir"); let manager = manager(dir.path()); - for (service, expected) in [ - ( - "radrootsd", - ManagedRuntimeInspectionAvailability::Unsupported, - ), - ("myc", ManagedRuntimeInspectionAvailability::Unsupported), - ("hyf", ManagedRuntimeInspectionAvailability::Unsupported), - ( - "unknown", - ManagedRuntimeInspectionAvailability::Unconfigured, - ), - ] { + for service in ["myc", "rhi"] { let target = resolve_runtime_target(&manager, context(service, "default", dir.path())) .expect("target"); let action = inspect_runtime_action(&target, ManagedRuntimeLifecycleAction::ConfigSet); - assert_eq!(action.availability, expected); + assert_eq!( + action.availability, + ManagedRuntimeInspectionAvailability::Unsupported + ); assert!(!action.view.mutates_bindings); assert!(action.view.next_step.is_none()); } } + + #[test] + fn durable_management_contract_requires_explicit_instances_and_rejects_any_drift() { + let contract = parse_contract_str(HARDENED_MANAGEMENT_CONTRACT).expect("contract"); + assert_eq!(contract.service_targets.len(), 2); + assert!(contract.bootstrap.is_empty()); + + for raw in [ + HARDENED_MANAGEMENT_CONTRACT.replace("schema_version = 1", "schema_version = 2"), + HARDENED_MANAGEMENT_CONTRACT.replace( + "defined = [\"myc\", \"rhi\"]", + "active = [\"myc\"]\ndefined = [\"rhi\"]", + ), + HARDENED_MANAGEMENT_CONTRACT.replace( + "managed_runtime_lookup = \"typed_instance_registry\"", + "managed_runtime_lookup = \"different\"", + ), + HARDENED_MANAGEMENT_CONTRACT.replace("active = [\"cli\"]", "active = [\"other\"]"), + HARDENED_MANAGEMENT_CONTRACT.replace( + "supported_profiles = [\"interactive\", \"repo_local\"]", + "supported_profiles = [\"interactive\"]", + ), + HARDENED_MANAGEMENT_CONTRACT.replace( + "required_fields = [\"service_id\", \"instance_id\"]", + "required_fields = [\"service_id\"]", + ), + HARDENED_MANAGEMENT_CONTRACT.replace( + "distribution_contract = \"hardened-service-targets.v1.toml\"", + "distribution_contract = \"different.toml\"", + ), + format!("{HARDENED_MANAGEMENT_CONTRACT}\nunknown = true\n"), + ] { + assert!(parse_contract_str(&raw).is_err()); + } + } } diff --git a/crates/runtime_manager/src/model.rs b/crates/runtime_manager/src/model.rs @@ -1,8 +1,10 @@ +use radroots_runtime_distribution::HardenedServiceTargets; use radroots_runtime_paths::{InstanceId, RuntimeContext, ServiceId}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct RadrootsRuntimeManagementContract { pub schema: String, pub schema_version: u32, @@ -13,6 +15,7 @@ pub struct RadrootsRuntimeManagementContract { pub defaults: ManagementDefaults, pub management_clients: RuntimeGroups, pub managed_runtime_targets: RuntimeGroups, + pub service_targets: HardenedServiceTargets, pub lifecycle: LifecycleContract, pub mode: BTreeMap<String, ManagementModeContract>, pub paths: BTreeMap<String, ManagementPathContract>, @@ -21,6 +24,7 @@ pub struct RadrootsRuntimeManagementContract { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct ManagementDefaults { pub instance_cardinality: String, pub managed_runtime_lookup: String, @@ -29,6 +33,7 @@ pub struct ManagementDefaults { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)] +#[serde(deny_unknown_fields)] pub struct RuntimeGroups { #[serde(default)] pub active: Vec<String>, @@ -39,6 +44,7 @@ pub struct RuntimeGroups { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct LifecycleContract { #[serde(default)] pub actions: Vec<String>, @@ -49,6 +55,7 @@ pub struct LifecycleContract { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct ManagementModeContract { pub contract_state: String, #[serde(default)] @@ -63,6 +70,7 @@ pub struct ManagementModeContract { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct ManagementPathContract { pub shared_namespace: String, pub instance_registry_root_class: String, @@ -82,6 +90,7 @@ pub struct ManagementPathContract { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct InstanceMetadataContract { #[serde(default)] pub required_fields: Vec<String>, @@ -90,18 +99,28 @@ pub struct InstanceMetadataContract { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct BootstrapRuntimeContract { - pub runtime_id: String, - pub management_mode: String, - pub default_instance_id: String, - pub install_strategy: String, - pub config_format: String, - pub requires_bootstrap_secret: bool, - pub requires_config_bootstrap: bool, - pub requires_signer_provider: bool, - pub health_surface: String, - pub preferred_cli_binding: bool, - pub notes: Option<String>, + service_id: ServiceId, + default_instance_id: InstanceId, + preferred_cli_binding: bool, +} + +impl BootstrapRuntimeContract { + #[must_use] + pub fn service_id(&self) -> &ServiceId { + &self.service_id + } + + #[must_use] + pub fn default_instance_id(&self) -> &InstanceId { + &self.default_instance_id + } + + #[must_use] + pub const fn preferred_cli_binding(&self) -> bool { + self.preferred_cli_binding + } } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] @@ -144,6 +163,7 @@ pub struct ManagedRuntimeInstanceRecord { } impl ManagedRuntimeInstanceRecord { + #[cfg(test)] #[must_use] pub(crate) fn new(context: &RuntimeContext, install_state: ManagedRuntimeInstallState) -> Self { Self { diff --git a/crates/runtime_manager/src/paths.rs b/crates/runtime_manager/src/paths.rs @@ -187,6 +187,7 @@ pub(crate) fn resolve_shared_paths(context: &RuntimeContext) -> ManagedRuntimeSh } #[must_use] +#[cfg(test)] pub(crate) fn resolve_instance_paths( shared: &ManagedRuntimeSharedPaths, context: &RuntimeContext, diff --git a/crates/runtime_manager/src/registry.rs b/crates/runtime_manager/src/registry.rs @@ -87,6 +87,7 @@ fn normalize_registry( Ok(registry) } +#[cfg(test)] pub(crate) fn upsert_instance( registry: &mut ManagedRuntimeInstanceRegistry, record: ManagedRuntimeInstanceRecord, @@ -117,6 +118,7 @@ pub fn instance<'a>( .find(|record| record.service_id() == service_id && record.instance_id() == instance_id) } +#[cfg(test)] pub(crate) fn remove_instance( registry: &mut ManagedRuntimeInstanceRegistry, service_id: &ServiceId, diff --git a/crates/runtime_manager/tests/fixtures/hardened_service_management.v1.toml b/crates/runtime_manager/tests/fixtures/hardened_service_management.v1.toml @@ -0,0 +1,90 @@ +schema = "radroots-runtime-management" +schema_version = 1 +owner_doc = "service-hardening-v1" +runtime_registry = "registry.toml" +distribution_contract = "hardened-service-targets.v1.toml" +capabilities_contract = "service-capabilities.v1.toml" + +[defaults] +instance_cardinality = "multiple" +managed_runtime_lookup = "typed_instance_registry" +explicit_runtime_endpoint_overrides_precede_managed_instance_binding = true +global_path_mutation_forbidden = true + +[management_clients] +active = ["cli"] + +[managed_runtime_targets] +defined = ["myc", "rhi"] + +[lifecycle] +actions = [] +destructive_actions = [] +health_states = [] + +[mode.interactive_user_managed] +contract_state = "active" +platforms = ["linux", "macos"] +supported_profiles = ["interactive", "repo_local"] +service_manager_integration = false +uses_absolute_binary_paths = true +default_instance_cardinality = "multiple" + +[mode.service_host_managed] +contract_state = "active" +platforms = ["linux"] +supported_profiles = ["service_host"] +service_manager_integration = true +uses_absolute_binary_paths = true +default_instance_cardinality = "multiple" + +[paths.context_bound] +shared_namespace = "services" +instance_registry_root_class = "config" +instance_registry_rel = "instances.toml" +artifact_cache_root_class = "cache" +artifact_cache_rel = "artifacts" +install_root_class = "state" +install_root_rel = "installs" +state_root_class = "state" +state_root_rel = "state.sqlite" +logs_root_class = "logs" +logs_root_rel = "instances" +run_root_class = "run" +run_root_rel = "admin.sock" +secrets_root_class = "secrets" +secrets_namespace_rel = "credentials" + +[instance_metadata] +required_fields = ["service_id", "instance_id"] +optional_fields = [] + +[service_targets.myc] +service_id = "myc" +instance_support = "multiple" +config_format = "toml" +state_initialization = "explicit" +run_state_policy = "existing_only" +admin_transport = "http11_over_unix_domain_socket" +admin_base_path = "/v1" +admin_contract_version = 1 +status_surface = "local_admin_service_status_v1" +operations_surface = "cached_livez_readyz_metrics" +support_posture = "target" +tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] + +[service_targets.rhi] +service_id = "rhi" +instance_support = "multiple" +config_format = "toml" +state_initialization = "explicit" +run_state_policy = "existing_only" +admin_transport = "http11_over_unix_domain_socket" +admin_base_path = "/v1" +admin_contract_version = 1 +status_surface = "local_admin_service_status_v1" +operations_surface = "cached_livez_readyz_metrics" +support_posture = "target" +tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] + +[bootstrap] diff --git a/crates/runtime_manager/tests/service_target_boundary.rs b/crates/runtime_manager/tests/service_target_boundary.rs @@ -0,0 +1,25 @@ +const MANAGEMENT_FIXTURE: &str = include_str!("fixtures/hardened_service_management.v1.toml"); + +#[test] +fn hardened_services_remain_metadata_only_in_management_contract() { + for forbidden in [ + "active = [\"myc", + "active = [\"rhi", + "install_strategy", + "binary_name", + "artifact_adapter", + "qualified", + "default_instance_id", + "preferred_cli_binding", + ] { + assert!( + !MANAGEMENT_FIXTURE.contains(forbidden), + "management fixture contains deferred authority `{forbidden}`" + ); + } + + assert!(MANAGEMENT_FIXTURE.contains("defined = [\"myc\", \"rhi\"]")); + assert!(MANAGEMENT_FIXTURE.contains("actions = []")); + assert!(MANAGEMENT_FIXTURE.contains("destructive_actions = []")); + assert!(MANAGEMENT_FIXTURE.contains("[bootstrap]")); +}