Adding Actions
Guide to implementing new action handlers.
Action Architecture
Actions implement the Action trait:
#![allow(unused)] fn main() { #[async_trait] pub trait Action: Send + Sync { fn name(&self) -> &str; fn description(&self) -> &str; fn required_parameters(&self) -> Vec<ParameterDef>; fn supports_rollback(&self) -> bool; async fn execute(&self, context: ActionContext) -> Result<ActionResult, ActionError>; async fn rollback(&self, context: ActionContext) -> Result<ActionResult, ActionError> { Err(ActionError::RollbackNotSupported) } } }
Implementing an Action
1. Create the File
touch crates/tw-actions/src/my_action.rs
2. Define the Action
#![allow(unused)] fn main() { use crate::registry::{ Action, ActionContext, ActionError, ActionResult, ParameterDef, ParameterType, }; use async_trait::async_trait; use chrono::Utc; use std::collections::HashMap; use tracing::{info, instrument}; /// My custom action handler. pub struct MyAction; impl MyAction { pub fn new() -> Self { Self } } impl Default for MyAction { fn default() -> Self { Self::new() } } #[async_trait] impl Action for MyAction { fn name(&self) -> &str { "my_action" } fn description(&self) -> &str { "Description of what this action does" } fn required_parameters(&self) -> Vec<ParameterDef> { vec![ ParameterDef::required( "target", "The target of the action", ParameterType::String, ), ParameterDef::optional( "force", "Force the action even if conditions aren't met", ParameterType::Boolean, serde_json::json!(false), ), ] } fn supports_rollback(&self) -> bool { true } #[instrument(skip(self, context))] async fn execute(&self, context: ActionContext) -> Result<ActionResult, ActionError> { let started_at = Utc::now(); // Get required parameter let target = context.require_string("target")?; // Get optional parameter with default let force = context .get_param("force") .and_then(|v| v.as_bool()) .unwrap_or(false); info!("Executing my_action on target: {}", target); // Perform the action // ... // Build output let mut output = HashMap::new(); output.insert("target".to_string(), serde_json::json!(target)); output.insert("success".to_string(), serde_json::json!(true)); Ok(ActionResult::success( self.name(), &format!("Action completed on {}", target), started_at, output, )) } async fn rollback(&self, context: ActionContext) -> Result<ActionResult, ActionError> { let started_at = Utc::now(); let target = context.require_string("target")?; info!("Rolling back my_action on target: {}", target); // Perform rollback // ... let mut output = HashMap::new(); output.insert("target".to_string(), serde_json::json!(target)); Ok(ActionResult::success( &format!("{}_rollback", self.name()), &format!("Rollback completed on {}", target), started_at, output, )) } } }
3. Add to Module
#![allow(unused)] fn main() { // crates/tw-actions/src/lib.rs mod my_action; pub use my_action::MyAction; }
4. Register in Registry
#![allow(unused)] fn main() { // crates/tw-actions/src/registry.rs impl ActionRegistry { pub fn new() -> Self { let mut registry = Self { actions: HashMap::new(), }; // Register built-in actions registry.register(Box::new(QuarantineEmailAction::new())); registry.register(Box::new(BlockSenderAction::new())); registry.register(Box::new(MyAction::new())); // Add here registry } } }
Parameter Types
Available parameter types:
#![allow(unused)] fn main() { pub enum ParameterType { String, Integer, Float, Boolean, List, Object, } }
Define parameters:
#![allow(unused)] fn main() { fn required_parameters(&self) -> Vec<ParameterDef> { vec![ ParameterDef::required("name", "Description", ParameterType::String), ParameterDef::optional("count", "Description", ParameterType::Integer, json!(10)), ParameterDef::optional("tags", "Description", ParameterType::List, json!([])), ] } }
Using Connectors
Actions can use connectors via dependency injection:
#![allow(unused)] fn main() { pub struct MyAction { connector: Arc<dyn MyConnector + Send + Sync>, } impl MyAction { pub fn new(connector: Arc<dyn MyConnector + Send + Sync>) -> Self { Self { connector } } } #[async_trait] impl Action for MyAction { async fn execute(&self, context: ActionContext) -> Result<ActionResult, ActionError> { // Use connector let result = self.connector.do_something().await .map_err(|e| ActionError::ExecutionFailed(e.to_string()))?; // ... } } }
Error Handling
Use appropriate error types:
#![allow(unused)] fn main() { pub enum ActionError { /// Missing or invalid parameters InvalidParameters(String), /// Execution failed ExecutionFailed(String), /// Action timed out Timeout, /// Rollback not supported RollbackNotSupported, /// Policy denied the action PolicyDenied(String), } }
Testing
Unit Tests
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use uuid::Uuid; #[tokio::test] async fn test_my_action_success() { let action = MyAction::new(); let context = ActionContext::new(Uuid::new_v4()) .with_param("target", serde_json::json!("test-target")); let result = action.execute(context).await.unwrap(); assert!(result.success); assert_eq!(result.output["target"], "test-target"); } #[tokio::test] async fn test_my_action_missing_param() { let action = MyAction::new(); let context = ActionContext::new(Uuid::new_v4()); let result = action.execute(context).await; assert!(matches!(result, Err(ActionError::InvalidParameters(_)))); } #[tokio::test] async fn test_my_action_rollback() { let action = MyAction::new(); assert!(action.supports_rollback()); let context = ActionContext::new(Uuid::new_v4()) .with_param("target", serde_json::json!("test-target")); let result = action.rollback(context).await.unwrap(); assert!(result.success); } } }
Policy Integration
Actions are automatically evaluated by the policy engine. Configure default approval:
# Default policy for new action
[[policy.rules]]
name = "my_action_default"
action = "my_action"
approval_level = "analyst"
Documentation
Document your action:
#![allow(unused)] fn main() { //! My custom action. //! //! This action performs X on target Y. //! //! # Parameters //! //! - `target` (required): The target to act on //! - `force` (optional): Force execution (default: false) //! //! # Example //! //! ```yaml //! - action: my_action //! parameters: //! target: "example" //! force: true //! ``` //! //! # Rollback //! //! This action supports rollback via `my_action_rollback`. }