Skip to main content

ggen_config/config_lib/
schema.rs

1//! Schema definitions for ggen.toml configuration
2//!
3//! This module defines the complete structure of ggen.toml files
4//! using serde-compatible Rust structs.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Root configuration structure for ggen.toml
10/// `PartialEq` without Eq: Contains nested config structs via composition
11#[allow(clippy::derive_partial_eq_without_eq)]
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(deny_unknown_fields)]
14pub struct GgenConfig {
15    /// Project metadata
16    pub project: ProjectConfig,
17
18    /// AI configuration (optional)
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub ai: Option<AiConfig>,
21
22    /// Templates configuration (optional)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub templates: Option<TemplatesConfig>,
25
26    /// RDF configuration (optional)
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub rdf: Option<RdfConfig>,
29
30    /// SPARQL configuration (optional)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub sparql: Option<SparqlConfig>,
33
34    /// Lifecycle configuration (optional)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub lifecycle: Option<LifecycleConfig>,
37
38    /// Security settings (optional)
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub security: Option<SecurityConfig>,
41
42    /// Performance settings (optional)
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub performance: Option<PerformanceConfig>,
45
46    /// Logging configuration (optional)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub logging: Option<LoggingConfig>,
49
50    /// Telemetry configuration (optional)
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub telemetry: Option<TelemetryConfig>,
53
54    /// Feature flags (optional)
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub features: Option<HashMap<String, bool>>,
57
58    /// Environment-specific overrides (optional)
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub env: Option<HashMap<String, serde_json::Value>>,
61
62    /// Build configuration (optional)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub build: Option<BuildConfig>,
65
66    /// Test configuration (optional)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub test: Option<TestConfig>,
69
70    /// Package metadata (for marketplace packages)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub package: Option<PackageMetadata>,
73
74    /// Inference configuration (optional)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub inference: Option<InferenceConfig>,
77
78    /// Generation configuration (optional)
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub generation: Option<GenerationConfig>,
81
82    /// MCP configuration (optional)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub mcp: Option<McpConfig>,
85
86    /// A2A configuration (optional)
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub a2a: Option<A2AConfig>,
89}
90
91/// Project metadata configuration
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93pub struct ProjectConfig {
94    /// Project name
95    pub name: String,
96
97    /// Project version
98    pub version: String,
99
100    /// Project description (optional)
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub description: Option<String>,
103
104    /// Project authors (optional)
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub authors: Option<Vec<String>>,
107
108    /// Project license (optional)
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub license: Option<String>,
111
112    /// Project repository URL (optional)
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub repository: Option<String>,
115}
116
117/// AI provider configuration
118/// `PartialEq` without Eq: temperature (f32) does not implement Eq
119#[allow(clippy::derive_partial_eq_without_eq)]
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121pub struct AiConfig {
122    /// AI provider (openai, ollama, anthropic, etc.)
123    pub provider: String,
124
125    /// Model name
126    pub model: String,
127
128    /// Temperature for generation (0.0 - 1.0)
129    #[serde(default = "default_temperature")]
130    pub temperature: f32,
131
132    /// Maximum tokens for generation
133    #[serde(default = "default_max_tokens")]
134    pub max_tokens: u32,
135
136    /// Request timeout in seconds
137    #[serde(default = "default_timeout")]
138    pub timeout: u32,
139
140    /// System and user prompts (optional)
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub prompts: Option<AiPrompts>,
143
144    /// Validation settings (optional)
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub validation: Option<AiValidation>,
147}
148
149/// AI prompt configuration
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
151pub struct AiPrompts {
152    /// System prompt
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub system: Option<String>,
155
156    /// User prompt prefix
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub user_prefix: Option<String>,
159}
160
161/// AI validation configuration
162/// `PartialEq` without Eq: `quality_threshold` (f32) does not implement Eq
163#[allow(clippy::derive_partial_eq_without_eq)]
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
165pub struct AiValidation {
166    /// Whether validation is enabled
167    #[serde(default)]
168    pub enabled: bool,
169
170    /// Quality threshold (0.0 - 1.0)
171    #[serde(default = "default_quality_threshold")]
172    pub quality_threshold: f32,
173
174    /// Maximum validation iterations
175    #[serde(default = "default_max_iterations")]
176    pub max_iterations: u32,
177}
178
179/// Templates configuration
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
181pub struct TemplatesConfig {
182    /// Template source directory
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub directory: Option<String>,
185
186    /// Output directory for generated files
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub output_directory: Option<String>,
189
190    /// Enable backup before overwriting
191    #[serde(default)]
192    pub backup_enabled: bool,
193
194    /// Idempotent generation (only update if changed)
195    #[serde(default)]
196    pub idempotent: bool,
197}
198
199/// RDF configuration
200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
201pub struct RdfConfig {
202    /// Base IRI/URI for RDF entities
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub base_uri: Option<String>,
205
206    /// Base IRI (alternative name)
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub base_iri: Option<String>,
209
210    /// RDF namespace prefixes
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub prefixes: Option<HashMap<String, String>>,
213
214    /// Default RDF format (turtle, rdfxml, etc.)
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub default_format: Option<String>,
217
218    /// Enable query caching
219    #[serde(default)]
220    pub cache_queries: bool,
221}
222
223/// SPARQL configuration
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225pub struct SparqlConfig {
226    /// Query timeout in seconds
227    #[serde(default = "default_sparql_timeout")]
228    pub timeout: u32,
229
230    /// Maximum results per query
231    #[serde(default = "default_max_results")]
232    pub max_results: u32,
233
234    /// Enable query caching
235    #[serde(default)]
236    pub cache_enabled: bool,
237}
238
239/// Lifecycle configuration
240#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
241pub struct LifecycleConfig {
242    /// Enable lifecycle management
243    #[serde(default)]
244    pub enabled: bool,
245
246    /// Lifecycle config file (e.g., make.toml)
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub config_file: Option<String>,
249
250    /// Cache directory
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub cache_directory: Option<String>,
253
254    /// State file path
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub state_file: Option<String>,
257
258    /// Lifecycle phases
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub phases: Option<HashMap<String, Vec<String>>>,
261}
262
263/// Security configuration
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
265pub struct SecurityConfig {
266    /// Enable path traversal protection
267    #[serde(default = "default_true")]
268    pub path_traversal_protection: bool,
269
270    /// Enable shell injection protection
271    #[serde(default = "default_true")]
272    pub shell_injection_protection: bool,
273
274    /// Enable template sandboxing
275    #[serde(default = "default_true")]
276    pub template_sandboxing: bool,
277
278    /// Validate file paths
279    #[serde(default = "default_true")]
280    pub validate_paths: bool,
281
282    /// Require user confirmation for destructive operations
283    #[serde(default)]
284    pub require_confirmation: bool,
285
286    /// Audit all operations
287    #[serde(default)]
288    pub audit_operations: bool,
289
290    /// Backup before write operations
291    #[serde(default)]
292    pub backup_before_write: bool,
293}
294
295/// Performance configuration
296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
297pub struct PerformanceConfig {
298    /// Enable parallel execution
299    #[serde(default)]
300    pub parallel_execution: bool,
301
302    /// Maximum parallel workers
303    #[serde(default = "default_max_workers")]
304    pub max_workers: u32,
305
306    /// Cache size (as string, e.g., "1GB")
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub cache_size: Option<String>,
309
310    /// Enable profiling
311    #[serde(default)]
312    pub enable_profiling: bool,
313
314    /// Memory limit in MB
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub memory_limit_mb: Option<u32>,
317}
318
319/// Logging configuration
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
321pub struct LoggingConfig {
322    /// Log level (debug, info, warn, error)
323    #[serde(default = "default_log_level")]
324    pub level: String,
325
326    /// Log format (json, text)
327    #[serde(default = "default_log_format")]
328    pub format: String,
329
330    /// Log file path (optional)
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub file: Option<String>,
333
334    /// Log rotation (daily, size-based, etc.)
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub rotation: Option<String>,
337}
338
339/// Telemetry configuration
340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
341pub struct TelemetryConfig {
342    /// OTLP endpoint (default: <http://localhost:4317>)
343    #[serde(default = "default_telemetry_endpoint")]
344    pub endpoint: String,
345
346    /// Service name for traces (default: "ggen")
347    #[serde(default = "default_telemetry_service_name")]
348    pub service_name: String,
349
350    /// Whether to enable console output
351    #[serde(default = "default_telemetry_console_output")]
352    pub console_output: bool,
353}
354
355/// Build configuration
356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
357pub struct BuildConfig {
358    /// Build target (release, debug)
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub target: Option<String>,
361
362    /// Features to enable
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub features: Option<Vec<String>>,
365
366    /// Build profile
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub profile: Option<String>,
369
370    /// Number of parallel build jobs
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub parallel_jobs: Option<u32>,
373}
374
375/// Test configuration
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
377pub struct TestConfig {
378    /// Test framework
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub framework: Option<String>,
381
382    /// Enable parallel test execution
383    #[serde(default)]
384    pub parallel: bool,
385
386    /// Test timeout in seconds
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub timeout_seconds: Option<u32>,
389
390    /// Enable code coverage
391    #[serde(default)]
392    pub coverage_enabled: bool,
393
394    /// Coverage threshold percentage
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub coverage_threshold: Option<u32>,
397}
398
399/// Package metadata (for marketplace packages)
400#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
401pub struct PackageMetadata {
402    /// Package name
403    pub name: String,
404
405    /// Package version
406    pub version: String,
407
408    /// Description
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub description: Option<String>,
411
412    /// Authors
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub authors: Option<Vec<String>>,
415
416    /// License
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub license: Option<String>,
419
420    /// Repository URL
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub repository: Option<String>,
423
424    /// Keywords
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub keywords: Option<Vec<String>>,
427
428    /// Categories
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub categories: Option<Vec<String>>,
431
432    /// Package-specific metadata
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub metadata: Option<HashMap<String, serde_json::Value>>,
435}
436
437/// MCP (Model Context Protocol) configuration
438///
439/// Configuration for MCP server and client integration.
440/// Enables LLM-driven agents to discover and invoke tools through
441/// a standardized JSON-RPC interface.
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
443pub struct McpConfig {
444    /// Server name
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub name: Option<String>,
447
448    /// Server version
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub version: Option<String>,
451
452    /// Maximum time to wait for tool execution (milliseconds)
453    #[serde(default = "default_mcp_tool_timeout")]
454    pub tool_timeout_ms: u64,
455
456    /// Maximum concurrent requests
457    #[serde(default = "default_mcp_max_concurrent")]
458    pub max_concurrent_requests: usize,
459
460    /// Server transport configuration
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub transport: Option<McpTransportConfig>,
463
464    /// Tool registry configuration
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub tools: Option<McpToolsConfig>,
467
468    /// ZAI integration settings
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub zai: Option<McpZaiConfig>,
471
472    /// Enable MCP server
473    #[serde(default = "default_mcp_enabled")]
474    pub enabled: bool,
475
476    /// Server discovery settings
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub discovery: Option<McpDiscoveryConfig>,
479}
480
481/// MCP transport configuration
482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
483pub struct McpTransportConfig {
484    /// Transport type (stdio, http, websocket)
485    #[serde(default = "default_mcp_transport_type")]
486    pub transport_type: String,
487
488    /// HTTP server port (for http/websocket transport)
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub port: Option<u16>,
491
492    /// HTTP server host
493    #[serde(default = "default_mcp_host")]
494    pub host: String,
495
496    /// TLS configuration
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub tls: Option<McpTlsConfig>,
499
500    /// Request timeout (seconds)
501    #[serde(default = "default_mcp_request_timeout")]
502    pub request_timeout_seconds: u64,
503}
504
505/// MCP TLS configuration
506#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
507pub struct McpTlsConfig {
508    /// Enable TLS
509    #[serde(default)]
510    pub enabled: bool,
511
512    /// Path to certificate file
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub cert_path: Option<String>,
515
516    /// Path to private key file
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub key_path: Option<String>,
519
520    /// Path to CA certificate file
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub ca_path: Option<String>,
523}
524
525/// MCP tools configuration
526#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
527pub struct McpToolsConfig {
528    /// Tool discovery path (directory to scan for tools)
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub discovery_path: Option<String>,
531
532    /// Tool registration is required
533    #[serde(default)]
534    pub require_registration: bool,
535
536    /// Validate tool signatures
537    #[serde(default = "default_true")]
538    pub validate_signatures: bool,
539
540    /// Allowed tool prefixes (whitelist)
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub allowed_prefixes: Option<Vec<String>>,
543}
544
545/// MCP ZAI integration configuration
546#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
547pub struct McpZaiConfig {
548    /// Enable ZAI integration
549    #[serde(default)]
550    pub enabled: bool,
551
552    /// ZAI provider URL
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub provider_url: Option<String>,
555
556    /// ZAI model for tool processing
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub model: Option<String>,
559
560    /// Cache ZAI responses
561    #[serde(default = "default_mcp_zai_cache")]
562    pub cache_enabled: bool,
563
564    /// Cache TTL (seconds)
565    #[serde(default = "default_mcp_zai_cache_ttl")]
566    pub cache_ttl_seconds: u64,
567}
568
569/// MCP discovery configuration
570#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
571pub struct McpDiscoveryConfig {
572    /// Enable server discovery
573    #[serde(default)]
574    pub enabled: bool,
575
576    /// Discovery method (local, remote, hybrid)
577    #[serde(default = "default_mcp_discovery_method")]
578    pub method: String,
579
580    /// Remote discovery URL
581    #[serde(skip_serializing_if = "Option::is_none")]
582    pub registry_url: Option<String>,
583
584    /// Discovery cache TTL (seconds)
585    #[serde(default = "default_mcp_discovery_cache_ttl")]
586    pub cache_ttl_seconds: u64,
587}
588
589/// A2A (Agent-to-Agent) configuration
590///
591/// Configuration for A2A message passing and agent coordination.
592/// Enables agents to communicate, coordinate, and orchestrate workflows.
593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
594pub struct A2AConfig {
595    /// Agent identifier
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub agent_id: Option<String>,
598
599    /// Agent name
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub agent_name: Option<String>,
602
603    /// Agent type (coordinator, worker, specialist, etc.)
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub agent_type: Option<String>,
606
607    /// Transport configuration
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub transport: Option<A2ATransportConfig>,
610
611    /// Message handling configuration
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub messaging: Option<A2AMessagingConfig>,
614
615    /// Orchestration settings
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub orchestration: Option<A2AOrchestrationConfig>,
618
619    /// Agent capabilities
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub capabilities: Option<Vec<String>>,
622
623    /// Enable A2A
624    #[serde(default = "default_a2a_enabled")]
625    pub enabled: bool,
626}
627
628/// A2A transport configuration
629#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
630pub struct A2ATransportConfig {
631    /// Transport type (memory, http, websocket, amqp)
632    #[serde(default = "default_a2a_transport_type")]
633    pub transport_type: String,
634
635    /// Server bind address
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub bind_address: Option<String>,
638
639    /// Server port
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub port: Option<u16>,
642
643    /// Connection timeout (milliseconds)
644    #[serde(default = "default_a2a_timeout")]
645    pub timeout_ms: u64,
646
647    /// Maximum concurrent connections
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub max_connections: Option<usize>,
650
651    /// Retry configuration
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub retry: Option<A2ARetryConfig>,
654}
655
656/// A2A retry configuration
657#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
658pub struct A2ARetryConfig {
659    /// Maximum retry attempts
660    #[serde(default = "default_a2a_max_retries")]
661    pub max_attempts: u32,
662
663    /// Initial retry delay (milliseconds)
664    #[serde(default = "default_a2a_retry_delay")]
665    pub initial_delay_ms: u64,
666
667    /// Maximum retry delay (milliseconds)
668    #[serde(default = "default_a2a_max_retry_delay")]
669    pub max_delay_ms: u64,
670
671    /// Enable exponential backoff
672    #[serde(default)]
673    pub exponential_backoff: bool,
674}
675
676/// A2A messaging configuration
677#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
678pub struct A2AMessagingConfig {
679    /// Message queue size
680    #[serde(default = "default_a2a_queue_size")]
681    pub queue_size: usize,
682
683    /// Message TTL (seconds)
684    #[serde(default = "default_a2a_message_ttl")]
685    pub message_ttl_seconds: u64,
686
687    /// Enable message persistence
688    #[serde(default)]
689    pub persistence_enabled: bool,
690
691    /// Persistence path
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub persistence_path: Option<String>,
694
695    /// Enable message signing
696    #[serde(default)]
697    pub signing_enabled: bool,
698
699    /// Signature algorithm (ed25519, rsa, ecdsa)
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub signature_algorithm: Option<String>,
702}
703
704/// A2A orchestration configuration
705#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
706pub struct A2AOrchestrationConfig {
707    /// Orchestration mode (centralized, decentralized, hierarchical)
708    #[serde(default = "default_a2a_orchestration_mode")]
709    pub mode: String,
710
711    /// Coordinator address (for centralized mode)
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub coordinator_address: Option<String>,
714
715    /// Heartbeat interval (seconds)
716    #[serde(default = "default_a2a_heartbeat_interval")]
717    pub heartbeat_interval_seconds: u64,
718
719    /// Agent timeout (seconds)
720    #[serde(default = "default_a2a_agent_timeout")]
721    pub agent_timeout_seconds: u64,
722
723    /// Enable consensus
724    #[serde(default)]
725    pub consensus_enabled: bool,
726
727    /// Consensus algorithm (raft, pbft, naive)
728    #[serde(skip_serializing_if = "Option::is_none")]
729    pub consensus_algorithm: Option<String>,
730}
731
732/// Inference configuration for graph modifications
733#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
734pub struct InferenceConfig {
735    /// List of inference rules
736    #[serde(default)]
737    pub rules: Vec<InferenceRule>,
738}
739
740/// A single inference rule
741#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
742pub struct InferenceRule {
743    /// Rule name
744    pub name: String,
745    /// CONSTRUCT query
746    pub construct: String,
747    /// Optional execution order
748    #[serde(default)]
749    pub order: i32,
750    /// Optional ASK query condition
751    #[serde(skip_serializing_if = "Option::is_none")]
752    pub when: Option<String>,
753}
754
755/// Generation configuration
756#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
757pub struct GenerationConfig {
758    /// Output directory
759    pub output_dir: String,
760    /// Generation rules
761    #[serde(default)]
762    pub rules: Vec<serde_json::Value>,
763}
764
765// Default value functions for serde
766const fn default_temperature() -> f32 {
767    0.7
768}
769
770const fn default_max_tokens() -> u32 {
771    2000
772}
773
774const fn default_timeout() -> u32 {
775    30
776}
777
778const fn default_quality_threshold() -> f32 {
779    0.8
780}
781
782const fn default_max_iterations() -> u32 {
783    3
784}
785
786const fn default_sparql_timeout() -> u32 {
787    10
788}
789
790const fn default_max_results() -> u32 {
791    1000
792}
793
794fn default_max_workers() -> u32 {
795    num_cpus()
796}
797
798fn default_log_level() -> String {
799    "info".to_string()
800}
801
802fn default_log_format() -> String {
803    "text".to_string()
804}
805
806const fn default_true() -> bool {
807    true
808}
809
810fn num_cpus() -> u32 {
811    std::thread::available_parallelism().map_or(4, |n| n.get() as u32)
812}
813
814impl Default for GgenConfig {
815    fn default() -> Self {
816        Self {
817            project: ProjectConfig {
818                name: "unnamed".to_string(),
819                version: "0.1.0".to_string(),
820                description: None,
821                authors: None,
822                license: None,
823                repository: None,
824            },
825            ai: None,
826            templates: None,
827            rdf: None,
828            sparql: None,
829            lifecycle: None,
830            security: None,
831            performance: None,
832            logging: None,
833            telemetry: None,
834            features: None,
835            env: None,
836            build: None,
837            test: None,
838            package: None,
839            inference: None,
840            generation: None,
841            mcp: None,
842            a2a: None,
843        }
844    }
845}
846
847// MCP and A2A default value functions for serde
848const fn default_mcp_tool_timeout() -> u64 {
849    30000
850}
851
852const fn default_mcp_max_concurrent() -> usize {
853    100
854}
855
856fn default_mcp_transport_type() -> String {
857    "stdio".to_string()
858}
859
860fn default_mcp_host() -> String {
861    "127.0.0.1".to_string()
862}
863
864const fn default_mcp_request_timeout() -> u64 {
865    30
866}
867
868fn default_mcp_enabled() -> bool {
869    false
870}
871
872const fn default_mcp_zai_cache() -> bool {
873    true
874}
875
876const fn default_mcp_zai_cache_ttl() -> u64 {
877    3600
878}
879
880fn default_mcp_discovery_method() -> String {
881    "local".to_string()
882}
883
884const fn default_mcp_discovery_cache_ttl() -> u64 {
885    7200
886}
887
888fn default_a2a_enabled() -> bool {
889    false
890}
891
892fn default_a2a_transport_type() -> String {
893    "memory".to_string()
894}
895
896const fn default_a2a_timeout() -> u64 {
897    5000
898}
899
900const fn default_a2a_max_retries() -> u32 {
901    3
902}
903
904const fn default_a2a_retry_delay() -> u64 {
905    1000
906}
907
908const fn default_a2a_max_retry_delay() -> u64 {
909    30000
910}
911
912const fn default_a2a_queue_size() -> usize {
913    1000
914}
915
916const fn default_a2a_message_ttl() -> u64 {
917    3600
918}
919
920fn default_a2a_orchestration_mode() -> String {
921    "decentralized".to_string()
922}
923
924const fn default_a2a_heartbeat_interval() -> u64 {
925    30
926}
927
928const fn default_a2a_agent_timeout() -> u64 {
929    300
930}
931
932fn default_telemetry_endpoint() -> String {
933    "http://localhost:4317".to_string()
934}
935
936fn default_telemetry_service_name() -> String {
937    "ggen".to_string()
938}
939
940fn default_telemetry_console_output() -> bool {
941    false
942}
943
944// =========================================================================
945// Validate Trait Implementations
946// =========================================================================
947
948use star_toml::{Validate, Validator};
949
950impl Validate for GgenConfig {
951    fn validate(&self, v: &mut Validator) {
952        v.field("project", |v| self.project.validate(v));
953        if let Some(ai) = &self.ai {
954            v.field("ai", |v| ai.validate(v));
955        }
956        if let Some(templates) = &self.templates {
957            v.field("templates", |v| templates.validate(v));
958        }
959        if let Some(rdf) = &self.rdf {
960            v.field("rdf", |v| rdf.validate(v));
961        }
962        if let Some(sparql) = &self.sparql {
963            v.field("sparql", |v| sparql.validate(v));
964        }
965        if let Some(lifecycle) = &self.lifecycle {
966            v.field("lifecycle", |v| lifecycle.validate(v));
967        }
968        if let Some(security) = &self.security {
969            v.field("security", |v| security.validate(v));
970        }
971        if let Some(performance) = &self.performance {
972            v.field("performance", |v| performance.validate(v));
973        }
974        if let Some(logging) = &self.logging {
975            v.field("logging", |v| logging.validate(v));
976        }
977        if let Some(telemetry) = &self.telemetry {
978            v.field("telemetry", |v| telemetry.validate(v));
979        }
980        if let Some(build) = &self.build {
981            v.field("build", |v| build.validate(v));
982        }
983        if let Some(test) = &self.test {
984            v.field("test", |v| test.validate(v));
985        }
986        if let Some(package) = &self.package {
987            v.field("package", |v| package.validate(v));
988        }
989        if let Some(mcp) = &self.mcp {
990            v.field("mcp", |v| mcp.validate(v));
991        }
992        if let Some(a2a) = &self.a2a {
993            v.field("a2a", |v| a2a.validate(v));
994        }
995    }
996}
997
998impl Validate for ProjectConfig {
999    fn validate(&self, v: &mut Validator) {
1000        v.check_non_empty("name", &self.name);
1001        v.check_semver("version", &self.version);
1002    }
1003}
1004
1005impl Validate for AiConfig {
1006    fn validate(&self, v: &mut Validator) {
1007        let providers = ["openai", "ollama", "anthropic", "cohere", "huggingface"];
1008        v.check_one_of("provider", &self.provider, &providers);
1009        v.check_range("temperature", self.temperature, 0.0..=1.0);
1010        v.check_predicate(
1011            "max_tokens",
1012            self.max_tokens > 0,
1013            "out_of_range",
1014            "AI max_tokens must be greater than 0",
1015        );
1016        v.check_predicate(
1017            "timeout",
1018            self.timeout > 0,
1019            "out_of_range",
1020            "AI timeout must be greater than 0",
1021        );
1022        if let Some(prompts) = &self.prompts {
1023            v.field("prompts", |v| prompts.validate(v));
1024        }
1025        if let Some(validation) = &self.validation {
1026            v.field("validation", |v| validation.validate(v));
1027        }
1028    }
1029}
1030
1031impl Validate for AiPrompts {
1032    fn validate(&self, _v: &mut Validator) {}
1033}
1034
1035impl Validate for AiValidation {
1036    fn validate(&self, v: &mut Validator) {
1037        v.check_range("quality_threshold", self.quality_threshold, 0.0..=1.0);
1038    }
1039}
1040
1041impl Validate for TemplatesConfig {
1042    fn validate(&self, v: &mut Validator) {
1043        if let Some(dir) = &self.directory {
1044            v.check_path("directory", dir, None);
1045        }
1046        if let Some(out_dir) = &self.output_directory {
1047            v.check_path("output_directory", out_dir, None);
1048        }
1049    }
1050}
1051
1052impl Validate for RdfConfig {
1053    fn validate(&self, _v: &mut Validator) {}
1054}
1055
1056impl Validate for SparqlConfig {
1057    fn validate(&self, _v: &mut Validator) {}
1058}
1059
1060impl Validate for LifecycleConfig {
1061    fn validate(&self, _v: &mut Validator) {}
1062}
1063
1064impl Validate for SecurityConfig {
1065    fn validate(&self, _v: &mut Validator) {}
1066}
1067
1068impl Validate for PerformanceConfig {
1069    fn validate(&self, v: &mut Validator) {
1070        v.check_consistent(
1071            "max_workers",
1072            &["parallel_execution"],
1073            !self.parallel_execution || self.max_workers > 0,
1074            "out_of_range",
1075            "Performance max_workers must be greater than 0 when parallel_execution is enabled",
1076        );
1077        if let Some(cache_size) = &self.cache_size {
1078            v.check_size_format("cache_size", cache_size);
1079        }
1080    }
1081}
1082
1083impl Validate for LoggingConfig {
1084    fn validate(&self, v: &mut Validator) {
1085        let level = self.level.to_lowercase();
1086        v.check_one_of(
1087            "level",
1088            &level,
1089            &["trace", "debug", "info", "warn", "error"],
1090        );
1091        let format = self.format.to_lowercase();
1092        v.check_one_of("format", &format, &["json", "text", "pretty"]);
1093        if let Some(file) = &self.file {
1094            v.check_path("file", file, None);
1095        }
1096    }
1097}
1098
1099impl Validate for TelemetryConfig {
1100    fn validate(&self, _v: &mut Validator) {}
1101}
1102
1103impl Validate for BuildConfig {
1104    fn validate(&self, _v: &mut Validator) {}
1105}
1106
1107impl Validate for TestConfig {
1108    fn validate(&self, _v: &mut Validator) {}
1109}
1110
1111impl Validate for PackageMetadata {
1112    fn validate(&self, _v: &mut Validator) {}
1113}
1114
1115impl Validate for McpConfig {
1116    fn validate(&self, v: &mut Validator) {
1117        v.check_predicate(
1118            "tool_timeout_ms",
1119            self.tool_timeout_ms > 0,
1120            "out_of_range",
1121            "MCP tool_timeout_ms must be greater than 0",
1122        );
1123        v.check_predicate(
1124            "max_concurrent_requests",
1125            self.max_concurrent_requests > 0,
1126            "out_of_range",
1127            "MCP max_concurrent_requests must be greater than 0",
1128        );
1129        if let Some(transport) = &self.transport {
1130            v.field("transport", |v| transport.validate(v));
1131        }
1132        if let Some(tools) = &self.tools {
1133            v.field("tools", |v| tools.validate(v));
1134        }
1135        if let Some(zai) = &self.zai {
1136            v.field("zai", |v| zai.validate(v));
1137        }
1138        if let Some(discovery) = &self.discovery {
1139            v.field("discovery", |v| discovery.validate(v));
1140        }
1141    }
1142}
1143
1144impl Validate for McpTransportConfig {
1145    fn validate(&self, v: &mut Validator) {
1146        let valid_transports = ["stdio", "http", "websocket"];
1147        v.check_one_of("transport_type", &self.transport_type, &valid_transports);
1148        if let Some(port) = self.port {
1149            v.check_range("port", port, 1..=65535);
1150        }
1151        if let Some(tls) = &self.tls {
1152            v.field("tls", |v| tls.validate(v));
1153        }
1154    }
1155}
1156
1157impl Validate for McpTlsConfig {
1158    fn validate(&self, v: &mut Validator) {
1159        if let Some(cert_path) = &self.cert_path {
1160            v.check_path("cert_path", cert_path, None);
1161        }
1162        if let Some(key_path) = &self.key_path {
1163            v.check_path("key_path", key_path, None);
1164        }
1165        if let Some(ca_path) = &self.ca_path {
1166            v.check_path("ca_path", ca_path, None);
1167        }
1168    }
1169}
1170
1171impl Validate for McpToolsConfig {
1172    fn validate(&self, v: &mut Validator) {
1173        if let Some(discovery_path) = &self.discovery_path {
1174            v.check_path("discovery_path", discovery_path, None);
1175        }
1176    }
1177}
1178
1179impl Validate for McpZaiConfig {
1180    fn validate(&self, _v: &mut Validator) {}
1181}
1182
1183impl Validate for McpDiscoveryConfig {
1184    fn validate(&self, _v: &mut Validator) {}
1185}
1186
1187impl Validate for A2AConfig {
1188    fn validate(&self, v: &mut Validator) {
1189        if let Some(transport) = &self.transport {
1190            v.field("transport", |v| transport.validate(v));
1191        }
1192        if let Some(messaging) = &self.messaging {
1193            v.field("messaging", |v| messaging.validate(v));
1194        }
1195        if let Some(orchestration) = &self.orchestration {
1196            v.field("orchestration", |v| orchestration.validate(v));
1197        }
1198    }
1199}
1200
1201impl Validate for A2ATransportConfig {
1202    fn validate(&self, v: &mut Validator) {
1203        let valid_transports = ["memory", "http", "websocket", "amqp"];
1204        v.check_one_of("transport_type", &self.transport_type, &valid_transports);
1205        if let Some(port) = self.port {
1206            v.check_range("port", port, 1..=65535);
1207        }
1208        if let Some(retry) = &self.retry {
1209            v.field("retry", |v| retry.validate(v));
1210        }
1211    }
1212}
1213
1214impl Validate for A2ARetryConfig {
1215    fn validate(&self, _v: &mut Validator) {}
1216}
1217
1218impl Validate for A2AMessagingConfig {
1219    fn validate(&self, v: &mut Validator) {
1220        if let Some(persistence_path) = &self.persistence_path {
1221            v.check_path("persistence_path", persistence_path, None);
1222        }
1223    }
1224}
1225
1226impl Validate for A2AOrchestrationConfig {
1227    fn validate(&self, v: &mut Validator) {
1228        let valid_modes = ["centralized", "decentralized", "hierarchical"];
1229        v.check_one_of("mode", &self.mode, &valid_modes);
1230        if self.consensus_enabled {
1231            if let Some(algo) = &self.consensus_algorithm {
1232                let valid_algos = ["raft", "pbft", "naive"];
1233                v.check_one_of("consensus_algorithm", algo, &valid_algos);
1234            } else {
1235                v.check_predicate(
1236                    "consensus_algorithm",
1237                    false,
1238                    "missing",
1239                    "A2A consensus algorithm must be specified when consensus is enabled",
1240                );
1241            }
1242        }
1243    }
1244}
1245
1246#[cfg(test)]
1247#[allow(clippy::unwrap_used, clippy::expect_used)]
1248mod tests {
1249    use super::*;
1250    use star_toml::Validate;
1251
1252    #[test]
1253    fn test_ggen_config_validate_trait() {
1254        let mut config = GgenConfig::default();
1255        config.project.name = String::new(); // Invalid: empty
1256        config.project.version = "1.0".to_string(); // Invalid: bad semver
1257
1258        let errs = config.check().unwrap_err();
1259        let error_msgs: Vec<String> = errs.errors().iter().map(|e| e.loc.to_string()).collect();
1260        assert!(error_msgs.contains(&"project.name".to_string()));
1261        assert!(error_msgs.contains(&"project.version".to_string()));
1262
1263        config.project.name = "my-project".to_string();
1264        config.project.version = "1.0.0".to_string();
1265        assert!(config.check().is_ok());
1266
1267        // Test AI config validation
1268        config.ai = Some(AiConfig {
1269            provider: "unknown".to_string(),
1270            model: "gpt-4".to_string(),
1271            temperature: 1.5, // Invalid: > 1.0
1272            max_tokens: 0,    // Invalid: must be > 0
1273            timeout: 0,       // Invalid: must be > 0
1274            prompts: None,
1275            validation: Some(AiValidation {
1276                enabled: true,
1277                quality_threshold: -0.5, // Invalid: < 0.0
1278                max_iterations: 3,
1279            }),
1280        });
1281
1282        let errs = config.check().unwrap_err();
1283        let error_locs: Vec<String> = errs.errors().iter().map(|e| e.loc.to_string()).collect();
1284        assert!(error_locs.contains(&"ai.provider".to_string()));
1285        assert!(error_locs.contains(&"ai.temperature".to_string()));
1286        assert!(error_locs.contains(&"ai.max_tokens".to_string()));
1287        assert!(error_locs.contains(&"ai.timeout".to_string()));
1288        assert!(error_locs.contains(&"ai.validation.quality_threshold".to_string()));
1289    }
1290
1291    #[test]
1292    fn test_ggen_config_optional_subconfigs_none() {
1293        let mut config = GgenConfig::default();
1294        config.project.name = "my-project".to_string();
1295        config.project.version = "1.0.0".to_string();
1296        // Set all optional fields to None
1297        config.ai = None;
1298        config.templates = None;
1299        config.rdf = None;
1300        config.sparql = None;
1301        config.lifecycle = None;
1302        config.security = None;
1303        config.performance = None;
1304        config.logging = None;
1305        config.telemetry = None;
1306        config.build = None;
1307        config.test = None;
1308        config.package = None;
1309        config.mcp = None;
1310        config.a2a = None;
1311
1312        assert!(config.check().is_ok());
1313    }
1314
1315    #[test]
1316    fn test_ggen_config_extreme_values() {
1317        let mut config = GgenConfig::default();
1318        config.project.name = "my-project".to_string();
1319        config.project.version = "1.0.0".to_string();
1320
1321        // 1. Performance Config max_workers extreme values
1322        config.performance = Some(PerformanceConfig {
1323            parallel_execution: true,
1324            max_workers: 0, // Invalid: must be > 0 when parallel_execution is true
1325            cache_size: Some("invalid_size".to_string()), // Invalid size format
1326            enable_profiling: false,
1327            memory_limit_mb: None,
1328        });
1329
1330        let errs = config.check().unwrap_err();
1331        let error_locs: Vec<String> = errs.errors().iter().map(|e| e.loc.to_string()).collect();
1332        assert!(error_locs.contains(&"performance.max_workers".to_string()));
1333        assert!(error_locs.contains(&"performance.cache_size".to_string()));
1334
1335        // 2. Logging Config invalid values
1336        config.performance = None;
1337        config.logging = Some(LoggingConfig {
1338            level: "SUPER_FATAL".to_string(), // Invalid level
1339            format: "xml".to_string(),        // Invalid format
1340            file: None,
1341            rotation: None,
1342        });
1343
1344        let errs = config.check().unwrap_err();
1345        let error_locs: Vec<String> = errs.errors().iter().map(|e| e.loc.to_string()).collect();
1346        assert!(error_locs.contains(&"logging.level".to_string()));
1347        assert!(error_locs.contains(&"logging.format".to_string()));
1348
1349        // 3. MCP Config validation
1350        config.logging = None;
1351        config.mcp = Some(McpConfig {
1352            name: Some("test-mcp".to_string()),
1353            version: Some("1.0.0".to_string()),
1354            enabled: true,
1355            tool_timeout_ms: 0,         // Invalid: must be > 0
1356            max_concurrent_requests: 0, // Invalid: must be > 0
1357            transport: Some(McpTransportConfig {
1358                transport_type: "ftp".to_string(), // Invalid transport
1359                port: Some(0),                     // Invalid port
1360                host: "localhost".to_string(),
1361                tls: None,
1362                request_timeout_seconds: 30,
1363            }),
1364            tools: None,
1365            zai: None,
1366            discovery: None,
1367        });
1368
1369        let errs = config.check().unwrap_err();
1370        let error_locs: Vec<String> = errs.errors().iter().map(|e| e.loc.to_string()).collect();
1371        assert!(error_locs.contains(&"mcp.tool_timeout_ms".to_string()));
1372        assert!(error_locs.contains(&"mcp.max_concurrent_requests".to_string()));
1373        assert!(error_locs.contains(&"mcp.transport.transport_type".to_string()));
1374        assert!(error_locs.contains(&"mcp.transport.port".to_string()));
1375
1376        // 4. A2A Config validation
1377        config.mcp = None;
1378        config.a2a = Some(A2AConfig {
1379            agent_id: Some("agent-1".to_string()),
1380            agent_name: Some("test-agent".to_string()),
1381            agent_type: Some("worker".to_string()),
1382            capabilities: None,
1383            enabled: true,
1384            transport: Some(A2ATransportConfig {
1385                transport_type: "gRPC".to_string(), // Invalid transport
1386                bind_address: Some("127.0.0.1".to_string()),
1387                port: Some(9999), // Invalid port (>65535) is handled by u16 type, let's use 0 to trigger port range validation
1388                timeout_ms: 5000,
1389                max_connections: Some(10),
1390                retry: None,
1391            }),
1392            messaging: None,
1393            orchestration: Some(A2AOrchestrationConfig {
1394                mode: "dynamic".to_string(), // Invalid mode
1395                coordinator_address: Some("127.0.0.1".to_string()),
1396                heartbeat_interval_seconds: 5,
1397                agent_timeout_seconds: 30,
1398                consensus_enabled: true,
1399                consensus_algorithm: Some("paxos".to_string()), // Invalid consensus algo
1400            }),
1401        });
1402
1403        // Actually A2ATransportConfig's port is Option<u16>, so 99999 won't fit, let's use Some(0) to trigger range violation.
1404        config
1405            .a2a
1406            .as_mut()
1407            .unwrap()
1408            .transport
1409            .as_mut()
1410            .unwrap()
1411            .port = Some(0);
1412
1413        let errs = config.check().unwrap_err();
1414        let error_locs: Vec<String> = errs.errors().iter().map(|e| e.loc.to_string()).collect();
1415        assert!(error_locs.contains(&"a2a.transport.transport_type".to_string()));
1416        assert!(error_locs.contains(&"a2a.transport.port".to_string()));
1417        assert!(error_locs.contains(&"a2a.orchestration.mode".to_string()));
1418        assert!(error_locs.contains(&"a2a.orchestration.consensus_algorithm".to_string()));
1419    }
1420
1421    #[test]
1422    fn test_ggen_config_path_validation_gaps() {
1423        let mut config = GgenConfig::default();
1424        config.project.name = "my-project".to_string();
1425        config.project.version = "1.0.0".to_string();
1426
1427        // Setup various traversal path strings and null bytes
1428        let malicious_path = "path/../../to/malicious/file".to_string();
1429        let backslash_path = "path\\..\\to\\malicious\\file".to_string();
1430        let null_byte_path = "path/with\0null/byte".to_string();
1431
1432        // 1. Templates Config
1433        config.templates = Some(TemplatesConfig {
1434            directory: Some(malicious_path.clone()),
1435            output_directory: Some(backslash_path.clone()),
1436            backup_enabled: false,
1437            idempotent: false,
1438        });
1439
1440        let errs = config.check().unwrap_err();
1441        let locs: Vec<String> = errs.errors().iter().map(|e| e.loc.to_string()).collect();
1442        assert!(locs.contains(&"templates.directory".to_string()));
1443        assert!(locs.contains(&"templates.output_directory".to_string()));
1444
1445        // 2. Logging Config
1446        config.templates = None;
1447        config.logging = Some(LoggingConfig {
1448            level: "info".to_string(),
1449            format: "json".to_string(),
1450            file: Some(null_byte_path.clone()),
1451            rotation: None,
1452        });
1453
1454        let errs = config.check().unwrap_err();
1455        assert!(errs
1456            .errors()
1457            .iter()
1458            .any(|e| e.loc.to_string() == "logging.file"));
1459
1460        // 3. MCP Config
1461        config.logging = None;
1462        config.mcp = Some(McpConfig {
1463            name: Some("test-mcp".to_string()),
1464            version: Some("1.0.0".to_string()),
1465            enabled: true,
1466            tool_timeout_ms: 1000,
1467            max_concurrent_requests: 10,
1468            transport: Some(McpTransportConfig {
1469                transport_type: "stdio".to_string(),
1470                port: None,
1471                host: "localhost".to_string(),
1472                tls: Some(McpTlsConfig {
1473                    enabled: false,
1474                    cert_path: Some(malicious_path.clone()),
1475                    key_path: Some(backslash_path.clone()),
1476                    ca_path: Some(null_byte_path.clone()),
1477                }),
1478                request_timeout_seconds: 30,
1479            }),
1480            tools: Some(McpToolsConfig {
1481                discovery_path: Some(malicious_path.clone()),
1482                require_registration: false,
1483                validate_signatures: false,
1484                allowed_prefixes: None,
1485            }),
1486            zai: None,
1487            discovery: None,
1488        });
1489
1490        let errs = config.check().unwrap_err();
1491        let locs: Vec<String> = errs.errors().iter().map(|e| e.loc.to_string()).collect();
1492        assert!(locs.contains(&"mcp.transport.tls.cert_path".to_string()));
1493        assert!(locs.contains(&"mcp.transport.tls.key_path".to_string()));
1494        assert!(locs.contains(&"mcp.transport.tls.ca_path".to_string()));
1495        assert!(locs.contains(&"mcp.tools.discovery_path".to_string()));
1496
1497        // 4. A2A Config
1498        config.mcp = None;
1499        config.a2a = Some(A2AConfig {
1500            agent_id: Some("agent-1".to_string()),
1501            agent_name: Some("test-agent".to_string()),
1502            agent_type: Some("worker".to_string()),
1503            capabilities: None,
1504            enabled: true,
1505            transport: None,
1506            messaging: Some(A2AMessagingConfig {
1507                queue_size: 100,
1508                message_ttl_seconds: 60,
1509                persistence_enabled: true,
1510                persistence_path: Some(malicious_path.clone()),
1511                signing_enabled: false,
1512                signature_algorithm: None,
1513            }),
1514            orchestration: None,
1515        });
1516
1517        let errs = config.check().unwrap_err();
1518        assert!(errs
1519            .errors()
1520            .iter()
1521            .any(|e| e.loc.to_string() == "a2a.messaging.persistence_path"));
1522    }
1523}