use serde::{Deserialize, Serialize};
use crate::hook::{FailAction, Hook, HookEvent, HookHandler};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct HookProfile {
pub name: String,
pub description: String,
pub hooks: Vec<Hook>,
}
impl HookProfile {
#[must_use]
pub(crate) fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
hooks: Vec::new(),
}
}
#[must_use]
pub(crate) fn with_hook(mut self, hook: Hook) -> Self {
self.hooks.push(hook);
self
}
#[must_use]
pub(crate) fn minimal() -> Self {
Self::new("minimal", "Minimal profile with no hooks")
}
#[must_use]
pub(crate) fn logging() -> Self {
Self::new("logging", "Profile that logs all major events to stdout")
.with_hook(
Hook::new(HookEvent::SessionStart)
.with_name("log-session-start")
.with_handler(HookHandler::Command {
command: "echo".to_string(),
args: vec!["[ASTRID] Session started: $ASTRID_SESSION_ID".to_string()],
env: std::collections::HashMap::new(),
working_dir: None,
})
.with_fail_action(FailAction::Ignore)
.async_mode(),
)
.with_hook(
Hook::new(HookEvent::SessionEnd)
.with_name("log-session-end")
.with_handler(HookHandler::Command {
command: "echo".to_string(),
args: vec!["[ASTRID] Session ended: $ASTRID_SESSION_ID".to_string()],
env: std::collections::HashMap::new(),
working_dir: None,
})
.with_fail_action(FailAction::Ignore)
.async_mode(),
)
.with_hook(
Hook::new(HookEvent::PreToolCall)
.with_name("log-tool-call")
.with_handler(HookHandler::Command {
command: "echo".to_string(),
args: vec!["[ASTRID] Tool call: $ASTRID_HOOK_DATA".to_string()],
env: std::collections::HashMap::new(),
working_dir: None,
})
.with_fail_action(FailAction::Ignore)
.async_mode(),
)
}
#[must_use]
pub(crate) fn security() -> Self {
Self::new(
"security",
"Profile that adds security checks before sensitive operations",
)
.with_hook(
Hook::new(HookEvent::PreToolCall)
.with_name("block-dangerous-tools")
.with_description("Block execution of potentially dangerous tools")
.with_handler(HookHandler::Command {
command: "sh".to_string(),
args: vec![
"-c".to_string(),
r#"
TOOL=$(echo "$ASTRID_HOOK_DATA" | grep -o '"tool_name":"[^"]*"' | cut -d'"' -f4)
case "$TOOL" in
rm|sudo|chmod|chown|mkfs|dd)
echo "block: Dangerous tool '$TOOL' blocked by security policy"
;;
*)
echo "continue"
;;
esac
"#
.to_string(),
],
env: std::collections::HashMap::new(),
working_dir: None,
})
.with_fail_action(FailAction::Block)
.with_timeout(5),
)
}
#[must_use]
pub(crate) fn notifications(webhook_url: impl Into<String>) -> Self {
let url = webhook_url.into();
Self::new(
"notifications",
"Profile that sends webhook notifications for events",
)
.with_hook(
Hook::new(HookEvent::SessionStart)
.with_name("notify-session-start")
.with_handler(HookHandler::Http {
url: url.clone(),
method: "POST".to_string(),
headers: std::collections::HashMap::new(),
body_template: Some(
r#"{"event": "session_start", "session_id": "{{session_id}}"}"#.to_string(),
),
})
.with_fail_action(FailAction::Warn)
.async_mode(),
)
.with_hook(
Hook::new(HookEvent::SessionEnd)
.with_name("notify-session-end")
.with_handler(HookHandler::Http {
url,
method: "POST".to_string(),
headers: std::collections::HashMap::new(),
body_template: Some(
r#"{"event": "session_end", "session_id": "{{session_id}}"}"#.to_string(),
),
})
.with_fail_action(FailAction::Warn)
.async_mode(),
)
}
#[must_use]
pub(crate) fn development() -> Self {
Self::new(
"development",
"Profile for development with debugging helpers",
)
.with_hook(
Hook::new(HookEvent::PreToolCall)
.with_name("debug-tool-calls")
.with_handler(HookHandler::Command {
command: "sh".to_string(),
args: vec![
"-c".to_string(),
r#"echo "[DEBUG] Tool call at $(date): $ASTRID_HOOK_DATA" >> /tmp/astrid-debug.log"#.to_string(),
],
env: std::collections::HashMap::new(),
working_dir: None,
})
.with_fail_action(FailAction::Ignore)
.async_mode(),
)
.with_hook(
Hook::new(HookEvent::ToolError)
.with_name("debug-tool-errors")
.with_handler(HookHandler::Command {
command: "sh".to_string(),
args: vec![
"-c".to_string(),
r#"echo "[ERROR] Tool error at $(date): $ASTRID_HOOK_DATA" >> /tmp/astrid-debug.log"#.to_string(),
],
env: std::collections::HashMap::new(),
working_dir: None,
})
.with_fail_action(FailAction::Ignore)
.async_mode(),
)
}
}
#[must_use]
pub(crate) fn get_profile(name: &str) -> Option<HookProfile> {
match name {
"minimal" => Some(HookProfile::minimal()),
"logging" => Some(HookProfile::logging()),
"security" => Some(HookProfile::security()),
"development" => Some(HookProfile::development()),
_ => None,
}
}
#[must_use]
pub(crate) fn available_profiles() -> Vec<&'static str> {
vec!["minimal", "logging", "security", "development"]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_minimal_profile() {
let profile = HookProfile::minimal();
assert_eq!(profile.name, "minimal");
assert!(profile.hooks.is_empty());
}
#[test]
fn test_logging_profile() {
let profile = HookProfile::logging();
assert_eq!(profile.name, "logging");
assert!(!profile.hooks.is_empty());
let events: Vec<_> = profile.hooks.iter().map(|h| h.event).collect();
assert!(events.contains(&HookEvent::SessionStart));
assert!(events.contains(&HookEvent::SessionEnd));
assert!(events.contains(&HookEvent::PreToolCall));
}
#[test]
fn test_security_profile() {
let profile = HookProfile::security();
assert_eq!(profile.name, "security");
assert!(
profile
.hooks
.iter()
.any(|h| h.fail_action == FailAction::Block)
);
}
#[test]
fn test_notifications_profile() {
let profile = HookProfile::notifications("https://example.com/webhook");
assert_eq!(profile.name, "notifications");
assert!(
profile
.hooks
.iter()
.all(|h| matches!(h.handler, HookHandler::Http { .. }))
);
}
#[test]
fn test_get_profile() {
assert!(get_profile("minimal").is_some());
assert!(get_profile("logging").is_some());
assert!(get_profile("security").is_some());
assert!(get_profile("unknown").is_none());
}
#[test]
fn test_available_profiles() {
let profiles = available_profiles();
assert!(profiles.contains(&"minimal"));
assert!(profiles.contains(&"logging"));
assert!(profiles.contains(&"security"));
}
}