1#![deny(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3mod auth;
12mod error;
13pub mod handlers;
14mod state;
15pub mod types;
16
17use crate::state::OpsState;
18use axum::{middleware, Router};
19use klieo_a2a::task_store::A2aTaskStore;
20use klieo_auth_common::Authenticator;
21use klieo_core::resume::KvResumeBuffer;
22use klieo_runlog::RunLogStore;
23use std::sync::Arc;
24
25#[derive(Debug, thiserror::Error)]
27#[non_exhaustive]
28pub enum OpsBuilderError {
29 #[error("authenticator not configured — call with_authenticator() before build()")]
31 MissingAuthenticator,
32}
33
34pub struct OpsRouterBuilder {
36 authenticator: Option<Arc<dyn Authenticator>>,
37 run_log_store: Option<Arc<dyn RunLogStore>>,
38 task_store: Option<Arc<A2aTaskStore>>,
39 resume_buffer: Option<Arc<KvResumeBuffer>>,
40}
41
42impl OpsRouterBuilder {
43 pub fn new() -> Self {
47 Self {
48 authenticator: None,
49 run_log_store: None,
50 task_store: None,
51 resume_buffer: None,
52 }
53 }
54
55 pub fn with_authenticator(mut self, auth: Arc<dyn Authenticator>) -> Self {
57 self.authenticator = Some(auth);
58 self
59 }
60
61 #[cfg(feature = "dev-auth")]
72 pub fn with_dev_auth(self) -> Self {
73 self.with_authenticator(Arc::new(klieo_auth_common::AllowAnonymous))
74 }
75
76 pub fn with_run_log_store(mut self, store: Arc<dyn RunLogStore>) -> Self {
78 self.run_log_store = Some(store);
79 self
80 }
81
82 pub fn with_task_store(mut self, store: Arc<A2aTaskStore>) -> Self {
84 self.task_store = Some(store);
85 self
86 }
87
88 pub fn with_resume_buffer(mut self, buf: Arc<KvResumeBuffer>) -> Self {
90 self.resume_buffer = Some(buf);
91 self
92 }
93
94 pub fn build(self) -> Result<Router, OpsBuilderError> {
98 let authenticator = self
99 .authenticator
100 .ok_or(OpsBuilderError::MissingAuthenticator)?;
101
102 let state = Arc::new(OpsState {
103 authenticator,
104 run_log_store: self.run_log_store,
105 task_store: self.task_store,
106 resume_buffer: self.resume_buffer,
107 });
108
109 use crate::handlers::{runs, streams, tasks};
110 use axum::routing::get;
111
112 Ok(Router::new()
113 .route("/runs", get(runs::list_runs))
114 .route("/runs/:run_id", get(runs::get_run))
115 .route("/agents", get(runs::list_agents))
116 .route("/tasks", get(tasks::list_tasks))
117 .route("/streams", get(streams::list_streams))
118 .layer(middleware::from_fn_with_state(
119 Arc::clone(&state),
120 crate::auth::require_auth,
121 ))
122 .with_state(state))
123 }
124}
125
126impl Default for OpsRouterBuilder {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn build_without_authenticator_returns_missing_authenticator() {
138 let err = OpsRouterBuilder::new().build().unwrap_err();
139 assert!(matches!(err, OpsBuilderError::MissingAuthenticator));
140 }
141
142 #[cfg(feature = "dev-auth")]
143 #[test]
144 fn with_dev_auth_satisfies_authenticator_requirement() {
145 let result = OpsRouterBuilder::new().with_dev_auth().build();
146 assert!(
147 result.is_ok(),
148 "with_dev_auth must produce a buildable router"
149 );
150 }
151}