Skip to main content

chasm/api/
mod.rs

1// Copyright (c) 2024-2028 Nervosys LLC
2// SPDX-License-Identifier: AGPL-3.0-only
3//! HTTP API Server for Chat System Manager
4//!
5//! Provides a REST API for the web frontend and mobile app to interact with CSM.
6//! Uses Actix-web for the HTTP server.
7
8#[cfg(feature = "enterprise")]
9mod audit;
10mod auth;
11pub mod caching;
12mod docs;
13mod graphql;
14mod handlers_simple;
15mod handlers_swe;
16#[cfg(feature = "enterprise")]
17mod retention;
18pub mod sdk;
19#[cfg(feature = "enterprise")]
20mod sso;
21mod recording;
22mod state;
23mod sync;
24mod webhooks;
25mod websocket;
26
27pub use recording::{
28    configure_recording_routes, create_recording_state,
29};
30#[cfg(feature = "enterprise")]
31pub use audit::{
32    configure_audit_routes, AuditAction, AuditCategory, AuditEvent, AuditEventBuilder, AuditService,
33};
34pub use auth::configure_auth_routes;
35#[cfg(feature = "enterprise")]
36pub use retention::{configure_retention_routes, RetentionPolicy, RetentionService};
37#[cfg(feature = "enterprise")]
38pub use sso::{configure_sso_routes, SamlIdpConfig, SsoService};
39pub use state::AppState;
40pub use sync::{configure_sync_routes, create_sync_state};
41pub use websocket::{configure_websocket_routes, WebSocketState};
42
43use actix_cors::Cors;
44use actix_web::{middleware, web, App, HttpServer};
45use anyhow::Result;
46use std::path::PathBuf;
47
48use crate::database::ChatDatabase;
49
50/// API server configuration
51#[derive(Debug, Clone)]
52pub struct ServerConfig {
53    pub host: String,
54    pub port: u16,
55    pub database_path: String,
56    pub cors_origins: Vec<String>,
57}
58
59impl Default for ServerConfig {
60    fn default() -> Self {
61        Self {
62            host: "0.0.0.0".to_string(), // Bind to all interfaces
63            port: 8787,
64            database_path: dirs::data_local_dir()
65                .map(|p| p.join("csm").join("csm.db").to_string_lossy().to_string())
66                .unwrap_or_else(|| "csm.db".to_string()),
67            cors_origins: vec![
68                "http://localhost:5173".to_string(),
69                "http://localhost:3000".to_string(),
70                "http://127.0.0.1:5173".to_string(),
71                "http://127.0.0.1:3000".to_string(),
72                "http://localhost:8081".to_string(),  // Expo web
73                "http://127.0.0.1:8081".to_string(),  // Expo web
74                "http://localhost:19006".to_string(), // Expo web alt
75                "http://127.0.0.1:19006".to_string(), // Expo web alt
76            ],
77        }
78    }
79}
80
81/// Configure API routes
82fn configure_routes(cfg: &mut web::ServiceConfig) {
83    use handlers_simple::*;
84
85    eprintln!("[DEBUG] Configuring routes...");
86
87    // Routes for /api
88    cfg.service(
89        web::scope("/api")
90            .route("/health", web::get().to(health_check))
91            .route("/workspaces", web::get().to(list_workspaces))
92            .route("/workspaces/{id}", web::get().to(get_workspace))
93            .route("/sessions", web::get().to(list_sessions))
94            .route("/sessions/search", web::get().to(search_sessions))
95            .route("/sessions/{id}", web::get().to(get_session))
96            .route("/providers", web::get().to(list_providers))
97            .route("/stats", web::get().to(get_stats))
98            .route("/stats/overview", web::get().to(get_stats))
99            // Agent routes
100            .route("/agents", web::get().to(list_agents))
101            .route("/agents", web::post().to(create_agent))
102            .route("/agents/{id}", web::get().to(get_agent))
103            .route("/agents/{id}", web::put().to(update_agent))
104            .route("/agents/{id}", web::delete().to(delete_agent))
105            // Swarm routes
106            .route("/swarms", web::get().to(list_swarms))
107            .route("/swarms", web::post().to(create_swarm))
108            .route("/swarms/{id}", web::get().to(get_swarm))
109            .route("/swarms/{id}", web::delete().to(delete_swarm))
110            // Settings routes
111            .route("/settings", web::get().to(get_settings))
112            .route("/settings", web::put().to(update_settings))
113            .route("/settings/accounts", web::get().to(list_accounts))
114            .route("/settings/accounts", web::post().to(create_account))
115            .route("/settings/accounts/{id}", web::delete().to(delete_account))
116            // System routes
117            .route("/system/info", web::get().to(get_system_info))
118            .route("/system/health", web::get().to(get_system_health))
119            .route(
120                "/system/providers/health",
121                web::get().to(get_provider_health),
122            )
123            // MCP routes
124            .route("/mcp/tools", web::get().to(list_mcp_tools))
125            .route("/mcp/call", web::post().to(call_mcp_tool))
126            .route("/mcp/batch", web::post().to(call_mcp_tools_batch))
127            .route("/mcp/system-prompt", web::get().to(get_csm_system_prompt))
128            // SWE routes
129            .route("/swe/projects", web::get().to(handlers_swe::list_projects))
130            .route(
131                "/swe/projects",
132                web::post().to(handlers_swe::create_project),
133            )
134            .route(
135                "/swe/projects/{id}",
136                web::get().to(handlers_swe::get_project),
137            )
138            .route(
139                "/swe/projects/{id}",
140                web::delete().to(handlers_swe::delete_project),
141            )
142            .route(
143                "/swe/projects/{id}/open",
144                web::post().to(handlers_swe::open_project),
145            )
146            .route(
147                "/swe/projects/{id}/context",
148                web::get().to(handlers_swe::get_context),
149            )
150            .route(
151                "/swe/projects/{id}/execute",
152                web::post().to(handlers_swe::execute_tool),
153            )
154            .route(
155                "/swe/projects/{project_id}/memory",
156                web::get().to(handlers_swe::list_memory),
157            )
158            .route(
159                "/swe/projects/{project_id}/memory",
160                web::post().to(handlers_swe::create_memory),
161            )
162            .route(
163                "/swe/projects/{project_id}/memory/{id}",
164                web::get().to(handlers_swe::get_memory),
165            )
166            .route(
167                "/swe/projects/{project_id}/memory/{id}",
168                web::put().to(handlers_swe::update_memory),
169            )
170            .route(
171                "/swe/projects/{project_id}/memory/{id}",
172                web::delete().to(handlers_swe::delete_memory),
173            )
174            .route(
175                "/swe/projects/{project_id}/rules",
176                web::get().to(handlers_swe::list_rules),
177            )
178            .route(
179                "/swe/projects/{project_id}/rules",
180                web::post().to(handlers_swe::create_rule),
181            )
182            .route(
183                "/swe/projects/{project_id}/rules/{id}",
184                web::put().to(handlers_swe::update_rule),
185            )
186            .route(
187                "/swe/projects/{project_id}/rules/{id}",
188                web::delete().to(handlers_swe::delete_rule),
189            ),
190    );
191
192    eprintln!("[DEBUG] Added /api routes");
193}
194
195/// Start the API server
196pub async fn start_server(config: ServerConfig) -> Result<()> {
197    // Ensure database directory exists
198    let db_path = PathBuf::from(&config.database_path);
199    if let Some(parent) = db_path.parent() {
200        std::fs::create_dir_all(parent)?;
201    }
202
203    // Open database
204    let db = ChatDatabase::open(&db_path)?;
205
206    // Initialize SWE tables
207    {
208        let conn = rusqlite::Connection::open(&db_path)?;
209        if let Err(e) = handlers_swe::init_swe_tables(&conn) {
210            eprintln!("[WARN] Failed to initialize SWE tables: {}", e);
211        }
212    }
213
214    // Initialize Auth tables
215    {
216        let conn = rusqlite::Connection::open(&db_path)?;
217        if let Err(e) = auth::init_auth_tables(&conn) {
218            eprintln!("[WARN] Failed to initialize Auth tables: {}", e);
219        }
220    }
221
222    let state = web::Data::new(AppState::new(db, db_path));
223    let sync_state = web::Data::new(create_sync_state());
224    let ws_state = web::Data::new(WebSocketState::new());
225    let recording_state = web::Data::new(create_recording_state());
226    let cors_origins = config.cors_origins.clone();
227
228    println!("[*] CSM API Server starting...");
229    println!("   Address: http://{}:{}", config.host, config.port);
230    println!("   Database: {}", config.database_path);
231    println!();
232    println!("[*] Mobile app endpoints:");
233    println!("   GET /api/workspaces     - List workspaces");
234    println!("   GET /api/sessions       - List sessions");
235    println!("   GET /api/sessions/:id   - Get session details");
236    println!("   GET /api/stats          - Database statistics");
237    println!();
238    println!("[*] SWE Mode endpoints:");
239    println!("   GET /api/swe/projects   - List SWE projects");
240    println!("   POST /api/swe/projects  - Create SWE project");
241    println!();
242    println!("[*] Recording endpoints:");
243    println!("   POST /recording/events    - Send recording events");
244    println!("   POST /recording/snapshot  - Store session snapshot");
245    println!("   GET /recording/sessions   - List active sessions");
246    println!("   GET /recording/status     - Recording status");
247    println!("   GET /recording/ws         - WebSocket for real-time recording");
248    println!();
249    println!("[*] Sync endpoints:");
250    println!("   GET /sync/version       - Get current sync version");
251    println!("   GET /sync/delta?from=N  - Get changes since version N");
252    println!("   POST /sync/event        - Push a sync event");
253    println!("   GET /sync/snapshot      - Get full data snapshot");
254    println!("   GET /sync/subscribe     - SSE stream for real-time updates");
255    println!("   GET /ws                 - WebSocket for bidirectional updates");
256    println!();
257    println!("Press Ctrl+C to stop the server...");
258    println!();
259
260    eprintln!("[DEBUG] Creating HttpServer...");
261    let server = HttpServer::new(move || {
262        let origins = cors_origins.clone();
263        let cors = Cors::default()
264            .allowed_origin_fn(move |origin, _req_head| {
265                let origin_str = origin.to_str().unwrap_or("");
266                origins.iter().any(|allowed| allowed == origin_str)
267                    || origin_str.starts_with("http://localhost:")
268                    || origin_str.starts_with("http://127.0.0.1:")
269                    || origin_str.starts_with("exp://")
270            })
271            .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"])
272            .allowed_headers(vec!["Content-Type", "Authorization", "Accept"])
273            .supports_credentials()
274            .max_age(3600);
275
276        App::new()
277            .app_data(state.clone())
278            .app_data(sync_state.clone())
279            .app_data(ws_state.clone())
280            .app_data(recording_state.clone())
281            .wrap(cors)
282            .wrap(middleware::Logger::default())
283            .configure(configure_routes)
284            .configure(configure_sync_routes)
285            .configure(configure_auth_routes)
286            .configure(configure_recording_routes)
287            .configure(|cfg| configure_websocket_routes(cfg, ws_state.clone()))
288    });
289
290    eprintln!("[DEBUG] Binding to {}:{}...", config.host, config.port);
291    let server = server.bind((config.host.as_str(), config.port))?;
292
293    eprintln!("[DEBUG] Starting server...");
294    server.run().await?;
295
296    eprintln!("[DEBUG] Server stopped.");
297    Ok(())
298}
299
300
301