Skip to main content

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}