1pub mod auth;
11pub mod tls;
12mod routes;
13mod state;
14mod thq_register;
15pub mod xagent;
16
17const EMBEDDED_CEDAR_POLICY: &str = include_str!("../policies/trustee_default.cedar");
19const EMBEDDED_CEDAR_SCHEMA: &str = include_str!("../policies/trustee_schema.cedarschema");
20
21use std::net::SocketAddr;
22use std::sync::Arc;
23
24use anyhow::Result;
25use axum::routing::{get, post};
26use tower_http::cors::CorsLayer;
27
28pub use auth::{AuthConfig, AuthState};
29pub use state::ServerState;
30
31pub async fn run(
43 config_toml: String,
44 secrets: std::collections::HashMap<String, String>,
45 build_info: trustee_core::types::BuildInfo,
46 addr: SocketAddr,
47 use_tls: bool,
48) -> Result<()> {
49 let auth_state = if let Some(cfg) = AuthConfig::from_toml(&config_toml) {
51 let is_dev = cfg.dev_config.local_dev_mode;
52 tracing::info!(
53 "Auth enabled: {} mode, issuer={}",
54 if is_dev { "development" } else { "production" },
55 cfg.issuer_url
56 );
57
58 let cedar_boot = parse_cedar_config(&config_toml)
60 .await
61 .map_err(|e| anyhow::anyhow!("{e}"))?;
62 cedar_boot_decision(
63 true,
64 cedar_boot.authorizer.is_some(),
65 cedar_boot.allow_disabled,
66 )
67 .map_err(|e| anyhow::anyhow!("{e}"))?;
68
69 let mut issuer_fallbacks = crate::state::service_issuers_from_config(&config_toml);
72 for si in crate::thq_register::discover_service_issuers() {
73 if !issuer_fallbacks.contains(&si) {
74 issuer_fallbacks.push(si);
75 }
76 }
77 issuer_fallbacks.retain(|si| *si != cfg.issuer_url);
78 if !issuer_fallbacks.is_empty() {
79 tracing::info!("Auth issuer fallbacks armed: {:?}", issuer_fallbacks);
80 }
81 Some(Arc::new(
82 AuthState::with_cedar(cfg, cedar_boot.authorizer)
83 .with_issuer_fallbacks(issuer_fallbacks),
84 ))
85 } else {
86 tracing::warn!("AUTH NOT CONFIGURED: trustee-web is running WITHOUT authentication or Cedar authorization (no [oidc]/[dev] section). Never expose this to a network.");
88 None
89 };
90
91 let thq_config = thq_register::ThqConfig::from_toml(&config_toml);
93
94 let config_toml_for_state = config_toml.clone();
96 let secrets_for_state = secrets.clone();
97 let build_info_for_state = build_info.clone();
98 let (mut session, workflow_rx) = trustee_core::session::Session::new();
99 session.config_toml = Some(config_toml);
100 session.secrets = Some(secrets);
101 session.build_info = Some(build_info);
102 session.parse_auto_handoff_config();
103
104 if let Some(ref config_toml_str) = session.config_toml {
106 if let Ok(table) = config_toml_str.parse::<toml::Value>() {
107 if let Some(name) = table.get("agent").and_then(|a| a.get("name")).and_then(|n| n.as_str()) {
108 session.agent_name = name.to_string();
109 }
110 }
111 }
112
113 let (ws_tx, _ws_rx) = tokio::sync::broadcast::channel::<String>(256);
115
116 let (max_sessions, allow_llm_overlay) = {
119 let config_str: &str = &config_toml_for_state;
120 match toml::from_str::<toml::Value>(config_str) {
121 Ok(v) => {
122 let max_sessions = v
123 .get("web")
124 .and_then(|w| w.as_table())
125 .and_then(|w| w.get("max_sessions_per_user").and_then(|v| v.as_integer()))
126 .map(|v| v as usize)
127 .unwrap_or(4);
128 let allow_llm_overlay = v
129 .get("users")
130 .and_then(|u| u.as_table())
131 .and_then(|u| u.get("allow_llm_overlay").and_then(|v| v.as_bool()))
132 .unwrap_or(false);
133 (max_sessions, allow_llm_overlay)
134 }
135 Err(_) => (4, false),
136 }
137 };
138
139 let state = ServerState::new(session, ws_tx, auth_state)
140 .with_config_toml(config_toml_for_state)
141 .with_secrets(secrets_for_state)
142 .with_build_info(build_info_for_state)
143 .with_max_sessions_per_user(max_sessions)
144 .with_allow_llm_overlay(allow_llm_overlay);
145
146 state.clone().spawn_drain_task(workflow_rx);
148
149 thq_register::spawn_all(thq_config, state.clone());
153
154 let app = axum::Router::new()
162 .route("/api/v1/health", get(routes::health))
164 .nest("/auth", auth::auth_routes())
165 .route("/api/v1/models", get(routes::list_models))
167 .route("/api/v1/session", get(routes::get_session))
168 .route("/api/v1/session/command", post(routes::post_command))
169 .route("/api/v1/session/cancel", post(routes::post_cancel))
170 .route("/api/v1/session/handoff", post(routes::post_handoff))
171 .route("/api/v1/session/stream", get(routes::ws_handler))
172 .route("/api/v1/session/name", post(routes::set_session_name))
174 .route("/api/v1/session/new", post(routes::new_session))
175 .route("/api/v1/project/name", post(routes::set_project_name))
176 .route("/api/v1/sessions", get(routes::list_sessions).post(routes::create_session))
179 .route("/api/v1/sessions/live", get(routes::list_live_sessions))
180 .route("/api/v1/sessions/{id}", get(routes::get_session_detail).delete(routes::destroy_session))
181 .route("/api/v1/sessions/{id}/live", get(routes::get_live_session))
182 .route("/api/v1/sessions/{id}/resume", post(routes::resume_session))
183 .route("/api/v1/sessions/{id}/history", get(routes::get_session_history))
184 .route("/api/v1/sessions/{id}/command", post(routes::post_command_session))
186 .route("/api/v1/sessions/{id}/cancel", post(routes::post_cancel_session))
187 .route("/api/v1/sessions/{id}/handoff", post(routes::post_handoff_session))
188 .route("/api/v1/sessions/{id}/name", post(routes::set_session_name_session))
189 .route("/api/v1/sessions/{id}/stream", get(routes::ws_session_handler))
190 .route("/", get(routes::serve_index))
192 .route("/{file}", get(routes::serve_static))
193 .merge(crate::xagent::router())
195 .layer(CorsLayer::permissive())
196 .layer(axum::extract::DefaultBodyLimit::max(10 * 1024 * 1024))
197 .with_state(state);
198
199 let listener = tokio::net::TcpListener::bind(addr).await?;
201
202 if use_tls {
203 let _ = rustls::crypto::ring::default_provider().install_default();
207
208 let cert_dir = tls::default_cert_dir();
210 let (cert_path, key_path) = tls::ensure_certs(&cert_dir)?;
211
212 let tls_config = tls::load_tls_config(&cert_path, &key_path)?;
214 let acceptor = tokio_rustls::TlsAcceptor::from(std::sync::Arc::new(tls_config));
215
216 tracing::info!("Trustee API listening on https://{}", addr);
217
218 loop {
220 let (tcp_stream, peer_addr) = match listener.accept().await {
221 Ok(stream) => stream,
222 Err(e) => {
223 tracing::warn!("TCP accept failed: {}", e);
224 continue;
225 }
226 };
227
228 let acceptor = acceptor.clone();
229 let app = app.clone();
230
231 tokio::spawn(async move {
232 let tls_stream = match acceptor.accept(tcp_stream).await {
233 Ok(s) => s,
234 Err(e) => {
235 tracing::debug!("TLS accept failed from {}: {}", peer_addr, e);
236 return;
237 }
238 };
239
240 let io = hyper_util::rt::TokioIo::new(tls_stream);
243 let svc = hyper_util::service::TowerToHyperService::new(app);
244
245 let _ = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new())
246 .serve_connection_with_upgrades(io, svc)
247 .await;
248 });
249 }
250 } else {
251 tracing::info!("Trustee API listening on http://{}", addr);
252 axum::serve(listener, app).await?;
253 }
254
255 Ok(())
256}
257
258struct CedarBoot {
269 authorizer: Option<Arc<pep::cedar::CedarAuthorizer>>,
270 allow_disabled: bool,
275}
276
277pub(crate) fn cedar_boot_decision(
279 auth_configured: bool,
280 cedar_present: bool,
281 allow_disabled: bool,
282) -> Result<(), String> {
283 if !auth_configured {
284 return Ok(());
287 }
288 if cedar_present || allow_disabled {
289 Ok(())
290 } else {
291 Err(
292 "Cedar authorization is REQUIRED in web mode (fail-closed, nghr 645809c3). \
293 Either configure it: [cedar] enabled = true (policies ship embedded), \
294 or explicitly opt out per environment: [cedar] allow_disabled = true."
295 .to_string(),
296 )
297 }
298}
299
300async fn parse_cedar_config(config_toml: &str) -> Result<CedarBoot, String> {
301 let parsed: Option<toml::Table> = toml::from_str(config_toml).ok();
302 let cedar_table = parsed.as_ref().and_then(|t| t.get("cedar"));
303 let allow_disabled = cedar_table
304 .and_then(|c| c.get("allow_disabled"))
305 .and_then(|v| v.as_bool())
306 .unwrap_or(false);
307
308 let Some(cedar_section) = cedar_table.and_then(|c| c.as_table().cloned()) else {
309 return Ok(CedarBoot {
310 authorizer: None,
311 allow_disabled,
312 });
313 };
314 let enabled = cedar_section
315 .get("enabled")
316 .and_then(|v| v.as_bool())
317 .unwrap_or(false);
318
319 if !enabled {
320 tracing::debug!("Cedar authorization disabled (default)");
321 return Ok(CedarBoot {
322 authorizer: None,
323 allow_disabled,
324 });
325 }
326
327 tracing::info!("Cedar authorization enabled — initializing authorizer");
328
329 let agent_name = parsed
332 .as_ref()
333 .and_then(|t| t.get("agent"))
334 .and_then(|a| a.as_table())
335 .and_then(|a| a.get("name"))
336 .and_then(|n| n.as_str())
337 .unwrap_or("trustee");
338
339 let home_policies_dir = dirs::home_dir()
340 .map(|h| h.join(format!(".{}", agent_name)).join("policies"))
341 .unwrap_or_else(|| std::path::PathBuf::from("/nonexistent"));
342
343 let default_policy_path = home_policies_dir.join("trustee_default.cedar");
344 let default_schema_path = home_policies_dir.join("trustee_schema.cedarschema");
345
346 let policy_path = cedar_section
347 .get("policy_path")
348 .and_then(|v| v.as_str())
349 .filter(|s| !s.is_empty())
350 .map(std::path::PathBuf::from)
351 .unwrap_or(default_policy_path);
352
353 let schema_path = cedar_section
354 .get("schema_path")
355 .and_then(|v| v.as_str())
356 .filter(|s| !s.is_empty())
357 .map(std::path::PathBuf::from)
358 .or_else(|| Some(default_schema_path));
359
360 let policy_store_url = cedar_section
361 .get("policy_store_url")
362 .and_then(|v| v.as_str())
363 .map(String::from);
364
365 let policy_store_token = cedar_section
366 .get("policy_store_token")
367 .and_then(|v| v.as_str())
368 .map(String::from);
369
370 let cedar_config = pep::cedar::CedarConfig {
371 policy_path,
372 schema_path,
373 entities_path: None,
374 default_decision: pep::cedar::DefaultDecision::Deny,
375 validate_on_load: true,
376 policy_store_url,
377 policy_store_token,
378 embedded_policy: Some(EMBEDDED_CEDAR_POLICY),
379 embedded_schema: Some(EMBEDDED_CEDAR_SCHEMA),
380 };
381
382 match pep::cedar::CedarAuthorizer::new_with_policy_store(cedar_config).await {
383 Ok(auth) => {
384 tracing::info!("Cedar authorizer initialized successfully");
385 Ok(CedarBoot {
386 authorizer: Some(Arc::new(auth)),
387 allow_disabled,
388 })
389 }
390 Err(e) => {
394 let msg = format!(
395 "Cedar authorization is enabled but FAILED to initialize: {e}. \
396 Refusing to boot (fail-closed). Fix the policy/schema configuration \
397 or explicitly set [cedar] allow_disabled = true to run identity-only."
398 );
399 tracing::error!("{msg}");
400 Err(msg)
401 }
402 }
403}