Skip to main content

clawft_kernel/
service.rs

1//! System service registry and lifecycle management.
2//!
3//! The [`ServiceRegistry`] manages named services that implement the
4//! [`SystemService`] trait, providing start/stop lifecycle and health
5//! check aggregation.
6
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use dashmap::DashMap;
11use serde::{Deserialize, Serialize};
12use tracing::{info, warn};
13
14use chrono::{DateTime, Utc};
15
16use crate::health::HealthStatus;
17use crate::process::Pid;
18
19/// Type of system service.
20#[non_exhaustive]
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub enum ServiceType {
23    /// Core kernel service (message bus, process table, etc.).
24    Core,
25    /// Plugin-provided service.
26    Plugin,
27    /// Cron/scheduler service.
28    Cron,
29    /// API/HTTP service.
30    Api,
31    /// Custom service with a user-defined label.
32    Custom(String),
33}
34
35impl std::fmt::Display for ServiceType {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            ServiceType::Core => write!(f, "core"),
39            ServiceType::Plugin => write!(f, "plugin"),
40            ServiceType::Cron => write!(f, "cron"),
41            ServiceType::Api => write!(f, "api"),
42            ServiceType::Custom(s) => write!(f, "custom({s})"),
43        }
44    }
45}
46
47// ── Service identity model (D1, D9, D19) ───────────────────────
48
49/// How to reach a service at runtime.
50///
51/// A service can be backed by an in-kernel agent (most common),
52/// an external system (Redis, HTTP endpoint), or a container.
53#[non_exhaustive]
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub enum ServiceEndpoint {
56    /// Backed by an in-kernel agent (route to its inbox).
57    AgentInbox(Pid),
58    /// Backed by an external system (K3/K4 ServiceApi adapter required).
59    External {
60        /// URL or connection string for the external system.
61        url: String,
62    },
63    /// Backed by a managed container (K4).
64    Container {
65        /// Container identifier.
66        id: String,
67    },
68}
69
70/// Audit level for service call witnessing (D9).
71///
72/// Controls how much of a service's activity is recorded in the
73/// ExoChain. The default is `Full` -- every call is witnessed.
74#[non_exhaustive]
75#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
76pub enum ServiceAuditLevel {
77    /// Witness every call (default, per D9).
78    #[default]
79    Full,
80    /// Only log governance gate decisions (opt-out for high-frequency services).
81    GateOnly,
82}
83
84/// First-class service identity in the registry (D1).
85///
86/// A `ServiceEntry` is metadata about a service -- who owns it, how
87/// to reach it, and how deeply to audit it. It lives alongside (not
88/// replacing) the `SystemService` trait implementations.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ServiceEntry {
91    /// Service name (unique within the registry).
92    pub name: String,
93    /// PID of the agent that owns this service (`None` for external services).
94    pub owner_pid: Option<Pid>,
95    /// How to reach the service at runtime.
96    pub endpoint: ServiceEndpoint,
97    /// Audit depth for ExoChain witnessing.
98    pub audit_level: ServiceAuditLevel,
99    /// When this entry was registered.
100    pub registered_at: DateTime<Utc>,
101}
102
103/// A system service managed by the kernel.
104///
105/// Services are started during boot and stopped during shutdown.
106/// Each service provides a health check for monitoring.
107#[async_trait]
108pub trait SystemService: Send + Sync {
109    /// Human-readable service name.
110    fn name(&self) -> &str;
111
112    /// Service type category.
113    fn service_type(&self) -> ServiceType;
114
115    /// Start the service.
116    async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
117
118    /// Stop the service.
119    async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
120
121    /// Perform a health check.
122    async fn health_check(&self) -> HealthStatus;
123
124    /// Liveness probe (K2b-G2, os-patterns).
125    ///
126    /// Returns whether the service process is alive. Default implementation
127    /// returns `Live` for backward compatibility.
128    #[cfg(feature = "os-patterns")]
129    async fn liveness_check(&self) -> crate::health::ProbeResult {
130        crate::health::ProbeResult::Live
131    }
132
133    /// Readiness probe (K2b-G2, os-patterns).
134    ///
135    /// Returns whether the service is ready to accept traffic. Default
136    /// implementation returns `Ready` for backward compatibility.
137    #[cfg(feature = "os-patterns")]
138    async fn readiness_check(&self) -> crate::health::ProbeResult {
139        crate::health::ProbeResult::Ready
140    }
141}
142
143/// Registry of system services with lifecycle management.
144///
145/// Uses [`DashMap`] for concurrent access from multiple kernel
146/// subsystems. Maintains two maps:
147///
148/// - `services`: `SystemService` trait object implementations (existing)
149/// - `entries`: `ServiceEntry` metadata for service identity (D1, K2.1)
150///
151/// A service can have metadata before it has a running implementation
152/// (useful for external services), and vice versa.
153pub struct ServiceRegistry {
154    services: DashMap<String, Arc<dyn SystemService>>,
155    entries: DashMap<String, ServiceEntry>,
156}
157
158impl ServiceRegistry {
159    /// Create a new, empty service registry.
160    pub fn new() -> Self {
161        Self {
162            services: DashMap::new(),
163            entries: DashMap::new(),
164        }
165    }
166
167    /// Register a service.
168    ///
169    /// Returns an error if a service with the same name is already
170    /// registered.
171    pub fn register(
172        &self,
173        service: Arc<dyn SystemService>,
174    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
175        let name = service.name().to_owned();
176        if self.services.contains_key(&name) {
177            return Err(format!("service already registered: {name}").into());
178        }
179        info!(service = %name, "registering service");
180        self.services.insert(name, service);
181        Ok(())
182    }
183
184    /// Unregister a service by name.
185    pub fn unregister(&self, name: &str) -> Option<Arc<dyn SystemService>> {
186        self.services.remove(name).map(|(_, s)| s)
187    }
188
189    /// Get a service by name.
190    pub fn get(&self, name: &str) -> Option<Arc<dyn SystemService>> {
191        self.services.get(name).map(|s| s.value().clone())
192    }
193
194    /// List all registered services with their types.
195    pub fn list(&self) -> Vec<(String, ServiceType)> {
196        self.services
197            .iter()
198            .map(|entry| (entry.key().clone(), entry.value().service_type()))
199            .collect()
200    }
201
202    /// Start all registered services.
203    ///
204    /// Individual service failures are logged as warnings but do not
205    /// prevent other services from starting.
206    pub async fn start_all(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
207        for entry in self.services.iter() {
208            let name = entry.key().clone();
209            info!(service = %name, "starting service");
210            if let Err(e) = entry.value().start().await {
211                warn!(service = %name, error = %e, "service failed to start");
212            }
213        }
214        Ok(())
215    }
216
217    /// Stop all registered services.
218    ///
219    /// Individual service failures are logged as warnings.
220    pub async fn stop_all(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
221        for entry in self.services.iter() {
222            let name = entry.key().clone();
223            info!(service = %name, "stopping service");
224            if let Err(e) = entry.value().stop().await {
225                warn!(service = %name, error = %e, "service failed to stop");
226            }
227        }
228        Ok(())
229    }
230
231    /// Return a snapshot of all services as a `Vec`.
232    ///
233    /// This copies all `(name, Arc<dyn SystemService>)` pairs out of
234    /// the `DashMap`, so the returned collection owns no DashMap refs
235    /// and is safe to hold across await points and send across threads.
236    pub fn snapshot(&self) -> Vec<(String, Arc<dyn SystemService>)> {
237        self.services
238            .iter()
239            .map(|entry| (entry.key().clone(), entry.value().clone()))
240            .collect()
241    }
242
243    /// Run health checks on all registered services.
244    pub async fn health_all(&self) -> Vec<(String, HealthStatus)> {
245        let mut results = Vec::new();
246        for entry in self.services.iter() {
247            let name = entry.key().clone();
248            let status = entry.value().health_check().await;
249            results.push((name, status));
250        }
251        results
252    }
253
254    // ── ServiceEntry metadata (D1, K2.1) ──────────────────────────
255
256    /// Register a service entry (metadata, not a running implementation).
257    ///
258    /// Returns an error if an entry with the same name already exists.
259    pub fn register_entry(
260        &self,
261        entry: ServiceEntry,
262    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
263        let name = entry.name.clone();
264        if self.entries.contains_key(&name) {
265            return Err(format!("service entry already registered: {name}").into());
266        }
267        info!(service = %name, "registering service entry");
268        self.entries.insert(name, entry);
269        Ok(())
270    }
271
272    /// Get a service entry by name.
273    pub fn get_entry(&self, name: &str) -> Option<ServiceEntry> {
274        self.entries.get(name).map(|e| e.value().clone())
275    }
276
277    /// Resolve a service name to its owning agent PID.
278    ///
279    /// Returns `None` if the service is not registered or has no
280    /// `owner_pid` (e.g. external services).
281    pub fn resolve_target(&self, name: &str) -> Option<Pid> {
282        self.entries.get(name).and_then(|e| e.value().owner_pid)
283    }
284
285    /// List all registered service entries.
286    pub fn list_entries(&self) -> Vec<ServiceEntry> {
287        self.entries.iter().map(|e| e.value().clone()).collect()
288    }
289
290    /// Remove a service entry by name.
291    pub fn unregister_entry(&self, name: &str) -> Option<ServiceEntry> {
292        self.entries.remove(name).map(|(_, e)| e)
293    }
294
295    /// Register a service and create a resource tree node + chain event.
296    ///
297    /// When the exochain feature is enabled and a tree manager is provided,
298    /// creates a node at `/kernel/services/{name}` in the resource tree
299    /// and appends a corresponding chain event via `TreeManager`.
300    #[cfg(feature = "exochain")]
301    pub fn register_with_tree(
302        &self,
303        service: Arc<dyn SystemService>,
304        tree_manager: &crate::tree_manager::TreeManager,
305    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
306        let name = service.name().to_owned();
307        self.register(service)?;
308
309        // Create tree node + chain event through the unified TreeManager path
310        if let Err(e) = tree_manager.register_service(&name) {
311            tracing::debug!(service = %name, error = %e, "failed to register service in tree");
312        }
313
314        Ok(())
315    }
316
317    /// Get the number of registered services.
318    pub fn len(&self) -> usize {
319        self.services.len()
320    }
321
322    /// Check whether the registry is empty.
323    pub fn is_empty(&self) -> bool {
324        self.services.is_empty()
325    }
326
327    /// Register a service and automatically create a chain-anchored contract (C3).
328    ///
329    /// This is the recommended registration path for K4+ services that want
330    /// immutable API contracts stored on the ExoChain. It combines service
331    /// registration, contract creation, and chain logging in one call.
332    #[cfg(feature = "exochain")]
333    pub fn register_with_contract(
334        &self,
335        service: Arc<dyn SystemService>,
336        methods: Vec<String>,
337        chain: &crate::chain::ChainManager,
338    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
339        let name = service.name().to_owned();
340        let stype = service.service_type().to_string();
341
342        // Register the service first
343        self.register(service)?;
344
345        // Build canonical contract content and hash it
346        let contract_content = serde_json::json!({
347            "service": &name,
348            "type": &stype,
349            "methods": &methods,
350        });
351        let content_hash = {
352            use sha2::{Digest, Sha256};
353            let bytes = serde_json::to_string(&contract_content)
354                .unwrap_or_default();
355            format!("{:x}", Sha256::digest(bytes.as_bytes()))
356        };
357
358        let contract = ServiceContract {
359            service_name: name,
360            version: "1.0.0".into(),
361            methods,
362            content_hash,
363        };
364
365        self.register_contract(&contract, chain)?;
366
367        Ok(())
368    }
369
370    /// Register a service contract and log it to the chain (K2 C3, K4 G2).
371    ///
372    /// A service contract is a versioned interface declaration that is
373    /// anchored in the ExoChain for immutability. Once a contract version
374    /// is registered, it cannot be changed — only superseded by a new version.
375    #[cfg(feature = "exochain")]
376    pub fn register_contract(
377        &self,
378        contract: &ServiceContract,
379        chain: &crate::chain::ChainManager,
380    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
381        info!(
382            service = %contract.service_name,
383            version = %contract.version,
384            "registering service contract on chain"
385        );
386        chain.append(
387            &contract.service_name,
388            "service.contract.register",
389            Some(serde_json::json!({
390                "service": contract.service_name,
391                "version": contract.version,
392                "methods": contract.methods,
393                "hash": contract.content_hash,
394            })),
395        );
396        Ok(())
397    }
398}
399
400// ---------------------------------------------------------------------------
401// K4 G2: Service contracts (K2 C3)
402// ---------------------------------------------------------------------------
403
404/// A versioned service contract anchored in the ExoChain.
405///
406/// Contracts declare the interface a service exposes. Once registered
407/// on-chain, a contract version is immutable.
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ServiceContract {
410    /// Name of the service this contract belongs to.
411    pub service_name: String,
412    /// Semantic version string (e.g. "1.0.0").
413    pub version: String,
414    /// Method names exposed by this contract version.
415    pub methods: Vec<String>,
416    /// SHAKE-256 hash of the canonical contract content.
417    pub content_hash: String,
418}
419
420impl Default for ServiceRegistry {
421    fn default() -> Self {
422        Self::new()
423    }
424}
425
426// ── ServiceApi trait (K3 C2) ─────────────────────────────────────
427
428/// Internal API surface for protocol adapters (Shell, MCP, HTTP).
429///
430/// Protocol adapters bind to this trait to invoke kernel services
431/// through a uniform interface. The kernel provides a concrete
432/// implementation backed by the ServiceRegistry + A2ARouter.
433#[async_trait]
434pub trait ServiceApi: Send + Sync {
435    /// Call a method on a named service.
436    async fn call(
437        &self,
438        service: &str,
439        method: &str,
440        params: serde_json::Value,
441    ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>>;
442
443    /// List available services.
444    async fn list_services(&self) -> Vec<ServiceInfo>;
445
446    /// Get service health.
447    async fn health(
448        &self,
449        service: &str,
450    ) -> Result<HealthStatus, Box<dyn std::error::Error + Send + Sync>>;
451}
452
453/// Service info returned by [`ServiceApi::list_services`].
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct ServiceInfo {
456    /// Service name.
457    pub name: String,
458    /// Service type label (e.g. "core", "plugin").
459    pub service_type: String,
460    /// Whether the service is currently healthy.
461    pub healthy: bool,
462}
463
464/// Shell protocol adapter -- dispatches shell commands through [`ServiceApi`].
465pub struct ShellAdapter {
466    api: Arc<dyn ServiceApi>,
467}
468
469impl ShellAdapter {
470    /// Create a new shell adapter bound to the given service API.
471    pub fn new(api: Arc<dyn ServiceApi>) -> Self {
472        Self { api }
473    }
474
475    /// Execute a shell-style command string through the service API.
476    ///
477    /// Parses `"service.method arg1 arg2"` format into a
478    /// [`ServiceApi::call`].
479    pub async fn execute(
480        &self,
481        command: &str,
482    ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
483        let parts: Vec<&str> = command.splitn(2, ' ').collect();
484        let (service_method, args_str) = match parts.as_slice() {
485            [sm] => (*sm, ""),
486            [sm, args] => (*sm, *args),
487            _ => return Err("empty command".into()),
488        };
489
490        let (service, method) = service_method
491            .split_once('.')
492            .ok_or_else(|| format!("expected 'service.method', got '{service_method}'"))?;
493
494        let params = if args_str.is_empty() {
495            serde_json::Value::Null
496        } else if args_str.starts_with('{') || args_str.starts_with('[') {
497            serde_json::from_str(args_str)?
498        } else {
499            serde_json::json!({"args": args_str})
500        };
501
502        self.api.call(service, method, params).await
503    }
504}
505
506/// MCP protocol adapter -- dispatches MCP tool calls through [`ServiceApi`].
507pub struct McpAdapter {
508    api: Arc<dyn ServiceApi>,
509}
510
511impl McpAdapter {
512    /// Create a new MCP adapter bound to the given service API.
513    pub fn new(api: Arc<dyn ServiceApi>) -> Self {
514        Self { api }
515    }
516
517    /// Handle an MCP `tool_call` by routing through the service API.
518    ///
519    /// MCP tool names map to `service.method` via either underscore or
520    /// dot separator (e.g. `"kernel_status"` -> `("kernel", "status")`).
521    pub async fn handle_tool_call(
522        &self,
523        tool_name: &str,
524        arguments: serde_json::Value,
525    ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
526        let (service, method) = tool_name
527            .split_once('_')
528            .or_else(|| tool_name.split_once('.'))
529            .ok_or_else(|| format!("invalid tool name format: {tool_name}"))?;
530
531        self.api.call(service, method, arguments).await
532    }
533
534    /// List available tools (mapped from services).
535    pub async fn list_tools(&self) -> Vec<serde_json::Value> {
536        let services = self.api.list_services().await;
537        services
538            .into_iter()
539            .map(|s| {
540                serde_json::json!({
541                    "name": s.name,
542                    "description": format!("WeftOS {} service", s.service_type),
543                })
544            })
545            .collect()
546    }
547}
548
549// ── Registry trait implementation ────────────────────────────────────
550
551impl clawft_types::Registry for ServiceRegistry {
552    type Value = Arc<dyn SystemService>;
553
554    fn get(&self, key: &str) -> Option<Self::Value> {
555        self.services.get(key).map(|s| s.value().clone())
556    }
557
558    fn list_keys(&self) -> Vec<String> {
559        self.services.iter().map(|e| e.key().clone()).collect()
560    }
561
562    fn contains(&self, key: &str) -> bool {
563        self.services.contains_key(key)
564    }
565
566    fn count(&self) -> usize {
567        self.services.len()
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    /// A mock service for testing.
576    struct MockService {
577        name: String,
578        service_type: ServiceType,
579    }
580
581    impl MockService {
582        fn new(name: &str, stype: ServiceType) -> Self {
583            Self {
584                name: name.to_owned(),
585                service_type: stype,
586            }
587        }
588    }
589
590    #[async_trait]
591    impl SystemService for MockService {
592        fn name(&self) -> &str {
593            &self.name
594        }
595
596        fn service_type(&self) -> ServiceType {
597            self.service_type.clone()
598        }
599
600        async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
601            Ok(())
602        }
603
604        async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
605            Ok(())
606        }
607
608        async fn health_check(&self) -> HealthStatus {
609            HealthStatus::Healthy
610        }
611    }
612
613    #[test]
614    fn register_and_get() {
615        let registry = ServiceRegistry::new();
616        let svc = Arc::new(MockService::new("test-svc", ServiceType::Core));
617        registry.register(svc).unwrap();
618
619        let retrieved = registry.get("test-svc");
620        assert!(retrieved.is_some());
621        assert_eq!(retrieved.unwrap().name(), "test-svc");
622    }
623
624    #[test]
625    fn register_duplicate_fails() {
626        let registry = ServiceRegistry::new();
627        let svc1 = Arc::new(MockService::new("dup-svc", ServiceType::Core));
628        let svc2 = Arc::new(MockService::new("dup-svc", ServiceType::Plugin));
629
630        registry.register(svc1).unwrap();
631        let result = registry.register(svc2);
632        assert!(result.is_err());
633    }
634
635    #[test]
636    fn unregister() {
637        let registry = ServiceRegistry::new();
638        let svc = Arc::new(MockService::new("rm-svc", ServiceType::Core));
639        registry.register(svc).unwrap();
640
641        let removed = registry.unregister("rm-svc");
642        assert!(removed.is_some());
643        assert!(registry.get("rm-svc").is_none());
644    }
645
646    #[test]
647    fn list_services() {
648        let registry = ServiceRegistry::new();
649        registry
650            .register(Arc::new(MockService::new("svc-a", ServiceType::Core)))
651            .unwrap();
652        registry
653            .register(Arc::new(MockService::new("svc-b", ServiceType::Cron)))
654            .unwrap();
655
656        let list = registry.list();
657        assert_eq!(list.len(), 2);
658    }
659
660    #[tokio::test]
661    async fn start_and_stop_all() {
662        let registry = ServiceRegistry::new();
663        registry
664            .register(Arc::new(MockService::new("svc-1", ServiceType::Core)))
665            .unwrap();
666        registry
667            .register(Arc::new(MockService::new("svc-2", ServiceType::Plugin)))
668            .unwrap();
669
670        registry.start_all().await.unwrap();
671        registry.stop_all().await.unwrap();
672    }
673
674    #[tokio::test]
675    async fn health_all() {
676        let registry = ServiceRegistry::new();
677        registry
678            .register(Arc::new(MockService::new("svc-1", ServiceType::Core)))
679            .unwrap();
680        registry
681            .register(Arc::new(MockService::new("svc-2", ServiceType::Plugin)))
682            .unwrap();
683
684        let health = registry.health_all().await;
685        assert_eq!(health.len(), 2);
686        for (_, status) in &health {
687            assert_eq!(*status, HealthStatus::Healthy);
688        }
689    }
690
691    #[test]
692    fn len_and_is_empty() {
693        let registry = ServiceRegistry::new();
694        assert!(registry.is_empty());
695        assert_eq!(registry.len(), 0);
696
697        registry
698            .register(Arc::new(MockService::new("svc", ServiceType::Core)))
699            .unwrap();
700        assert!(!registry.is_empty());
701        assert_eq!(registry.len(), 1);
702    }
703
704    #[test]
705    fn service_type_display() {
706        assert_eq!(ServiceType::Core.to_string(), "core");
707        assert_eq!(ServiceType::Plugin.to_string(), "plugin");
708        assert_eq!(ServiceType::Cron.to_string(), "cron");
709        assert_eq!(ServiceType::Api.to_string(), "api");
710        assert_eq!(
711            ServiceType::Custom("webhook".into()).to_string(),
712            "custom(webhook)"
713        );
714    }
715
716    // ── ServiceEntry tests (K2.1 T3: D1) ───────────────────────
717
718    #[test]
719    fn register_and_get_entry() {
720        let registry = ServiceRegistry::new();
721        let entry = ServiceEntry {
722            name: "auth".into(),
723            owner_pid: Some(42),
724            endpoint: ServiceEndpoint::AgentInbox(42),
725            audit_level: ServiceAuditLevel::Full,
726            registered_at: Utc::now(),
727        };
728        registry.register_entry(entry).unwrap();
729
730        let retrieved = registry.get_entry("auth").unwrap();
731        assert_eq!(retrieved.name, "auth");
732        assert_eq!(retrieved.owner_pid, Some(42));
733    }
734
735    #[test]
736    fn register_entry_duplicate_fails() {
737        let registry = ServiceRegistry::new();
738        let entry1 = ServiceEntry {
739            name: "dup".into(),
740            owner_pid: Some(1),
741            endpoint: ServiceEndpoint::AgentInbox(1),
742            audit_level: ServiceAuditLevel::Full,
743            registered_at: Utc::now(),
744        };
745        let entry2 = ServiceEntry {
746            name: "dup".into(),
747            owner_pid: Some(2),
748            endpoint: ServiceEndpoint::AgentInbox(2),
749            audit_level: ServiceAuditLevel::Full,
750            registered_at: Utc::now(),
751        };
752        registry.register_entry(entry1).unwrap();
753        let result = registry.register_entry(entry2);
754        assert!(result.is_err());
755    }
756
757    #[test]
758    fn resolve_target_returns_owner_pid() {
759        let registry = ServiceRegistry::new();
760        let entry = ServiceEntry {
761            name: "cache".into(),
762            owner_pid: Some(99),
763            endpoint: ServiceEndpoint::AgentInbox(99),
764            audit_level: ServiceAuditLevel::Full,
765            registered_at: Utc::now(),
766        };
767        registry.register_entry(entry).unwrap();
768
769        assert_eq!(registry.resolve_target("cache"), Some(99));
770        assert_eq!(registry.resolve_target("missing"), None);
771    }
772
773    #[test]
774    fn resolve_target_external_returns_none() {
775        let registry = ServiceRegistry::new();
776        let entry = ServiceEntry {
777            name: "redis".into(),
778            owner_pid: None,
779            endpoint: ServiceEndpoint::External {
780                url: "redis://localhost:6379".into(),
781            },
782            audit_level: ServiceAuditLevel::GateOnly,
783            registered_at: Utc::now(),
784        };
785        registry.register_entry(entry).unwrap();
786
787        assert_eq!(registry.resolve_target("redis"), None);
788    }
789
790    #[test]
791    fn unregister_entry() {
792        let registry = ServiceRegistry::new();
793        let entry = ServiceEntry {
794            name: "temp".into(),
795            owner_pid: Some(5),
796            endpoint: ServiceEndpoint::AgentInbox(5),
797            audit_level: ServiceAuditLevel::Full,
798            registered_at: Utc::now(),
799        };
800        registry.register_entry(entry).unwrap();
801
802        let removed = registry.unregister_entry("temp");
803        assert!(removed.is_some());
804        assert!(registry.get_entry("temp").is_none());
805    }
806
807    #[test]
808    fn list_entries() {
809        let registry = ServiceRegistry::new();
810        registry
811            .register_entry(ServiceEntry {
812                name: "svc-a".into(),
813                owner_pid: Some(1),
814                endpoint: ServiceEndpoint::AgentInbox(1),
815                audit_level: ServiceAuditLevel::Full,
816                registered_at: Utc::now(),
817            })
818            .unwrap();
819        registry
820            .register_entry(ServiceEntry {
821                name: "svc-b".into(),
822                owner_pid: None,
823                endpoint: ServiceEndpoint::Container { id: "c1".into() },
824                audit_level: ServiceAuditLevel::GateOnly,
825                registered_at: Utc::now(),
826            })
827            .unwrap();
828
829        let list = registry.list_entries();
830        assert_eq!(list.len(), 2);
831    }
832
833    #[test]
834    fn service_entry_serde_roundtrip() {
835        let entry = ServiceEntry {
836            name: "auth".into(),
837            owner_pid: Some(42),
838            endpoint: ServiceEndpoint::AgentInbox(42),
839            audit_level: ServiceAuditLevel::Full,
840            registered_at: Utc::now(),
841        };
842        let json = serde_json::to_string(&entry).unwrap();
843        let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
844        assert_eq!(restored.name, "auth");
845        assert_eq!(restored.owner_pid, Some(42));
846        assert_eq!(restored.audit_level, ServiceAuditLevel::Full);
847    }
848
849    #[test]
850    fn service_endpoint_variants_serde() {
851        let endpoints = vec![
852            ServiceEndpoint::AgentInbox(1),
853            ServiceEndpoint::External {
854                url: "https://api.example.com".into(),
855            },
856            ServiceEndpoint::Container {
857                id: "container-abc".into(),
858            },
859        ];
860        for ep in endpoints {
861            let json = serde_json::to_string(&ep).unwrap();
862            let _: ServiceEndpoint = serde_json::from_str(&json).unwrap();
863        }
864    }
865
866    // ── ServiceApi tests (K3 C2) ──────────────────────
867
868    struct MockServiceApi;
869
870    #[async_trait]
871    impl ServiceApi for MockServiceApi {
872        async fn call(
873            &self,
874            service: &str,
875            method: &str,
876            _params: serde_json::Value,
877        ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
878            Ok(serde_json::json!({"service": service, "method": method}))
879        }
880        async fn list_services(&self) -> Vec<ServiceInfo> {
881            vec![ServiceInfo {
882                name: "test".into(),
883                service_type: "core".into(),
884                healthy: true,
885            }]
886        }
887        async fn health(
888            &self,
889            _service: &str,
890        ) -> Result<HealthStatus, Box<dyn std::error::Error + Send + Sync>> {
891            Ok(HealthStatus::Healthy)
892        }
893    }
894
895    #[tokio::test]
896    async fn shell_adapter_parses_command() {
897        let api = Arc::new(MockServiceApi);
898        let shell = ShellAdapter::new(api);
899        let result = shell.execute("kernel.status").await.unwrap();
900        assert_eq!(result["service"], "kernel");
901        assert_eq!(result["method"], "status");
902    }
903
904    #[tokio::test]
905    async fn shell_adapter_with_args() {
906        let api = Arc::new(MockServiceApi);
907        let shell = ShellAdapter::new(api);
908        let result = shell
909            .execute("agent.spawn {\"name\":\"test\"}")
910            .await
911            .unwrap();
912        assert_eq!(result["service"], "agent");
913    }
914
915    #[tokio::test]
916    async fn mcp_adapter_routes_tool_call() {
917        let api = Arc::new(MockServiceApi);
918        let mcp = McpAdapter::new(api);
919        let result = mcp
920            .handle_tool_call("kernel_status", serde_json::json!({}))
921            .await
922            .unwrap();
923        assert_eq!(result["service"], "kernel");
924    }
925
926    #[tokio::test]
927    async fn mcp_adapter_list_tools() {
928        let api = Arc::new(MockServiceApi);
929        let mcp = McpAdapter::new(api);
930        let tools = mcp.list_tools().await;
931        assert_eq!(tools.len(), 1);
932    }
933
934    #[test]
935    fn audit_level_default_is_full() {
936        assert_eq!(ServiceAuditLevel::default(), ServiceAuditLevel::Full);
937    }
938
939    #[test]
940    fn dual_registry_independent() {
941        // SystemService and ServiceEntry registrations are independent
942        let registry = ServiceRegistry::new();
943
944        // Register a SystemService
945        registry
946            .register(Arc::new(MockService::new("both", ServiceType::Core)))
947            .unwrap();
948
949        // Register a ServiceEntry with the same name (independent map)
950        registry
951            .register_entry(ServiceEntry {
952                name: "both".into(),
953                owner_pid: Some(1),
954                endpoint: ServiceEndpoint::AgentInbox(1),
955                audit_level: ServiceAuditLevel::Full,
956                registered_at: Utc::now(),
957            })
958            .unwrap();
959
960        assert!(registry.get("both").is_some());
961        assert!(registry.get_entry("both").is_some());
962    }
963
964    // --- K4 G2: Service contract tests ---
965
966    #[test]
967    fn service_contract_serde() {
968        let contract = ServiceContract {
969            service_name: "auth".into(),
970            version: "1.0.0".into(),
971            methods: vec!["login".into(), "logout".into()],
972            content_hash: "abc123".into(),
973        };
974        let json = serde_json::to_string(&contract).unwrap();
975        let restored: ServiceContract = serde_json::from_str(&json).unwrap();
976        assert_eq!(restored.service_name, "auth");
977        assert_eq!(restored.methods.len(), 2);
978    }
979
980    #[test]
981    #[cfg(feature = "exochain")]
982    fn contract_on_chain() {
983        let registry = ServiceRegistry::new();
984        let chain = crate::chain::ChainManager::new(0, 1000);
985        let contract = ServiceContract {
986            service_name: "auth".into(),
987            version: "1.0.0".into(),
988            methods: vec!["login".into(), "logout".into()],
989            content_hash: "abc123".into(),
990        };
991        registry.register_contract(&contract, &chain).unwrap();
992        // Chain should have genesis + 1 contract event
993        assert_eq!(chain.len(), 2);
994        let tail = chain.tail(1);
995        assert_eq!(tail[0].kind, "service.contract.register");
996    }
997
998    #[test]
999    #[cfg(feature = "exochain")]
1000    fn contract_version_immutability() {
1001        let chain = crate::chain::ChainManager::new(0, 1000);
1002        let registry = ServiceRegistry::new();
1003        let v1 = ServiceContract {
1004            service_name: "auth".into(),
1005            version: "1.0.0".into(),
1006            methods: vec!["login".into()],
1007            content_hash: "v1hash".into(),
1008        };
1009        let v2 = ServiceContract {
1010            service_name: "auth".into(),
1011            version: "2.0.0".into(),
1012            methods: vec!["login".into(), "refresh".into()],
1013            content_hash: "v2hash".into(),
1014        };
1015        registry.register_contract(&v1, &chain).unwrap();
1016        registry.register_contract(&v2, &chain).unwrap();
1017        // Both versions recorded — chain is append-only so v1 cannot be mutated
1018        assert_eq!(chain.len(), 3); // genesis + v1 + v2
1019    }
1020
1021    // --- C3: register_with_contract tests ---
1022
1023    #[test]
1024    #[cfg(feature = "exochain")]
1025    fn register_with_contract_anchors_to_chain() {
1026        let chain = crate::chain::ChainManager::new(0, 1000);
1027        let registry = ServiceRegistry::new();
1028        let svc = Arc::new(MockService::new("api-v1", ServiceType::Api));
1029
1030        registry
1031            .register_with_contract(
1032                svc,
1033                vec!["get".into(), "set".into(), "delete".into()],
1034                &chain,
1035            )
1036            .unwrap();
1037
1038        // Service should be registered
1039        assert!(registry.get("api-v1").is_some());
1040
1041        // Contract should be on chain
1042        let events = chain.tail(1);
1043        assert_eq!(events[0].kind, "service.contract.register");
1044    }
1045
1046    #[test]
1047    #[cfg(feature = "exochain")]
1048    fn register_with_contract_duplicate_fails() {
1049        let chain = crate::chain::ChainManager::new(0, 1000);
1050        let registry = ServiceRegistry::new();
1051        let svc1 = Arc::new(MockService::new("dup-c3", ServiceType::Core));
1052        let svc2 = Arc::new(MockService::new("dup-c3", ServiceType::Core));
1053
1054        registry
1055            .register_with_contract(svc1, vec!["ping".into()], &chain)
1056            .unwrap();
1057        // Second registration with same name should fail
1058        let result = registry.register_with_contract(svc2, vec!["ping".into()], &chain);
1059        assert!(result.is_err());
1060    }
1061
1062    #[test]
1063    #[cfg(feature = "exochain")]
1064    fn register_with_contract_hash_deterministic() {
1065        let chain = crate::chain::ChainManager::new(0, 1000);
1066        let registry = ServiceRegistry::new();
1067        let svc = Arc::new(MockService::new("hash-svc", ServiceType::Plugin));
1068
1069        registry
1070            .register_with_contract(
1071                svc,
1072                vec!["alpha".into(), "beta".into()],
1073                &chain,
1074            )
1075            .unwrap();
1076
1077        let events = chain.tail(1);
1078        let payload = events[0].payload.as_ref().unwrap();
1079        let hash = payload["hash"].as_str().unwrap();
1080        // Hash should be a 64-char hex string (SHA-256)
1081        assert_eq!(hash.len(), 64);
1082        assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
1083    }
1084
1085    // ── Sprint 09a: serde roundtrip tests ────────────────────────
1086
1087    #[test]
1088    fn service_type_serde_roundtrip_core() {
1089        let st = ServiceType::Core;
1090        let json = serde_json::to_string(&st).unwrap();
1091        let restored: ServiceType = serde_json::from_str(&json).unwrap();
1092        assert_eq!(restored, ServiceType::Core);
1093    }
1094
1095    #[test]
1096    fn service_type_serde_roundtrip_plugin() {
1097        let st = ServiceType::Plugin;
1098        let json = serde_json::to_string(&st).unwrap();
1099        let restored: ServiceType = serde_json::from_str(&json).unwrap();
1100        assert_eq!(restored, ServiceType::Plugin);
1101    }
1102
1103    #[test]
1104    fn service_type_serde_roundtrip_custom() {
1105        let st = ServiceType::Custom("webhook".into());
1106        let json = serde_json::to_string(&st).unwrap();
1107        let restored: ServiceType = serde_json::from_str(&json).unwrap();
1108        assert_eq!(restored, ServiceType::Custom("webhook".into()));
1109    }
1110
1111    #[test]
1112    fn service_audit_level_serde_roundtrip() {
1113        for level in [ServiceAuditLevel::Full, ServiceAuditLevel::GateOnly] {
1114            let json = serde_json::to_string(&level).unwrap();
1115            let restored: ServiceAuditLevel = serde_json::from_str(&json).unwrap();
1116            assert_eq!(restored, level);
1117        }
1118    }
1119
1120    #[test]
1121    fn service_info_serde_roundtrip() {
1122        let info = ServiceInfo {
1123            name: "cache".into(),
1124            service_type: "core".into(),
1125            healthy: true,
1126        };
1127        let json = serde_json::to_string(&info).unwrap();
1128        let restored: ServiceInfo = serde_json::from_str(&json).unwrap();
1129        assert_eq!(restored.name, "cache");
1130        assert_eq!(restored.service_type, "core");
1131        assert!(restored.healthy);
1132    }
1133
1134    #[test]
1135    fn service_info_unhealthy_roundtrip() {
1136        let info = ServiceInfo {
1137            name: "broken".into(),
1138            service_type: "plugin".into(),
1139            healthy: false,
1140        };
1141        let json = serde_json::to_string(&info).unwrap();
1142        let restored: ServiceInfo = serde_json::from_str(&json).unwrap();
1143        assert!(!restored.healthy);
1144    }
1145
1146    #[test]
1147    fn snapshot_returns_all_services() {
1148        let registry = ServiceRegistry::new();
1149        registry
1150            .register(Arc::new(MockService::new("svc-a", ServiceType::Core)))
1151            .unwrap();
1152        registry
1153            .register(Arc::new(MockService::new("svc-b", ServiceType::Plugin)))
1154            .unwrap();
1155
1156        let snapshot = registry.snapshot();
1157        assert_eq!(snapshot.len(), 2);
1158        let names: Vec<&str> = snapshot.iter().map(|(n, _)| n.as_str()).collect();
1159        assert!(names.contains(&"svc-a"));
1160        assert!(names.contains(&"svc-b"));
1161    }
1162
1163    #[test]
1164    fn snapshot_is_independent_of_registry() {
1165        let registry = ServiceRegistry::new();
1166        registry
1167            .register(Arc::new(MockService::new("snap-svc", ServiceType::Core)))
1168            .unwrap();
1169
1170        let snapshot = registry.snapshot();
1171        // Remove from registry -- snapshot should still have it
1172        registry.unregister("snap-svc");
1173        assert!(registry.get("snap-svc").is_none());
1174        assert_eq!(snapshot.len(), 1);
1175    }
1176
1177    #[test]
1178    fn get_nonexistent_service_returns_none() {
1179        let registry = ServiceRegistry::new();
1180        assert!(registry.get("ghost").is_none());
1181    }
1182
1183    #[test]
1184    fn get_nonexistent_entry_returns_none() {
1185        let registry = ServiceRegistry::new();
1186        assert!(registry.get_entry("ghost").is_none());
1187    }
1188
1189    #[tokio::test]
1190    async fn shell_adapter_missing_dot_returns_error() {
1191        let api = Arc::new(MockServiceApi);
1192        let shell = ShellAdapter::new(api);
1193        let result = shell.execute("nodot").await;
1194        assert!(result.is_err());
1195    }
1196
1197    #[tokio::test]
1198    async fn shell_adapter_json_array_args() {
1199        let api = Arc::new(MockServiceApi);
1200        let shell = ShellAdapter::new(api);
1201        let result = shell.execute("svc.method [1,2,3]").await.unwrap();
1202        assert_eq!(result["service"], "svc");
1203        assert_eq!(result["method"], "method");
1204    }
1205
1206    #[tokio::test]
1207    async fn mcp_adapter_dot_separator() {
1208        let api = Arc::new(MockServiceApi);
1209        let mcp = McpAdapter::new(api);
1210        let result = mcp
1211            .handle_tool_call("kernel.status", serde_json::json!({}))
1212            .await
1213            .unwrap();
1214        assert_eq!(result["service"], "kernel");
1215        assert_eq!(result["method"], "status");
1216    }
1217
1218    #[tokio::test]
1219    async fn mcp_adapter_invalid_tool_name_returns_error() {
1220        let api = Arc::new(MockServiceApi);
1221        let mcp = McpAdapter::new(api);
1222        let result = mcp
1223            .handle_tool_call("noseparator", serde_json::json!({}))
1224            .await;
1225        assert!(result.is_err());
1226    }
1227
1228    #[test]
1229    fn service_type_all_variants_serde() {
1230        let variants = vec![
1231            ServiceType::Core,
1232            ServiceType::Plugin,
1233            ServiceType::Cron,
1234            ServiceType::Api,
1235            ServiceType::Custom("special".into()),
1236        ];
1237        for v in variants {
1238            let json = serde_json::to_string(&v).unwrap();
1239            let _: ServiceType = serde_json::from_str(&json).unwrap();
1240        }
1241    }
1242
1243    #[test]
1244    fn service_entry_external_endpoint_roundtrip() {
1245        let entry = ServiceEntry {
1246            name: "ext".into(),
1247            owner_pid: None,
1248            endpoint: ServiceEndpoint::External {
1249                url: "https://api.example.com/v2".into(),
1250            },
1251            audit_level: ServiceAuditLevel::GateOnly,
1252            registered_at: Utc::now(),
1253        };
1254        let json = serde_json::to_string(&entry).unwrap();
1255        let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
1256        assert_eq!(restored.name, "ext");
1257        assert!(restored.owner_pid.is_none());
1258        assert_eq!(restored.audit_level, ServiceAuditLevel::GateOnly);
1259    }
1260
1261    #[test]
1262    fn service_entry_container_endpoint_roundtrip() {
1263        let entry = ServiceEntry {
1264            name: "docker-svc".into(),
1265            owner_pid: None,
1266            endpoint: ServiceEndpoint::Container {
1267                id: "abc123".into(),
1268            },
1269            audit_level: ServiceAuditLevel::Full,
1270            registered_at: Utc::now(),
1271        };
1272        let json = serde_json::to_string(&entry).unwrap();
1273        let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
1274        assert!(matches!(
1275            restored.endpoint,
1276            ServiceEndpoint::Container { ref id } if id == "abc123"
1277        ));
1278    }
1279}