use crate::server::AppState;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde::{Deserialize, Serialize};
pub async fn health(State(st): State<AppState>) -> impl IntoResponse {
let providers: Vec<ProviderHealth> = st
.runtime
.validate_providers()
.into_iter()
.map(|(id, res)| ProviderHealth {
id,
status: if res.is_ok() { "ok" } else { "error" }.to_string(),
detail: res.err().map(|e| e.to_string()),
})
.collect();
let workspace_ok = st.runtime.workspace_writable();
let all_ok = providers.iter().all(|p| p.status == "ok") && workspace_ok;
let body = HealthResponse {
status: if all_ok { "ok" } else { "degraded" }.to_string(),
version: st.version.to_string(),
uptime_secs: st.uptime_secs(),
tools: st.runtime.tool_count(),
presets: st.runtime.preset_count(),
workspace_ok,
providers,
};
Json(body)
}
#[derive(Serialize)]
pub struct HealthResponse {
status: String,
version: String,
uptime_secs: u64,
tools: usize,
presets: usize,
workspace_ok: bool,
providers: Vec<ProviderHealth>,
}
#[derive(Serialize)]
pub struct ProviderHealth {
id: String,
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
}
pub async fn version(State(st): State<AppState>) -> impl IntoResponse {
Json(
serde_json::json!({ "daemon": format!("harnessd/{}", st.version), "protocol": harness_proto::PROTOCOL_VERSION }),
)
}
pub async fn list_sessions(State(st): State<AppState>) -> Result<Json<Vec<SessionDto>>, ApiError> {
let sessions = st.runtime.store().list_sessions()?;
Ok(Json(sessions.into_iter().map(SessionDto::from).collect()))
}
#[derive(Serialize)]
pub struct SessionDto {
id: String,
title: String,
provider: String,
tool_policy: String,
created_at: String,
last_seq: u64,
last_activity: String,
}
impl From<harness_core::store::SessionSummary> for SessionDto {
fn from(s: harness_core::store::SessionSummary) -> Self {
SessionDto {
id: s.id,
title: s.title,
provider: s.provider,
tool_policy: s.tool_policy,
created_at: s.created_at,
last_seq: s.last_seq,
last_activity: s.last_activity,
}
}
}
#[derive(Deserialize)]
pub struct CreateSessionBody {
#[serde(default = "default_title")]
title: String,
#[serde(default)]
provider: Option<String>,
#[serde(default)]
preset: Option<String>,
}
fn default_title() -> String {
"New session".to_string()
}
pub async fn create_session(
State(st): State<AppState>,
Json(body): Json<CreateSessionBody>,
) -> Result<Json<serde_json::Value>, ApiError> {
let id = st.runtime.create_session_with_preset(
&body.title,
body.provider.as_deref(),
body.preset.as_deref(),
)?;
Ok(Json(serde_json::json!({ "id": id })))
}
#[derive(Deserialize)]
pub struct EventsQuery {
#[serde(default)]
from_seq: u64,
}
pub async fn session_events(
State(st): State<AppState>,
Path(id): Path<String>,
Query(q): Query<EventsQuery>,
) -> Result<Json<Vec<harness_proto::EventEnvelope>>, ApiError> {
let events = st.runtime.store().events_since(&id, q.from_seq)?;
Ok(Json(events))
}
pub async fn list_providers(State(st): State<AppState>) -> impl IntoResponse {
let main = st.runtime.config().roles.main.clone();
let providers: Vec<_> = st
.runtime
.all_providers()
.iter()
.map(|p| {
serde_json::json!({
"id": p.id,
"kind": p.kind,
"model": p.model,
"enabled": p.enabled,
"has_key": st.runtime.provider_has_key(p),
"is_main": main.as_deref() == Some(p.id.as_str()),
})
})
.collect();
Json(providers)
}
#[derive(Deserialize)]
pub struct AddProviderBody {
id: String,
kind: harness_core::config::ProviderKind,
#[serde(default)]
model: String,
#[serde(default)]
base_url: Option<String>,
#[serde(default)]
command: Option<String>,
#[serde(default)]
api_key: Option<String>,
}
pub async fn add_provider(
State(st): State<AppState>,
Json(body): Json<AddProviderBody>,
) -> Result<Json<serde_json::Value>, ApiError> {
let cfg = harness_core::config::ProviderConfig {
id: body.id.clone(),
kind: body.kind,
model: body.model,
api_key_env: None,
base_url: body.base_url,
command: body.command,
extra_args: Vec::new(),
enabled: true,
};
st.runtime.add_or_update_provider(cfg, body.api_key)?;
Ok(Json(serde_json::json!({ "id": body.id, "ok": true })))
}
#[derive(Deserialize)]
pub struct SetKeyBody {
api_key: String,
}
pub async fn set_provider_key(
State(st): State<AppState>,
Path(id): Path<String>,
Json(body): Json<SetKeyBody>,
) -> Result<Json<serde_json::Value>, ApiError> {
st.runtime.set_provider_key(&id, &body.api_key)?;
Ok(Json(serde_json::json!({ "id": id, "ok": true })))
}
pub async fn list_presets(State(st): State<AppState>) -> impl IntoResponse {
let presets: Vec<_> = st
.runtime
.presets()
.iter()
.map(|p| {
serde_json::json!({
"id": p.meta.id,
"name": p.meta.name,
"description": p.meta.description,
"scheduled": p.schedule.enabled,
})
})
.collect();
Json(presets)
}
pub async fn preset_requirements(
State(st): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>, ApiError> {
let preset = st
.runtime
.preset(&id)
.ok_or_else(|| harness_core::CoreError::Config(format!("unknown preset `{id}`")))?;
let reqs = preset.requirements(&st.runtime.available_namespaces());
Ok(Json(serde_json::json!({
"id": preset.meta.id,
"name": preset.meta.name,
"requirements": reqs,
})))
}
pub async fn pair_start(State(st): State<AppState>) -> Result<Json<serde_json::Value>, ApiError> {
let code = crate::auth::generate_code();
let expires_at = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5))
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default();
st.runtime
.store()
.add_pairing_code(&crate::auth::hash(&code), &expires_at)?;
Ok(Json(serde_json::json!({
"code": code,
"expires_at": expires_at,
})))
}
#[derive(Deserialize)]
pub struct PairCompleteBody {
code: String,
#[serde(default = "default_device_name")]
device_name: String,
}
fn default_device_name() -> String {
"device".to_string()
}
pub async fn pair_complete(
State(st): State<AppState>,
Json(body): Json<PairCompleteBody>,
) -> Result<Json<serde_json::Value>, ApiError> {
let now = time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default();
let ok = st
.runtime
.store()
.consume_pairing_code(&crate::auth::hash(&body.code), &now)?;
if !ok {
return Err(
harness_core::CoreError::Config("invalid or expired pairing code".into()).into(),
);
}
let token = crate::auth::random_hex(32);
st.runtime
.store()
.add_device(&crate::auth::hash(&token), &body.device_name)?;
Ok(Json(serde_json::json!({ "device_token": token })))
}
pub struct ApiError(harness_core::CoreError);
impl From<harness_core::CoreError> for ApiError {
fn from(e: harness_core::CoreError) -> Self {
ApiError(e)
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
use harness_core::CoreError::*;
let status = match &self.0 {
UnknownSession(_) | UnknownProvider(_) => StatusCode::NOT_FOUND,
Config(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
let body =
Json(serde_json::json!({ "code": self.0.code(), "message": self.0.to_string() }));
(status, body).into_response()
}
}