v0.2.6-pre.003

This commit is contained in:
2026-08-20 17:08:32 +02:00
parent 1a57cdeef0
commit f9f9ac79b6
40 changed files with 1735 additions and 122 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs
// version: 6
// version: 7
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)]
@@ -266,6 +266,10 @@ fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJ
if let std::result::Result::Err(error) = composite_validation {
return std::result::Result::Err(error);
}
let wallet_validation = crate::validate_wallet_document_contract(document);
if let std::result::Result::Err(error) = wallet_validation {
return std::result::Result::Err(error);
}
if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
return std::result::Result::Ok(());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 14
// version: 15
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -9,8 +9,8 @@
//!
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
//! provenance. Standard Logging and HTTP Transport documents map explicitly to their runtime settings contracts, while the management surface provides
//! typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
//! provenance. Standard Logging, HTTP Transport and Wallet documents map explicitly to their runtime consumer contracts, while the management surface
//! provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
mod bootstrap;
mod composite;
@@ -25,6 +25,7 @@ mod profile;
mod registry;
mod sensitivity;
mod transport;
mod wallet;
/// Bootstrap argument used to replace the configuration document root.
pub use self::bootstrap::ARG_CFG_PATH;
@@ -138,6 +139,8 @@ pub use self::registry::ConfigFileId;
pub use self::registry::ConfigFileKind;
/// Registry of KSP-known logical Config files and their replaceable physical filenames.
pub use self::registry::ConfigFileRegistry;
/// Default physical filename for the Wallet Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME;
/// Default physical filename for the generic composite JSON Schema document.
pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
/// Default physical filename for the standard Logging configuration document.
@@ -148,16 +151,26 @@ pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
/// Default physical filename for the standard Wallet configuration document.
pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
/// Default physical filename for the standard Wallet JSON Schema document.
pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME;
/// Logical file identifier for the Wallet Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
/// Logical file identifier for the generic composite JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
/// Logical file identifier for the standard Logging JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
/// Logical file identifier for the standard Logging configuration document.
pub use self::registry::FILE_ID_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport configuration document.
pub use self::registry::FILE_ID_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet configuration document.
pub use self::registry::FILE_ID_STD_WALLET;
/// Sensitivity assigned to one Config value after environment resolution.
pub use self::sensitivity::ConfigSensitivity;
/// Provenance segment participating in one resolved Config value.
@@ -170,6 +183,8 @@ pub use self::sensitivity::ResolvedConfigJson;
pub use self::sensitivity::ResolvedConfigText;
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
pub use self::transport::ResolvedTransportConfig;
/// Effective standard Wallet configuration resolved to validated filesystem roots.
pub use self::wallet::ResolvedWalletConfig;
/// Validates composite document contract.
pub(crate) use self::composite::validate_composite_document_contract;
@@ -189,3 +204,5 @@ pub(crate) use self::profile::load_resolved_profile_with_source;
pub(crate) use self::profile::validate_document_profile_contract;
/// Executes the crate-internal build registry operation for the owning module.
pub(crate) use self::registry::build_registry;
/// Validates standard Wallet source path invariants before environment resolution.
pub(crate) use self::wallet::validate_wallet_document_contract;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/logging.rs
// version: 1
// version: 2
/// Effective standard Logging configuration resolved from Config and mapped to the Logging runtime contract.
#[derive(Clone, Eq, PartialEq)]
@@ -103,6 +103,24 @@ impl crate::ConfigDocumentEngine {
};
return resolve_logging_profile(&profile, environment);
}
/// Maps an already resolved standard Logging profile to the runtime Logging adapter while preserving its selection provenance.
///
/// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.logging`.
pub fn resolve_logging_config_profile(
&self,
profile: &crate::ResolvedConfigProfile,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedLoggingConfig> {
if profile.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Logging document"));
}
let descriptor = self.registry().descriptor(profile.file_id());
if let std::result::Result::Err(error) = descriptor {
return std::result::Result::Err(error);
}
return resolve_logging_profile(profile, environment);
}
}
#[derive(serde::Deserialize)]

View File

