Skip to main content

runtimo_core/
capability.rs

1//! Capability trait and registry.
2//!
3//! The [`Capability`] trait is the core abstraction — every pluggable operation
4//! (file read, file write, shell exec, etc.) implements this trait. The
5//! [`CapabilityRegistry`] collects and dispatches to registered capabilities.
6
7use crate::Result;
8use serde_json::Value;
9use std::path::PathBuf;
10
11/// Context provided to capabilities during execution.
12///
13/// Carries execution metadata such as dry-run mode, the owning job ID,
14/// and the working directory for relative path resolution.
15pub struct Context {
16    /// If true, the capability should not perform side effects.
17    pub dry_run: bool,
18    /// The job ID that owns this execution.
19    pub job_id: String,
20    /// Working directory for relative path resolution.
21    pub working_dir: PathBuf,
22}
23
24/// Output from capability execution.
25///
26/// Returned by every [`Capability::execute`] call. The `data` field holds
27/// structured JSON output, while `message` provides a human-readable summary.
28#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
29pub struct Output {
30    /// Whether the capability completed successfully.
31    pub success: bool,
32    /// Structured output data (JSON).
33    pub data: Value,
34    /// Human-readable status or error message.
35    pub message: Option<String>,
36}
37
38/// The Capability trait — all capabilities must implement this.
39///
40/// Each capability defines its name, argument schema, validation logic,
41/// and execution behavior. The executor calls these methods in order:
42/// `name()` → `schema()` → `validate()` → `execute()`.
43///
44/// # Example
45///
46/// ```rust
47/// use runtimo_core::capability::{Capability, Context, Output};
48/// use runtimo_core::Result;
49/// use serde_json::Value;
50///
51/// struct Echo;
52///
53/// impl Capability for Echo {
54///     fn name(&self) -> &'static str { "Echo" }
55///     fn description(&self) -> &'static str { "Echo back arguments" }
56///     fn schema(&self) -> Value { serde_json::json!({"type":"object"}) }
57///     fn validate(&self, _args: &Value) -> Result<()> { Ok(()) }
58///     fn execute(&self, args: &Value, _ctx: &Context) -> Result<Output> {
59///         Ok(Output { success: true, data: args.clone(), message: None })
60///     }
61/// }
62/// ```
63pub trait Capability: Send + Sync {
64    /// Returns the capability name (e.g., `"FileRead"`, `"FileWrite"`).
65    ///
66    /// This name is used for registry lookups and WAL event tagging.
67    fn name(&self) -> &'static str;
68
69    /// Returns a one-line human-readable description of what this capability does.
70    ///
71    /// Used by the CLI `list` command and `--help` output to help users
72    /// discover available capabilities.
73    fn description(&self) -> &'static str;
74
75    /// Returns the JSON Schema for the capability's arguments.
76    ///
77    /// The schema is used by [`Capability::validate`] and by the CLI
78    /// to generate `--help` output for each capability.
79    fn schema(&self) -> Value;
80
81    /// Validates the arguments against the schema.
82    ///
83    /// Implementations should deserialize `args` into their typed args struct
84    /// and perform semantic checks (e.g., path traversal rejection).
85    ///
86    /// # Errors
87    ///
88    /// Returns [`Error::SchemaValidationFailed`](crate::Error::SchemaValidationFailed)
89    /// if arguments are malformed or semantically invalid.
90    fn validate(&self, args: &Value) -> Result<()>;
91
92    /// Executes the capability with the given arguments and context.
93    ///
94    /// This is called after `validate()` passes. Implementations should
95    /// perform the actual work and return an [`Output`].
96    ///
97    /// # Errors
98    ///
99    /// Returns [`Error::ExecutionFailed`](crate::Error::ExecutionFailed)
100    /// if the operation cannot be completed.
101    fn execute(&self, args: &Value, ctx: &Context) -> Result<Output>;
102}
103
104/// Registry of available capabilities.
105///
106/// Stores capabilities by name and provides lookup, listing, and registration.
107///
108/// # Example
109///
110/// ```rust,ignore
111/// use runtimo_core::{CapabilityRegistry, FileRead, FileWrite};
112/// use std::path::PathBuf;
113///
114/// let mut registry = CapabilityRegistry::new();
115/// registry.register(FileRead);
116/// registry.register(FileWrite::new(PathBuf::from("/tmp/backups")).unwrap());
117///
118/// assert!(registry.get("FileRead").is_some());
119/// let caps = registry.list();
120/// assert_eq!(caps.len(), 2);
121/// assert!(caps.contains(&"FileRead"));
122/// assert!(caps.contains(&"FileWrite"));
123/// ```
124pub struct CapabilityRegistry {
125    capabilities: std::collections::HashMap<String, Box<dyn Capability>>,
126}
127
128impl CapabilityRegistry {
129    /// Creates an empty registry.
130    pub fn new() -> Self {
131        Self {
132            capabilities: std::collections::HashMap::new(),
133        }
134    }
135
136    /// Registers a capability in the registry.
137    ///
138    /// The capability is stored under its [`Capability::name`]. If a capability
139    /// with the same name already exists, it is replaced.
140    pub fn register<C: Capability + 'static>(&mut self, capability: C) {
141        let name = capability.name().to_string();
142        self.capabilities.insert(name, Box::new(capability));
143    }
144
145    /// Looks up a capability by name.
146    ///
147    /// Returns `None` if no capability with the given name is registered.
148    pub fn get(&self, name: &str) -> Option<&dyn Capability> {
149        self.capabilities.get(name).map(|c| c.as_ref())
150    }
151
152    /// Returns the names of all registered capabilities.
153    pub fn list(&self) -> Vec<&str> {
154        self.capabilities.keys().map(|s| s.as_str()).collect()
155    }
156}
157
158impl Default for CapabilityRegistry {
159    fn default() -> Self {
160        Self::new()
161    }
162}