mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 09:01:17 +00:00
132 lines
3.8 KiB
Rust
132 lines
3.8 KiB
Rust
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use axum::extract::State;
|
|
use axum::http::{header, HeaderMap, StatusCode};
|
|
use axum::response::sse::KeepAlive;
|
|
use axum::response::{IntoResponse, Response, Sse};
|
|
use axum::routing::{get, post};
|
|
use axum::{Json, Router};
|
|
use serde::Serialize;
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::process::{AdapterError, AdapterRuntime, PostOutcome};
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct HealthResponse {
|
|
ok: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct Problem {
|
|
r#type: &'static str,
|
|
title: &'static str,
|
|
status: u16,
|
|
detail: String,
|
|
}
|
|
|
|
pub fn build_router(runtime: Arc<AdapterRuntime>) -> Router {
|
|
Router::new()
|
|
.route("/v1/health", get(get_health))
|
|
.route("/v1/rpc", post(post_rpc).get(get_rpc).delete(delete_rpc))
|
|
.with_state(runtime)
|
|
}
|
|
|
|
async fn get_health() -> Json<HealthResponse> {
|
|
Json(HealthResponse { ok: true })
|
|
}
|
|
|
|
async fn post_rpc(
|
|
State(runtime): State<Arc<AdapterRuntime>>,
|
|
headers: HeaderMap,
|
|
Json(payload): Json<Value>,
|
|
) -> Response {
|
|
if !is_json_content_type(&headers) {
|
|
return problem(
|
|
StatusCode::UNSUPPORTED_MEDIA_TYPE,
|
|
"unsupported_media_type",
|
|
"content-type must be application/json",
|
|
);
|
|
}
|
|
|
|
match runtime.post(payload).await {
|
|
Ok(PostOutcome::Response(value)) => (StatusCode::OK, Json(value)).into_response(),
|
|
Ok(PostOutcome::Accepted) => StatusCode::ACCEPTED.into_response(),
|
|
Err(err) => map_error(err),
|
|
}
|
|
}
|
|
|
|
async fn get_rpc(
|
|
State(runtime): State<Arc<AdapterRuntime>>,
|
|
headers: HeaderMap,
|
|
) -> impl IntoResponse {
|
|
let last_event_id = headers
|
|
.get("last-event-id")
|
|
.and_then(|value| value.to_str().ok())
|
|
.and_then(|value| value.parse::<u64>().ok());
|
|
|
|
let stream = runtime.clone().sse_stream(last_event_id).await;
|
|
Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(15)))
|
|
}
|
|
|
|
async fn delete_rpc() -> StatusCode {
|
|
StatusCode::NO_CONTENT
|
|
}
|
|
|
|
fn is_json_content_type(headers: &HeaderMap) -> bool {
|
|
headers
|
|
.get(header::CONTENT_TYPE)
|
|
.and_then(|value| value.to_str().ok())
|
|
.map(|value| value.starts_with("application/json"))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn map_error(err: AdapterError) -> Response {
|
|
match err {
|
|
AdapterError::InvalidEnvelope => problem(
|
|
StatusCode::BAD_REQUEST,
|
|
"invalid_envelope",
|
|
"request body must be a JSON-RPC object",
|
|
),
|
|
AdapterError::Timeout => problem(
|
|
StatusCode::GATEWAY_TIMEOUT,
|
|
"timeout",
|
|
"timed out waiting for agent response",
|
|
),
|
|
AdapterError::Write(write) => problem(
|
|
StatusCode::BAD_GATEWAY,
|
|
"write_failed",
|
|
&format!("failed writing to agent stdin: {write}"),
|
|
),
|
|
AdapterError::Serialize(ser) => problem(
|
|
StatusCode::BAD_REQUEST,
|
|
"serialize_failed",
|
|
&format!("failed to serialize JSON payload: {ser}"),
|
|
),
|
|
AdapterError::Spawn(spawn) => problem(
|
|
StatusCode::BAD_GATEWAY,
|
|
"spawn_failed",
|
|
&format!("failed to start agent process: {spawn}"),
|
|
),
|
|
AdapterError::MissingStdin | AdapterError::MissingStdout | AdapterError::MissingStderr => {
|
|
problem(
|
|
StatusCode::BAD_GATEWAY,
|
|
"io_setup_failed",
|
|
"agent subprocess pipes were not available",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn problem(status: StatusCode, title: &'static str, detail: &str) -> Response {
|
|
(
|
|
status,
|
|
Json(json!(Problem {
|
|
r#type: "about:blank",
|
|
title,
|
|
status: status.as_u16(),
|
|
detail: detail.to_string(),
|
|
})),
|
|
)
|
|
.into_response()
|
|
}
|