1use crate::core::keyword_masking::{KeywordEntry, KeywordMaskingConfig};
2use crate::core::{Config, ProxyAuth};
3use crate::server::{app_state::AppState, error::AppError};
4use actix_web::{web, HttpResponse};
5use chrono::{SecondsFormat, Utc};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use tokio::fs;
9
10use crate::agent::llm::AVAILABLE_PROVIDERS;
11
12#[derive(Serialize)]
18struct WorkflowListItem {
19 name: String,
21 filename: String,
23 size: u64,
25 modified_at: Option<String>,
27}
28
29#[derive(Serialize)]
31struct WorkflowGetResponse {
32 name: String,
34 filename: String,
36 content: String,
38 size: u64,
40 modified_at: Option<String>,
42}
43
44fn is_masked_api_key(value: &str) -> bool {
49 let v = value.trim();
50 v.is_empty() || v.contains("***") || v.contains("...") || v == "****...****"
51}
52
53fn redact_config_for_api(mut value: Value, config: &Config) -> Value {
54 if let Some(root) = value.as_object_mut() {
56 root.remove("proxy_auth_encrypted");
57
58 if let Some(providers) = root.get_mut("providers").and_then(|v| v.as_object_mut()) {
59 for (name, provider_cfg) in providers.iter_mut() {
60 let Some(provider_obj) = provider_cfg.as_object_mut() else {
61 continue;
62 };
63
64 provider_obj.remove("api_key_encrypted");
65
66 let configured = match name.as_str() {
67 "openai" => config
68 .providers
69 .openai
70 .as_ref()
71 .map(|c| !c.api_key.trim().is_empty() || c.api_key_encrypted.is_some())
72 .unwrap_or(false),
73 "anthropic" => config
74 .providers
75 .anthropic
76 .as_ref()
77 .map(|c| !c.api_key.trim().is_empty() || c.api_key_encrypted.is_some())
78 .unwrap_or(false),
79 "gemini" => config
80 .providers
81 .gemini
82 .as_ref()
83 .map(|c| !c.api_key.trim().is_empty() || c.api_key_encrypted.is_some())
84 .unwrap_or(false),
85 _ => false,
86 };
87
88 if configured {
89 provider_obj.insert(
90 "api_key".to_string(),
91 Value::String("****...****".to_string()),
92 );
93 } else {
94 provider_obj.remove("api_key");
95 }
96 }
97 }
98
99 if let Some(mcp) = root.get_mut("mcp").and_then(|v| v.as_object_mut()) {
102 if let Some(servers) = mcp.get_mut("servers").and_then(|v| v.as_array_mut()) {
103 for server in servers.iter_mut() {
104 let Some(server_obj) = server.as_object_mut() else {
105 continue;
106 };
107 let server_id = server_obj
108 .get("id")
109 .and_then(|v| v.as_str())
110 .unwrap_or_default()
111 .to_string();
112
113 let Some(transport) = server_obj
114 .get_mut("transport")
115 .and_then(|v| v.as_object_mut())
116 else {
117 continue;
118 };
119
120 let transport_type = transport
121 .get("type")
122 .and_then(|v| v.as_str())
123 .unwrap_or_default();
124
125 match transport_type {
126 "stdio" => {
127 let mut keys: Vec<String> = transport
130 .get("env_encrypted")
131 .and_then(|v| v.as_object())
132 .map(|obj| obj.keys().cloned().collect())
133 .unwrap_or_default();
134
135 if keys.is_empty() {
136 if let Some(cfg_server) =
138 config.mcp.servers.iter().find(|s| s.id == server_id)
139 {
140 if let crate::agent::mcp::TransportConfig::Stdio(stdio) =
141 &cfg_server.transport
142 {
143 keys = stdio.env.keys().cloned().collect();
144 }
145 }
146 }
147
148 transport.remove("env_encrypted");
149 let env_obj = keys
150 .into_iter()
151 .map(|k| (k, Value::String("****...****".to_string())))
152 .collect::<serde_json::Map<String, Value>>();
153 transport.insert("env".to_string(), Value::Object(env_obj));
154 }
155 "sse" => {
156 if let Some(headers) =
157 transport.get_mut("headers").and_then(|v| v.as_array_mut())
158 {
159 for header in headers.iter_mut() {
160 let Some(header_obj) = header.as_object_mut() else {
161 continue;
162 };
163 header_obj.remove("value_encrypted");
164 header_obj.insert(
166 "value".to_string(),
167 Value::String("****...****".to_string()),
168 );
169 }
170 }
171 }
172 _ => {}
173 }
174 }
175 }
176 }
177 }
178
179 value
180}
181
182fn redact_providers_for_api(mut value: Value, config: &Config) -> Value {
183 let Some(obj) = value.as_object_mut() else {
184 return value;
185 };
186
187 for (name, provider_cfg) in obj.iter_mut() {
188 let Some(provider_obj) = provider_cfg.as_object_mut() else {
189 continue;
190 };
191
192 provider_obj.remove("api_key_encrypted");
193
194 let configured = match name.as_str() {
195 "openai" => config
196 .providers
197 .openai
198 .as_ref()
199 .map(|c| !c.api_key.trim().is_empty() || c.api_key_encrypted.is_some())
200 .unwrap_or(false),
201 "anthropic" => config
202 .providers
203 .anthropic
204 .as_ref()
205 .map(|c| !c.api_key.trim().is_empty() || c.api_key_encrypted.is_some())
206 .unwrap_or(false),
207 "gemini" => config
208 .providers
209 .gemini
210 .as_ref()
211 .map(|c| !c.api_key.trim().is_empty() || c.api_key_encrypted.is_some())
212 .unwrap_or(false),
213 _ => false,
214 };
215
216 if configured {
217 provider_obj.insert(
218 "api_key".to_string(),
219 Value::String("****...****".to_string()),
220 );
221 } else {
222 provider_obj.remove("api_key");
223 }
224 }
225
226 value
227}
228
229fn is_safe_workflow_name(name: &str) -> bool {
231 if name.is_empty() || name.len() > 255 {
233 return false;
234 }
235
236 let trimmed = name.trim();
238 if trimmed != name || trimmed.is_empty() {
239 return false;
240 }
241
242 if name.contains('/') || name.contains('\\') || name.contains("..") {
244 return false;
245 }
246
247 if name.chars().any(|c| c.is_control() || c == '\0') {
249 return false;
250 }
251
252 let upper = name.to_uppercase();
254 let stem = upper.split('.').next().unwrap_or(&upper);
255 let reserved = [
256 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
257 "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
258 ];
259 if reserved.contains(&stem) {
260 return false;
261 }
262
263 name.chars()
265 .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == ' ')
266}
267
268pub async fn list_workflows(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
298 let dir = app_state.app_data_dir.join("workflows");
299
300 fs::create_dir_all(&dir).await?;
301
302 let mut entries = fs::read_dir(&dir).await?;
303 let mut workflows: Vec<WorkflowListItem> = Vec::new();
304
305 while let Some(entry) = entries.next_entry().await? {
306 let file_type = entry.file_type().await?;
307 if !file_type.is_file() {
308 continue;
309 }
310
311 let path = entry.path();
312 if path.extension().and_then(|s| s.to_str()) != Some("md") {
313 continue;
314 }
315
316 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
317 continue;
318 };
319
320 let filename = path
321 .file_name()
322 .and_then(|s| s.to_str())
323 .unwrap_or_default()
324 .to_string();
325
326 let metadata = entry.metadata().await?;
327 workflows.push(WorkflowListItem {
328 name: stem.to_string(),
329 filename,
330 size: metadata.len(),
331 modified_at: None,
332 });
333 }
334
335 workflows.sort_by(|a, b| a.name.cmp(&b.name));
336
337 Ok(HttpResponse::Ok().json(workflows))
338}
339
340pub async fn get_workflow(
370 app_state: web::Data<AppState>,
371 workflow_name: web::Path<String>,
372) -> Result<HttpResponse, AppError> {
373 let name = workflow_name.into_inner();
374 if !is_safe_workflow_name(&name) {
375 return Err(AppError::NotFound("Workflow".to_string()));
376 }
377
378 let dir = app_state.app_data_dir.join("workflows");
379 fs::create_dir_all(&dir).await?;
380
381 let filename = format!("{name}.md");
382 let file_path = dir.join(&filename);
383
384 let metadata = match fs::metadata(&file_path).await {
385 Ok(m) => m,
386 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
387 return Err(AppError::NotFound(format!("Workflow '{name}'")))
388 }
389 Err(e) => return Err(AppError::StorageError(e)),
390 };
391
392 let content = fs::read_to_string(&file_path).await?;
393
394 Ok(HttpResponse::Ok().json(WorkflowGetResponse {
395 name,
396 filename,
397 content,
398 size: metadata.len(),
399 modified_at: None,
400 }))
401}
402
403#[derive(Deserialize)]
405pub struct SaveWorkflowRequest {
406 name: String,
408 content: String,
410}
411
412pub async fn save_workflow(
445 app_state: web::Data<AppState>,
446 payload: web::Json<SaveWorkflowRequest>,
447) -> Result<HttpResponse, AppError> {
448 let name = payload.name.trim();
449 if !is_safe_workflow_name(name) {
450 return Err(AppError::BadRequest("Invalid workflow name".to_string()));
451 }
452
453 let dir = app_state.app_data_dir.join("workflows");
454 fs::create_dir_all(&dir).await?;
455
456 let file_path = dir.join(format!("{}.md", name));
457 fs::write(&file_path, &payload.content).await?;
458
459 Ok(HttpResponse::Ok().json(serde_json::json!({
460 "success": true,
461 "path": file_path.to_string_lossy()
462 })))
463}
464
465pub async fn delete_workflow(
491 app_state: web::Data<AppState>,
492 workflow_name: web::Path<String>,
493) -> Result<HttpResponse, AppError> {
494 let name = workflow_name.into_inner();
495 if !is_safe_workflow_name(&name) {
496 return Err(AppError::BadRequest("Invalid workflow name".to_string()));
497 }
498
499 let dir = app_state.app_data_dir.join("workflows");
500 let file_path = dir.join(format!("{}.md", name));
501
502 if !file_path.exists() {
503 return Err(AppError::NotFound(format!("Workflow '{}'", name)));
504 }
505
506 fs::remove_file(&file_path).await?;
507
508 Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true })))
509}
510
511#[derive(Serialize)]
517struct SetupStatus {
518 is_complete: bool,
520 has_proxy_config: bool,
522 has_proxy_env: bool,
524 message: String,
526}
527
528fn has_proxy_config(config: &Value) -> bool {
530 config
531 .get("http_proxy")
532 .and_then(|value| value.as_str())
533 .is_some_and(|value| !value.trim().is_empty())
534 || config
535 .get("https_proxy")
536 .and_then(|value| value.as_str())
537 .is_some_and(|value| !value.trim().is_empty())
538}
539
540fn collect_proxy_environment_flags() -> Vec<&'static str> {
541 ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]
542 .iter()
543 .copied()
544 .filter(|key| {
545 std::env::var(key)
546 .ok()
547 .map(|value| !value.trim().is_empty())
548 .unwrap_or(false)
549 })
550 .collect()
551}
552
553fn is_setup_completed_from_typed(config: &Config) -> bool {
554 config
555 .extra
556 .get("setup")
557 .and_then(|setup| setup.get("completed"))
558 .and_then(|value| value.as_bool())
559 .unwrap_or(false)
560}
561
562async fn persist_app_config(app_state: &AppState) -> Result<(), AppError> {
563 app_state
564 .persist_config()
565 .await
566 .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to save config: {e}")))
567}
568
569fn deep_merge_json(dst: &mut Value, src: Value) {
570 match (dst, src) {
571 (Value::Object(dst_obj), Value::Object(src_obj)) => {
572 for (k, v) in src_obj {
573 deep_merge_json(dst_obj.entry(k).or_insert(Value::Null), v);
574 }
575 }
576 (dst_slot, src_val) => {
577 *dst_slot = src_val;
578 }
579 }
580}
581
582fn should_show_setup(setup_completed: bool, _has_proxy_config: bool, _has_proxy_env: bool) -> bool {
583 !setup_completed
584}
585
586fn setup_status_message(
587 setup_completed: bool,
588 has_proxy_config: bool,
589 proxy_environment_flags: &[&str],
590) -> String {
591 if setup_completed {
592 return "Setup has already been completed in config.json.".to_string();
593 }
594
595 if has_proxy_config {
596 return "Proxy configuration already exists in config.json. Setup is not required."
597 .to_string();
598 }
599
600 if !proxy_environment_flags.is_empty() {
601 return format!(
602 "Detected proxy environment variables: {}. Please confirm proxy settings in setup.",
603 proxy_environment_flags.join(", ")
604 );
605 }
606
607 "No proxy configuration or proxy environment variables detected. Setup is not required."
608 .to_string()
609}
610
611pub async fn get_setup_status(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
634 let config = app_state.config.read().await.clone();
635 let config_value = serde_json::to_value(&config)?;
636 let has_proxy_config = has_proxy_config(&config_value);
637 let proxy_environment_flags = collect_proxy_environment_flags();
638 let has_proxy_env = !proxy_environment_flags.is_empty();
639 let setup_completed = is_setup_completed_from_typed(&config);
640
641 let is_complete = !should_show_setup(setup_completed, has_proxy_config, has_proxy_env);
642 let message = setup_status_message(setup_completed, has_proxy_config, &proxy_environment_flags);
643
644 Ok(HttpResponse::Ok().json(SetupStatus {
645 is_complete,
646 has_proxy_config,
647 has_proxy_env,
648 message,
649 }))
650}
651
652pub async fn mark_setup_complete(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
673 let completed_at = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
674 let config_to_save = {
675 let mut config = app_state.config.write().await;
676 let setup_entry = config
677 .extra
678 .entry("setup".to_string())
679 .or_insert_with(|| serde_json::json!({}));
680 let setup_obj = setup_entry.as_object_mut().ok_or_else(|| {
681 AppError::BadRequest("config.setup must be a JSON object".to_string())
682 })?;
683
684 setup_obj.insert("completed".to_string(), Value::Bool(true));
685 setup_obj.insert("completed_at".to_string(), Value::String(completed_at));
686 setup_obj.insert("version".to_string(), Value::Number(1.into()));
687
688 config.clone()
689 };
690
691 let _ = config_to_save;
692 persist_app_config(app_state.get_ref()).await?;
693
694 Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true })))
695}
696
697pub async fn mark_setup_incomplete(
718 app_state: web::Data<AppState>,
719) -> Result<HttpResponse, AppError> {
720 let reset_at = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
721 let config_to_save = {
722 let mut config = app_state.config.write().await;
723 if let Some(setup_entry) = config.extra.get_mut("setup") {
724 if let Some(setup_obj) = setup_entry.as_object_mut() {
725 setup_obj.insert("completed".to_string(), Value::Bool(false));
726 setup_obj.insert("reset_at".to_string(), Value::String(reset_at));
727 }
728 }
729 config.clone()
730 };
731
732 let _ = config_to_save;
733 persist_app_config(app_state.get_ref()).await?;
734
735 Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true })))
736}
737
738pub async fn get_bamboo_config(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
768 let path = app_state.app_data_dir.join("config.json");
769 if !path.exists() {
770 return Ok(HttpResponse::Ok().json(serde_json::json!({})));
771 }
772
773 let mut config = app_state.config.read().await.clone();
774 config.refresh_proxy_auth_encrypted()?;
775 config.refresh_provider_api_keys_encrypted()?;
776 let value = serde_json::to_value(&config)?;
777 Ok(HttpResponse::Ok().json(redact_config_for_api(value, &config)))
778}
779
780pub async fn set_bamboo_config(
822 app_state: web::Data<AppState>,
823 payload: web::Json<Value>,
824) -> Result<HttpResponse, AppError> {
825 let mut patch = payload.into_inner();
826 let patch_obj = patch
827 .as_object_mut()
828 .ok_or_else(|| AppError::BadRequest("config.json must be a JSON object".to_string()))?;
829
830 patch_obj.remove("proxy_auth");
832 patch_obj.remove("proxy_auth_encrypted");
833 patch_obj.remove("data_dir");
834
835 if let Some(servers) = patch
837 .get_mut("mcp")
838 .and_then(|m| m.get_mut("servers"))
839 .and_then(|v| v.as_array_mut())
840 {
841 for server in servers.iter_mut() {
842 let Some(server_obj) = server.as_object_mut() else {
843 continue;
844 };
845 let Some(transport) = server_obj
846 .get_mut("transport")
847 .and_then(|v| v.as_object_mut())
848 else {
849 continue;
850 };
851
852 match transport.get("type").and_then(|v| v.as_str()) {
853 Some("stdio") => {
854 transport.remove("env_encrypted");
855 }
856 Some("sse") => {
857 if let Some(headers) =
858 transport.get_mut("headers").and_then(|v| v.as_array_mut())
859 {
860 for header in headers.iter_mut() {
861 let Some(header_obj) = header.as_object_mut() else {
862 continue;
863 };
864 header_obj.remove("value_encrypted");
865 }
866 }
867 }
868 _ => {}
869 }
870 }
871 }
872
873 let current = app_state.config.read().await.clone();
874 let mut merged = serde_json::to_value(¤t)?;
875
876 if let Some(patch_providers) = patch.get_mut("providers").and_then(|v| v.as_object_mut()) {
878 for (provider_name, provider_patch) in patch_providers.iter_mut() {
879 let Some(patch_cfg_obj) = provider_patch.as_object_mut() else {
880 continue;
881 };
882
883 patch_cfg_obj.remove("api_key_encrypted");
885
886 let Some(api_key) = patch_cfg_obj.get("api_key").and_then(|v| v.as_str()) else {
887 continue;
888 };
889 if !is_masked_api_key(api_key) {
890 continue;
891 }
892
893 let existing_plain = match provider_name.as_str() {
894 "openai" => current.providers.openai.as_ref().map(|c| c.api_key.clone()),
895 "anthropic" => current
896 .providers
897 .anthropic
898 .as_ref()
899 .map(|c| c.api_key.clone()),
900 "gemini" => current.providers.gemini.as_ref().map(|c| c.api_key.clone()),
901 _ => None,
902 };
903
904 if let Some(existing_plain) = existing_plain {
905 if !existing_plain.trim().is_empty() {
906 patch_cfg_obj.insert("api_key".to_string(), Value::String(existing_plain));
907 } else {
908 patch_cfg_obj.remove("api_key");
909 }
910 } else {
911 patch_cfg_obj.remove("api_key");
912 }
913 }
914 }
915
916 deep_merge_json(&mut merged, patch);
917
918 let mut new_config: Config = serde_json::from_value(merged)?;
919 new_config.data_dir = app_state.app_data_dir.clone();
920 new_config.hydrate_proxy_auth_from_encrypted();
921 new_config.hydrate_provider_api_keys_from_encrypted();
922
923 if let Err(e) = crate::agent::llm::validate_provider_config(&new_config) {
925 return Err(AppError::BadRequest(format!("Invalid configuration: {e}")));
926 }
927
928 {
930 let mut cfg = app_state.config.write().await;
931 *cfg = new_config.clone();
932 }
933 persist_app_config(app_state.get_ref()).await?;
934 app_state.reload_provider().await.map_err(|e| {
935 AppError::InternalError(anyhow::anyhow!(
936 "Failed to reload provider after updating config: {e}"
937 ))
938 })?;
939
940 let mut config_for_response = new_config.clone();
941 config_for_response.refresh_proxy_auth_encrypted()?;
942 config_for_response.refresh_provider_api_keys_encrypted()?;
943 let value = serde_json::to_value(&config_for_response)?;
944 Ok(HttpResponse::Ok().json(redact_config_for_api(value, &config_for_response)))
945}
946
947#[derive(Deserialize)]
949pub struct ProxyAuthPayload {
950 username: Option<String>,
952 password: Option<String>,
954}
955
956pub async fn set_proxy_auth(
990 app_state: web::Data<AppState>,
991 payload: web::Json<ProxyAuthPayload>,
992) -> Result<HttpResponse, AppError> {
993 let username = payload.username.clone().unwrap_or_default();
994 let password = payload.password.clone().unwrap_or_default();
995
996 let auth = if username.trim().is_empty() {
998 None
999 } else {
1000 Some(ProxyAuth { username, password })
1001 };
1002
1003 let config_to_save = {
1004 let mut config = app_state.config.write().await;
1005 config.proxy_auth = auth;
1006 config.refresh_proxy_auth_encrypted()?;
1007 config.clone()
1008 };
1009
1010 let _ = config_to_save;
1011 persist_app_config(app_state.get_ref()).await?;
1012
1013 app_state.reload_provider().await.map_err(|e| {
1015 AppError::InternalError(anyhow::anyhow!(
1016 "Failed to reload provider after updating proxy auth: {e}"
1017 ))
1018 })?;
1019
1020 Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true })))
1021}
1022
1023pub async fn get_proxy_auth_status(
1047 app_state: web::Data<AppState>,
1048) -> Result<HttpResponse, AppError> {
1049 let config = app_state.config.read().await;
1050 if let Some(auth) = config.proxy_auth.as_ref() {
1051 return Ok(HttpResponse::Ok().json(serde_json::json!({
1052 "configured": true,
1053 "username": auth.username,
1054 })));
1055 }
1056
1057 Ok(HttpResponse::Ok().json(serde_json::json!({
1058 "configured": false,
1059 "username": serde_json::Value::Null
1060 })))
1061}
1062
1063pub async fn reset_bamboo_config(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
1087 let path = app_state.app_data_dir.join("config.json");
1088 match fs::try_exists(&path).await {
1090 Ok(true) => {
1091 fs::remove_file(&path)
1092 .await
1093 .map_err(AppError::StorageError)?;
1094 }
1095 Ok(false) => {
1096 }
1098 Err(err) => return Err(AppError::StorageError(err)),
1099 }
1100
1101 let new_config = app_state.reload_config().await;
1103 if let Err(e) = app_state.reload_provider().await {
1104 log::warn!(
1105 "Config reset updated config to provider={}, but provider reload failed: {}",
1106 new_config.provider,
1107 e
1108 );
1109 }
1110 Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true })))
1111}
1112
1113pub async fn get_anthropic_model_mapping(
1114 app_state: web::Data<AppState>,
1115) -> Result<HttpResponse, AppError> {
1116 let config = app_state.config.read().await;
1117 Ok(HttpResponse::Ok().json(config.anthropic_model_mapping.clone()))
1118}
1119
1120pub async fn set_anthropic_model_mapping(
1121 app_state: web::Data<AppState>,
1122 payload: web::Json<crate::core::model_mapping::AnthropicModelMapping>,
1123) -> Result<HttpResponse, AppError> {
1124 let mapping = payload.into_inner();
1125 let config_to_save = {
1126 let mut config = app_state.config.write().await;
1127 config.anthropic_model_mapping = mapping.clone();
1128 config.clone()
1129 };
1130 let _ = config_to_save;
1131 persist_app_config(app_state.get_ref()).await?;
1132 Ok(HttpResponse::Ok().json(mapping))
1133}
1134
1135#[derive(Debug, Serialize, Deserialize)]
1141struct KeywordMaskingResponse {
1142 entries: Vec<KeywordEntry>,
1144}
1145
1146#[derive(Debug, Serialize, Deserialize)]
1148struct ValidationError {
1149 index: usize,
1151 message: String,
1153}
1154
1155pub async fn get_keyword_masking_config(
1181 app_state: web::Data<AppState>,
1182) -> Result<HttpResponse, AppError> {
1183 let config = app_state.config.read().await;
1184 Ok(HttpResponse::Ok().json(KeywordMaskingResponse {
1185 entries: config.keyword_masking.entries.clone(),
1186 }))
1187}
1188
1189pub async fn update_keyword_masking_config(
1230 app_state: web::Data<AppState>,
1231 payload: web::Json<Vec<KeywordEntry>>,
1232) -> Result<HttpResponse, AppError> {
1233 let entries = payload.into_inner();
1234
1235 const MAX_ENTRIES: usize = 100;
1237 const MAX_PATTERN_LENGTH: usize = 500;
1238
1239 if entries.len() > MAX_ENTRIES {
1240 return Err(AppError::BadRequest(format!(
1241 "Too many entries: {} (max {})",
1242 entries.len(),
1243 MAX_ENTRIES
1244 )));
1245 }
1246
1247 for (idx, entry) in entries.iter().enumerate() {
1249 if entry.pattern.len() > MAX_PATTERN_LENGTH {
1250 return Err(AppError::BadRequest(format!(
1251 "Pattern at index {} too long: {} chars (max {})",
1252 idx,
1253 entry.pattern.len(),
1254 MAX_PATTERN_LENGTH
1255 )));
1256 }
1257 }
1258
1259 let config = KeywordMaskingConfig { entries };
1260
1261 if let Err(errors) = config.validate() {
1263 let validation_errors: Vec<ValidationError> = errors
1264 .into_iter()
1265 .map(|(idx, msg)| ValidationError {
1266 index: idx,
1267 message: msg,
1268 })
1269 .collect();
1270 return Err(AppError::BadRequest(format!(
1271 "Validation failed: {:?}",
1272 validation_errors
1273 )));
1274 }
1275
1276 let config_to_save = {
1277 let mut current = app_state.config.write().await;
1278 current.keyword_masking = config.clone();
1279 current.clone()
1280 };
1281 let _ = config_to_save;
1282 persist_app_config(app_state.get_ref()).await?;
1283 app_state.reload_provider().await.map_err(|e| {
1285 AppError::InternalError(anyhow::anyhow!(
1286 "Failed to reload provider after updating keyword masking: {e}"
1287 ))
1288 })?;
1289
1290 Ok(HttpResponse::Ok().json(KeywordMaskingResponse {
1291 entries: config.entries,
1292 }))
1293}
1294
1295pub async fn validate_keyword_entries(
1334 payload: web::Json<Vec<KeywordEntry>>,
1335) -> Result<HttpResponse, AppError> {
1336 let entries = payload.into_inner();
1337 let config = KeywordMaskingConfig { entries };
1338
1339 match config.validate() {
1340 Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({ "valid": true }))),
1341 Err(errors) => {
1342 let validation_errors: Vec<ValidationError> = errors
1343 .into_iter()
1344 .map(|(idx, msg)| ValidationError {
1345 index: idx,
1346 message: msg,
1347 })
1348 .collect();
1349 Ok(HttpResponse::Ok().json(serde_json::json!({
1350 "valid": false,
1351 "errors": validation_errors
1352 })))
1353 }
1354 }
1355}
1356
1357#[derive(Serialize)]
1363struct ProviderConfigResponse {
1364 provider: String,
1366 available_providers: Vec<String>,
1368 providers: Value,
1370}
1371
1372#[derive(Deserialize)]
1374pub struct UpdateProviderRequest {
1375 provider: String,
1377 #[serde(default)]
1379 providers: Value,
1380}
1381
1382pub async fn get_provider_config(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
1412 let mut config = app_state.config.read().await.clone();
1413 let provider = config.provider.clone();
1414 config.refresh_provider_api_keys_encrypted()?;
1415 let providers = serde_json::to_value(&config.providers)?;
1416 let masked_providers = redact_providers_for_api(providers, &config);
1417
1418 let response = ProviderConfigResponse {
1419 provider,
1420 available_providers: AVAILABLE_PROVIDERS.iter().map(|s| s.to_string()).collect(),
1421 providers: masked_providers,
1422 };
1423
1424 Ok(HttpResponse::Ok().json(response))
1425}
1426
1427pub async fn update_provider_config(
1479 app_state: web::Data<AppState>,
1480 payload: web::Json<UpdateProviderRequest>,
1481) -> Result<HttpResponse, AppError> {
1482 let current = app_state.config.read().await.clone();
1483 let mut merged = serde_json::to_value(¤t)?;
1484
1485 let mut patch = serde_json::json!({
1488 "provider": payload.provider,
1489 "providers": payload.providers,
1490 });
1491
1492 if let Some(patch_providers) = patch.get_mut("providers").and_then(|v| v.as_object_mut()) {
1493 for (provider_name, provider_patch) in patch_providers.iter_mut() {
1494 let Some(patch_cfg_obj) = provider_patch.as_object_mut() else {
1495 continue;
1496 };
1497
1498 patch_cfg_obj.remove("api_key_encrypted");
1500
1501 let Some(api_key) = patch_cfg_obj.get("api_key").and_then(|v| v.as_str()) else {
1502 continue;
1503 };
1504 if !is_masked_api_key(api_key) {
1505 continue;
1506 }
1507
1508 let existing_plain = match provider_name.as_str() {
1509 "openai" => current.providers.openai.as_ref().map(|c| c.api_key.clone()),
1510 "anthropic" => current
1511 .providers
1512 .anthropic
1513 .as_ref()
1514 .map(|c| c.api_key.clone()),
1515 "gemini" => current.providers.gemini.as_ref().map(|c| c.api_key.clone()),
1516 _ => None,
1517 };
1518
1519 if let Some(existing_plain) = existing_plain {
1520 if !existing_plain.trim().is_empty() {
1521 patch_cfg_obj.insert("api_key".to_string(), Value::String(existing_plain));
1522 } else {
1523 patch_cfg_obj.remove("api_key");
1524 }
1525 } else {
1526 patch_cfg_obj.remove("api_key");
1527 }
1528 }
1529 }
1530
1531 deep_merge_json(&mut merged, patch);
1532
1533 let mut new_config: Config = serde_json::from_value(merged)?;
1534 new_config.data_dir = app_state.app_data_dir.clone();
1535 new_config.hydrate_proxy_auth_from_encrypted();
1536 new_config.hydrate_provider_api_keys_from_encrypted();
1537
1538 if let Err(e) = crate::agent::llm::validate_provider_config(&new_config) {
1539 return Ok(HttpResponse::BadRequest().json(serde_json::json!({
1540 "success": false,
1541 "error": format!("Invalid configuration: {}", e)
1542 })));
1543 }
1544
1545 {
1546 let mut cfg = app_state.config.write().await;
1547 *cfg = new_config.clone();
1548 }
1549 persist_app_config(app_state.get_ref()).await?;
1550 app_state.reload_provider().await.map_err(|e| {
1551 AppError::InternalError(anyhow::anyhow!(
1552 "Failed to reload provider after updating configuration: {e}"
1553 ))
1554 })?;
1555
1556 Ok(HttpResponse::Ok().json(serde_json::json!({
1557 "success": true,
1558 "provider": new_config.provider
1559 })))
1560}
1561
1562pub async fn fetch_provider_models(
1564 app_state: web::Data<AppState>,
1565 payload: web::Json<serde_json::Value>,
1566) -> Result<HttpResponse, AppError> {
1567 let config = app_state.config.read().await.clone();
1568 let provider_type = payload
1569 .get("provider")
1570 .and_then(|v| v.as_str())
1571 .unwrap_or(config.provider.as_str());
1572
1573 let client = crate::agent::llm::http_client::build_http_client(&config).map_err(|e| {
1575 AppError::InternalError(anyhow::anyhow!("Failed to build HTTP client: {e}"))
1576 })?;
1577
1578 let models =
1579 match provider_type {
1580 "copilot" => {
1581 let provider = app_state.get_provider().await;
1582 provider.list_models().await.map_err(|e| {
1583 let msg = e.to_string();
1584 if msg.contains("proxy") || msg.contains("407") {
1585 AppError::ProxyAuthRequired
1586 } else {
1587 AppError::InternalError(anyhow::anyhow!("Failed to fetch models: {e}"))
1588 }
1589 })?
1590 }
1591 "openai" => {
1592 let openai = config.providers.openai.as_ref().ok_or_else(|| {
1593 AppError::BadRequest("OpenAI configuration required".to_string())
1594 })?;
1595 if openai.api_key.trim().is_empty() {
1596 return Err(AppError::BadRequest("API key not configured".to_string()));
1597 }
1598 fetch_models_from_api(
1599 &client,
1600 "openai",
1601 &openai.api_key,
1602 openai.base_url.as_deref(),
1603 )
1604 .await?
1605 }
1606 "anthropic" => {
1607 let anthropic = config.providers.anthropic.as_ref().ok_or_else(|| {
1608 AppError::BadRequest("Anthropic configuration required".to_string())
1609 })?;
1610 if anthropic.api_key.trim().is_empty() {
1611 return Err(AppError::BadRequest("API key not configured".to_string()));
1612 }
1613 fetch_models_from_api(
1614 &client,
1615 "anthropic",
1616 &anthropic.api_key,
1617 anthropic.base_url.as_deref(),
1618 )
1619 .await?
1620 }
1621 "gemini" => {
1622 let gemini = config.providers.gemini.as_ref().ok_or_else(|| {
1623 AppError::BadRequest("Gemini configuration required".to_string())
1624 })?;
1625 if gemini.api_key.trim().is_empty() {
1626 return Err(AppError::BadRequest("API key not configured".to_string()));
1627 }
1628 fetch_models_from_api(
1629 &client,
1630 "gemini",
1631 &gemini.api_key,
1632 gemini.base_url.as_deref(),
1633 )
1634 .await?
1635 }
1636 other => {
1637 return Err(AppError::BadRequest(format!(
1638 "Unsupported provider: {other}"
1639 )));
1640 }
1641 };
1642
1643 Ok(HttpResponse::Ok().json(serde_json::json!({ "models": models })))
1644}
1645
1646async fn fetch_models_from_api(
1648 client: &reqwest::Client,
1649 provider: &str,
1650 api_key: &str,
1651 base_url: Option<&str>,
1652) -> Result<Vec<String>, AppError> {
1653 let (url, auth_header, use_query_param) = match provider {
1654 "openai" => {
1655 let url = if let Some(base) = base_url {
1656 let base = base.trim_end_matches('/');
1657 format!("{}/models", base)
1658 } else {
1659 "https://api.openai.com/v1/models".to_string()
1660 };
1661 (url, format!("Bearer {}", api_key), false)
1662 }
1663 "anthropic" => {
1664 let url = if let Some(base) = base_url {
1665 let base = base.trim_end_matches('/');
1666 format!("{}/models", base)
1667 } else {
1668 "https://api.anthropic.com/v1/models".to_string()
1669 };
1670 (url, api_key.to_string(), false) }
1672 "gemini" => {
1673 let url = if let Some(base) = base_url {
1674 let base = base.trim_end_matches('/');
1675 format!("{}?key={}", base, api_key)
1676 } else {
1677 format!(
1678 "https://generativelanguage.googleapis.com/v1beta/models?key={}",
1679 api_key
1680 )
1681 };
1682 (url, String::new(), true) }
1684 _ => {
1685 return Err(AppError::BadRequest(format!(
1686 "Unsupported provider: {}",
1687 provider
1688 )));
1689 }
1690 };
1691
1692 log::info!("Fetching models from: {}", url);
1693
1694 let mut request = client.get(&url);
1695
1696 if !use_query_param {
1698 if provider == "anthropic" {
1699 request = request.header("x-api-key", auth_header);
1700 } else {
1701 request = request.header("Authorization", auth_header);
1702 }
1703 }
1704
1705 let response = request
1706 .send()
1707 .await
1708 .map_err(|e| AppError::InternalError(anyhow::anyhow!("Request failed: {}", e)))?;
1709
1710 if !response.status().is_success() {
1711 let status = response.status();
1712 let error_text = response.text().await.unwrap_or_default();
1713 return Err(AppError::InternalError(anyhow::anyhow!(
1714 "API request failed: {} - {}",
1715 status,
1716 error_text
1717 )));
1718 }
1719
1720 let json: Value = response
1721 .json()
1722 .await
1723 .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to parse JSON: {}", e)))?;
1724
1725 let models: Vec<String> = if let Some(data) = json.get("data").and_then(|d| d.as_array()) {
1727 data.iter()
1729 .filter_map(|model| {
1730 model
1731 .get("id")
1732 .and_then(|id| id.as_str())
1733 .map(|s| s.to_string())
1734 })
1735 .collect()
1736 } else if let Some(models_arr) = json.get("models").and_then(|m| m.as_array()) {
1737 models_arr
1739 .iter()
1740 .filter_map(|model| {
1741 if let Some(name) = model.get("name").and_then(|n| n.as_str()) {
1743 Some(name.to_string())
1744 } else if let Some(id) = model.get("id").and_then(|i| i.as_str()) {
1745 Some(id.to_string())
1746 } else {
1747 model.as_str().map(|s| s.to_string())
1748 }
1749 })
1750 .collect()
1751 } else if let Some(arr) = json.as_array() {
1752 arr.iter()
1754 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1755 .collect()
1756 } else {
1757 return Err(AppError::InternalError(anyhow::anyhow!(
1758 "Unexpected response format"
1759 )));
1760 };
1761
1762 log::info!("Fetched {} models", models.len());
1763 Ok(models)
1764}
1765
1766pub async fn reload_provider_config(
1802 app_state: web::Data<AppState>,
1803) -> Result<HttpResponse, AppError> {
1804 let new_config = app_state.reload_config().await;
1806
1807 if let Err(e) = crate::agent::llm::validate_provider_config(&new_config) {
1809 return Ok(HttpResponse::BadRequest().json(serde_json::json!({
1810 "success": false,
1811 "error": e.to_string()
1812 })));
1813 }
1814
1815 if let Err(e) = app_state.reload_provider().await {
1817 return Ok(HttpResponse::InternalServerError().json(serde_json::json!({
1818 "success": false,
1819 "error": format!("Failed to reload provider: {}", e)
1820 })));
1821 }
1822
1823 log::info!("Provider reloaded successfully: {}", new_config.provider);
1824
1825 Ok(HttpResponse::Ok().json(serde_json::json!({
1826 "success": true,
1827 "provider": new_config.provider
1828 })))
1829}
1830
1831pub fn config(cfg: &mut web::ServiceConfig) {
1867 cfg.route("/bamboo/workflows", web::get().to(list_workflows))
1868 .route("/bamboo/workflows/{name}", web::get().to(get_workflow))
1869 .route("/bamboo/workflows", web::post().to(save_workflow))
1870 .route(
1871 "/bamboo/workflows/{name}",
1872 web::delete().to(delete_workflow),
1873 )
1874 .route("/bamboo/setup/status", web::get().to(get_setup_status))
1876 .route(
1877 "/bamboo/setup/complete",
1878 web::post().to(mark_setup_complete),
1879 )
1880 .route(
1881 "/bamboo/setup/incomplete",
1882 web::post().to(mark_setup_incomplete),
1883 )
1884 .route("/bamboo/config", web::get().to(get_bamboo_config))
1886 .route("/bamboo/config", web::post().to(set_bamboo_config))
1887 .route("/bamboo/config/reset", web::post().to(reset_bamboo_config))
1888 .route("/bamboo/proxy-auth", web::post().to(set_proxy_auth))
1890 .route(
1891 "/bamboo/proxy-auth/status",
1892 web::get().to(get_proxy_auth_status),
1893 )
1894 .route(
1896 "/bamboo/keyword-masking",
1897 web::get().to(get_keyword_masking_config),
1898 )
1899 .route(
1900 "/bamboo/keyword-masking",
1901 web::post().to(update_keyword_masking_config),
1902 )
1903 .route(
1904 "/bamboo/keyword-masking/validate",
1905 web::post().to(validate_keyword_entries),
1906 )
1907 .route(
1909 "/bamboo/settings/provider",
1910 web::get().to(get_provider_config),
1911 )
1912 .route(
1913 "/bamboo/settings/provider",
1914 web::post().to(update_provider_config),
1915 )
1916 .route(
1917 "/bamboo/settings/provider/models",
1918 web::post().to(fetch_provider_models),
1919 )
1920 .route(
1921 "/bamboo/settings/reload",
1922 web::post().to(reload_provider_config),
1923 )
1924 .route(
1926 "/bamboo/anthropic-model-mapping",
1927 web::get().to(get_anthropic_model_mapping),
1928 )
1929 .route(
1930 "/bamboo/anthropic-model-mapping",
1931 web::post().to(set_anthropic_model_mapping),
1932 );
1933}