1pub mod auth;
11pub mod tls;
12mod routes;
13mod state;
14mod thq_register;
15
16const EMBEDDED_CEDAR_POLICY: &str = include_str!("../policies/trustee_default.cedar");
18const EMBEDDED_CEDAR_SCHEMA: &str = include_str!("../policies/trustee_schema.cedarschema");
19
20use std::net::SocketAddr;
21use std::sync::Arc;
22
23use anyhow::Result;
24use axum::routing::{get, post};
25use tower_http::cors::CorsLayer;
26
27pub use auth::{AuthConfig, AuthState};
28pub use state::ServerState;
29
30pub async fn run(
42 config_toml: String,
43 secrets: std::collections::HashMap<String, String>,
44 build_info: trustee_core::types::BuildInfo,
45 addr: SocketAddr,
46 use_tls: bool,
47) -> Result<()> {
48 let auth_state = if let Some(cfg) = AuthConfig::from_toml(&config_toml) {
50 let is_dev = cfg.dev_config.local_dev_mode;
51 tracing::info!(
52 "Auth enabled: {} mode, issuer={}",
53 if is_dev { "development" } else { "production" },
54 cfg.issuer_url
55 );
56
57 let cedar_authorizer = parse_cedar_config(&config_toml).await;
59
60 Some(Arc::new(AuthState::with_cedar(cfg, cedar_authorizer)))
61 } else {
62 None
63 };
64
65 let thq_config = thq_register::ThqConfig::from_toml(&config_toml);
67
68 let config_toml_for_state = config_toml.clone();
70 let secrets_for_state = secrets.clone();
71 let build_info_for_state = build_info.clone();
72 let (mut session, workflow_rx) = trustee_core::session::Session::new();
73 session.config_toml = Some(config_toml);
74 session.secrets = Some(secrets);
75 session.build_info = Some(build_info);
76 session.parse_auto_handoff_config();
77
78 if let Some(ref config_toml_str) = session.config_toml {
80 if let Ok(table) = config_toml_str.parse::<toml::Value>() {
81 if let Some(name) = table.get("agent").and_then(|a| a.get("name")).and_then(|n| n.as_str()) {
82 session.agent_name = name.to_string();
83 }
84 }
85 }
86
87 let (ws_tx, _ws_rx) = tokio::sync::broadcast::channel::<String>(256);
89
90 let max_sessions: usize = {
93 let config_str: &str = &config_toml_for_state;
94 match toml::from_str::<toml::Value>(config_str) {
95 Ok(v) => v
96 .get("web")
97 .and_then(|w| w.as_table())
98 .and_then(|w| w.get("max_sessions_per_user").and_then(|v| v.as_integer()))
99 .map(|v| v as usize)
100 .unwrap_or(4),
101 Err(_) => 4,
102 }
103 };
104
105 let state = ServerState::new(session, ws_tx, auth_state)
106 .with_config_toml(config_toml_for_state)
107 .with_secrets(secrets_for_state)
108 .with_build_info(build_info_for_state)
109 .with_max_sessions_per_user(max_sessions);
110
111 state.clone().spawn_drain_task(workflow_rx);
113
114 if let Some(cfg) = thq_config {
116 thq_register::spawn(cfg);
117 } else {
118 tracing::debug!("THQ registration not configured (no [thq] section)");
119 }
120
121 let app = axum::Router::new()
129 .route("/api/v1/health", get(routes::health))
131 .nest("/auth", auth::auth_routes())
132 .route("/api/v1/models", get(routes::list_models))
134 .route("/api/v1/session", get(routes::get_session))
135 .route("/api/v1/session/command", post(routes::post_command))
136 .route("/api/v1/session/cancel", post(routes::post_cancel))
137 .route("/api/v1/session/handoff", post(routes::post_handoff))
138 .route("/api/v1/session/stream", get(routes::ws_handler))
139 .route("/api/v1/session/name", post(routes::set_session_name))
141 .route("/api/v1/session/new", post(routes::new_session))
142 .route("/api/v1/project/name", post(routes::set_project_name))
143 .route("/api/v1/sessions", get(routes::list_sessions).post(routes::create_session))
146 .route("/api/v1/sessions/live", get(routes::list_live_sessions))
147 .route("/api/v1/sessions/{id}", get(routes::get_session_detail).delete(routes::destroy_session))
148 .route("/api/v1/sessions/{id}/live", get(routes::get_live_session))
149 .route("/api/v1/sessions/{id}/resume", post(routes::resume_session))
150 .route("/api/v1/sessions/{id}/history", get(routes::get_session_history))
151 .route("/api/v1/sessions/{id}/command", post(routes::post_command_session))
153 .route("/api/v1/sessions/{id}/cancel", post(routes::post_cancel_session))
154 .route("/api/v1/sessions/{id}/handoff", post(routes::post_handoff_session))
155 .route("/api/v1/sessions/{id}/name", post(routes::set_session_name_session))
156 .route("/api/v1/sessions/{id}/stream", get(routes::ws_session_handler))
157 .route("/", get(routes::serve_index))
159 .route("/{file}", get(routes::serve_static))
160 .layer(CorsLayer::permissive())
161 .layer(axum::extract::DefaultBodyLimit::max(10 * 1024 * 1024))
162 .with_state(state);
163
164 let listener = tokio::net::TcpListener::bind(addr).await?;
166
167 if use_tls {
168 let _ = rustls::crypto::ring::default_provider().install_default();
172
173 let cert_dir = tls::default_cert_dir();
175 let (cert_path, key_path) = tls::ensure_certs(&cert_dir)?;
176
177 let tls_config = tls::load_tls_config(&cert_path, &key_path)?;
179 let acceptor = tokio_rustls::TlsAcceptor::from(std::sync::Arc::new(tls_config));
180
181 tracing::info!("Trustee API listening on https://{}", addr);
182
183 loop {
185 let (tcp_stream, peer_addr) = match listener.accept().await {
186 Ok(stream) => stream,
187 Err(e) => {
188 tracing::warn!("TCP accept failed: {}", e);
189 continue;
190 }
191 };
192
193 let acceptor = acceptor.clone();
194 let app = app.clone();
195
196 tokio::spawn(async move {
197 let tls_stream = match acceptor.accept(tcp_stream).await {
198 Ok(s) => s,
199 Err(e) => {
200 tracing::debug!("TLS accept failed from {}: {}", peer_addr, e);
201 return;
202 }
203 };
204
205 let io = hyper_util::rt::TokioIo::new(tls_stream);
208 let svc = hyper_util::service::TowerToHyperService::new(app);
209
210 let _ = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new())
211 .serve_connection_with_upgrades(io, svc)
212 .await;
213 });
214 }
215 } else {
216 tracing::info!("Trustee API listening on http://{}", addr);
217 axum::serve(listener, app).await?;
218 }
219
220 Ok(())
221}
222
223async fn parse_cedar_config(config_toml: &str) -> Option<Arc<pep::cedar::CedarAuthorizer>> {
233 let table: toml::Table = match toml::from_str(config_toml) {
234 Ok(t) => t,
235 Err(_) => return None,
236 };
237
238 let cedar_section = table.get("cedar")?.as_table()?;
239 let enabled = cedar_section
240 .get("enabled")
241 .and_then(|v| v.as_bool())
242 .unwrap_or(false);
243
244 if !enabled {
245 tracing::debug!("Cedar authorization disabled (default)");
246 return None;
247 }
248
249 tracing::info!("Cedar authorization enabled — initializing authorizer");
250
251 let agent_name = table
254 .get("agent")
255 .and_then(|a| a.as_table())
256 .and_then(|a| a.get("name"))
257 .and_then(|n| n.as_str())
258 .unwrap_or("trustee");
259
260 let home_policies_dir = dirs::home_dir()
261 .map(|h| h.join(format!(".{}", agent_name)).join("policies"))
262 .unwrap_or_else(|| std::path::PathBuf::from("/nonexistent"));
263
264 let default_policy_path = home_policies_dir.join("trustee_default.cedar");
265 let default_schema_path = home_policies_dir.join("trustee_schema.cedarschema");
266
267 let policy_path = cedar_section
268 .get("policy_path")
269 .and_then(|v| v.as_str())
270 .filter(|s| !s.is_empty())
271 .map(std::path::PathBuf::from)
272 .unwrap_or(default_policy_path);
273
274 let schema_path = cedar_section
275 .get("schema_path")
276 .and_then(|v| v.as_str())
277 .filter(|s| !s.is_empty())
278 .map(std::path::PathBuf::from)
279 .or_else(|| Some(default_schema_path));
280
281 let policy_store_url = cedar_section
282 .get("policy_store_url")
283 .and_then(|v| v.as_str())
284 .map(String::from);
285
286 let policy_store_token = cedar_section
287 .get("policy_store_token")
288 .and_then(|v| v.as_str())
289 .map(String::from);
290
291 let cedar_config = pep::cedar::CedarConfig {
292 policy_path,
293 schema_path,
294 entities_path: None,
295 default_decision: pep::cedar::DefaultDecision::Deny,
296 validate_on_load: true,
297 policy_store_url,
298 policy_store_token,
299 embedded_policy: Some(EMBEDDED_CEDAR_POLICY),
300 embedded_schema: Some(EMBEDDED_CEDAR_SCHEMA),
301 };
302
303 match pep::cedar::CedarAuthorizer::new_with_policy_store(cedar_config).await {
304 Ok(auth) => {
305 tracing::info!("Cedar authorizer initialized successfully");
306 Some(Arc::new(auth))
307 }
308 Err(e) => {
309 tracing::error!("Failed to initialize Cedar authorizer: {}", e);
310 tracing::warn!("Cedar was enabled but initialization failed — auth will proceed WITHOUT Cedar");
311 None
312 }
313 }
314}