use std::sync::atomic::Ordering;
use std::sync::Arc;
use axum::{
extract::State,
http::{HeaderMap, StatusCode},
Json,
};
use crate::daemon::AppState;
use crate::envelope::{
arch_string, current_timestamp, new_event_id, AgentType, EventEnvelope, HookEventType, Verdict,
VerdictResponse,
};
use crate::privacy;
pub async fn pre_tool_use(
State(state): State<Arc<AppState>>,
Json(body): Json<serde_json::Value>,
) -> (StatusCode, HeaderMap, Json<VerdictResponse>) {
let start = std::time::Instant::now();
process_hook_event(state, body, HookEventType::PreToolUse, start).await
}
pub async fn user_prompt_submit(
State(state): State<Arc<AppState>>,
Json(body): Json<serde_json::Value>,
) -> (StatusCode, HeaderMap, Json<VerdictResponse>) {
let start = std::time::Instant::now();
process_hook_event(state, body, HookEventType::UserPromptSubmit, start).await
}
pub async fn stop(
State(state): State<Arc<AppState>>,
Json(body): Json<serde_json::Value>,
) -> (StatusCode, HeaderMap, Json<VerdictResponse>) {
let start = std::time::Instant::now();
process_hook_event(state, body, HookEventType::Stop, start).await
}
async fn process_hook_event(
state: Arc<AppState>,
body: serde_json::Value,
event_type: HookEventType,
start: std::time::Instant,
) -> (StatusCode, HeaderMap, Json<VerdictResponse>) {
let event_id = new_event_id();
let mut headers = HeaderMap::new();
let session_id = body
.get("session_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let tool_name = body
.get("tool_name")
.or_else(|| body.get("tool").and_then(|t| t.get("name")))
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let tool_input = body
.get("tool_input")
.or_else(|| body.get("tool").and_then(|t| t.get("input")))
.cloned()
.unwrap_or(serde_json::Value::Null);
let is_duplicate = state
.dedup
.check_and_insert(&session_id, &tool_name, &tool_input);
let verdict = match event_type {
HookEventType::Stop => Verdict::Approve,
_ => Verdict::Allow,
};
if is_duplicate {
tracing::debug!(
code = crate::error::ERR_EVENT_DEDUPED,
session_id,
tool_name,
"Event deduplicated within TTL window"
);
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
let response = match event_type {
HookEventType::Stop => VerdictResponse::approve(event_id.clone(), latency_ms),
_ => VerdictResponse::allow(event_id.clone(), latency_ms),
};
headers.insert(
"x-openlatch-dedup",
"true".parse().expect("static header value is valid"),
);
return (StatusCode::OK, headers, Json(response));
}
let user_prompt = body
.get("user_prompt")
.or_else(|| body.get("prompt"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let reason = body
.get("reason")
.or_else(|| body.get("stopReason"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut filtered_input = tool_input;
privacy::filter_event_with(&mut filtered_input, &state.privacy_filter);
let filtered_user_prompt = user_prompt.map(|p| {
let mut val = serde_json::Value::String(p);
privacy::filter_event_with(&mut val, &state.privacy_filter);
val.as_str().unwrap_or_default().to_string()
});
let pre_log_latency_ms = start.elapsed().as_millis() as u64;
let envelope = EventEnvelope {
schema_version: "1.0".to_string(),
id: event_id.clone(),
timestamp: current_timestamp(),
event_type,
session_id,
tool_name: Some(tool_name),
tool_input: Some(filtered_input),
user_prompt: filtered_user_prompt,
reason,
verdict,
latency_ms: pre_log_latency_ms,
agent_platform: AgentType::ClaudeCode, agent_version: body
.get("agent_version")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
os: crate::envelope::os_string().to_string(),
arch: arch_string().to_string(),
client_version: env!("CARGO_PKG_VERSION").to_string(),
};
let envelope_json = serde_json::to_string(&envelope).unwrap_or_default();
state.event_logger.log(envelope_json);
state.event_counter.fetch_add(1, Ordering::Relaxed);
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
let response = match event_type {
HookEventType::Stop => VerdictResponse::approve(event_id, latency_ms),
_ => VerdictResponse::allow(event_id, latency_ms),
};
(StatusCode::OK, headers, Json(response))
}
pub async fn health(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let uptime_secs = state.started_at.elapsed().as_secs();
Json(serde_json::json!({
"status": "ok",
"version": env!("CARGO_PKG_VERSION"),
"uptime_secs": uptime_secs,
}))
}
pub async fn metrics(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let events = state.event_counter.load(Ordering::Relaxed);
let uptime_secs = state.started_at.elapsed().as_secs();
let update_available = state.get_available_update();
Json(serde_json::json!({
"events_processed": events,
"uptime_secs": uptime_secs,
"update_available": update_available,
}))
}
pub async fn shutdown_handler(State(state): State<Arc<AppState>>) -> StatusCode {
let mut tx = state.shutdown_tx.lock().await;
if let Some(sender) = tx.take() {
let _ = sender.send(());
StatusCode::OK
} else {
StatusCode::GONE
}
}