1#![deny(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3mod auth;
12mod error;
13pub mod handlers;
14mod pagination;
15mod state;
16pub mod types;
17
18use crate::state::OpsState;
19use axum::{middleware, Router};
20use klieo_a2a::task_store::A2aTaskStore;
21use klieo_auth_common::Authenticator;
22use klieo_core::resume::KvResumeBuffer;
23use klieo_core::KvStore;
24use klieo_runlog::RunLogStore;
25use std::sync::Arc;
26
27#[derive(Debug, thiserror::Error)]
29#[non_exhaustive]
30pub enum OpsBuilderError {
31 #[error("authenticator not configured — call with_authenticator() before build()")]
33 MissingAuthenticator,
34}
35
36pub struct OpsRouterBuilder {
38 authenticator: Option<Arc<dyn Authenticator>>,
39 run_log_store: Option<Arc<dyn RunLogStore>>,
40 task_store: Option<Arc<A2aTaskStore>>,
41 resume_buffer: Option<Arc<KvResumeBuffer>>,
42 kv_store: Option<Arc<dyn KvStore>>,
43}
44
45impl OpsRouterBuilder {
46 pub fn new() -> Self {
50 Self {
51 authenticator: None,
52 run_log_store: None,
53 task_store: None,
54 resume_buffer: None,
55 kv_store: None,
56 }
57 }
58
59 pub fn with_authenticator(mut self, auth: Arc<dyn Authenticator>) -> Self {
61 self.authenticator = Some(auth);
62 self
63 }
64
65 #[cfg(feature = "dev-auth")]
76 pub fn with_dev_auth(self) -> Self {
77 self.with_authenticator(Arc::new(klieo_auth_common::AllowAnonymous))
78 }
79
80 pub fn with_run_log_store(mut self, store: Arc<dyn RunLogStore>) -> Self {
86 self.run_log_store = Some(store);
87 self
88 }
89
90 pub fn with_task_store(mut self, store: Arc<A2aTaskStore>) -> Self {
92 self.task_store = Some(store);
93 self
94 }
95
96 pub fn with_resume_buffer(mut self, buf: Arc<KvResumeBuffer>) -> Self {
98 self.resume_buffer = Some(buf);
99 self
100 }
101
102 pub fn with_kv_store(mut self, kv: Arc<dyn KvStore>) -> Self {
106 self.kv_store = Some(kv);
107 self
108 }
109
110 pub fn build(self) -> Result<Router, OpsBuilderError> {
114 let authenticator = self
115 .authenticator
116 .ok_or(OpsBuilderError::MissingAuthenticator)?;
117
118 let state = Arc::new(OpsState {
119 authenticator,
120 run_log_store: self.run_log_store,
121 task_store: self.task_store,
122 resume_buffer: self.resume_buffer,
123 kv_store: self.kv_store,
124 });
125
126 use crate::handlers::{reconciliation, runs, streams, tasks};
127 use axum::routing::get;
128
129 Ok(Router::new()
130 .route("/runs", get(runs::list_runs))
131 .route("/runs/:run_id", get(runs::get_run))
132 .route(
133 "/runs/:run_id/reconciliation",
134 get(reconciliation::get_reconciliation),
135 )
136 .route("/agents", get(runs::list_agents))
137 .route("/tasks", get(tasks::list_tasks))
138 .route("/streams", get(streams::list_streams))
139 .layer(middleware::from_fn_with_state(
140 Arc::clone(&state),
141 crate::auth::require_auth,
142 ))
143 .with_state(state))
144 }
145}
146
147impl Default for OpsRouterBuilder {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn build_without_authenticator_returns_missing_authenticator() {
159 let err = OpsRouterBuilder::new().build().unwrap_err();
160 assert!(matches!(err, OpsBuilderError::MissingAuthenticator));
161 }
162
163 #[cfg(feature = "dev-auth")]
164 #[test]
165 fn with_dev_auth_satisfies_authenticator_requirement() {
166 let result = OpsRouterBuilder::new().with_dev_auth().build();
167 assert!(
168 result.is_ok(),
169 "with_dev_auth must produce a buildable router"
170 );
171 }
172}