1use crate::ast::*;
5use crate::pack::PackError;
6use cellstate_core::SecretString;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug)]
14pub enum ConfigError {
15 YamlParse(String),
16 NameConflict(String),
17 MissingName(String),
18 InvalidValue(String),
19 UnknownProvider(String),
20 UnknownAdapter(String),
21}
22
23impl std::fmt::Display for ConfigError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 ConfigError::YamlParse(msg) => write!(f, "YAML parse error: {}", msg),
39 ConfigError::NameConflict(msg) => write!(f, "Name conflict: {}", msg),
40 ConfigError::MissingName(msg) => write!(f, "Missing name: {}", msg),
41 ConfigError::InvalidValue(msg) => write!(f, "Invalid value: {}", msg),
42 ConfigError::UnknownProvider(msg) => write!(f, "Unknown provider: {}", msg),
43 ConfigError::UnknownAdapter(msg) => write!(f, "Unknown adapter: {}", msg),
44 }
45 }
46}
47
48impl std::error::Error for ConfigError {}
49
50impl From<ConfigError> for PackError {
51 fn from(err: ConfigError) -> Self {
63 PackError::Validation(err.to_string())
64 }
65}
66
67fn resolve_block_name(
76 header_name: Option<&str>,
77 payload_name: &Option<String>,
78 block_kind: &str,
79) -> Result<String, ConfigError> {
80 match (header_name, payload_name) {
81 (Some(header), None) => Ok(header.to_owned()),
82 (Some(_), Some(_)) => Err(ConfigError::NameConflict(format!(
83 "Name in both fence header and YAML payload for {}",
84 block_kind
85 ))),
86 (None, Some(name)) => Ok(name.clone()),
87 (None, None) => Err(ConfigError::MissingName(format!(
88 "{} block has no name in header or payload",
89 block_kind
90 ))),
91 }
92}
93
94#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
99#[serde(deny_unknown_fields)]
100pub struct AdapterConfig {
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub name: Option<String>,
103 pub adapter_type: String,
104 pub connection: String,
105 #[serde(default)]
106 pub options: Vec<(String, String)>,
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
110#[serde(deny_unknown_fields)]
111pub struct MemoryConfig {
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub name: Option<String>,
114 pub memory_type: String,
115 #[serde(default)]
116 pub schema: Vec<FieldConfig>,
117 pub retention: String,
118 pub lifecycle: String,
119 #[serde(default)]
120 pub parent: Option<String>,
121 #[serde(default)]
122 pub indexes: Vec<IndexConfig>,
123 #[serde(default)]
124 pub inject_on: Vec<String>,
125 #[serde(default)]
126 pub artifacts: Vec<String>,
127 #[serde(default)]
128 pub modifiers: Vec<String>,
129}
130
131#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
132#[serde(deny_unknown_fields)]
133pub struct FieldConfig {
134 pub name: String,
135 #[serde(alias = "type")]
137 pub field_type: String,
138 #[serde(default)]
139 pub nullable: bool,
140 #[serde(default)]
141 pub default: Option<String>,
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
145#[serde(deny_unknown_fields)]
146pub struct IndexConfig {
147 pub field: String,
148 #[serde(alias = "type")]
150 pub index_type: String,
151 #[serde(default)]
152 pub options: Vec<(String, String)>,
153}
154
155#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
156#[serde(deny_unknown_fields)]
157pub struct PolicyConfig {
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub name: Option<String>,
160 pub rules: Vec<PolicyRuleConfig>,
161}
162
163#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
164#[serde(deny_unknown_fields)]
165pub struct PolicyRuleConfig {
166 pub trigger: String,
167 pub actions: Vec<ActionConfig>,
168}
169
170#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
171#[serde(deny_unknown_fields)]
172#[serde(tag = "type")]
173pub enum ActionConfig {
174 #[serde(rename = "summarize")]
175 Summarize { target: String },
176 #[serde(rename = "checkpoint")]
177 Checkpoint { target: String },
178 #[serde(rename = "extract_artifacts")]
179 ExtractArtifacts { target: String },
180 #[serde(rename = "notify")]
181 Notify { target: String },
182 #[serde(rename = "inject")]
183 Inject { target: String, mode: String },
184}
185
186#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
187#[serde(deny_unknown_fields)]
188pub struct InjectionConfig {
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub name: Option<String>,
191 pub source: String,
192 pub target: String,
193 pub mode: String,
194 pub priority: i32,
195 #[serde(default)]
196 pub max_tokens: Option<i32>,
197}
198
199#[derive(Clone, Deserialize, Serialize, PartialEq)]
200#[serde(deny_unknown_fields)]
201pub struct ProviderConfig {
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub name: Option<String>,
204 pub provider_type: String,
205 pub api_key: SecretString,
206 pub model: String,
207 #[serde(default)]
208 pub options: Vec<(String, String)>,
209}
210
211impl std::fmt::Debug for ProviderConfig {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.debug_struct("ProviderConfig")
214 .field("name", &self.name)
215 .field("provider_type", &self.provider_type)
216 .field(
217 "api_key",
218 &format!("[REDACTED, {} chars]", self.api_key.len()),
219 )
220 .field("model", &self.model)
221 .field("options", &self.options)
222 .finish()
223 }
224}
225
226#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
227#[serde(deny_unknown_fields)]
228pub struct CacheConfig {
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub name: Option<String>,
231 pub backend: String,
232 #[serde(default)]
233 pub path: Option<String>,
234 pub size_mb: i32,
235 pub default_freshness: FreshnessConfig,
236 #[serde(default)]
237 pub max_entries: Option<i32>,
238 #[serde(default)]
239 pub ttl: Option<String>,
240}
241
242#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
243#[serde(deny_unknown_fields)]
244#[serde(tag = "type")]
245pub enum FreshnessConfig {
246 #[serde(rename = "best_effort")]
247 BestEffort { max_staleness: String },
248 #[serde(rename = "strict")]
249 Strict,
250}
251
252#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
253#[serde(deny_unknown_fields)]
254pub struct TrajectoryConfig {
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub name: Option<String>,
257 #[serde(default)]
258 pub description: Option<String>,
259 pub agent_type: String,
260 pub token_budget: i32,
261 #[serde(default)]
262 pub memory_refs: Vec<String>,
263 #[serde(default)]
264 pub metadata: Option<serde_json::Value>,
265}
266
267#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
268#[serde(deny_unknown_fields)]
269pub struct AgentConfig {
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub name: Option<String>,
272 pub capabilities: Vec<String>,
273 pub constraints: AgentConstraintsConfig,
274 pub permissions: PermissionMatrixConfig,
275}
276
277#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
278#[serde(deny_unknown_fields)]
279pub struct AgentConstraintsConfig {
280 pub max_concurrent: i32,
281 pub timeout_ms: i64,
282}
283
284#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
285#[serde(deny_unknown_fields)]
286pub struct PermissionMatrixConfig {
287 #[serde(default)]
288 pub read: Vec<String>,
289 #[serde(default)]
290 pub write: Vec<String>,
291 #[serde(default)]
292 pub lock: Vec<String>,
293}
294
295#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
300#[serde(deny_unknown_fields)]
301pub struct IntentConfig {
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub name: Option<String>,
304 #[serde(default)]
305 pub goals: Vec<String>,
306 #[serde(default)]
307 pub resolution_rules: Vec<ResolutionRuleConfig>,
308 #[serde(default)]
309 pub autonomy_level: Option<String>,
310 #[serde(default)]
311 pub delegation_boundaries: Option<DelegationBoundaryConfig>,
312 #[serde(default)]
313 pub alignment_signals: Vec<AlignmentSignalConfig>,
314 #[serde(default)]
315 pub drift_threshold: Option<f64>,
316}
317
318#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
319#[serde(deny_unknown_fields)]
320pub struct ResolutionRuleConfig {
321 pub condition: String,
322 pub priority: Vec<String>,
323 #[serde(default)]
324 pub escalate_to: Option<String>,
325 #[serde(default)]
326 pub max_authority: f64,
327}
328
329#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
330#[serde(deny_unknown_fields)]
331pub struct DelegationBoundaryConfig {
332 #[serde(default)]
333 pub authorized_actions: Vec<String>,
334 #[serde(default)]
335 pub requires_approval: Vec<String>,
336 #[serde(default)]
337 pub forbidden_actions: Vec<String>,
338}
339
340#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
341#[serde(deny_unknown_fields)]
342pub struct AlignmentSignalConfig {
343 pub name: String,
344 pub source: String,
345 pub metric: String,
346 pub target: SignalTargetConfig,
347}
348
349#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
350#[serde(deny_unknown_fields)]
351#[serde(tag = "type")]
352pub enum SignalTargetConfig {
353 #[serde(rename = "above")]
354 Above { value: f64 },
355 #[serde(rename = "below")]
356 Below { value: f64 },
357 #[serde(rename = "between")]
358 Between { min: f64, max: f64 },
359}
360
361pub fn parse_adapter_block(
395 header_name: Option<&str>,
396 content: &str,
397) -> Result<AdapterDef, ConfigError> {
398 let config: AdapterConfig =
400 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
401
402 let name = resolve_block_name(header_name, &config.name, "adapter")?;
404
405 validate_adapter_config(&config)?;
407
408 let adapter_type = parse_adapter_type(&config.adapter_type)?;
409
410 Ok(AdapterDef {
411 name,
412 adapter_type,
413 connection: config.connection,
414 options: config.options,
415 })
416}
417
418pub fn parse_memory_block(
453 header_name: Option<&str>,
454 content: &str,
455) -> Result<MemoryDef, ConfigError> {
456 let config: MemoryConfig =
457 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
458
459 let name = resolve_block_name(header_name, &config.name, "memory")?;
460
461 let memory_type = parse_memory_type(&config.memory_type)?;
463 let retention = parse_retention(&config.retention)?;
464 let lifecycle = parse_lifecycle(&config.lifecycle)?;
465 let schema = config
466 .schema
467 .into_iter()
468 .map(parse_field_def)
469 .collect::<Result<Vec<_>, _>>()?;
470 let indexes = config
471 .indexes
472 .into_iter()
473 .map(parse_index_def)
474 .collect::<Result<Vec<_>, _>>()?;
475 let inject_on = config
476 .inject_on
477 .into_iter()
478 .map(|s| parse_trigger(&s))
479 .collect::<Result<Vec<_>, _>>()?;
480 let modifiers = config
481 .modifiers
482 .into_iter()
483 .map(parse_modifier)
484 .collect::<Result<Vec<_>, _>>()?;
485
486 Ok(MemoryDef {
487 name,
488 memory_type,
489 schema,
490 retention,
491 lifecycle,
492 parent: config.parent,
493 indexes,
494 inject_on,
495 artifacts: config.artifacts,
496 modifiers,
497 })
498}
499
500pub fn parse_policy_block(
531 header_name: Option<&str>,
532 content: &str,
533) -> Result<PolicyDef, ConfigError> {
534 let config: PolicyConfig =
535 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
536
537 let name = resolve_block_name(header_name, &config.name, "policy")?;
538
539 let rules = config
540 .rules
541 .into_iter()
542 .map(parse_policy_rule)
543 .collect::<Result<Vec<_>, _>>()?;
544
545 Ok(PolicyDef { name, rules })
546}
547
548pub fn parse_injection_block(
571 header_name: Option<&str>,
572 content: &str,
573) -> Result<InjectionDef, ConfigError> {
574 let config: InjectionConfig =
575 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
576
577 if let (Some(_), Some(_)) = (header_name, &config.name) {
580 return Err(ConfigError::NameConflict(
581 "Name in both fence header and YAML payload for injection".to_string(),
582 ));
583 }
584
585 let mode = parse_injection_mode(&config.mode)?;
586
587 Ok(InjectionDef {
588 source: config.source,
589 target: config.target,
590 mode,
591 priority: config.priority,
592 max_tokens: config.max_tokens,
593 filter: None, })
595}
596
597pub fn parse_provider_block(
625 header_name: Option<&str>,
626 content: &str,
627) -> Result<ProviderDef, ConfigError> {
628 let config: ProviderConfig =
629 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
630
631 let name = resolve_block_name(header_name, &config.name, "provider")?;
632
633 let provider_type = parse_provider_type(&config.provider_type)?;
634 let api_key = parse_env_value(config.api_key.expose_secret());
635
636 Ok(ProviderDef {
637 name,
638 provider_type,
639 api_key,
640 model: config.model,
641 options: config.options,
642 })
643}
644
645pub fn parse_cache_block(
665 header_name: Option<&str>,
666 content: &str,
667) -> Result<CacheDef, ConfigError> {
668 let config: CacheConfig =
669 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
670
671 if let (Some(_), Some(_)) = (header_name, &config.name) {
673 return Err(ConfigError::NameConflict(
674 "Name in both fence header and YAML payload for cache".to_string(),
675 ));
676 }
677
678 let backend = parse_cache_backend(&config.backend)?;
679 let default_freshness = parse_freshness_def(config.default_freshness)?;
680
681 Ok(CacheDef {
682 backend,
683 path: config.path,
684 size_mb: config.size_mb,
685 default_freshness,
686 max_entries: config.max_entries,
687 ttl: config.ttl,
688 })
689}
690
691pub fn parse_trajectory_block(
721 header_name: Option<&str>,
722 content: &str,
723) -> Result<TrajectoryDef, ConfigError> {
724 let config: TrajectoryConfig =
725 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
726
727 let name = resolve_block_name(header_name, &config.name, "trajectory")?;
728
729 Ok(TrajectoryDef {
730 name,
731 description: config.description,
732 agent_type: config.agent_type,
733 token_budget: config.token_budget,
734 memory_refs: config.memory_refs,
735 metadata: config.metadata,
736 })
737}
738
739pub fn parse_agent_block(
776 header_name: Option<&str>,
777 content: &str,
778) -> Result<AgentDef, ConfigError> {
779 let config: AgentConfig =
780 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
781
782 let name = resolve_block_name(header_name, &config.name, "agent")?;
783
784 Ok(AgentDef {
785 name,
786 capabilities: config.capabilities,
787 constraints: AgentConstraints {
788 max_concurrent: config.constraints.max_concurrent,
789 timeout_ms: config.constraints.timeout_ms,
790 },
791 permissions: PermissionMatrix {
792 read: config.permissions.read,
793 write: config.permissions.write,
794 lock: config.permissions.lock,
795 },
796 })
797}
798
799pub fn parse_intent_block(
830 header_name: Option<&str>,
831 content: &str,
832) -> Result<IntentDef, ConfigError> {
833 let config: IntentConfig =
834 serde_yaml::from_str(content).map_err(|e| ConfigError::YamlParse(e.to_string()))?;
835
836 let name = resolve_block_name(header_name, &config.name, "intent")?;
837
838 let resolution_rules = config
840 .resolution_rules
841 .into_iter()
842 .map(|r| ResolutionRule {
843 condition: r.condition,
844 priority: r.priority,
845 escalate_to: r.escalate_to,
846 max_authority: r.max_authority,
847 })
848 .collect();
849
850 let autonomy_level = match config.autonomy_level.as_deref() {
852 Some(s) => parse_autonomy_level(s)?,
853 None => AutonomyLevel::default(),
854 };
855
856 let delegation_boundaries = match config.delegation_boundaries {
858 Some(db) => DelegationBoundary {
859 authorized_actions: db.authorized_actions,
860 requires_approval: db.requires_approval,
861 forbidden_actions: db.forbidden_actions,
862 },
863 None => DelegationBoundary::default(),
864 };
865
866 let alignment_signals = config
868 .alignment_signals
869 .into_iter()
870 .map(|s| {
871 let target = match s.target {
872 SignalTargetConfig::Above { value } => SignalTarget::Above(value),
873 SignalTargetConfig::Below { value } => SignalTarget::Below(value),
874 SignalTargetConfig::Between { min, max } => SignalTarget::Between { min, max },
875 };
876 AlignmentSignal {
877 name: s.name,
878 source: s.source,
879 metric: s.metric,
880 target,
881 }
882 })
883 .collect();
884
885 let drift_threshold = config.drift_threshold.unwrap_or(0.85);
886 if !(0.0..=1.0).contains(&drift_threshold) {
887 return Err(ConfigError::InvalidValue(
888 "drift_threshold must be between 0.0 and 1.0".to_string(),
889 ));
890 }
891
892 Ok(IntentDef {
893 name,
894 goals: config.goals,
895 resolution_rules,
896 autonomy_level,
897 delegation_boundaries,
898 alignment_signals,
899 drift_threshold,
900 })
901}
902
903fn validate_adapter_config(config: &AdapterConfig) -> Result<(), ConfigError> {
928 parse_adapter_type(&config.adapter_type)?;
930 Ok(())
931}
932
933fn parse_adapter_type(s: &str) -> Result<AdapterType, ConfigError> {
952 match s.to_lowercase().as_str() {
953 "postgres" | "postgresql" => Ok(AdapterType::Postgres),
954 "redis" => Ok(AdapterType::Redis),
955 "memory" => Ok(AdapterType::Memory),
956 other => Err(ConfigError::UnknownAdapter(other.to_string())),
957 }
958}
959
960fn parse_memory_type(s: &str) -> Result<MemoryType, ConfigError> {
977 match s.to_lowercase().as_str() {
978 "ephemeral" => Ok(MemoryType::Ephemeral),
979 "working" => Ok(MemoryType::Working),
980 "episodic" => Ok(MemoryType::Episodic),
981 "semantic" => Ok(MemoryType::Semantic),
982 "procedural" => Ok(MemoryType::Procedural),
983 "meta" => Ok(MemoryType::Meta),
984 other => Err(ConfigError::InvalidValue(format!(
985 "Unknown memory type '{}'",
986 other
987 ))),
988 }
989}
990
991fn parse_retention(s: &str) -> Result<Retention, ConfigError> {
1006 match s.to_lowercase().as_str() {
1007 "persistent" => Ok(Retention::Persistent),
1008 "session" => Ok(Retention::Session),
1009 "scope" => Ok(Retention::Scope),
1010 other => {
1011 if let Some(duration) = other.strip_prefix("duration:") {
1012 Ok(Retention::Duration(duration.to_string()))
1013 } else if let Some(max_str) = other.strip_prefix("max:") {
1014 let count = max_str
1015 .parse()
1016 .map_err(|e| ConfigError::InvalidValue(format!("Invalid max count: {e}")))?;
1017 Ok(Retention::Max(count))
1018 } else {
1019 Err(ConfigError::InvalidValue(format!(
1020 "Unknown retention '{}'",
1021 other
1022 )))
1023 }
1024 }
1025 }
1026}
1027
1028fn parse_lifecycle(s: &str) -> Result<Lifecycle, ConfigError> {
1051 match s.to_lowercase().as_str() {
1052 "explicit" => Ok(Lifecycle::Explicit),
1053 other => {
1054 if let Some(autoclose_str) = other.strip_prefix("autoclose:") {
1055 let trigger = parse_trigger(autoclose_str.trim())?;
1056 Ok(Lifecycle::AutoClose(trigger))
1057 } else {
1058 Err(ConfigError::InvalidValue(format!(
1059 "Unknown lifecycle '{}'",
1060 other
1061 )))
1062 }
1063 }
1064 }
1065}
1066
1067fn parse_trigger(s: &str) -> Result<Trigger, ConfigError> {
1086 match s.to_lowercase().as_str() {
1087 "task_start" => Ok(Trigger::TaskStart),
1088 "task_end" => Ok(Trigger::TaskEnd),
1089 "scope_close" => Ok(Trigger::ScopeClose),
1090 "turn_end" => Ok(Trigger::TurnEnd),
1091 "manual" => Ok(Trigger::Manual),
1092 other => {
1093 if let Some(schedule_str) = other.strip_prefix("schedule:") {
1094 Ok(Trigger::Schedule(schedule_str.to_string()))
1095 } else {
1096 Err(ConfigError::InvalidValue(format!(
1097 "Unknown trigger '{}'",
1098 other
1099 )))
1100 }
1101 }
1102 }
1103}
1104
1105fn parse_field_def(config: FieldConfig) -> Result<FieldDef, ConfigError> {
1128 let field_type = parse_field_type(&config.field_type)?;
1129 Ok(FieldDef {
1130 name: config.name,
1131 field_type,
1132 nullable: config.nullable,
1133 default: config.default,
1134 security: None, })
1136}
1137
1138fn parse_field_type(s: &str) -> Result<FieldType, ConfigError> {
1155 match s.to_lowercase().as_str() {
1156 "uuid" => Ok(FieldType::Uuid),
1157 "text" => Ok(FieldType::Text),
1158 "int" => Ok(FieldType::Int),
1159 "float" => Ok(FieldType::Float),
1160 "bool" => Ok(FieldType::Bool),
1161 "timestamp" => Ok(FieldType::Timestamp),
1162 "json" => Ok(FieldType::Json),
1163 other => {
1164 if let Some(dim_str) = other.strip_prefix("embedding:") {
1165 let dim = dim_str.parse().ok();
1166 Ok(FieldType::Embedding(dim))
1167 } else {
1168 Err(ConfigError::InvalidValue(format!(
1169 "Unknown field type '{}'",
1170 other
1171 )))
1172 }
1173 }
1174 }
1175}
1176
1177fn parse_index_def(config: IndexConfig) -> Result<IndexDef, ConfigError> {
1198 let index_type = parse_index_type(&config.index_type)?;
1199 Ok(IndexDef {
1200 field: config.field,
1201 index_type,
1202 options: config.options,
1203 })
1204}
1205
1206fn parse_index_type(s: &str) -> Result<IndexType, ConfigError> {
1221 match s.to_lowercase().as_str() {
1222 "btree" => Ok(IndexType::Btree),
1223 "hash" => Ok(IndexType::Hash),
1224 "gin" => Ok(IndexType::Gin),
1225 "hnsw" => Ok(IndexType::Hnsw),
1226 "ivfflat" => Ok(IndexType::Ivfflat),
1227 other => Err(ConfigError::InvalidValue(format!(
1228 "Unknown index type '{}'",
1229 other
1230 ))),
1231 }
1232}
1233
1234fn parse_modifier(s: String) -> Result<ModifierDef, ConfigError> {
1258 let s_lower = s.to_lowercase();
1259
1260 if let Some(provider) = s_lower.strip_prefix("embeddable:") {
1261 Ok(ModifierDef::Embeddable {
1262 provider: provider.to_string(),
1263 })
1264 } else if let Some(style_str) = s_lower.strip_prefix("summarizable:") {
1265 let style = match style_str {
1266 "brief" => SummaryStyle::Brief,
1267 "detailed" => SummaryStyle::Detailed,
1268 other => {
1269 return Err(ConfigError::InvalidValue(format!(
1270 "invalid summary style '{}', expected 'brief' or 'detailed'",
1271 other
1272 )))
1273 }
1274 };
1275 Ok(ModifierDef::Summarizable {
1276 style,
1277 on_triggers: vec![], })
1279 } else if let Some(mode_str) = s_lower.strip_prefix("lockable:") {
1280 let mode = match mode_str {
1281 "exclusive" => LockMode::Exclusive,
1282 "shared" => LockMode::Shared,
1283 other => {
1284 return Err(ConfigError::InvalidValue(format!(
1285 "invalid lock mode '{}', expected 'exclusive' or 'shared'",
1286 other
1287 )))
1288 }
1289 };
1290 Ok(ModifierDef::Lockable { mode })
1291 } else if s_lower == "embeddable" {
1292 Ok(ModifierDef::Embeddable {
1294 provider: String::new(),
1295 })
1296 } else if s_lower == "summarizable" {
1297 Ok(ModifierDef::Summarizable {
1299 style: SummaryStyle::Brief,
1300 on_triggers: vec![],
1301 })
1302 } else if s_lower == "lockable" {
1303 Ok(ModifierDef::Lockable {
1305 mode: LockMode::Exclusive,
1306 })
1307 } else {
1308 Err(ConfigError::InvalidValue(format!(
1309 "invalid modifier '{}', expected 'embeddable', 'summarizable', or 'lockable'",
1310 s
1311 )))
1312 }
1313}
1314
1315fn parse_policy_rule(config: PolicyRuleConfig) -> Result<PolicyRule, ConfigError> {
1331 let trigger = parse_trigger(&config.trigger)?;
1332 let actions = config
1333 .actions
1334 .into_iter()
1335 .map(parse_action)
1336 .collect::<Result<Vec<_>, _>>()?;
1337 Ok(PolicyRule { trigger, actions })
1338}
1339
1340fn parse_action(config: ActionConfig) -> Result<Action, ConfigError> {
1354 match config {
1355 ActionConfig::Summarize { target } => Ok(Action::Summarize(target)),
1356 ActionConfig::Checkpoint { target } => Ok(Action::Checkpoint(target)),
1357 ActionConfig::ExtractArtifacts { target } => Ok(Action::ExtractArtifacts(target)),
1358 ActionConfig::Notify { target } => Ok(Action::Notify(target)),
1359 ActionConfig::Inject { target, mode } => {
1360 let injection_mode = parse_injection_mode(&mode)?;
1361 Ok(Action::Inject {
1362 target,
1363 mode: injection_mode,
1364 })
1365 }
1366 }
1367}
1368
1369fn parse_injection_mode(s: &str) -> Result<InjectionMode, ConfigError> {
1384 match s.to_lowercase().as_str() {
1385 "full" => Ok(InjectionMode::Full),
1386 "summary" => Ok(InjectionMode::Summary),
1387 other if other.starts_with("topk:") => {
1388 let k = other["topk:".len()..]
1389 .parse()
1390 .map_err(|e| ConfigError::InvalidValue(format!("Invalid topk value: {e}")))?;
1391 Ok(InjectionMode::TopK(k))
1392 }
1393 other if other.starts_with("relevant:") => {
1394 let threshold = other["relevant:".len()..]
1395 .parse()
1396 .map_err(|e| ConfigError::InvalidValue(format!("Invalid threshold: {e}")))?;
1397 Ok(InjectionMode::Relevant(threshold))
1398 }
1399 other => Err(ConfigError::InvalidValue(format!(
1400 "Unknown injection mode '{}'",
1401 other
1402 ))),
1403 }
1404}
1405
1406fn parse_provider_type(s: &str) -> Result<ProviderType, ConfigError> {
1420 match s.to_lowercase().as_str() {
1421 "openai" => Ok(ProviderType::OpenAI),
1422 "anthropic" => Ok(ProviderType::Anthropic),
1423 "local" => Ok(ProviderType::Local),
1424 "custom" => Ok(ProviderType::Custom),
1425 other => Err(ConfigError::UnknownProvider(other.to_string())),
1426 }
1427}
1428
1429fn parse_env_value(s: &str) -> EnvValue {
1444 if let Some(rest) = s.strip_prefix("env:") {
1445 EnvValue::Env(rest.trim().to_string())
1446 } else {
1447 EnvValue::Literal(s.to_string())
1448 }
1449}
1450
1451fn parse_cache_backend(s: &str) -> Result<CacheBackendType, ConfigError> {
1469 match s.to_lowercase().as_str() {
1470 "lmdb" => Ok(CacheBackendType::Lmdb),
1471 "memory" => Ok(CacheBackendType::Memory),
1472 other => Err(ConfigError::InvalidValue(format!(
1473 "Unknown cache backend '{}'",
1474 other
1475 ))),
1476 }
1477}
1478
1479fn parse_freshness_def(config: FreshnessConfig) -> Result<FreshnessDef, ConfigError> {
1496 match config {
1497 FreshnessConfig::BestEffort { max_staleness } => {
1498 Ok(FreshnessDef::BestEffort { max_staleness })
1499 }
1500 FreshnessConfig::Strict => Ok(FreshnessDef::Strict),
1501 }
1502}
1503
1504fn parse_autonomy_level(s: &str) -> Result<AutonomyLevel, ConfigError> {
1516 match s.to_lowercase().as_str() {
1517 "operator" => Ok(AutonomyLevel::Operator),
1518 "collaborator" => Ok(AutonomyLevel::Collaborator),
1519 "consultant" => Ok(AutonomyLevel::Consultant),
1520 "approver" => Ok(AutonomyLevel::Approver),
1521 "observer" => Ok(AutonomyLevel::Observer),
1522 other => Err(ConfigError::InvalidValue(format!(
1523 "Unknown autonomy level '{}', expected operator|collaborator|consultant|approver|observer",
1524 other
1525 ))),
1526 }
1527}
1528
1529#[cfg(test)]
1534mod tests {
1535 use super::*;
1536
1537 #[test]
1538 fn test_adapter_parse_with_header_name() {
1539 let yaml = r#"
1540adapter_type: postgres
1541connection: "postgresql://localhost/test"
1542"#;
1543 let result = parse_adapter_block(Some("postgres_main"), yaml);
1544 assert!(
1545 result.is_ok(),
1546 "Failed to parse adapter: {:?}",
1547 result.err()
1548 );
1549
1550 let adapter = result.expect("adapter parsing verified above");
1551 assert_eq!(adapter.name, "postgres_main");
1552 assert_eq!(adapter.adapter_type, AdapterType::Postgres);
1553 assert_eq!(adapter.connection, "postgresql://localhost/test");
1554 }
1555
1556 #[test]
1557 fn test_adapter_parse_with_payload_name() {
1558 let yaml = r#"
1559name: postgres_main
1560adapter_type: postgres
1561connection: "postgresql://localhost/test"
1562"#;
1563 let result = parse_adapter_block(None, yaml);
1564 assert!(
1565 result.is_ok(),
1566 "Failed to parse adapter: {:?}",
1567 result.err()
1568 );
1569
1570 let adapter = result.expect("adapter parsing verified above");
1571 assert_eq!(adapter.name, "postgres_main");
1572 }
1573
1574 #[test]
1575 fn test_adapter_deny_unknown_fields() {
1576 let yaml = r#"
1577adapter_type: postgres
1578connection: "postgresql://localhost/test"
1579unknown_field: bad
1580"#;
1581 let result = parse_adapter_block(Some("test"), yaml);
1582 assert!(result.is_err(), "Should reject unknown field");
1583
1584 let err = result.unwrap_err();
1585 match err {
1586 ConfigError::YamlParse(msg) => {
1587 assert!(
1588 msg.contains("unknown field"),
1589 "Expected 'unknown field' error, got: {}",
1590 msg
1591 );
1592 }
1593 _ => panic!("Expected YamlParse error, got: {:?}", err),
1594 }
1595 }
1596
1597 #[test]
1598 fn test_adapter_name_conflict() {
1599 let yaml = r#"
1600name: payload_name
1601adapter_type: postgres
1602connection: "postgresql://localhost/test"
1603"#;
1604 let result = parse_adapter_block(Some("header_name"), yaml);
1605 assert!(result.is_err(), "Should reject name conflict");
1606
1607 let err = result.unwrap_err();
1608 match err {
1609 ConfigError::NameConflict(_) => {
1610 }
1612 _ => panic!("Expected NameConflict error, got: {:?}", err),
1613 }
1614 }
1615
1616 #[test]
1617 fn test_adapter_missing_name() {
1618 let yaml = r#"
1619adapter_type: postgres
1620connection: "postgresql://localhost/test"
1621"#;
1622 let result = parse_adapter_block(None, yaml);
1623 assert!(result.is_err(), "Should require name");
1624
1625 let err = result.unwrap_err();
1626 match err {
1627 ConfigError::MissingName(_) => {
1628 }
1630 _ => panic!("Expected MissingName error, got: {:?}", err),
1631 }
1632 }
1633
1634 #[test]
1635 fn test_adapter_case_preservation() {
1636 let yaml = r#"
1637adapter_type: PostgreS
1638connection: "PostgreSQL://LocalHost/Test"
1639"#;
1640 let result = parse_adapter_block(Some("MyAdapter"), yaml);
1641 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1642
1643 let adapter = result.expect("adapter parsing verified above");
1644 assert_eq!(adapter.name, "MyAdapter");
1645 assert_eq!(adapter.connection, "PostgreSQL://LocalHost/Test");
1647 }
1648
1649 #[test]
1650 fn test_provider_parse_with_env_key() {
1651 let yaml = r#"
1652provider_type: openai
1653api_key: env:OPENAI_API_KEY
1654model: "gpt-4"
1655"#;
1656 let result = parse_provider_block(Some("my_provider"), yaml);
1657 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1658
1659 let provider = result.expect("provider parsing verified above");
1660 assert_eq!(provider.name, "my_provider");
1661 assert_eq!(provider.provider_type, ProviderType::OpenAI);
1662 match provider.api_key {
1663 EnvValue::Env(var) => assert_eq!(var, "OPENAI_API_KEY"),
1664 _ => panic!("Expected Env variant"),
1665 }
1666 }
1667
1668 #[test]
1669 fn test_provider_deny_unknown_fields() {
1670 let yaml = r#"
1671provider_type: openai
1672api_key: "secret"
1673model: "gpt-4"
1674invalid_option: true
1675"#;
1676 let result = parse_provider_block(Some("test"), yaml);
1677 assert!(result.is_err(), "Should reject unknown field");
1678 }
1679
1680 #[test]
1681 fn test_provider_config_debug_redacts_api_key() {
1682 let yaml = r#"
1683provider_type: openai
1684api_key: "sk_test_real_secret"
1685model: "gpt-4"
1686"#;
1687 let parsed: ProviderConfig = serde_yaml::from_str(yaml).expect("provider yaml");
1688 let dbg = format!("{parsed:?}");
1689 assert!(dbg.contains("[REDACTED"));
1690 assert!(!dbg.contains("sk_test_real_secret"));
1691 }
1692
1693 #[test]
1694 fn test_injection_mode_parsing() {
1695 let yaml = r#"
1696source: "memories.episodic"
1697target: "context.main"
1698mode: full
1699priority: 100
1700"#;
1701 let result = parse_injection_block(None, yaml);
1702 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1703
1704 let injection = result.expect("injection parsing verified above");
1705 assert_eq!(injection.mode, InjectionMode::Full);
1706 assert_eq!(injection.priority, 100);
1707 }
1708
1709 #[test]
1710 fn test_cache_freshness_parsing() {
1711 let yaml = r#"
1712backend: lmdb
1713path: "/var/cache"
1714size_mb: 1024
1715default_freshness:
1716 type: best_effort
1717 max_staleness: "60s"
1718"#;
1719 let result = parse_cache_block(Some("main"), yaml);
1720 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1721
1722 let cache = result.expect("cache parsing verified above");
1723 match cache.default_freshness {
1724 FreshnessDef::BestEffort { max_staleness } => {
1725 assert_eq!(max_staleness, "60s");
1726 }
1727 _ => panic!("Expected BestEffort variant"),
1728 }
1729 }
1730
1731 #[test]
1736 fn test_intent_parse_with_header_name() {
1737 let yaml = r#"
1738goals:
1739 - "maximize_customer_retention"
1740 - "minimize_churn"
1741autonomy_level: collaborator
1742drift_threshold: 0.9
1743"#;
1744 let result = parse_intent_block(Some("customer_success"), yaml);
1745 assert!(result.is_ok(), "Failed to parse intent: {:?}", result.err());
1746
1747 let intent = result.expect("intent parsing verified above");
1748 assert_eq!(intent.name, "customer_success");
1749 assert_eq!(intent.goals.len(), 2);
1750 assert_eq!(intent.autonomy_level, AutonomyLevel::Collaborator);
1751 assert!((intent.drift_threshold - 0.9).abs() < f64::EPSILON);
1752 }
1753
1754 #[test]
1755 fn test_intent_parse_with_payload_name() {
1756 let yaml = r#"
1757name: revenue_growth
1758goals:
1759 - "increase_arpu"
1760"#;
1761 let result = parse_intent_block(None, yaml);
1762 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1763
1764 let intent = result.expect("intent parsing verified above");
1765 assert_eq!(intent.name, "revenue_growth");
1766 }
1767
1768 #[test]
1769 fn test_intent_name_conflict() {
1770 let yaml = r#"
1771name: payload_name
1772goals: []
1773"#;
1774 let result = parse_intent_block(Some("header_name"), yaml);
1775 assert!(result.is_err(), "Should reject name conflict");
1776 assert!(matches!(result.unwrap_err(), ConfigError::NameConflict(_)));
1777 }
1778
1779 #[test]
1780 fn test_intent_missing_name() {
1781 let yaml = r#"
1782goals: []
1783"#;
1784 let result = parse_intent_block(None, yaml);
1785 assert!(result.is_err(), "Should require name");
1786 assert!(matches!(result.unwrap_err(), ConfigError::MissingName(_)));
1787 }
1788
1789 #[test]
1790 fn test_intent_full_config() {
1791 let yaml = r#"
1792goals:
1793 - "retain_customers"
1794 - "grow_revenue"
1795resolution_rules:
1796 - condition: "customer_sentiment < 0.3"
1797 priority: ["retain_customers", "grow_revenue"]
1798 escalate_to: "customer_success_manager"
1799 max_authority: 500.0
1800autonomy_level: approver
1801delegation_boundaries:
1802 authorized_actions:
1803 - "send_discount"
1804 - "schedule_call"
1805 requires_approval:
1806 - "refund_over_100"
1807 forbidden_actions:
1808 - "delete_account"
1809alignment_signals:
1810 - name: retention_rate
1811 source: customer_events
1812 metric: 30d_retention
1813 target:
1814 type: above
1815 value: 0.85
1816drift_threshold: 0.8
1817"#;
1818 let result = parse_intent_block(Some("full_intent"), yaml);
1819 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1820
1821 let intent = result.expect("intent parsing verified above");
1822 assert_eq!(intent.name, "full_intent");
1823 assert_eq!(intent.goals.len(), 2);
1824 assert_eq!(intent.resolution_rules.len(), 1);
1825 assert_eq!(
1826 intent.resolution_rules[0].escalate_to,
1827 Some("customer_success_manager".to_string())
1828 );
1829 assert!((intent.resolution_rules[0].max_authority - 500.0).abs() < f64::EPSILON);
1830 assert_eq!(intent.autonomy_level, AutonomyLevel::Approver);
1831 assert_eq!(intent.delegation_boundaries.authorized_actions.len(), 2);
1832 assert_eq!(intent.delegation_boundaries.requires_approval.len(), 1);
1833 assert_eq!(intent.delegation_boundaries.forbidden_actions.len(), 1);
1834 assert_eq!(intent.alignment_signals.len(), 1);
1835 assert!(
1836 matches!(intent.alignment_signals[0].target, SignalTarget::Above(v) if (v - 0.85).abs() < f64::EPSILON)
1837 );
1838 assert!((intent.drift_threshold - 0.8).abs() < f64::EPSILON);
1839 }
1840
1841 #[test]
1842 fn test_intent_defaults() {
1843 let yaml = r#"
1844goals: []
1845"#;
1846 let result = parse_intent_block(Some("minimal"), yaml);
1847 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
1848
1849 let intent = result.expect("intent parsing verified above");
1850 assert_eq!(intent.autonomy_level, AutonomyLevel::Operator);
1851 assert_eq!(intent.delegation_boundaries, DelegationBoundary::default());
1852 assert!(intent.alignment_signals.is_empty());
1853 assert!((intent.drift_threshold - 0.85).abs() < f64::EPSILON);
1854 }
1855
1856 #[test]
1857 fn test_intent_deny_unknown_fields() {
1858 let yaml = r#"
1859goals: []
1860unknown_field: bad
1861"#;
1862 let result = parse_intent_block(Some("test"), yaml);
1863 assert!(result.is_err(), "Should reject unknown field");
1864 assert!(matches!(result.unwrap_err(), ConfigError::YamlParse(_)));
1865 }
1866}