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_boot = parse_cedar_config(&config_toml)
59 .await
60 .map_err(|e| anyhow::anyhow!("{e}"))?;
61 cedar_boot_decision(
62 true,
63 cedar_boot.authorizer.is_some(),
64 cedar_boot.allow_disabled,
65 )
66 .map_err(|e| anyhow::anyhow!("{e}"))?;
67
68 Some(Arc::new(AuthState::with_cedar(cfg, cedar_boot.authorizer)))
69 } else {
70 tracing::warn!("AUTH NOT CONFIGURED: trustee-web is running WITHOUT authentication or Cedar authorization (no [oidc]/[dev] section). Never expose this to a network.");
72 None
73 };
74
75 let thq_config = thq_register::ThqConfig::from_toml(&config_toml);
77
78 let config_toml_for_state = config_toml.clone();
80 let secrets_for_state = secrets.clone();
81 let build_info_for_state = build_info.clone();
82 let (mut session, workflow_rx) = trustee_core::session::Session::new();
83 session.config_toml = Some(config_toml);
84 session.secrets = Some(secrets);
85 session.build_info = Some(build_info);
86 session.parse_auto_handoff_config();
87
88 if let Some(ref config_toml_str) = session.config_toml {
90 if let Ok(table) = config_toml_str.parse::<toml::Value>() {
91 if let Some(name) = table.get("agent").and_then(|a| a.get("name")).and_then(|n| n.as_str()) {
92 session.agent_name = name.to_string();
93 }
94 }
95 }
96
97 let (ws_tx, _ws_rx) = tokio::sync::broadcast::channel::<String>(256);
99
100 let (max_sessions, allow_llm_overlay) = {
103 let config_str: &str = &config_toml_for_state;
104 match toml::from_str::<toml::Value>(config_str) {
105 Ok(v) => {
106 let max_sessions = v
107 .get("web")
108 .and_then(|w| w.as_table())
109 .and_then(|w| w.get("max_sessions_per_user").and_then(|v| v.as_integer()))
110 .map(|v| v as usize)
111 .unwrap_or(4);
112 let allow_llm_overlay = v
113 .get("users")
114 .and_then(|u| u.as_table())
115 .and_then(|u| u.get("allow_llm_overlay").and_then(|v| v.as_bool()))
116 .unwrap_or(false);
117 (max_sessions, allow_llm_overlay)
118 }
119 Err(_) => (4, false),
120 }
121 };
122
123 let state = ServerState::new(session, ws_tx, auth_state)
124 .with_config_toml(config_toml_for_state)
125 .with_secrets(secrets_for_state)
126 .with_build_info(build_info_for_state)
127 .with_max_sessions_per_user(max_sessions)
128 .with_allow_llm_overlay(allow_llm_overlay);
129
130 state.clone().spawn_drain_task(workflow_rx);
132
133 thq_register::spawn_all(thq_config, state.clone());
137
138 let app = axum::Router::new()
146 .route("/api/v1/health", get(routes::health))
148 .nest("/auth", auth::auth_routes())
149 .route("/api/v1/models", get(routes::list_models))
151 .route("/api/v1/session", get(routes::get_session))
152 .route("/api/v1/session/command", post(routes::post_command))
153 .route("/api/v1/session/cancel", post(routes::post_cancel))
154 .route("/api/v1/session/handoff", post(routes::post_handoff))
155 .route("/api/v1/session/stream", get(routes::ws_handler))
156 .route("/api/v1/session/name", post(routes::set_session_name))
158 .route("/api/v1/session/new", post(routes::new_session))
159 .route("/api/v1/project/name", post(routes::set_project_name))
160 .route("/api/v1/sessions", get(routes::list_sessions).post(routes::create_session))
163 .route("/api/v1/sessions/live", get(routes::list_live_sessions))
164 .route("/api/v1/sessions/{id}", get(routes::get_session_detail).delete(routes::destroy_session))
165 .route("/api/v1/sessions/{id}/live", get(routes::get_live_session))
166 .route("/api/v1/sessions/{id}/resume", post(routes::resume_session))
167 .route("/api/v1/sessions/{id}/history", get(routes::get_session_history))
168 .route("/api/v1/sessions/{id}/command", post(routes::post_command_session))
170 .route("/api/v1/sessions/{id}/cancel", post(routes::post_cancel_session))
171 .route("/api/v1/sessions/{id}/handoff", post(routes::post_handoff_session))
172 .route("/api/v1/sessions/{id}/name", post(routes::set_session_name_session))
173 .route("/api/v1/sessions/{id}/stream", get(routes::ws_session_handler))
174 .route("/", get(routes::serve_index))
176 .route("/{file}", get(routes::serve_static))
177 .layer(CorsLayer::permissive())
178 .layer(axum::extract::DefaultBodyLimit::max(10 * 1024 * 1024))
179 .with_state(state);
180
181 let listener = tokio::net::TcpListener::bind(addr).await?;
183
184 if use_tls {
185 let _ = rustls::crypto::ring::default_provider().install_default();
189
190 let cert_dir = tls::default_cert_dir();
192 let (cert_path, key_path) = tls::ensure_certs(&cert_dir)?;
193
194 let tls_config = tls::load_tls_config(&cert_path, &key_path)?;
196 let acceptor = tokio_rustls::TlsAcceptor::from(std::sync::Arc::new(tls_config));
197
198 tracing::info!("Trustee API listening on https://{}", addr);
199
200 loop {
202 let (tcp_stream, peer_addr) = match listener.accept().await {
203 Ok(stream) => stream,
204 Err(e) => {
205 tracing::warn!("TCP accept failed: {}", e);
206 continue;
207 }
208 };
209
210 let acceptor = acceptor.clone();
211 let app = app.clone();
212
213 tokio::spawn(async move {
214 let tls_stream = match acceptor.accept(tcp_stream).await {
215 Ok(s) => s,
216 Err(e) => {
217 tracing::debug!("TLS accept failed from {}: {}", peer_addr, e);
218 return;
219 }
220 };
221
222 let io = hyper_util::rt::TokioIo::new(tls_stream);
225 let svc = hyper_util::service::TowerToHyperService::new(app);
226
227 let _ = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new())
228 .serve_connection_with_upgrades(io, svc)
229 .await;
230 });
231 }
232 } else {
233 tracing::info!("Trustee API listening on http://{}", addr);
234 axum::serve(listener, app).await?;
235 }
236
237 Ok(())
238}
239
240struct CedarBoot {
251 authorizer: Option<Arc<pep::cedar::CedarAuthorizer>>,
252 allow_disabled: bool,
257}
258
259pub(crate) fn cedar_boot_decision(
261 auth_configured: bool,
262 cedar_present: bool,
263 allow_disabled: bool,
264) -> Result<(), String> {
265 if !auth_configured {
266 return Ok(());
269 }
270 if cedar_present || allow_disabled {
271 Ok(())
272 } else {
273 Err(
274 "Cedar authorization is REQUIRED in web mode (fail-closed, nghr 645809c3). \
275 Either configure it: [cedar] enabled = true (policies ship embedded), \
276 or explicitly opt out per environment: [cedar] allow_disabled = true."
277 .to_string(),
278 )
279 }
280}
281
282async fn parse_cedar_config(config_toml: &str) -> Result<CedarBoot, String> {
283 let parsed: Option<toml::Table> = toml::from_str(config_toml).ok();
284 let cedar_table = parsed.as_ref().and_then(|t| t.get("cedar"));
285 let allow_disabled = cedar_table
286 .and_then(|c| c.get("allow_disabled"))
287 .and_then(|v| v.as_bool())
288 .unwrap_or(false);
289
290 let Some(cedar_section) = cedar_table.and_then(|c| c.as_table().cloned()) else {
291 return Ok(CedarBoot {
292 authorizer: None,
293 allow_disabled,
294 });
295 };
296 let enabled = cedar_section
297 .get("enabled")
298 .and_then(|v| v.as_bool())
299 .unwrap_or(false);
300
301 if !enabled {
302 tracing::debug!("Cedar authorization disabled (default)");
303 return Ok(CedarBoot {
304 authorizer: None,
305 allow_disabled,
306 });
307 }
308
309 tracing::info!("Cedar authorization enabled — initializing authorizer");
310
311 let agent_name = parsed
314 .as_ref()
315 .and_then(|t| t.get("agent"))
316 .and_then(|a| a.as_table())
317 .and_then(|a| a.get("name"))
318 .and_then(|n| n.as_str())
319 .unwrap_or("trustee");
320
321 let home_policies_dir = dirs::home_dir()
322 .map(|h| h.join(format!(".{}", agent_name)).join("policies"))
323 .unwrap_or_else(|| std::path::PathBuf::from("/nonexistent"));
324
325 let default_policy_path = home_policies_dir.join("trustee_default.cedar");
326 let default_schema_path = home_policies_dir.join("trustee_schema.cedarschema");
327
328 let policy_path = cedar_section
329 .get("policy_path")
330 .and_then(|v| v.as_str())
331 .filter(|s| !s.is_empty())
332 .map(std::path::PathBuf::from)
333 .unwrap_or(default_policy_path);
334
335 let schema_path = cedar_section
336 .get("schema_path")
337 .and_then(|v| v.as_str())
338 .filter(|s| !s.is_empty())
339 .map(std::path::PathBuf::from)
340 .or_else(|| Some(default_schema_path));
341
342 let policy_store_url = cedar_section
343 .get("policy_store_url")
344 .and_then(|v| v.as_str())
345 .map(String::from);
346
347 let policy_store_token = cedar_section
348 .get("policy_store_token")
349 .and_then(|v| v.as_str())
350 .map(String::from);
351
352 let cedar_config = pep::cedar::CedarConfig {
353 policy_path,
354 schema_path,
355 entities_path: None,
356 default_decision: pep::cedar::DefaultDecision::Deny,
357 validate_on_load: true,
358 policy_store_url,
359 policy_store_token,
360 embedded_policy: Some(EMBEDDED_CEDAR_POLICY),
361 embedded_schema: Some(EMBEDDED_CEDAR_SCHEMA),
362 };
363
364 match pep::cedar::CedarAuthorizer::new_with_policy_store(cedar_config).await {
365 Ok(auth) => {
366 tracing::info!("Cedar authorizer initialized successfully");
367 Ok(CedarBoot {
368 authorizer: Some(Arc::new(auth)),
369 allow_disabled,
370 })
371 }
372 Err(e) => {
376 let msg = format!(
377 "Cedar authorization is enabled but FAILED to initialize: {e}. \
378 Refusing to boot (fail-closed). Fix the policy/schema configuration \
379 or explicitly set [cedar] allow_disabled = true to run identity-only."
380 );
381 tracing::error!("{msg}");
382 Err(msg)
383 }
384 }
385}