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 auth;
9pub mod automations;
10pub mod dashboard;
11pub mod dto;
12pub mod error;
13pub mod handlers;
14pub mod mcp;
15pub mod openapi;
16pub mod sse;
17
18use std::sync::Arc;
19
20use axum::Router;
21use axum::routing::{delete, get, post, put};
22use prospero_core::bus::EventBus;
23use prospero_core::store::Store;
24use prospero_core::{FleetAdmin, FleetProvider};
25
26/// Shared application state handed to every handler. Backend-agnostic (#76):
27/// the control plane is a `FleetProvider`, the workspace-registry plane an
28/// optional `FleetAdmin` (`None` under k8s → those routes 405), and
29/// observability (history/SSE) reads the shared `Store`/`EventBus` directly.
30#[derive(Clone)]
31pub struct AppState {
32    /// The fleet control plane (ensure/stop/restart/snapshot/readiness/metrics/
33    /// remove_agent/send_input). `LocalFleet` or `K8sFleet`.
34    pub fleet: Arc<dyn FleetProvider>,
35    /// The workspace-registry/config plane. `Some` for local; `None` for k8s.
36    pub admin: Option<Arc<dyn FleetAdmin>>,
37    /// Shared durable event store — agent history reads route here.
38    pub store: Arc<dyn Store>,
39    /// Shared event bus — SSE subscribe routes here.
40    pub bus: Arc<dyn EventBus>,
41    /// Inbound auth configuration (#2).
42    pub auth: Arc<auth::AuthState>,
43    /// The automation runtime (#220). `None` when the backend has no shared
44    /// config store to hold automations in — those routes then 405 rather
45    /// than pretending to schedule something nothing will fire.
46    pub automations: Option<Arc<prospero_core::automation::AutomationEngine>>,
47}
48
49/// Build the application router over the backend seams (constructed once, at the
50/// daemon's composition edge — see `prospero-daemon`'s `main.rs`).
51pub fn router_with_auth(
52    fleet: Arc<dyn FleetProvider>,
53    admin: Option<Arc<dyn FleetAdmin>>,
54    store: Arc<dyn Store>,
55    bus: Arc<dyn EventBus>,
56    automations: Option<Arc<prospero_core::automation::AutomationEngine>>,
57    auth: auth::AuthState,
58) -> Router {
59    let auth = Arc::new(auth);
60    // #218: the MCP service holds its own handles to the same seams — the
61    // state below moves them.
62    let mcp_service = mcp::service(fleet.clone(), store.clone());
63    let state = AppState {
64        fleet,
65        admin,
66        store,
67        bus,
68        auth: auth.clone(),
69        automations,
70    };
71    Router::new()
72        // The dashboard (Dioxus/WASM, #97) is the only UI: the document at
73        // `/`, its files under `/assets/`. It shared the server with a
74        // vanilla-JS page until #197 removed that, and the version-suffixed
75        // `/v1` and `/v2` paths went with it.
76        //
77        // Assets sit under a prefix rather than at the root so the bundle
78        // cannot shadow the API namespace. The catch-all covers the JS glue,
79        // the .wasm, the stylesheet, and wasm-bindgen's hashed `snippets/`
80        // tree, whose filenames change with every dependency bump.
81        .route("/", get(dashboard::index))
82        .route("/assets/{*path}", get(dashboard::asset))
83        .route("/healthz", get(handlers::healthz))
84        .route("/readyz", get(handlers::readyz))
85        .route(
86            "/api/session",
87            get(auth::handlers::get_session)
88                .post(auth::handlers::post_session)
89                .delete(auth::handlers::delete_session),
90        )
91        // #222: the machine-readable description of everything below. Open,
92        // like the guide it mirrors: it describes the shape of the API, never
93        // any fleet data, and a client needs it before it has a credential.
94        .route("/api/openapi.json", get(openapi::get_openapi))
95        .route("/api/metrics", get(handlers::get_metrics))
96        .route("/api/capabilities", get(handlers::get_capabilities))
97        // Fleet + workspaces.
98        .route("/api/fleet", get(handlers::get_fleet))
99        .route("/api/fleet/stream", get(sse::fleet_stream))
100        // #218: the fleet as an MCP server, behind the same scope check as the
101        // REST routes (see `auth::required_access`).
102        .nest_service("/mcp", mcp_service)
103        .route("/api/usage", get(handlers::get_usage))
104        .route(
105            "/api/workspaces",
106            get(handlers::get_workspaces).post(handlers::add_workspace),
107        )
108        .route("/api/workspaces/{name}", delete(handlers::delete_workspace))
109        .route(
110            "/api/workspaces/{name}/config",
111            put(handlers::set_workspace_config),
112        )
113        .route(
114            "/api/workspaces/{workspace}/agents",
115            get(handlers::get_workspace_agents).post(handlers::spawn_agent),
116        )
117        // Agents.
118        .route(
119            "/api/agents/{id}",
120            get(handlers::get_agent).delete(handlers::rm_agent),
121        )
122        .route("/api/agents/{id}/events", get(handlers::get_agent_events))
123        .route("/api/agents/{id}/stream", get(sse::agent_stream))
124        .route("/api/agents/{id}/kill", post(handlers::kill_agent))
125        .route("/api/agents/{id}/respawn", post(handlers::respawn_agent))
126        .route("/api/agents/{id}/input", post(handlers::agent_input))
127        .route(
128            "/api/agents/{id}/end-input",
129            post(handlers::agent_end_input),
130        )
131        // Automations (#220).
132        .route(
133            "/api/automations",
134            get(automations::list_automations).post(automations::create_automation),
135        )
136        .route(
137            "/api/automations/{id}",
138            delete(automations::delete_automation),
139        )
140        .route(
141            "/api/automations/{id}/enabled",
142            put(automations::set_enabled),
143        )
144        .route("/api/automations/{id}/runs", get(automations::list_runs))
145        .route("/api/automations/{id}/run", post(automations::run_now))
146        .route("/api/automations/{id}/trigger", post(automations::trigger))
147        .route_layer(axum::middleware::from_fn_with_state(auth, auth::middleware))
148        .with_state(state)
149}
150
151/// Build the router with authentication disabled (tests, loopback dev).
152pub fn router(
153    fleet: Arc<dyn FleetProvider>,
154    admin: Option<Arc<dyn FleetAdmin>>,
155    store: Arc<dyn Store>,
156    bus: Arc<dyn EventBus>,
157) -> Router {
158    router_with_auth(fleet, admin, store, bus, None, auth::AuthState::disabled())
159}
160
161/// Build the router with automations wired and authentication disabled.
162pub fn router_with_automations(
163    fleet: Arc<dyn FleetProvider>,
164    admin: Option<Arc<dyn FleetAdmin>>,
165    store: Arc<dyn Store>,
166    bus: Arc<dyn EventBus>,
167    automations: Arc<prospero_core::automation::AutomationEngine>,
168) -> Router {
169    router_with_auth(
170        fleet,
171        admin,
172        store,
173        bus,
174        Some(automations),
175        auth::AuthState::disabled(),
176    )
177}