v0.1.3-pre.011
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 53
|
||||
# version: 54
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3-pre.10.fix.1"
|
||||
version = "0.1.3-pre.11"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/environment.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Default local environment file read by Config from the process launch directory.
|
||||
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
||||
@@ -21,14 +21,13 @@ pub enum ConfigEnvironmentSource {
|
||||
Fallback,
|
||||
}
|
||||
|
||||
/// One resolved Config environment variable and its winning source.
|
||||
///
|
||||
/// This type intentionally does not implement `Debug`: environment values may contain secrets. Sensitivity-aware safe representations are added by the next
|
||||
/// bounded Config prerelease.
|
||||
/// One resolved Config environment variable with real/safe values, sensitivity and its winning source.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ConfigEnvironmentValue {
|
||||
variable_name: String,
|
||||
value: String,
|
||||
safe_value: String,
|
||||
sensitivity: crate::ConfigSensitivity,
|
||||
source: ConfigEnvironmentSource,
|
||||
}
|
||||
|
||||
@@ -45,11 +44,41 @@ impl ConfigEnvironmentValue {
|
||||
return self.value.as_str();
|
||||
}
|
||||
|
||||
/// Returns the representation safe for ordinary diagnostics.
|
||||
#[must_use]
|
||||
pub fn safe_value(&self) -> &str {
|
||||
return self.safe_value.as_str();
|
||||
}
|
||||
|
||||
/// Returns the sensitivity derived from the variable namespace.
|
||||
#[must_use]
|
||||
pub const fn sensitivity(&self) -> crate::ConfigSensitivity {
|
||||
return self.sensitivity;
|
||||
}
|
||||
|
||||
/// Returns the source that won process > `.env` > fallback resolution.
|
||||
#[must_use]
|
||||
pub const fn source(&self) -> ConfigEnvironmentSource {
|
||||
return self.source;
|
||||
}
|
||||
|
||||
/// Returns provenance without embedding the resolved value.
|
||||
#[must_use]
|
||||
pub fn provenance(&self) -> crate::ConfigValueProvenance {
|
||||
return environment_provenance(self.variable_name.as_str(), self.source);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ConfigEnvironmentValue {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("ConfigEnvironmentValue")
|
||||
.field("variable_name", &self.variable_name)
|
||||
.field("safe_value", &self.safe_value)
|
||||
.field("sensitivity", &self.sensitivity)
|
||||
.field("source", &self.source)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Config-owned snapshot of KSP/KSPB process environment values and the local `.env` file.
|
||||
@@ -84,25 +113,13 @@ impl ConfigEnvironment {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::option::Option::Some(value) = self.process.get(variable_name) {
|
||||
return std::result::Result::Ok(ConfigEnvironmentValue {
|
||||
variable_name: variable_name.to_owned(),
|
||||
value: value.clone(),
|
||||
source: ConfigEnvironmentSource::Process,
|
||||
});
|
||||
return resolved_environment_value(variable_name, value.as_str(), ConfigEnvironmentSource::Process);
|
||||
}
|
||||
if let std::option::Option::Some(value) = self.dotenv.get(variable_name) {
|
||||
return std::result::Result::Ok(ConfigEnvironmentValue {
|
||||
variable_name: variable_name.to_owned(),
|
||||
value: value.clone(),
|
||||
source: ConfigEnvironmentSource::DotEnv,
|
||||
});
|
||||
return resolved_environment_value(variable_name, value.as_str(), ConfigEnvironmentSource::DotEnv);
|
||||
}
|
||||
if let std::option::Option::Some(value) = fallback {
|
||||
return std::result::Result::Ok(ConfigEnvironmentValue {
|
||||
variable_name: variable_name.to_owned(),
|
||||
value: value.to_owned(),
|
||||
source: ConfigEnvironmentSource::Fallback,
|
||||
});
|
||||
return resolved_environment_value(variable_name, value, ConfigEnvironmentSource::Fallback);
|
||||
}
|
||||
emit_missing_variable_warning(variable_name);
|
||||
return std::result::Result::Err(missing_variable_error(variable_name));
|
||||
@@ -110,20 +127,49 @@ impl ConfigEnvironment {
|
||||
|
||||
/// Resolves `${NAME}` and `${NAME:-fallback}` placeholders embedded in one UTF-8 string.
|
||||
///
|
||||
/// Multiple placeholders are supported. Fallback text is literal in this prerelease and is not recursively interpreted as another placeholder expression.
|
||||
/// This compatibility helper returns only the real runtime string. Use [`Self::resolve_text_detailed`] when safe value, sensitivity or provenance are
|
||||
/// needed.
|
||||
pub fn resolve_text(&self, source: &str) -> ksp_core_lib::Result<String> {
|
||||
let mut output = String::new();
|
||||
let resolved = self.resolve_text_detailed(source);
|
||||
return match resolved {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value.value().to_owned()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolves one UTF-8 string while preserving real/safe representations, strongest sensitivity and ordered provenance.
|
||||
///
|
||||
/// Multiple placeholders are supported. Fallback text is literal and inherits the sensitivity of the referenced variable. Literal-only strings are
|
||||
/// classified as `Internal`; when placeholders are present, the result sensitivity is the strongest placeholder sensitivity.
|
||||
pub fn resolve_text_detailed(&self, source: &str) -> ksp_core_lib::Result<crate::ResolvedConfigText> {
|
||||
let mut value = String::new();
|
||||
let mut safe_value = String::new();
|
||||
let mut provenance = std::vec::Vec::<crate::ConfigValueProvenance>::new();
|
||||
let mut sensitivity = crate::ConfigSensitivity::Public;
|
||||
let mut saw_placeholder = false;
|
||||
let mut remaining = source;
|
||||
loop {
|
||||
let start = remaining.find("${");
|
||||
let start = match start {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
output.push_str(remaining);
|
||||
return std::result::Result::Ok(output);
|
||||
if !remaining.is_empty() {
|
||||
value.push_str(remaining);
|
||||
safe_value.push_str(remaining);
|
||||
provenance.push(crate::ConfigValueProvenance::DocumentLiteral);
|
||||
}
|
||||
if !saw_placeholder {
|
||||
sensitivity = crate::ConfigSensitivity::Internal;
|
||||
}
|
||||
return std::result::Result::Ok(crate::ResolvedConfigText::new(value, safe_value, sensitivity, provenance));
|
||||
},
|
||||
};
|
||||
output.push_str(&remaining[..start]);
|
||||
let literal = &remaining[..start];
|
||||
if !literal.is_empty() {
|
||||
value.push_str(literal);
|
||||
safe_value.push_str(literal);
|
||||
provenance.push(crate::ConfigValueProvenance::DocumentLiteral);
|
||||
}
|
||||
let expression_and_tail = &remaining[start + 2..];
|
||||
let end = expression_and_tail.find('}');
|
||||
let end = match end {
|
||||
@@ -141,45 +187,48 @@ impl ConfigEnvironment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
output.push_str(resolved.value());
|
||||
saw_placeholder = true;
|
||||
sensitivity = sensitivity.strongest(resolved.sensitivity());
|
||||
value.push_str(resolved.value());
|
||||
safe_value.push_str(resolved.safe_value());
|
||||
provenance.push(resolved.provenance());
|
||||
remaining = &expression_and_tail[end + 1..];
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively resolves environment placeholders in JSON string values while preserving keys and non-string JSON values.
|
||||
/// Recursively resolves environment placeholders in JSON values and returns only the real runtime tree.
|
||||
///
|
||||
/// Use [`Self::resolve_json_detailed`] when safe value, sensitivity or per-location provenance are needed.
|
||||
pub fn resolve_json(&self, source: &serde_json::Value) -> ksp_core_lib::Result<serde_json::Value> {
|
||||
return match source {
|
||||
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => std::result::Result::Ok(source.clone()),
|
||||
serde_json::Value::String(value) => {
|
||||
let resolved = self.resolve_text(value.as_str());
|
||||
match resolved {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(serde_json::Value::String(value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
serde_json::Value::Array(values) => resolve_array(self, values),
|
||||
serde_json::Value::Object(values) => {
|
||||
let resolved = self.resolve_map(values);
|
||||
match resolved {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(serde_json::Value::Object(value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
let resolved = self.resolve_json_detailed(source);
|
||||
return match resolved {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value.value().clone()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Recursively resolves environment placeholders in one JSON object map while leaving the map keys unchanged.
|
||||
/// Recursively resolves environment placeholders in JSON values while preserving real/safe trees, strongest sensitivity and JSON-Pointer provenance.
|
||||
pub fn resolve_json_detailed(&self, source: &serde_json::Value) -> ksp_core_lib::Result<crate::ResolvedConfigJson> {
|
||||
let mut provenance = std::collections::BTreeMap::<String, std::vec::Vec<crate::ConfigValueProvenance>>::new();
|
||||
let resolved = resolve_json_node(self, source, "", &mut provenance);
|
||||
let (value, safe_value, sensitivity) = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ResolvedConfigJson::new(value, safe_value, sensitivity, provenance));
|
||||
}
|
||||
|
||||
/// Recursively resolves environment placeholders in one JSON object map while leaving map keys unchanged.
|
||||
pub fn resolve_map(&self, source: &serde_json::Map<String, serde_json::Value>) -> ksp_core_lib::Result<serde_json::Map<String, serde_json::Value>> {
|
||||
let mut output = serde_json::Map::<String, serde_json::Value>::new();
|
||||
for (key, value) in source {
|
||||
let resolved = self.resolve_json(value);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
output.insert(key.clone(), resolved);
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
let resolved = self.resolve_json(&serde_json::Value::Object(source.clone()));
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match resolved {
|
||||
serde_json::Value::Object(value) => std::result::Result::Ok(value),
|
||||
_ => std::result::Result::Err(invalid_placeholder_error("resolved JSON object changed shape")),
|
||||
};
|
||||
}
|
||||
|
||||
fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> {
|
||||
@@ -350,20 +399,117 @@ fn parse_placeholder_expression(expression: &str) -> ksp_core_lib::Result<(&str,
|
||||
return std::result::Result::Ok((variable_name, fallback));
|
||||
}
|
||||
|
||||
fn resolve_array(environment: &ConfigEnvironment, source: &[serde_json::Value]) -> ksp_core_lib::Result<serde_json::Value> {
|
||||
let mut output = std::vec::Vec::<serde_json::Value>::with_capacity(source.len());
|
||||
for value in source {
|
||||
let resolved = environment.resolve_json(value);
|
||||
let resolved = match resolved {
|
||||
fn resolved_environment_value(variable_name: &str, value: &str, source: ConfigEnvironmentSource) -> ksp_core_lib::Result<ConfigEnvironmentValue> {
|
||||
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
||||
let sensitivity = match sensitivity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let safe_value = if sensitivity.is_secret() { crate::REDACTED_CONFIG_VALUE.to_owned() } else { value.to_owned() };
|
||||
return std::result::Result::Ok(ConfigEnvironmentValue {
|
||||
variable_name: variable_name.to_owned(),
|
||||
value: value.to_owned(),
|
||||
safe_value,
|
||||
sensitivity,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
fn environment_provenance(variable_name: &str, source: ConfigEnvironmentSource) -> crate::ConfigValueProvenance {
|
||||
return match source {
|
||||
ConfigEnvironmentSource::Process => crate::ConfigValueProvenance::EnvironmentProcess { variable_name: variable_name.to_owned() },
|
||||
ConfigEnvironmentSource::DotEnv => crate::ConfigValueProvenance::EnvironmentDotEnv { variable_name: variable_name.to_owned() },
|
||||
ConfigEnvironmentSource::Fallback => crate::ConfigValueProvenance::EnvironmentFallback { variable_name: variable_name.to_owned() },
|
||||
};
|
||||
}
|
||||
|
||||
fn resolve_json_node(
|
||||
environment: &ConfigEnvironment,
|
||||
source: &serde_json::Value,
|
||||
pointer: &str,
|
||||
provenance: &mut std::collections::BTreeMap<String, std::vec::Vec<crate::ConfigValueProvenance>>,
|
||||
) -> ksp_core_lib::Result<(serde_json::Value, serde_json::Value, crate::ConfigSensitivity)> {
|
||||
return match source {
|
||||
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {
|
||||
provenance.insert(pointer.to_owned(), vec![crate::ConfigValueProvenance::DocumentLiteral]);
|
||||
std::result::Result::Ok((source.clone(), source.clone(), crate::ConfigSensitivity::Internal))
|
||||
},
|
||||
serde_json::Value::String(value) => {
|
||||
let resolved = environment.resolve_text_detailed(value.as_str());
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
provenance.insert(pointer.to_owned(), resolved.provenance().to_vec());
|
||||
std::result::Result::Ok((
|
||||
serde_json::Value::String(resolved.value().to_owned()),
|
||||
serde_json::Value::String(resolved.safe_value().to_owned()),
|
||||
resolved.sensitivity(),
|
||||
))
|
||||
},
|
||||
serde_json::Value::Array(values) => resolve_json_array(environment, values, pointer, provenance),
|
||||
serde_json::Value::Object(values) => resolve_json_object(environment, values, pointer, provenance),
|
||||
};
|
||||
}
|
||||
|
||||
fn resolve_json_array(
|
||||
environment: &ConfigEnvironment,
|
||||
source: &[serde_json::Value],
|
||||
pointer: &str,
|
||||
provenance: &mut std::collections::BTreeMap<String, std::vec::Vec<crate::ConfigValueProvenance>>,
|
||||
) -> ksp_core_lib::Result<(serde_json::Value, serde_json::Value, crate::ConfigSensitivity)> {
|
||||
let mut value = std::vec::Vec::<serde_json::Value>::with_capacity(source.len());
|
||||
let mut safe_value = std::vec::Vec::<serde_json::Value>::with_capacity(source.len());
|
||||
let mut sensitivity = crate::ConfigSensitivity::Public;
|
||||
if source.is_empty() {
|
||||
sensitivity = crate::ConfigSensitivity::Internal;
|
||||
}
|
||||
for (index, item) in source.iter().enumerate() {
|
||||
let child_pointer = format!("{pointer}/{index}");
|
||||
let resolved = resolve_json_node(environment, item, child_pointer.as_str(), provenance);
|
||||
let (resolved_value, resolved_safe_value, resolved_sensitivity) = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
output.push(resolved);
|
||||
value.push(resolved_value);
|
||||
safe_value.push(resolved_safe_value);
|
||||
sensitivity = sensitivity.strongest(resolved_sensitivity);
|
||||
}
|
||||
return std::result::Result::Ok(serde_json::Value::Array(output));
|
||||
return std::result::Result::Ok((serde_json::Value::Array(value), serde_json::Value::Array(safe_value), sensitivity));
|
||||
}
|
||||
|
||||
fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
|
||||
fn resolve_json_object(
|
||||
environment: &ConfigEnvironment,
|
||||
source: &serde_json::Map<String, serde_json::Value>,
|
||||
pointer: &str,
|
||||
provenance: &mut std::collections::BTreeMap<String, std::vec::Vec<crate::ConfigValueProvenance>>,
|
||||
) -> ksp_core_lib::Result<(serde_json::Value, serde_json::Value, crate::ConfigSensitivity)> {
|
||||
let mut value = serde_json::Map::<String, serde_json::Value>::new();
|
||||
let mut safe_value = serde_json::Map::<String, serde_json::Value>::new();
|
||||
let mut sensitivity = crate::ConfigSensitivity::Public;
|
||||
if source.is_empty() {
|
||||
sensitivity = crate::ConfigSensitivity::Internal;
|
||||
}
|
||||
for (key, item) in source {
|
||||
let escaped_key = escape_json_pointer_token(key.as_str());
|
||||
let child_pointer = format!("{pointer}/{escaped_key}");
|
||||
let resolved = resolve_json_node(environment, item, child_pointer.as_str(), provenance);
|
||||
let (resolved_value, resolved_safe_value, resolved_sensitivity) = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
value.insert(key.clone(), resolved_value);
|
||||
safe_value.insert(key.clone(), resolved_safe_value);
|
||||
sensitivity = sensitivity.strongest(resolved_sensitivity);
|
||||
}
|
||||
return std::result::Result::Ok((serde_json::Value::Object(value), serde_json::Value::Object(safe_value), sensitivity));
|
||||
}
|
||||
|
||||
fn escape_json_pointer_token(value: &str) -> String {
|
||||
return value.replace('~', "~0").replace('/', "~1");
|
||||
}
|
||||
|
||||
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
|
||||
if !has_supported_namespace(variable_name) {
|
||||
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! KSP-owned application configuration facade.
|
||||
//!
|
||||
//! `0.1.3-pre.010` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution, generic composite
|
||||
//! `0.1.3-pre.011` owns bootstrap roots, the logical file registry, generic JSON/JSON Schema loading, standard-document profile resolution, generic composite
|
||||
//! resolution and KSP/KSPB environment resolution through process + `.env` + fallback precedence. The standard Logging document remains the first registered
|
||||
//! runtime document. Sensitivity-aware safe values and persistence remain in later bounded prereleases.
|
||||
//! runtime document. Environment-derived values now preserve real/safe representations, sensitivity and provenance; persistence remains in a later bounded
|
||||
//! prerelease.
|
||||
|
||||
mod bootstrap;
|
||||
mod composite;
|
||||
@@ -17,6 +18,7 @@ mod environment;
|
||||
mod error;
|
||||
mod profile;
|
||||
mod registry;
|
||||
mod sensitivity;
|
||||
|
||||
/// Bootstrap argument used to replace the configuration document root.
|
||||
pub use self::bootstrap::ARG_CFG_PATH;
|
||||
@@ -40,7 +42,7 @@ pub use self::document::ConfigJsonDocument;
|
||||
pub use self::environment::ConfigEnvironment;
|
||||
/// Source that supplied one resolved Config environment variable.
|
||||
pub use self::environment::ConfigEnvironmentSource;
|
||||
/// One resolved Config environment variable and its winning source.
|
||||
/// One resolved Config environment variable with real/safe values, sensitivity and its winning source.
|
||||
pub use self::environment::ConfigEnvironmentValue;
|
||||
/// Versioned environment contract template expected at the repository/runtime root.
|
||||
pub use self::environment::DEFAULT_DOTENV_EXAMPLE_PATH;
|
||||
@@ -112,3 +114,13 @@ pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
||||
/// Logical file identifier for the standard Logging configuration document.
|
||||
pub use self::registry::FILE_ID_STD_LOGGING;
|
||||
/// Sensitivity assigned to one Config value after environment resolution.
|
||||
pub use self::sensitivity::ConfigSensitivity;
|
||||
/// Provenance segment participating in one resolved Config value.
|
||||
pub use self::sensitivity::ConfigValueProvenance;
|
||||
/// Replacement used for secret environment fragments in safe diagnostic representations.
|
||||
pub use self::sensitivity::REDACTED_CONFIG_VALUE;
|
||||
/// Recursively resolved JSON value preserving real/safe trees and provenance.
|
||||
pub use self::sensitivity::ResolvedConfigJson;
|
||||
/// One resolved Config string preserving real/safe representations and provenance.
|
||||
pub use self::sensitivity::ResolvedConfigText;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/profile.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
/// Origin of one top-level value in a resolved standard Config profile.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -83,9 +83,31 @@ impl ResolvedConfigProfile {
|
||||
return self.origins.get(key).copied();
|
||||
}
|
||||
|
||||
/// Resolves environment placeholders in the effective view while preserving this source profile and its Global/Profile provenance unchanged.
|
||||
/// Resolves environment placeholders in the effective view and returns only the real runtime map.
|
||||
///
|
||||
/// Use [`Self::resolve_effective_environment_detailed`] when safe value, sensitivity and environment provenance are required. Global/Profile provenance on
|
||||
/// this source profile remains unchanged in both cases.
|
||||
pub fn resolve_effective_environment(&self, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<serde_json::Map<String, serde_json::Value>> {
|
||||
return environment.resolve_map(&self.effective);
|
||||
let resolved = self.resolve_effective_environment_detailed(environment);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match resolved.value() {
|
||||
serde_json::Value::Object(value) => std::result::Result::Ok(value.clone()),
|
||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID,
|
||||
"resolved Config profile effective view changed JSON shape",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolves environment placeholders while preserving real/safe JSON trees, strongest sensitivity and JSON-Pointer environment provenance.
|
||||
///
|
||||
/// Top-level Global/Profile provenance remains available through [`Self::origin`]; the returned value adds literal/process/`.env`/fallback provenance for
|
||||
/// the environment-resolution stage.
|
||||
pub fn resolve_effective_environment_detailed(&self, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<crate::ResolvedConfigJson> {
|
||||
return environment.resolve_json_detailed(&serde_json::Value::Object(self.effective.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +115,8 @@ impl crate::ConfigDocumentEngine {
|
||||
/// Loads, validates and resolves one standard Config document to its default or explicitly requested profile.
|
||||
///
|
||||
/// Passing `None` selects the autonomous `default_profile` declared by the document. Passing `Some(profile_id)` selects that profile explicitly.
|
||||
/// Environment interpolation is intentionally not applied implicitly by profile selection. Call `ResolvedConfigProfile::resolve_effective_environment` with
|
||||
/// a Config-owned environment snapshot when an effective runtime view is required.
|
||||
/// Environment interpolation is intentionally not applied implicitly by profile selection. Call `ResolvedConfigProfile::resolve_effective_environment` for
|
||||
/// a real runtime map or `ResolvedConfigProfile::resolve_effective_environment_detailed` when safe value, sensitivity and provenance are also required.
|
||||
pub fn load_resolved_profile(
|
||||
&self,
|
||||
file_id: &crate::ConfigFileId,
|
||||
|
||||
209
crates/ksp-config-lib/src/sensitivity.rs
Normal file
209
crates/ksp-config-lib/src/sensitivity.rs
Normal file
@@ -0,0 +1,209 @@
|
||||
// file: crates/ksp-config-lib/src/sensitivity.rs
|
||||
// version: 1
|
||||
|
||||
/// Replacement used for secret environment fragments in safe diagnostic representations.
|
||||
pub const REDACTED_CONFIG_VALUE: &str = "********";
|
||||
|
||||
/// Sensitivity assigned to one Config value after environment resolution.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum ConfigSensitivity {
|
||||
/// Value may be exposed by a public projection when the consumer contract allows it.
|
||||
Public,
|
||||
/// Value is available to the runtime but is not generically public.
|
||||
Internal,
|
||||
/// Value must remain available to legitimate runtime/management consumers while being redacted from ordinary diagnostics.
|
||||
Secret,
|
||||
}
|
||||
|
||||
impl ConfigSensitivity {
|
||||
/// Classifies one supported KSP/KSPB environment variable by its namespace.
|
||||
pub fn from_variable_name(variable_name: &str) -> ksp_core_lib::Result<Self> {
|
||||
let validation = crate::environment::validate_supported_variable_name(variable_name);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if variable_name.starts_with("KSP_SECRET_") || variable_name.starts_with("KSPB_SECRET_") {
|
||||
return std::result::Result::Ok(Self::Secret);
|
||||
}
|
||||
if variable_name.starts_with("KSP_PUBLIC_") || variable_name.starts_with("KSPB_PUBLIC_") {
|
||||
return std::result::Result::Ok(Self::Public);
|
||||
}
|
||||
return std::result::Result::Ok(Self::Internal);
|
||||
}
|
||||
|
||||
/// Returns the strongest of two sensitivities using `Secret > Internal > Public`.
|
||||
#[must_use]
|
||||
pub const fn strongest(self, other: Self) -> Self {
|
||||
if self as u8 >= other as u8 {
|
||||
return self;
|
||||
}
|
||||
return other;
|
||||
}
|
||||
|
||||
/// Returns whether this sensitivity requires ordinary diagnostic redaction.
|
||||
#[must_use]
|
||||
pub const fn is_secret(self) -> bool {
|
||||
return matches!(self, Self::Secret);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provenance segment participating in one resolved Config value.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ConfigValueProvenance {
|
||||
/// Literal text/value came directly from the Config document.
|
||||
DocumentLiteral,
|
||||
/// Environment substitution came from the inherited process environment.
|
||||
EnvironmentProcess {
|
||||
/// Referenced variable name; never its value.
|
||||
variable_name: String,
|
||||
},
|
||||
/// Environment substitution came from the local `.env` file.
|
||||
EnvironmentDotEnv {
|
||||
/// Referenced variable name; never its value.
|
||||
variable_name: String,
|
||||
},
|
||||
/// Environment substitution used the placeholder/API fallback.
|
||||
EnvironmentFallback {
|
||||
/// Referenced variable name; never its value.
|
||||
variable_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl ConfigValueProvenance {
|
||||
/// Returns the referenced variable name for environment provenance.
|
||||
#[must_use]
|
||||
pub fn variable_name(&self) -> std::option::Option<&str> {
|
||||
return match self {
|
||||
Self::DocumentLiteral => std::option::Option::None,
|
||||
Self::EnvironmentProcess { variable_name } | Self::EnvironmentDotEnv { variable_name } | Self::EnvironmentFallback { variable_name } => {
|
||||
std::option::Option::Some(variable_name.as_str())
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the environment source represented by this provenance segment when applicable.
|
||||
#[must_use]
|
||||
pub const fn environment_source(&self) -> std::option::Option<crate::ConfigEnvironmentSource> {
|
||||
return match self {
|
||||
Self::DocumentLiteral => std::option::Option::None,
|
||||
Self::EnvironmentProcess { .. } => std::option::Option::Some(crate::ConfigEnvironmentSource::Process),
|
||||
Self::EnvironmentDotEnv { .. } => std::option::Option::Some(crate::ConfigEnvironmentSource::DotEnv),
|
||||
Self::EnvironmentFallback { .. } => std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// One resolved Config string with real/safe representations, sensitivity and ordered provenance.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ResolvedConfigText {
|
||||
value: String,
|
||||
safe_value: String,
|
||||
sensitivity: ConfigSensitivity,
|
||||
provenance: std::vec::Vec<ConfigValueProvenance>,
|
||||
}
|
||||
|
||||
impl ResolvedConfigText {
|
||||
pub(crate) fn new(value: String, safe_value: String, sensitivity: ConfigSensitivity, provenance: std::vec::Vec<ConfigValueProvenance>) -> Self {
|
||||
return Self { value, safe_value, sensitivity, provenance };
|
||||
}
|
||||
|
||||
/// Returns the real runtime value.
|
||||
#[must_use]
|
||||
pub fn value(&self) -> &str {
|
||||
return self.value.as_str();
|
||||
}
|
||||
|
||||
/// Returns the representation safe for ordinary diagnostics.
|
||||
#[must_use]
|
||||
pub fn safe_value(&self) -> &str {
|
||||
return self.safe_value.as_str();
|
||||
}
|
||||
|
||||
/// Returns the strongest sensitivity contributed by referenced environment placeholders.
|
||||
#[must_use]
|
||||
pub const fn sensitivity(&self) -> ConfigSensitivity {
|
||||
return self.sensitivity;
|
||||
}
|
||||
|
||||
/// Returns ordered provenance segments participating in the resolved string.
|
||||
#[must_use]
|
||||
pub fn provenance(&self) -> &[ConfigValueProvenance] {
|
||||
return self.provenance.as_slice();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ResolvedConfigText {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("ResolvedConfigText")
|
||||
.field("safe_value", &self.safe_value)
|
||||
.field("sensitivity", &self.sensitivity)
|
||||
.field("provenance", &self.provenance)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively resolved JSON value preserving a real tree, a safe tree and provenance indexed by JSON Pointer.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ResolvedConfigJson {
|
||||
value: serde_json::Value,
|
||||
safe_value: serde_json::Value,
|
||||
sensitivity: ConfigSensitivity,
|
||||
provenance: std::collections::BTreeMap<String, std::vec::Vec<ConfigValueProvenance>>,
|
||||
}
|
||||
|
||||
impl ResolvedConfigJson {
|
||||
pub(crate) fn new(
|
||||
value: serde_json::Value,
|
||||
safe_value: serde_json::Value,
|
||||
sensitivity: ConfigSensitivity,
|
||||
provenance: std::collections::BTreeMap<String, std::vec::Vec<ConfigValueProvenance>>,
|
||||
) -> Self {
|
||||
return Self { value, safe_value, sensitivity, provenance };
|
||||
}
|
||||
|
||||
/// Returns the real JSON tree intended for legitimate runtime consumers.
|
||||
#[must_use]
|
||||
pub const fn value(&self) -> &serde_json::Value {
|
||||
return &self.value;
|
||||
}
|
||||
|
||||
/// Returns the JSON tree safe for ordinary diagnostics.
|
||||
#[must_use]
|
||||
pub const fn safe_value(&self) -> &serde_json::Value {
|
||||
return &self.safe_value;
|
||||
}
|
||||
|
||||
/// Returns the strongest sensitivity found anywhere in the resolved JSON tree.
|
||||
#[must_use]
|
||||
pub const fn sensitivity(&self) -> ConfigSensitivity {
|
||||
return self.sensitivity;
|
||||
}
|
||||
|
||||
/// Returns provenance indexed by RFC 6901 JSON Pointer strings.
|
||||
#[must_use]
|
||||
pub const fn provenance(&self) -> &std::collections::BTreeMap<String, std::vec::Vec<ConfigValueProvenance>> {
|
||||
return &self.provenance;
|
||||
}
|
||||
|
||||
/// Returns provenance for one JSON Pointer when the resolved value recorded that location.
|
||||
#[must_use]
|
||||
pub fn provenance_at(&self, json_pointer: &str) -> std::option::Option<&[ConfigValueProvenance]> {
|
||||
return self.provenance.get(json_pointer).map(std::vec::Vec::as_slice);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ResolvedConfigJson {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("ResolvedConfigJson")
|
||||
.field("safe_value", &self.safe_value)
|
||||
.field("sensitivity", &self.sensitivity)
|
||||
.field("provenance", &self.provenance)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/sensitivity.rs"]
|
||||
mod tests;
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite and environment-resolution contracts.
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution and sensitivity contracts.
|
||||
|
||||
#[test]
|
||||
fn bootstrap_contract_is_available_from_crate_root() {
|
||||
@@ -143,3 +143,28 @@ fn environment_resolution_contract_is_available_from_crate_root() {
|
||||
assert_ne!(ksp_config_lib::ConfigEnvironmentSource::Process, ksp_config_lib::ConfigEnvironmentSource::DotEnv);
|
||||
assert_ne!(ksp_config_lib::ConfigEnvironmentSource::DotEnv, ksp_config_lib::ConfigEnvironmentSource::Fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sensitivity_and_safe_resolution_contracts_are_available_from_crate_root() {
|
||||
assert_eq!(
|
||||
ksp_config_lib::ConfigSensitivity::from_variable_name("KSP_PUBLIC_HOST").ok(),
|
||||
std::option::Option::Some(ksp_config_lib::ConfigSensitivity::Public),
|
||||
);
|
||||
assert_eq!(
|
||||
ksp_config_lib::ConfigSensitivity::from_variable_name("KSP_MODE").ok(),
|
||||
std::option::Option::Some(ksp_config_lib::ConfigSensitivity::Internal)
|
||||
);
|
||||
assert_eq!(
|
||||
ksp_config_lib::ConfigSensitivity::from_variable_name("KSP_SECRET_TOKEN").ok(),
|
||||
std::option::Option::Some(ksp_config_lib::ConfigSensitivity::Secret),
|
||||
);
|
||||
assert_eq!(ksp_config_lib::REDACTED_CONFIG_VALUE, "********");
|
||||
let detailed_text: fn(&ksp_config_lib::ConfigEnvironment, &str) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigText> =
|
||||
ksp_config_lib::ConfigEnvironment::resolve_text_detailed;
|
||||
let detailed_json: fn(&ksp_config_lib::ConfigEnvironment, &serde_json::Value) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigJson> =
|
||||
ksp_config_lib::ConfigEnvironment::resolve_json_detailed;
|
||||
let _ = (detailed_text, detailed_json);
|
||||
let provenance = ksp_config_lib::ConfigValueProvenance::EnvironmentFallback { variable_name: "KSP_SECRET_TOKEN".to_owned() };
|
||||
assert_eq!(provenance.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN"));
|
||||
assert_eq!(provenance.environment_source(), std::option::Option::Some(ksp_config_lib::ConfigEnvironmentSource::Fallback));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/environment.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
|
||||
@@ -234,3 +234,129 @@ fn env_example_inventory_contains_current_runtime_variable_with_preceding_commen
|
||||
fn workspace_root() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_environment_value_keeps_real_value_but_redacts_safe_and_debug_views() {
|
||||
let canary = "KSP_SECRET_CANARY_91b7c6";
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_TEST_TOKEN".to_owned(), canary.to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_variable("KSP_SECRET_TEST_TOKEN", std::option::Option::None);
|
||||
assert!(resolved.is_ok(), "secret process value should resolve");
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(resolved.value(), canary);
|
||||
assert_eq!(resolved.safe_value(), crate::REDACTED_CONFIG_VALUE);
|
||||
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
|
||||
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process);
|
||||
let debug = format!("{resolved:?}");
|
||||
assert!(!debug.contains(canary), "Debug must not reveal the secret canary");
|
||||
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detailed_text_redacts_only_secret_segments_and_keeps_ordered_provenance() {
|
||||
let secret = "secret-canary-4a62";
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_PUBLIC_HOST".to_owned(), "rpc.example.test".to_owned());
|
||||
process.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_text_detailed("https://${KSP_PUBLIC_HOST}/?token=${KSP_SECRET_TOKEN}");
|
||||
assert!(resolved.is_ok(), "composed secret URL should resolve");
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(resolved.value(), "https://rpc.example.test/?token=secret-canary-4a62");
|
||||
assert_eq!(resolved.safe_value(), "https://rpc.example.test/?token=********");
|
||||
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
|
||||
assert_eq!(resolved.provenance().len(), 4);
|
||||
assert_eq!(resolved.provenance()[0], crate::ConfigValueProvenance::DocumentLiteral);
|
||||
assert_eq!(resolved.provenance()[1].variable_name(), std::option::Option::Some("KSP_PUBLIC_HOST"));
|
||||
assert_eq!(resolved.provenance()[2], crate::ConfigValueProvenance::DocumentLiteral);
|
||||
assert_eq!(resolved.provenance()[3].variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN"));
|
||||
let debug = format!("{resolved:?}");
|
||||
assert!(!debug.contains(secret), "resolved text Debug must not reveal a secret segment");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() {
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_text_detailed("token=${KSP_SECRET_TOKEN:-false-secret}");
|
||||
assert!(resolved.is_ok(), "secret fallback should resolve");
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(resolved.value(), "token=false-secret");
|
||||
assert_eq!(resolved.safe_value(), "token=********");
|
||||
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
|
||||
assert_eq!(resolved.provenance()[1].environment_source(), std::option::Option::Some(super::ConfigEnvironmentSource::Fallback));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detailed_json_preserves_safe_tree_sensitivity_and_pointer_provenance() {
|
||||
let secret = "nested-secret-canary-2d11";
|
||||
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
|
||||
dotenv.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
|
||||
let source = serde_json::json!({"transport": {"url": "https://host/?token=${KSP_SECRET_TOKEN}"}, "items": ["plain", 7]});
|
||||
let resolved = environment.resolve_json_detailed(&source);
|
||||
assert!(resolved.is_ok(), "detailed JSON should resolve");
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(resolved.value()["transport"]["url"], serde_json::Value::String(format!("https://host/?token={secret}")));
|
||||
assert_eq!(resolved.safe_value()["transport"]["url"], serde_json::Value::String("https://host/?token=********".to_owned()));
|
||||
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
|
||||
let provenance = resolved.provenance_at("/transport/url");
|
||||
assert!(provenance.is_some(), "JSON pointer provenance should exist");
|
||||
if let std::option::Option::Some(provenance) = provenance {
|
||||
assert_eq!(provenance.last().and_then(crate::ConfigValueProvenance::variable_name), std::option::Option::Some("KSP_SECRET_TOKEN"));
|
||||
}
|
||||
let debug = format!("{resolved:?}");
|
||||
assert!(!debug.contains(secret), "resolved JSON Debug must not reveal a secret canary");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detailed_profile_environment_keeps_global_origin_and_adds_environment_provenance() {
|
||||
let workspace = workspace_root();
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
let profile = engine.load_resolved_profile(&file_id, std::option::Option::None);
|
||||
let profile = match profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let effective = profile.resolve_effective_environment_detailed(&environment);
|
||||
assert!(effective.is_ok(), "detailed committed Logging profile should resolve");
|
||||
let effective = match effective {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(profile.origin("logs_directory"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||
assert_eq!(effective.value()["logs_directory"], serde_json::Value::String("logs".to_owned()));
|
||||
assert_eq!(effective.safe_value()["logs_directory"], serde_json::Value::String("logs".to_owned()));
|
||||
assert_eq!(
|
||||
effective.provenance_at("/logs_directory").and_then(|items| items.last()).and_then(crate::ConfigValueProvenance::variable_name),
|
||||
std::option::Option::Some("KSP_LOGS_DIRECTORY"),
|
||||
);
|
||||
}
|
||||
|
||||
31
crates/ksp-config-lib/unit_tests/sensitivity.rs
Normal file
31
crates/ksp-config-lib/unit_tests/sensitivity.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/sensitivity.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn environment_names_map_to_expected_sensitivity() {
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(super::ConfigSensitivity::Public));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_MODE").ok(), std::option::Option::Some(super::ConfigSensitivity::Internal));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_SECRET_PASSWORD").ok(), std::option::Option::Some(super::ConfigSensitivity::Secret));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(super::ConfigSensitivity::Public));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_MODE").ok(), std::option::Option::Some(super::ConfigSensitivity::Internal));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_SECRET_PASSWORD").ok(), std::option::Option::Some(super::ConfigSensitivity::Secret));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strongest_sensitivity_follows_secret_internal_public_order() {
|
||||
assert_eq!(super::ConfigSensitivity::Public.strongest(super::ConfigSensitivity::Internal), super::ConfigSensitivity::Internal);
|
||||
assert_eq!(super::ConfigSensitivity::Internal.strongest(super::ConfigSensitivity::Secret), super::ConfigSensitivity::Secret);
|
||||
assert_eq!(super::ConfigSensitivity::Secret.strongest(super::ConfigSensitivity::Public), super::ConfigSensitivity::Secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provenance_exposes_names_and_sources_without_values() {
|
||||
let process = super::ConfigValueProvenance::EnvironmentProcess { variable_name: "KSP_SECRET_TOKEN".to_owned() };
|
||||
let dotenv = super::ConfigValueProvenance::EnvironmentDotEnv { variable_name: "KSP_MODE".to_owned() };
|
||||
let fallback = super::ConfigValueProvenance::EnvironmentFallback { variable_name: "KSP_PUBLIC_HOST".to_owned() };
|
||||
assert_eq!(process.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN"));
|
||||
assert_eq!(process.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
|
||||
assert_eq!(dotenv.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::DotEnv));
|
||||
assert_eq!(fallback.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback));
|
||||
assert_eq!(super::ConfigValueProvenance::DocumentLiteral.variable_name(), std::option::Option::None);
|
||||
}
|
||||
329
deltas/0.1.3/pre.011.md
Normal file
329
deltas/0.1.3/pre.011.md
Normal file
@@ -0,0 +1,329 @@
|
||||
<!-- file: deltas/0.1.3/pre.011.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.1.3-pre.011
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée :
|
||||
|
||||
```text
|
||||
0.1.3-pre.010-fix.001
|
||||
```
|
||||
|
||||
Version technique de cette base :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.10.fix.1"
|
||||
Cargo.toml header version = 53
|
||||
```
|
||||
|
||||
Validations utilisateur exécutées le 2026-08-15 :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace OK
|
||||
cargo tree -p ksp-config-lib OK
|
||||
cargo tree -p ksp-config-lib -d OK, doublon transitif syn 2/3 déjà connu
|
||||
cargo tree -p ksp-config-lib -e features OK
|
||||
```
|
||||
|
||||
`cargo test --workspace` confirme notamment 53 tests unitaires + 8 tests publics pour `ksp-config-lib`.
|
||||
|
||||
## Objet de pre.011
|
||||
|
||||
Ajouter la couche de sécurité/provenance qui manquait au resolver environnemental :
|
||||
|
||||
```text
|
||||
Config source string / JSON
|
||||
+
|
||||
process > .env > fallback
|
||||
->
|
||||
real runtime value
|
||||
safe diagnostic value
|
||||
Public | Internal | Secret
|
||||
ordered / JSON-Pointer provenance
|
||||
```
|
||||
|
||||
Cette tranche ne modifie pas encore les documents Config, ne construit pas `LoggingSettings` et ne persiste rien.
|
||||
|
||||
## Classification de sensibilité
|
||||
|
||||
`ConfigSensitivity` expose :
|
||||
|
||||
```text
|
||||
Public
|
||||
Internal
|
||||
Secret
|
||||
```
|
||||
|
||||
Classification nominale :
|
||||
|
||||
```text
|
||||
KSP_SECRET_* / KSPB_SECRET_* -> Secret
|
||||
KSP_PUBLIC_* / KSPB_PUBLIC_* -> Public
|
||||
autre KSP_* / KSPB_* -> Internal
|
||||
```
|
||||
|
||||
Ordre :
|
||||
|
||||
```text
|
||||
Secret > Internal > Public
|
||||
```
|
||||
|
||||
La sensibilité d'une chaîne contenant des placeholders est la sensibilité la plus forte des placeholders effectivement référencés. Une chaîne littérale sans placeholder est `Internal`.
|
||||
|
||||
Un fallback hérite toujours de la sensibilité du nom de variable référencé. Ainsi :
|
||||
|
||||
```text
|
||||
${KSP_SECRET_TOKEN:-false-secret}
|
||||
```
|
||||
|
||||
reste `Secret` même si la valeur réelle vient du fallback.
|
||||
|
||||
## Valeur réelle et représentation sûre
|
||||
|
||||
`ConfigEnvironmentValue` conserve désormais :
|
||||
|
||||
```text
|
||||
variable_name
|
||||
value réel runtime
|
||||
safe_value diagnostic sûr
|
||||
sensitivity
|
||||
source Process | DotEnv | Fallback
|
||||
provenance
|
||||
```
|
||||
|
||||
Pour `Secret` :
|
||||
|
||||
```text
|
||||
value = valeur réelle
|
||||
safe_value = ********
|
||||
```
|
||||
|
||||
Pour `Public` et `Internal`, la représentation sûre conserve la valeur réelle dans cette tranche.
|
||||
|
||||
`ConfigEnvironmentValue` possède un `Debug` manuel qui n'affiche jamais `value`; il affiche `safe_value`, sensibilité, source et nom de variable.
|
||||
|
||||
## Chaînes composées
|
||||
|
||||
Nouveau contrat :
|
||||
|
||||
```text
|
||||
ResolvedConfigText
|
||||
```
|
||||
|
||||
Il conserve :
|
||||
|
||||
```text
|
||||
value
|
||||
safe_value
|
||||
sensitivity
|
||||
provenance[]
|
||||
```
|
||||
|
||||
Exemple :
|
||||
|
||||
```text
|
||||
source:
|
||||
https://${KSP_PUBLIC_HOST}/?token=${KSP_SECRET_TOKEN}
|
||||
|
||||
real:
|
||||
https://rpc.example.test/?token=secret-canary
|
||||
|
||||
safe:
|
||||
https://rpc.example.test/?token=********
|
||||
```
|
||||
|
||||
La redaction est réalisée par segment de substitution. Les fragments littéraux et non secrets restent visibles dans la représentation sûre.
|
||||
|
||||
`ConfigEnvironment::resolve_text_detailed()` expose ce contrat.
|
||||
|
||||
L'API existante :
|
||||
|
||||
```text
|
||||
resolve_text(...)
|
||||
```
|
||||
|
||||
reste compatible et retourne uniquement la valeur réelle.
|
||||
|
||||
## Provenance
|
||||
|
||||
`ConfigValueProvenance` distingue :
|
||||
|
||||
```text
|
||||
DocumentLiteral
|
||||
EnvironmentProcess { variable_name }
|
||||
EnvironmentDotEnv { variable_name }
|
||||
EnvironmentFallback { variable_name }
|
||||
```
|
||||
|
||||
La provenance n'embarque jamais la valeur d'environnement elle-même.
|
||||
|
||||
Une chaîne composée conserve l'ordre des segments/références ayant participé à sa construction.
|
||||
|
||||
## JSON détaillé
|
||||
|
||||
Nouveau contrat :
|
||||
|
||||
```text
|
||||
ResolvedConfigJson
|
||||
```
|
||||
|
||||
Il conserve :
|
||||
|
||||
```text
|
||||
value arbre JSON réel
|
||||
safe_value arbre JSON redacted
|
||||
sensitivity plus forte sensibilité contenue
|
||||
provenance map JSON Pointer -> provenance[]
|
||||
```
|
||||
|
||||
`ConfigEnvironment::resolve_json_detailed()` parcourt récursivement objets et tableaux sans modifier les clés.
|
||||
|
||||
Les JSON Pointer suivent RFC 6901 pour les clés contenant `~` ou `/`.
|
||||
|
||||
L'API existante :
|
||||
|
||||
```text
|
||||
resolve_json(...)
|
||||
resolve_map(...)
|
||||
```
|
||||
|
||||
reste compatible et retourne uniquement la valeur réelle.
|
||||
|
||||
## Profil effectif
|
||||
|
||||
`ResolvedConfigProfile` ajoute :
|
||||
|
||||
```text
|
||||
resolve_effective_environment_detailed(...)
|
||||
```
|
||||
|
||||
La provenance top-level déjà existante :
|
||||
|
||||
```text
|
||||
Global
|
||||
Profile
|
||||
```
|
||||
|
||||
reste attachée au profil source via `origin(key)`.
|
||||
|
||||
La résolution détaillée ajoute séparément la provenance de la couche environnement :
|
||||
|
||||
```text
|
||||
DocumentLiteral
|
||||
Process
|
||||
DotEnv
|
||||
Fallback
|
||||
```
|
||||
|
||||
L'ancienne méthode `resolve_effective_environment()` reste disponible et retourne seulement la map réelle.
|
||||
|
||||
## Non-divulgation
|
||||
|
||||
Les tests canary vérifient notamment :
|
||||
|
||||
- secret venant du process : réel disponible, safe redacted, `Debug` sans canary ;
|
||||
- secret venant d'un fallback : fallback réel disponible mais safe redacted ;
|
||||
- URL composée public + secret : seul le segment secret est masqué ;
|
||||
- JSON imbriqué : arbre réel complet, arbre safe redacted, `Debug` sans canary ;
|
||||
- provenance de la variable sans transport de la valeur elle-même.
|
||||
|
||||
`ksp-logging-lib` ne reçoit aucune logique générique de redaction de messages arbitraires. La valeur sûre est construite par Config avant qu'un diagnostic ne l'utilise.
|
||||
|
||||
## Clarification `KSP_LOGS_DIRECTORY`
|
||||
|
||||
La décision préparatoire de l'adapter `pre.012` est enregistrée dans le plan :
|
||||
|
||||
- après interpolation, `logs_directory` pourra être absolu ou relatif ;
|
||||
- un chemin relatif sera interprété relativement au current working directory du processus qui initialise Logging, pas automatiquement au répertoire du binaire ;
|
||||
- le fallback `logs` s'applique seulement si `KSP_LOGS_DIRECTORY` est absent ;
|
||||
- une valeur explicitement présente mais invalide ne déclenche pas le fallback et devra produire un diagnostic de configuration effective invalide ;
|
||||
- les paths de sinks restent relatifs sous `logs_directory` sans traversal.
|
||||
|
||||
Aucun code de validation/conversion Logging correspondant n'est ajouté dans `pre.011`; cette responsabilité appartient à `pre.012`.
|
||||
|
||||
## `.env.example`
|
||||
|
||||
Aucune nouvelle variable runtime n'est introduite.
|
||||
|
||||
`.env.example` reste donc inchangé et contient toujours la seule variable actuellement utilisée :
|
||||
|
||||
```text
|
||||
KSP_LOGS_DIRECTORY=logs
|
||||
```
|
||||
|
||||
## Dépendances
|
||||
|
||||
Aucune dépendance ou feature Cargo n'est ajoutée/modifiée.
|
||||
|
||||
La direction reste :
|
||||
|
||||
```text
|
||||
ksp-config-lib -> ksp-logging-lib -> ksp-core-lib
|
||||
ksp-config-lib -> ksp-core-lib
|
||||
```
|
||||
|
||||
Logging ne dépend toujours pas de Config.
|
||||
|
||||
## Version technique
|
||||
|
||||
La prerelease devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = "0.1.3-pre.11"
|
||||
Cargo.toml header version = 54
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-config-lib/src/sensitivity.rs
|
||||
crates/ksp-config-lib/unit_tests/sensitivity.rs
|
||||
deltas/0.1.3/pre.011.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-config-lib/src/environment.rs
|
||||
crates/ksp-config-lib/src/lib.rs
|
||||
crates/ksp-config-lib/src/profile.rs
|
||||
crates/ksp-config-lib/tests/public_api.rs
|
||||
crates/ksp-config-lib/unit_tests/environment.rs
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
|
||||
docs/rules/RULES_KSP.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Contrôles exécutés dans l'environnement de génération
|
||||
|
||||
- comparaison statique avec la base validée `pre.010-fix.001` ;
|
||||
- absence de nouvelle dépendance/feature Cargo ;
|
||||
- absence de nouvelle variable runtime nécessitant une modification de `.env.example` ;
|
||||
- audit statique du nouveau code Config contre `unsafe`, `unwrap`, `expect`, `panic!` et opérateur `?` de production ;
|
||||
- contrôle des headers/version ;
|
||||
- contrôle des lignes Rust à 160 colonnes maximum ;
|
||||
- contrôle canary dans les tests : les représentations safe/Debug attendues ne contiennent pas le secret ;
|
||||
- reproduction du delta sur la base précédente avant packaging.
|
||||
|
||||
Le toolchain Rust n'est pas disponible dans l'environnement de génération. `cargo fmt/check/clippy/test` doivent donc être exécutés par l'utilisateur.
|
||||
|
||||
## Étape suivante
|
||||
|
||||
Après validation utilisateur :
|
||||
|
||||
```text
|
||||
0.1.3-pre.012 — adapter Config -> Logging
|
||||
```
|
||||
|
||||
Cette tranche devra notamment valider/résoudre `logs_directory`, convertir le profil Logging effectif vers les contrats publics `ksp_logging_lib::*` et démontrer `initialize/reinitialize` sans créer de dépendance Logging -> Config.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 17 -->
|
||||
<!-- version: 18 -->
|
||||
|
||||
# Plans KSP
|
||||
|
||||
@@ -13,7 +13,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
|
||||
- [`002-FUNCTIONAL_RELEASE_SEQUENCE.md`](002-FUNCTIONAL_RELEASE_SEQUENCE.md) — séquence active de référence des premières releases fonctionnelles ;
|
||||
- [`003-V0_1_1_CORE_FOUNDATION_PLAN.md`](003-V0_1_1_CORE_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.1`, établi par `0.1.1-pre.001` puis consolidé jusqu'à `0.1.1-rel.001`.
|
||||
- [`004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](004-V0_1_2_LOGGING_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.1.2`, établi par `0.1.2-pre.001` puis consolidé jusqu'à `0.1.2-rel.001`.
|
||||
- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` a fermé le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et le premier `std.logging.json`, `pre.008` la résolution globals/profils/`default_profile`, puis `pre.009` les compositions génériques par `file_id`; `pre.010` a livré `.env`, process env, `.env.example` et le resolver `${...}`; `pre.011` poursuivra avec sensibilité, valeur réelle/sûre et provenance.
|
||||
- [`005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan actif de `0.1.3 — Configuration foundation`, établi par `0.1.3-pre.001`, corrigé par `pre.001-fix.001`, complété par `pre.001-fix.002` pour le registre `file_id`/bootstrap/non-régression Logging, regranularisé par `pre.001-fix.003`, puis rescindé pendant `pre.005`; `pre.006` a fermé le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema et le premier `std.logging.json`, `pre.008` la résolution globals/profils/`default_profile`, puis `pre.009` les compositions génériques par `file_id`; `pre.010` a livré `.env`, process env, `.env.example` et le resolver `${...}`; `pre.011` ajoute sensibilité, valeur réelle/sûre, redaction et provenance; `pre.012` poursuivra avec l'adapter Config -> Logging.
|
||||
|
||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
|
||||
# Séquence des releases fonctionnelles KSP
|
||||
|
||||
@@ -219,7 +219,7 @@ pre.015 clôture
|
||||
|
||||
Cette prévision n'est pas un plafond : chaque prerelease doit rester une petite tranche, avec scission explicite si l'objectif dépasse environ 15–20 minutes de travail effectif.
|
||||
|
||||
`pre.006` a fermé le routing Logging structuré `domain`; `pre.007` a livré le moteur JSON/JSON Schema et `std.logging.json`; `pre.008` a ajouté la résolution générique globals/profils/`default_profile`; `pre.009` a ajouté les compositions génériques par `file_id`, avec `schema.composite` mais sans composite runtime fictif; `pre.010` ajoute le snapshot process + `.env`, `.env.example` et le resolver `${...}`. Après validation utilisateur, `pre.011` ajoutera sensibilité, valeur réelle/sûre et provenance enrichie.
|
||||
`pre.006` a fermé le routing Logging structuré `domain`; `pre.007` a livré le moteur JSON/JSON Schema et `std.logging.json`; `pre.008` a ajouté la résolution générique globals/profils/`default_profile`; `pre.009` a ajouté les compositions génériques par `file_id`, avec `schema.composite` mais sans composite runtime fictif; `pre.010` a ajouté le snapshot process + `.env`, `.env.example` et le resolver `${...}`; `pre.011` ajoute sensibilité, valeurs réelle/sûre, redaction et provenance enrichie. Après validation utilisateur, `pre.012` construira l'adapter Config -> Logging.
|
||||
|
||||
## `0.1.4` — Config desktop par défaut
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Plan `0.1.3` — Configuration foundation
|
||||
|
||||
## 1. Statut et objectif
|
||||
|
||||
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats, `pre.006` le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema, `pre.008` la résolution des globals/profils/`default_profile`, `pre.009` les compositions génériques par `file_id` et `pre.010` le snapshot process + `.env` et le resolver `${...}`. La prochaine tranche est `pre.011` pour sensibilité, valeur réelle/sûre et provenance enrichie.
|
||||
Ce plan a été établi par `0.1.3-pre.001`, corrigé par `0.1.3-pre.001-fix.001/.002/.003`, puis exécuté par petites tranches. `pre.002` a livré le bootstrap Config, `pre.003` le registre `file_id`, `pre.004` les contrats publics multi-output de Logging, `pre.005` le runtime multi-sink sur niveau/target/formats, `pre.006` le routing structuré `domain`, `pre.007` le moteur JSON/JSON Schema, `pre.008` la résolution des globals/profils/`default_profile`, `pre.009` les compositions génériques par `file_id`, `pre.010` le snapshot process + `.env` et le resolver `${...}`, puis `pre.011` la sensibilité et les représentations real/safe/provenance. La prochaine tranche est `pre.012` pour l'adapter Config -> Logging.
|
||||
|
||||
La base auditée reste la release stable `v0.1.2`.
|
||||
|
||||
@@ -583,6 +583,8 @@ Décisions :
|
||||
|
||||
- `logs_directory` est global, hors profils ;
|
||||
- son fallback est exprimé dans le document au point d'usage ;
|
||||
- après interpolation, `logs_directory` peut être absolu ou relatif ; un chemin relatif est interprété relativement au current working directory du processus qui initialise Logging, et non automatiquement relativement au répertoire du binaire ;
|
||||
- un `KSP_LOGS_DIRECTORY` explicitement présent mais invalide n'active jamais le fallback : le fallback reste réservé à l'absence de la variable et l'adapter Config -> Logging doit retourner un diagnostic de configuration effective invalide ;
|
||||
- `default_profile` est global et référence un `profile_id` existant ;
|
||||
- `profile_id` est obligatoire et unique dans le document ;
|
||||
- `default_filter` gouverne le comportement général du profil ;
|
||||
@@ -1802,15 +1804,23 @@ Tranche livrée :
|
||||
- la règle durable impose désormais d'ajouter toute nouvelle variable runtime KSP/KSPB à `.env.example`, avec commentaire d'usage, dans le même delta que sa première utilisation ;
|
||||
- `.gitignore` possédait déjà la règle correcte `.env`, `.env.*`, `!.env.example`; aucun changement n'est nécessaire.
|
||||
|
||||
La validation utilisateur de `pre.010-fix.001` est acquise : `fmt/check/clippy/test` passent, les 53 tests unitaires Config et 8 tests publics passent, et le graphe Cargo reste conforme avec le doublon transitif `syn 2`/`syn 3` déjà connu via `jsonschema`.
|
||||
|
||||
### `0.1.3-pre.011` — sensibilité + valeurs real/safe/provenance
|
||||
|
||||
- `Public/Internal/Secret` ;
|
||||
- propagation de sensibilité par chaîne composée ;
|
||||
- redaction par segment ;
|
||||
- contrat `ResolvedValue` ou équivalent ;
|
||||
- valeur réelle + représentation sûre + provenance ;
|
||||
- interdiction des secrets dans logs/diagnostics ordinaires ;
|
||||
- tests canary de non-divulgation.
|
||||
Tranche livrée :
|
||||
|
||||
- `ConfigSensitivity::{Public, Internal, Secret}` dérive la classification directement des namespaces KSP/KSPB et conserve l'ordre `Secret > Internal > Public` ;
|
||||
- `ConfigEnvironmentValue` conserve désormais valeur réelle, valeur sûre, sensibilité, source et provenance ; son `Debug` utilise uniquement la valeur sûre ;
|
||||
- `ResolvedConfigText` conserve la chaîne réelle, sa représentation sûre, la sensibilité la plus forte et une provenance ordonnée ;
|
||||
- les segments issus de `KSP_SECRET_*`/`KSPB_SECRET_*` sont remplacés par `********` dans la représentation sûre, y compris lorsqu'ils viennent d'un fallback ;
|
||||
- les chaînes composées propagent la sensibilité la plus forte des placeholders utilisés ; une chaîne sans placeholder est `Internal` ;
|
||||
- `ConfigValueProvenance` distingue `DocumentLiteral`, `EnvironmentProcess`, `EnvironmentDotEnv` et `EnvironmentFallback` sans embarquer les valeurs elles-mêmes ;
|
||||
- `ResolvedConfigJson` conserve arbres réel/sûr, sensibilité agrégée et provenance indexée par JSON Pointer ;
|
||||
- les APIs existantes `resolve_text`, `resolve_json` et `resolve_effective_environment` restent compatibles et retournent uniquement les valeurs réelles ; les variantes `*_detailed` exposent le contrat enrichi ;
|
||||
- la provenance Global/Profile déjà portée par `ResolvedConfigProfile` reste distincte et inchangée ; `resolve_effective_environment_detailed` ajoute la provenance de la couche environnement ;
|
||||
- des canaries de tests vérifient qu'une valeur secrète process, fallback ou imbriquée dans un JSON n'apparait pas dans `safe_value` ni dans `Debug` ;
|
||||
- aucune nouvelle dépendance Cargo et aucune nouvelle variable d'environnement ne sont introduites dans cette tranche.
|
||||
|
||||
### `0.1.3-pre.012` — adapter Config -> Logging
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/RULES_KSP.md -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
|
||||
# Règles spécifiques à KSP
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
- **KSP-CONFIG-005** — `.env.example` est versionné à la racine et inventorie toutes les variables d'environnement runtime KSP/KSPB utilisées par les fichiers Config ou le code ; toute nouvelle variable y est ajoutée dans le même delta que sa première utilisation.
|
||||
- **KSP-CONFIG-006** — Chaque entrée de `.env.example` est précédée d'un commentaire décrivant son usage/utilité. Sa valeur peut être un défaut sûr, une valeur générique non secrète ou une entrée commentée ; aucun vrai secret n'y est enregistré.
|
||||
- **KSP-CONFIG-007** — Les placeholders Config utilisent `${NAME}` ou `${NAME:-fallback}`. Le fallback s'applique uniquement si la variable est absente ; l'interpolation appartient à `ksp-config-lib` et non aux consumers.
|
||||
- **KSP-CONFIG-008** — La sensibilité d'une variable est dérivée de son nom : `KSP_SECRET_*`/`KSPB_SECRET_*` -> `Secret`, `KSP_PUBLIC_*`/`KSPB_PUBLIC_*` -> `Public`, les autres variables KSP/KSPB -> `Internal`, avec l'ordre `Secret > Internal > Public`.
|
||||
- **KSP-CONFIG-009** — Toute valeur Config contenant un segment `Secret` conserve une valeur réelle pour le runtime légitime et une représentation sûre où chaque segment secret est remplacé par `********`; les `Debug` génériques ne révèlent jamais la valeur réelle secrète.
|
||||
- **KSP-CONFIG-010** — La sensibilité d'une chaîne composée est la plus forte des placeholders utilisés. Un fallback d'une variable `Secret` reste `Secret` et doit être redacted même si la valeur provient du fallback.
|
||||
- **KSP-CONFIG-011** — La provenance de résolution n'embarque jamais la valeur d'environnement elle-même ; elle distingue document literal, process, `.env` et fallback avec le nom de variable concerné.
|
||||
|
||||
## Programmes et exécution
|
||||
|
||||
|
||||
Reference in New Issue
Block a user