use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub session_id: String,
pub timestamp: DateTime<Utc>,
pub tool_name: String,
pub input: Value,
pub duration_ms: Option<u64>,
pub output: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FileOperation {
Read,
Write,
Edit,
Glob,
Grep,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileAccess {
pub session_id: String,
pub timestamp: DateTime<Utc>,
pub path: String,
pub operation: FileOperation,
pub line_range: Option<(u64, u64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BashCommand {
pub session_id: String,
pub timestamp: DateTime<Utc>,
pub command: String,
pub is_destructive: bool,
pub output_preview: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NetworkTool {
WebFetch,
WebSearch,
McpCall { server: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkCall {
pub session_id: String,
pub timestamp: DateTime<Utc>,
pub url: String,
pub tool: NetworkTool,
pub domain: String,
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum AlertSeverity {
Info,
Warning,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AlertCategory {
CredentialAccess,
DestructiveCommand,
ExternalExfil,
ScopeViolation,
ForcePush,
}
impl AlertCategory {
pub fn action_hint(&self) -> &'static str {
match self {
AlertCategory::CredentialAccess => {
"Verify the credential wasn't exposed. If it was, rotate it immediately."
}
AlertCategory::DestructiveCommand => {
"Check if deleted files are recoverable (Trash, git stash, backup)."
}
AlertCategory::ExternalExfil => {
"Review what data was sent to this domain and whether it was intentional."
}
AlertCategory::ScopeViolation => {
"Inspect the file written outside the project root. Delete it if unintended."
}
AlertCategory::ForcePush => {
"Run git reflog to find the overwritten commit. Force-push a revert if needed."
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_hint_all_variants_non_empty() {
let variants = [
AlertCategory::CredentialAccess,
AlertCategory::DestructiveCommand,
AlertCategory::ExternalExfil,
AlertCategory::ScopeViolation,
AlertCategory::ForcePush,
];
for variant in &variants {
let hint = variant.action_hint();
assert!(!hint.is_empty(), "{:?} has an empty action hint", variant);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Alert {
pub session_id: String,
pub timestamp: DateTime<Utc>,
pub severity: AlertSeverity,
pub category: AlertCategory,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ActivitySummary {
pub file_accesses: Vec<FileAccess>,
pub bash_commands: Vec<BashCommand>,
pub network_calls: Vec<NetworkCall>,
pub alerts: Vec<Alert>,
}