diff --git a/lib/crates/fabro-config/src/run.rs b/lib/crates/fabro-config/src/run.rs index 691200293..d6114b835 100644 --- a/lib/crates/fabro-config/src/run.rs +++ b/lib/crates/fabro-config/src/run.rs @@ -121,7 +121,7 @@ fn resolve_goal_file( let resolved = file .resolve(process_env_var) .map_err(|err| ResolveRunGoalError::EnvLookup { var: err.name })?; - let path = resolve_goal_file_path(&resolved.value, base_dir); + let path = resolve_goal_file_path(&resolved, base_dir); let text = std::fs::read_to_string(&path).map_err(|source| ResolveRunGoalError::Io { path: path.clone(), source, diff --git a/lib/crates/fabro-hooks/src/executor.rs b/lib/crates/fabro-hooks/src/executor.rs index 9fda710d5..a409a49b7 100644 --- a/lib/crates/fabro-hooks/src/executor.rs +++ b/lib/crates/fabro-hooks/src/executor.rs @@ -77,9 +77,7 @@ fn resolve_interp(value: &InterpString, env: &E) -> Result resolved.value, + Ok(resolved) => resolved, Err(err) => { warn!( run_id = %run_id, @@ -2425,12 +2425,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result) -> Result { + pub fn resolve_with(&self, ctx: &mut ResolveCtx<'_>) -> Result { let mut value = String::new(); - let mut env_names = Vec::new(); - let mut secret_names = Vec::new(); for seg in &self.segments { match seg { Segment::Literal(text) => value.push_str(text), @@ -298,19 +297,11 @@ impl InterpString { return Err(ResolveError::missing(*namespace, name)); }; value.push_str(&resolved); - match namespace { - Namespace::Env => env_names.push(name.clone()), - Namespace::Secrets => secret_names.push(name.clone()), - Namespace::Vars | Namespace::Inputs => {} - } } } } - Ok(Resolved { - value, - provenance: Provenance::from_names(env_names, secret_names), - }) + Ok(value) } /// Substitute tokens for the namespaces `ctx` provides, preserving tokens @@ -347,7 +338,7 @@ impl InterpString { /// `lookup` should return the current value for a given env var name (or /// `None` if unset). Tokens in any other namespace fail with /// [`ResolveErrorKind::Unavailable`]. - pub fn resolve(&self, lookup: F) -> Result + pub fn resolve(&self, lookup: F) -> Result where F: FnMut(&str) -> Option, { @@ -368,8 +359,7 @@ impl InterpString { where F: FnMut(&str) -> Option, { - self.resolve(lookup) - .map_or_else(|_| self.as_source(), |resolved| resolved.value) + self.resolve(lookup).unwrap_or_else(|_| self.as_source()) } /// Substitute only `{{ vars.* }}` tokens while preserving all other @@ -426,40 +416,6 @@ impl From<&str> for InterpString { } } -/// The outcome of a successful interpolation resolution. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Resolved { - pub value: String, - pub provenance: Provenance, -} - -/// Provenance metadata for resolved config values. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Provenance { - /// No env var or secret contributed to this value. - Literal, - /// One or more env vars and/or secrets contributed to this value. Used by - /// outward-facing renderers to redact sensitive-sourced values uniformly. - /// `vars`/`inputs` are non-sensitive and do not mark a value as sourced. - Sourced { - env_names: Vec, - secret_names: Vec, - }, -} - -impl Provenance { - fn from_names(env_names: Vec, secret_names: Vec) -> Self { - if env_names.is_empty() && secret_names.is_empty() { - Self::Literal - } else { - Self::Sourced { - env_names, - secret_names, - } - } - } -} - /// An error from resolving or substituting interpolation tokens. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolveError { @@ -614,8 +570,7 @@ mod tests { fn resolve_literal_string() { let s = InterpString::parse("static"); let resolved = s.resolve(lookup_from(&[])).unwrap(); - assert_eq!(resolved.value, "static"); - assert_eq!(resolved.provenance, Provenance::Literal); + assert_eq!(resolved, "static"); } #[test] @@ -624,18 +579,14 @@ mod tests { let resolved = s .resolve(lookup_from(&[("API_KEY", "secret-123")])) .unwrap(); - assert_eq!(resolved.value, "secret-123"); - assert_eq!(resolved.provenance, Provenance::Sourced { - env_names: vec!["API_KEY".into()], - secret_names: vec![], - }); + assert_eq!(resolved, "secret-123"); } #[test] fn resolve_substring() { let s = InterpString::parse("Bearer {{ env.TOKEN }}"); let resolved = s.resolve(lookup_from(&[("TOKEN", "abc")])).unwrap(); - assert_eq!(resolved.value, "Bearer abc"); + assert_eq!(resolved, "Bearer abc"); } #[test] @@ -644,11 +595,7 @@ mod tests { let resolved = s .resolve(lookup_from(&[("USER", "root"), ("HOST", "example.com")])) .unwrap(); - assert_eq!(resolved.value, "root@example.com"); - assert_eq!(resolved.provenance, Provenance::Sourced { - env_names: vec!["USER".into(), "HOST".into()], - secret_names: vec![], - }); + assert_eq!(resolved, "root@example.com"); } #[test] @@ -668,8 +615,7 @@ mod tests { fn unterminated_token_treated_as_literal() { let s = InterpString::parse("{{ env.OPEN"); let resolved = s.resolve(lookup_from(&[])).unwrap(); - assert_eq!(resolved.value, "{{ env.OPEN"); - assert_eq!(resolved.provenance, Provenance::Literal); + assert_eq!(resolved, "{{ env.OPEN"); } #[test] @@ -685,7 +631,7 @@ mod tests { let s = InterpString::parse(raw); assert!(s.is_literal(), "{raw} should stay literal"); let resolved = s.resolve(lookup_from(&[])).unwrap(); - assert_eq!(resolved.value, raw); + assert_eq!(resolved, raw); } } @@ -736,11 +682,7 @@ mod tests { ) .unwrap(); - assert_eq!(resolved.value, "https://us-east-1.example.com"); - assert_eq!(resolved.provenance, Provenance::Sourced { - env_names: vec!["REGION".into()], - secret_names: vec![], - }); + assert_eq!(resolved, "https://us-east-1.example.com"); } #[test] @@ -796,7 +738,7 @@ mod tests { } #[test] - fn resolve_with_secrets_tracks_provenance() { + fn resolve_with_substitutes_secrets_and_env() { let s = InterpString::parse("Bearer {{ secrets.API_KEY }} via {{ env.PROXY }}"); let resolved = s @@ -807,11 +749,7 @@ mod tests { ) .unwrap(); - assert_eq!(resolved.value, "Bearer vault-value via proxy.internal"); - assert_eq!(resolved.provenance, Provenance::Sourced { - env_names: vec!["PROXY".into()], - secret_names: vec!["API_KEY".into()], - }); + assert_eq!(resolved, "Bearer vault-value via proxy.internal"); } #[test] diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index 872fa3519..5bf91e4d9 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -25,7 +25,7 @@ pub use cli::{ CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetSettings, CliUpdatesSettings, }; pub use duration::{Duration, ParseDurationError}; -pub use interp::{InterpString, Provenance, ResolveCtx, ResolveError, ResolveErrorKind, Resolved}; +pub use interp::{InterpString, ResolveCtx, ResolveError, ResolveErrorKind}; pub use model_ref::{ AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef, }; diff --git a/lib/crates/fabro-types/src/settings/run.rs b/lib/crates/fabro-types/src/settings/run.rs index 6befaf457..51410c44d 100644 --- a/lib/crates/fabro-types/src/settings/run.rs +++ b/lib/crates/fabro-types/src/settings/run.rs @@ -1110,7 +1110,7 @@ impl RunEnvironmentSettings { for (name, value) in &self.env { let references_secrets = value.references(Namespace::Secrets); let resolved_value = match value.resolve_with(&mut ctx) { - Ok(resolved) => resolved.value, + Ok(resolved) => resolved, Err(err) if err.namespace == Namespace::Env && !references_secrets => { #[expect( clippy::disallowed_methods, @@ -1654,7 +1654,7 @@ fn resolve_env_string( let mut ctx = ResolveCtx::new() .with_env(&mut *env_lookup) .with_secrets(&mut *secrets_lookup); - *value = InterpString::parse(value).resolve_with(&mut ctx)?.value; + *value = InterpString::parse(value).resolve_with(&mut ctx)?; Ok(()) } @@ -2347,21 +2347,22 @@ pub struct ArtifactsSettings { } /// Outcome of resolving a [`RunGoal`] to its final goal text. /// -/// Carries provenance alongside the text so downstream consumers (e.g. the -/// run manifest builder) can distinguish inline goals from file-sourced goals. +/// Carries source metadata alongside the text so downstream consumers (e.g. +/// the run manifest builder) can distinguish inline goals from file-sourced +/// goals. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedRunGoal { pub text: String, pub source: ResolvedGoalSource, } -/// Provenance of a [`ResolvedRunGoal`]. +/// Source metadata for a [`ResolvedRunGoal`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolvedGoalSource { /// Goal text came from a literal `run.goal = "..."` value. Inline, /// Goal text was read from a file on disk. The absolute path of that - /// file is carried for provenance / error reporting. + /// file is carried for error reporting. File { path: std::path::PathBuf }, }