@@ -1,8 +1,10 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 7
// version: 8
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
/// Default physical filename for the Wallet Desk composite configuration document.
pub const DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME: &str = "composite.ksp-app-wallet-desk.json";
/// Default physical filename for the generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Default physical filename for the standard Logging configuration document.
@@ -13,16 +15,26 @@ pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Default physical filename for the standard Wallet configuration document.
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
/// Default physical filename for the standard Wallet JSON Schema document.
pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json";
/// Logical file identifier for the Wallet Desk composite configuration document.
pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-wallet-desk";
/// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the standard Wallet JSON Schema document.
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
/// Logical file identifier for the standard Logging configuration document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard HTTP Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Logical file identifier for the standard Wallet configuration document.
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";
/// Stable logical identifier for a Config-managed file.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -128,6 +140,22 @@ pub struct ConfigFileRegistry {
impl ConfigFileRegistry {
/// Creates the registry containing the KSP default file mappings known in the current release.
pub fn defaults() -> ksp_core_lib::Result<Self> {
let composite = ConfigFileDescriptor::new(
FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK,
ConfigFileKind::Config,
DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_COMPOSITE),
);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
let composite_schema = match composite_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging = ConfigFileDescriptor::new(
FILE_ID_STD_LOGGING,
ConfigFileKind::Config,
@@ -160,13 +188,23 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
let composite_schema = match composite_schema {
let wallet = ConfigFileDescriptor::new(
FILE_ID_STD_WALLET,
ConfigFileKind::Config,
DEFAULT_STD_WALLET_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_STD_WALLET),
);
let wallet = match wallet {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
let wallet_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_WALLET, ConfigFileKind::Schema, DEFAULT_STD_WALLET_SCHEMA_FILENAME, std::option::Option::None);
let wallet_schema = match wallet_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::build_registry([composite, composite_schema, logging, logging_schema, transport, transport_schema, wallet, wallet_schema]);
}
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.

View File

@@ -0,0 +1,290 @@
// file: crates/ksp-config-lib/src/wallet.rs
// version: 1
/// Effective standard Wallet configuration resolved from Config.
#[derive(Clone, Eq, PartialEq)]
pub struct ResolvedWalletConfig {
effective: crate::ResolvedConfigJson,
effective_wallets_directory: std::path::PathBuf,
file_id: crate::ConfigFileId,
profile_id: String,
selection_source: crate::ConfigProfileSelectionSource,
source_path: std::path::PathBuf,
wallets_directory: std::path::PathBuf,
wallets_subdirectory: std::option::Option<std::path::PathBuf>,
}
impl ResolvedWalletConfig {
/// Returns the detailed environment-resolved effective Config view.
#[must_use]
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
return &self.effective;
}
/// Returns the effective Wallet directory after applying the optional profile subdirectory.
#[must_use]
pub fn effective_wallets_directory(&self) -> &std::path::Path {
return self.effective_wallets_directory.as_path();
}
/// Returns the logical Config document identifier used by this runtime configuration.
#[must_use]
pub const fn file_id(&self) -> &crate::ConfigFileId {
return &self.file_id;
}
/// Returns the selected standard Wallet profile identifier.
#[must_use]
pub fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the source that selected the standard Wallet profile.
#[must_use]
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
return self.selection_source;
}
/// Returns the physical source Config document path.
#[must_use]
pub fn source_path(&self) -> &std::path::Path {
return self.source_path.as_path();
}
/// Returns the resolved global Wallet root directory.
#[must_use]
pub fn wallets_directory(&self) -> &std::path::Path {
return self.wallets_directory.as_path();
}
/// Returns the validated optional relative Wallet subdirectory selected by the profile.
#[must_use]
pub fn wallets_subdirectory(&self) -> std::option::Option<&std::path::Path> {
return self.wallets_subdirectory.as_deref();
}
}
impl std::fmt::Debug for ResolvedWalletConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("ResolvedWalletConfig")
.field("effective", &self.effective)
.field("effective_wallets_directory", &self.effective_wallets_directory)
.field("file_id", &self.file_id)
.field("profile_id", &self.profile_id)
.field("selection_source", &self.selection_source)
.field("source_path", &self.source_path)
.field("wallets_directory", &self.wallets_directory)
.field("wallets_subdirectory", &self.wallets_subdirectory)
.finish();
}
}
impl crate::ConfigDocumentEngine {
/// Loads the standard Wallet document, selects a profile, resolves environment placeholders and validates the effective directory contract.
///
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. `wallets_directory` may be absolute
/// or relative to the process current working directory. `wallets_subdirectory` is always relative and may contain nested normal path components only.
pub fn load_resolved_wallet_config(
&self,
requested_profile: std::option::Option<&str>,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedWalletConfig> {
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_WALLET);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = self.load_resolved_profile(&file_id, requested_profile);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return resolve_wallet_profile(&profile, environment);
}
/// Maps an already resolved standard Wallet profile to the runtime Wallet Config adapter.
///
/// This entry point is intended for profiles selected by a composite and preserves their original selection source instead of reclassifying the profile as
/// an explicit selection. The profile must reference `cfg.std.wallet`.
pub fn resolve_wallet_config_profile(
&self,
profile: &crate::ResolvedConfigProfile,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedWalletConfig> {
if profile.file_id().as_str() != crate::FILE_ID_STD_WALLET {
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Wallet document"));
}
let descriptor = self.registry().descriptor(profile.file_id());
if let std::result::Result::Err(error) = descriptor {
return std::result::Result::Err(error);
}
return resolve_wallet_profile(profile, environment);
}
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveWalletSource {
format_version: u32,
profile_id: String,
wallets_directory: String,
wallets_subdirectory: std::option::Option<String>,
}
/// Validates standard Wallet source path invariants that are meaningful before environment resolution.
pub(crate) fn validate_wallet_document_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
if document.file_id().as_str() != crate::FILE_ID_STD_WALLET {
return std::result::Result::Ok(());
}
let profiles = document.value().get("profiles").and_then(serde_json::Value::as_array);
let profiles = match profiles {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(()),
};
for (profile_index, profile) in profiles.iter().enumerate() {
let subdirectory = profile.get("wallets_subdirectory").and_then(serde_json::Value::as_str);
if let std::option::Option::Some(value) = subdirectory {
let validation = validate_relative_subdirectory(value);
if let std::result::Result::Err(_) = validation {
return std::result::Result::Err(
wallet_document_error(document, "wallets_subdirectory must contain only relative normal path components")
.with_context("profile_index", profile_index.to_string()),
);
}
}
}
return std::result::Result::Ok(());
}
fn resolve_wallet_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedWalletConfig> {
let effective = profile.resolve_effective_environment_detailed(environment);
let effective = match effective {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if effective.sensitivity().is_secret() {
return std::result::Result::Err(effective_error(profile, "standard Wallet configuration must not consume Secret environment values"));
}
let source = serde_json::from_value::<EffectiveWalletSource>(effective.value().clone());
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
effective_error(profile, "effective Wallet Config cannot be decoded into the runtime adapter contract").with_source(error),
);
},
};
if source.format_version != 1 {
return std::result::Result::Err(effective_error(profile, "effective Wallet format_version is unsupported"));
}
if source.profile_id != profile.profile_id() {
return std::result::Result::Err(effective_error(profile, "effective Wallet profile_id does not match the selected profile"));
}
let wallets_directory = resolve_wallets_directory(source.wallets_directory.as_str(), profile);
let wallets_directory = match wallets_directory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallets_subdirectory = resolve_wallets_subdirectory(source.wallets_subdirectory.as_deref(), profile);
let wallets_subdirectory = match wallets_subdirectory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective_wallets_directory = match &wallets_subdirectory {
std::option::Option::Some(value) => wallets_directory.join(value),
std::option::Option::None => wallets_directory.clone(),
};
return std::result::Result::Ok(ResolvedWalletConfig {
effective,
effective_wallets_directory,
file_id: profile.file_id().clone(),
profile_id: profile.profile_id().to_owned(),
selection_source: profile.selection_source(),
source_path: profile.path().to_path_buf(),
wallets_directory,
wallets_subdirectory,
});
}
fn resolve_wallets_directory(value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result<std::path::PathBuf> {
if value.trim().is_empty() {
return std::result::Result::Err(effective_field_error(profile, "wallets_directory", "effective Wallet wallets_directory must not be empty"));
}
let path = std::path::PathBuf::from(value);
if path.is_absolute() {
return std::result::Result::Ok(path);
}
let current_directory = std::env::current_dir();
return match current_directory {
std::result::Result::Ok(current_directory) => std::result::Result::Ok(current_directory.join(path)),
std::result::Result::Err(error) => std::result::Result::Err(
effective_field_error(profile, "wallets_directory", "process current working directory cannot be resolved").with_source(error),
),
};
}
fn resolve_wallets_subdirectory(
value: std::option::Option<&str>,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::option::Option<std::path::PathBuf>> {
let value = match value {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let validation = validate_relative_subdirectory(value);
if let std::result::Result::Err(()) = validation {
return std::result::Result::Err(effective_field_error(
profile,
"wallets_subdirectory",
"effective Wallet wallets_subdirectory must contain only relative normal path components",
));
}
return std::result::Result::Ok(std::option::Option::Some(std::path::PathBuf::from(value)));
}
fn validate_relative_subdirectory(value: &str) -> std::result::Result<(), ()> {
if value.trim().is_empty() {
return std::result::Result::Err(());
}
let path = std::path::Path::new(value);
if path.is_absolute() {
return std::result::Result::Err(());
}
let mut component_count: usize = 0;
for component in path.components() {
match component {
std::path::Component::Normal(_) => component_count += 1,
std::path::Component::CurDir | std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => {
return std::result::Result::Err(());
},
}
}
if component_count == 0 {
return std::result::Result::Err(());
}
return std::result::Result::Ok(());
}
fn wallet_document_error(document: &crate::ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "standard Wallet Config violates KSP semantic invariants")
.with_context("file_id", document.file_id().as_str())
.with_context("path", document.path().to_string_lossy().into_owned())
.with_context("reason", reason);
}
fn effective_field_error(profile: &crate::ResolvedConfigProfile, field: &'static str, reason: &'static str) -> ksp_core_lib::Error {
return effective_error(profile, reason).with_context("field", field);
}
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
.with_context("file_id", profile.file_id().as_str())
.with_context("path", profile.path().to_string_lossy().into_owned())
.with_context("profile_id", profile.profile_id())
.with_context("reason", reason);
}
#[cfg(test)]
#[path = "../unit_tests/wallet.rs"]
mod tests;