use std::net::SocketAddr;
use axum::{
Json, Router,
body::Bytes,
extract::{ConnectInfo, Query, State},
http::StatusCode,
response::IntoResponse,
routing::post,
};
use serde::Deserialize;
use serde_json::json;
use crate::AppState;
use crate::agent::state::parse_agent_value;
use crate::engine::pty::agent_events::MAX_HOOK_BODY_BYTES;
#[derive(Deserialize)]
pub struct HookQuery {
token: String,
}
pub fn routes() -> Router<AppState> {
Router::new().route("/internal/agent-event", post(post_agent_event))
}
async fn post_agent_event(
State(state): State<AppState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Query(query): Query<HookQuery>,
body: Bytes,
) -> impl IntoResponse {
if !addr.ip().is_loopback() {
return (StatusCode::FORBIDDEN, Json(json!({ "error": "loopback only" })));
}
if body.len() > MAX_HOOK_BODY_BYTES {
return (StatusCode::PAYLOAD_TOO_LARGE, Json(json!({ "error": "body too large" })));
}
let store = state.engines.pty_agent_events();
let Some(session_key) = store.key_for_token(&query.token) else {
return (StatusCode::UNAUTHORIZED, Json(json!({ "error": "unknown token" })));
};
let text = String::from_utf8_lossy(&body);
let Some(snapshot) = parse_agent_value(text.trim()) else {
return (StatusCode::BAD_REQUEST, Json(json!({ "error": "malformed agent value" })));
};
let accepted = store.record(&session_key, snapshot);
(StatusCode::OK, Json(json!({ "ok": true, "accepted": accepted })))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::pty::agent_events::AgentEventStore;
#[test]
fn parse_hook_body_round_trip() {
let snap = parse_agent_value("claude:waiting:decision:PermissionRequest:1719000000.12345")
.expect("payload must parse");
assert_eq!(snap.agent_state.as_str(), "waiting");
assert!(parse_agent_value("").is_none());
assert!(parse_agent_value("nonsense").is_none());
}
#[tokio::test]
async fn store_rejects_unknown_token_and_accepts_known() {
let store = AgentEventStore::new();
assert!(store.key_for_token("bogus").is_none());
let token = store.register("s1");
assert_eq!(store.key_for_token(&token).as_deref(), Some("s1"));
}
}