1use std::collections::HashMap;
10use std::sync::Arc;
11use std::time::Duration;
12
13use axum::Router;
14use axum::extract::{DefaultBodyLimit, State};
15use axum::http::{HeaderMap, StatusCode, header};
16use axum::middleware::from_fn_with_state;
17use axum::response::IntoResponse;
18use axum::routing::get;
19use serde_json::json;
20use tracing::error;
21
22use crate::admin::require_admin;
23use crate::config::Config;
24use crate::config::ConfigError;
25use crate::events::EventBus;
26use crate::http::{
27 Json, MAX_BODY_BYTES, ScopeState, cors_layer, scope_layer, security_headers_layer,
28};
29use crate::module::{HARNESS_API, Module, ModuleContext, harness_api_mismatch};
30use crate::ports::Dispatcher;
31use crate::ports::{Clock, Database, Port, Ports, Statement, SystemClock, warn_undeclared_ports};
32use crate::problem::Problem;
33use crate::sidecar::SidecarMount;
34use crate::surface::{RenderedSurface, SurfaceDocument, SurfaceSource, UiContext, UiMount};
35use crate::template::{Template, TemplateRegistry};
36use crate::venture::Venture;
37
38pub trait Runtime: Send + Sync + 'static {
43 fn provides(&self) -> Vec<Port>;
44}
45
46pub struct Harness {
48 venture: Arc<Venture>,
49 modules: Vec<Arc<dyn Module>>,
50 templates: Arc<TemplateRegistry>,
51 events: EventBus,
52 runtime: Option<Arc<dyn Runtime>>,
53 well_known: Option<Router>,
56 surface: Arc<SurfaceVariants>,
59 ui: Option<Arc<dyn UiMount>>,
61}
62
63struct SurfaceVariants {
64 document: Arc<SurfaceDocument>,
65 full: RenderedSurface,
66 public: RenderedSurface,
67}
68
69impl SurfaceVariants {
70 fn compose(
71 venture: &Venture,
72 modules: &[Arc<dyn Module>],
73 ui: Option<&Arc<dyn UiMount>>,
74 ) -> Self {
75 let mut document = SurfaceDocument::compose(venture, modules);
76 document.ui = ui.and_then(|ui| ui.describe());
77 Self {
78 full: RenderedSurface::render(&document),
79 public: RenderedSurface::render(&document.public()),
80 document: Arc::new(document),
81 }
82 }
83}
84
85impl std::fmt::Debug for Harness {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 let modules: Vec<&str> = self.modules.iter().map(|m| m.name()).collect();
88 f.debug_struct("Harness")
89 .field("venture", &self.venture.name)
90 .field("modules", &modules)
91 .finish_non_exhaustive()
92 }
93}
94
95impl Harness {
96 pub fn builder() -> HarnessBuilder {
97 HarnessBuilder::default()
98 }
99
100 pub fn venture(&self) -> &Arc<Venture> {
101 &self.venture
102 }
103
104 pub fn modules(&self) -> &[Arc<dyn Module>] {
105 &self.modules
106 }
107
108 pub fn templates(&self) -> &Arc<TemplateRegistry> {
109 &self.templates
110 }
111
112 pub fn events(&self) -> &EventBus {
113 &self.events
114 }
115
116 pub fn module_context(&self, module: &dyn Module, ports: &Ports) -> ModuleContext {
121 ModuleContext {
122 config: Arc::clone(&ports.config),
123 ports: ports.view_for(module),
124 events: self.events.clone(),
125 templates: Arc::clone(&self.templates),
126 venture: Arc::clone(&self.venture),
127 ui_mounted: self.ui.is_some(),
128 }
129 }
130
131 fn sidecar_mounts(&self, ports: &Ports) -> Vec<SidecarMount> {
137 let mounts = match crate::sidecar::SidecarMounts::from_config(ports.config.as_ref()) {
138 Ok(mounts) => mounts,
139 Err(errors) => {
140 for error in errors {
141 tracing::error!(error, "ignoring the sidecar mount table");
142 }
143 crate::sidecar::SidecarMounts::default()
144 }
145 };
146 let module_names: Vec<&str> = self.modules.iter().map(|m| m.name()).collect();
147 for collision in mounts.collisions(&module_names) {
148 tracing::error!(error = collision, "ignoring the colliding sidecar mount");
149 }
150 mounts
151 .iter()
152 .filter(|m| !module_names.contains(&m.name.as_str()))
153 .cloned()
154 .collect()
155 }
156
157 pub fn runtime(&self) -> Option<&Arc<dyn Runtime>> {
159 self.runtime.as_ref()
160 }
161
162 #[must_use]
166 pub fn surface(&self) -> &SurfaceDocument {
167 &self.surface.document
168 }
169
170 pub fn router(&self, ports: Ports) -> Router {
178 let mut api = Router::new();
179 for module in &self.modules {
180 let ctx = self.module_context(module.as_ref(), &ports);
181 api = api.nest(&format!("/v1/{}", module.name()), module.router(ctx));
182 }
183 let mounted = self.sidecar_mounts(&ports);
184 for mount in &mounted {
185 api = api.nest(
186 &format!("/v1/{}", mount.name),
187 crate::sidecar::router(mount.clone(), ports.dispatcher.clone()),
188 );
189 }
190 let api = api
191 .layer(axum::middleware::from_fn(security_headers_layer))
192 .layer(DefaultBodyLimit::max(MAX_BODY_BYTES));
193
194 let surface_source: Arc<dyn SurfaceSource> = Arc::new(MergedSurface {
195 base: Arc::clone(&self.surface),
196 mounts: mounted,
197 dispatcher: ports.dispatcher.clone(),
198 });
199
200 let ui = self.ui.as_ref().map(|ui| {
201 ui.router(UiContext {
202 surface: Arc::clone(&surface_source),
203 api: api.clone(),
204 config: Arc::clone(&ports.config),
205 venture: Arc::clone(&self.venture),
206 captcha_configured: ports.captcha.is_some(),
207 signer: ports.signer.clone(),
208 rate_limiter: ports.rate_limiter.clone(),
209 })
210 .layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
211 });
212
213 let Ports {
214 config,
215 db,
216 mailer,
217 captcha,
218 rate_limiter: _,
219 signer: _,
220 kv: _,
221 blob: _,
222 http: _,
223 clock,
224 id_gen,
225 defer,
226 dispatcher: _,
227 } = ports;
228
229 let health_state = HealthState {
230 venture: Arc::clone(&self.venture),
231 modules: self.modules.clone(),
232 harness_build: config
233 .get("HARNESS_BUILD")
234 .filter(|build| !build.is_empty()),
235 mailer_configured: mailer.is_some(),
236 captcha_configured: captcha.is_some(),
237 };
238
239 let scope_state = ScopeState {
240 defer: defer.unwrap_or_else(|| Arc::new(crate::ports::NoopDefer)),
241 id_gen: id_gen.unwrap_or_else(|| Arc::new(crate::ports::UlidIdGen)),
242 };
243 let ready_state = ReadyState {
244 db,
245 clock: clock.unwrap_or_else(|| Arc::new(SystemClock)),
246 };
247
248 let surface_state = SurfaceState {
249 config,
250 source: surface_source,
251 };
252
253 let root = Router::new()
254 .route("/__health", get(health_handler))
255 .with_state(health_state)
256 .route("/__ready", get(ready_handler))
257 .with_state(ready_state)
258 .route("/__surface", get(surface_handler))
259 .with_state(surface_state)
260 .merge(api);
261 let root = match &self.well_known {
262 Some(well_known) => root.nest(
263 "/.well-known",
264 well_known
265 .clone()
266 .layer(DefaultBodyLimit::max(MAX_BODY_BYTES)),
267 ),
268 None => root,
269 };
270 let root = match ui {
271 Some(ui) => root.nest("/ui", ui),
272 None => root,
273 };
274
275 root.layer(from_fn_with_state(scope_state, scope_layer))
276 .layer(cors_layer(&self.venture.cors_origins))
277 }
278}
279
280#[derive(Clone)]
281struct HealthState {
282 venture: Arc<Venture>,
283 modules: Vec<Arc<dyn Module>>,
284 harness_build: Option<String>,
286 mailer_configured: bool,
287 captcha_configured: bool,
288}
289
290async fn health_handler(State(state): State<HealthState>) -> impl IntoResponse {
291 let modules: Vec<serde_json::Value> = state
292 .modules
293 .iter()
294 .map(|module| {
295 json!({
296 "name": module.name(),
297 "version": module.version(),
298 "emits": module.emits(),
299 })
300 })
301 .collect();
302 Json(json!({
303 "venture": state.venture.name,
304 "env": state.venture.env.as_str(),
305 "harness_api": HARNESS_API,
306 "harness_build": state.harness_build,
307 "mailer": if state.mailer_configured { "configured" } else { "not_configured" },
310 "captcha": if state.captcha_configured { "configured" } else { "absent" },
311 "modules": modules,
312 }))
313}
314
315#[derive(Clone)]
316struct ReadyState {
317 db: Option<Arc<dyn Database>>,
318 clock: Arc<dyn Clock>,
319}
320
321async fn ready_handler(State(state): State<ReadyState>) -> impl IntoResponse {
325 let Some(db) = state.db else {
326 return Problem::not_ready("database port is not configured").into_response();
327 };
328 let stmt = Statement::new("SELECT 1");
329 let query = async move { db.query(&stmt).await };
330 match crate::ports::timeout(&*state.clock, query, Duration::from_secs(2)).await {
331 Some(Ok(_rows)) => Json(json!({ "ok": true })).into_response(),
332 Some(Err(err)) => {
333 error!(error = %err, "readiness probe query failed");
334 Problem::not_ready("database query failed").into_response()
335 }
336 None => Problem::not_ready("database did not answer within 2 s").into_response(),
337 }
338}
339
340#[derive(Clone)]
341struct SurfaceState {
342 config: Arc<dyn Config>,
343 source: Arc<dyn SurfaceSource>,
344}
345
346struct MergedSurface {
353 base: Arc<SurfaceVariants>,
354 mounts: Vec<SidecarMount>,
355 dispatcher: Option<Arc<dyn Dispatcher>>,
356}
357
358impl MergedSurface {
359 async fn sidecar_modules(&self) -> Vec<crate::surface::ModuleSurface> {
361 let mut extra = Vec::new();
362 let Some(dispatcher) = &self.dispatcher else {
363 return extra;
364 };
365 for mount in &self.mounts {
366 if !dispatcher.has(&mount.binding) {
367 continue;
368 }
369 let request = axum::http::Request::builder()
370 .method(axum::http::Method::GET)
371 .uri("/__surface")
372 .header(header::ACCEPT, "application/json")
373 .body(bytes::Bytes::new())
374 .expect("static request builds");
375 let answer = match dispatcher.dispatch(&mount.binding, request).await {
376 Ok(response) if response.status().is_success() => response,
377 Ok(response) => {
378 tracing::warn!(module = mount.name, status = %response.status(), "sidecar surface not available");
379 continue;
380 }
381 Err(err) => {
382 tracing::warn!(module = mount.name, error = %err, "sidecar surface fetch failed");
383 continue;
384 }
385 };
386 match serde_json::from_slice::<SurfaceDocument>(answer.body()) {
387 Ok(document) => extra.extend(
388 document
389 .modules
390 .into_iter()
391 .filter(|m| m.name == mount.name)
392 .map(|m| crate::surface::ModuleSurface {
393 name: m.name,
394 version: m.version,
395 surface: m.surface.public(),
396 }),
397 ),
398 Err(err) => {
399 tracing::warn!(module = mount.name, error = %err, "sidecar surface is not a surface document");
400 }
401 }
402 }
403 extra
404 }
405}
406
407#[async_trait::async_trait]
408impl SurfaceSource for MergedSurface {
409 async fn current(&self) -> Arc<SurfaceDocument> {
410 if self.mounts.is_empty() {
411 return Arc::clone(&self.base.document);
412 }
413 let mut document = (*self.base.document).clone();
414 document.modules.extend(self.sidecar_modules().await);
415 Arc::new(document)
416 }
417
418 fn built(&self) -> Arc<SurfaceDocument> {
419 Arc::clone(&self.base.document)
420 }
421
422 fn rendered(&self, admin: bool) -> Option<&RenderedSurface> {
423 Some(if admin {
424 &self.base.full
425 } else {
426 &self.base.public
427 })
428 }
429}
430
431async fn surface_handler(
438 State(state): State<SurfaceState>,
439 headers: HeaderMap,
440) -> impl IntoResponse {
441 let admin = require_admin(&*state.config, &headers).is_ok();
442 let current = state.source.current().await;
446 let built = state.source.built();
447 let prerendered = Arc::ptr_eq(¤t, &built).then(|| state.source.rendered(admin));
448 let fresh;
449 let rendered: &RenderedSurface = if let Some(rendered) = prerendered.flatten() {
450 rendered
451 } else {
452 fresh = if admin {
453 RenderedSurface::render(¤t)
454 } else {
455 RenderedSurface::render(¤t.public())
456 };
457 &fresh
458 };
459 let matches = headers
460 .get(header::IF_NONE_MATCH)
461 .and_then(|value| value.to_str().ok())
462 .is_some_and(|value| {
463 value
464 .split(',')
465 .map(str::trim)
466 .any(|tag| tag == "*" || tag == rendered.etag)
467 });
468 let mut response = if matches {
469 StatusCode::NOT_MODIFIED.into_response()
470 } else {
471 (
472 [(header::CONTENT_TYPE, "application/json")],
473 rendered.json.clone(),
474 )
475 .into_response()
476 };
477 let response_headers = response.headers_mut();
478 response_headers.insert(
479 header::ETAG,
480 header::HeaderValue::from_str(&rendered.etag).expect("hex etag is a valid header"),
481 );
482 response_headers.insert(
483 header::CACHE_CONTROL,
484 header::HeaderValue::from_static("no-cache"),
485 );
486 response_headers.insert(
487 header::VARY,
488 header::HeaderValue::from_static("Authorization"),
489 );
490 response
491}
492
493#[derive(Default)]
496pub struct HarnessBuilder {
497 venture: Option<Venture>,
498 modules: Vec<Arc<dyn Module>>,
499 provides: Vec<Port>,
500 runtime: Option<Arc<dyn Runtime>>,
501 module_templates: Vec<(String, Box<dyn Template>)>,
502 overrides: Vec<(String, Box<dyn Template>)>,
503 ui: Option<Arc<dyn UiMount>>,
504}
505
506impl HarnessBuilder {
507 #[must_use]
508 pub fn venture(mut self, venture: Venture) -> Self {
509 self.venture = Some(venture);
510 self
511 }
512
513 #[must_use]
516 pub fn module(mut self, module: impl Module) -> Self {
517 self.modules.push(Arc::new(module));
518 self
519 }
520
521 #[must_use]
524 pub fn module_arc(mut self, module: Arc<dyn Module>) -> Self {
525 self.modules.push(module);
526 self
527 }
528
529 #[must_use]
533 pub fn runtime(mut self, runtime: impl Runtime) -> Self {
534 let runtime: Arc<dyn Runtime> = Arc::new(runtime);
535 self.provides = runtime.provides();
536 self.runtime = Some(runtime);
537 self
538 }
539
540 #[must_use]
543 pub fn templates(
544 mut self,
545 templates: impl IntoIterator<Item = (String, Box<dyn Template>)>,
546 ) -> Self {
547 self.module_templates.extend(templates);
548 self
549 }
550
551 #[must_use]
554 pub fn ui(mut self, ui: impl UiMount) -> Self {
555 self.ui = Some(Arc::new(ui));
556 self
557 }
558
559 #[must_use]
562 pub fn template(mut self, id: impl Into<String>, template: Box<dyn Template>) -> Self {
563 self.overrides.push((id.into(), template));
564 self
565 }
566
567 pub fn build(self) -> Result<Harness, ConfigError> {
578 let mut errors = ConfigError::default();
579
580 let venture = if let Some(venture) = self.venture {
581 venture.validate(&mut errors);
582 venture
583 } else {
584 errors.push("missing venture: call .venture(Venture::new(..)) before .build()");
585 Venture::new("invalid", "invalid.invalid")
586 };
587
588 let mut names: HashMap<&'static str, usize> = HashMap::new();
589 let mut tables: HashMap<&'static str, &'static str> = HashMap::new();
590
591 for module in &self.modules {
592 if module.harness_api() != HARNESS_API {
593 errors.push(harness_api_mismatch(module.as_ref()));
594 }
595
596 let name = module.name();
597 if name.is_empty() || !is_module_name(name) {
598 errors.push(format!(
599 "module name `{name}` must be kebab-case ([a-z0-9]+ separated by '-')"
600 ));
601 }
602 match names.get(name) {
603 Some(_) => errors.push(format!("duplicate module name `{name}`")),
604 None => {
605 names.insert(name, 1);
606 }
607 }
608
609 for port in module.requires().iter().chain(module.optional()) {
610 if !Port::ALL.contains(port) {
611 errors.push(format!(
612 "module `{name}` declares unknown port {}",
613 port.name()
614 ));
615 }
616 }
617 for port in module.requires() {
618 if module.optional().contains(port) {
619 errors.push(format!(
620 "module `{name}` lists port {} in both requires() and optional()",
621 port.name()
622 ));
623 }
624 }
625
626 module.surface().validate(name, &mut errors);
627
628 for table in module.tables() {
629 match tables.get(table) {
630 Some(owner) => errors.push(format!(
631 "duplicate table `{table}` claimed by modules `{owner}` and `{name}`"
632 )),
633 None => {
634 tables.insert(table, name);
635 }
636 }
637 }
638 }
639
640 let well_known = collect_well_known(&self.modules, &mut errors);
641
642 for module in &self.modules {
643 for port in module.requires() {
644 if !self.provides.contains(port) {
645 errors.push(format!(
646 "module `{}` requires port {} which the runtime does not provide",
647 module.name(),
648 port.name()
649 ));
650 }
651 }
652 warn_undeclared_ports(module.as_ref(), &self.provides);
653 }
654
655 check_template_ids(
656 self.overrides.iter().chain(self.module_templates.iter()),
657 &names,
658 &mut errors,
659 );
660
661 let surface = Arc::new(SurfaceVariants::compose(
662 &venture,
663 &self.modules,
664 self.ui.as_ref(),
665 ));
666 if let Some(ui) = &self.ui {
667 ui.validate(&surface.document, &mut errors);
668 }
669
670 errors.into_result()?;
671
672 let mut registry = TemplateRegistry::new();
673 registry.register_all(self.module_templates);
674 registry.register_all(self.overrides);
675
676 let mut events = EventBus::new();
677 for module in &self.modules {
678 for (name, handler) in module.events() {
679 events = events.on(name, handler);
680 }
681 }
682
683 Ok(Harness {
684 venture: Arc::new(venture),
685 modules: self.modules,
686 templates: Arc::new(registry),
687 events,
688 runtime: self.runtime,
689 well_known,
690 surface,
691 ui: self.ui,
692 })
693 }
694}
695
696fn check_template_ids<'a>(
698 ids: impl Iterator<Item = &'a (String, Box<dyn Template>)>,
699 names: &HashMap<&'static str, usize>,
700 errors: &mut ConfigError,
701) {
702 for (id, _) in ids {
703 let Some(module_name) = id.split('/').next() else {
704 continue;
705 };
706 if !names.contains_key(module_name) {
707 errors.push(format!(
708 "template `{id}` names module `{module_name}` which is not registered"
709 ));
710 }
711 }
712}
713
714fn is_module_name(name: &str) -> bool {
715 name.split('-').all(|part| {
716 !part.is_empty()
717 && part
718 .chars()
719 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
720 })
721}
722
723fn collect_well_known(modules: &[Arc<dyn Module>], errors: &mut ConfigError) -> Option<Router> {
727 let mut providers: Vec<&'static str> = Vec::new();
728 let mut well_known = None;
729 for module in modules {
730 if let Some(router) = module.well_known() {
731 providers.push(module.name());
732 well_known = Some(router);
733 }
734 }
735 if providers.len() > 1 {
736 let listed = providers
737 .iter()
738 .map(|name| format!("`{name}`"))
739 .collect::<Vec<_>>()
740 .join(", ");
741 errors.push(format!(
742 "modules {listed} all provide a well-known router; at most one module may occupy /.well-known"
743 ));
744 }
745 well_known
746}