Jesús Pérez 93b0e5225c
feat(platform): control plane — NATS JetStream + SurrealDB + SOLID enforcement
New crates
  - platform-nats: async_nats JetStream bridge; pull/push consumers, explicit ACK,
    subject prefixing under provisioning.>, 6 stream definitions on startup
  - platform-db: SurrealDB pool (embedded RocksDB solo, Surreal<Mem> tests,
    WebSocket server multi-user); migrate() with DEFINE TABLE IF NOT EXISTS DDL

  Service integrations
  - orchestrator: NATS pub on task state transitions, execution_logs → SurrealDB,
    webhook handler (HMAC-SHA256), AuditCollector (batch INSERT, 100-event/1s flush)
  - control-center: solo_auth_middleware (intentional bypass, --mode solo only),
    NATS session events, WebSocket bridge via JetStream subscription (no polling)
  - vault-service: NATS lease flow; credentials over HTTPS only (lease_id in NATS);
    SurrealDB storage backend with MVCC retry + exponential backoff
  - secretumvault: complete SurrealDB backend replacing HashMap; 9 unit + 19 integration tests
  - extension-registry: NATS lifecycle events, vault:// credential resolver with TTL cache,
    cache invalidation via provisioning.workspace.*.deploy.done

  Clippy workspace clean
  cargo clippy --workspace -- -D warnings: 0 errors
  Patterns fixed: derivable_impls (#[default] on enum variants), excessive_nesting
  (let-else, boolean arithmetic in retain, extracted helpers), io_error_other,
  redundant_closure, iter_kv_map, manual_range_contains, pathbuf_instead_of_path
2026-02-17 23:58:14 +00:00

150 lines
4.0 KiB
Rust

use std::collections::HashMap;
use std::path::Path;
use anyhow::Context;
use platform_config::ConfigLoader;
use serde::{Deserialize, Serialize};
/// AI Service configuration
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AiServiceConfig {
pub ai_service: AiServiceSettings,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AiServiceSettings {
pub server: ServerConfig,
pub rag: RagConfig,
pub mcp: McpConfig,
pub dag: DagConfig,
#[serde(default)]
pub monitoring: Option<MonitoringConfig>,
#[serde(default)]
pub logging: Option<LoggingConfig>,
#[serde(default)]
pub build: Option<DockerBuildConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
#[serde(default)]
pub workers: Option<usize>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8082,
workers: Some(4),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RagConfig {
pub enabled: bool,
pub rag_service_url: Option<String>,
pub timeout: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct McpConfig {
pub enabled: bool,
pub mcp_service_url: Option<String>,
pub timeout: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DagConfig {
pub max_concurrent_tasks: Option<usize>,
pub task_timeout: Option<u64>,
pub retry_attempts: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoringConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerBuildConfig {
pub base_image: String,
#[serde(default)]
pub build_args: HashMap<String, String>,
}
impl AiServiceConfig {
pub fn load() -> anyhow::Result<Self> {
let config_json = platform_config::load_service_config_from_ncl("ai-service")
.context("Failed to load ai-service configuration from Nickel")?;
serde_json::from_value(config_json)
.context("Failed to deserialize ai-service configuration")
}
}
impl ConfigLoader for AiServiceConfig {
fn service_name() -> &'static str {
"ai-service"
}
fn load_from_hierarchy() -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>>
{
if let Some(path) = platform_config::resolve_config_path(Self::service_name()) {
return Self::from_path(&path);
}
Ok(Self::default())
}
fn apply_env_overrides(
&mut self,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(host) = std::env::var("AI_SERVICE_SERVER_HOST") {
self.ai_service.server.host = host;
}
if let Ok(port) = std::env::var("AI_SERVICE_SERVER_PORT") {
if let Ok(p) = port.parse() {
self.ai_service.server.port = p;
}
}
Ok(())
}
fn from_path<P: AsRef<Path>>(
path: P,
) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let path = path.as_ref();
let json_value = platform_config::format::load_config(path)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
serde_json::from_value(json_value).map_err(|e| {
let err_msg = format!("Failed to deserialize ai-service config: {}", e);
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
err_msg,
)) as Box<dyn std::error::Error + Send + Sync>
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = AiServiceConfig::default();
assert_eq!(config.ai_service.server.port, 8082);
assert!(!config.ai_service.rag.enabled);
assert!(!config.ai_service.mcp.enabled);
}
}