prospero_api/lib.rs
1//! HTTP API and embedded web dashboard for Prospero.
2//!
3//! Turns a [`FleetManager`] into a control surface: REST endpoints for the
4//! fleet, repos, and agents; a Server-Sent-Events stream per agent
5//! (replay-then-tail); and a static dashboard. The CLI and the browser both
6//! talk to this one surface.
7
8pub mod dashboard;
9pub mod dashboard_v2;
10pub mod dto;
11pub mod error;
12pub mod handlers;
13pub mod sse;
14
15use std::sync::Arc;
16
17use axum::Router;
18use axum::routing::{delete, get, post, put};
19use prospero_core::bus::EventBus;
20use prospero_core::store::Store;
21use prospero_core::{FleetAdmin, FleetProvider};
22
23/// Shared application state handed to every handler. Backend-agnostic (#76):
24/// the control plane is a `FleetProvider`, the workspace-registry plane an
25/// optional `FleetAdmin` (`None` under k8s → those routes 405), and
26/// observability (history/SSE) reads the shared `Store`/`EventBus` directly.
27#[derive(Clone)]
28pub struct AppState {
29 /// The fleet control plane (ensure/stop/restart/snapshot/readiness/metrics/
30 /// remove_agent/send_input). `LocalFleet` or `K8sFleet`.
31 pub fleet: Arc<dyn FleetProvider>,
32 /// The workspace-registry/config plane. `Some` for local; `None` for k8s.
33 pub admin: Option<Arc<dyn FleetAdmin>>,
34 /// Shared durable event store — agent history reads route here.
35 pub store: Arc<dyn Store>,
36 /// Shared event bus — SSE subscribe routes here.
37 pub bus: Arc<dyn EventBus>,
38}
39
40/// Build the application router over the backend seams (constructed once, at the
41/// daemon's composition edge — see `prospero-daemon`'s `main.rs`).
42pub fn router(
43 fleet: Arc<dyn FleetProvider>,
44 admin: Option<Arc<dyn FleetAdmin>>,
45 store: Arc<dyn Store>,
46 bus: Arc<dyn EventBus>,
47) -> Router {
48 let state = AppState {
49 fleet,
50 admin,
51 store,
52 bus,
53 };
54 Router::new()
55 // Dashboard v2 (Dioxus/WASM, #97) is the default surface (#191). The
56 // scaffold parked it at `/v2` so `/` stayed untouched while epic #95
57 // landed; that transition is done, so `/` serves it now and `/v2`
58 // remains a permanent alias — the bundle's own asset URLs are absolute
59 // `/v2/...`, and existing links and bookmarks point there.
60 //
61 // The catch-all covers the JS glue, the .wasm, the stylesheet, and
62 // wasm-bindgen's hashed `snippets/` tree.
63 .route("/", get(dashboard_v2::index))
64 .route("/v2", get(dashboard_v2::index))
65 .route("/v2/{*path}", get(dashboard_v2::asset))
66 // Dashboard v1, deprecated (#191). Kept reachable — and only reachable
67 // under `/v1` — so an operator who hits a v2 regression has somewhere to
68 // land. Deleting it is a follow-up, once v2 has a release of real use.
69 .route("/v1", get(dashboard::index))
70 .route("/v1/app.js", get(dashboard::app_js))
71 .route("/healthz", get(handlers::healthz))
72 .route("/readyz", get(handlers::readyz))
73 .route("/api/metrics", get(handlers::get_metrics))
74 .route("/api/capabilities", get(handlers::get_capabilities))
75 // Fleet + workspaces.
76 .route("/api/fleet", get(handlers::get_fleet))
77 .route("/api/usage", get(handlers::get_usage))
78 .route(
79 "/api/workspaces",
80 get(handlers::get_workspaces).post(handlers::add_workspace),
81 )
82 .route("/api/workspaces/{name}", delete(handlers::delete_workspace))
83 .route(
84 "/api/workspaces/{name}/config",
85 put(handlers::set_workspace_config),
86 )
87 .route(
88 "/api/workspaces/{workspace}/agents",
89 get(handlers::get_workspace_agents).post(handlers::spawn_agent),
90 )
91 // Agents.
92 .route(
93 "/api/agents/{id}",
94 get(handlers::get_agent).delete(handlers::rm_agent),
95 )
96 .route("/api/agents/{id}/events", get(handlers::get_agent_events))
97 .route("/api/agents/{id}/stream", get(sse::agent_stream))
98 .route("/api/agents/{id}/kill", post(handlers::kill_agent))
99 .route("/api/agents/{id}/respawn", post(handlers::respawn_agent))
100 .route("/api/agents/{id}/input", post(handlers::agent_input))
101 .route(
102 "/api/agents/{id}/end-input",
103 post(handlers::agent_end_input),
104 )
105 .with_state(state)
106}