use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[expect(clippy::struct_excessive_bools)]
pub(crate) struct HooksConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_timeout")]
pub default_timeout_secs: u64,
#[serde(default = "default_max_hooks")]
pub max_hooks: usize,
#[serde(default)]
pub hook_directories: Vec<PathBuf>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default = "default_true")]
pub allow_async_hooks: bool,
#[serde(default)]
pub allow_wasm_hooks: bool,
#[serde(default)]
pub allow_agent_hooks: bool,
#[serde(default = "default_true")]
pub allow_http_hooks: bool,
#[serde(default = "default_true")]
pub allow_command_hooks: bool,
#[serde(default)]
pub global_env: std::collections::HashMap<String, String>,
#[serde(default)]
pub default_working_dir: Option<PathBuf>,
}
fn default_enabled() -> bool {
true
}
fn default_timeout() -> u64 {
30
}
fn default_max_hooks() -> usize {
100
}
fn default_true() -> bool {
true
}
impl Default for HooksConfig {
fn default() -> Self {
Self {
enabled: true,
default_timeout_secs: 30,
max_hooks: 100,
hook_directories: Vec::new(),
profile: None,
allow_async_hooks: true,
allow_wasm_hooks: false,
allow_agent_hooks: false,
allow_http_hooks: true,
allow_command_hooks: true,
global_env: std::collections::HashMap::new(),
default_working_dir: None,
}
}
}
impl HooksConfig {
#[must_use]
pub(crate) fn new() -> Self {
Self::default()
}
#[must_use]
pub(crate) fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
#[must_use]
pub(crate) fn minimal() -> Self {
Self {
enabled: true,
default_timeout_secs: 5,
max_hooks: 10,
allow_async_hooks: false,
allow_wasm_hooks: false,
allow_agent_hooks: false,
allow_http_hooks: false,
allow_command_hooks: true,
..Default::default()
}
}
#[must_use]
pub(crate) fn with_timeout(mut self, secs: u64) -> Self {
self.default_timeout_secs = secs;
self
}
#[must_use]
pub(crate) fn with_directory(mut self, dir: PathBuf) -> Self {
self.hook_directories.push(dir);
self
}
#[must_use]
pub(crate) fn with_profile(mut self, profile: impl Into<String>) -> Self {
self.profile = Some(profile.into());
self
}
#[must_use]
pub(crate) fn enable_wasm(mut self) -> Self {
self.allow_wasm_hooks = true;
self
}
#[must_use]
pub(crate) fn enable_agent(mut self) -> Self {
self.allow_agent_hooks = true;
self
}
#[must_use]
pub(crate) fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.global_env.insert(key.into(), value.into());
self
}
#[must_use]
pub(crate) fn is_handler_allowed(&self, handler: &crate::hook::HookHandler) -> bool {
use crate::hook::HookHandler;
match handler {
HookHandler::Command { .. } => self.allow_command_hooks,
HookHandler::Http { .. } => self.allow_http_hooks,
HookHandler::Wasm { .. } => self.allow_wasm_hooks,
HookHandler::Agent { .. } => self.allow_agent_hooks,
}
}
pub(crate) fn validate_hook(&self, hook: &crate::hook::Hook) -> Result<(), String> {
if !self.enabled {
return Err("Hooks are disabled".to_string());
}
if !self.is_handler_allowed(&hook.handler) {
return Err(format!(
"Handler type {:?} is not allowed",
std::mem::discriminant(&hook.handler)
));
}
if hook.async_mode && !self.allow_async_hooks {
return Err("Async hooks are not allowed".to_string());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hook::{Hook, HookEvent, HookHandler};
#[test]
fn test_config_default() {
let config = HooksConfig::default();
assert!(config.enabled);
assert_eq!(config.default_timeout_secs, 30);
assert!(config.allow_command_hooks);
assert!(!config.allow_wasm_hooks);
}
#[test]
fn test_config_disabled() {
let config = HooksConfig::disabled();
assert!(!config.enabled);
}
#[test]
fn test_config_minimal() {
let config = HooksConfig::minimal();
assert!(config.enabled);
assert_eq!(config.default_timeout_secs, 5);
assert!(!config.allow_async_hooks);
}
#[test]
fn test_config_builder() {
let config = HooksConfig::new()
.with_timeout(60)
.with_profile("security")
.enable_wasm()
.with_env("MY_VAR", "my_value");
assert_eq!(config.default_timeout_secs, 60);
assert_eq!(config.profile, Some("security".to_string()));
assert!(config.allow_wasm_hooks);
assert_eq!(
config.global_env.get("MY_VAR"),
Some(&"my_value".to_string())
);
}
#[test]
fn test_is_handler_allowed() {
let config = HooksConfig::minimal();
assert!(config.is_handler_allowed(&HookHandler::command("echo")));
assert!(!config.is_handler_allowed(&HookHandler::http("http://example.com")));
assert!(!config.is_handler_allowed(&HookHandler::wasm("/path/to/module.wasm")));
}
#[test]
fn test_validate_hook() {
let config = HooksConfig::minimal();
let valid_hook =
Hook::new(HookEvent::SessionStart).with_handler(HookHandler::command("echo"));
assert!(config.validate_hook(&valid_hook).is_ok());
let http_hook = Hook::new(HookEvent::SessionStart)
.with_handler(HookHandler::http("http://example.com"));
assert!(config.validate_hook(&http_hook).is_err());
let async_hook = Hook::new(HookEvent::SessionStart)
.with_handler(HookHandler::command("echo"))
.async_mode();
assert!(config.validate_hook(&async_hook).is_err());
}
}