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}