use std::{convert::Infallible, path::PathBuf, sync::Arc, time::Duration};
use axum::{
Json, Router,
extract::{Path, Query, State},
response::sse::{Event, KeepAlive, Sse},
routing::{get, post},
};
use futures::Stream;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
#[cfg(feature = "swagger-ui")]
use utoipa::OpenApi;
#[cfg(feature = "swagger-ui")]
use utoipa_swagger_ui::SwaggerUi;
use crate::core::compress::CompressionLevel;
use crate::core::hook::HookEvent;
use crate::core::project::ProjectInfo;
use crate::core::session::Session;
use super::error::DaemonError;
use super::rpc::core_ops;
use super::rpc::sessions_legacy_ops as sessions_ops;
use super::state::DaemonState;
pub mod control_routes;
pub use control_routes::{
ctl_auth_session, ctl_connect_session, ctl_list_sessions, ctl_run_session, ctl_stop_session,
};
pub mod claude_config_routes;
pub use claude_config_routes::*;
pub(crate) mod session_start_correlation;
#[cfg(test)]
use session_start_correlation::session_end_pane_still_live;
#[cfg(test)]
use crate::core::session::SessionId;
pub mod coordinator_routes;
pub use coordinator_routes::*;
pub mod web_routes;
pub use web_routes::*;
pub mod origin_guard;
pub use origin_guard::origin_allowed;
pub mod rpc;
pub use rpc::rpc_handler;
pub mod request_deadline;
pub mod deliverable_routes;
pub use deliverable_routes::{
create_deliverable, create_milestone, get_deliverable, get_milestone, list_deliverables,
list_milestones, patch_deliverable, patch_milestone,
};
use super::managed_routes::{
adopt_existing_session, answer_session_decision, decommission_ephemeral_route,
decommission_managed_session, delete_managed_session, fleet_by_project_route, get_attach_cmd,
get_managed_session, get_project_registry_route, get_session_activity, list_managed_sessions,
list_projects_registry_route, patch_project_registry_route, project_status_route, proxy_router,
prune_managed_route, reactivate_managed_session, register_project_registry_route,
rename_managed_session, resume_managed_session, send_to_session, spawn_session,
stop_managed_session, stop_managed_session_runtime,
};
use super::{bus::routes::bus_router, manager::manager_router};
pub mod types;
pub use types::*;
pub fn router(state: Arc<DaemonState>) -> Router {
let router = Router::new()
.route("/health", get(health))
.route("/sessions", get(list_sessions).post(register_session))
.route("/api/v1/sessions/connect", post(connect_session))
.route("/sessions/dead", axum::routing::delete(reap_sessions))
.route("/sessions/discover", post(discover_sessions))
.route("/sessions/{id}", get(get_session).delete(remove_session))
.route("/sessions/{id}/events", get(stream_session_events))
.route("/sessions/{id}/events/poll", get(session_events))
.route("/sessions/{id}/pause", post(pause_session))
.route("/sessions/{id}/resume", post(resume_session))
.route("/sessions/{id}/command", post(send_command))
.route("/sessions/{id}/output", get(get_output))
.route("/sessions/{id}/pane", get(get_output))
.route("/sessions/{id}/pid", axum::routing::patch(set_session_pid))
.route("/projects", get(list_projects).post(register_project))
.route("/projects/current", get(current_project))
.route("/projects/discover", get(discover_projects))
.route(
"/api/v1/projects",
get(list_projects_registry_route).post(register_project_registry_route),
)
.route(
"/api/v1/projects/{name}",
get(get_project_registry_route).patch(patch_project_registry_route),
)
.route("/api/v1/projects/{name}/status", get(project_status_route))
.route("/events", get(stream_events))
.route("/events/poll", get(recent_events))
.route("/hooks", post(ingest_hook))
.route("/breakers", get(breakers))
.route("/optimizer", get(get_optimizer))
.route("/overseer", get(get_overseer))
.route("/llm/chat", post(llm_chat))
.route("/api/v1/sessions/context", get(coordinator_context))
.route(COORDINATOR_CHAT_PATH, post(coordinator_chat))
.route("/api/v1/session-manager/context", get(coordinator_context))
.route("/api/v1/session-manager/chat", post(coordinator_chat))
.route("/web", get(web_chat_page))
.route("/tmux/sessions", get(list_tmux_sessions))
.route("/tmux/sessions/{name}/snapshot", get(tmux_snapshot))
.route("/tmux/adopt", post(adopt_tmux_session))
.route("/claude-config", get(get_claude_config))
.route("/claude-config/apply", post(apply_claude_config))
.route("/claude-config/restart", post(restart_claude_code))
.route(
"/claude-config/checkpoints",
get(list_checkpoints).post(create_checkpoint),
)
.route(
"/claude-config/checkpoints/{id}",
axum::routing::delete(delete_checkpoint),
)
.route("/claude-config/restore", post(restore_checkpoint))
.route("/claude-config/profiles", get(list_profiles))
.route("/claude-config/deploy", post(deploy_profile))
.merge(super::pairing_routes::router())
.merge(super::delegation_routes::router())
.merge(super::builder_slot_routes::router())
.route("/api/v1/doctor", get(doctor))
.route("/api/v1/errors", get(list_errors))
.route("/api/v1/report-bug", post(report_bug_http))
.route("/api/v1/control/sessions", get(ctl_list_sessions))
.route("/api/v1/control/sessions/run", post(ctl_run_session))
.route(
"/api/v1/control/sessions/{id}/connect",
post(ctl_connect_session),
)
.route("/api/v1/control/sessions/{id}/stop", post(ctl_stop_session))
.route("/api/v1/control/sessions/{id}/auth", get(ctl_auth_session))
.route(
"/api/v1/sessions/managed",
post(spawn_session).get(list_managed_sessions),
)
.route(
"/api/v1/sessions/managed/adopt",
post(adopt_existing_session),
)
.route(
"/api/v1/sessions/managed/decommission-ephemeral",
post(decommission_ephemeral_route),
)
.route("/api/v1/sessions/managed/prune", post(prune_managed_route))
.merge(super::managed_routes::reconcile::worktree_routes())
.merge(super::managed_routes::supervisor::supervisor_routes())
.route(
"/api/v1/sessions/managed/fleet",
get(fleet_by_project_route),
)
.route(
"/api/v1/sessions/managed/{id}",
get(get_managed_session)
.delete(stop_managed_session)
.patch(rename_managed_session),
)
.route("/api/v1/sessions/managed/{id}/send", post(send_to_session))
.merge(super::managed_routes::files::router())
.merge(super::managed_routes::provision_status::router())
.merge(super::managed_routes::sync_assets::router())
.merge(super::managed_routes::rebind::router())
.route(
"/api/v1/sessions/managed/{id}/answer",
post(answer_session_decision),
)
.route(
"/api/v1/sessions/managed/{id}/attach-cmd",
get(get_attach_cmd),
)
.route(
"/api/v1/sessions/managed/{id}/activity",
get(get_session_activity),
)
.route(
"/api/v1/sessions/managed/{id}/runtime-stop",
post(stop_managed_session_runtime),
)
.route(
"/api/v1/sessions/managed/{id}/resume",
post(resume_managed_session),
)
.route(
"/api/v1/sessions/managed/{id}/reactivate",
post(reactivate_managed_session),
)
.route(
"/api/v1/sessions/managed/{id}/decommission",
post(decommission_managed_session),
)
.route(
"/api/v1/sessions/managed/{id}/delete",
post(delete_managed_session),
)
.merge(proxy_router())
.merge(manager_router())
.route(
"/api/v1/projects/{name}/deliverables",
get(list_deliverables).post(create_deliverable),
)
.route(
"/api/v1/projects/{name}/deliverables/{id}",
get(get_deliverable).patch(patch_deliverable),
)
.route(
"/api/v1/projects/{name}/milestones",
get(list_milestones).post(create_milestone),
)
.route(
"/api/v1/projects/{name}/milestones/{id}",
get(get_milestone).patch(patch_milestone),
)
.route("/rpc", post(rpc_handler));
#[cfg(feature = "swagger-ui")]
let router = router.merge(
SwaggerUi::new("/api-docs")
.url("/api-docs/openapi.json", super::openapi::ApiDoc::openapi()),
);
router
.merge(bus_router())
.layer(axum::middleware::from_fn(request_deadline::enforce))
.with_state(state)
}
#[utoipa::path(
get,
path = "/health",
tag = "config",
responses((status = 200, description = "Daemon is alive", body = HealthResponse))
)]
pub async fn health(State(state): State<Arc<DaemonState>>) -> Json<HealthResponse> {
Json(core_ops::health(&state).await)
}
#[derive(serde::Deserialize, Default)]
pub struct SessionQuery {
pub project: Option<PathBuf>,
}
#[utoipa::path(
get,
path = "/sessions",
tag = "sessions",
params(("project" = Option<String>, Query, description = "Filter by project path")),
responses((status = 200, description = "Array of managed sessions", body = [Session]))
)]
pub async fn list_sessions(
State(state): State<Arc<DaemonState>>,
Query(query): Query<SessionQuery>,
) -> Json<SessionsResponse> {
Json(sessions_ops::list_sessions(&state, query))
}
#[utoipa::path(
get,
path = "/events/poll",
tag = "events",
responses((status = 200, description = "Recent hook events across all sessions"))
)]
pub async fn recent_events(State(state): State<Arc<DaemonState>>) -> Json<EventsResponse> {
Json(sessions_ops::recent_events(&state))
}
pub async fn stream_events(
State(state): State<Arc<DaemonState>>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let rx = state.event_subscribe();
let stream = BroadcastStream::new(rx).filter_map(|result| match result {
Ok(val) => Some(Ok(Event::default().data(val.to_string()))),
Err(_) => None,
});
Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(Duration::from_secs(15))
.text("ping"),
)
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct RegisterSession {
pub project: String,
#[serde(default)]
#[schema(value_type = Option<String>)]
pub project_path: Option<PathBuf>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
#[schema(value_type = Option<String>)]
pub workdir: Option<PathBuf>,
}
#[utoipa::path(
post,
path = "/sessions",
tag = "sessions",
request_body = RegisterSession,
responses(
(status = 201, description = "Session registered; returns its id and name"),
(status = 422, description = "Spawn requested but `claude` binary or tmux is unavailable"),
(status = 500, description = "tmux command failed while creating the session"),
)
)]
pub async fn register_session(
State(state): State<Arc<DaemonState>>,
Json(body): Json<RegisterSession>,
) -> Result<Json<RegisterSessionResponse>, DaemonError> {
Ok(Json(sessions_ops::register_session(&state, body)?))
}
#[utoipa::path(
post,
path = "/api/v1/sessions/connect",
tag = "sessions",
request_body = RegisterSession,
responses((status = 201, description = "Session registered for connect; returns its id and name"))
)]
pub async fn connect_session(
state: State<Arc<DaemonState>>,
body: Json<RegisterSession>,
) -> Result<Json<RegisterSessionResponse>, DaemonError> {
register_session(state, body).await
}
#[utoipa::path(
get,
path = "/sessions/{id}",
tag = "sessions",
params(("id" = String, Path, description = "Session UUID")),
responses(
(status = 200, description = "Session detail", body = Session),
(status = 400, description = "Malformed session id"),
(status = 404, description = "No session with that id"),
)
)]
pub async fn get_session(
State(state): State<Arc<DaemonState>>,
Path(id): Path<String>,
) -> Result<Json<Session>, DaemonError> {
Ok(Json(sessions_ops::get_session(&state, &id)?))
}
#[utoipa::path(
delete,
path = "/sessions/{id}",
tag = "sessions",
params(("id" = String, Path, description = "Session UUID or friendly tmux name")),
responses(
(status = 200, description = "Session removed and tmux host killed"),
(status = 404, description = "No session with that id or name"),
)
)]
pub async fn remove_session(
State(state): State<Arc<DaemonState>>,
Path(id): Path<String>,
) -> Result<Json<RemoveSessionResponse>, DaemonError> {
Ok(Json(sessions_ops::remove_session(&state, &id).await?))
}
#[utoipa::path(
delete,
path = "/sessions/dead",
tag = "sessions",
responses((status = 200, description = "Dead sessions reaped; returns the removed count"))
)]
pub async fn reap_sessions(State(state): State<Arc<DaemonState>>) -> Json<ReapResponse> {
Json(sessions_ops::reap_sessions(&state))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct SetPidRequest {
pub pid: u32,
}
#[utoipa::path(
patch,
path = "/sessions/{id}/pid",
tag = "sessions",
params(("id" = String, Path, description = "Session UUID")),
request_body = SetPidRequest,
responses(
(status = 200, description = "PID recorded for the session"),
(status = 404, description = "No session with that id"),
)
)]
pub async fn set_session_pid(
State(state): State<Arc<DaemonState>>,
Path(id): Path<String>,
Json(body): Json<SetPidRequest>,
) -> Result<Json<SetPidResponse>, DaemonError> {
Ok(Json(sessions_ops::set_session_pid(&state, &id, body.pid)?))
}
#[utoipa::path(
post,
path = "/sessions/discover",
tag = "sessions",
responses((status = 200, description = "tmux sessions running Claude Code, newly registered"))
)]
pub async fn discover_sessions(State(state): State<Arc<DaemonState>>) -> Json<DiscoverResponse> {
Json(sessions_ops::discover_sessions(&state).await)
}
#[utoipa::path(
get,
path = "/sessions/{id}/events/poll",
tag = "events",
params(("id" = String, Path, description = "Session UUID")),
responses(
(status = 200, description = "Recent hook events for the session"),
(status = 404, description = "No session with that id"),
)
)]
pub async fn session_events(
State(state): State<Arc<DaemonState>>,
Path(id): Path<String>,
) -> Result<Json<EventsResponse>, DaemonError> {
Ok(Json(sessions_ops::session_events(&state, &id)?))
}
pub async fn stream_session_events(
Path(id): Path<String>,
State(state): State<Arc<DaemonState>>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let rx = state.event_subscribe();
let stream = BroadcastStream::new(rx).filter_map(move |result| match result {
Ok(val) => {
if val.to_string().contains(&id) {
Some(Ok(Event::default().data(val.to_string())))
} else {
None
}
}
Err(_) => None,
});
Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(Duration::from_secs(15))
.text("ping"),
)
}
#[derive(serde::Deserialize, utoipa::ToSchema, Default)]
pub struct PauseRequest {
#[serde(default)]
pub summary: Option<String>,
}
#[utoipa::path(
post,
path = "/sessions/{id}/pause",
tag = "sessions",
params(("id" = String, Path, description = "Session UUID or friendly name")),
request_body = PauseRequest,
responses(
(status = 200, description = "Session paused; returns the pause summary"),
(status = 404, description = "No session with that id or name"),
)
)]
pub async fn pause_session(
State(state): State<Arc<DaemonState>>,
Path(id): Path<String>,
Json(body): Json<PauseRequest>,
) -> Result<Json<PauseResponse>, DaemonError> {
Ok(Json(sessions_ops::pause_session(
&state,
&id,
body.summary,
)?))
}
#[utoipa::path(
post,
path = "/sessions/{id}/resume",
tag = "sessions",
params(("id" = String, Path, description = "Session UUID or friendly name")),
responses(
(status = 200, description = "Session resumed"),
(status = 404, description = "No session with that id or name"),
(status = 409, description = "Session is not paused"),
)
)]
pub async fn resume_session(
State(state): State<Arc<DaemonState>>,
Path(id): Path<String>,
) -> Result<Json<ResumeResponse>, DaemonError> {
Ok(Json(sessions_ops::resume_session(&state, &id)?))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct CommandRequest {
pub command: String,
}
#[derive(serde::Deserialize, Default)]
pub struct CommandQuery {
#[serde(default)]
pub compress: Option<CompressionLevel>,
}
#[utoipa::path(
post,
path = "/sessions/{id}/command",
tag = "sessions",
params(
("id" = String, Path, description = "Session UUID or friendly name"),
("compress" = Option<String>, Query, description = "Compression level: off, trim, summarise, caveman"),
),
request_body = CommandRequest,
responses(
(status = 200, description = "Command sent; returns captured pane output"),
(status = 404, description = "No session with that id or name"),
(status = 409, description = "Session is stopped"),
)
)]
pub async fn send_command(
State(state): State<Arc<DaemonState>>,
Path(id): Path<String>,
Query(query): Query<CommandQuery>,
Json(body): Json<CommandRequest>,
) -> Result<Json<CommandResponse>, DaemonError> {
Ok(Json(
sessions_ops::send_command(&state, &id, &body.command, query.compress).await?,
))
}
#[derive(serde::Deserialize, Default)]
pub struct OutputQuery {
#[serde(default)]
pub lines: Option<u32>,
#[serde(default)]
pub compress: Option<CompressionLevel>,
}
#[utoipa::path(
get,
path = "/sessions/{id}/output",
tag = "sessions",
params(
("id" = String, Path, description = "Session UUID or friendly name"),
("lines" = Option<u32>, Query, description = "Trailing lines to capture (default 50)"),
("compress" = Option<String>, Query, description = "Compression level: off, trim, summarise, caveman"),
),
responses(
(status = 200, description = "Captured pane output"),
(status = 404, description = "No session with that id or name"),
)
)]
pub async fn get_output(
State(state): State<Arc<DaemonState>>,
Path(id): Path<String>,
Query(query): Query<OutputQuery>,
) -> Result<Json<OutputResponse>, DaemonError> {
Ok(Json(sessions_ops::get_output(
&state,
&id,
query.lines,
query.compress,
)?))
}
#[utoipa::path(
get,
path = "/breakers",
tag = "config",
responses((status = 200, description = "Array of per-agent circuit-breaker states"))
)]
pub async fn breakers(State(state): State<Arc<DaemonState>>) -> Json<BreakersResponse> {
Json(core_ops::breakers(&state))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct HookPost {
pub session_id: String,
#[schema(value_type = String)]
pub event: HookEvent,
#[serde(default)]
#[schema(value_type = Object)]
pub payload: serde_json::Value,
}
#[utoipa::path(
post,
path = "/hooks",
tag = "internal",
request_body = HookPost,
responses(
(status = 200, description = "Hook event accepted"),
(status = 400, description = "Unknown event name or malformed session id"),
(status = 403, description = "Overseer blocked the event"),
)
)]
pub async fn ingest_hook(
State(state): State<Arc<DaemonState>>,
Json(post): Json<HookPost>,
) -> Result<Json<HookAcceptedResponse>, DaemonError> {
Ok(Json(sessions_ops::ingest_hook(&state, post).await?))
}
#[utoipa::path(
get,
path = "/overseer",
tag = "config",
responses((status = 200, description = "Overseer enabled flag and handler type"))
)]
pub async fn get_overseer(State(state): State<Arc<DaemonState>>) -> Json<OverseerResponse> {
Json(core_ops::overseer(&state))
}
#[utoipa::path(
post,
path = "/llm/chat",
tag = "config",
request_body = LlmChatRequest,
responses(
(status = 200, description = "Assistant reply and updated history"),
(status = 503, description = "LLM chat is not configured on this daemon"),
)
)]
pub async fn llm_chat(
State(state): State<Arc<DaemonState>>,
Json(body): Json<LlmChatRequest>,
) -> Result<Json<LlmChatResponse>, DaemonError> {
Ok(Json(core_ops::llm_chat(&state, body).await?))
}
#[utoipa::path(
get,
path = "/optimizer",
tag = "config",
responses((status = 200, description = "Current token-use optimizer configuration"))
)]
pub async fn get_optimizer(State(state): State<Arc<DaemonState>>) -> Json<OptimizerResponse> {
Json(core_ops::optimizer(&state))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct RegisterProject {
#[schema(value_type = String)]
pub path: PathBuf,
}
#[utoipa::path(
post,
path = "/projects",
tag = "projects",
request_body = RegisterProject,
responses((status = 201, description = "Project registered", body = ProjectInfo))
)]
pub async fn register_project(
State(state): State<Arc<DaemonState>>,
Json(body): Json<RegisterProject>,
) -> Json<ProjectInfo> {
Json(crate::daemon::rpc::registry::projects::register_project_op(
&state, body.path,
))
}
#[utoipa::path(
get,
path = "/projects",
tag = "projects",
responses((status = 200, description = "Array of registered projects", body = [ProjectInfo]))
)]
pub async fn list_projects(State(state): State<Arc<DaemonState>>) -> Json<ProjectsResponse> {
Json(crate::daemon::rpc::registry::projects::list_projects_op(
&state,
))
}
#[derive(serde::Deserialize)]
pub struct CurrentProjectQuery {
pub path: PathBuf,
}
#[utoipa::path(
get,
path = "/projects/current",
tag = "projects",
params(("path" = String, Query, description = "Directory whose project to resolve")),
responses(
(status = 200, description = "The project registered for the path", body = ProjectInfo),
(status = 404, description = "Path is not a registered project"),
)
)]
pub async fn current_project(
State(state): State<Arc<DaemonState>>,
Query(query): Query<CurrentProjectQuery>,
) -> Result<Json<ProjectInfo>, DaemonError> {
Ok(Json(
crate::daemon::rpc::registry::projects::current_project_op(&state, &query.path)?,
))
}
#[utoipa::path(
get,
path = "/projects/discover",
tag = "projects",
responses((status = 200, description = "Projects discovered from Claude Code config"))
)]
pub async fn discover_projects(
State(_state): State<Arc<DaemonState>>,
) -> Json<DiscoverProjectsResponse> {
Json(crate::daemon::rpc::registry::projects::discover_projects_op())
}
#[utoipa::path(
get,
path = "/tmux/sessions",
tag = "tmux",
responses((status = 200, description = "All tmux sessions with origin labels"))
)]
pub async fn list_tmux_sessions(
State(state): State<Arc<DaemonState>>,
) -> Json<TmuxSessionsResponse> {
Json(core_ops::list_tmux_sessions(&state))
}
#[utoipa::path(
get,
path = "/tmux/sessions/{name}/snapshot",
tag = "tmux",
params(("name" = String, Path, description = "tmux session name")),
responses(
(status = 200, description = "Session snapshot"),
(status = 404, description = "Session not found or tmux unavailable"),
)
)]
pub async fn tmux_snapshot(
State(state): State<Arc<DaemonState>>,
Path(name): Path<String>,
) -> Result<Json<TmuxSnapshotResponse>, DaemonError> {
Ok(Json(core_ops::tmux_snapshot(&state, &name)?))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct AdoptRequest {
pub session: String,
}
#[utoipa::path(
post,
path = "/tmux/adopt",
tag = "tmux",
request_body = AdoptRequest,
responses(
(status = 200, description = "Session adopted; returns its captured state"),
(status = 404, description = "Session not found or tmux unavailable"),
)
)]
pub async fn adopt_tmux_session(
State(state): State<Arc<DaemonState>>,
Json(body): Json<AdoptRequest>,
) -> Result<Json<AdoptResponse>, DaemonError> {
Ok(Json(core_ops::adopt_tmux(&state, body)?))
}
#[utoipa::path(
post,
path = "/pair/request",
tag = "config",
responses((status = 200, description = "A one-time pairing code and its TTL"))
)]
pub async fn pair_request(
State(state): State<Arc<DaemonState>>,
) -> Json<super::services::PairCode> {
Json(crate::daemon::rpc::registry::pairing::request_op(&state))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct PairConfirmRequest {
pub code: String,
pub chat_id: i64,
}
#[utoipa::path(
post,
path = "/pair/confirm",
tag = "config",
request_body = PairConfirmRequest,
responses((status = 200, description = "Pairing result (success flag and chat id or error)"))
)]
pub async fn pair_confirm(
State(state): State<Arc<DaemonState>>,
Json(body): Json<PairConfirmRequest>,
) -> Json<PairConfirmResponse> {
Json(crate::daemon::rpc::registry::pairing::confirm_op(
&state, &body,
))
}
#[utoipa::path(
get,
path = "/pair/status",
tag = "config",
responses((status = 200, description = "Pairing status (paired flag and chat id)"))
)]
pub async fn pair_status(
State(state): State<Arc<DaemonState>>,
) -> Json<super::services::PairStatus> {
Json(crate::daemon::rpc::registry::pairing::status_op(&state))
}
#[utoipa::path(
post,
path = "/pair/reset",
tag = "config",
responses((status = 200, description = "Pairing cleared"))
)]
pub async fn pair_reset(State(state): State<Arc<DaemonState>>) -> Json<PairResetResponse> {
Json(crate::daemon::rpc::registry::pairing::reset_op(&state))
}
#[derive(serde::Deserialize, Default)]
pub struct DoctorQuery {
pub project: Option<PathBuf>,
}
#[utoipa::path(
get,
path = "/api/v1/doctor",
tag = "config",
params(("project" = Option<String>, Query, description = "Project directory to scope the instruction probe to")),
responses((status = 200, description = "Full diagnostic report"))
)]
pub async fn doctor(
State(state): State<Arc<DaemonState>>,
Query(query): Query<DoctorQuery>,
) -> Json<crate::core::doctor::DoctorReport> {
Json(core_ops::doctor(&state, query).await)
}
#[derive(serde::Deserialize, Default)]
pub struct ErrorsQuery {
#[serde(default)]
pub limit: Option<u64>,
}
#[utoipa::path(
get,
path = "/api/v1/errors",
tag = "bug-reporting",
params(("limit" = Option<u64>, Query, description = "Max errors to return (default 20, max 100)")),
responses((status = 200, description = "Deduplicated error list"))
)]
pub async fn list_errors(
State(state): State<Arc<DaemonState>>,
Query(query): Query<ErrorsQuery>,
) -> Json<ErrorsResponse> {
Json(core_ops::list_errors(&state, query))
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
pub struct ReportBugApiRequest {
pub fingerprint: String,
#[serde(default)]
pub confirm: bool,
}
pub use super::rpc::core_ops::to_wire_preview;
#[utoipa::path(
post,
path = "/api/v1/report-bug",
tag = "bug-reporting",
request_body = ReportBugApiRequest,
responses(
(status = 200, description = "Filing result or graceful failure; always 200"),
)
)]
pub async fn report_bug_http(
State(state): State<Arc<DaemonState>>,
Json(body): Json<ReportBugApiRequest>,
) -> Json<ReportBugHttpResponse> {
Json(core_ops::report_bug(&state, body).await)
}
#[cfg(test)]
pub(crate) use super::rpc::sessions_legacy_ops::{apply_compression, compression_level_label};
#[cfg(test)]
#[path = "api_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "coordinator_sm_tests.rs"]
mod coordinator_sm_tests;