Skip to main content

trustee_api/
lib.rs

1//! Trustee API — REST + WebSocket server for the Trustee agent.
2//!
3//! Wraps a [`trustee_core::session::Session`] and exposes it over HTTP.
4//! Static frontend files are served from [`trustee_web`].
5//!
6//! Authentication is optional. When `[oidc]` or `[dev]` sections are present
7//! in the config TOML, all `/api/v1/*` endpoints require a valid JWT or dev
8//! token. Otherwise, all endpoints are open.
9
10pub mod auth;
11mod routes;
12mod state;
13
14use std::net::SocketAddr;
15use std::sync::Arc;
16
17use anyhow::Result;
18use axum::routing::{get, post};
19use tower_http::cors::CorsLayer;
20
21pub use auth::{AuthConfig, AuthState};
22pub use state::ServerState;
23
24/// Run the API server.
25///
26/// Creates a `Session` with the given config, starts a background task to
27/// drain workflow messages and broadcast them to WebSocket clients, then
28/// serves the REST + WebSocket + static files on `addr`.
29///
30/// If `[oidc]` or `[dev]` sections are found in the config TOML, auth is
31/// enabled — all `/api/v1/*` endpoints (except health) require a valid token.
32pub async fn run(
33    config_toml: String,
34    secrets: std::collections::HashMap<String, String>,
35    build_info: trustee_core::types::BuildInfo,
36    addr: SocketAddr,
37) -> Result<()> {
38    // Parse auth config from TOML (returns None if no [oidc] or [dev] sections)
39    let auth_state = AuthConfig::from_toml(&config_toml).map(|cfg| {
40        let is_dev = cfg.dev_config.local_dev_mode;
41        tracing::info!(
42            "Auth enabled: {} mode, issuer={}",
43            if is_dev { "development" } else { "production" },
44            cfg.issuer_url
45        );
46        Arc::new(AuthState::new(cfg))
47    });
48
49    // Build the session
50    let (mut session, workflow_rx) = trustee_core::session::Session::new();
51    session.config_toml = Some(config_toml);
52    session.secrets = Some(secrets);
53    session.build_info = Some(build_info);
54    session.parse_auto_handoff_config();
55
56    // Create the broadcast channel for WebSocket fan-out
57    let (ws_tx, _ws_rx) = tokio::sync::broadcast::channel::<String>(256);
58
59    // Wrap session in shared state
60    let state = ServerState::new(session, ws_tx, auth_state);
61
62    // Start background message drain task (owns workflow_rx directly — no deadlock)
63    state.clone().spawn_drain_task(workflow_rx);
64
65    // Build router
66    //
67    // Auth middleware approach: since axum 0.8's from_fn_with_state has
68    // trait bound issues with nested routers, we apply auth checking at
69    // the handler level via a helper. Each protected route's handler
70    // calls auth::check_auth() first. This is simpler and avoids type
71    // complexity.
72    let app = axum::Router::new()
73        // Public routes
74        .route("/api/v1/health", get(routes::health))
75        .nest("/auth", auth::auth_routes())
76        // Protected API routes
77        .route("/api/v1/session", get(routes::get_session))
78        .route("/api/v1/session/command", post(routes::post_command))
79        .route("/api/v1/session/cancel", post(routes::post_cancel))
80        .route("/api/v1/session/handoff", post(routes::post_handoff))
81        .route("/api/v1/session/stream", get(routes::ws_handler))
82        // Static files from trustee-web
83        .route("/", get(routes::serve_index))
84        .route("/{file}", get(routes::serve_static))
85        .layer(CorsLayer::permissive())
86        .layer(axum::extract::DefaultBodyLimit::max(10 * 1024 * 1024))
87        .with_state(state);
88
89    // Start server
90    let listener = tokio::net::TcpListener::bind(addr).await?;
91    tracing::info!("Trustee API listening on http://{}", addr);
92    axum::serve(listener, app).await?;
93
94    Ok(())
95}